From 5dd7c1236fb8de09dec2cad317aadd5e5f9da33c Mon Sep 17 00:00:00 2001 From: Oleksandr Sanin Date: Tue, 23 Jun 2026 09:12:10 +0000 Subject: [PATCH] fix(kubernetes): scope NetworkPolicyEdgeBuilder connections to same namespace Kubernetes NetworkPolicies are namespace-scoped resources: a policy in namespace A has no effect on pods in namespace B. However, NetworkPolicyEdgeBuilder was connecting a NetworkPolicy to every Pod in the graph regardless of namespace, producing false-positive PASS results in CKV2_K8S_6 for pods that shared no NetworkPolicy in their own namespace. The fix extracts the namespace from each NetworkPolicy and Pod vertex (falling back to "default" when not set, matching the Kubernetes API behaviour) and skips the connection when they differ. Both the empty-podSelector wildcard path and the matchLabels path now respect namespace scoping. Closes #7474 Signed-off-by: Oleksandr Sanin --- .../edge_builders/NetworkPolicyEdgeBuilder.py | 11 ++++- .../network-policy-namespace-scoping.yaml | 45 +++++++++++++++++++ tests/kubernetes/graph/test_local_graph.py | 17 +++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/kubernetes/graph/resources/Keyword/network-policy-namespace-scoping.yaml diff --git a/checkov/kubernetes/graph_builder/graph_components/edge_builders/NetworkPolicyEdgeBuilder.py b/checkov/kubernetes/graph_builder/graph_components/edge_builders/NetworkPolicyEdgeBuilder.py index 8dc63e3c9e..6df5219120 100644 --- a/checkov/kubernetes/graph_builder/graph_components/edge_builders/NetworkPolicyEdgeBuilder.py +++ b/checkov/kubernetes/graph_builder/graph_components/edge_builders/NetworkPolicyEdgeBuilder.py @@ -29,12 +29,19 @@ def find_connections(vertex: KubernetesBlock, vertices: list[KubernetesBlock]) - """ connections: list[int] = [] + network_policy = vertex + network_policy_namespace = (network_policy.attributes.get("metadata") or {}).get("namespace") or "default" + for potential_pod_index, potential_vertex in enumerate(vertices): if potential_vertex.id == vertex.id or potential_vertex.attributes.get("kind") != "Pod": continue - network_policy = vertex pod = potential_vertex + pod_namespace = (pod.attributes.get("metadata") or {}).get("namespace") or "default" + + # NetworkPolicies are namespace-scoped and only apply to pods in the same namespace + if pod_namespace != network_policy_namespace: + continue pod_spec = network_policy.attributes.get("spec", {}) if pod_spec is None: @@ -54,7 +61,7 @@ def find_connections(vertex: KubernetesBlock, vertices: list[KubernetesBlock]) - shared_labels = [k for k in match_labels if k in pod_labels and match_labels[k] == pod_labels[k]] if len(shared_labels) == len(match_labels): connections.append(potential_pod_index) - # the network policy has a podSelector property with no labels and should apply for all pods + # the network policy has a podSelector property with no labels and should apply for all pods in the namespace else: connections.append(potential_pod_index) diff --git a/tests/kubernetes/graph/resources/Keyword/network-policy-namespace-scoping.yaml b/tests/kubernetes/graph/resources/Keyword/network-policy-namespace-scoping.yaml new file mode 100644 index 0000000000..13b215cf0b --- /dev/null +++ b/tests/kubernetes/graph/resources/Keyword/network-policy-namespace-scoping.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-in-namespace-a + namespace: namespace-a + labels: + app: myapp +spec: + containers: + - name: app + image: nginx:1.7.9 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-in-namespace-b + namespace: namespace-b + labels: + app: myapp +spec: + containers: + - name: app + image: nginx:1.7.9 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-in-namespace-a + namespace: namespace-a +spec: + podSelector: {} + policyTypes: + - Ingress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: label-policy-in-namespace-a + namespace: namespace-a +spec: + podSelector: + matchLabels: + app: myapp + policyTypes: + - Ingress diff --git a/tests/kubernetes/graph/test_local_graph.py b/tests/kubernetes/graph/test_local_graph.py index 97883a6619..a58ad7d245 100644 --- a/tests/kubernetes/graph/test_local_graph.py +++ b/tests/kubernetes/graph/test_local_graph.py @@ -147,6 +147,23 @@ def test_LabelSelectorEdgeBuilder_on_templates_with_network_policy(self) -> None self.assertEqual(5, len(local_graph.vertices)) self.assertEqual(4, len(local_graph.edges)) + def test_NetworkPolicyEdgeBuilder_respects_namespace_scoping(self) -> None: + relative_file_path = "resources/Keyword/network-policy-namespace-scoping.yaml" + definitions = {} + file = os.path.realpath(os.path.join(TEST_DIRNAME, relative_file_path)) + (definitions[relative_file_path], definitions_raw) = parse(file) + graph_flags = K8sGraphFlags(create_complex_vertices=True, create_edges=True) + + local_graph = KubernetesLocalGraph(definitions) + local_graph.edge_builders = [NetworkPolicyEdgeBuilder] + local_graph.build_graph(render_variables=False, graph_flags=graph_flags) + # 2 pods + 2 network policies = 4 vertices + self.assertEqual(4, len(local_graph.vertices)) + # Only pod-in-namespace-a should be connected; pod-in-namespace-b is in a different namespace + # allow-all-in-namespace-a -> pod-in-namespace-a (empty podSelector, same namespace) + # label-policy-in-namespace-a -> pod-in-namespace-a (matchLabels match, same namespace) + self.assertEqual(2, len(local_graph.edges)) + def test_extracting_pod_from_container_types(self) -> None: relative_file_path = "resources/statefulstate_nested_resource.yaml" definitions = {}