diff --git a/README.md b/README.md index 78212ddd..a2ec8b83 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,13 @@ Flags: -f, --file string Write output to specified file --focusworkload Focus connections of specified workload in the output (supported formats: , /) (to focus connections from Ingress/Route only, use `ingress-controller` as ) - -o, --output string Required output format (txt, json, dot, csv, md) (default "txt") + -o, --output string Required output format (txt, json, dot, csv, md) or (txt, dot) with exposure analysis (default "txt") -h, --help help for list Global Flags: -c, --context string Kubernetes context to use when evaluating connections in a live cluster --dirpath string Resources dir path when evaluating connections from a dir + --exposure Runs also exposure analysis --fail fail on the first encountered error --include-json consider JSON manifests (in addition to YAML) when analyzing from dir -k, --kubeconfig string Path and file to use for kubeconfig when evaluating connections in a live cluster diff --git a/pkg/cli/command_test.go b/pkg/cli/command_test.go index 0e0f445d..b1bbbfd7 100644 --- a/pkg/cli/command_test.go +++ b/pkg/cli/command_test.go @@ -51,7 +51,7 @@ func buildAndExecuteCommand(args []string) (string, error) { } // append the optional args of a command if the values are not empty -func addCmdOptionalArgs(format, outputFile, focusWorkload string) []string { +func addCmdOptionalArgs(format, outputFile, focusWorkload string, exposure bool) []string { res := []string{} if focusWorkload != "" { res = append(res, "--focusworkload", focusWorkload) @@ -62,6 +62,9 @@ func addCmdOptionalArgs(format, outputFile, focusWorkload string) []string { if outputFile != "" { res = append(res, "-f", outputFile) } + if exposure { + res = append(res, "--exposure") + } return res } @@ -75,9 +78,10 @@ func determineFileSuffix(format string) string { } // gets the test name and name of expected output file for a list command from its args -func getListCmdTestNameAndExpectedOutputFile(dirName, focusWorkload, format string) (testName, expectedOutputFileName string) { +func getListCmdTestNameAndExpectedOutputFile(dirName, focusWorkload, format string, exposureFlag bool) (testName, + expectedOutputFileName string) { fileSuffix := determineFileSuffix(format) - return testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, fileSuffix) + return testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, fileSuffix, exposureFlag) } func testInfo(testName string) string { @@ -211,6 +215,7 @@ func TestListCommandOutput(t *testing.T) { focusWorkload string format string outputFile string + exposureFlag bool }{ // when focusWorkload is empty, output should be the connlist of the dir // when format is empty - output should be in defaultFormat (txt) @@ -259,13 +264,17 @@ func TestListCommandOutput(t *testing.T) { dirName: "onlineboutique", outputFile: outFileName, }, + { + dirName: "acs-security-demos", + exposureFlag: true, + }, } for _, tt := range cases { tt := tt - testName, expectedOutputFileName := getListCmdTestNameAndExpectedOutputFile(tt.dirName, tt.focusWorkload, tt.format) + testName, expectedOutputFileName := getListCmdTestNameAndExpectedOutputFile(tt.dirName, tt.focusWorkload, tt.format, tt.exposureFlag) t.Run(testName, func(t *testing.T) { args := []string{"list", "--dirpath", testutils.GetTestDirPath(tt.dirName)} - args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, tt.focusWorkload)...) + args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, tt.focusWorkload, tt.exposureFlag)...) actualOut, err := buildAndExecuteCommand(args) require.Nil(t, err, "test: %q", testName) testutils.CheckActualVsExpectedOutputMatch(t, expectedOutputFileName, actualOut, testInfo(testName), currentPkg) @@ -318,7 +327,7 @@ func TestDiffCommandOutput(t *testing.T) { t.Run(testName, func(t *testing.T) { args := []string{"diff", "--dir1", testutils.GetTestDirPath(tt.dir1), "--dir2", testutils.GetTestDirPath(tt.dir2)} - args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, "")...) + args = append(args, addCmdOptionalArgs(tt.format, tt.outputFile, "", false)...) actualOut, err := buildAndExecuteCommand(args) require.Nil(t, err, "test: %q", testName) testutils.CheckActualVsExpectedOutputMatch(t, expectedOutputFileName, actualOut, testInfo(testName), currentPkg) diff --git a/pkg/cli/diff.go b/pkg/cli/diff.go index 0c263db5..8878faf4 100644 --- a/pkg/cli/diff.go +++ b/pkg/cli/diff.go @@ -101,7 +101,7 @@ func newCommandDiff() *cobra.Command { c.Flags().StringVarP(&dir1, dir1Arg, "", "", "Original Resources path to be compared") c.Flags().StringVarP(&dir2, dir2Arg, "", "", "New Resources path to compare with original resources path") supportedDiffFormats := strings.Join(diff.ValidDiffFormats, ",") - c.Flags().StringVarP(&outFormat, "output", "o", outconsts.DefaultFormat, getOutputFormatDescription(supportedDiffFormats)) + c.Flags().StringVarP(&outFormat, "output", "o", outconsts.DefaultFormat, getRequiredOutputFormatString(supportedDiffFormats)) // out file c.Flags().StringVarP(&outFile, "file", "f", "", "Write output to specified file") return c diff --git a/pkg/cli/list.go b/pkg/cli/list.go index 8bd8b2ad..9661aed4 100644 --- a/pkg/cli/list.go +++ b/pkg/cli/list.go @@ -33,10 +33,20 @@ var ( outFile string // output file ) -func getOutputFormatDescription(validFormats string) string { +// getRequiredOutputFormatString returns the description of required format(s) of the command +func getRequiredOutputFormatString(validFormats string) string { return fmt.Sprintf("Required output format (%s)", validFormats) } +// getListOutputFormatDescription returns the description of the required formats of the list command +// exposure analysis is supported with less formats +func getListOutputFormatDescription() string { + comma := "," + supportedFormats := strings.Join(connlist.ValidFormats, comma) + supportedExposureFormats := strings.Join(connlist.ExposureValidFormats, comma) + return getRequiredOutputFormatString(supportedFormats) + " or (" + supportedExposureFormats + " with exposure analysis) " +} + func runListCommand() error { var conns []connlist.Peer2PeerConnection var err error @@ -108,7 +118,7 @@ defined`, k8snetpolicy list -k ./kube/config`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if err := connlist.ValidateOutputFormat(output); err != nil { + if err := connlist.ValidateOutputFormat(output, exposureAnalysis); err != nil { return err } // call parent pre-run @@ -137,8 +147,7 @@ defined`, "Focus connections of specified workload in the output ( or )") c.Flags().BoolVarP(&exposureAnalysis, "exposure", "", false, "Turn on exposure analysis and append results to the output") // output format - default txt - supportedFormats := strings.Join(connlist.ValidFormats, ",") - c.Flags().StringVarP(&output, "output", "o", outconsts.DefaultFormat, getOutputFormatDescription(supportedFormats)) + c.Flags().StringVarP(&output, "output", "o", outconsts.DefaultFormat, getListOutputFormatDescription()) // out file c.Flags().StringVarP(&outFile, "file", "f", "", "Write output to specified file") diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index 04ea8b7f..36532d9d 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -35,8 +35,12 @@ func GetTestDirPath(dirName string) string { } // ConnlistTestNameByTestArgs returns connlist test name and test's expected output file from some tests args -func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string) (testName, expectedOutputFileName string) { +func ConnlistTestNameByTestArgs(dirName, focusWorkload, format string, exposureFlag bool) (testName, expectedOutputFileName string) { namePrefix := dirName + if exposureFlag { + // if dir name contains a separator; last element is enough for the test and file names + namePrefix = "exposure_" + filepath.Base(dirName) + } if focusWorkload != "" { namePrefix += focusWlAnnotation + strings.Replace(focusWorkload, "/", underscore, 1) } diff --git a/pkg/netpol/connlist/connlist.go b/pkg/netpol/connlist/connlist.go index 04664b8a..aee266e8 100644 --- a/pkg/netpol/connlist/connlist.go +++ b/pkg/netpol/connlist/connlist.go @@ -95,6 +95,8 @@ func (ca *ConnlistAnalyzer) ConnlistFromDirPath(dirPath string) ([]Peer2PeerConn var ValidFormats = []string{output.TextFormat, output.JSONFormat, output.DOTFormat, output.CSVFormat, output.MDFormat} +var ExposureValidFormats = []string{output.TextFormat} + // ConnlistAnalyzerOption is the type for specifying options for ConnlistAnalyzer, // using Golang's Options Pattern (https://golang.cafe/blog/golang-functional-options-pattern.html). type ConnlistAnalyzerOption func(*ConnlistAnalyzer) @@ -262,12 +264,12 @@ func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clients // ConnectionsListToString returns a string of connections from list of Peer2PeerConnection objects in the required output format func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection) (string, error) { - connsFormatter, err := getFormatter(ca.outputFormat) + connsFormatter, err := ca.getFormatter() if err != nil { ca.errors = append(ca.errors, newResultFormattingError(err)) return "", err } - out, err := connsFormatter.writeOutput(conns) + out, err := connsFormatter.writeOutput(conns, ca.exposureResult) if err != nil { ca.errors = append(ca.errors, newResultFormattingError(err)) return "", err @@ -276,8 +278,12 @@ func (ca *ConnlistAnalyzer) ConnectionsListToString(conns []Peer2PeerConnection) } // validate the value of the output format -func ValidateOutputFormat(format string) error { - for _, formatName := range ValidFormats { +func ValidateOutputFormat(format string, exposureFlag bool) error { + formatList := ValidFormats + if exposureFlag { + formatList = ExposureValidFormats + } + for _, formatName := range formatList { if format == formatName { return nil } @@ -286,23 +292,23 @@ func ValidateOutputFormat(format string) error { } // returns the relevant formatter for the analyzer's outputFormat -func getFormatter(format string) (connsFormatter, error) { - if err := ValidateOutputFormat(format); err != nil { +func (ca *ConnlistAnalyzer) getFormatter() (connsFormatter, error) { + if err := ValidateOutputFormat(ca.outputFormat, ca.exposureAnalysis); err != nil { return nil, err } - switch format { + switch ca.outputFormat { case output.JSONFormat: - return formatJSON{}, nil + return &formatJSON{}, nil case output.TextFormat: - return formatText{}, nil + return &formatText{}, nil case output.DOTFormat: - return formatDOT{}, nil + return &formatDOT{}, nil case output.CSVFormat: - return formatCSV{}, nil + return &formatCSV{}, nil case output.MDFormat: - return formatMD{}, nil + return &formatMD{}, nil default: - return formatText{}, nil + return &formatText{}, nil } } diff --git a/pkg/netpol/connlist/connlist_test.go b/pkg/netpol/connlist/connlist_test.go index 44b9c62f..b67030c7 100644 --- a/pkg/netpol/connlist/connlist_test.go +++ b/pkg/netpol/connlist/connlist_test.go @@ -76,7 +76,7 @@ func TestConnListFromDir(t *testing.T) { t.Run(tt.testDirName, func(t *testing.T) { t.Parallel() for _, format := range tt.outputFormats { - pTest := prepareTest(tt.testDirName, tt.focusWorkload, format) + pTest := prepareTest(tt.testDirName, tt.focusWorkload, format, tt.exposureAnalysis) res, _, err := pTest.analyzer.ConnlistFromDirPath(pTest.dirPath) require.Nil(t, err, pTest.testInfo) out, err := pTest.analyzer.ConnectionsListToString(res) @@ -95,7 +95,7 @@ func TestConnListFromResourceInfos(t *testing.T) { t.Run(tt.testDirName, func(t *testing.T) { t.Parallel() for _, format := range tt.outputFormats { - pTest := prepareTest(tt.testDirName, tt.focusWorkload, format) + pTest := prepareTest(tt.testDirName, tt.focusWorkload, format, tt.exposureAnalysis) infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{pTest.dirPath}, true, false) // require.Empty(t, errs, testInfo) - TODO: add info about expected errors // from each test here (these errors do not stop the analysis or affect the output) @@ -189,7 +189,7 @@ func testFatalErr(t *testing.T, func getAnalysisResFromAPI(apiName, dirName, focusWorkload string) ( analyzer *ConnlistAnalyzer, connsRes []Peer2PeerConnection, peersRes []Peer, err error) { - pTest := prepareTest(dirName, focusWorkload, output.DefaultFormat) + pTest := prepareTest(dirName, focusWorkload, output.DefaultFormat, false) switch apiName { case ResourceInfosFunc: infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{pTest.dirPath}, true, false) @@ -468,11 +468,15 @@ type preparedTest struct { analyzer *ConnlistAnalyzer } -func prepareTest(dirName, focusWorkload, format string) preparedTest { +func prepareTest(dirName, focusWorkload, format string, exposureFlag bool) preparedTest { res := preparedTest{} - res.testName, res.expectedOutputFileName = testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, format) + res.testName, res.expectedOutputFileName = testutils.ConnlistTestNameByTestArgs(dirName, focusWorkload, format, exposureFlag) res.testInfo = fmt.Sprintf("test: %q, output format: %q", res.testName, format) - res.analyzer = NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload)) + cAnalyzer := NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload)) + if exposureFlag { + cAnalyzer = NewConnlistAnalyzer(WithOutputFormat(format), WithFocusWorkload(focusWorkload), WithExposureAnalysis()) + } + res.analyzer = cAnalyzer res.dirPath = testutils.GetTestDirPath(dirName) return res } @@ -488,6 +492,7 @@ func TestConnlistOutputFatalErrors(t *testing.T) { dirName string format string errorStrContains string + exposureFlag bool }{ { name: "giving_unsupported_output_format_option_should_return_fatal_error", @@ -495,12 +500,19 @@ func TestConnlistOutputFatalErrors(t *testing.T) { format: "docx", errorStrContains: netpolerrors.FormatNotSupportedErrStr("docx"), }, + { + name: "unsupported_output_format_for_exposure_analysis_should_return_fatal_error", + dirName: "acs-security-demos", + format: "json", + errorStrContains: netpolerrors.FormatNotSupportedErrStr("json"), + exposureFlag: true, + }, } for _, tt := range cases { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - preparedTest := prepareTest(tt.dirName, "", tt.format) + preparedTest := prepareTest(tt.dirName, "", tt.format, tt.exposureFlag) connsRes, peersRes, err := preparedTest.analyzer.ConnlistFromDirPath(preparedTest.dirPath) require.Nil(t, err, tt.name) @@ -514,7 +526,7 @@ func TestConnlistOutputFatalErrors(t *testing.T) { testutils.CheckErrorContainment(t, tt.name, tt.errorStrContains, err.Error()) // re-run the test with new analyzer (to clear the analyzer.errors array ) - preparedTest = prepareTest(tt.dirName, "", tt.format) + preparedTest = prepareTest(tt.dirName, "", tt.format, tt.exposureFlag) infos, _ := fsscanner.GetResourceInfosFromDirPath([]string{preparedTest.dirPath}, true, false) connsRes2, peersRes2, err2 := preparedTest.analyzer.ConnlistFromResourceInfos(infos) @@ -531,9 +543,10 @@ func TestConnlistOutputFatalErrors(t *testing.T) { } var goodPathTests = []struct { - testDirName string - outputFormats []string - focusWorkload string + testDirName string + outputFormats []string + focusWorkload string + exposureAnalysis bool }{ { testDirName: "ipblockstest", @@ -737,4 +750,74 @@ var goodPathTests = []struct { focusWorkload: "ingress-controller", outputFormats: []string{output.TextFormat}, }, + { + testDirName: "acs-security-demos", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_allow_all", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_allow_all_in_cluster", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_allow_egress_deny_ingress", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_allow_ingress_deny_egress", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_matched_and_unmatched_rules", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_only_matched_rules", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_multiple_unmatched_rules", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_new_namespace_conn_and_entire_cluster", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_same_unmatched_rule_in_ingress_egress", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_with_no_netpols", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_egress_to_entire_cluster_with_named_ports", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_ingress_from_entire_cluster_with_named_ports", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, + { + testDirName: "test_egress_exposure_with_named_port", + exposureAnalysis: true, + outputFormats: []string{output.TextFormat}, + }, } diff --git a/pkg/netpol/connlist/conns_formatter.go b/pkg/netpol/connlist/conns_formatter.go index 6ac721ee..ee16c31a 100644 --- a/pkg/netpol/connlist/conns_formatter.go +++ b/pkg/netpol/connlist/conns_formatter.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" + "k8s.io/apimachinery/pkg/labels" + "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common" ) @@ -13,7 +15,7 @@ var newLineChar = fmt.Sprintln("") func sortConnections(conns []Peer2PeerConnection) []singleConnFields { connItems := make([]singleConnFields, len(conns)) for i := range conns { - connItems[i] = formSingleConn(conns[i]) + connItems[i] = formSingleP2PConn(conns[i]) } sort.Slice(connItems, func(i, j int) bool { if connItems[i].Src != connItems[j].Src { @@ -27,7 +29,7 @@ func sortConnections(conns []Peer2PeerConnection) []singleConnFields { // connsFormatter implements output formatting in the required output format type connsFormatter interface { - writeOutput(conns []Peer2PeerConnection) (string, error) + writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (string, error) } // singleConnFields represents a single connection object @@ -42,8 +44,44 @@ func (c singleConnFields) string() string { return fmt.Sprintf("%s => %s : %s", c.Src, c.Dst, c.ConnString) } -// formSingleConn returns a string representation of single connection fields as singleConnFields object -func formSingleConn(conn Peer2PeerConnection) singleConnFields { +// formSingleP2PConn returns a string representation of single connection fields as singleConnFields object +func formSingleP2PConn(conn Peer2PeerConnection) singleConnFields { connStr := common.ConnStrFromConnProperties(conn.AllProtocolsAndPorts(), conn.ProtocolsAndPorts()) return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr} } + +// commonly (to be) used for exposure analysis output formatters + +const entireCluster = "entire-cluster" + +// formSingleExposureConn returns a representation of single exposure connection fields as singleConnFields object +func formSingleExposureConn(peer, repPeer string, conn common.Connection, isIngress bool) singleConnFields { + connStr := conn.(*common.ConnectionSet).String() + if isIngress { + return singleConnFields{Src: repPeer, Dst: peer, ConnString: connStr} + } + return singleConnFields{Src: peer, Dst: repPeer, ConnString: connStr} +} + +// formExposureItemAsSingleConnFiled returns a singleConnFields object for an item in the XgressExposureData list +func formExposureItemAsSingleConnFiled(peerStr string, exposureItem XgressExposureData, isIngress bool) singleConnFields { + if exposureItem.IsExposedToEntireCluster() { + return formSingleExposureConn(peerStr, entireCluster, exposureItem.PotentialConnectivity(), isIngress) + } + if len(exposureItem.NamespaceLabels()) > 0 { + return formSingleExposureConn(peerStr, peerStrWithNsLabels(exposureItem.NamespaceLabels()), + exposureItem.PotentialConnectivity(), isIngress) + } + // @todo handle podLabels + return singleConnFields{} +} + +// convertLabelsMapToString returns a string representation of the given labels map +func convertLabelsMapToString(labelsMap map[string]string) string { + return labels.SelectorFromSet(labels.Set(labelsMap)).String() +} + +// peerStrWithNsLabels returns a string representation of a potential peer with namespace labels +func peerStrWithNsLabels(nsLabels map[string]string) string { + return "namespace with " + convertLabelsMapToString(nsLabels) +} diff --git a/pkg/netpol/connlist/conns_formatter_csv.go b/pkg/netpol/connlist/conns_formatter_csv.go index df82c1bf..5877d0ac 100644 --- a/pkg/netpol/connlist/conns_formatter_csv.go +++ b/pkg/netpol/connlist/conns_formatter_csv.go @@ -10,7 +10,8 @@ type formatCSV struct { } // returns a CSV string form of connections from list of Peer2PeerConnection objects -func (cs formatCSV) writeOutput(conns []Peer2PeerConnection) (string, error) { +// this format is not supported with exposure analysis; exposureConns is not used; +func (cs *formatCSV) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (string, error) { // get an array of sorted conns items ([]singleConnFields) sortedConnItems := sortConnections(conns) var headerCSV = []string{"src", "dst", "conn"} diff --git a/pkg/netpol/connlist/conns_formatter_dot.go b/pkg/netpol/connlist/conns_formatter_dot.go index e2110cf9..fba0ff2f 100644 --- a/pkg/netpol/connlist/conns_formatter_dot.go +++ b/pkg/netpol/connlist/conns_formatter_dot.go @@ -41,7 +41,8 @@ func getPeerLine(peer Peer) (string, bool) { } // returns a dot string form of connections from list of Peer2PeerConnection objects -func (d formatDOT) writeOutput(conns []Peer2PeerConnection) (string, error) { +// this format is not supported with exposure analysis; exposureConns is not used; +func (d *formatDOT) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (string, error) { nsPeers := make(map[string][]string) // map from namespace to its peers (grouping peers by namespaces) externalPeersLines := make([]string, 0) // list of peers which are not in a cluster's namespace (will not be grouped) edgeLines := make([]string, len(conns)) // list of edges lines diff --git a/pkg/netpol/connlist/conns_formatter_json.go b/pkg/netpol/connlist/conns_formatter_json.go index d952b9e6..a0040f74 100644 --- a/pkg/netpol/connlist/conns_formatter_json.go +++ b/pkg/netpol/connlist/conns_formatter_json.go @@ -7,7 +7,8 @@ type formatJSON struct { } // returns a json string form of connections from list of Peer2PeerConnection objects -func (j formatJSON) writeOutput(conns []Peer2PeerConnection) (string, error) { +// this format is not supported with exposure analysis; exposureConns is not used; +func (j *formatJSON) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (string, error) { // get an array of sorted conns items ([]singleConnFields) sortedConnItems := sortConnections(conns) jsonConns, err := json.MarshalIndent(sortedConnItems, "", " ") diff --git a/pkg/netpol/connlist/conns_formatter_md.go b/pkg/netpol/connlist/conns_formatter_md.go index a7097a74..1a10f855 100644 --- a/pkg/netpol/connlist/conns_formatter_md.go +++ b/pkg/netpol/connlist/conns_formatter_md.go @@ -21,10 +21,11 @@ func getMDLine(c singleConnFields) string { } // returns a md string form of connections from list of Peer2PeerConnection objects -func (md formatMD) writeOutput(conns []Peer2PeerConnection) (string, error) { +// this format is not supported with exposure analysis; exposureConns is not used; +func (md *formatMD) writeOutput(conns []Peer2PeerConnection, exposureConns []ExposedPeer) (string, error) { mdLines := make([]string, len(conns)) for index := range conns { - mdLines[index] = getMDLine(formSingleConn(conns[index])) + mdLines[index] = getMDLine(formSingleP2PConn(conns[index])) } sort.Strings(mdLines) allLines := []string{getMDHeader()} diff --git a/pkg/netpol/connlist/conns_formatter_txt.go b/pkg/netpol/connlist/conns_formatter_txt.go index e4519b1e..97af6984 100644 --- a/pkg/netpol/connlist/conns_formatter_txt.go +++ b/pkg/netpol/connlist/conns_formatter_txt.go @@ -1,20 +1,190 @@ 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 type formatText struct { + // connections with IP-peers should appear in both connlist and exposure-analysis output sections + + // PeerToConnsFromIPs map from real peer.String() to its ingress connections from ip-blocks + // extracted from the []Peer2PeerConnection conns to be appended also to the exposure-analysis output + // i.e : if connlist output contains `0.0.0.0-255.255.255.255 => ns1/workload-a : All Connections` + // the PeerToConnsFromIPs will contain following entry: (to be written also in exposure output) + // {ns1/workload-a: []singleConnFields{{src: 0.0.0.0-255.255.255.255, dst: ns1/workload-a, conn: All Connections},}} + PeerToConnsFromIPs map[string][]singleConnFields + + // peerToConnsToIPs map from real peer.String() to its egress connections to ip-blocks + // extracted from the []Peer2PeerConnection conns to be appended also to the exposure-analysis output + peerToConnsToIPs map[string][]singleConnFields } -// returns a textual string format of connections from list of Peer2PeerConnection objects -func (t formatText) writeOutput(conns []Peer2PeerConnection) (string, error) { +// 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) (string, error) { + exposureFlag := len(exposureConns) > 0 + res := t.writeConnlistOutput(conns, exposureFlag) + if !exposureFlag { + return res, nil + } + // else append exposure analysis results: + if res != "" { + res += "\n\n" + } + res += t.writeExposureOutput(exposureConns) + return res, nil +} + +// 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)) + if saveIPConns { + t.peerToConnsToIPs = make(map[string][]singleConnFields) + t.PeerToConnsFromIPs = make(map[string][]singleConnFields) + } for i := range conns { - connLines[i] = formSingleConn(conns[i]).string() + connLines[i] = formSingleP2PConn(conns[i]).string() + // if we have exposure analysis results, also check if src/dst is an IP and store the connection + if saveIPConns { + t.saveConnsWithIPs(conns[i]) + } } sort.Strings(connLines) - return strings.Join(connLines, newLineChar), nil + return strings.Join(connLines, newLineChar) +} + +// 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 (t *formatText) saveConnsWithIPs(conn Peer2PeerConnection) { + if conn.Src().IsPeerIPType() { + t.PeerToConnsFromIPs[conn.Dst().String()] = append(t.PeerToConnsFromIPs[conn.Dst().String()], formSingleP2PConn(conn)) + } + if conn.Dst().IsPeerIPType() { + t.peerToConnsToIPs[conn.Src().String()] = append(t.peerToConnsToIPs[conn.Src().String()], formSingleP2PConn(conn)) + } +} + +const ( + exposureAnalysisHeader = "Exposure Analysis Result:\n" + egressExpHeader = "Egress Exposure:\n" + ingressExpHeader = "\nIngress Exposure:\n" + unprotectedHeader = "\nWorkloads not protected by network policies:\n" +) + +// writeExposureOutput writes the section of the exposure-analysis result +func (t *formatText) writeExposureOutput(exposureResults []ExposedPeer) string { + // sorting the exposed peers slice so we get unique sorted output by Peer.String() + sortedExposureResults := sortExposedPeerSlice(exposureResults) + // getting the max peer String length (to be used for writing fixed indented lines) + maxPeerStrLen := getMaxPeerStringLength(exposureResults) + // results lines + ingressExpLines := make([]string, 0) + egressExpLines := make([]string, 0) + unprotectedLines := make([]string, 0) + for _, ep := range sortedExposureResults { + // ingress and egress lines per peer, internally sorted + pIngressLines, pEgressLines, pUnprotectedLines := t.writePeerExposureAndUnprotectedLines(ep, maxPeerStrLen) + ingressExpLines = append(ingressExpLines, pIngressLines...) + egressExpLines = append(egressExpLines, pEgressLines...) + unprotectedLines = append(unprotectedLines, pUnprotectedLines...) + } + sort.Strings(unprotectedLines) + // results of exposure for all peers + res := exposureAnalysisHeader + res += writeExposureSubSection(egressExpLines, egressExpHeader) + res += writeExposureSubSection(ingressExpLines, ingressExpHeader) + res += writeExposureSubSection(unprotectedLines, unprotectedHeader) + return res +} + +// writePeerExposureAndUnprotectedLines returns exposure lines of given peer; i.e. ingress exposure lines, +// egress exposure lines and unprotected by policies lines +func (t *formatText) writePeerExposureAndUnprotectedLines(ep ExposedPeer, maxPeerStrLen int) (ingressLines, + egressLines, unprotectedLines []string) { + // get ingress lines + ingressLines, ingUnprotected := t.getPeerXgressExposureLines(ep.ExposedPeer().String(), ep.IngressExposure(), + ep.IsProtectedByIngressNetpols(), true, maxPeerStrLen) + // get egress lines + egressLines, egUnprotected := t.getPeerXgressExposureLines(ep.ExposedPeer().String(), ep.EgressExposure(), + ep.IsProtectedByEgressNetpols(), false, maxPeerStrLen) + unprotectedLines = append(unprotectedLines, ingUnprotected...) + unprotectedLines = append(unprotectedLines, egUnprotected...) + return ingressLines, egressLines, unprotectedLines +} + +// writeExposureSubSection if the list is not empty returns it as string lines with the matching given header +func writeExposureSubSection(lines []string, header string) string { + res := "" + if len(lines) > 0 { + res += header + res += strings.Join(lines, newLineChar) + res += newLineChar + } + return res +} + +// getPeerXgressExposureLines returns the peer's exposure data on the given direction ingress/egress arranged in output lines +func (t *formatText) getPeerXgressExposureLines(exposedPeerStr string, xgressExposure []XgressExposureData, + isProtected, isIngress bool, maxLen int) (xgressLines, xgressUnprotectedLine []string) { + direction := "Ingress" + if !isIngress { + direction = "Egress" + } + // if a peer is not protected, two lines are to be added to exposure analysis result: + // 1. all conns with entire cluster (added here) + // 2. all conns with ip-blocks (all destinations); for sure found in the ip conns map so will be added automatically + // also unprotected line will be added + if !isProtected { + xgressLines = append(xgressLines, formSingleExposureConn(exposedPeerStr, entireCluster, + common.MakeConnectionSet(true), isIngress).exposureString(isIngress, maxLen)) + xgressUnprotectedLine = append(xgressUnprotectedLine, exposedPeerStr+" is not protected on "+direction) + } else { // protected + for _, data := range xgressExposure { + // for txt output append the string of the singleConnFields + xgressLines = append(xgressLines, formExposureItemAsSingleConnFiled(exposedPeerStr, data, isIngress).exposureString(isIngress, maxLen)) + } + } + // append xgress ip conns to this peer from the relevant map + ipMap := t.PeerToConnsFromIPs + if !isIngress { + ipMap = t.peerToConnsToIPs + } + if ipConns, ok := ipMap[exposedPeerStr]; ok { + for i := range ipConns { + connLine := ipConns[i].exposureString(isIngress, maxLen) + xgressLines = append(xgressLines, connLine) + } + } + sort.Strings(xgressLines) + return xgressLines, xgressUnprotectedLine +} + +// sortExposedPeerSlice returns sorted ExposedPeer list +func sortExposedPeerSlice(exposedPeers []ExposedPeer) (ep []ExposedPeer) { + sort.Slice(exposedPeers, func(i, j int) bool { + return exposedPeers[i].ExposedPeer().String() < exposedPeers[j].ExposedPeer().String() + }) + return exposedPeers +} + +// getMaxPeerStringLength returns the length of the longest peer string in the given exposed peers slice +func getMaxPeerStringLength(exposedPeers []ExposedPeer) (maxPeerStrLen int) { + for i := range exposedPeers { + maxPeerStrLen = max(maxPeerStrLen, len(exposedPeers[i].ExposedPeer().String())) + } + return maxPeerStrLen +} + +// exposureString writes the current singleConnFields in the format of exposure result line +func (c singleConnFields) exposureString(isIngress bool, maxStrLen int) string { + formatStr := fmt.Sprintf("%%-%ds \t%%s \t%%s : %%s", maxStrLen) + if isIngress { + return fmt.Sprintf(formatStr, c.Dst, "<=", c.Src, c.ConnString) + } + return fmt.Sprintf(formatStr, c.Src, "=>", c.Dst, c.ConnString) } diff --git a/pkg/netpol/eval/exposure.go b/pkg/netpol/eval/exposure.go index 0278c78b..3c23091b 100644 --- a/pkg/netpol/eval/exposure.go +++ b/pkg/netpol/eval/exposure.go @@ -50,6 +50,22 @@ func (pe *PolicyEngine) generateRepresentativePeers(selectorsLabels []k8s.Single return nil } +// extractLabelsAndRefineRepresentativePeers extracts the labels of the given pod object and its namespace and refine matching peers +// helping func - added in order to avoid code dup. in upsertWorkload and upsertPod +func (pe *PolicyEngine) extractLabelsAndRefineRepresentativePeers(podObj *k8s.Pod) error { + // since namespaces are already upserted; if pod's ns not existing resolve it + if _, ok := pe.namspacesMap[podObj.Namespace]; !ok { + // the "kubernetes.io/metadata.name" is added automatically to the ns; so representative peers with such selector will be refined + err := pe.resolveSingleMissingNamespace(podObj.Namespace, nil) + if err != nil { + return err + } + } + // check if there are representative peers in the policy engine which match the current pod; if yes remove them + pe.refineRepresentativePeersMatchingLabels(podObj.Labels, pe.namspacesMap[podObj.Namespace].Labels) + return nil +} + // refineRepresentativePeersMatchingLabels removes from the policy engine all representative peers // with labels matching the given labels of a real pod func (pe *PolicyEngine) refineRepresentativePeersMatchingLabels(realPodLabels, realNsLabels map[string]string) { diff --git a/pkg/netpol/eval/resources.go b/pkg/netpol/eval/resources.go index 184aa438..d2c8b9fe 100644 --- a/pkg/netpol/eval/resources.go +++ b/pkg/netpol/eval/resources.go @@ -75,31 +75,37 @@ func NewPolicyEngineWithOptions(exposureFlag bool) *PolicyEngine { return pe } -// AddObjects adds k8s objects to the policy engine: first adds network-policies and then other objects +// AddObjects adds k8s objects to the policy engine: first adds network-policies and namespaces and then other objects // called only for exposure analysis; otherwise does nothing +// for exposure analysis we need to upsert first policies and namespaces so: +// 1. policies: so representative peer for each policy rule is added +// 2. namespaces: so when upserting workloads, we'll be able to refine correctly representativePeers with +// namespace name/ labels similar to those belonging the workloads' namespace func (pe *PolicyEngine) AddObjects(objects []parser.K8sObject) error { if !pe.exposureAnalysisFlag { // should not be true ever return nil } - policies, nonPolicies := splitPoliciesAndOtherObjects(objects) - err := pe.addObjectsByKind(policies) + policiesAndNamespaces, otherObjects := splitPoliciesAndNamespacesAndOtherObjects(objects) + err := pe.addObjectsByKind(policiesAndNamespaces) if err != nil { return err } - err = pe.addObjectsByKind(nonPolicies) + err = pe.addObjectsByKind(otherObjects) return err } -func splitPoliciesAndOtherObjects(objects []parser.K8sObject) (policies, nonPolicies []parser.K8sObject) { +func splitPoliciesAndNamespacesAndOtherObjects(objects []parser.K8sObject) (policiesAndNs, others []parser.K8sObject) { for _, obj := range objects { switch obj.Kind { case parser.Networkpolicy: - policies = append(policies, obj) + policiesAndNs = append(policiesAndNs, obj) + case parser.Namespace: + policiesAndNs = append(policiesAndNs, obj) default: - nonPolicies = append(nonPolicies, obj) + others = append(others, obj) } } - return policies, nonPolicies + return policiesAndNs, others } // addObjectsByKind adds different k8s objects from parsed resources to the policy engine @@ -136,7 +142,10 @@ func (pe *PolicyEngine) addObjectsByKind(objects []parser.K8sObject) error { return err } } - return pe.resolveMissingNamespaces() + if !pe.exposureAnalysisFlag { // for exposure analysis; this already done + return pe.resolveMissingNamespaces() + } + return nil } func (pe *PolicyEngine) resolveMissingNamespaces() error { @@ -341,10 +350,9 @@ func (pe *PolicyEngine) upsertWorkload(rs interface{}, kind string) error { } // running this on last podObj: as all pods from same workload object are in same namespace and having same pod labels if pe.exposureAnalysisFlag { - // check if there are representative peers in the policy engine which match the current pod; if yes remove them - pe.refineRepresentativePeersMatchingLabels(podObj.Labels, pe.namspacesMap[podObj.Namespace].Labels) + err = pe.extractLabelsAndRefineRepresentativePeers(podObj) } - return nil + return err } func (pe *PolicyEngine) upsertPod(pod *corev1.Pod) error { @@ -357,10 +365,9 @@ func (pe *PolicyEngine) upsertPod(pod *corev1.Pod) error { // update cache with new pod associated to to its owner pe.cache.addPod(podObj, podStr.String()) if pe.exposureAnalysisFlag { - // check if there are representative peers in the policy engine which match the current pod; if yes remove them - pe.refineRepresentativePeersMatchingLabels(podObj.Labels, pe.namspacesMap[podObj.Namespace].Labels) + err = pe.extractLabelsAndRefineRepresentativePeers(podObj) } - return nil + return err } func initPolicyGeneralConns() k8s.PolicyGeneralRulesConns { diff --git a/test_outputs/cli/exposure_acs-security-demos_connlist_output.txt b/test_outputs/cli/exposure_acs-security-demos_connlist_output.txt new file mode 100644 index 00000000..2d9cb286 --- /dev/null +++ b/test_outputs/cli/exposure_acs-security-demos_connlist_output.txt @@ -0,0 +1,26 @@ +backend/checkout[Deployment] => backend/notification[Deployment] : TCP 8080 +backend/checkout[Deployment] => backend/recommendation[Deployment] : TCP 8080 +backend/checkout[Deployment] => payments/gateway[Deployment] : TCP 8080 +backend/recommendation[Deployment] => backend/catalog[Deployment] : TCP 8080 +backend/reports[Deployment] => backend/catalog[Deployment] : TCP 8080 +backend/reports[Deployment] => backend/recommendation[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/checkout[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/recommendation[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/reports[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/shipping[Deployment] : TCP 8080 +payments/gateway[Deployment] => payments/mastercard-processor[Deployment] : TCP 8080 +payments/gateway[Deployment] => payments/visa-processor[Deployment] : TCP 8080 +{ingress-controller} => frontend/asset-cache[Deployment] : TCP 8080 +{ingress-controller} => frontend/webapp[Deployment] : TCP 8080 + +Exposure Analysis Result: +Egress Exposure: +backend/checkout[Deployment] => entire-cluster : UDP 5353 +backend/recommendation[Deployment] => entire-cluster : UDP 5353 +backend/reports[Deployment] => entire-cluster : UDP 5353 +frontend/webapp[Deployment] => entire-cluster : UDP 5353 +payments/gateway[Deployment] => entire-cluster : UDP 5353 + +Ingress Exposure: +frontend/asset-cache[Deployment] <= entire-cluster : TCP 8080 +frontend/webapp[Deployment] <= entire-cluster : TCP 8080 diff --git a/test_outputs/connlist/exposure_acs-security-demos_connlist_output.txt b/test_outputs/connlist/exposure_acs-security-demos_connlist_output.txt new file mode 100644 index 00000000..2d9cb286 --- /dev/null +++ b/test_outputs/connlist/exposure_acs-security-demos_connlist_output.txt @@ -0,0 +1,26 @@ +backend/checkout[Deployment] => backend/notification[Deployment] : TCP 8080 +backend/checkout[Deployment] => backend/recommendation[Deployment] : TCP 8080 +backend/checkout[Deployment] => payments/gateway[Deployment] : TCP 8080 +backend/recommendation[Deployment] => backend/catalog[Deployment] : TCP 8080 +backend/reports[Deployment] => backend/catalog[Deployment] : TCP 8080 +backend/reports[Deployment] => backend/recommendation[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/checkout[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/recommendation[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/reports[Deployment] : TCP 8080 +frontend/webapp[Deployment] => backend/shipping[Deployment] : TCP 8080 +payments/gateway[Deployment] => payments/mastercard-processor[Deployment] : TCP 8080 +payments/gateway[Deployment] => payments/visa-processor[Deployment] : TCP 8080 +{ingress-controller} => frontend/asset-cache[Deployment] : TCP 8080 +{ingress-controller} => frontend/webapp[Deployment] : TCP 8080 + +Exposure Analysis Result: +Egress Exposure: +backend/checkout[Deployment] => entire-cluster : UDP 5353 +backend/recommendation[Deployment] => entire-cluster : UDP 5353 +backend/reports[Deployment] => entire-cluster : UDP 5353 +frontend/webapp[Deployment] => entire-cluster : UDP 5353 +payments/gateway[Deployment] => entire-cluster : UDP 5353 + +Ingress Exposure: +frontend/asset-cache[Deployment] <= entire-cluster : TCP 8080 +frontend/webapp[Deployment] <= entire-cluster : TCP 8080 diff --git a/test_outputs/connlist/exposure_test_allow_all_connlist_output.txt b/test_outputs/connlist/exposure_test_allow_all_connlist_output.txt new file mode 100644 index 00000000..afbb7266 --- /dev/null +++ b/test_outputs/connlist/exposure_test_allow_all_connlist_output.txt @@ -0,0 +1,11 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-a[Deployment] : All Connections +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] <= entire-cluster : All Connections diff --git a/test_outputs/connlist/exposure_test_allow_all_in_cluster_connlist_output.txt b/test_outputs/connlist/exposure_test_allow_all_in_cluster_connlist_output.txt new file mode 100644 index 00000000..7d8326ae --- /dev/null +++ b/test_outputs/connlist/exposure_test_allow_all_in_cluster_connlist_output.txt @@ -0,0 +1,6 @@ +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8050 diff --git a/test_outputs/connlist/exposure_test_allow_egress_deny_ingress_connlist_output.txt b/test_outputs/connlist/exposure_test_allow_egress_deny_ingress_connlist_output.txt new file mode 100644 index 00000000..32e44580 --- /dev/null +++ b/test_outputs/connlist/exposure_test_allow_egress_deny_ingress_connlist_output.txt @@ -0,0 +1,6 @@ +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections diff --git a/test_outputs/connlist/exposure_test_allow_ingress_deny_egress_connlist_output.txt b/test_outputs/connlist/exposure_test_allow_ingress_deny_egress_connlist_output.txt new file mode 100644 index 00000000..aae4107a --- /dev/null +++ b/test_outputs/connlist/exposure_test_allow_ingress_deny_egress_connlist_output.txt @@ -0,0 +1,7 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-a[Deployment] : All Connections + +Exposure Analysis Result: + +Ingress Exposure: +hello-world/workload-a[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] <= entire-cluster : All Connections diff --git a/test_outputs/connlist/exposure_test_egress_exposure_with_named_port_connlist_output.txt b/test_outputs/connlist/exposure_test_egress_exposure_with_named_port_connlist_output.txt new file mode 100644 index 00000000..2212598f --- /dev/null +++ b/test_outputs/connlist/exposure_test_egress_exposure_with_named_port_connlist_output.txt @@ -0,0 +1,6 @@ +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => namespace with foo.com/managed-state=managed : TCP http + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8000 diff --git a/test_outputs/connlist/exposure_test_egress_to_entire_cluster_with_named_ports_connlist_output.txt b/test_outputs/connlist/exposure_test_egress_to_entire_cluster_with_named_ports_connlist_output.txt new file mode 100644 index 00000000..8450be4d --- /dev/null +++ b/test_outputs/connlist/exposure_test_egress_to_entire_cluster_with_named_ports_connlist_output.txt @@ -0,0 +1,12 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-a[Deployment] : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => entire-cluster : TCP http,local-dns + +Ingress Exposure: +hello-world/workload-a[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] <= entire-cluster : All Connections + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Ingress diff --git a/test_outputs/connlist/exposure_test_ingress_from_entire_cluster_with_named_ports_connlist_output.txt b/test_outputs/connlist/exposure_test_ingress_from_entire_cluster_with_named_ports_connlist_output.txt new file mode 100644 index 00000000..9854c8e8 --- /dev/null +++ b/test_outputs/connlist/exposure_test_ingress_from_entire_cluster_with_named_ports_connlist_output.txt @@ -0,0 +1,12 @@ +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8000,8090 + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Egress diff --git a/test_outputs/connlist/exposure_test_matched_and_unmatched_rules_connlist_output.txt b/test_outputs/connlist/exposure_test_matched_and_unmatched_rules_connlist_output.txt new file mode 100644 index 00000000..08bf1c78 --- /dev/null +++ b/test_outputs/connlist/exposure_test_matched_and_unmatched_rules_connlist_output.txt @@ -0,0 +1,22 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => hello-world/workload-a[Deployment] : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8050 +hello-world/workload-b[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] <= entire-cluster : All Connections + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Egress +hello-world/workload-b[Deployment] is not protected on Egress +hello-world/workload-b[Deployment] is not protected on Ingress diff --git a/test_outputs/connlist/exposure_test_multiple_unmatched_rules_connlist_output.txt b/test_outputs/connlist/exposure_test_multiple_unmatched_rules_connlist_output.txt new file mode 100644 index 00000000..1b0e528d --- /dev/null +++ b/test_outputs/connlist/exposure_test_multiple_unmatched_rules_connlist_output.txt @@ -0,0 +1,14 @@ +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= namespace with effect=NoSchedule : TCP 8050 +hello-world/workload-a[Deployment] <= namespace with foo.com/managed-state=managed : TCP 8050 +hello-world/workload-a[Deployment] <= namespace with release=stable : All Connections + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Egress diff --git a/test_outputs/connlist/exposure_test_new_namespace_conn_and_entire_cluster_connlist_output.txt b/test_outputs/connlist/exposure_test_new_namespace_conn_and_entire_cluster_connlist_output.txt new file mode 100644 index 00000000..54e72814 --- /dev/null +++ b/test_outputs/connlist/exposure_test_new_namespace_conn_and_entire_cluster_connlist_output.txt @@ -0,0 +1,23 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => hello-world/workload-a[Deployment] : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8050 +hello-world/workload-a[Deployment] <= namespace with foo.com/managed-state=managed : TCP 8050,8090 +hello-world/workload-b[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] <= entire-cluster : All Connections + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Egress +hello-world/workload-b[Deployment] is not protected on Egress +hello-world/workload-b[Deployment] is not protected on Ingress diff --git a/test_outputs/connlist/exposure_test_only_matched_rules_connlist_output.txt b/test_outputs/connlist/exposure_test_only_matched_rules_connlist_output.txt new file mode 100644 index 00000000..41679dd8 --- /dev/null +++ b/test_outputs/connlist/exposure_test_only_matched_rules_connlist_output.txt @@ -0,0 +1,17 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-a[Deployment] => hello-world/workload-b[Deployment] : All Connections +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => hello-world/workload-a[Deployment] : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-b[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-b[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-b[Deployment] <= entire-cluster : All Connections + +Workloads not protected by network policies: +hello-world/workload-b[Deployment] is not protected on Egress +hello-world/workload-b[Deployment] is not protected on Ingress diff --git a/test_outputs/connlist/exposure_test_same_unmatched_rule_in_ingress_egress_connlist_output.txt b/test_outputs/connlist/exposure_test_same_unmatched_rule_in_ingress_egress_connlist_output.txt new file mode 100644 index 00000000..731ce7c5 --- /dev/null +++ b/test_outputs/connlist/exposure_test_same_unmatched_rule_in_ingress_egress_connlist_output.txt @@ -0,0 +1,7 @@ +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => namespace with foo.com/managed-state=managed : TCP 8050 + +Ingress Exposure: +hello-world/workload-a[Deployment] <= entire-cluster : TCP 8000 +hello-world/workload-a[Deployment] <= namespace with foo.com/managed-state=managed : TCP 8000,8090 diff --git a/test_outputs/connlist/exposure_test_with_no_netpols_connlist_output.txt b/test_outputs/connlist/exposure_test_with_no_netpols_connlist_output.txt new file mode 100644 index 00000000..b69587ed --- /dev/null +++ b/test_outputs/connlist/exposure_test_with_no_netpols_connlist_output.txt @@ -0,0 +1,15 @@ +0.0.0.0-255.255.255.255 => hello-world/workload-a[Deployment] : All Connections +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections + +Exposure Analysis Result: +Egress Exposure: +hello-world/workload-a[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] => entire-cluster : All Connections + +Ingress Exposure: +hello-world/workload-a[Deployment] <= 0.0.0.0-255.255.255.255 : All Connections +hello-world/workload-a[Deployment] <= entire-cluster : All Connections + +Workloads not protected by network policies: +hello-world/workload-a[Deployment] is not protected on Egress +hello-world/workload-a[Deployment] is not protected on Ingress diff --git a/tests/test_egress_exposure_with_named_port/namespace_and_deployments.yaml b/tests/test_egress_exposure_with_named_port/namespace_and_deployments.yaml new file mode 100644 index 00000000..9960c423 --- /dev/null +++ b/tests/test_egress_exposure_with_named_port/namespace_and_deployments.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + replicas: 2 + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - name: local-dns + containerPort: 8000 # containerport1 + - name: local-dns2 + containerPort: 8050 # containerport2 + - name: http + containerPort: 8090 # containerport3 +--- \ No newline at end of file diff --git a/tests/test_egress_exposure_with_named_port/netpol.yaml b/tests/test_egress_exposure_with_named_port/netpol.yaml new file mode 100644 index 00000000..5dbb93a7 --- /dev/null +++ b/tests/test_egress_exposure_with_named_port/netpol.yaml @@ -0,0 +1,26 @@ +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: egress-with-named-port + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + ingress: + - from: + - namespaceSelector: {} + ports: + - port: local-dns + protocol: TCP + egress: + - to: + - namespaceSelector: + matchExpressions: + - key: foo.com/managed-state + operator: In + values: + - managed + ports: + - port: http + protocol: TCP \ No newline at end of file diff --git a/tests/test_egress_to_entire_cluster_with_named_ports/namespace_and_deployments.yaml b/tests/test_egress_to_entire_cluster_with_named_ports/namespace_and_deployments.yaml new file mode 100644 index 00000000..9960c423 --- /dev/null +++ b/tests/test_egress_to_entire_cluster_with_named_ports/namespace_and_deployments.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + replicas: 2 + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - name: local-dns + containerPort: 8000 # containerport1 + - name: local-dns2 + containerPort: 8050 # containerport2 + - name: http + containerPort: 8090 # containerport3 +--- \ No newline at end of file diff --git a/tests/test_egress_to_entire_cluster_with_named_ports/policy.yaml b/tests/test_egress_to_entire_cluster_with_named_ports/policy.yaml new file mode 100644 index 00000000..42351cb6 --- /dev/null +++ b/tests/test_egress_to_entire_cluster_with_named_ports/policy.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: egress-based-on-named-ports + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: {} + ports: + - port: local-dns + - port: http \ No newline at end of file diff --git a/tests/test_ingress_from_entire_cluster_with_named_ports/namespace_and_deployments.yaml b/tests/test_ingress_from_entire_cluster_with_named_ports/namespace_and_deployments.yaml new file mode 100644 index 00000000..b7f10e5a --- /dev/null +++ b/tests/test_ingress_from_entire_cluster_with_named_ports/namespace_and_deployments.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: hello-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-a + namespace: hello-world + labels: + app: a-app +spec: + selector: + matchLabels: + app: a-app + template: + metadata: + labels: + app: a-app + spec: + containers: + - name: hello-world + image: quay.io/shfa/hello-world:latest + ports: + - name: local-dns + containerPort: 8000 # containerport1 + - name: local-dns2 + containerPort: 8050 # containerport2 + - name: http + containerPort: 8090 # containerport3 +--- diff --git a/tests/test_ingress_from_entire_cluster_with_named_ports/policy.yaml b/tests/test_ingress_from_entire_cluster_with_named_ports/policy.yaml new file mode 100644 index 00000000..4175d1b2 --- /dev/null +++ b/tests/test_ingress_from_entire_cluster_with_named_ports/policy.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ingress-based-on-named-ports + namespace: hello-world +spec: + podSelector: + matchLabels: + app: a-app + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: {} + ports: + - port: local-dns + - port: http