From 46844111c69744880d3d8008f5789563841461aa Mon Sep 17 00:00:00 2001 From: Tanya Veksler Date: Tue, 10 Dec 2024 09:51:35 +0200 Subject: [PATCH] explainability --- pkg/cli/list.go | 5 + pkg/internal/testutils/testutils.go | 13 + pkg/netpol/connlist/connlist.go | 47 +- pkg/netpol/connlist/conns_formatter.go | 37 +- pkg/netpol/connlist/conns_formatter_csv.go | 9 +- pkg/netpol/connlist/conns_formatter_dot.go | 2 +- pkg/netpol/connlist/conns_formatter_json.go | 5 +- pkg/netpol/connlist/conns_formatter_md.go | 9 +- pkg/netpol/connlist/conns_formatter_txt.go | 61 +- pkg/netpol/connlist/explanation_test.go | 78 ++ pkg/netpol/connlist/exposure_analysis.go | 2 + pkg/netpol/connlist/exposure_analysis_test.go | 35 +- .../ingressanalyzer/ingress_analyzer.go | 22 +- .../ingressanalyzer/ingress_analyzer_test.go | 7 +- .../internal/ingressanalyzer/service_test.go | 2 +- pkg/netpol/eval/check.go | 34 +- pkg/netpol/eval/eval_test.go | 11 +- pkg/netpol/eval/internal/k8s/adminnetpol.go | 48 +- .../internal/k8s/baseline_admin_netpol.go | 12 +- pkg/netpol/eval/internal/k8s/netpol.go | 126 +-- pkg/netpol/eval/internal/k8s/netpol_test.go | 14 +- pkg/netpol/eval/internal/k8s/pod.go | 12 +- .../eval/internal/k8s/policy_connections.go | 54 +- pkg/netpol/eval/resources.go | 9 +- .../internal/common/augmented_intervalset.go | 721 ++++++++++++++++++ pkg/netpol/internal/common/connection.go | 2 +- pkg/netpol/internal/common/connectionset.go | 319 +++++--- pkg/netpol/internal/common/portset.go | 135 ++-- .../anp_banp_blog_demo_2_explain_output.txt | 126 +++ ..._workload_my-monitoring_explain_output.txt | 33 + tests/anp_banp_blog_demo_2/ns.yaml | 35 + tests/anp_banp_blog_demo_2/policies.yaml | 87 +++ tests/anp_banp_blog_demo_2/workloads.yaml | 57 ++ 33 files changed, 1858 insertions(+), 311 deletions(-) create mode 100644 pkg/netpol/connlist/explanation_test.go create mode 100644 pkg/netpol/internal/common/augmented_intervalset.go create mode 100644 test_outputs/connlist/anp_banp_blog_demo_2_explain_output.txt create mode 100644 test_outputs/connlist/anp_banp_blog_demo_focus_workload_my-monitoring_explain_output.txt create mode 100644 tests/anp_banp_blog_demo_2/ns.yaml create mode 100644 tests/anp_banp_blog_demo_2/policies.yaml create mode 100644 tests/anp_banp_blog_demo_2/workloads.yaml diff --git a/pkg/cli/list.go b/pkg/cli/list.go index 37a11e61..3d52a69f 100644 --- a/pkg/cli/list.go +++ b/pkg/cli/list.go @@ -22,6 +22,7 @@ import ( var ( focusWorkload string exposureAnalysis bool + explain bool output string // output format outFile string // output file ) @@ -85,6 +86,9 @@ func getConnlistOptions(l *logger.DefaultLogger) []connlist.ConnlistAnalyzerOpti if exposureAnalysis { res = append(res, connlist.WithExposureAnalysis()) } + if explain { + res = append(res, connlist.WithExplanation()) + } return res } @@ -130,6 +134,7 @@ defined`, c.Flags().StringVarP(&focusWorkload, "focusworkload", "", "", "Focus connections of specified workload in the output ( or )") c.Flags().BoolVarP(&exposureAnalysis, "exposure", "", false, "Enhance the analysis of permitted connectivity with exposure analysis") + c.Flags().BoolVarP(&explain, "explain", "", false, "Enhance the analysis of permitted connectivity with explainability information") // output format - default txt // output format - default txt supportedFormats := strings.Join(connlist.ValidFormats, ",") diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index 8e1f05c9..89a71237 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -26,6 +26,7 @@ var update = flag.Bool("update", false, "write or override golden files") const ( connlistExpectedOutputFilePartialName = "connlist_output." + explainExpectedOutputFilePartialName = "explain_output." exposureExpectedOutputFilePartialName = "exposure_output." underscore = "_" dotSign = "." @@ -56,6 +57,18 @@ func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string, exposureF return testName, expectedOutputFileName } +// ExplainTestNameByTestArgs returns explain test name and test's expected output file from some tests args +func ExplainTestNameByTestArgs(dirName, focusWorkload string) (testName, expectedOutputFileName string) { + namePrefix := dirName + if focusWorkload != "" { + namePrefix += focusWlAnnotation + strings.Replace(focusWorkload, "/", underscore, 1) + } + testName = namePrefix + outputPartialName := explainExpectedOutputFilePartialName + expectedOutputFileName = namePrefix + underscore + outputPartialName + output.TextFormat + return testName, expectedOutputFileName +} + // DiffTestNameByTestArgs returns diff test name and test's expected output file from some tests args func DiffTestNameByTestArgs(ref1, ref2, format string) (testName, expectedOutputFileName string) { namePrefix := "diff_between_" + ref2 + "_and_" + ref1 diff --git a/pkg/netpol/connlist/connlist.go b/pkg/netpol/connlist/connlist.go index 5381d971..522665ff 100644 --- a/pkg/netpol/connlist/connlist.go +++ b/pkg/netpol/connlist/connlist.go @@ -47,6 +47,7 @@ type ConnlistAnalyzer struct { focusWorkload string exposureAnalysis bool exposureResult []ExposedPeer + explain bool outputFormat string muteErrsAndWarns bool peersList []Peer // internally used peersList used in dot formatting; in case of focusWorkload option contains only relevant peers @@ -136,6 +137,13 @@ func WithExposureAnalysis() ConnlistAnalyzerOption { } } +// WithExplanation is a functional option which directs ConnlistAnalyzer to return explainability of connectivity +func WithExplanation() ConnlistAnalyzerOption { + return func(c *ConnlistAnalyzer) { + c.explain = true + } +} + // WithOutputFormat is a functional option, allowing user to choose the output format txt/json/dot/csv/md. func WithOutputFormat(outputFormat string) ConnlistAnalyzerOption { return func(p *ConnlistAnalyzer) { @@ -158,6 +166,7 @@ func NewConnlistAnalyzer(options ...ConnlistAnalyzerOption) *ConnlistAnalyzer { stopOnError: false, exposureAnalysis: false, exposureResult: nil, + explain: false, errors: []ConnlistError{}, outputFormat: output.DefaultFormat, } @@ -201,10 +210,10 @@ func (ca *ConnlistAnalyzer) hasFatalError() error { func (ca *ConnlistAnalyzer) getPolicyEngine(objectsList []parser.K8sObject) (*eval.PolicyEngine, error) { // TODO: do we need logger in policyEngine? if !ca.exposureAnalysis { - return eval.NewPolicyEngineWithObjects(objectsList) + return eval.NewPolicyEngineWithObjects(objectsList, ca.explain) } // else build new policy engine with exposure analysis option - pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis) + pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis, ca.explain) err := pe.AddObjectsForExposureAnalysis(objectsList) return pe, err } @@ -225,7 +234,7 @@ func (ca *ConnlistAnalyzer) connsListFromParsedResources(objectsList []parser.K8 // ConnlistFromK8sCluster returns the allowed connections list from k8s cluster resources, and list of all peers names func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clientset) ([]Peer2PeerConnection, []Peer, error) { - pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis) + pe := eval.NewPolicyEngineWithOptions(ca.exposureAnalysis, ca.explain) // get all resources from k8s cluster ctx, cancel := context.WithTimeout(context.Background(), ctxTimeoutSeconds*time.Second) @@ -275,7 +284,7 @@ func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection) ca.errors = append(ca.errors, newResultFormattingError(err)) return "", err } - out, err := connsFormatter.writeOutput(conns, ca.exposureResult, ca.exposureAnalysis) + out, err := connsFormatter.writeOutput(conns, ca.exposureResult, ca.exposureAnalysis, ca.explain) if err != nil { ca.errors = append(ca.errors, newResultFormattingError(err)) return "", err @@ -325,10 +334,11 @@ const ( // connection implements the Peer2PeerConnection interface type connection struct { - src Peer - dst Peer - allConnections bool - protocolsAndPorts map[v1.Protocol][]common.PortRange + src Peer + dst Peer + allConnections bool + commonImplyingRules common.ImplyingRulesType // used for explainability, when allConnections is true + protocolsAndPorts map[v1.Protocol][]common.PortRange } func (c *connection) Src() Peer { @@ -344,13 +354,19 @@ func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]common.PortRange { return c.protocolsAndPorts } +func (c *connection) OnlySystemDefaultRule() bool { + return c.allConnections && len(c.protocolsAndPorts) == 0 && c.commonImplyingRules.OnlySystemDefaultRule() +} + // returns a *common.ConnectionSet from Peer2PeerConnection data func GetConnectionSetFromP2PConnection(c Peer2PeerConnection) *common.ConnectionSet { protocolsToPortSetMap := make(map[v1.Protocol]*common.PortSet, len(c.ProtocolsAndPorts())) for protocol, portRangeArr := range c.ProtocolsAndPorts() { protocolsToPortSetMap[protocol] = common.MakePortSet(false) for _, p := range portRangeArr { - protocolsToPortSetMap[protocol].AddPortRange(p.Start(), p.End()) + augmentedRange := p.(*common.PortRangeData) + // we cannot fill explainability data here, so we pass an empty rule name and an arbitrary direction (isIngress being true) + protocolsToPortSetMap[protocol].AddPortRange(augmentedRange.Start(), augmentedRange.End(), augmentedRange.InSet(), "", true) } } connectionSet := &common.ConnectionSet{AllowAll: c.AllProtocolsAndPorts(), AllowedProtocols: protocolsToPortSetMap} @@ -538,8 +554,8 @@ func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine, pe return nil, nil, err } } - // skip empty connections - if allowedConnections.IsEmpty() { + // skip empty connections when running without explainability + if allowedConnections.IsEmpty() && !ca.explain { continue } p2pConnection, err := ca.getP2PConnOrUpdateExposureConn(pe, allowedConnections, srcPeer, dstPeer, exposureMaps) @@ -636,10 +652,11 @@ func (ca *ConnlistAnalyzer) getP2PConnOrUpdateExposureConn(pe *eval.PolicyEngine // helper function - returns a connection object from the given fields func createConnectionObject(allowedConnections common.Connection, src, dst Peer) *connection { return &connection{ - src: src, - dst: dst, - allConnections: allowedConnections.IsAllConnections(), - protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(), + src: src, + dst: dst, + allConnections: allowedConnections.IsAllConnections(), + commonImplyingRules: allowedConnections.(*common.ConnectionSet).CommonImplyingRules, + protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(true), } } diff --git a/pkg/netpol/connlist/conns_formatter.go b/pkg/netpol/connlist/conns_formatter.go index a7701610..4456aa73 100644 --- a/pkg/netpol/connlist/conns_formatter.go +++ b/pkg/netpol/connlist/conns_formatter.go @@ -37,12 +37,12 @@ type ipMaps struct { // saveConnsWithIPs gets a P2P connection; if the connection includes an IP-Peer as one of its end-points; the conn is saved in the // matching map of the formatText maps -func (i *ipMaps) saveConnsWithIPs(conn Peer2PeerConnection) { +func (i *ipMaps) saveConnsWithIPs(conn Peer2PeerConnection, explain bool) { if conn.Src().IsPeerIPType() { - i.PeerToConnsFromIPs[conn.Dst().String()] = append(i.PeerToConnsFromIPs[conn.Dst().String()], formSingleP2PConn(conn)) + i.PeerToConnsFromIPs[conn.Dst().String()] = append(i.PeerToConnsFromIPs[conn.Dst().String()], formSingleP2PConn(conn, explain)) } if conn.Dst().IsPeerIPType() { - i.peerToConnsToIPs[conn.Src().String()] = append(i.peerToConnsToIPs[conn.Src().String()], formSingleP2PConn(conn)) + i.peerToConnsToIPs[conn.Src().String()] = append(i.peerToConnsToIPs[conn.Src().String()], formSingleP2PConn(conn, explain)) } } @@ -57,14 +57,15 @@ func createIPMaps(initMapsFlag bool) (ipMaps ipMaps) { // connsFormatter implements output formatting in the required output format type connsFormatter interface { - writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) + writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool, explain bool) (string, error) } // singleConnFields represents a single connection object type singleConnFields struct { - Src string `json:"src"` - Dst string `json:"dst"` - ConnString string `json:"conn"` + Src string `json:"src"` + Dst string `json:"dst"` + ConnString string `json:"conn"` + Explanation string `json:"explanation,omitempty"` } // string representation of the singleConnFields struct @@ -72,10 +73,22 @@ func (c singleConnFields) string() string { return fmt.Sprintf("%s => %s : %s", c.Src, c.Dst, c.ConnString) } +func (c singleConnFields) nodePairString() string { + return fmt.Sprintf("%s => %s", c.Src, c.Dst) +} + +func (c singleConnFields) stringWithExplanation() string { + return fmt.Sprintf("CONNECTIONS BETWEEN %s => %s:\n\n%s", c.Src, c.Dst, c.Explanation) +} + // formSingleP2PConn returns a string representation of single connection fields as singleConnFields object -func formSingleP2PConn(conn Peer2PeerConnection) singleConnFields { +func formSingleP2PConn(conn Peer2PeerConnection, explain bool) singleConnFields { connStr := common.ConnStrFromConnProperties(conn.AllProtocolsAndPorts(), conn.ProtocolsAndPorts()) - return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr} + expl := "" + if explain { + expl = common.ExplanationFromConnProperties(conn.AllProtocolsAndPorts(), conn.(*connection).commonImplyingRules, conn.ProtocolsAndPorts()) + } + return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr, Explanation: expl} } // commonly (to be) used for exposure analysis output formatters @@ -181,13 +194,13 @@ func getRepresentativePodString(podLabels v1.LabelSelector, txtOutFlag bool) str // getConnlistAsSortedSingleConnFieldsArray returns a sorted singleConnFields list from Peer2PeerConnection list. // creates ipMaps object if the format requires it (to be used for exposure results later) -func getConnlistAsSortedSingleConnFieldsArray(conns []Peer2PeerConnection, ipMaps ipMaps, saveToIPMaps bool) []singleConnFields { +func getConnlistAsSortedSingleConnFieldsArray(conns []Peer2PeerConnection, ipMaps ipMaps, saveToIPMaps, explain bool) []singleConnFields { connItems := make([]singleConnFields, len(conns)) for i := range conns { if saveToIPMaps { - ipMaps.saveConnsWithIPs(conns[i]) + ipMaps.saveConnsWithIPs(conns[i], explain) } - connItems[i] = formSingleP2PConn(conns[i]) + connItems[i] = formSingleP2PConn(conns[i], explain) } return sortConnFields(connItems, true) } diff --git a/pkg/netpol/connlist/conns_formatter_csv.go b/pkg/netpol/connlist/conns_formatter_csv.go index a8ed59df..acfaf70a 100644 --- a/pkg/netpol/connlist/conns_formatter_csv.go +++ b/pkg/netpol/connlist/conns_formatter_csv.go @@ -18,12 +18,13 @@ type formatCSV struct { // writeOutput returns a CSV string form of connections from list of Peer2PeerConnection objects // and exposure analysis results from list ExposedPeer if exists -func (cs *formatCSV) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) { +func (cs *formatCSV) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) { + // Tanya TODO - handle explain flag // writing csv rows into a buffer buf := new(bytes.Buffer) writer := csv.NewWriter(buf) - err := cs.writeCsvConnlistTable(conns, writer, exposureFlag) + err := cs.writeCsvConnlistTable(conns, writer, exposureFlag, explain) if err != nil { return "", err } @@ -61,14 +62,14 @@ func writeTableRows(conns []singleConnFields, writer *csv.Writer, srcFirst bool) } // writeCsvConnlistTable writes csv table for the Peer2PeerConnection list -func (cs *formatCSV) writeCsvConnlistTable(conns []Peer2PeerConnection, writer *csv.Writer, saveIPConns bool) error { +func (cs *formatCSV) writeCsvConnlistTable(conns []Peer2PeerConnection, writer *csv.Writer, saveIPConns, explain bool) error { err := writeCsvColumnsHeader(writer, true) if err != nil { return err } cs.ipMaps = createIPMaps(saveIPConns) // get an array of sorted conns items ([]singleConnFields), if required also save the relevant conns to ipMaps - sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, cs.ipMaps, saveIPConns) + sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, cs.ipMaps, saveIPConns, explain) return writeTableRows(sortedConnItems, writer, true) } diff --git a/pkg/netpol/connlist/conns_formatter_dot.go b/pkg/netpol/connlist/conns_formatter_dot.go index adcde568..94660398 100644 --- a/pkg/netpol/connlist/conns_formatter_dot.go +++ b/pkg/netpol/connlist/conns_formatter_dot.go @@ -59,7 +59,7 @@ func getPeerLine(peer Peer) (string, bool) { // returns a dot string form of connections from list of Peer2PeerConnection objects // and from exposure-analysis results if exists -func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) { +func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) { // 1. declaration of maps and slices to be used for forming the graph lines nsPeers := make(map[string][]string) // map from namespace to its peers (grouping peers by namespaces) nsRepPeers := make(map[string][]string) // map from representative namespace to its representative peers diff --git a/pkg/netpol/connlist/conns_formatter_json.go b/pkg/netpol/connlist/conns_formatter_json.go index c7b8ecce..09b1d966 100644 --- a/pkg/netpol/connlist/conns_formatter_json.go +++ b/pkg/netpol/connlist/conns_formatter_json.go @@ -29,13 +29,14 @@ type exposureFields struct { // writeOutput returns a json string form of connections from list of Peer2PeerConnection objects // and exposure analysis results from list ExposedPeer if exists -func (j *formatJSON) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) { +func (j *formatJSON) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) { + // Tanya TODO - handle explain flag j.ipMaps = createIPMaps(exposureFlag) // output variables var jsonConns []byte var err error // get an array of sorted connlist items ([]singleConnFields) - sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, j.ipMaps, exposureFlag) + sortedConnItems := getConnlistAsSortedSingleConnFieldsArray(conns, j.ipMaps, exposureFlag, explain) if exposureFlag { // get an array of sorted exposure items ingressExposureItems, egressExposureItems, _ := getExposureConnsAsSortedSingleConnFieldsArray(exposureConns, j.ipMaps) diff --git a/pkg/netpol/connlist/conns_formatter_md.go b/pkg/netpol/connlist/conns_formatter_md.go index 888dc92d..256d76c3 100644 --- a/pkg/netpol/connlist/conns_formatter_md.go +++ b/pkg/netpol/connlist/conns_formatter_md.go @@ -45,9 +45,10 @@ func getMDLine(c singleConnFields, srcFirst bool) string { // writeOutput returns a md string form of connections from list of Peer2PeerConnection objects, // and exposure analysis results from list ExposedPeer if exists -func (md *formatMD) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) { +func (md *formatMD) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) { + // Tanya TODO - handle explain flag // first write connlist lines - allLines := md.writeMdConnlistLines(conns, exposureFlag) + allLines := md.writeMdConnlistLines(conns, exposureFlag, explain) if !exposureFlag { return strings.Join(allLines, newLineChar) + newLineChar, nil } @@ -66,9 +67,9 @@ func writeMdLines(conns []singleConnFields, srcFirst bool) []string { } // writeMdConnlistLines returns md lines from the list of Peer2PeerConnection -func (md *formatMD) writeMdConnlistLines(conns []Peer2PeerConnection, saveIPConns bool) []string { +func (md *formatMD) writeMdConnlistLines(conns []Peer2PeerConnection, saveIPConns, explain bool) []string { md.ipMaps = createIPMaps(saveIPConns) - sortedConns := getConnlistAsSortedSingleConnFieldsArray(conns, md.ipMaps, saveIPConns) + sortedConns := getConnlistAsSortedSingleConnFieldsArray(conns, md.ipMaps, saveIPConns, explain) connlistLines := []string{getMDHeader(true)} // connlist results are formatted: src | dst | conn connlistLines = append(connlistLines, writeMdLines(sortedConns, true)...) return connlistLines diff --git a/pkg/netpol/connlist/conns_formatter_txt.go b/pkg/netpol/connlist/conns_formatter_txt.go index 2168419f..cee00c6f 100644 --- a/pkg/netpol/connlist/conns_formatter_txt.go +++ b/pkg/netpol/connlist/conns_formatter_txt.go @@ -9,7 +9,8 @@ package connlist import ( "fmt" "sort" - "strings" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" ) // formatText: implements the connsFormatter interface for txt output format @@ -19,8 +20,8 @@ type formatText struct { // writeOutput returns a textual string format of connections from list of Peer2PeerConnection objects, // and exposure analysis results if exist -func (t *formatText) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag bool) (string, error) { - res := t.writeConnlistOutput(conns, exposureFlag) +func (t *formatText) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer, exposureFlag, explain bool) (string, error) { + res := t.writeConnlistOutput(conns, exposureFlag, explain) if !exposureFlag { return res, nil } @@ -33,22 +34,62 @@ func (t *formatText) writeOutput(conns []Peer2PeerConnection, exposureConns []Ex } // writeConnlistOutput writes the section of the connlist result of the output -func (t *formatText) writeConnlistOutput(conns []Peer2PeerConnection, saveIPConns bool) string { - connLines := make([]string, len(conns)) +func (t *formatText) writeConnlistOutput(conns []Peer2PeerConnection, saveIPConns, explain bool) string { + connLines := make([]singleConnFields, 0, len(conns)) + systemDefaultConnLines := make([]singleConnFields, 0, len(conns)) t.ipMaps = createIPMaps(saveIPConns) for i := range conns { - connLines[i] = formSingleP2PConn(conns[i]).string() + p2pConn := formSingleP2PConn(conns[i], explain) + if explain { + // when running with explanation, we print system default connections at the end + if conns[i].(*connection).OnlySystemDefaultRule() { + systemDefaultConnLines = append(systemDefaultConnLines, p2pConn) + } else { + connLines = append(connLines, p2pConn) + } + } else { + connLines = append(connLines, p2pConn) + } // if we have exposure analysis results, also check if src/dst is an IP and store the connection if saveIPConns { - t.ipMaps.saveConnsWithIPs(conns[i]) + t.ipMaps.saveConnsWithIPs(conns[i], explain) + } + } + sortConnFields(connLines, true) + if explain { + sortConnFields(systemDefaultConnLines, true) + } + result := "" + if explain { + result = writeExplanationOutput(connLines, systemDefaultConnLines) + } else { + for _, p2pConn := range connLines { + result += p2pConn.string() + newLineChar + } + } + return result +} + +func writeExplanationOutput(connLines, systemDefaultConnLines []singleConnFields) string { + result := "" + for _, p2pConn := range connLines { + result += nodePairSeparationLine + result += p2pConn.stringWithExplanation() + newLineChar + } + if len(systemDefaultConnLines) > 0 { + result += nodePairSeparationLine + systemDefaultPairsHeader + for _, p2pConn := range systemDefaultConnLines { + result += p2pConn.nodePairString() + newLineChar } } - sort.Strings(connLines) - return strings.Join(connLines, newLineChar) + newLineChar + return result } const ( - unprotectedHeader = "\nWorkloads not protected by network policies:\n" + unprotectedHeader = "\nWorkloads not protected by network policies:\n" + separationLine80 = "--------------------------------------------------------------------------------" + nodePairSeparationLine = separationLine80 + separationLine80 + common.NewLine + systemDefaultPairsHeader = "The following nodes are connected due to " + common.SystemDefaultRule + ":\n" ) // writeExposureOutput writes the section of the exposure-analysis result diff --git a/pkg/netpol/connlist/explanation_test.go b/pkg/netpol/connlist/explanation_test.go new file mode 100644 index 00000000..a308e941 --- /dev/null +++ b/pkg/netpol/connlist/explanation_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2023- IBM Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +package connlist + +import ( + "fmt" + "testing" + + "github.com/np-guard/netpol-analyzer/pkg/internal/output" + "github.com/np-guard/netpol-analyzer/pkg/internal/testutils" + + "github.com/stretchr/testify/require" +) + +// file for testing functionality of explainability analysis + +func TestExplainFromDir(t *testing.T) { + t.Parallel() + for _, tt := range explainTests { + tt := tt + t.Run(tt.testDirName, func(t *testing.T) { + t.Parallel() + pTest := prepareExplainTest(tt.testDirName, tt.focusWorkload) + res, _, err := pTest.analyzer.ConnlistFromDirPath(pTest.dirPath) + require.Nil(t, err, pTest.testInfo) + out, err := pTest.analyzer.ConnectionsListToString(res) + require.Nil(t, err, pTest.testInfo) + testutils.CheckActualVsExpectedOutputMatch(t, pTest.expectedOutputFileName, out, + pTest.testInfo, currentPkg) + }) + } +} + +func prepareExplainTest(dirName, focusWorkload string) preparedTest { + res := preparedTest{} + res.testName, res.expectedOutputFileName = testutils.ExplainTestNameByTestArgs(dirName, focusWorkload) + res.testInfo = fmt.Sprintf("test: %q", res.testName) + cAnalyzer := NewConnlistAnalyzer(WithOutputFormat(output.TextFormat), WithFocusWorkload(focusWorkload), WithExplanation()) + res.analyzer = cAnalyzer + res.dirPath = testutils.GetTestDirPath(dirName) + return res +} + +var explainTests = []struct { + testDirName string + focusWorkload string +}{ + // { + // testDirName: "anp_test_10", + // }, + { + testDirName: "anp_banp_blog_demo", + focusWorkload: "my-monitoring", + }, + { + testDirName: "anp_banp_blog_demo_2", + // focusWorkload: "my-monitoring", + }, + // { + // testDirName: "ipblockstest", + // }, + // { + // testDirName: "onlineboutique", + // }, + // { + // testDirName: "anp_banp_blog_demo", + // }, + // { + // testDirName: "acs-security-demos", + // }, + // { + // testDirName: "acs-security-demos", + // focusWorkload: "ingress-controller", + // }, +} diff --git a/pkg/netpol/connlist/exposure_analysis.go b/pkg/netpol/connlist/exposure_analysis.go index e2c76d74..a3491975 100644 --- a/pkg/netpol/connlist/exposure_analysis.go +++ b/pkg/netpol/connlist/exposure_analysis.go @@ -78,6 +78,8 @@ func xgressExposureListToXgressExposureDataList(xgressExp []*xgressExposure) []X res := make([]XgressExposureData, len(xgressExp)) for i := range xgressExp { res[i] = xgressExp[i] + exposure := res[i].(*xgressExposure) + exposure.potentialConn = exposure.potentialConn.GetEquivalentCanonicalConnectionSet() } return res } diff --git a/pkg/netpol/connlist/exposure_analysis_test.go b/pkg/netpol/connlist/exposure_analysis_test.go index ccda3db3..fd46228e 100644 --- a/pkg/netpol/connlist/exposure_analysis_test.go +++ b/pkg/netpol/connlist/exposure_analysis_test.go @@ -54,7 +54,7 @@ func newTCPConnWithPorts(ports []int) *common.ConnectionSet { conn := common.MakeConnectionSet(false) portSet := common.MakePortSet(false) for i := range ports { - portSet.AddPort(intstr.FromInt(ports[i])) + portSet.AddPort(intstr.FromInt(ports[i]), common.InitImplyingRules()) } conn.AddConnection(v1.ProtocolTCP, portSet) return conn @@ -337,16 +337,41 @@ func checkExpectedVsActualData(t *testing.T, testName string, actualExp ExposedP "test: %q, mismatch in is egress protected for peer %q", testName, actualExp.ExposedPeer().String()) require.Equal(t, expectedData.isIngressProtected, actualExp.IsProtectedByIngressNetpols(), "test: %q, mismatch in is ingress protected for peer %q", testName, actualExp.ExposedPeer().String()) - require.Equal(t, expectedData.lenIngressExposedConns, len(actualExp.IngressExposure()), + ingressExposure := actualExp.IngressExposure() + require.Equal(t, expectedData.lenIngressExposedConns, len(ingressExposure), "test: %q, mismatch in length of ingress exposure slice for peer %q", testName, actualExp.ExposedPeer().String()) for i := range expectedData.ingressExp { - require.Contains(t, actualExp.IngressExposure(), expectedData.ingressExp[i], + require.True(t, checkXgressExposureContainment(ingressExposure, expectedData.ingressExp[i]), "test: %q, expected ingress data %v is not contained in actual results", testName, expectedData.ingressExp[i]) } - require.Equal(t, expectedData.lenEgressExposedConns, len(actualExp.EgressExposure()), + egressExposure := actualExp.EgressExposure() + require.Equal(t, expectedData.lenEgressExposedConns, len(egressExposure), "test: %q, mismatch in length of egress exposure slice for peer %q", testName, actualExp.ExposedPeer().String()) for i := range expectedData.egressExp { - require.Contains(t, actualExp.EgressExposure(), expectedData.egressExp[i], + require.True(t, checkXgressExposureContainment(egressExposure, expectedData.egressExp[i]), "test: %q, expected egress data %v is not contained in actual results", testName, expectedData.egressExp[i]) } } + +func checkXgressExposureContainment(actualArray []XgressExposureData, expectedItem *xgressExposure) bool { + for i := range actualArray { + currItem := actualArray[i].(*xgressExposure) + if currItem.IsExposedToEntireCluster() != expectedItem.IsExposedToEntireCluster() { + continue + } + if !currItem.IsExposedToEntireCluster() { + if currItem.namespaceLabels.String() != expectedItem.namespaceLabels.String() { + continue + } + if currItem.podLabels.String() != expectedItem.podLabels.String() { + continue + } + } + conn1 := expectedItem.PotentialConnectivity().(*common.ConnectionSet) + conn2 := currItem.PotentialConnectivity().(*common.ConnectionSet) + if conn1.Equal(conn2) { + return true + } + } + return false +} diff --git a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go index 897f9701..c6c488b2 100644 --- a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go +++ b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package ingressanalyzer import ( + "fmt" "strconv" ocroutev1 "github.com/openshift/api/route/v1" @@ -283,7 +284,7 @@ func (ia *IngressAnalyzer) AllowedIngressConnections() (map[string]*PeerAndIngre func mergeResults(routesMap, ingressMap map[string]*PeerAndIngressConnSet) { for k, v := range routesMap { if _, ok := ingressMap[k]; ok { - ingressMap[k].ConnSet.Union(v.ConnSet) + ingressMap[k].ConnSet.Union(v.ConnSet, false) } else { ingressMap[k] = v } @@ -301,20 +302,20 @@ func (ia *IngressAnalyzer) allowedIngressConnectionsByResourcesType(mapToIterate continue } for objName, svcList := range objSvcMap { - ingressObjTargetPeersAndPorts, err := ia.getIngressObjectTargetedPeersAndPorts(ns, svcList) + ingObjStr := types.NamespacedName{Namespace: ns, Name: objName}.String() + ingressObjTargetPeersAndPorts, err := ia.getIngressObjectTargetedPeersAndPorts(ns, ingObjStr, svcList, ingType) if err != nil { return nil, err } // avoid duplicates in the result, consider the different ports supported for peer, pConn := range ingressObjTargetPeersAndPorts { - ingObjStr := types.NamespacedName{Namespace: ns, Name: objName}.String() if _, ok := res[peer.String()]; !ok { mapLen := 2 ingressObjs := make(map[string][]string, mapLen) ingressObjs[ingType] = []string{ingObjStr} res[peer.String()] = &PeerAndIngressConnSet{Peer: peer, ConnSet: pConn, IngressObjects: ingressObjs} } else { - res[peer.String()].ConnSet.Union(pConn) + res[peer.String()].ConnSet.Union(pConn, false) res[peer.String()].IngressObjects[ingType] = append(res[peer.String()].IngressObjects[ingType], ingObjStr) } } @@ -326,23 +327,24 @@ func (ia *IngressAnalyzer) allowedIngressConnectionsByResourcesType(mapToIterate // getIngressObjectTargetedPeersAndPorts returns map from peers which are targeted by Route/k8s-Ingress objects in their namespace to // the Ingress required connections -func (ia *IngressAnalyzer) getIngressObjectTargetedPeersAndPorts(ns string, - svcList []serviceInfo) (map[eval.Peer]*common.ConnectionSet, error) { +func (ia *IngressAnalyzer) getIngressObjectTargetedPeersAndPorts(ns, ingObjStr string, + svcList []serviceInfo, ingType string) (map[eval.Peer]*common.ConnectionSet, error) { res := make(map[eval.Peer]*common.ConnectionSet) for _, svc := range svcList { peersAndPorts, ok := ia.servicesToPortsAndPeersMap[ns][svc.serviceName] if !ok { ia.logWarning("Ignoring target service " + svc.serviceName + " : service not found") } + ruleName := fmt.Sprintf("[%s] %s//service %s", ingType, ingObjStr, svc.serviceName) for _, peer := range peersAndPorts.peers { - currIngressPeerConn, err := ia.getIngressPeerConnection(peer, peersAndPorts.ports, svc.servicePort) + currIngressPeerConn, err := ia.getIngressPeerConnection(peer, peersAndPorts.ports, svc.servicePort, ruleName) if err != nil { return nil, err } if _, ok := res[peer]; !ok { res[peer] = currIngressPeerConn } else { - res[peer].Union(currIngressPeerConn) + res[peer].Union(currIngressPeerConn, false) } } } @@ -351,7 +353,7 @@ func (ia *IngressAnalyzer) getIngressObjectTargetedPeersAndPorts(ns string, // getIngressPeerConnection returns the ingress connection to a peer based on the required port specified in the ingress objects func (ia *IngressAnalyzer) getIngressPeerConnection(peer eval.Peer, actualServicePorts []corev1.ServicePort, - requiredPort intstr.IntOrString) (*common.ConnectionSet, error) { + requiredPort intstr.IntOrString, ruleName string) (*common.ConnectionSet, error) { peerTCPConn := eval.GetPeerExposedTCPConnections(peer) // get the peer port/s which may be accessed by the service required port // (if the required port is not specified, all service ports are allowed) @@ -374,7 +376,7 @@ func (ia *IngressAnalyzer) getIngressPeerConnection(peer eval.Peer, actualServic if peerTCPConn.Contains(strconv.Itoa(portNum), string(corev1.ProtocolTCP)) { permittedPort := common.MakePortSet(false) - permittedPort.AddPort(intstr.FromInt(portNum)) + permittedPort.AddPort(intstr.FromInt(portNum), common.MakeImplyingRulesWithRule(ruleName, true)) res.AddConnection(corev1.ProtocolTCP, permittedPort) } } diff --git a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go index 307e6f6a..b4dfbc3e 100644 --- a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go +++ b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go @@ -27,7 +27,7 @@ func getIngressAnalyzerFromDirObjects(t *testing.T, testName, dirName string, pr objects, fpErrs := parser.ResourceInfoListToK8sObjectsList(rList, logger.NewDefaultLogger(), false) require.Len(t, fpErrs, processingErrsNum, "test: %q, expected %d processing errors but got %d", testName, processingErrsNum, len(fpErrs)) - pe, err := eval.NewPolicyEngineWithObjects(objects) + pe, err := eval.NewPolicyEngineWithObjects(objects, false) require.Empty(t, err, "test: %q", testName) ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger(), false) require.Empty(t, err, "test: %q", testName) @@ -97,9 +97,10 @@ func checkConnsEquality(t *testing.T, testName string, ingressConns map[string]* "test: %q, mismatch in ingress connections to %q", testName, peerStr) // if all connections is false; check if actual conns are as expected if !expectedIngressToPeer.allConnections { - require.Contains(t, ingressConnsToPeer.ConnSet.ProtocolsAndPortsMap(), v1.Protocol(expectedIngressToPeer.protocol), + require.Contains(t, ingressConnsToPeer.ConnSet.ProtocolsAndPortsMap(false), v1.Protocol(expectedIngressToPeer.protocol), "test: %q, mismatch in ingress connections to peer %q, should contain protocol %q", testName, peerStr, expectedIngressToPeer.protocol) - connPortRange := ingressConnsToPeer.ConnSet.ProtocolsAndPortsMap()[v1.Protocol(expectedIngressToPeer.protocol)] + connSet := ingressConnsToPeer.ConnSet.GetEquivalentCanonicalConnectionSet() + connPortRange := connSet.ProtocolsAndPortsMap(false)[v1.Protocol(expectedIngressToPeer.protocol)] require.Len(t, connPortRange, len(expectedIngressToPeer.ports), "test: %q, mismatch in ingress connections to %q", testName, peerStr) for i := range expectedIngressToPeer.ports { diff --git a/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go b/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go index 7939629e..d73c7341 100644 --- a/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go +++ b/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go @@ -90,7 +90,7 @@ func TestServiceMappingToPods(t *testing.T) { objects, processingErrs := parser.ResourceInfoListToK8sObjectsList(rList, logger.NewDefaultLogger(), false) require.Len(t, processingErrs, 1, "test: %q", tt.name) // no policies require.Len(t, objects, 17, "test: %q", tt.name) // found 6 services and 11 pods - pe, err := eval.NewPolicyEngineWithObjects(objects) + pe, err := eval.NewPolicyEngineWithObjects(objects, false) require.Empty(t, err, "test: %q", tt.name) ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger(), false) require.Empty(t, err, "test: %q", tt.name) diff --git a/pkg/netpol/eval/check.go b/pkg/netpol/eval/check.go index 3964a0f1..f7568ced 100644 --- a/pkg/netpol/eval/check.go +++ b/pkg/netpol/eval/check.go @@ -16,7 +16,6 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/np-guard/models/pkg/netset" - "github.com/np-guard/netpol-analyzer/pkg/internal/netpolerrors" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" @@ -247,7 +246,10 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) var err error // cases where any connection is always allowed if isPodToItself(srcK8sPeer, dstK8sPeer) || isPeerNodeIP(srcK8sPeer, dstK8sPeer) || isPeerNodeIP(dstK8sPeer, srcK8sPeer) { - return common.MakeConnectionSet(true), nil + res = common.MakeConnectionSet(true) + res.AddCommonImplyingRule(common.PodToItselfRule, true) + res.AddCommonImplyingRule(common.PodToItselfRule, false) + return res, nil } // egress: get egress allowed connections between the src and dst by // walking through all k8s egress policies capturing the src; @@ -256,6 +258,7 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) if err != nil { return nil, err } + res.SetExplResult(false) if res.IsEmpty() { return res, nil } @@ -266,6 +269,7 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) if err != nil { return nil, err } + ingressRes.SetExplResult(true) res.Intersection(ingressRes) return res, nil } @@ -276,6 +280,10 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) // admin-network-policies, network-policies and baseline-admin-network-policies; // considering the precedence of each policy func (pe *PolicyEngine) allAllowedXgressConnections(src, dst k8s.Peer, isIngress bool) (allowedConns *common.ConnectionSet, err error) { + // Tanya TODO: think about the implicitly denied protocols/port ranges + // (due to NPs capturing this src/dst, but defining only some of protocols/ports) + // How to update implying rules in this case? + // first get allowed xgress conn between the src and dst from the ANPs // note that: // - anpConns may contain allowed, denied or/and passed connections @@ -287,6 +295,7 @@ func (pe *PolicyEngine) allAllowedXgressConnections(src, dst k8s.Peer, isIngress } // optimization: if all the conns between src and dst were determined by the ANPs : return the allowed conns if anpCaptured && anpConns.DeterminesAllConns() { + anpConns.AllowedConns.Subtract(anpConns.DeniedConns) // update explainabiliy data return anpConns.AllowedConns, nil } // second get the allowed xgress conns between the src and dst from the netpols @@ -386,7 +395,7 @@ func (pe *PolicyEngine) getAllAllowedXgressConnsFromNetpols(src, dst k8s.Peer, i if pe.exposureAnalysisFlag { updatePeerXgressClusterWideExposure(policy, src, dst, isIngress) } - allowedConns.Union(policyAllowedConnectionsPerDirection) + allowedConns.Union(policyAllowedConnectionsPerDirection, true) // collect implying rules from multiple NPs } // putting the result in policiesConns object to be compared with conns allowed by ANP/BANP later policiesConns = k8s.NewPolicyConnections() @@ -406,7 +415,7 @@ func (pe *PolicyEngine) determineAllowedConnsPerDirection(policy *k8s.NetworkPol case policy.IngressPolicyExposure.ClusterWideExposure.AllowAll && src.PeerType() == k8s.PodType: return policy.IngressPolicyExposure.ClusterWideExposure, nil default: - return policy.GetIngressAllowedConns(src, dst) + return policy.GetXgressAllowedConns(src, dst, true) } } // else get egress allowed conns between src and dst @@ -416,7 +425,7 @@ func (pe *PolicyEngine) determineAllowedConnsPerDirection(policy *k8s.NetworkPol case policy.EgressPolicyExposure.ClusterWideExposure.AllowAll && dst.PeerType() == k8s.PodType: return policy.EgressPolicyExposure.ClusterWideExposure, nil default: - return policy.GetEgressAllowedConns(dst) + return policy.GetXgressAllowedConns(src, dst, false) } } @@ -485,7 +494,11 @@ func (pe *PolicyEngine) getAllAllowedXgressConnectionsFromANPs(src, dst k8s.Peer } if policiesConns.IsEmpty() { // conns between src and dst were not captured by the adminNetpols, to be determined by netpols/default conns - return k8s.NewPolicyConnections(), false, nil + policiesConns.ComplementPassConns() + return policiesConns, false, nil + } + if !policiesConns.PassConns.AllowAll { + policiesConns.ComplementPassConns() } return policiesConns, true, nil @@ -502,7 +515,7 @@ func (pe *PolicyEngine) getAllAllowedXgressConnectionsFromANPs(src, dst k8s.Peer func (pe *PolicyEngine) getXgressDefaultConns(src, dst k8s.Peer, isIngress bool) (*k8s.PolicyConnections, error) { res := k8s.NewPolicyConnections() if pe.baselineAdminNetpol == nil { - res.AllowedConns = common.MakeConnectionSet(true) + res.AllowedConns = common.MakeAllConnectionSetWithRule(common.SystemDefaultRule, isIngress) return res, nil } if isIngress { // ingress @@ -530,8 +543,9 @@ func (pe *PolicyEngine) getXgressDefaultConns(src, dst k8s.Peer, isIngress bool) } } } - if res.IsEmpty() { // banp rules didn't capture xgress conn between src and dst, return system-default: allow-all - res.AllowedConns = common.MakeConnectionSet(true) - } + // if banp rules didn't capture xgress conn between src and dst, return system-default: allow-all; + // if banp rule captured xgress conn, only DeniedConns should be impacted by banp rule, + // whenever AllowedConns should anyway be system-default: allow-all + res.AllowedConns = common.MakeAllConnectionSetWithRule(common.SystemDefaultRule, isIngress) return res, nil } diff --git a/pkg/netpol/eval/eval_test.go b/pkg/netpol/eval/eval_test.go index 6a5ccb45..83d9be89 100644 --- a/pkg/netpol/eval/eval_test.go +++ b/pkg/netpol/eval/eval_test.go @@ -1786,7 +1786,7 @@ func TestPolicyEngineWithWorkloads(t *testing.T) { if len(processingErrs) > 0 { t.Fatalf("TestPolicyEngineWithWorkloads errors: %v", processingErrs) } - pe, err := NewPolicyEngineWithObjects(objects) + pe, err := NewPolicyEngineWithObjects(objects, false) if err != nil { t.Fatalf("TestPolicyEngineWithWorkloads error: %v", err) } @@ -1806,6 +1806,9 @@ func pickContainedConn(conn *common.ConnectionSet) (resProtocol, resPort string) return string(v1.ProtocolTCP), defaultPort } for protocol, portSet := range conn.AllowedProtocols { + if portSet.IsEmpty() { // at least in some protocol, portSet will not be empty + continue + } resProtocol = string(protocol) if portSet.IsAll() { resPort = defaultPort @@ -1829,7 +1832,8 @@ func runParsedResourcesEvalTests(t *testing.T, testList []examples.ParsedResourc test := &testList[i] t.Run(test.Name, func(t *testing.T) { t.Parallel() - pe, err := NewPolicyEngineWithObjects(test.GetK8sObjects()) + // TODO - support explain (and then change the 'false' below to 'true') + pe, err := NewPolicyEngineWithObjects(test.GetK8sObjects(), false) require.Nil(t, err, test.TestInfo) for _, evalTest := range test.EvalTests { src := evalTest.Src @@ -1956,7 +1960,8 @@ func TestDirPathEvalResults(t *testing.T) { require.Empty(t, errs, "test: %q", testName) objectsList, processingErrs := parser.ResourceInfoListToK8sObjectsList(rList, logger.NewDefaultLogger(), false) require.Empty(t, processingErrs, "test: %q", testName) - pe, err := NewPolicyEngineWithObjects(objectsList) + // TODO - support explain (and then change the 'false' below to 'true') + pe, err := NewPolicyEngineWithObjects(objectsList, false) require.Nil(t, err, "test: %q", testName) var src, dst string for podStr, podObj := range pe.podsMap { diff --git a/pkg/netpol/eval/internal/k8s/adminnetpol.go b/pkg/netpol/eval/internal/k8s/adminnetpol.go index 96f6a826..721dc4ba 100644 --- a/pkg/netpol/eval/internal/k8s/adminnetpol.go +++ b/pkg/netpol/eval/internal/k8s/adminnetpol.go @@ -119,7 +119,8 @@ func ingressRuleSelectsPeer(rulePeers []apisv1a.AdminNetworkPolicyIngressPeer, s // updateConnsIfEgressRuleSelectsPeer checks if the given dst is selected by given egress rule, // if yes, updates given policyConns with the rule's connections func updateConnsIfEgressRuleSelectsPeer(rulePeers []apisv1a.AdminNetworkPolicyEgressPeer, - rulePorts *[]apisv1a.AdminNetworkPolicyPort, dst Peer, policyConns *PolicyConnections, action string, isBANPrule bool) error { + rulePorts *[]apisv1a.AdminNetworkPolicyPort, ruleName string, dst Peer, policyConns *PolicyConnections, + action string, isBANPrule bool) error { if len(rulePeers) == 0 { return errors.New(netpolerrors.ANPEgressRulePeersErr) } @@ -130,14 +131,15 @@ func updateConnsIfEgressRuleSelectsPeer(rulePeers []apisv1a.AdminNetworkPolicyEg if !peerSelected { return nil } - err = updatePolicyConns(rulePorts, policyConns, dst, action, isBANPrule) + err = updatePolicyConns(rulePorts, ruleName, policyConns, dst, action, isBANPrule, false) return err } // updateConnsIfIngressRuleSelectsPeer checks if the given src is selected by given ingress rule, // if yes, updates given policyConns with the rule's connections func updateConnsIfIngressRuleSelectsPeer(rulePeers []apisv1a.AdminNetworkPolicyIngressPeer, - rulePorts *[]apisv1a.AdminNetworkPolicyPort, src, dst Peer, policyConns *PolicyConnections, action string, isBANPrule bool) error { + rulePorts *[]apisv1a.AdminNetworkPolicyPort, ruleName string, src, dst Peer, policyConns *PolicyConnections, + action string, isBANPrule bool) error { if len(rulePeers) == 0 { return errors.New(netpolerrors.ANPIngressRulePeersErr) } @@ -148,16 +150,16 @@ func updateConnsIfIngressRuleSelectsPeer(rulePeers []apisv1a.AdminNetworkPolicyI if !peerSelected { return nil } - err = updatePolicyConns(rulePorts, policyConns, dst, action, isBANPrule) + err = updatePolicyConns(rulePorts, ruleName, policyConns, dst, action, isBANPrule, true) return err } // updatePolicyConns gets the rule connections from the rule.ports and updates the input policy connections // with the rule's conns considering the action -func updatePolicyConns(rulePorts *[]apisv1a.AdminNetworkPolicyPort, policyConns *PolicyConnections, dst Peer, - action string, isBANPrule bool) error { +func updatePolicyConns(rulePorts *[]apisv1a.AdminNetworkPolicyPort, ruleName string, policyConns *PolicyConnections, dst Peer, + action string, isBANPrule, isIngress bool) error { // get rule connections from rulePorts - ruleConns, err := ruleConnections(rulePorts, dst) + ruleConns, err := ruleConnections(rulePorts, ruleName, dst, isIngress) if err != nil { return err } @@ -167,9 +169,9 @@ func updatePolicyConns(rulePorts *[]apisv1a.AdminNetworkPolicyPort, policyConns } // ruleConnections returns the connectionSet from the current rule.Ports -func ruleConnections(ports *[]apisv1a.AdminNetworkPolicyPort, dst Peer) (*common.ConnectionSet, error) { - if ports == nil { - return common.MakeConnectionSet(true), nil // If Ports is not set then the rule does not filter traffic via port. +func ruleConnections(ports *[]apisv1a.AdminNetworkPolicyPort, ruleName string, dst Peer, isIngress bool) (*common.ConnectionSet, error) { + if ports == nil { // If Ports is not set then the rule does not filter traffic via port. + return common.MakeAllConnectionSetWithRule(ruleName, isIngress), nil } res := common.MakeConnectionSet(false) for _, anpPort := range *ports { @@ -183,7 +185,7 @@ func ruleConnections(ports *[]apisv1a.AdminNetworkPolicyPort, dst Peer) (*common if anpPort.PortNumber.Protocol != "" { protocol = anpPort.PortNumber.Protocol } - portSet.AddPort(intstr.FromInt32(anpPort.PortNumber.Port)) + portSet.AddPort(intstr.FromInt32(anpPort.PortNumber.Port), common.MakeImplyingRulesWithRule(ruleName, isIngress)) case anpPort.NamedPort != nil: podProtocol, podPort := dst.GetPeerPod().ConvertPodNamedPort(*anpPort.NamedPort) if podPort == common.NoPort { // pod does not have this named port in its container @@ -192,7 +194,7 @@ func ruleConnections(ports *[]apisv1a.AdminNetworkPolicyPort, dst Peer) (*common if podProtocol != "" { protocol = v1.Protocol(podProtocol) } - portSet.AddPort(intstr.FromInt32(podPort)) + portSet.AddPort(intstr.FromInt32(podPort), common.MakeImplyingRulesWithRule(ruleName, isIngress)) case anpPort.PortRange != nil: if anpPort.PortRange.Protocol != "" { protocol = anpPort.PortRange.Protocol @@ -200,7 +202,7 @@ func ruleConnections(ports *[]apisv1a.AdminNetworkPolicyPort, dst Peer) (*common if isEmptyPortRange(int64(anpPort.PortRange.Start), int64(anpPort.PortRange.End)) { continue // @todo should raise a warning } - portSet.AddPortRange(int64(anpPort.PortRange.Start), int64(anpPort.PortRange.End)) + portSet.AddPortRange(int64(anpPort.PortRange.Start), int64(anpPort.PortRange.End), true, ruleName, isIngress) } res.AddConnection(protocol, portSet) } @@ -400,13 +402,27 @@ func (anp *AdminNetworkPolicy) anpRuleErr(ruleName, description string) error { return fmt.Errorf("%s %q: %s %q: %s", anpErrTitle, anp.Name, ruleErrTitle, ruleName, description) } +func (anp *AdminNetworkPolicy) fullName() string { + return "[ANP] " + anp.Name +} + +func ruleFullName(policyName, ruleName, action string, isIngress bool) string { + xgress := egressName + if isIngress { + xgress = ingressName + } + return fmt.Sprintf("%s//%s rule %s (%s)", policyName, xgress, ruleName, action) +} + // GetIngressPolicyConns returns the connections from the ingress rules selecting the src in spec of the adminNetworkPolicy func (anp *AdminNetworkPolicy) GetIngressPolicyConns(src, dst Peer) (*PolicyConnections, error) { res := NewPolicyConnections() for _, rule := range anp.Spec.Ingress { // rule is apisv1a.AdminNetworkPolicyIngressRule rulePeers := rule.From rulePorts := rule.Ports - if err := updateConnsIfIngressRuleSelectsPeer(rulePeers, rulePorts, src, dst, res, string(rule.Action), false); err != nil { + if err := updateConnsIfIngressRuleSelectsPeer(rulePeers, rulePorts, + ruleFullName(anp.fullName(), rule.Name, string(rule.Action), true), + src, dst, res, string(rule.Action), false); err != nil { return nil, anp.anpRuleErr(rule.Name, err.Error()) } } @@ -419,7 +435,9 @@ func (anp *AdminNetworkPolicy) GetEgressPolicyConns(dst Peer) (*PolicyConnection for _, rule := range anp.Spec.Egress { // rule is apisv1a.AdminNetworkPolicyEgressRule rulePeers := rule.To rulePorts := rule.Ports - if err := updateConnsIfEgressRuleSelectsPeer(rulePeers, rulePorts, dst, res, string(rule.Action), false); err != nil { + if err := updateConnsIfEgressRuleSelectsPeer(rulePeers, rulePorts, + ruleFullName(anp.fullName(), rule.Name, string(rule.Action), false), + dst, res, string(rule.Action), false); err != nil { return nil, anp.anpRuleErr(rule.Name, err.Error()) } } diff --git a/pkg/netpol/eval/internal/k8s/baseline_admin_netpol.go b/pkg/netpol/eval/internal/k8s/baseline_admin_netpol.go index a4e99ed8..181f79f9 100644 --- a/pkg/netpol/eval/internal/k8s/baseline_admin_netpol.go +++ b/pkg/netpol/eval/internal/k8s/baseline_admin_netpol.go @@ -51,13 +51,19 @@ func banpRuleErr(ruleName, description string) error { return fmt.Errorf("%s%s %q: %s", banpErrTitle, ruleErrTitle, ruleName, description) } +func (banp *BaselineAdminNetworkPolicy) fullName() string { + return "[BANP] " + banp.Name +} + // GetEgressPolicyConns returns the connections from the egress rules selecting the dst in spec of the baselineAdminNetworkPolicy func (banp *BaselineAdminNetworkPolicy) GetEgressPolicyConns(dst Peer) (*PolicyConnections, error) { res := NewPolicyConnections() for _, rule := range banp.Spec.Egress { // rule is apisv1a.BaselineAdminNetworkPolicyEgressRule rulePeers := rule.To rulePorts := rule.Ports - if err := updateConnsIfEgressRuleSelectsPeer(rulePeers, rulePorts, dst, res, string(rule.Action), true); err != nil { + if err := updateConnsIfEgressRuleSelectsPeer(rulePeers, rulePorts, + ruleFullName(banp.fullName(), rule.Name, string(rule.Action), false), + dst, res, string(rule.Action), true); err != nil { return nil, banpRuleErr(rule.Name, err.Error()) } } @@ -70,7 +76,9 @@ func (banp *BaselineAdminNetworkPolicy) GetIngressPolicyConns(src, dst Peer) (*P for _, rule := range banp.Spec.Ingress { // rule is apisv1a.BaselineAdminNetworkPolicyIngressRule rulePeers := rule.From rulePorts := rule.Ports - if err := updateConnsIfIngressRuleSelectsPeer(rulePeers, rulePorts, src, dst, res, string(rule.Action), true); err != nil { + if err := updateConnsIfIngressRuleSelectsPeer(rulePeers, rulePorts, + ruleFullName(banp.fullName(), rule.Name, string(rule.Action), true), + src, dst, res, string(rule.Action), true); err != nil { return nil, banpRuleErr(rule.Name, err.Error()) } } diff --git a/pkg/netpol/eval/internal/k8s/netpol.go b/pkg/netpol/eval/internal/k8s/netpol.go index c2784fcd..ca715730 100644 --- a/pkg/netpol/eval/internal/k8s/netpol.go +++ b/pkg/netpol/eval/internal/k8s/netpol.go @@ -61,8 +61,10 @@ type PolicyExposureWithoutSelectors struct { // if so, also consider concurrent access (or declare not goroutine safe?) const ( - portBase = 10 - portBits = 32 + portBase = 10 + portBits = 32 + egressName = "Egress" + ingressName = "Ingress" ) func getProtocolStr(p *v1.Protocol) string { @@ -116,6 +118,13 @@ func isEmptyPortRange(start, end int64) bool { return start == common.NoPort && end == common.NoPort } +func (np *NetworkPolicy) rulePeersAndPorts(ruleIdx int, isIngress bool) ([]netv1.NetworkPolicyPeer, []netv1.NetworkPolicyPort) { + if isIngress { + return np.Spec.Ingress[ruleIdx].From, np.Spec.Ingress[ruleIdx].Ports + } + return np.Spec.Egress[ruleIdx].To, np.Spec.Egress[ruleIdx].Ports +} + // doesRulePortContain gets protocol and port numbers of a rule and other protocol and port; // returns if other is contained in the rule's port func doesRulePortContain(ruleProtocol, otherProtocol string, ruleStartPort, ruleEndPort, otherPort int64) bool { @@ -131,12 +140,15 @@ func doesRulePortContain(ruleProtocol, otherProtocol string, ruleStartPort, rule return false } -func (np *NetworkPolicy) ruleConnections(rulePorts []netv1.NetworkPolicyPort, dst Peer) (*common.ConnectionSet, error) { +func (np *NetworkPolicy) ruleConnections(rulePorts []netv1.NetworkPolicyPort, dst Peer, + ruleIdx int, isIngress bool) (*common.ConnectionSet, error) { if len(rulePorts) == 0 { - return common.MakeConnectionSet(true), nil // If this field is empty or missing, this rule matches all ports + // If this field is empty or missing, this rule matches all ports // (traffic not restricted by port) + return common.MakeAllConnectionSetWithRule(np.ruleName(ruleIdx, isIngress), isIngress), nil } res := common.MakeConnectionSet(false) + ruleName := np.ruleName(ruleIdx, isIngress) for i := range rulePorts { protocol := v1.ProtocolTCP if rulePorts[i].Protocol != nil { @@ -166,10 +178,10 @@ func (np *NetworkPolicy) ruleConnections(rulePorts []netv1.NetworkPolicyPort, ds // 4- in order to get a connection from any pod to an ip dst (will not get here, as named ports are not defined for ip-blocks) // adding portName string to the portSet - ports.AddPort(intstr.FromString(portName)) + ports.AddPort(intstr.FromString(portName), common.MakeImplyingRulesWithRule(ruleName, isIngress)) } if !isEmptyPortRange(startPort, endPort) { - ports.AddPortRange(startPort, endPort) + ports.AddPortRange(startPort, endPort, true, ruleName, isIngress) } } res.AddConnection(protocol, ports) @@ -355,54 +367,58 @@ func (np *NetworkPolicy) EgressAllowedConn(dst Peer, protocol, port string) (boo return false, nil } -// GetEgressAllowedConns returns the set of allowed connections from any captured pod to the destination peer -func (np *NetworkPolicy) GetEgressAllowedConns(dst Peer) (*common.ConnectionSet, error) { - res := common.MakeConnectionSet(false) - for _, rule := range np.Spec.Egress { - rulePeers := rule.To - rulePorts := rule.Ports - peerSelected, err := np.ruleSelectsPeer(rulePeers, dst) - if err != nil { - return res, err - } - if !peerSelected { - continue - } - ruleConns, err := np.ruleConnections(rulePorts, dst) - if err != nil { - return res, err - } - res.Union(ruleConns) - if res.AllowAll { - return res, nil - } +const ( + NoXgressRulesExpl = "(no %s rules defined)" + CapturedButNotSelectedExpl = "(captured but not selected by any %s rule)" +) + +func (np *NetworkPolicy) nameWithDirectionAndExpl(isIngress bool, expl string) string { + xgress := "Egress" + if isIngress { + xgress = "Ingress" } - return res, nil + return fmt.Sprintf("%s//%s "+expl, np.fullName(), xgress, xgress) } -// GetIngressAllowedConns returns the set of allowed connections to a captured dst pod from the src peer -func (np *NetworkPolicy) GetIngressAllowedConns(src, dst Peer) (*common.ConnectionSet, error) { +// GetXgressAllowedConns returns the set of allowed connections to a captured dst pod from the src peer (for Ingress) +// or from any captured pod to the dst peer (for Egress) +func (np *NetworkPolicy) GetXgressAllowedConns(src, dst Peer, isIngress bool) (*common.ConnectionSet, error) { res := common.MakeConnectionSet(false) - for _, rule := range np.Spec.Ingress { - rulePeers := rule.From - rulePorts := rule.Ports - peerSelected, err := np.ruleSelectsPeer(rulePeers, src) + if (isIngress && len(np.Spec.Ingress) == 0) || (!isIngress && len(np.Spec.Egress) == 0) { + res.AddCommonImplyingRule(np.nameWithDirectionAndExpl(isIngress, NoXgressRulesExpl), isIngress) + return res, nil + } + peerSelectedByAnyRule := false + numOfRules := len(np.Spec.Egress) + if isIngress { + numOfRules = len(np.Spec.Ingress) + } + for idx := 0; idx < numOfRules; idx++ { + rulePeers, rulePorts := np.rulePeersAndPorts(idx, isIngress) + peerToSelect := dst + if isIngress { + peerToSelect = src + } + peerSelected, err := np.ruleSelectsPeer(rulePeers, peerToSelect) if err != nil { return res, err } if !peerSelected { continue } - - ruleConns, err := np.ruleConnections(rulePorts, dst) + peerSelectedByAnyRule = true + ruleConns, err := np.ruleConnections(rulePorts, dst, idx, isIngress) if err != nil { return res, err } - res.Union(ruleConns) + res.Union(ruleConns, false) if res.AllowAll { return res, nil } } + if !peerSelectedByAnyRule { + res.AddCommonImplyingRule(np.nameWithDirectionAndExpl(isIngress, CapturedButNotSelectedExpl), isIngress) + } return res, nil } @@ -510,7 +526,15 @@ func (np *NetworkPolicy) Selects(p *Pod, direction netv1.PolicyType) (bool, erro } func (np *NetworkPolicy) fullName() string { - return types.NamespacedName{Name: np.Name, Namespace: np.Namespace}.String() + return "[NP] " + types.NamespacedName{Name: np.Name, Namespace: np.Namespace}.String() +} + +func (np *NetworkPolicy) ruleName(ruleIdx int, isIngress bool) string { + xgress := egressName + if isIngress { + xgress = ingressName + } + return fmt.Sprintf("%s//%s rule #%d", np.fullName(), xgress, ruleIdx+1) } // ///////////////////////////////////////////////////////////////////////////////////////////// @@ -548,10 +572,10 @@ func (np *NetworkPolicy) GetPolicyRulesSelectorsAndUpdateExposureClusterWideConn // scanIngressRules handles policy's ingress rules (for updating policy's wide conns/ returning specific rules' selectors) func (np *NetworkPolicy) scanIngressRules() ([]SingleRuleSelectors, error) { rulesSelectors := []SingleRuleSelectors{} - for _, rule := range np.Spec.Ingress { + for idx, rule := range np.Spec.Ingress { rulePeers := rule.From rulePorts := rule.Ports - selectors, err := np.getSelectorsAndUpdateExposureClusterWideConns(rulePeers, rulePorts, true) + selectors, err := np.getSelectorsAndUpdateExposureClusterWideConns(rulePeers, rulePorts, idx, true) if err != nil { return nil, err } @@ -563,10 +587,10 @@ func (np *NetworkPolicy) scanIngressRules() ([]SingleRuleSelectors, error) { // scanEgressRules handles policy's egress rules (for updating policy's wide conns/ returning specific rules' selectors) func (np *NetworkPolicy) scanEgressRules() ([]SingleRuleSelectors, error) { rulesSelectors := []SingleRuleSelectors{} - for _, rule := range np.Spec.Egress { + for idx, rule := range np.Spec.Egress { rulePeers := rule.To rulePorts := rule.Ports - selectors, err := np.getSelectorsAndUpdateExposureClusterWideConns(rulePeers, rulePorts, false) + selectors, err := np.getSelectorsAndUpdateExposureClusterWideConns(rulePeers, rulePorts, idx, false) if err != nil { return nil, err } @@ -584,9 +608,9 @@ func (np *NetworkPolicy) scanEgressRules() ([]SingleRuleSelectors, error) { // - if a rule contains at least one defined selector : appends the rule selectors to a selector list which will be returned. // this func assumes rules are legal (rules correctness check occurs later) func (np *NetworkPolicy) getSelectorsAndUpdateExposureClusterWideConns(rules []netv1.NetworkPolicyPeer, rulePorts []netv1.NetworkPolicyPort, - isIngress bool) (rulesSelectors []SingleRuleSelectors, err error) { + ruleIdx int, isIngress bool) (rulesSelectors []SingleRuleSelectors, err error) { if len(rules) == 0 { - err = np.updateNetworkPolicyExposureClusterWideConns(true, true, rulePorts, isIngress) + err = np.updateNetworkPolicyExposureClusterWideConns(true, true, rulePorts, ruleIdx, isIngress) return nil, err } for i := range rules { @@ -601,7 +625,7 @@ func (np *NetworkPolicy) getSelectorsAndUpdateExposureClusterWideConns(rules []n // if podSelector is not nil but namespaceSelector is nil, this is the netpol's namespace if rules[i].NamespaceSelector != nil && rules[i].NamespaceSelector.Size() == 0 && (rules[i].PodSelector == nil || rules[i].PodSelector.Size() == 0) { - err = np.updateNetworkPolicyExposureClusterWideConns(false, true, rulePorts, isIngress) + err = np.updateNetworkPolicyExposureClusterWideConns(false, true, rulePorts, ruleIdx, isIngress) return nil, err } // else selectors' combination specifies workloads by labels (at least one is not nil and not empty) @@ -614,23 +638,23 @@ func (np *NetworkPolicy) getSelectorsAndUpdateExposureClusterWideConns(rules []n // updateNetworkPolicyExposureClusterWideConns updates the cluster-wide exposure connections of the policy func (np *NetworkPolicy) updateNetworkPolicyExposureClusterWideConns(externalExposure, entireCluster bool, - rulePorts []netv1.NetworkPolicyPort, isIngress bool) error { - ruleConns, err := np.ruleConnections(rulePorts, nil) + rulePorts []netv1.NetworkPolicyPort, ruleIdx int, isIngress bool) error { + ruleConns, err := np.ruleConnections(rulePorts, nil, ruleIdx, isIngress) if err != nil { return err } if externalExposure { if isIngress { - np.IngressPolicyExposure.ExternalExposure.Union(ruleConns) + np.IngressPolicyExposure.ExternalExposure.Union(ruleConns, false) } else { - np.EgressPolicyExposure.ExternalExposure.Union(ruleConns) + np.EgressPolicyExposure.ExternalExposure.Union(ruleConns, false) } } if entireCluster { if isIngress { - np.IngressPolicyExposure.ClusterWideExposure.Union(ruleConns) + np.IngressPolicyExposure.ClusterWideExposure.Union(ruleConns, false) } else { - np.EgressPolicyExposure.ClusterWideExposure.Union(ruleConns) + np.EgressPolicyExposure.ClusterWideExposure.Union(ruleConns, false) } } return nil diff --git a/pkg/netpol/eval/internal/k8s/netpol_test.go b/pkg/netpol/eval/internal/k8s/netpol_test.go index e36c0671..333c7822 100644 --- a/pkg/netpol/eval/internal/k8s/netpol_test.go +++ b/pkg/netpol/eval/internal/k8s/netpol_test.go @@ -11,6 +11,7 @@ import ( v1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -93,8 +94,17 @@ func TestNetworkPolicyPortAnalysis(t *testing.T) { Protocol: &UDP, Port: &PortHello, } - n := &NetworkPolicy{} - res, err := n.ruleConnections([]netv1.NetworkPolicyPort{AllowNamedPortOnProtocol}, &dst) + n := &NetworkPolicy{ + &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-namespace", + }, + }, + PolicyExposureWithoutSelectors{}, + PolicyExposureWithoutSelectors{}, + } + res, err := n.ruleConnections([]netv1.NetworkPolicyPort{AllowNamedPortOnProtocol}, &dst, 0, false) expectedConnStr := "UDP 22" if res.String() != expectedConnStr { t.Fatalf("mismatch on ruleConnections result: expected %v, got %v", expectedConnStr, res.String()) diff --git a/pkg/netpol/eval/internal/k8s/pod.go b/pkg/netpol/eval/internal/k8s/pod.go index d84d93e0..f02ea3ac 100644 --- a/pkg/netpol/eval/internal/k8s/pod.go +++ b/pkg/netpol/eval/internal/k8s/pod.go @@ -270,7 +270,7 @@ func (pod *Pod) PodExposedTCPConnections() *common.ConnectionSet { protocol := corev1.ProtocolTCP if cPort.Protocol == "" || protocol == corev1.ProtocolTCP { ports := common.MakePortSet(false) - ports.AddPortRange(int64(cPort.ContainerPort), int64(cPort.ContainerPort)) + ports.AddPortRange(int64(cPort.ContainerPort), int64(cPort.ContainerPort), true, "", true) res.AddConnection(protocol, ports) } } @@ -300,12 +300,12 @@ func (pod *Pod) UpdatePodXgressExposureToEntireClusterData(ruleConns *common.Con // matching port number convertedConns := pod.checkAndConvertNamedPortsInConnection(ruleConns) if convertedConns != nil { - pod.IngressExposureData.ClusterWideConnection.Union(convertedConns) + pod.IngressExposureData.ClusterWideConnection.Union(convertedConns, false) } else { - pod.IngressExposureData.ClusterWideConnection.Union(ruleConns) + pod.IngressExposureData.ClusterWideConnection.Union(ruleConns, false) } } else { - pod.EgressExposureData.ClusterWideConnection.Union(ruleConns) + pod.EgressExposureData.ClusterWideConnection.Union(ruleConns, false) } } @@ -319,10 +319,10 @@ func (pod *Pod) checkAndConvertNamedPortsInConnection(conns *common.ConnectionSe connsCopy := conns.Copy() // copying the connectionSet; in order to replace // the named ports with pod's port numbers if possible for protocol, namedPorts := range connNamedPorts { - for _, namedPort := range namedPorts { + for namedPort, implyingRules := range namedPorts { podProtocol, portNum := pod.ConvertPodNamedPort(namedPort) if podProtocol == string(protocol) && portNum != common.NoPort { // matching port and protocol - connsCopy.ReplaceNamedPortWithMatchingPortNum(protocol, namedPort, portNum) + connsCopy.ReplaceNamedPortWithMatchingPortNum(protocol, namedPort, portNum, implyingRules) } } } diff --git a/pkg/netpol/eval/internal/k8s/policy_connections.go b/pkg/netpol/eval/internal/k8s/policy_connections.go index ca7b74b3..dd4e4c6d 100644 --- a/pkg/netpol/eval/internal/k8s/policy_connections.go +++ b/pkg/netpol/eval/internal/k8s/policy_connections.go @@ -49,18 +49,18 @@ func (pc *PolicyConnections) UpdateWithRuleConns(ruleConns *common.ConnectionSet case string(apisv1a.AdminNetworkPolicyRuleActionAllow): ruleConns.Subtract(pc.DeniedConns) ruleConns.Subtract(pc.PassConns) - pc.AllowedConns.Union(ruleConns) + pc.AllowedConns.Union(ruleConns, false) case string(apisv1a.AdminNetworkPolicyRuleActionDeny): ruleConns.Subtract(pc.AllowedConns) ruleConns.Subtract(pc.PassConns) - pc.DeniedConns.Union(ruleConns) + pc.DeniedConns.Union(ruleConns, false) case string(apisv1a.AdminNetworkPolicyRuleActionPass): if banpRules { return fmt.Errorf(netpolerrors.UnknownRuleActionErr) } ruleConns.Subtract(pc.AllowedConns) ruleConns.Subtract(pc.DeniedConns) - pc.PassConns.Union(ruleConns) + pc.PassConns.Union(ruleConns, false) default: return fmt.Errorf(netpolerrors.UnknownRuleActionErr) } @@ -79,9 +79,16 @@ func (pc *PolicyConnections) CollectANPConns(newAdminPolicyConns *PolicyConnecti newAdminPolicyConns.PassConns.Subtract(pc.DeniedConns) newAdminPolicyConns.PassConns.Subtract(pc.AllowedConns) // add the new conns from current policy to the connections from the policies with higher precedence - pc.DeniedConns.Union(newAdminPolicyConns.DeniedConns) - pc.AllowedConns.Union(newAdminPolicyConns.AllowedConns) - pc.PassConns.Union(newAdminPolicyConns.PassConns) + pc.DeniedConns.Union(newAdminPolicyConns.DeniedConns, false) + pc.AllowedConns.Union(newAdminPolicyConns.AllowedConns, false) + pc.PassConns.Union(newAdminPolicyConns.PassConns, false) +} + +// ComplementPassConns complements pass connections to all connections (by adding the absent conections) +func (pc *PolicyConnections) ComplementPassConns() { + defaultPassConn := NewPolicyConnections() + defaultPassConn.PassConns = common.MakeConnectionSet(true) + pc.CollectANPConns(defaultPassConn) } // CollectAllowedConnsFromNetpols updates allowed conns of current PolicyConnections object with allowed connections from @@ -92,14 +99,21 @@ func (pc *PolicyConnections) CollectANPConns(newAdminPolicyConns *PolicyConnecti // and any connection that is not allowed by the netpols is denied. // 2. pass connections in current PolicyConnections object will be determined by the input PolicyConnections parameter. func (pc *PolicyConnections) CollectAllowedConnsFromNetpols(npConns *PolicyConnections) { + // This intersection with PassConn does not have effect the resulting connectios, + // but it updates implying rules, representing the effect of PassConn as well + // We start from PassConn, and intersect it with npConns.AllowedConns, + // because the order of intersection impacts the order of implying rules. + newConn := pc.PassConns.Copy() + newConn.Intersection(npConns.AllowedConns) // collect implying rules from pc.PassConns and npConns.AllowedConns // subtract the denied conns (which are non-overridden) from input conns - npConns.AllowedConns.Subtract(pc.DeniedConns) + newConn.Subtract(pc.DeniedConns) // PASS conns are determined by npConns // currently, npConns.AllowedConns contains: // 1. traffic that was passed by ANPs (if there are such conns) // 2. traffic that had no match in ANPs // so we can update current allowed conns with them - pc.AllowedConns.Union(npConns.AllowedConns) + // 'false' below: we don't add implying rules from NPs if the connections were defined by ANPs + pc.AllowedConns.Union(newConn, false) // now pc.AllowedConns contains all allowed conns by the ANPs and NPs // the content of pc.Denied and pc.Pass is not relevant anymore; // all the connections that are not allowed by the ANPs and NPs are denied. @@ -116,14 +130,22 @@ func (pc *PolicyConnections) CollectAllowedConnsFromNetpols(npConns *PolicyConne // is allowed by default func (pc *PolicyConnections) CollectConnsFromBANP(banpConns *PolicyConnections) { // allowed and denied conns of current pc are non-overridden - banpConns.DeniedConns.Subtract(pc.AllowedConns) - pc.DeniedConns.Union(banpConns.DeniedConns) - // now Pass conns which are denied by BANP were handled automatically; - // Pass Conns which are allowed or not captured by BANP, will be handled now with all other conns. - // pc.PassConns is not relevant anymore. + + // This Union with PassConn does not have effect on the resulting connectios, + // but it updates implying rules, representing the effect of PassConn as well + // We start from PassConn, and union banpConns.DeniedConns with it, + // because the order of Union impacts the order of implying rules. + newDenied := pc.PassConns.Copy() + newDenied.Intersection(banpConns.DeniedConns) // collect implying rules from pc.PassConns and banpConns.DeniedConns + newDenied.Subtract(pc.AllowedConns) + pc.DeniedConns.Union(newDenied, true) // 'true' because denied conns are defined by rules from both sides // the allowed conns are "all conns - the denied conns" - // since all conns that are not determined by the ANP and BANP are allowed by default - pc.AllowedConns = common.MakeConnectionSet(true) + // all conns that are not determined by the ANP and BANP are allowed by default, + // and are kept in banpConns.AllowedConns (were returned by getXgressDefaultConns) + newAllowed := pc.PassConns.Copy() + newAllowed.Intersection(banpConns.AllowedConns) // collect implying rules from pc.PassConns and banpConns.AllowedConns + pc.AllowedConns.Union(newAllowed, false) // 'false' because allowed conns may be already defined by pc.AllowedConns + pc.AllowedConns.Subtract(pc.DeniedConns) } @@ -136,6 +158,6 @@ func (pc *PolicyConnections) IsEmpty() bool { // selects all the connections func (pc *PolicyConnections) DeterminesAllConns() bool { selectedConns := pc.AllowedConns.Copy() - selectedConns.Union(pc.DeniedConns) + selectedConns.Union(pc.DeniedConns, false) return selectedConns.IsAllConnections() } diff --git a/pkg/netpol/eval/resources.go b/pkg/netpol/eval/resources.go index d2b46e95..f8d2ad2d 100644 --- a/pkg/netpol/eval/resources.go +++ b/pkg/netpol/eval/resources.go @@ -22,7 +22,6 @@ import ( apisv1a "sigs.k8s.io/network-policy-api/apis/v1alpha1" "github.com/np-guard/models/pkg/netset" - "github.com/np-guard/netpol-analyzer/pkg/internal/netpolerrors" "github.com/np-guard/netpol-analyzer/pkg/manifests/parser" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" @@ -44,6 +43,7 @@ type ( baselineAdminNetpol *k8s.BaselineAdminNetworkPolicy // pointer to BaselineAdminNetworkPolicy which is a cluster singleton object cache *evalCache exposureAnalysisFlag bool + explain bool representativePeersMap map[string]*k8s.WorkloadPeer // map from unique labels string to representative peer object, // used only with exposure analysis (representative peer object is a workloadPeer with kind == "RepresentativePeer") } @@ -68,23 +68,26 @@ func NewPolicyEngine() *PolicyEngine { adminNetpolsMap: make(map[string]bool), cache: newEvalCache(), exposureAnalysisFlag: false, + explain: false, } } -func NewPolicyEngineWithObjects(objects []parser.K8sObject) (*PolicyEngine, error) { +func NewPolicyEngineWithObjects(objects []parser.K8sObject, explain bool) (*PolicyEngine, error) { pe := NewPolicyEngine() + pe.explain = explain err := pe.addObjectsByKind(objects) return pe, err } // NewPolicyEngineWithOptions returns a new policy engine with an empty state but updating the exposure analysis flag // TBD: currently exposure-analysis is the only option supported by policy-engine, so no need for options list param -func NewPolicyEngineWithOptions(exposureFlag bool) *PolicyEngine { +func NewPolicyEngineWithOptions(exposureFlag, explain bool) *PolicyEngine { pe := NewPolicyEngine() pe.exposureAnalysisFlag = exposureFlag if exposureFlag { pe.representativePeersMap = make(map[string]*k8s.WorkloadPeer) } + pe.explain = explain return pe } diff --git a/pkg/netpol/internal/common/augmented_intervalset.go b/pkg/netpol/internal/common/augmented_intervalset.go new file mode 100644 index 00000000..a98c097f --- /dev/null +++ b/pkg/netpol/internal/common/augmented_intervalset.go @@ -0,0 +1,721 @@ +/* +Copyright 2023- IBM Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package common + +import ( + "fmt" + "log" + "slices" + "sort" + "strings" + + "github.com/np-guard/models/pkg/interval" +) + +type ExplResultType int + +const ( + NoResult ExplResultType = iota + AllowResult + DenyResult +) + +type ImplyingXgressRulesType struct { + Rules map[string]int + // Result will keep the final connectivity decision which follows from the above rules + // (allow, deny or not set) + // It is used for specifying explainability decision per direction (Egress/Ingress) + Result ExplResultType +} + +type ImplyingRulesType struct { + Ingress ImplyingXgressRulesType // an ordered set of ingress rules, used for explainability + Egress ImplyingXgressRulesType // an ordered set of egress rules, used for explainability +} + +func InitImplyingXgressRules() ImplyingXgressRulesType { + return ImplyingXgressRulesType{Rules: map[string]int{}, Result: NoResult} +} + +func MakeImplyingXgressRulesWithRule(rule string) ImplyingXgressRulesType { + res := InitImplyingXgressRules() + res.AddXgressRule(rule) + return res +} + +func InitImplyingRules() ImplyingRulesType { + return ImplyingRulesType{Ingress: InitImplyingXgressRules(), Egress: InitImplyingXgressRules()} +} + +func MakeImplyingRulesWithRule(rule string, isIngress bool) ImplyingRulesType { + res := InitImplyingRules() + if isIngress { + res.Ingress = MakeImplyingXgressRulesWithRule(rule) + } else { + res.Egress = MakeImplyingXgressRulesWithRule(rule) + } + return res +} + +func (rules *ImplyingXgressRulesType) Copy() ImplyingXgressRulesType { + if rules == nil { + return InitImplyingXgressRules() + } + res := ImplyingXgressRulesType{Rules: map[string]int{}, Result: rules.Result} + for k, v := range rules.Rules { + res.Rules[k] = v + } + return res +} + +func (rules *ImplyingRulesType) Copy() ImplyingRulesType { + res := InitImplyingRules() + res.Ingress = rules.Ingress.Copy() + res.Egress = rules.Egress.Copy() + return res +} + +const ( + ExplWithRulesTitle = "due to the following policies//rules:" + IngressDirectionTitle = "\tINGRESS DIRECTION" + EgressDirectionTitle = "\tEGRESS DIRECTION" + NewLine = "\n" + SpaceSeparator = " " + ExplAllowAll = "(Allow all)" + SystemDefaultRule = "the system default " + ExplAllowAll + ExplSystemDefault = "due to " + SystemDefaultRule + PodToItselfRule = "pod to itself " + ExplAllowAll + allowResultStr = "ALLOWED" + denyResultStr = "DENIED" +) + +func (rules *ImplyingXgressRulesType) onlySystemDefaultRule() bool { + if _, ok := rules.Rules[SystemDefaultRule]; ok { + return len(rules.Rules) == 1 + } + return false +} + +func formattedExpl(expl string) string { + return "(" + expl + ")" +} + +func (rules *ImplyingXgressRulesType) resultString() string { + switch rules.Result { + case AllowResult: + return formattedExpl(allowResultStr) + case DenyResult: + return formattedExpl(denyResultStr) + default: + return "" + } +} + +func (rules *ImplyingXgressRulesType) String() string { + if rules.Empty() { + return rules.resultString() + } + // print the rules according to their order + formattedRules := make([]string, 0, len(rules.Rules)) + for name, order := range rules.Rules { + formattedRules = append(formattedRules, fmt.Sprintf("\t\t%d) %s", order+1, name)) + } + sort.Strings(formattedRules) // the rule index begins the string, like "2)" + return rules.resultString() + NewLine + strings.Join(formattedRules, NewLine) +} + +func (rules *ImplyingRulesType) OnlySystemDefaultRule() bool { + return rules.Ingress.onlySystemDefaultRule() && rules.Egress.onlySystemDefaultRule() +} + +func (rules ImplyingRulesType) String() string { + if rules.OnlySystemDefaultRule() { + return SpaceSeparator + SystemDefaultRule + NewLine + } + res := "" + if !rules.Egress.Empty() { + res += EgressDirectionTitle + if rules.Egress.onlySystemDefaultRule() { + res += SpaceSeparator + rules.Egress.resultString() + SpaceSeparator + ExplSystemDefault + NewLine + } else { + res += SpaceSeparator + rules.Egress.String() + NewLine + } + } + if !rules.Ingress.Empty() { + res += IngressDirectionTitle + if rules.Ingress.onlySystemDefaultRule() { + res += SpaceSeparator + rules.Ingress.resultString() + SpaceSeparator + ExplSystemDefault + NewLine + } else { + res += SpaceSeparator + rules.Ingress.String() + NewLine + } + } + if res == "" { + return NewLine + } + return SpaceSeparator + ExplWithRulesTitle + NewLine + res +} + +func (rules *ImplyingXgressRulesType) Empty() bool { + return len(rules.Rules) == 0 +} + +func (rules ImplyingRulesType) Empty(isIngress bool) bool { + if isIngress { + return rules.Ingress.Empty() + } + return rules.Egress.Empty() +} + +func (rules *ImplyingXgressRulesType) AddXgressRule(ruleName string) { + if ruleName != "" { + if _, ok := rules.Rules[ruleName]; !ok { + rules.Rules[ruleName] = len(rules.Rules) // a new rule should be the last + } + } +} + +func (rules *ImplyingRulesType) AddRule(ruleName string, isIngress bool) { + if isIngress { + rules.Ingress.AddXgressRule(ruleName) + } else { + rules.Egress.AddXgressRule(ruleName) + } +} + +func (rules *ImplyingXgressRulesType) SetXgressResult(isAllowed bool) { + if rules.Result != NoResult { + log.Panic(errConflictingExplResult) + } + if isAllowed { + rules.Result = AllowResult + } else { + rules.Result = DenyResult + } +} + +func (rules *ImplyingRulesType) SetResult(isAllowed, isIngress bool) { + if isIngress { + rules.Ingress.SetXgressResult(isAllowed) + } else { + rules.Egress.SetXgressResult(isAllowed) + } +} + +func (rules *ImplyingXgressRulesType) Union(other ImplyingXgressRulesType, collectRules bool) { + if !collectRules { + if rules.Empty() { + *rules = other.Copy() + } + return + } + + // first, count how many rules are common in both sets + common := 0 + for name := range other.Rules { + if _, ok := rules.Rules[name]; ok { + common += 1 + } + } + offset := len(rules.Rules) - common + for name, order := range other.Rules { + if _, ok := rules.Rules[name]; !ok { // for the common rules, keep their original order in the current rules + rules.Rules[name] = order + offset // other rules should be addded after the current rules + } + } + // update Result if set + if other.Result != NoResult { + rules.SetXgressResult(other.Result == AllowResult) + } +} + +func (rules *ImplyingXgressRulesType) mayBeUpdatedBy(other ImplyingXgressRulesType, collectRules bool) bool { + if !collectRules { + return rules.Empty() && !other.Empty() + } + for name := range other.Rules { + if _, ok := rules.Rules[name]; !ok { + return true + } + } + return false +} + +func (rules *ImplyingRulesType) Union(other ImplyingRulesType, collectRules bool) { + rules.Ingress.Union(other.Ingress, collectRules) + rules.Egress.Union(other.Egress, collectRules) +} + +func (rules ImplyingRulesType) mayBeUpdatedBy(other ImplyingRulesType, collectRules bool) bool { + return rules.Ingress.mayBeUpdatedBy(other.Ingress, collectRules) || rules.Egress.mayBeUpdatedBy(other.Egress, collectRules) +} + +const ( + NoIndex = -1 +) + +type AugmentedInterval struct { + interval interval.Interval + inSet bool + implyingRules ImplyingRulesType +} + +func NewAugmentedInterval(start, end int64, inSet bool) AugmentedInterval { + return AugmentedInterval{interval: interval.New(start, end), inSet: inSet, implyingRules: InitImplyingRules()} +} + +func NewAugmentedIntervalWithRule(start, end int64, inSet bool, rule string, isIngress bool) AugmentedInterval { + return AugmentedInterval{interval: interval.New(start, end), inSet: inSet, implyingRules: MakeImplyingRulesWithRule(rule, isIngress)} +} + +func NewAugmentedIntervalWithRules(start, end int64, inSet bool, rules ImplyingRulesType) AugmentedInterval { + return AugmentedInterval{interval: interval.New(start, end), inSet: inSet, implyingRules: rules.Copy()} +} + +// AugmentedCanonicalSet is a set of int64 integers, implemented using an ordered slice of non-overlapping, non-touching intervals. +// The intervals should include both included intervals and holes; +// i.e., start of every interval is the end of a previous interval incremented by 1. +// An AugmentedCanonicalSet is created with an interval/hole covering the whole range for this kind of set. +// The assumption is that further operations on a set will never extend this initial range, +// i.e., the MinValue() and MaxValue() functions will always return the same results. +type AugmentedCanonicalSet struct { + intervalSet []AugmentedInterval +} + +func NewAugmentedCanonicalSet(minValue, maxValue int64, isAll bool) *AugmentedCanonicalSet { + return &AugmentedCanonicalSet{ + intervalSet: []AugmentedInterval{ + NewAugmentedInterval(minValue, maxValue, isAll), // the full range interval (isAll==true) or 'hole' (isAll==false) + }, + } +} + +func NewAugmentedCanonicalSetWithRules(minValue, maxValue int64, isAll bool, rules ImplyingRulesType) *AugmentedCanonicalSet { + return &AugmentedCanonicalSet{ + intervalSet: []AugmentedInterval{ + NewAugmentedIntervalWithRules(minValue, maxValue, isAll, rules), // the full range interval (isAll==true) or 'hole' (isAll==false) + }, + } +} + +func (c *AugmentedCanonicalSet) Intervals() []AugmentedInterval { + return slices.Clone(c.intervalSet) +} + +func (c *AugmentedCanonicalSet) NumIntervals() int { + return len(c.intervalSet) +} + +const ( + errMinFromEmptySet = "cannot take min from empty interval set" + errOutOfRangeInterval = "cannot add interval which is out of scope of AugmentedCanonicalSet" + errConflictingExplResult = "cannot override explanation result that has been already set" +) + +func (c *AugmentedCanonicalSet) MinValue() int64 { + if len(c.intervalSet) == 0 { + log.Panic(errMinFromEmptySet) + } + return c.intervalSet[0].interval.Start() +} + +func (c *AugmentedCanonicalSet) MaxValue() int64 { + size := len(c.intervalSet) + if size == 0 { + log.Panic(errMinFromEmptySet) + } + return c.intervalSet[size-1].interval.End() +} + +func (c *AugmentedCanonicalSet) Min() int64 { + if len(c.intervalSet) == 0 { + log.Panic(errMinFromEmptySet) + } + for _, interval := range c.intervalSet { + if interval.inSet { + return interval.interval.Start() + } + } + log.Panic(errMinFromEmptySet) + return 0 // making linter happy +} + +// IsEmpty returns true if the AugmentedCanonicalSet is semantically empty (i.e., no 'inSet' intervals, but may possibly include holes) +func (c *AugmentedCanonicalSet) IsEmpty() bool { + for _, interval := range c.intervalSet { + if interval.inSet { + return false + } + } + return true +} + +// Unfilled returns true if the AugmentedCanonicalSet is syntactically empty (i.e., none of intervals or holes in the interval set) +func (c *AugmentedCanonicalSet) IsUnfilled() bool { + return len(c.intervalSet) == 0 +} + +func (c *AugmentedCanonicalSet) CalculateSize() int64 { + var res int64 = 0 + for _, r := range c.intervalSet { + if r.inSet { + res += r.interval.Size() + } + } + return res +} + +// func (c *AugmentedCanonicalSet) isConsistent() bool { +// lastInd := len(c.intervalSet) - 1 +// if lastInd < 0 { +// return true // the set is empty +// } +// lastInterval := c.intervalSet[lastInd] +// if lastInterval.inSet || lastInterval.interval.Start() < 0 || lastInterval.interval.End() != MaxValue { +// return false +// } +// return true +// } + +// nextIncludedInterval finds an interval included in set (not hole), starting from fromInd. +// if there are a few continuous in set intervals, it will return the union of all of them. +// it returns the found (potentially extended) interval, and the biggest index contributing to the result +func (c *AugmentedCanonicalSet) nextIncludedInterval(fromInd int) (res interval.Interval, index int) { + start := fromInd + for start < len(c.intervalSet) && !c.intervalSet[start].inSet { + start++ + } + if start >= len(c.intervalSet) { + return interval.New(0, -1), NoIndex + } + end := start + for end < len(c.intervalSet) && c.intervalSet[end].inSet { + end++ + } + return interval.New(c.intervalSet[start].interval.Start(), c.intervalSet[end-1].interval.End()), end - 1 +} + +// Equal returns true if the AugmentedCanonicalSet semantically equals the other AugmentedCanonicalSet; +// only numeric intervals are compared; the implying rules are not compared. +func (c *AugmentedCanonicalSet) Equal(other *AugmentedCanonicalSet) bool { + if c == other { + return true + } + currThisInd := 0 + currOtherInd := 0 + + for currThisInd != NoIndex { + thisInterval, thisInd := c.nextIncludedInterval(currThisInd) + otherInterval, otherInd := other.nextIncludedInterval(currOtherInd) + if (thisInd == NoIndex) != (otherInd == NoIndex) { + return false + } + if thisInd == NoIndex { + break + } + if !(thisInterval.Equal(otherInterval)) { + return false + } + currThisInd = thisInd + 1 + currOtherInd = otherInd + 1 + } + return true +} + +// AddAugmentedInterval adds a new interval/hole to the set, +// and updates the implying rules accordingly +// +//gocyclo:ignore +func (c *AugmentedCanonicalSet) AddAugmentedInterval(v AugmentedInterval, collectRules bool) { + if v.interval.Start() < c.MinValue() || v.interval.End() > c.MaxValue() { + log.Panic(errOutOfRangeInterval) + } + if v.interval.IsEmpty() { + return + } + set := c.intervalSet + left := sort.Search(len(set), func(i int) bool { + return set[i].interval.End() >= v.interval.Start() + }) + right := sort.Search(len(set), func(j int) bool { + return set[j].interval.End() >= v.interval.End() + }) + var result []AugmentedInterval + // copy left-end intervals not impacted by v + result = append(result, slices.Clone(set[0:left])...) + + // handle the left-hand side of the intersection of v with set + if v.interval.Start() > set[left].interval.Start() && + (set[left].inSet != v.inSet || set[left].implyingRules.mayBeUpdatedBy(v.implyingRules, collectRules)) { + // split set[left] into two intervals, while the implying rules of the second interval should get the new value (from v) + new1 := AugmentedInterval{interval: interval.New(set[left].interval.Start(), v.interval.Start()-1), + inSet: set[left].inSet, implyingRules: set[left].implyingRules.Copy()} + var newImplyingRules ImplyingRulesType + if set[left].inSet == v.inSet { + newImplyingRules = set[left].implyingRules.Copy() + newImplyingRules.Union(v.implyingRules, collectRules) + } else { + newImplyingRules = v.implyingRules.Copy() + } + new2 := AugmentedInterval{interval: interval.New(v.interval.Start(), min(set[left].interval.End(), v.interval.End())), + inSet: v.inSet, implyingRules: newImplyingRules} + result = append(result, new1, new2) + left++ + } + for ind := left; ind <= right; ind++ { + if ind == right && v.interval.End() < set[right].interval.End() && + (set[right].inSet != v.inSet || set[right].implyingRules.mayBeUpdatedBy(v.implyingRules, collectRules)) { + break // this is the corner case handled following the loop below + } + var newImplyingRules ImplyingRulesType + if set[ind].inSet == v.inSet { + // this interval is not impacted by v; + // however, its implying rules may be updated by those of v. + newImplyingRules = set[ind].implyingRules.Copy() + newImplyingRules.Union(v.implyingRules, collectRules) + } else { + newImplyingRules = v.implyingRules.Copy() + } + result = append(result, AugmentedInterval{interval: set[ind].interval, inSet: v.inSet, implyingRules: newImplyingRules}) + } + // handle the right-hand side of the intersection of v with set + if v.interval.End() < set[right].interval.End() && + (set[right].inSet != v.inSet || set[right].implyingRules.mayBeUpdatedBy(v.implyingRules, collectRules)) { + // split set[right] into two intervals, while the implying rules of the first interval should get the new value (from v) + if left < right || (left == right && v.interval.Start() == set[left].interval.Start()) { + // a special case when left==right (i.e., v is included in one interval from set) was already handled + // at the left-hand side of the intersection of v with set + var newImplyingRules ImplyingRulesType + if set[right].inSet == v.inSet { + newImplyingRules = set[right].implyingRules.Copy() + newImplyingRules.Union(v.implyingRules, collectRules) + } else { + newImplyingRules = v.implyingRules.Copy() + } + new1 := AugmentedInterval{interval: interval.New(set[right].interval.Start(), v.interval.End()), + inSet: v.inSet, implyingRules: newImplyingRules} + result = append(result, new1) + } + new2 := AugmentedInterval{interval: interval.New(v.interval.End()+1, set[right].interval.End()), + inSet: set[right].inSet, implyingRules: set[right].implyingRules.Copy()} + result = append(result, new2) + } + + // copy right-end intervals not impacted by v + result = append(result, slices.Clone(set[right+1:])...) + c.intervalSet = result +} + +// String returns a string representation of the current CanonicalSet object +func (c *AugmentedCanonicalSet) String() string { + if c.IsEmpty() { + return "" + } + res := "" + canonical := c.GetEquivalentCanonicalAugmentedSet() + for _, interval := range canonical.intervalSet { + if interval.inSet { + res += interval.interval.ShortString() + "," + } + } + return res[:len(res)-1] +} + +// Union returns the union of the two sets +// Note: this function is not symmetrical regarding the update of implying rules: +// it always prefers implying rules of 'c', and adds to it those of 'other' depending if collectRules == true +func (c *AugmentedCanonicalSet) Union(other *AugmentedCanonicalSet, collectRules bool) *AugmentedCanonicalSet { + if c == other { + return c.Copy() + } + // first, we add all 'out of set' intervals from both sets + // then, we add all 'in set' intervals from both sets + // this way we get the effect of union, while preserving all relevant implying rules + res := NewAugmentedCanonicalSet(c.MinValue(), c.MaxValue(), false) + for _, left := range c.intervalSet { + if !left.inSet { + res.AddAugmentedInterval(left, false) + } + } + for _, right := range other.intervalSet { + if !right.inSet { + res.AddAugmentedInterval(right, false) + } + } + for _, left := range c.intervalSet { + if left.inSet { + res.AddAugmentedInterval(left, collectRules) + } + } + for _, right := range other.intervalSet { + if right.inSet { + res.AddAugmentedInterval(right, collectRules) + } + } + return res +} + +// Copy returns a new copy of the CanonicalSet object +func (c *AugmentedCanonicalSet) Copy() *AugmentedCanonicalSet { + return &AugmentedCanonicalSet{intervalSet: slices.Clone(c.intervalSet)} +} + +func (c *AugmentedCanonicalSet) Contains(n int64) bool { + otherSet := NewAugmentedCanonicalSet(c.MinValue(), c.MaxValue(), false) + otherSet.AddAugmentedInterval(NewAugmentedInterval(n, n, true), false) + return otherSet.ContainedIn(c) +} + +// ContainedIn returns true of the current AugmentedCanonicalSet is contained in the other AugmentedCanonicalSet +func (c *AugmentedCanonicalSet) ContainedIn(other *AugmentedCanonicalSet) bool { + if c == other { + return true + } + currThisInd := 0 + currOtherInd := 0 + for currThisInd != NoIndex { + thisInterval, thisInd := c.nextIncludedInterval(currThisInd) + otherInterval, otherInd := other.nextIncludedInterval(currOtherInd) + if thisInd == NoIndex { + return true // end of this interval set + } + if otherInd == NoIndex { + return false // end of other interval set, but still have uncovered interval in this set + } + if thisInterval.IsSubset(otherInterval) { + // this interval is included in other; move to next intervals + currThisInd = thisInd + 1 + currOtherInd = otherInd + 1 + continue + } + if thisInterval.Overlap(otherInterval) { + // only part of this interval is contained + return false + } + if thisInterval.End() < otherInterval.Start() { + // this interval is not contained here + return false + } + // otherInterval.End() < thisInterval.Start() + // increment currOtherInd + currOtherInd = otherInd + 1 + } + return true +} + +// Intersect returns the intersection of the current set with the input set +func (c *AugmentedCanonicalSet) Intersect(other *AugmentedCanonicalSet) *AugmentedCanonicalSet { + if c == other { + return c.Copy() + } + // first, we add all 'in set' intervals from both sets + // then, we add all 'out of set' intervals from both sets + // this way we get the effect of intersection, while preserving all relevant implying rules + res := NewAugmentedCanonicalSet(c.MinValue(), c.MaxValue(), false) + for _, left := range c.intervalSet { + if left.inSet { + res.AddAugmentedInterval(left, true) // collect implying rules allowed by both sets + } + } + for _, right := range other.intervalSet { + if right.inSet { + res.AddAugmentedInterval(right, true) // collect implying rules allowed by both sets + } + } + for _, left := range c.intervalSet { + if !left.inSet { + res.AddAugmentedInterval(left, false) + } + } + for _, right := range other.intervalSet { + if !right.inSet { + res.AddAugmentedInterval(right, false) + } + } + return res +} + +// Overlap returns true if current AugmentedCanonicalSet overlaps with input AugmentedCanonicalSet +func (c *AugmentedCanonicalSet) Overlap(other *AugmentedCanonicalSet) bool { + if c == other { + return !c.IsEmpty() + } + currThisInd := 0 + currOtherInd := 0 + for currThisInd != NoIndex { + thisInterval, thisInd := c.nextIncludedInterval(currThisInd) + otherInterval, otherInd := other.nextIncludedInterval(currOtherInd) + if thisInd == NoIndex || otherInd == NoIndex { + return false // did not find overlapping interval + } + if thisInterval.Overlap(otherInterval) { + return true + } + if thisInterval.End() < otherInterval.Start() { + // increment currThisInd + currThisInd = thisInd + 1 + } else { // otherInterval.End() < thisInterval.Start() + // increment currOtherInd + currOtherInd = otherInd + 1 + } + } + return false +} + +// Subtract returns the subtraction result of other AugmentedCanonicalSet +func (c *AugmentedCanonicalSet) Subtract(other *AugmentedCanonicalSet) *AugmentedCanonicalSet { + if c == other { + return NewAugmentedCanonicalSet(c.MinValue(), c.MaxValue(), false) + } + res := c.Copy() + for _, interval := range other.intervalSet { + if interval.inSet { + hole := interval + hole.inSet = false + res.AddAugmentedInterval(hole, false) + } + } + return res +} + +func (c *AugmentedCanonicalSet) ClearInSet() { + for i := range c.intervalSet { + c.intervalSet[i].inSet = false + } +} + +// Elements returns a slice with all the numbers contained in the set. +// USE WITH CARE. It can easily run out of memory for large sets. +func (c *AugmentedCanonicalSet) Elements() []int64 { + // allocate memory up front, to fail early + res := make([]int64, c.CalculateSize()) + i := 0 + for _, interval := range c.intervalSet { + if interval.inSet { + for v := interval.interval.Start(); v <= interval.interval.End(); v++ { + res[i] = v + i++ + } + } + } + return res +} + +func (c *AugmentedCanonicalSet) GetEquivalentCanonicalAugmentedSet() *AugmentedCanonicalSet { + res := NewAugmentedCanonicalSet(c.MinValue(), c.MaxValue(), false) + interv, index := c.nextIncludedInterval(0) + for index != NoIndex { + res.AddAugmentedInterval(NewAugmentedInterval(interv.Start(), interv.End(), true), false) + interv, index = c.nextIncludedInterval(index + 1) + } + return res +} + +func (c *AugmentedCanonicalSet) SetExplResult(isIngress bool) { + for ind, v := range c.intervalSet { + c.intervalSet[ind].implyingRules.SetResult(v.inSet, isIngress) + } +} diff --git a/pkg/netpol/internal/common/connection.go b/pkg/netpol/internal/common/connection.go index 665a39c2..2e5cac54 100644 --- a/pkg/netpol/internal/common/connection.go +++ b/pkg/netpol/internal/common/connection.go @@ -13,7 +13,7 @@ import ( // Connection represents a set of allowed connections between two peers type Connection interface { // ProtocolsAndPortsMap returns the set of allowed connections - ProtocolsAndPortsMap() map[v1.Protocol][]PortRange + ProtocolsAndPortsMap(includeBlockedPorts bool) map[v1.Protocol][]PortRange // IsAllConnections returns true if all ports are allowed for all protocols IsAllConnections() bool // IsEmpty returns true if no connection is allowed diff --git a/pkg/netpol/internal/common/connectionset.go b/pkg/netpol/internal/common/connectionset.go index 63763e44..9146cdfb 100644 --- a/pkg/netpol/internal/common/connectionset.go +++ b/pkg/netpol/internal/common/connectionset.go @@ -8,22 +8,27 @@ package common import ( "fmt" + "log" "sort" "strconv" "strings" - "k8s.io/apimachinery/pkg/util/intstr" - v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" "github.com/np-guard/models/pkg/interval" ) // ConnectionSet represents a set of allowed connections between two peers on a k8s env // and implements Connection interface +// The explainability information is represented as follows: every PortSet (in AllowedProtocols) +// includes information about implying rules for every range. +// CommonImplyingRules contain implying rules for empty or full ConectionSet (when AllowedProtocols is empty) +// The following variant should hold: CommonImplyingRules not empty <==> AllowedProtocols empty type ConnectionSet struct { - AllowAll bool - AllowedProtocols map[v1.Protocol]*PortSet // map from protocol name to set of allowed ports + AllowAll bool + AllowedProtocols map[v1.Protocol]*PortSet // map from protocol name to set of allowed ports + CommonImplyingRules ImplyingRulesType // used for explainability, when AllowedProtocols is empty (i.e., all allowed or all denied) } var allProtocols = []v1.Protocol{v1.ProtocolTCP, v1.ProtocolUDP, v1.ProtocolSCTP} @@ -31,9 +36,34 @@ var allProtocols = []v1.Protocol{v1.ProtocolTCP, v1.ProtocolUDP, v1.ProtocolSCTP // MakeConnectionSet returns a pointer to ConnectionSet object with all connections or no connections func MakeConnectionSet(all bool) *ConnectionSet { if all { - return &ConnectionSet{AllowAll: true, AllowedProtocols: map[v1.Protocol]*PortSet{}} + return &ConnectionSet{AllowAll: true, AllowedProtocols: map[v1.Protocol]*PortSet{}, CommonImplyingRules: InitImplyingRules()} + } + return &ConnectionSet{AllowedProtocols: map[v1.Protocol]*PortSet{}, CommonImplyingRules: InitImplyingRules()} +} + +func MakeAllConnectionSetWithRule(rule string, isIngress bool) *ConnectionSet { + return &ConnectionSet{AllowAll: true, AllowedProtocols: map[v1.Protocol]*PortSet{}, + CommonImplyingRules: MakeImplyingRulesWithRule(rule, isIngress)} +} + +// Add common implying rule, i.e., a rule that is relevant for the whole ConnectionSet +func (conn *ConnectionSet) AddCommonImplyingRule(implyingRule string, isIngress bool) { + conn.CommonImplyingRules.AddRule(implyingRule, isIngress) +} + +func (conn *ConnectionSet) GetEquivalentCanonicalConnectionSet() *ConnectionSet { + res := MakeConnectionSet(false) + if conn.AllowAll { + res.AllowAll = true + return res } - return &ConnectionSet{AllowedProtocols: map[v1.Protocol]*PortSet{}} + for protocol, ports := range conn.AllowedProtocols { + canonicalPorts := ports.GetEquivalentCanonicalPortSet() + if !canonicalPorts.IsEmpty() { + res.AllowedProtocols[protocol] = canonicalPorts + } + } + return res } // GetAllTCPConnections returns a pointer to ConnectionSet object with all TCP protocol connections @@ -44,116 +74,152 @@ func GetAllTCPConnections() *ConnectionSet { } // Intersection updates ConnectionSet object to be the intersection result with other ConnectionSet +// the implying rules are symmetrically updated by both conn and other, +// i.e., conn does not have a precedence over other func (conn *ConnectionSet) Intersection(other *ConnectionSet) { - if other.AllowAll { - return - } - if conn.AllowAll { - conn.AllowAll = false - for protocol, ports := range other.AllowedProtocols { - conn.AllowedProtocols[protocol] = ports.Copy() + if len(conn.AllowedProtocols) == 0 && len(other.AllowedProtocols) == 0 { + // each one of conn and other is either AllowAll or Empty + if other.IsEmpty() { + conn.AllowAll = false + conn.AllowedProtocols = map[v1.Protocol]*PortSet{} } + // union common implying rules - a symmetrical update + conn.CommonImplyingRules.Union(other.CommonImplyingRules, true) return } + // prepare conn and other for the intersection - we need to seep implying rules info into all protocols/ports + conn.rebuildExplicitly() + other.rebuildExplicitly() + conn.AllowAll = false for protocol := range conn.AllowedProtocols { otherPorts, ok := other.AllowedProtocols[protocol] if !ok { - delete(conn.AllowedProtocols, protocol) + log.Panic("We should not get here") } else { conn.AllowedProtocols[protocol].Intersection(otherPorts) - if conn.AllowedProtocols[protocol].IsEmpty() { - delete(conn.AllowedProtocols, protocol) - } } } + conn.updateIfAllConnections() // the result may be AllowAll if both conn and other were AllowAll } // IsEmpty returns true if the ConnectionSet has no allowed connections func (conn *ConnectionSet) IsEmpty() bool { - return !conn.AllowAll && len(conn.AllowedProtocols) == 0 + if conn.AllowAll { + return false + } + if len(conn.AllowedProtocols) == 0 { + return true + } + // now check semantically + for _, protocol := range allProtocols { + ports, ok := conn.AllowedProtocols[protocol] + if ok && !ports.IsEmpty() { // this is a semantic emptiness check (no included ports, may be holes) + return false + } + } + return true } -func (conn *ConnectionSet) isAllConnectionsWithoutAllowAll() bool { +func (conn *ConnectionSet) updateIfAllConnections() { if conn.AllowAll { - return false + return } for _, protocol := range allProtocols { ports, ok := conn.AllowedProtocols[protocol] if !ok { - return false + return } else if !ports.IsAll() { - return false + return } } + conn.AllowAll = true + // we keep conn.AllowedProtocols data, we might need the ImplyingRules info for explainability +} - return true +func (conn *ConnectionSet) SetExplResult(isIngress bool) { + if len(conn.AllowedProtocols) == 0 { + // no AllowedProtocols --> compute result according to AllowAll + conn.CommonImplyingRules.SetResult(conn.AllowAll, isIngress) + return + } + // compute result for every range in AllowedProtocols + for _, ports := range conn.AllowedProtocols { + ports.Ports.SetExplResult(isIngress) + } } -func (conn *ConnectionSet) checkIfAllConnections() { - if conn.isAllConnectionsWithoutAllowAll() { - conn.AllowAll = true - conn.AllowedProtocols = map[v1.Protocol]*PortSet{} +// rebuildExplicitly : represent All/No connections explicitly (All connections if AllowAll==true, No connections otherwise), +// by building AllowedProtocols and adding the whole range intervals/holes (depending on AllowAll field) +func (conn *ConnectionSet) rebuildExplicitly() { + if len(conn.AllowedProtocols) == len(allProtocols) { + return // if all protocols exist, nothing to add + } + var portSet *PortSet + if conn.AllowAll { + portSet = MakeAllPortSetWithImplyingRules(conn.CommonImplyingRules) + } else { + portSet = MakeEmptyPortSetWithImplyingRules(conn.CommonImplyingRules) + } + for _, protocol := range allProtocols { + if _, ok := conn.AllowedProtocols[protocol]; !ok { + conn.AddConnection(protocol, portSet) + } } + conn.CommonImplyingRules = InitImplyingRules() } // Union updates ConnectionSet object to be the union result with other ConnectionSet -func (conn *ConnectionSet) Union(other *ConnectionSet) { - if conn.AllowAll || other.IsEmpty() { +// the implying rules are updated only if something changes in conn, +// i.e., conn has a precedence over other +func (conn *ConnectionSet) Union(other *ConnectionSet, collectRules bool) { + if conn.IsEmpty() && (other.IsEmpty() || other.AllowAll) && len(conn.AllowedProtocols) == 0 && len(other.AllowedProtocols) == 0 { + if other.IsEmpty() { + // we should union implying rules - both contribute to the result being empty + conn.CommonImplyingRules.Union(other.CommonImplyingRules, collectRules) + } else { + // we should substitute the implying rules by others' rules + conn.CommonImplyingRules = other.CommonImplyingRules.Copy() + } + conn.AllowAll = other.AllowAll return } - if other.AllowAll { - conn.AllowAll = true - conn.AllowedProtocols = map[v1.Protocol]*PortSet{} - return + if other.IsEmpty() { + return // neither connections nor implying rules can be updated } + conn.rebuildExplicitly() + other.rebuildExplicitly() for protocol := range conn.AllowedProtocols { if otherPorts, ok := other.AllowedProtocols[protocol]; ok { - conn.AllowedProtocols[protocol].Union(otherPorts) - } - } - for protocol := range other.AllowedProtocols { - if _, ok := conn.AllowedProtocols[protocol]; !ok { - portsCopy := other.AllowedProtocols[protocol].Copy() - conn.AllowedProtocols[protocol] = portsCopy + conn.AllowedProtocols[protocol].Union(otherPorts, collectRules) } } - conn.checkIfAllConnections() + conn.CommonImplyingRules = InitImplyingRules() // clear common implying rules, since we have implying rules in AllowedProtocols + conn.updateIfAllConnections() } // Subtract : updates current ConnectionSet object with the result of // subtracting other ConnectionSet from current ConnectionSet +// the implying rules are updated by both conn and other func (conn *ConnectionSet) Subtract(other *ConnectionSet) { - if other.IsEmpty() { // nothing to subtract + if /*conn.IsEmpty() ||*/ other.IsEmpty() { // nothing to subtract return } - if other.AllowAll { // subtract everything + if other.AllowAll && len(other.AllowedProtocols) == 0 { + // a special case when we should replace current common implying rules by others' + conn.CommonImplyingRules = other.CommonImplyingRules.Copy() conn.AllowAll = false conn.AllowedProtocols = map[v1.Protocol]*PortSet{} return } - if conn.AllowAll { - conn.AllowAll = false // we are about to subtract something - conn.addAllConns() - } + conn.rebuildExplicitly() + conn.AllowAll = false for protocol, ports := range conn.AllowedProtocols { if otherPorts, ok := other.AllowedProtocols[protocol]; ok { - if ports.ContainedIn(otherPorts) { - delete(conn.AllowedProtocols, protocol) - } else { - ports.subtract(otherPorts) - } + ports.subtract(otherPorts) } } } -// addAllConns : add all possible connections to the current ConnectionSet's allowed protocols -// added explicitly, without using the `AllowAll` field -func (conn *ConnectionSet) addAllConns() { - for _, protocol := range allProtocols { - conn.AddConnection(protocol, MakePortSet(true)) - } -} - // Contains returns true if the input port+protocol is an allowed connection func (conn *ConnectionSet) Contains(port, protocol string) bool { intPort, err := strconv.Atoi(port) @@ -180,6 +246,9 @@ func (conn *ConnectionSet) ContainedIn(other *ConnectionSet) bool { return false } for protocol, ports := range conn.AllowedProtocols { + if ports.IsEmpty() { + continue // empty port set might exist due to preserving data for explainability + } otherPorts, ok := other.AllowedProtocols[protocol] if !ok { return false @@ -193,12 +262,15 @@ func (conn *ConnectionSet) ContainedIn(other *ConnectionSet) bool { // AddConnection updates current ConnectionSet object with new allowed connection func (conn *ConnectionSet) AddConnection(protocol v1.Protocol, ports *PortSet) { - if ports.IsEmpty() { + if ports.IsUnfilled() { + // The return below is only when 'ports' is syntactically empty; + // In the case of a hole (semantically empty set), we do want to add it + // in order to keep the explanation data return } connPorts, ok := conn.AllowedProtocols[protocol] if ok { - connPorts.Union(ports) + connPorts.Union(ports, true) } else { conn.AllowedProtocols[protocol] = ports.Copy() } @@ -213,7 +285,9 @@ func (conn *ConnectionSet) String() string { } resStrings := []string{} for protocol, ports := range conn.AllowedProtocols { - resStrings = append(resStrings, protocolAndPortsStr(protocol, ports.String())) + if portsString := ports.String(); portsString != "" { + resStrings = append(resStrings, protocolAndPortsStr(protocol, portsString)) + } } sort.Strings(resStrings) return strings.Join(resStrings, ",") @@ -224,11 +298,13 @@ func (conn *ConnectionSet) Equal(other *ConnectionSet) bool { if conn.AllowAll != other.AllowAll { return false } - if len(conn.AllowedProtocols) != len(other.AllowedProtocols) { + connCanonical := conn.GetEquivalentCanonicalConnectionSet() + otherCanonical := other.GetEquivalentCanonicalConnectionSet() + if len(connCanonical.AllowedProtocols) != len(otherCanonical.AllowedProtocols) { return false } - for protocol, ports := range conn.AllowedProtocols { - otherPorts, ok := other.AllowedProtocols[protocol] + for protocol, ports := range connCanonical.AllowedProtocols { + otherPorts, ok := otherCanonical.AllowedProtocols[protocol] if !ok { return false } @@ -246,14 +322,15 @@ func (conn *ConnectionSet) Copy() *ConnectionSet { for protocol, portSet := range conn.AllowedProtocols { res.AllowedProtocols[protocol] = portSet.Copy() } + res.CommonImplyingRules = conn.CommonImplyingRules.Copy() return res } -// GetNamedPorts returns map from protocol to list of its allowed named ports -func (conn *ConnectionSet) GetNamedPorts() map[v1.Protocol][]string { - res := make(map[v1.Protocol][]string, 0) +// GetNamedPorts returns map from protocol to its allowed named ports (including ImplyingRules info) +func (conn *ConnectionSet) GetNamedPorts() map[v1.Protocol]NamedPortsType { + res := make(map[v1.Protocol]NamedPortsType, 0) for protocol, portSet := range conn.AllowedProtocols { - if namedPorts := portSet.GetNamedPortsKeys(); len(namedPorts) > 0 { + if namedPorts := portSet.GetNamedPorts(); len(namedPorts) > 0 { res[protocol] = namedPorts } } @@ -262,43 +339,58 @@ func (conn *ConnectionSet) GetNamedPorts() map[v1.Protocol][]string { // ReplaceNamedPortWithMatchingPortNum : replacing given namedPort with the matching given port num in the connection // if port num is -1; just deletes the named port from the protocol's list -func (conn *ConnectionSet) ReplaceNamedPortWithMatchingPortNum(protocol v1.Protocol, namedPort string, portNum int32) { +func (conn *ConnectionSet) ReplaceNamedPortWithMatchingPortNum(protocol v1.Protocol, namedPort string, portNum int32, + implyingRules ImplyingRulesType) { protocolPortSet := conn.AllowedProtocols[protocol] if portNum != NoPort { - protocolPortSet.AddPort(intstr.FromInt32(portNum)) + protocolPortSet.AddPort(intstr.FromInt32(portNum), implyingRules) } // after adding the portNum to the protocol's portSet; remove the port name protocolPortSet.RemovePort(intstr.FromString(namedPort)) } -// portRange implements the PortRange interface -type portRange struct { - Interval interval.Interval +// PortRangeData implements the PortRange interface +type PortRangeData struct { + Interval AugmentedInterval } -func (p *portRange) Start() int64 { - return p.Interval.Start() +func (p *PortRangeData) Start() int64 { + return p.Interval.interval.Start() } -func (p *portRange) End() int64 { - return p.Interval.End() +func (p *PortRangeData) End() int64 { + return p.Interval.interval.End() } -func (p *portRange) String() string { - if p.Interval.End() != p.Interval.Start() { +func (p *PortRangeData) String() string { + if p.End() != p.Start() { return fmt.Sprintf("%d-%d", p.Start(), p.End()) } return fmt.Sprintf("%d", p.Start()) } +func (p *PortRangeData) StringWithExplanation(protocolString string) string { + resultStr := allowResultStr + if !p.InSet() { + resultStr = denyResultStr + } + return resultStr + SpaceSeparator + protocolString + ":" + p.String() + p.Interval.implyingRules.String() +} + +func (p *PortRangeData) InSet() bool { + return p.Interval.inSet +} + // ProtocolsAndPortsMap() returns a map from allowed protocol to list of allowed ports ranges. -func (conn *ConnectionSet) ProtocolsAndPortsMap() map[v1.Protocol][]PortRange { +func (conn *ConnectionSet) ProtocolsAndPortsMap(includeDeniedPorts bool) map[v1.Protocol][]PortRange { res := make(map[v1.Protocol][]PortRange, 0) for protocol, portSet := range conn.AllowedProtocols { res[protocol] = make([]PortRange, 0) // TODO: consider leave the slice of ports empty if portSet covers the full range for _, v := range portSet.Ports.Intervals() { - res[protocol] = append(res[protocol], &portRange{Interval: v}) + if includeDeniedPorts || v.inSet { + res[protocol] = append(res[protocol], &PortRangeData{Interval: v}) + } } } return res @@ -324,11 +416,12 @@ func ConnStrFromConnProperties(allProtocolsAndPorts bool, protocolsAndPorts map[ } var connStr string // connStrings will contain the string of given conns protocols and ports as is - connStrings := make([]string, len(protocolsAndPorts)) - index := 0 + connStrings := make([]string, 0, len(protocolsAndPorts)) for protocol, ports := range protocolsAndPorts { - connStrings[index] = protocolAndPortsStr(protocol, portsString(ports)) - index++ + if thePortsStr := portsString(ports); thePortsStr != "" { + // thePortsStr might be empty if 'ports' does not contain 'InSet' ports + connStrings = append(connStrings, protocolAndPortsStr(protocol, thePortsStr)) + } } sort.Strings(connStrings) connStr = strings.Join(connStrings, connsAndPortRangeSeparator) @@ -336,14 +429,60 @@ func ConnStrFromConnProperties(allProtocolsAndPorts bool, protocolsAndPorts map[ } // get string representation for a list of port ranges +// return a canonical form (longest in-set ranges) func portsString(ports []PortRange) string { - portsStr := make([]string, len(ports)) + portsStr := make([]string, 0, len(ports)) + currInterval := interval.New(0, -1) // an empty interval for i := range ports { - portsStr[i] = ports[i].String() + if ports[i].(*PortRangeData).InSet() { + switch { + case currInterval.IsEmpty(): + currInterval = interval.New(ports[i].Start(), ports[i].End()) + case currInterval.End()+1 == ports[i].Start(): + currInterval = interval.New(currInterval.Start(), ports[i].End()) // extend the interval + default: + portsStr = append(portsStr, currInterval.ShortString()) + currInterval = interval.New(0, -1) + } + } else if !currInterval.IsEmpty() { + portsStr = append(portsStr, currInterval.ShortString()) + currInterval = interval.New(0, -1) + } + } + if !currInterval.IsEmpty() { + portsStr = append(portsStr, currInterval.ShortString()) } return strings.Join(portsStr, connsAndPortRangeSeparator) } +func portsStringWithExplanation(ports []PortRange, protocolString string) string { + portsStr := make([]string, 0, len(ports)) + for i := range ports { + portsStr = append(portsStr, ports[i].(*PortRangeData).StringWithExplanation(protocolString)) + } + return strings.Join(portsStr, NewLine) +} + func protocolAndPortsStr(protocol v1.Protocol, ports string) string { - return string(protocol) + " " + ports + return string(protocol) + SpaceSeparator + ports +} + +func ExplanationFromConnProperties(allProtocolsAndPorts bool, commonImplyingRules ImplyingRulesType, + protocolsAndPorts map[v1.Protocol][]PortRange) string { + if len(protocolsAndPorts) == 0 { + connStr := noConnsStr + if allProtocolsAndPorts { + connStr = allConnsStr + } + return connStr + commonImplyingRules.String() + } + var connStr string + // connStrings will contain the string of given conns protocols and ports as is + connStrings := make([]string, 0, len(protocolsAndPorts)) + for protocol, ports := range protocolsAndPorts { + connStrings = append(connStrings, portsStringWithExplanation(ports, string(protocol))) + } + sort.Strings(connStrings) + connStr = strings.Join(connStrings, NewLine) + return connStr } diff --git a/pkg/netpol/internal/common/portset.go b/pkg/netpol/internal/common/portset.go index fd0b7417..768d7a43 100644 --- a/pkg/netpol/internal/common/portset.go +++ b/pkg/netpol/internal/common/portset.go @@ -12,8 +12,6 @@ import ( "strings" "k8s.io/apimachinery/pkg/util/intstr" - - "github.com/np-guard/models/pkg/interval" ) const ( @@ -22,95 +20,134 @@ const ( maxPort int64 = 65535 ) +type NamedPortsType map[string]ImplyingRulesType + +func portNames(ports NamedPortsType) []string { + res := []string{} + for p := range ports { + res = append(res, p) + } + return res +} + // PortSet: represents set of allowed ports in a connection type PortSet struct { - Ports *interval.CanonicalSet - NamedPorts map[string]bool - ExcludedNamedPorts map[string]bool + Ports *AugmentedCanonicalSet // ports, augmented with implying rules data (used for explainability) + // NamedPorts/ExcludedNamedPorts is a map from a port name to implying rule names (used for explainnability) + // When not running with explainability, existing (excluded)named ports will be represented by a mapping + // from a port name to an empty implying rules holder + NamedPorts NamedPortsType + ExcludedNamedPorts NamedPortsType } // MakePortSet: return a new PortSet object, with all ports or no ports allowed func MakePortSet(all bool) *PortSet { - if all { - return &PortSet{Ports: interval.New(minPort, maxPort).ToSet(), - NamedPorts: map[string]bool{}, - ExcludedNamedPorts: map[string]bool{}, - } + return &PortSet{Ports: NewAugmentedCanonicalSet(minPort, maxPort, all), + NamedPorts: NamedPortsType{}, + ExcludedNamedPorts: NamedPortsType{}, + } +} + +func MakeAllPortSetWithImplyingRules(rules ImplyingRulesType) *PortSet { + return &PortSet{Ports: NewAugmentedCanonicalSetWithRules(minPort, maxPort, true, rules), + NamedPorts: NamedPortsType{}, + ExcludedNamedPorts: NamedPortsType{}, } - return &PortSet{Ports: interval.NewCanonicalSet(), - NamedPorts: map[string]bool{}, - ExcludedNamedPorts: map[string]bool{}, +} + +func MakeEmptyPortSetWithImplyingRules(rules ImplyingRulesType) *PortSet { + return &PortSet{Ports: NewAugmentedCanonicalSetWithRules(minPort, maxPort, false, rules), + NamedPorts: NamedPortsType{}, + ExcludedNamedPorts: NamedPortsType{}, } } // Equal: return true if current object equals another PortSet object func (p *PortSet) Equal(other *PortSet) bool { - return p.Ports.Equal(other.Ports) && reflect.DeepEqual(p.NamedPorts, other.NamedPorts) && - reflect.DeepEqual(p.ExcludedNamedPorts, other.ExcludedNamedPorts) + return p.Ports.Equal(other.Ports) && reflect.DeepEqual(portNames(p.NamedPorts), portNames(other.NamedPorts)) && + reflect.DeepEqual(portNames(p.ExcludedNamedPorts), portNames(other.ExcludedNamedPorts)) } -// IsEmpty: return true if current object is empty (no ports allowed) +// IsEmpty: return true if current PortSet is semantically empty (no ports allowed) func (p *PortSet) IsEmpty() bool { return p.Ports.IsEmpty() && len(p.NamedPorts) == 0 } +// Unfilled: return true if current PortSet is syntactically empty +func (p *PortSet) IsUnfilled() bool { + return p.Ports.IsUnfilled() && len(p.NamedPorts) == 0 +} + // Copy: return a new copy of a PortSet object func (p *PortSet) Copy() *PortSet { res := MakePortSet(false) res.Ports = p.Ports.Copy() for k, v := range p.NamedPorts { - res.NamedPorts[k] = v + res.NamedPorts[k] = v.Copy() } for k, v := range p.ExcludedNamedPorts { - res.ExcludedNamedPorts[k] = v + res.ExcludedNamedPorts[k] = v.Copy() } return res } // AddPort: update current PortSet object with new added port as allowed -func (p *PortSet) AddPort(port intstr.IntOrString) { +func (p *PortSet) AddPort(port intstr.IntOrString, implyingRules ImplyingRulesType) { if port.Type == intstr.String { - p.NamedPorts[port.StrVal] = true + if _, ok := p.NamedPorts[port.StrVal]; !ok { + p.NamedPorts[port.StrVal] = InitImplyingRules() + } + theRules := p.NamedPorts[port.StrVal] + theRules.Union(implyingRules, true) + p.NamedPorts[port.StrVal] = theRules delete(p.ExcludedNamedPorts, port.StrVal) } else { - p.Ports.AddInterval(interval.New(int64(port.IntVal), int64(port.IntVal))) + p.Ports.AddAugmentedInterval(NewAugmentedIntervalWithRules(int64(port.IntVal), int64(port.IntVal), true, implyingRules), true) } } // RemovePort: update current PortSet object with removing input port from allowed ports func (p *PortSet) RemovePort(port intstr.IntOrString) { if port.Type == intstr.String { + p.ExcludedNamedPorts[port.StrVal] = p.NamedPorts[port.StrVal] delete(p.NamedPorts, port.StrVal) - p.ExcludedNamedPorts[port.StrVal] = true } else { - p.Ports.AddHole(interval.New(int64(port.IntVal), int64(port.IntVal))) + p.Ports.AddAugmentedInterval(NewAugmentedInterval(int64(port.IntVal), int64(port.IntVal), false), false) } } // AddPortRange: update current PortSet object with new added port range as allowed -func (p *PortSet) AddPortRange(minPort, maxPort int64) { - p.Ports.AddInterval(interval.New(minPort, maxPort)) +func (p *PortSet) AddPortRange(minPort, maxPort int64, inSet bool, fromRule string, isIngress bool) { + p.Ports.AddAugmentedInterval(NewAugmentedIntervalWithRule(minPort, maxPort, inSet, fromRule, isIngress), true) } // Union: update current PortSet object with union of input PortSet object -func (p *PortSet) Union(other *PortSet) { - p.Ports = p.Ports.Union(other.Ports) +// Note: this function is not symmetrical regarding the update of implying rules: +// it updates implying rules of 'p' by those of 'other' only for ports that get changed in 'p' +func (p *PortSet) Union(other *PortSet, collectRules bool) { + p.Ports = p.Ports.Union(other.Ports, collectRules) // union current namedPorts with other namedPorts, and delete other namedPorts from current excludedNamedPorts for k, v := range other.NamedPorts { - p.NamedPorts[k] = v + if _, ok := p.NamedPorts[k]; !ok { + // this named port was not in p --> take implying rules from other + p.NamedPorts[k] = v.Copy() + } delete(p.ExcludedNamedPorts, k) } // add excludedNamedPorts from other to current excludedNamedPorts if they are not in united p.NamedPorts for k, v := range other.ExcludedNamedPorts { - if !p.NamedPorts[k] { - p.ExcludedNamedPorts[k] = v + if _, ok := p.NamedPorts[k]; !ok { + if _, ok := p.ExcludedNamedPorts[k]; !ok { + // this exluded named port was not excluded in p --> take implying rules from other + p.ExcludedNamedPorts[k] = v.Copy() + } } } } // ContainedIn: return true if current PortSet object is contained in input PortSet object func (p *PortSet) ContainedIn(other *PortSet) bool { - return p.Ports.IsSubset(other.Ports) + return p.Ports.ContainedIn(other.Ports) } // Intersection: update current PortSet object as intersection with input PortSet object @@ -124,7 +161,6 @@ func (p *PortSet) IsAll() bool { } const comma = "," -const emptyStr = "Empty" // String: return string representation of current PortSet func (p *PortSet) String() string { @@ -132,10 +168,7 @@ func (p *PortSet) String() string { if len(p.NamedPorts) > 0 { sortedNamedPorts := p.GetNamedPortsKeys() sort.Strings(sortedNamedPorts) - // if p.Ports is empty but p.NamedPorts is not: start a new string - if res == emptyStr { - res = "" - } else { + if res != "" { res += comma } res += strings.Join(sortedNamedPorts, comma) @@ -148,7 +181,12 @@ func (p *PortSet) Contains(port int64) bool { return p.Ports.Contains(port) } -// GetNamedPortsKeys returns the named ports of current portSet +// GetNamedPorts returns the named ports of the current PortSet +func (p *PortSet) GetNamedPorts() NamedPortsType { + return p.NamedPorts +} + +// GetNamedPortsKeys returns the named ports names of the current PortSet func (p *PortSet) GetNamedPortsKeys() []string { res := make([]string, len(p.NamedPorts)) index := 0 @@ -162,14 +200,21 @@ func (p *PortSet) GetNamedPortsKeys() []string { // subtract: updates current portSet with the result of subtracting the given portSet from it func (p *PortSet) subtract(other *PortSet) { p.Ports = p.Ports.Subtract(other.Ports) - p.subtractNamedPorts(other.NamedPorts) + // delete other named ports from current portSet's named ports map + // and add the deleted named ports to excluded named ports map + for k, v := range other.NamedPorts { + if _, ok := p.ExcludedNamedPorts[k]; !ok { + p.ExcludedNamedPorts[k] = InitImplyingRules() + } + theRules := p.ExcludedNamedPorts[k] + theRules.Union(v.Copy(), true) + p.ExcludedNamedPorts[k] = theRules + delete(p.NamedPorts, k) + } } -// subtractNamedPorts: deletes given named ports from current portSet's named ports map -// and adds the deleted named ports to excluded named ports map -func (p *PortSet) subtractNamedPorts(otherNamedPorts map[string]bool) { - for namedPort := range otherNamedPorts { - delete(p.NamedPorts, namedPort) - p.ExcludedNamedPorts[namedPort] = true - } +func (p *PortSet) GetEquivalentCanonicalPortSet() *PortSet { + res := p.Copy() + res.Ports = p.Ports.GetEquivalentCanonicalAugmentedSet() + return res } diff --git a/test_outputs/connlist/anp_banp_blog_demo_2_explain_output.txt b/test_outputs/connlist/anp_banp_blog_demo_2_explain_output.txt new file mode 100644 index 00000000..fda432e6 --- /dev/null +++ b/test_outputs/connlist/anp_banp_blog_demo_2_explain_output.txt @@ -0,0 +1,126 @@ +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN 0.0.0.0-255.255.255.255 => foo/my-foo[Pod]: + +No Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (DENIED) + 1) [NP] foo/allow-monitoring//Ingress (captured but not selected by any Ingress rule) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN bar/my-bar[Pod] => foo/my-foo[Pod]: + +No Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (DENIED) + 1) [NP] foo/allow-monitoring//Ingress (captured but not selected by any Ingress rule) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN baz/my-baz[Pod] => foo/my-foo[Pod]: + +No Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (DENIED) + 1) [NP] foo/allow-monitoring//Ingress (captured but not selected by any Ingress rule) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => bar/my-bar[Pod]: + +ALLOWED SCTP:1-65535 the system default (Allow all) + +ALLOWED UDP:1-65535 the system default (Allow all) + +DENIED TCP:1-1233 due to the following policies//rules: + INGRESS DIRECTION (DENIED) + 1) [BANP] default//Ingress rule deny-ingress-from-all-namespaces (Deny) + +ALLOWED TCP:1234 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] allow-monitoring//Ingress rule allow-ingress-from-monitoring (Allow) + +DENIED TCP:1235-8079 due to the following policies//rules: + INGRESS DIRECTION (DENIED) + 1) [BANP] default//Ingress rule deny-ingress-from-all-namespaces (Deny) + +DENIED TCP:8080 due to the following policies//rules: + INGRESS DIRECTION (DENIED) + 1) [ANP] pass-monitoring//Ingress rule pass-ingress-from-monitoring (Pass) + 2) [BANP] default//Ingress rule deny-ingress-from-all-namespaces (Deny) + +DENIED TCP:8081-9000 due to the following policies//rules: + INGRESS DIRECTION (DENIED) + 1) [BANP] default//Ingress rule deny-ingress-from-all-namespaces (Deny) + +ALLOWED TCP:9001-65535 the system default (Allow all) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => baz/my-baz[Pod]: + +ALLOWED SCTP:1-65535 the system default (Allow all) + +ALLOWED TCP:1-1233 the system default (Allow all) + +ALLOWED TCP:1234 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] allow-monitoring//Ingress rule allow-ingress-from-monitoring (Allow) + +ALLOWED TCP:1235-65535 the system default (Allow all) + +ALLOWED UDP:1-65535 the system default (Allow all) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => foo/my-foo[Pod]: + +ALLOWED SCTP:1-65535 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [NP] foo/allow-monitoring//Ingress rule #1 + +ALLOWED TCP:1-1233 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [NP] foo/allow-monitoring//Ingress rule #1 + +ALLOWED TCP:1234 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] allow-monitoring//Ingress rule allow-ingress-from-monitoring (Allow) + +ALLOWED TCP:1235-8079 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [NP] foo/allow-monitoring//Ingress rule #1 + +ALLOWED TCP:8080 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] pass-monitoring//Ingress rule pass-ingress-from-monitoring (Pass) + 2) [NP] foo/allow-monitoring//Ingress rule #1 + +ALLOWED TCP:8081-65535 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [NP] foo/allow-monitoring//Ingress rule #1 + +ALLOWED UDP:1-65535 due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [NP] foo/allow-monitoring//Ingress rule #1 + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +The following nodes are connected due to the system default (Allow all): +0.0.0.0-255.255.255.255 => bar/my-bar[Pod] +0.0.0.0-255.255.255.255 => baz/my-baz[Pod] +0.0.0.0-255.255.255.255 => monitoring/my-monitoring[Pod] +bar/my-bar[Pod] => 0.0.0.0-255.255.255.255 +bar/my-bar[Pod] => baz/my-baz[Pod] +bar/my-bar[Pod] => monitoring/my-monitoring[Pod] +baz/my-baz[Pod] => 0.0.0.0-255.255.255.255 +baz/my-baz[Pod] => bar/my-bar[Pod] +baz/my-baz[Pod] => monitoring/my-monitoring[Pod] +foo/my-foo[Pod] => 0.0.0.0-255.255.255.255 +foo/my-foo[Pod] => bar/my-bar[Pod] +foo/my-foo[Pod] => baz/my-baz[Pod] +foo/my-foo[Pod] => monitoring/my-monitoring[Pod] +monitoring/my-monitoring[Pod] => 0.0.0.0-255.255.255.255 diff --git a/test_outputs/connlist/anp_banp_blog_demo_focus_workload_my-monitoring_explain_output.txt b/test_outputs/connlist/anp_banp_blog_demo_focus_workload_my-monitoring_explain_output.txt new file mode 100644 index 00000000..d661b490 --- /dev/null +++ b/test_outputs/connlist/anp_banp_blog_demo_focus_workload_my-monitoring_explain_output.txt @@ -0,0 +1,33 @@ +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => bar/my-bar[Pod]: + +No Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (DENIED) + 1) [ANP] pass-monitoring//Ingress rule pass-ingress-from-monitoring (Pass) + 2) [BANP] default//Ingress rule deny-ingress-from-all-namespaces (Deny) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => baz/my-baz[Pod]: + +All Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] allow-monitoring//Ingress rule allow-ingress-from-monitoring (Allow) + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +CONNECTIONS BETWEEN monitoring/my-monitoring[Pod] => foo/my-foo[Pod]: + +All Connections due to the following policies//rules: + EGRESS DIRECTION (ALLOWED) due to the system default (Allow all) + INGRESS DIRECTION (ALLOWED) + 1) [ANP] pass-monitoring//Ingress rule pass-ingress-from-monitoring (Pass) + 2) [NP] foo/allow-monitoring//Ingress rule #1 + +---------------------------------------------------------------------------------------------------------------------------------------------------------------- +The following nodes are connected due to the system default (Allow all): +0.0.0.0-255.255.255.255 => monitoring/my-monitoring[Pod] +bar/my-bar[Pod] => monitoring/my-monitoring[Pod] +baz/my-baz[Pod] => monitoring/my-monitoring[Pod] +foo/my-foo[Pod] => monitoring/my-monitoring[Pod] +monitoring/my-monitoring[Pod] => 0.0.0.0-255.255.255.255 diff --git a/tests/anp_banp_blog_demo_2/ns.yaml b/tests/anp_banp_blog_demo_2/ns.yaml new file mode 100644 index 00000000..c9b2481e --- /dev/null +++ b/tests/anp_banp_blog_demo_2/ns.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: foo + labels: + security: internal + kubernetes.io/metadata.name: foo +--- + +apiVersion: v1 +kind: Namespace +metadata: + name: bar + labels: + security: internal + kubernetes.io/metadata.name: bar + +--- + +apiVersion: v1 +kind: Namespace +metadata: + name: baz + labels: + kubernetes.io/metadata.name: baz + +--- + + +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring + labels: + kubernetes.io/metadata.name: monitoring \ No newline at end of file diff --git a/tests/anp_banp_blog_demo_2/policies.yaml b/tests/anp_banp_blog_demo_2/policies.yaml new file mode 100644 index 00000000..08f61ca7 --- /dev/null +++ b/tests/anp_banp_blog_demo_2/policies.yaml @@ -0,0 +1,87 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-monitoring + namespace: foo +spec: + podSelector: + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + +--- + +apiVersion: policy.networking.k8s.io/v1alpha1 +kind: BaselineAdminNetworkPolicy +metadata: + name: default +spec: + subject: + namespaces: + matchLabels: + security: internal + ingress: + - name: "deny-ingress-from-all-namespaces" + action: "Deny" + from: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - portRange: + protocol: TCP + start: 1 + end: 9000 + +--- + +apiVersion: policy.networking.k8s.io/v1alpha1 +kind: AdminNetworkPolicy +metadata: + name: allow-monitoring +spec: + priority: 9 + subject: + namespaces: {} + ingress: + - name: "allow-ingress-from-monitoring" + action: "Allow" + from: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - portNumber: + protocol: TCP + port: 1234 + + + +--- + +apiVersion: policy.networking.k8s.io/v1alpha1 +kind: AdminNetworkPolicy +metadata: + name: pass-monitoring +spec: + priority: 7 + subject: + namespaces: + matchLabels: + security: internal + ingress: + - name: "pass-ingress-from-monitoring" + action: "Pass" + from: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - portNumber: + protocol: TCP + port: 8080 + diff --git a/tests/anp_banp_blog_demo_2/workloads.yaml b/tests/anp_banp_blog_demo_2/workloads.yaml new file mode 100644 index 00000000..00b070ff --- /dev/null +++ b/tests/anp_banp_blog_demo_2/workloads.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: Pod +metadata: + namespace: foo + name: my-foo + labels: + security: internal +spec: + containers: + - name: myfirstContainer + image: fooimage + +--- + +apiVersion: v1 +kind: Pod +metadata: + namespace: bar + name: my-bar + labels: + security: internal +spec: + containers: + - name: myfirstContainer + image: barimage + +--- + +apiVersion: v1 +kind: Pod +metadata: + namespace: baz + name: my-baz + labels: + security: none +spec: + containers: + - name: myfirstContainer + image: bazimage + +--- + +apiVersion: v1 +kind: Pod +metadata: + namespace: monitoring + name: my-monitoring + labels: + security: monitoring +spec: + containers: + - name: myfirstContainer + image: monitoringimage + +--- + +