From 317fbeccc1583bb517c9c631e8c2ce200d89de37 Mon Sep 17 00:00:00 2001 From: shireenf-ibm Date: Mon, 8 Jan 2024 18:29:02 +0200 Subject: [PATCH] wip - initial exposure analysis - namespaceSelectors --- pkg/netpol/connlist/connlist.go | 13 +++ .../exposureanalysis/exposure_analysis.go | 101 ++++++++++++++++++ pkg/netpol/eval/exposure.go | 41 +++++++ .../eval/internal/k8s/netpol_exposure.go | 56 ++++++++++ .../internal/common/netpol_commands_common.go | 2 + .../namespace_and_deployments.yaml | 32 ++++++ tests/allow-all-test/netpol.yaml | 16 +++ .../namespace_and_deployments.yaml | 32 ++++++ tests/deny-all-test/netpol.yaml | 12 +++ .../namespace_and_deployments.yaml | 54 ++++++++++ .../netpol.yaml | 24 +++++ 11 files changed, 383 insertions(+) create mode 100644 pkg/netpol/connlist/internal/exposureanalysis/exposure_analysis.go create mode 100644 pkg/netpol/eval/exposure.go create mode 100644 pkg/netpol/eval/internal/k8s/netpol_exposure.go create mode 100644 tests/allow-all-test/namespace_and_deployments.yaml create mode 100644 tests/allow-all-test/netpol.yaml create mode 100644 tests/deny-all-test/namespace_and_deployments.yaml create mode 100644 tests/deny-all-test/netpol.yaml create mode 100644 tests/minimal_test_in_ns_with_multiple_ns_selector/namespace_and_deployments.yaml create mode 100644 tests/minimal_test_in_ns_with_multiple_ns_selector/netpol.yaml diff --git a/pkg/netpol/connlist/connlist.go b/pkg/netpol/connlist/connlist.go index 927a5bc9..97c8129f 100644 --- a/pkg/netpol/connlist/connlist.go +++ b/pkg/netpol/connlist/connlist.go @@ -25,6 +25,7 @@ import ( "github.com/np-guard/netpol-analyzer/pkg/logger" "github.com/np-guard/netpol-analyzer/pkg/manifests/fsscanner" "github.com/np-guard/netpol-analyzer/pkg/manifests/parser" + "github.com/np-guard/netpol-analyzer/pkg/netpol/connlist/internal/exposureanalysis" "github.com/np-guard/netpol-analyzer/pkg/netpol/connlist/internal/ingressanalyzer" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" @@ -385,6 +386,18 @@ func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine, ia *ingres } connsRes = peersAllowedConns + ////////////////////// + // exposure analysis + if ca.focusWorkload == "" { + // TODO: get results from this, and add to return values/ write in form of connsRes + err = exposureanalysis.GetPotentialAllowedConnections(pe, peerList) + if err != nil { + // ca.errors = append(ca.errors, newResourceEvaluationError(err)) + return nil, nil, err + } + } + //////////////////// + if excludeIngressAnalysis { return connsRes, peers, nil } diff --git a/pkg/netpol/connlist/internal/exposureanalysis/exposure_analysis.go b/pkg/netpol/connlist/internal/exposureanalysis/exposure_analysis.go new file mode 100644 index 00000000..ab49346a --- /dev/null +++ b/pkg/netpol/connlist/internal/exposureanalysis/exposure_analysis.go @@ -0,0 +1,101 @@ +package exposureanalysis + +import ( + "fmt" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" + "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" +) + +type xgressExposure struct { + protected bool + // entire namespace is exposed + namespacesExposed []string // TODO: add conns which the namespace exposed on (replace with map[string]common.Connection) + + // podsExposed (TODO: to adds pods exposed in next PRs) +} + +type exposureInfo struct { + ingressExposure xgressExposure + egressExposure xgressExposure +} + +// in next PRs - return the potential conns (to append to output results) +func GetPotentialAllowedConnections(pe *eval.PolicyEngine, peers []eval.Peer) error { + res := map[eval.Peer]exposureInfo{} // map from peer to its exposure info + for _, peer := range peers { + if peer.IsPeerIPType() { + continue + } + // get potentially ingress exposed + ingressEx := xgressExposure{} + captured, namespaces, err := pe.GetPeerPotentiallyAllowedConns(peer, true) + if err != nil { + return err + } + if !captured { + ingressEx.protected = false + ingressEx.namespacesExposed = nil + } else { + ingressEx.protected = true + ingressEx.namespacesExposed = namespaces + } + + // egress potentially + egressEx := xgressExposure{} + captured, namespaces, err = pe.GetPeerPotentiallyAllowedConns(peer, false) + if err != nil { + return err + } + // TODO : avoid code dup + if !captured { + egressEx.protected = false + egressEx.namespacesExposed = nil + } else { + egressEx.protected = true + egressEx.namespacesExposed = namespaces + } + + res[peer] = exposureInfo{ingressExposure: ingressEx, egressExposure: egressEx} + } + + printRes(res) + return nil +} + +func printRes(exposureAnalysisRes map[eval.Peer]exposureInfo) { + fmt.Printf("\n EXPOSURE ANALYSIS: \n") + for peer, exposureDetails := range exposureAnalysisRes { + if !exposureDetails.ingressExposure.protected && !exposureDetails.egressExposure.protected { + fmt.Printf("%q : is not protected in the cluster\n", peer.String()) + continue + } + if !exposureDetails.ingressExposure.protected { + fmt.Printf("%q : is not protected on Ingress\n", peer.String()) + } + if !exposureDetails.egressExposure.protected { + fmt.Printf("%q : is not protected on Egress\n", peer.String()) + } + if len(exposureDetails.ingressExposure.namespacesExposed) > 0 { + fmt.Printf("%q : is exposed on Ingress from:\n", peer.String()) + for i := range exposureDetails.ingressExposure.namespacesExposed { + if exposureDetails.ingressExposure.namespacesExposed[i] == common.AllNamespaces { + fmt.Printf("* %s\n", common.AllNamespaces) + } else { + fmt.Printf("* any namespace with selector/s: %q \n", exposureDetails.ingressExposure.namespacesExposed[i]) + } + } + } + if len(exposureDetails.egressExposure.namespacesExposed) > 0 { + fmt.Printf("%q : is exposed on Egress to:\n", peer.String()) + for i := range exposureDetails.egressExposure.namespacesExposed { + if exposureDetails.egressExposure.namespacesExposed[i] == common.AllNamespaces { + fmt.Printf("* %s\n", common.AllNamespaces) + } else { + fmt.Printf("* any namespace with selector/s : %q \n", exposureDetails.egressExposure.namespacesExposed[i]) + } + } + } + } + fmt.Printf("\n") +} diff --git a/pkg/netpol/eval/exposure.go b/pkg/netpol/eval/exposure.go new file mode 100644 index 00000000..300f17f4 --- /dev/null +++ b/pkg/netpol/eval/exposure.go @@ -0,0 +1,41 @@ +package eval + +import ( + netv1 "k8s.io/api/networking/v1" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" +) + +func (pe *PolicyEngine) GetPeerPotentiallyAllowedConns(checkedPeer Peer, isIngress bool) (captured bool, + netpolsExposed []string, err error) { + checkedPodPeer, err := pe.convertWorkloadPeerToPodPeer(checkedPeer) + if err != nil { + return false, nil, err + } + + policyType := netv1.PolicyTypeIngress + if !isIngress { + policyType = netv1.PolicyTypeEgress + } + + netpols, err := pe.getPoliciesSelectingPod(checkedPodPeer.GetPeerPod(), policyType) + if err != nil { + return false, nil, err + } + + if len(netpols) == 0 { + return false, nil, nil + } + for _, policy := range netpols { + netpolsExposed = append(netpolsExposed, getPotentiallyExposedNamespaces(policy, isIngress)...) + } + + return true, netpolsExposed, nil +} + +func getPotentiallyExposedNamespaces(policy *k8s.NetworkPolicy, isIngress bool) []string { + if isIngress { + return policy.GetPotentialExposedNamespacesForIngress() + } + return policy.GetPotentialExposedNamespacesForEgress() +} diff --git a/pkg/netpol/eval/internal/k8s/netpol_exposure.go b/pkg/netpol/eval/internal/k8s/netpol_exposure.go new file mode 100644 index 00000000..eb82a3a3 --- /dev/null +++ b/pkg/netpol/eval/internal/k8s/netpol_exposure.go @@ -0,0 +1,56 @@ +package k8s + +import ( + netv1 "k8s.io/api/networking/v1" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" +) + +// checks rules with only namespaceSelector +func (np *NetworkPolicy) GetPotentialExposedNamespacesForIngress() []string { + // nsToConns := make(map[string]*common.ConnectionSet, 0) + namespacesExposed := make([]string, 0) + for _, rule := range np.Spec.Ingress { + ruleFrom := rule.From + // rulePorts := rule.Ports // TODO: add on what ports (conns) the namespace is exposed + namespacesExposed = append(namespacesExposed, np.getNamespacesSelectedByRule(ruleFrom)...) + } + + return namespacesExposed +} + +func (np *NetworkPolicy) GetPotentialExposedNamespacesForEgress() []string { + namespacesExposed := make([]string, 0) + for _, rule := range np.Spec.Egress { + ruleTo := rule.To + namespacesExposed = append(namespacesExposed, np.getNamespacesSelectedByRule(ruleTo)...) + } + + return namespacesExposed +} + +func (np *NetworkPolicy) getNamespacesSelectedByRule(rulePeers []netv1.NetworkPolicyPeer) []string { + res := make([]string, 0) + if len(rulePeers) == 0 { // allow all ingress + res = append(res, common.AllNamespaces) + return res + } + for i := range rulePeers { // assumes all rules are good (since connlist analysis already returned errors) + if rulePeers[i].IPBlock != nil { + continue + } + if rulePeers[i].PodSelector != nil { + continue + } + // rule contains only namespaceSelector + selector, _ := np.parseNetpolLabelSelector(rulePeers[i].NamespaceSelector) + selectorStr := selector.String() + if selectorStr == "" { + res = append(res, common.AllNamespaces) + } else { + res = append(res, selector.String()) + } + } + + return res +} diff --git a/pkg/netpol/internal/common/netpol_commands_common.go b/pkg/netpol/internal/common/netpol_commands_common.go index 944e0cf6..47fed25a 100644 --- a/pkg/netpol/internal/common/netpol_commands_common.go +++ b/pkg/netpol/internal/common/netpol_commands_common.go @@ -9,6 +9,8 @@ type NetpolError interface { Location() string } +const AllNamespaces = "any namespace" + // Ingress Controller const - the name and namespace of an ingress-controller pod const ( // The actual ingress controller pod is usually unknown and not available in the input resources for the analysis. diff --git a/tests/allow-all-test/namespace_and_deployments.yaml b/tests/allow-all-test/namespace_and_deployments.yaml new file mode 100644 index 00000000..87d2a47b --- /dev/null +++ b/tests/allow-all-test/namespace_and_deployments.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + replicas: 2 + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- diff --git a/tests/allow-all-test/netpol.yaml b/tests/allow-all-test/netpol.yaml new file mode 100644 index 00000000..7a929953 --- /dev/null +++ b/tests/allow-all-test/netpol.yaml @@ -0,0 +1,16 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: allow-hello-world-b-to-a-app + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + policyTypes: + - Ingress + - Egress + ingress: + - {} + egress: + - {} \ No newline at end of file diff --git a/tests/deny-all-test/namespace_and_deployments.yaml b/tests/deny-all-test/namespace_and_deployments.yaml new file mode 100644 index 00000000..87d2a47b --- /dev/null +++ b/tests/deny-all-test/namespace_and_deployments.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + replicas: 2 + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- diff --git a/tests/deny-all-test/netpol.yaml b/tests/deny-all-test/netpol.yaml new file mode 100644 index 00000000..c9890708 --- /dev/null +++ b/tests/deny-all-test/netpol.yaml @@ -0,0 +1,12 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: allow-hello-world-b-to-a-app + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/tests/minimal_test_in_ns_with_multiple_ns_selector/namespace_and_deployments.yaml b/tests/minimal_test_in_ns_with_multiple_ns_selector/namespace_and_deployments.yaml new file mode 100644 index 00000000..56e1bcae --- /dev/null +++ b/tests/minimal_test_in_ns_with_multiple_ns_selector/namespace_and_deployments.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + replicas: 2 + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-b + namespace: hello-world + labels: + app: b-app +spec: + replicas: 2 + selector: + matchLabels: + app: b-app + template: + metadata: + labels: + app: b-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - containerPort: 8050 diff --git a/tests/minimal_test_in_ns_with_multiple_ns_selector/netpol.yaml b/tests/minimal_test_in_ns_with_multiple_ns_selector/netpol.yaml new file mode 100644 index 00000000..8847a00f --- /dev/null +++ b/tests/minimal_test_in_ns_with_multiple_ns_selector/netpol.yaml @@ -0,0 +1,24 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: allow-hello-world-b-to-a-app + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: hello-world + - from: + - namespaceSelector: + matchExpressions: + - key: foo.com/managed-state + operator: In + values: + - managed + ports: + - port: 8050 + protocol: TCP \ No newline at end of file