From 2177af46949a0d1ab620107bc8359f17db14c895 Mon Sep 17 00:00:00 2001 From: Ekansh Grover Date: Mon, 10 Aug 2026 12:18:09 +0000 Subject: [PATCH] fix(kubernetes): prevent CKV2_K8S_5 per-rule secrets bypass in ClusterRole The YAML check previously evaluated rules.resourceNames existence across the entire Role, not per-rule. A ClusterRole with one unrestricted secrets rule plus a scoped rule with resourceNames incorrectly passed. Replace three attribute checks with a single JSONPath that looks for the risky case on the same rule entry: resources includes secrets or wildcard, verbs includes get, watch, list or wildcard, and resourceNames is absent. Check now passes only when no such unscoped rule exists. Add MixedResourceNamesClusterRole fixture and update expected to require failure for mixed-secrets-global binding. Retains existing passing fixtures for scoped resourceNames. Fixes #7616 --- .../checks/graph_checks/ReadAllSecrets.yaml | 27 +++------------ .../MixedResourceNamesClusterRole.yaml | 33 +++++++++++++++++++ .../resources/ReadAllSecrets/expected.yaml | 3 +- 3 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 tests/kubernetes/graph/checks/resources/ReadAllSecrets/Failing/MixedResourceNamesClusterRole.yaml diff --git a/checkov/kubernetes/checks/graph_checks/ReadAllSecrets.yaml b/checkov/kubernetes/checks/graph_checks/ReadAllSecrets.yaml index 9248fa221b..50229af586 100644 --- a/checkov/kubernetes/checks/graph_checks/ReadAllSecrets.yaml +++ b/checkov/kubernetes/checks/graph_checks/ReadAllSecrets.yaml @@ -39,28 +39,11 @@ definition: - Role - or: - cond_type: attribute - attribute: rules.resources - operator: not_intersects - value: - - 'secrets' - - '*' - resource_types: - - ClusterRole - - Role - - cond_type: attribute - attribute: rules.verbs - operator: not_intersects - value: - - 'get' - - 'watch' - - 'list' - - '*' - resource_types: - - ClusterRole - - Role - - cond_type: attribute - attribute: rules.resourceNames - operator: exists + attribute: >- + $.rules[?((@.resources[*] == 'secrets' | @.resources[*] == '*') & + (@.verbs[*] == 'get' | @.verbs[*] == 'watch' | @.verbs[*] == 'list' | + @.verbs[*] == '*') & !@.resourceNames)] + operator: jsonpath_not_exists resource_types: - ClusterRole - Role diff --git a/tests/kubernetes/graph/checks/resources/ReadAllSecrets/Failing/MixedResourceNamesClusterRole.yaml b/tests/kubernetes/graph/checks/resources/ReadAllSecrets/Failing/MixedResourceNamesClusterRole.yaml new file mode 100644 index 0000000000..9fdeebaaea --- /dev/null +++ b/tests/kubernetes/graph/checks/resources/ReadAllSecrets/Failing/MixedResourceNamesClusterRole.yaml @@ -0,0 +1,33 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mixed-secret-reader +rules: +- apiGroups: + - "" + resources: + - "secrets" + verbs: + - "get" + - "watch" + - "list" +- apiGroups: + - "" + resources: + - "secrets" + resourceNames: + - "pull-secret" + verbs: + - "get" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mixed-secrets-global +subjects: +- kind: ServiceAccount + name: sa1 +roleRef: + kind: ClusterRole + name: mixed-secret-reader + apiGroup: rbac.authorization.k8s.io diff --git a/tests/kubernetes/graph/checks/resources/ReadAllSecrets/expected.yaml b/tests/kubernetes/graph/checks/resources/ReadAllSecrets/expected.yaml index 20ae3776ef..852c206f0a 100644 --- a/tests/kubernetes/graph/checks/resources/ReadAllSecrets/expected.yaml +++ b/tests/kubernetes/graph/checks/resources/ReadAllSecrets/expected.yaml @@ -2,4 +2,5 @@ pass: - "ClusterRoleBinding.default.read-pods-global" - "RoleBinding.my-namespace.my-role-binding" fail: - - "ClusterRoleBinding.default.read-secrets-global" \ No newline at end of file + - "ClusterRoleBinding.default.read-secrets-global" + - "ClusterRoleBinding.default.mixed-secrets-global"