diff --git a/.golangci.yml b/.golangci.yml index db387f4f..71904909 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -105,6 +105,7 @@ issues: - revive - goconst - funlen + - errcheck run: timeout: 5m \ No newline at end of file diff --git a/README.md b/README.md index bb9fb185..97e36b7b 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Global Flags: ### List command ``` -Lists all allowed connections based on the workloads and network policies defined. +Lists all allowed connections based on the workloads, network policies, and Ingress/Route resources defined. + Connections between workload to itself are excluded from the output. Usage: @@ -102,6 +103,8 @@ default/redis-cart[Deployment] => 0.0.0.0-255.255.255.255 : All Connections ``` +Additional details about the connectivity analysis and its output is specified [here](docs/connlist_output.md). + ## Build the project Make sure you have golang 1.19+ on your platform diff --git a/cmd/netpolicy/cmd/list.go b/cmd/netpolicy/cmd/list.go index 72fc3f63..2d5907e6 100644 --- a/cmd/netpolicy/cmd/list.go +++ b/cmd/netpolicy/cmd/list.go @@ -59,7 +59,7 @@ func newCommandList() *cobra.Command { c := &cobra.Command{ Use: "list", Short: "Lists all allowed connections", - Long: `Lists all allowed connections based on the workloads and network policies + Long: `Lists all allowed connections based on the workloads, network policies, and Ingress/Route resources defined`, Example: ` # Get list of allowed connections from resources dir path k8snetpolicy list --dirpath ./resources_dir/ diff --git a/docs/connlist_output.md b/docs/connlist_output.md new file mode 100644 index 00000000..2b5cfa83 --- /dev/null +++ b/docs/connlist_output.md @@ -0,0 +1,16 @@ +# List command - connectivity analysis output + +Resource manifests considered for a connectivity analysis: +- workload resources (such as Kubernetes Pod / Deployment) +- Kubernetes NetworkPolicy +- Kubernetes Ingress +- Openshift Route + +The connectivity output consists of lines of the form: `src` => `dst` : `connections` + +For connections inferred from network policy resources only, the `src` and `dst` are workloads or external IP-blocks. + +For Ingress/Route analysis, the `src` is specified as `{ingress-controller}`, representing the cluster's ingress controller Pod. +Its connectivity lines are of the form: `{ingress-controller}` => `dst` : `connections`, where `dst` is a workload in the cluster. +This analysis is currently activated only with `--dir-path` flag, and not on a live cluster. +It assumes that the ingress controller Pod is unknown, and thus using this notation of `{ingress-controller}`. diff --git a/pkg/netpol/eval/internal/k8s/CanonicalIntervalSet.go b/pkg/netpol/common/CanonicalIntervalSet.go similarity index 99% rename from pkg/netpol/eval/internal/k8s/CanonicalIntervalSet.go rename to pkg/netpol/common/CanonicalIntervalSet.go index 27ff7cd0..0b9d99e0 100644 --- a/pkg/netpol/eval/internal/k8s/CanonicalIntervalSet.go +++ b/pkg/netpol/common/CanonicalIntervalSet.go @@ -1,4 +1,4 @@ -package k8s +package common import ( "fmt" diff --git a/pkg/netpol/common/connection.go b/pkg/netpol/common/connection.go new file mode 100644 index 00000000..dcc00ce4 --- /dev/null +++ b/pkg/netpol/common/connection.go @@ -0,0 +1,26 @@ +package common + +import ( + v1 "k8s.io/api/core/v1" +) + +// 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 + // AllConnections returns true if all ports are allowed for all protocols + AllConnections() bool + // IsEmpty returns true if no connection is allowed + IsEmpty() bool +} + +// PortRange describes a port or a range of ports for allowed traffic +// If start port equals end port, it represents a single port +type PortRange interface { + // Start is the start port + Start() int64 + // End is the end port + End() int64 + // String returns a string representation of the PortRange object + String() string +} diff --git a/pkg/netpol/eval/internal/k8s/connectionset.go b/pkg/netpol/common/connectionset.go similarity index 86% rename from pkg/netpol/eval/internal/k8s/connectionset.go rename to pkg/netpol/common/connectionset.go index 172ebc1f..f00ffcc5 100644 --- a/pkg/netpol/eval/internal/k8s/connectionset.go +++ b/pkg/netpol/common/connectionset.go @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -package k8s +package common import ( "fmt" @@ -23,21 +23,22 @@ import ( ) // ConnectionSet represents a set of allowed connections between two peers on a k8s env +// and implements Connection interface type ConnectionSet struct { AllowAll bool AllowedProtocols map[v1.Protocol]*PortSet // map from protocol name to set of allowed ports } -// MakeConnectionSet returns a ConnectionSet object with all connections or no connections -func MakeConnectionSet(all bool) ConnectionSet { +// 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{}} } - return ConnectionSet{AllowedProtocols: map[v1.Protocol]*PortSet{}} + return &ConnectionSet{AllowedProtocols: map[v1.Protocol]*PortSet{}} } // Intersection updates ConnectionSet object to be the intersection result with other ConnectionSet -func (conn *ConnectionSet) Intersection(other ConnectionSet) { +func (conn *ConnectionSet) Intersection(other *ConnectionSet) { if other.AllowAll { return } @@ -92,7 +93,7 @@ func (conn *ConnectionSet) checkIfAllConnections() { } // Union updates ConnectionSet object to be the union result with other ConnectionSet -func (conn *ConnectionSet) Union(other ConnectionSet) { +func (conn *ConnectionSet) Union(other *ConnectionSet) { if conn.AllowAll || other.IsEmpty() { return } @@ -133,7 +134,7 @@ func (conn *ConnectionSet) Contains(port, protocol string) bool { } // ContainedIn returns true if current ConnectionSet is conatained in the input ConnectionSet object -func (conn *ConnectionSet) ContainedIn(other ConnectionSet) bool { +func (conn *ConnectionSet) ContainedIn(other *ConnectionSet) bool { if other.AllowAll { return true } @@ -181,7 +182,7 @@ func (conn *ConnectionSet) String() string { } // Equal returns true if the current ConnectionSet object is equal to the input object -func (conn *ConnectionSet) Equal(other ConnectionSet) bool { +func (conn *ConnectionSet) Equal(other *ConnectionSet) bool { if conn.AllowAll != other.AllowAll { return false } @@ -200,7 +201,7 @@ func (conn *ConnectionSet) Equal(other ConnectionSet) bool { return true } -// portRange implements the eval.PortRange interface +// portRange implements the PortRange interface type portRange struct { start int64 end int64 @@ -222,10 +223,10 @@ func (p *portRange) String() string { } // ProtocolsAndPortsMap() returns a map from allowed protocol to list of allowed ports ranges. -func (conn *ConnectionSet) ProtocolsAndPortsMap() map[v1.Protocol][]*portRange { - res := map[v1.Protocol][]*portRange{} +func (conn *ConnectionSet) ProtocolsAndPortsMap() map[v1.Protocol][]PortRange { + res := make(map[v1.Protocol][]PortRange, 0) for protocol, portSet := range conn.AllowedProtocols { - res[protocol] = []*portRange{} + res[protocol] = make([]PortRange, 0) // TODO: consider leave the slice of ports empty if portSet covers the full range for i := range portSet.Ports.IntervalSet { startPort := portSet.Ports.IntervalSet[i].Start @@ -236,3 +237,8 @@ func (conn *ConnectionSet) ProtocolsAndPortsMap() map[v1.Protocol][]*portRange { } return res } + +// AllConnections returns true if all ports are allowed for all protocols +func (conn *ConnectionSet) AllConnections() bool { + return conn.AllowAll +} diff --git a/pkg/netpol/eval/internal/k8s/intervals_test.go b/pkg/netpol/common/intervals_test.go similarity index 99% rename from pkg/netpol/eval/internal/k8s/intervals_test.go rename to pkg/netpol/common/intervals_test.go index 2296e89c..ccab490e 100644 --- a/pkg/netpol/eval/internal/k8s/intervals_test.go +++ b/pkg/netpol/common/intervals_test.go @@ -1,4 +1,4 @@ -package k8s +package common import ( "testing" diff --git a/pkg/netpol/eval/internal/k8s/portset.go b/pkg/netpol/common/portset.go similarity index 99% rename from pkg/netpol/eval/internal/k8s/portset.go rename to pkg/netpol/common/portset.go index 4927345b..0647c8b3 100644 --- a/pkg/netpol/eval/internal/k8s/portset.go +++ b/pkg/netpol/common/portset.go @@ -1,4 +1,4 @@ -package k8s +package common import ( "reflect" diff --git a/pkg/netpol/connlist/connlist.go b/pkg/netpol/connlist/connlist.go index 00b606b4..227ce1af 100644 --- a/pkg/netpol/connlist/connlist.go +++ b/pkg/netpol/connlist/connlist.go @@ -1,6 +1,8 @@ -// The connlist package of netpol-analyzer allows producing a k8s connectivity report based on network policies. +// The connlist package of netpol-analyzer allows producing a k8s connectivity report based on several resources: +// k8s NetworkPolicy, k8s Ingress, openshift Route // It lists the set of allowed connections between each pair of different peers (k8s workloads or ip-blocks). // Connections between workload to itself are excluded from the output. +// Connectivity inferred from Ingress/Route resources is between {ingress-controller} to k8s workloads. // The resources can be extracted from a directory containing YAML manifests, or from a k8s cluster. // For more information, see https://github.com/np-guard/netpol-analyzer. package connlist @@ -19,6 +21,8 @@ import ( v1 "k8s.io/api/core/v1" + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" + "github.com/np-guard/netpol-analyzer/pkg/netpol/connlist/internal/ingressanalyzer" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" @@ -179,7 +183,12 @@ func (ca *ConnlistAnalyzer) connslistFromParsedResources(objectsList []scan.K8sO ca.errors = append(ca.errors, newResourceEvaluationError(err)) return nil, err } - return ca.getConnectionsList(pe) + ia, err := ingressanalyzer.NewIngressAnalyzerWithObjects(objectsList, pe, ca.logger) + if err != nil { + ca.errors = append(ca.errors, newResourceEvaluationError(err)) + return nil, err + } + return ca.getConnectionsList(pe, ia) } // ConnlistFromK8sCluster returns the allowed connections list from k8s cluster resources @@ -224,7 +233,7 @@ func (ca *ConnlistAnalyzer) ConnlistFromK8sCluster(clientset *kubernetes.Clients return nil, err } } - return ca.getConnectionsList(pe) + return ca.getConnectionsList(pe, nil) } // ConnectionsListToString returns a string of connections from list of Peer2PeerConnection objects in the required output format @@ -284,7 +293,7 @@ type Peer2PeerConnection interface { // AllProtocolsAndPorts returns true if all ports are allowed for all protocols AllProtocolsAndPorts() bool // ProtocolsAndPorts returns the set of allowed connections - ProtocolsAndPorts() map[v1.Protocol][]eval.PortRange + ProtocolsAndPorts() map[v1.Protocol][]common.PortRange } ////////////////////////////////////////////////////////////////////////////////////////////// @@ -300,7 +309,7 @@ type connection struct { src eval.Peer dst eval.Peer allConnections bool - protocolsAndPorts map[v1.Protocol][]eval.PortRange + protocolsAndPorts map[v1.Protocol][]common.PortRange } func (c *connection) Src() eval.Peer { @@ -312,7 +321,7 @@ func (c *connection) Dst() eval.Peer { func (c *connection) AllProtocolsAndPorts() bool { return c.allConnections } -func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]eval.PortRange { +func (c *connection) ProtocolsAndPorts() map[v1.Protocol][]common.PortRange { return c.protocolsAndPorts } @@ -360,13 +369,42 @@ func (ca *ConnlistAnalyzer) includePairOfWorkloads(src, dst eval.Peer) bool { return false } -// getConnectionsList returns connections list from PolicyEngine object -func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine) ([]Peer2PeerConnection, error) { +// getConnectionsList returns connections list from PolicyEngine and ingressAnalyzer objects +func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine, ia *ingressanalyzer.IngressAnalyzer) ([]Peer2PeerConnection, error) { res := make([]Peer2PeerConnection, 0) if !pe.HasPodPeers() { return res, nil } + // compute connections between peers based on pe analysis of network policies + peersAllowedConns, err := ca.getConnectionsBetweenPeers(pe) + if err != nil { + ca.errors = append(ca.errors, newResourceEvaluationError(err)) + return nil, err + } + res = peersAllowedConns + + if ia == nil || ia.IsEmpty() { + return res, nil + } + + // analyze ingress connections - create connection objects for relevant ingress analyzer connections + ingressAllowedConns, err := ca.getIngressAllowedConnections(ia, pe) + if err != nil { + ca.errors = append(ca.errors, newResourceEvaluationError(err)) + return nil, err + } + res = append(res, ingressAllowedConns...) + + if len(peersAllowedConns) == 0 { + ca.logger.Warnf("connectivity analysis found no allowed connectivity between pairs from the configured workloads or external IP-blocks") + } + + return res, nil +} + +// getConnectionsList returns connections list from PolicyEngine object +func (ca *ConnlistAnalyzer) getConnectionsBetweenPeers(pe *eval.PolicyEngine) ([]Peer2PeerConnection, error) { // get workload peers and ip blocks peerList, err := pe.GetPeersList() if err != nil { @@ -374,6 +412,7 @@ func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine) ([]Peer2Pe return nil, err } + res := make([]Peer2PeerConnection, 0) for i := range peerList { for j := range peerList { srcPeer := peerList[i] @@ -383,30 +422,82 @@ func (ca *ConnlistAnalyzer) getConnectionsList(pe *eval.PolicyEngine) ([]Peer2Pe } allowedConnections, err := pe.AllAllowedConnectionsBetweenWorkloadPeers(srcPeer, dstPeer) if err != nil { - ca.errors = append(ca.errors, newResourceEvaluationError(err)) return nil, err } // skip empty connections if allowedConnections.IsEmpty() { continue } - connectionObj := &connection{ + p2pConnection := &connection{ src: srcPeer, dst: dstPeer, allConnections: allowedConnections.AllConnections(), protocolsAndPorts: allowedConnections.ProtocolsAndPortsMap(), } - res = append(res, connectionObj) + res = append(res, p2pConnection) + } + } + return res, nil +} + +// getIngressAllowedConnections returns connections list from IngressAnalyzer intersected with PolicyEngine's connections +func (ca *ConnlistAnalyzer) getIngressAllowedConnections(ia *ingressanalyzer.IngressAnalyzer, + pe *eval.PolicyEngine) ([]Peer2PeerConnection, error) { + res := make([]Peer2PeerConnection, 0) + ingressConns, err := ia.AllowedIngressConnections() + if err != nil { + return nil, err + } + // adding the ingress controller pod to the policy engine, + ingressControllerPod, err := pe.AddPodByNameAndNamespace(ingressanalyzer.IngressPodName, ingressanalyzer.IngressPodNamespace) + if err != nil { + return nil, err + } + for peerStr, peerAndConn := range ingressConns { + // compute allowed connections based on pe.policies to the peer, then intersect the conns with + // ingress connections to the peer -> the intersection will be appended to the result + peConn, err := pe.AllAllowedConnectionsBetweenWorkloadPeers(ingressControllerPod, peerAndConn.Peer) + if err != nil { + return nil, err + } + peerAndConn.ConnSet.Intersection(peConn.(*common.ConnectionSet)) + if peerAndConn.ConnSet.IsEmpty() { + ca.warnBlockedIngress(peerStr, peerAndConn.IngressObjects) + continue + } + p2pConnection := &connection{ + src: ingressControllerPod, + dst: peerAndConn.Peer, + allConnections: peerAndConn.ConnSet.AllConnections(), + protocolsAndPorts: peerAndConn.ConnSet.ProtocolsAndPortsMap(), } + res = append(res, p2pConnection) } return res, nil } // get string representation for a list of port ranges -func portsString(ports []eval.PortRange) string { +func portsString(ports []common.PortRange) string { portsStr := make([]string, len(ports)) for i := range ports { portsStr[i] = ports[i].String() } return strings.Join(portsStr, connsAndPortRangeSeparator) } + +func (ca *ConnlistAnalyzer) warnBlockedIngress(peerStr string, ingressObjs map[string][]string) { + warningMsg, commaSep := "", "," + if len(ingressObjs[scan.Ingress]) > 0 { + warningMsg = "K8s-Ingress object/s: " + strings.Join(ingressObjs[scan.Ingress], commaSep) + } + if len(ingressObjs[scan.Route]) > 0 { + if warningMsg != "" { + warningMsg += " and " + } + warningMsg += "Route objects/s: " + strings.Join(ingressObjs[scan.Route], commaSep) + } + warningMsg += " specified workload " + peerStr + " as a backend, but network policies are blocking " + + "ingress connections from an arbitrary in-cluster source to this workload." + + "Connectivity map will not include a possibly allowed connection between the ingress controller and this workload." + ca.logger.Warnf(warningMsg) +} diff --git a/pkg/netpol/connlist/connlist_test.go b/pkg/netpol/connlist/connlist_test.go index c5f99c4a..1fae25f0 100644 --- a/pkg/netpol/connlist/connlist_test.go +++ b/pkg/netpol/connlist/connlist_test.go @@ -25,37 +25,85 @@ func getConnlistFromDirPathRes(stopOnErr bool, path string) (*ConnlistAnalyzer, return analyzer, res, err } +type testEntry struct { + testDirName string + outputFormats []string + generateActualOutput bool // if true, overrides existing expected output file +} + +const expectedOutputFileNamePrefix = "connlist_output." + // TestConnList tests the output of ConnlistFromDirPath() for valid input resources func TestConnList(t *testing.T) { - testNames := []string{"ipblockstest", "onlineboutique", "onlineboutique_workloads", - "minikube_resources", "online_boutique_workloads_no_ns", "core_pods_without_host_ip"} - expectedOutputFileName := "connlist_output.txt" - generateActualOutput := false - for _, testName := range testNames { - path := filepath.Join(testutils.GetTestsDir(), testName) - expectedOutputFile := filepath.Join(path, expectedOutputFileName) - analyzer, res, err := getConnlistFromDirPathRes(false, path) - if err != nil { - t.Fatalf("Test %s: TestConnList FromDir err: %v", testName, err) - } - actualOutput, err := analyzer.ConnectionsListToString(res) - if err != nil { - t.Fatalf("Test %s: TestConnList writing output err: %v", testName, err) - } - if generateActualOutput { - // update expected output: override expected output with actual output - if err = os.WriteFile(expectedOutputFile, []byte(actualOutput), 0o600); err != nil { - t.Fatalf("Test %s: TestConnList WriteFile err: %v", testName, err) - } - } else { - // compare actual output to expected output - expectedStr, err := os.ReadFile(expectedOutputFile) - if err != nil { - t.Fatalf("Test %s: TestConnList ReadFile err: %v", testName, err) - } - if string(expectedStr) != actualOutput { - fmt.Printf("%s", actualOutput) - t.Fatalf("unexpected output result for test %v", testName) + testingEntries := []testEntry{ + { + testDirName: "ipblockstest", + outputFormats: []string{TextFormat}, + }, + { + testDirName: "onlineboutique", + outputFormats: []string{JSONFormat, MDFormat, TextFormat}, + }, + { + testDirName: "onlineboutique_workloads", + outputFormats: []string{CSVFormat, DOTFormat, TextFormat}, + }, + { + testDirName: "minikube_resources", + outputFormats: []string{TextFormat}, + }, + { + testDirName: "online_boutique_workloads_no_ns", + outputFormats: []string{TextFormat}, + }, + { + testDirName: "core_pods_without_host_ip", + outputFormats: []string{TextFormat}, + }, + { + testDirName: "acs_security_frontend_demos", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + { + testDirName: "demo_app_with_routes_and_ingress", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + { + testDirName: "k8s_ingress_test", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + { + testDirName: "multiple_ingress_objects_with_different_ports", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + { + testDirName: "one_ingress_multiple_ports", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + { + testDirName: "one_ingress_multiple_services", + outputFormats: []string{TextFormat, JSONFormat, CSVFormat, MDFormat, DOTFormat}, + }, + } + + for _, entry := range testingEntries { + dirPath := filepath.Join(testutils.GetTestsDir(), entry.testDirName) + for _, format := range entry.outputFormats { + analyzer := NewConnlistAnalyzer(WithOutputFormat(format)) + res, err := analyzer.ConnlistFromDirPath(dirPath) + require.Nil(t, err) + output, err := analyzer.ConnectionsListToString(res) + require.Nil(t, err) + expectedOutputFileName := expectedOutputFileNamePrefix + format + expectedOutputFile := filepath.Join(dirPath, expectedOutputFileName) + if entry.generateActualOutput { + // update expected output: override expected output with actual output + err := os.WriteFile(expectedOutputFile, []byte(output), 0o600) + require.Nil(t, err) + } else { + expectedOutput, err := os.ReadFile(expectedOutputFile) + require.Nil(t, err) + require.Equal(t, string(expectedOutput), output) } } } @@ -179,71 +227,6 @@ func TestConnlistAnalyzerBadDirNoYamls(t *testing.T) { require.True(t, errors.As(errs1[1].Error(), &secondErr)) } -func TestWithTextOutputFormat(t *testing.T) { - dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique") - analyzer := NewConnlistAnalyzer(WithOutputFormat("txt")) - res, err := analyzer.ConnlistFromDirPath(dirPath) - require.Nil(t, err) - txtRes, err := analyzer.ConnectionsListToString(res) - require.Nil(t, err) - expectedOutputFile := filepath.Join(dirPath, "connlist_output.txt") - expectedOutput, err := os.ReadFile(expectedOutputFile) - require.Nil(t, err) - require.Equal(t, string(expectedOutput), txtRes) -} - -func TestWithJSONOutputFormat(t *testing.T) { - dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique") - analyzer := NewConnlistAnalyzer(WithOutputFormat("json")) - res, err := analyzer.ConnlistFromDirPath(dirPath) - require.Nil(t, err) - jsonRes, err := analyzer.ConnectionsListToString(res) - require.Nil(t, err) - expectedOutputFile := filepath.Join(dirPath, "connlist_output.json") - expectedOutput, err := os.ReadFile(expectedOutputFile) - require.Nil(t, err) - require.Equal(t, string(expectedOutput), jsonRes) -} - -func TestWithDOTOutputFormat(t *testing.T) { - dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique_workloads") - analyzer := NewConnlistAnalyzer(WithOutputFormat("dot")) - res, err := analyzer.ConnlistFromDirPath(dirPath) - require.Nil(t, err) - dotRes, err := analyzer.ConnectionsListToString(res) - require.Nil(t, err) - expectedOutputFile := filepath.Join(dirPath, "connlist_output.dot") - expectedOutput, err := os.ReadFile(expectedOutputFile) - require.Nil(t, err) - require.Equal(t, string(expectedOutput), dotRes) -} - -func TestWithMDOutputFormat(t *testing.T) { - dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique") - analyzer := NewConnlistAnalyzer(WithOutputFormat("md")) - res, err := analyzer.ConnlistFromDirPath(dirPath) - require.Nil(t, err) - mdRes, err := analyzer.ConnectionsListToString(res) - require.Nil(t, err) - expectedOutputFile := filepath.Join(dirPath, "connlist_output.md") - expectedOutput, err := os.ReadFile(expectedOutputFile) - require.Nil(t, err) - require.Equal(t, string(expectedOutput), mdRes) -} - -func TestWithCSVOutputFormat(t *testing.T) { - dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique_workloads") - analyzer := NewConnlistAnalyzer(WithOutputFormat("csv")) - res, err := analyzer.ConnlistFromDirPath(dirPath) - require.Nil(t, err) - csvRes, err := analyzer.ConnectionsListToString(res) - require.Nil(t, err) - expectedOutputFile := filepath.Join(dirPath, "connlist_output.csv") - expectedOutput, err := os.ReadFile(expectedOutputFile) - require.Nil(t, err) - require.Equal(t, string(expectedOutput), csvRes) -} - func TestConnlistAnalyzerBadOutputFormat(t *testing.T) { dirPath := filepath.Join(testutils.GetTestsDir(), "onlineboutique") analyzer := NewConnlistAnalyzer(WithOutputFormat("jpeg")) @@ -266,3 +249,14 @@ func TestWithFocusWorkloadWithReplicasConnections(t *testing.T) { require.Nil(t, err) require.NotContains(t, out, "kube-system/calico-node[DaemonSet] => kube-system/calico-node[DaemonSet] : All Connections") } + +func TestWithFocusWorkloadWithIngressObjects(t *testing.T) { + analyzer := NewConnlistAnalyzer(WithFocusWorkload("details-v1-79f774bdb9")) + dirPath := filepath.Join(testutils.GetTestsDir(), "k8s_ingress_test") + res, err := analyzer.ConnlistFromDirPath(dirPath) + require.Len(t, res, 13) + require.Nil(t, err) + out, err := analyzer.ConnectionsListToString(res) + require.Nil(t, err) + require.Contains(t, out, "{ingress-controller} => default/details-v1-79f774bdb9[ReplicaSet] : TCP 9080") +} diff --git a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go new file mode 100644 index 00000000..0d7b7bee --- /dev/null +++ b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer.go @@ -0,0 +1,418 @@ +// Copyright 2022 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ingressanalyzer + +import ( + "strconv" + + ocroutev1 "github.com/openshift/api/route/v1" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" + "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" + "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" + "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" +) + +const ( + // The actual ingress controller pod is usually unknown and not available in the input resources for the analysis. + // IngressPodName and IngressPodNamespace are used to represent that pod with those placeholder values for name and namespace. + IngressPodName = "ingress-controller" + IngressPodNamespace = "ingress-controller-ns" +) + +type serviceInfo struct { + // used to populate routesToServicesMap and k8sIngressToServicesMap with their target services info + serviceName string + servicePort intstr.IntOrString // service port name or port number +} + +type peersAndPorts struct { + // used to populate the servicesToPortsAndPeersMap Map with its ports and selected peers + peers []eval.Peer + ports []corev1.ServicePort +} + +// IngressAnalyzer provides API to analyze Ingress/Route resources, to allow inferring potential connectivity +// from ingress-controller to pods in the cluster +type IngressAnalyzer struct { + logger logger.Logger + pe *eval.PolicyEngine // holds the workload peers relevant to the analysis + servicesToPortsAndPeersMap map[string]map[string]peersAndPorts // map from namespace to map from service name to its ports and + // its selected workloads + routesToServicesMap map[string]map[string][]serviceInfo // map from namespace to map from route name to its target service + k8sIngressToServicesMap map[string]map[string][]serviceInfo // map from namespace to map from k8s ingress object name to + // its target services +} + +// NewIngressAnalyzerWithObjects returns a new IngressAnalyzer with relevant objects +func NewIngressAnalyzerWithObjects(objects []scan.K8sObject, pe *eval.PolicyEngine, l logger.Logger) (*IngressAnalyzer, error) { + ia := &IngressAnalyzer{ + servicesToPortsAndPeersMap: make(map[string]map[string]peersAndPorts), + routesToServicesMap: make(map[string]map[string][]serviceInfo), + k8sIngressToServicesMap: make(map[string]map[string][]serviceInfo), + logger: l, + pe: pe, + } + + var err error + for _, obj := range objects { + switch obj.Kind { + case scan.Service: + err = ia.mapServiceToPeers(obj.Service) + case scan.Route: + ia.mapRouteToServices(obj.Route) + case scan.Ingress: + ia.mapK8sIngressToServices(obj.Ingress) + } + if err != nil { + return nil, err + } + } + return ia, err +} + +// IsEmpty returns whether there are no services to consider for Ingress analysis +func (ia *IngressAnalyzer) IsEmpty() bool { + return len(ia.servicesToPortsAndPeersMap) == 0 || + (len(ia.routesToServicesMap) == 0 && len(ia.k8sIngressToServicesMap) == 0) +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +// services analysis + +const ( + missingSelectorWarning = "K8s Service without selectors is not supported" + whiteSpace = " " + colon = ": " +) + +// mapServiceToPeers populates servicesToPortsAndPeersMap +func (ia *IngressAnalyzer) mapServiceToPeers(svc *corev1.Service) error { + // get peers selected by the service selectors + peers, err := ia.getServiceSelectedPeers(svc) + if err != nil { + return err + } + if len(peers) == 0 { + // service was ignored + return nil + } + if _, ok := ia.servicesToPortsAndPeersMap[svc.Namespace]; !ok { + ia.servicesToPortsAndPeersMap[svc.Namespace] = make(map[string]peersAndPorts) + } + ia.servicesToPortsAndPeersMap[svc.Namespace][svc.Name] = peersAndPorts{peers: peers, ports: svc.Spec.Ports} + return nil +} + +// getServicePeers given a service return its selected peers +func (ia *IngressAnalyzer) getServiceSelectedPeers(svc *corev1.Service) ([]eval.Peer, error) { + svcStr := types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}.String() + if svc.Spec.Selector == nil { + ia.logger.Warnf("ignoring " + scan.Service + whiteSpace + svcStr + colon + missingSelectorWarning) + return nil, nil + } + svcLabelsSelector, err := convertServiceSelectorToLabelSelector(svc.Spec.Selector) + if err != nil { + return nil, err + } + return ia.pe.GetSelectedPeers(svcLabelsSelector, svc.Namespace), nil +} + +// utility func +// convertServiceSelectorToLabelSelector converts service selector to LabelsSelector +func convertServiceSelectorToLabelSelector(svcSelector map[string]string) (labels.Selector, error) { + labelsSelector := metav1.LabelSelector{MatchLabels: svcSelector} + selectorRes, err := metav1.LabelSelectorAsSelector(&labelsSelector) + if err != nil { + return nil, err + } + return selectorRes, nil +} + +//////////////////////////////////////////////////////////////////////////////////////////////// +// routes analysis + +const ( + allowedTargetKind = scan.Service + routeTargetKindWarning = "ignoring target with unsupported kind" + routeBackendsWarning = "ignoring alternate backend without Service " +) + +// mapRouteToServices populates routesToServicesMap +func (ia *IngressAnalyzer) mapRouteToServices(rt *ocroutev1.Route) { + services := ia.getRouteServices(rt) + if len(services) == 0 { // all route targets were ignored + return + } + if _, ok := ia.routesToServicesMap[rt.Namespace]; !ok { + ia.routesToServicesMap[rt.Namespace] = make(map[string][]serviceInfo) + } + ia.routesToServicesMap[rt.Namespace][rt.Name] = services +} + +// getRouteServices given Route object returns its targeted services names +func (ia *IngressAnalyzer) getRouteServices(rt *ocroutev1.Route) []serviceInfo { + routeStr := types.NamespacedName{Namespace: rt.Namespace, Name: rt.Name}.String() + targetServices := make([]serviceInfo, 0, len(rt.Spec.AlternateBackends)+1) + var routeTargetPort intstr.IntOrString + if rt.Spec.Port != nil { + routeTargetPort = rt.Spec.Port.TargetPort + } + // Currently, only 'Service' is allowed as the kind of target that the route is referring to. + if rt.Spec.To.Kind != "" && rt.Spec.To.Kind != allowedTargetKind { + ia.logger.Warnf(scan.Route + whiteSpace + routeStr + colon + routeTargetKindWarning) + } else { + targetServices = append(targetServices, serviceInfo{serviceName: rt.Spec.To.Name, servicePort: routeTargetPort}) + } + + for _, backend := range rt.Spec.AlternateBackends { + if backend.Kind != "" && backend.Kind != allowedTargetKind { + ia.logger.Warnf(scan.Route + whiteSpace + routeStr + colon + routeBackendsWarning) + } else { + targetServices = append(targetServices, serviceInfo{serviceName: backend.Name, servicePort: routeTargetPort}) + } + } + return targetServices +} + +// /////////////////////////////////////////////////////////////////////////////////////////////// +// k8s Ingress objects analysis + +const ( + defaultBackendWarning = "ignoring default backend" + ruleBackendWarning = "ignoring rule backend without Service" +) + +// mapk8sIngressToServices populates k8sIngressToServicesMap +func (ia *IngressAnalyzer) mapK8sIngressToServices(ing *netv1.Ingress) { + services := ia.getK8sIngressServices(ing) + if len(services) == 0 { // all ingress backends were ignored + return + } + if _, ok := ia.k8sIngressToServicesMap[ing.Namespace]; !ok { + ia.k8sIngressToServicesMap[ing.Namespace] = make(map[string][]serviceInfo) + } + ia.k8sIngressToServicesMap[ing.Namespace][ing.Name] = services +} + +// getk8sIngressServices given k8s-Ingress object returns its targeted services info +func (ia *IngressAnalyzer) getK8sIngressServices(ing *netv1.Ingress) []serviceInfo { + ingressStr := types.NamespacedName{Namespace: ing.Namespace, Name: ing.Name}.String() + backendServices := make([]serviceInfo, 0) + // if DefaultBackend is provided add its service info to the result + if ing.Spec.DefaultBackend != nil { + if ing.Spec.DefaultBackend.Service == nil { + ia.logger.Warnf(scan.Ingress + whiteSpace + ingressStr + colon + defaultBackendWarning) + } else { + backendServices = append(backendServices, getServiceInfo(ing.Spec.DefaultBackend.Service)) + } + } + // add service names from the Ingress rules + for _, rule := range ing.Spec.Rules { + for _, path := range rule.IngressRuleValue.HTTP.Paths { + if path.Backend.Service == nil { + ia.logger.Warnf(scan.Ingress + whiteSpace + ingressStr + colon + ruleBackendWarning) + } else { + backendServices = append(backendServices, getServiceInfo(path.Backend.Service)) + } + } + } + return backendServices +} + +// utility func +// getServiceInfo returns serviceInfo struct (name and port) from a given Ingress backend service +func getServiceInfo(backendService *netv1.IngressServiceBackend) serviceInfo { + res := serviceInfo{serviceName: backendService.Name} + // Port.Name and Port.Number are mutually exclusive + if backendService.Port.Name != "" { + res.servicePort.StrVal = backendService.Port.Name + } else { + res.servicePort.IntVal = backendService.Port.Number + } + return res +} + +////////////////////////////////////////////////////////////////////////////////////////////// +// Ingress allowed connections + +// PeerAndIngressConnSet captures Peer object as allowed target from ingress-controller Pod, with its possible connections +type PeerAndIngressConnSet struct { + Peer eval.Peer + ConnSet *common.ConnectionSet + IngressObjects map[string][]string +} + +// AllowedIngressConnections returns a map of the possible connections from ingress-controller pod to workload peers, +// as inferred from Ingress and Route resources. The map is from a workload name to its PeerAndIngressConnSet object. +func (ia *IngressAnalyzer) AllowedIngressConnections() (map[string]*PeerAndIngressConnSet, error) { + // if there is at least one route/ ingress object that targets a service which selects a dst peer, + // then we have ingress connections to that peer + + // get all targeted workload peers and compute allowed conns of each workload peer + // 1. from routes + routesResult, err := ia.allowedIngressConnectionsByResourcesType(ia.routesToServicesMap, scan.Route) + if err != nil { + return nil, err + } + // 2. from k8s-ingress objects + ingressResult, err := ia.allowedIngressConnectionsByResourcesType(ia.k8sIngressToServicesMap, scan.Ingress) + if err != nil { + return nil, err + } + + // merge the map results from routes and k8s-ingress objects + mergeResults(routesResult, ingressResult) + return ingressResult, nil +} + +// utility func +// mergeResults merges routesMap into ingressMap , since routesMap may be wider with peers connections +func mergeResults(routesMap, ingressMap map[string]*PeerAndIngressConnSet) { + for k, v := range routesMap { + if _, ok := ingressMap[k]; ok { + ingressMap[k].ConnSet.Union(v.ConnSet) + } else { + ingressMap[k] = v + } + } +} + +// allowedIngressConnectionsByResourcesType returns map from peers names to the allowed ingress connections +// based on k8s-Ingress/routes objects rules +func (ia *IngressAnalyzer) allowedIngressConnectionsByResourcesType(mapToIterate map[string]map[string][]serviceInfo, ingType string) ( + map[string]*PeerAndIngressConnSet, error) { + res := make(map[string]*PeerAndIngressConnSet) + for ns, objSvcMap := range mapToIterate { + // if there are no services in same namespace of the Ingress, the ingress objects in this ns will be skipped + if _, ok := ia.servicesToPortsAndPeersMap[ns]; !ok { + continue + } + for objName, svcList := range objSvcMap { + ingressObjTargetPeersAndPorts, err := ia.getIngressObjectTargetedPeersAndPorts(ns, svcList) + 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 { + ingressObjs := make(map[string][]string, 2) + ingressObjs[ingType] = []string{ingObjStr} + res[peer.String()] = &PeerAndIngressConnSet{Peer: peer, ConnSet: pConn, IngressObjects: ingressObjs} + } else { + res[peer.String()].ConnSet.Union(pConn) + res[peer.String()].IngressObjects[ingType] = append(res[peer.String()].IngressObjects[ingType], ingObjStr) + } + } + } + } + + return res, nil +} + +// 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) { + res := make(map[eval.Peer]*common.ConnectionSet) + for _, svc := range svcList { + peersAndPorts, ok := ia.servicesToPortsAndPeersMap[ns][svc.serviceName] + if !ok { + ia.logger.Warnf("ignoring target service " + svc.serviceName + " : service not found") + } + for _, peer := range peersAndPorts.peers { + currIngressPeerConn, err := ia.getIngressPeerConnection(peer, peersAndPorts.ports, svc.servicePort) + if err != nil { + return nil, err + } + if _, ok := res[peer]; !ok { + res[peer] = currIngressPeerConn + } else { + res[peer].Union(currIngressPeerConn) + } + } + } + return res, nil +} + +// 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) { + 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) + peerPortsToFind := getPeerAccessPort(actualServicePorts, requiredPort) + // compute the connection to the peer with the required port/s + res := common.MakeConnectionSet(false) + for _, peerPortToFind := range peerPortsToFind { + portNum := peerPortToFind.IntValue() + if peerPortToFind.StrVal != "" { // if the port we are searching for is namedPort + portInt, err := ia.pe.ConvertPeerNamedPort(peerPortToFind.StrVal, peer) + if err != nil { + return nil, err + } + portNum = int(portInt) + } + + if peerTCPConn.Contains(strconv.Itoa(portNum), string(corev1.ProtocolTCP)) { + permittedPort := common.PortSet{} + permittedPort.AddPort(intstr.FromInt(portNum)) + res.AddConnection(corev1.ProtocolTCP, permittedPort) + } + } + return res, nil +} + +// getPeerAccessPort returns the peer's port to be exposed based on the service's port.targetPort value +func getPeerAccessPort(actualServicePorts []corev1.ServicePort, requiredPort intstr.IntOrString) []intstr.IntOrString { + res := make([]intstr.IntOrString, 0) + requiredPortEmpty := false // if the required port is empty , then all service's target ports will be used (required) + if requiredPort.IntVal == 0 && requiredPort.StrVal == "" { + requiredPortEmpty = true + } + + // get the peer port/s to find from the required port + for _, svcPort := range actualServicePorts { + var svcPodAccessPort intstr.IntOrString + // extracting the pod access port from the service port + if !(svcPort.TargetPort.IntVal == 0 && svcPort.TargetPort.StrVal == "") { + // servicePort.TargetPort is Number or name of the port to access on the pods targeted by the service. + svcPodAccessPort = svcPort.TargetPort + } else { + // if servicePort.TargetPort is not specified, the value of the 'port' field is used + svcPodAccessPort.IntVal = svcPort.Port + } + + switch requiredPortEmpty { + case false: // the required port is specified (not empty) + // checking if the service port matches the required port, if yes returning its pod access port + if svcPort.Name != "" && svcPort.Name == requiredPort.StrVal || svcPort.Port == requiredPort.IntVal || + svcPort.TargetPort == requiredPort { + res = append(res, svcPodAccessPort) + return res + } + case true: + res = append(res, svcPodAccessPort) + } + } + return res +} diff --git a/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go new file mode 100644 index 00000000..e209111e --- /dev/null +++ b/pkg/netpol/connlist/internal/ingressanalyzer/ingress_analyzer_test.go @@ -0,0 +1,231 @@ +package ingressanalyzer + +import ( + "os" + "path/filepath" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/stretchr/testify/require" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" + "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/testutils" + "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" + "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" +) + +// global scanner object for testing +var scanner = scan.NewResourcesScanner(logger.NewDefaultLogger(), false, filepath.WalkDir) + +func TestIngressAnalyzerWithRoutes(t *testing.T) { + routesNamespace := "frontend" + routeNameExample := "webapp" + path := filepath.Join(getTestsDir(), "acs_security_frontend_demos") + objects, processingErrs := scanner.FilesToObjectsList(path) + require.Empty(t, processingErrs) + pe, err := eval.NewPolicyEngineWithObjects(objects) + require.Empty(t, err) + ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger()) + require.Empty(t, err) + // routes map includes 1 namespace + require.Len(t, ia.routesToServicesMap, 1) + // the routes namespace includes 2 different routes + require.Len(t, ia.routesToServicesMap[routesNamespace], 2) + // each route is mapped to 1 service - check 1 for example + require.Len(t, ia.routesToServicesMap[routesNamespace][routeNameExample], 1) +} + +type ingressToPod struct { + peerName string + peerNamespace string + peerType string + allConnections bool + ports []int64 + protocol string +} + +type testEntry struct { + dirpath string + processingErrs int + testIngressEntries []ingressToPod +} + +func TestIngressAnalyzerConnectivityToAPod(t *testing.T) { + testingEntries := []testEntry{ + { + dirpath: "acs_security_frontend_demos", + processingErrs: 0, + testIngressEntries: []ingressToPod{ + { + peerName: "asset-cache", + peerNamespace: "frontend", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8080}, + protocol: "TCP", + }, + { + peerName: "webapp", + peerNamespace: "frontend", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8080}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "route_example_with_target_port", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "workload-with-multiple-ports", + peerNamespace: "routes-world", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8000, 8090}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "k8s_ingress_test", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "details-v1-79f774bdb9", + peerNamespace: "default", + peerType: scan.ReplicaSet, + allConnections: false, + ports: []int64{9080}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "demo_app_with_routes_and_ingress", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "hello-world", // this workload is selected by both Ingress and Route objects + peerNamespace: "helloworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8000}, + protocol: "TCP", + }, + { + peerName: "ingress-world", // this workload is selected by Ingress object only + peerNamespace: "ingressworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8090}, + protocol: "TCP", + }, + { + peerName: "route-world", // this workload is selected by route object only + peerNamespace: "routeworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8060}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "one_ingress_multiple_ports", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "ingress-world-multiple-ports", + peerNamespace: "ingressworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8000, 8090}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "one_ingress_multiple_services", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "ingress-world-multiple-ports", + peerNamespace: "ingressworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8000, 8090}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "multiple_ingress_objects_with_different_ports", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "ingress-world-multiple-ports", + peerNamespace: "ingressworld", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8050, 8090}, + protocol: "TCP", + }, + }, + }, + { + dirpath: "ingress_example_with_named_port", + processingErrs: 1, // no network-policies + testIngressEntries: []ingressToPod{ + { + peerName: "hello-deployment", + peerNamespace: "hello", + peerType: scan.Deployment, + allConnections: false, + ports: []int64{8080}, + protocol: "TCP", + }, + }, + }, + } + + for _, testEntry := range testingEntries { + path := filepath.Join(getTestsDir(), testEntry.dirpath) + objects, processingErrs := scanner.FilesToObjectsList(path) + require.Len(t, processingErrs, testEntry.processingErrs) + pe, err := eval.NewPolicyEngineWithObjects(objects) + require.Empty(t, err) + ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger()) + require.Empty(t, err) + ingressConns, err := ia.AllowedIngressConnections() + require.Empty(t, err) + for _, ingressEntry := range testEntry.testIngressEntries { + peerStr := types.NamespacedName{Name: ingressEntry.peerName, Namespace: ingressEntry.peerNamespace}.String() + + "[" + ingressEntry.peerType + "]" + require.Contains(t, ingressConns, peerStr) + peerAndConn := ingressConns[peerStr] + require.Equal(t, peerAndConn.ConnSet.AllConnections(), ingressEntry.allConnections) + if !peerAndConn.ConnSet.AllConnections() { + require.Contains(t, peerAndConn.ConnSet.ProtocolsAndPortsMap(), v1.Protocol(ingressEntry.protocol)) + connPortRange := peerAndConn.ConnSet.ProtocolsAndPortsMap()[v1.Protocol(ingressEntry.protocol)] + require.Len(t, connPortRange, len(ingressEntry.ports)) + for i := range ingressEntry.ports { + require.Equal(t, connPortRange[i].Start(), ingressEntry.ports[i]) + } + } + } + } +} + +func getTestsDir() string { + currentDir, _ := os.Getwd() + // go two levels up since currentDir under internal + parentDir := filepath.Dir(filepath.Dir(currentDir)) + os.Chdir(parentDir) + res := testutils.GetTestsDir() + os.Chdir(currentDir) + return res +} diff --git a/pkg/netpol/ingressanalyzer/service_test.go b/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go similarity index 72% rename from pkg/netpol/ingressanalyzer/service_test.go rename to pkg/netpol/connlist/internal/ingressanalyzer/service_test.go index 2e82e6d9..b44a2aea 100644 --- a/pkg/netpol/ingressanalyzer/service_test.go +++ b/pkg/netpol/connlist/internal/ingressanalyzer/service_test.go @@ -8,14 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" - "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/testutils" "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" ) type serviceMapping struct { serviceName string serviceNamespace string - numPods int + numWorkloads int expectedError error } @@ -25,48 +24,48 @@ func TestServiceMappingToPods(t *testing.T) { { serviceName: "demo", serviceNamespace: "default", - numPods: 1, + numWorkloads: 1, expectedError: nil, }, { serviceName: "ingress-nginx-controller", serviceNamespace: "ingress-nginx", - numPods: 1, + numWorkloads: 1, expectedError: nil, }, { serviceName: "ingress-nginx-controller-admission", serviceNamespace: "ingress-nginx", - numPods: 2, + numWorkloads: 2, expectedError: nil, }, { serviceName: "kube-dns", serviceNamespace: "kube-system", - numPods: 1, + numWorkloads: 1, expectedError: nil, }, { serviceName: "no-pods-selected", serviceNamespace: "default", - numPods: 0, + numWorkloads: 0, expectedError: nil, }, { serviceName: "not-existing-svc", serviceNamespace: "default", - numPods: 0, + numWorkloads: 0, expectedError: errors.New("service does not exist: default/not-existing-svc"), }, { serviceName: "not-existing-svc", serviceNamespace: "not-existing-ns", - numPods: 0, + numWorkloads: 0, expectedError: errors.New("service does not exist: not-existing-ns/not-existing-svc"), }, } - path := filepath.Join(testutils.GetTestsDir(), "services", "services_with_selectors") + path := filepath.Join(getTestsDir(), "services", "services_with_selectors") objects, processingErrs := scanner.FilesToObjectsList(path) require.Len(t, processingErrs, 1) // no policies require.Len(t, objects, 16) // found 5 services and 11 pods @@ -76,15 +75,17 @@ func TestServiceMappingToPods(t *testing.T) { require.Empty(t, err) for _, serviceMappingItem := range serviceMappingList { - require.Len(t, ia.servicesToPeersMap[serviceMappingItem.serviceNamespace][serviceMappingItem.serviceName], serviceMappingItem.numPods) + require.Len(t, ia.servicesToPortsAndPeersMap[serviceMappingItem.serviceNamespace][serviceMappingItem.serviceName].peers, + serviceMappingItem.numWorkloads) } } func TestNotSupportedService(t *testing.T) { - path := filepath.Join(testutils.GetTestsDir(), "services", "services_without_selector") + path := filepath.Join(getTestsDir(), "services", "services_without_selector") objects, processingErrs := scanner.FilesToObjectsList(path) - require.Len(t, objects, 1) + require.Len(t, objects, 1) // 1 service object require.Len(t, processingErrs, 2) // no policies nor workloads - _, err := NewIngressAnalyzerWithObjects(objects, nil, logger.NewDefaultLogger()) - require.Contains(t, err.Error(), missingSelectorError) + ia, err := NewIngressAnalyzerWithObjects(objects, nil, logger.NewDefaultLogger()) + require.Empty(t, err) + require.Len(t, ia.servicesToPortsAndPeersMap, 0) // service was ignored } diff --git a/pkg/netpol/eval/check.go b/pkg/netpol/eval/check.go index 0a5bd07e..370ce29d 100644 --- a/pkg/netpol/eval/check.go +++ b/pkg/netpol/eval/check.go @@ -23,6 +23,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" ) @@ -88,7 +89,7 @@ func (pe *PolicyEngine) changePodPeerToAnotherPodObject(peer *k8s.PodPeer) { // AllAllowedConnectionsBetweenWorkloadPeers returns the allowed connections from srcPeer to dstPeer, // expecting that srcPeer and dstPeer are in level of workloads (WorkloadPeer) -func (pe *PolicyEngine) AllAllowedConnectionsBetweenWorkloadPeers(srcPeer, dstPeer Peer) (Connection, error) { +func (pe *PolicyEngine) AllAllowedConnectionsBetweenWorkloadPeers(srcPeer, dstPeer Peer) (common.Connection, error) { if srcPeer.IsPeerIPType() && !dstPeer.IsPeerIPType() { // assuming dstPeer is WorkloadPeer, should be converted to k8s.Peer dstPodPeer, err := pe.convertWorkloadPeerToPodPeer(dstPeer) @@ -127,14 +128,13 @@ func (pe *PolicyEngine) AllAllowedConnectionsBetweenWorkloadPeers(srcPeer, dstPe // allAllowedConnectionsBetweenPeers: returns the allowed connections from srcPeer to dstPeer // expecting that srcPeer and dstPeer are in level of pods (PodPeer) -func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) (Connection, error) { +func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) (*common.ConnectionSet, error) { srcK8sPeer := srcPeer.(k8s.Peer) dstK8sPeer := dstPeer.(k8s.Peer) - res := k8s.ConnectionSet{} + res := &common.ConnectionSet{} // cases where any connection is always allowed if isPodToItself(srcK8sPeer, dstK8sPeer) || isPeerNodeIP(srcK8sPeer, dstK8sPeer) || isPeerNodeIP(dstK8sPeer, srcK8sPeer) { - conn := k8s.MakeConnectionSet(true) - return getConnectionObject(conn), nil + return common.MakeConnectionSet(true), nil } // egress res, err := pe.allallowedXgressConnections(srcK8sPeer, dstK8sPeer, false) @@ -142,7 +142,7 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) return nil, err } if res.IsEmpty() { - return getConnectionObject(res), nil + return res, nil } // ingress ingressRes, err := pe.allallowedXgressConnections(srcK8sPeer, dstK8sPeer, true) @@ -150,7 +150,7 @@ func (pe *PolicyEngine) allAllowedConnectionsBetweenPeers(srcPeer, dstPeer Peer) return nil, err } res.Intersection(ingressRes) - return getConnectionObject(res), nil + return res, nil } // getPod: returns a Pod object corresponding to the input pod name @@ -230,37 +230,38 @@ func (pe *PolicyEngine) allowedXgressConnection(src, dst k8s.Peer, isIngress boo // allallowedXgressConnections returns the set of allowed connections from src to dst on given // direction(ingress/egress), by network policies rules -func (pe *PolicyEngine) allallowedXgressConnections(src, dst k8s.Peer, isIngress bool) (k8s.ConnectionSet, error) { +func (pe *PolicyEngine) allallowedXgressConnections(src, dst k8s.Peer, isIngress bool) (*common.ConnectionSet, error) { // relevant policies: policies that capture dst if isIngress, else policies that capture src var err error var netpols []*k8s.NetworkPolicy if isIngress { if dst.PeerType() == k8s.IPBlockType { - return k8s.MakeConnectionSet(true), nil // all connections allowed - no restrictions on ingress to externalIP + return common.MakeConnectionSet(true), nil // all connections allowed - no restrictions on ingress to externalIP } netpols, err = pe.getPoliciesSelectingPod(dst.GetPeerPod(), netv1.PolicyTypeIngress) } else { if src.PeerType() == k8s.IPBlockType { - return k8s.MakeConnectionSet(true), nil // all connections allowed - no restrictions on egress from externalIP + return common.MakeConnectionSet(true), nil // all connections allowed - no restrictions on egress from externalIP } netpols, err = pe.getPoliciesSelectingPod(src.GetPeerPod(), netv1.PolicyTypeEgress) } if err != nil { - return k8s.ConnectionSet{}, err + return nil, err } if len(netpols) == 0 { - return k8s.MakeConnectionSet(true), nil // all connections allowed - no networkpolicy captures the relevant pod on the required direction + return common.MakeConnectionSet(true), nil // all connections allowed - no networkpolicy captures the relevant pod + // on the required direction } - allowedConns := k8s.MakeConnectionSet(false) + allowedConns := common.MakeConnectionSet(false) // iterate relevant network policies (that capture the required pod) for _, policy := range netpols { // if isIngress: check for ingress rules that capture src within 'from' // if not isIngress: check for egress rules that capture dst within 'to' // collect the allowed connectivity from the relevant rules into allowedConns - var policyAllowedConnectionsPerDirection k8s.ConnectionSet + var policyAllowedConnectionsPerDirection *common.ConnectionSet var err error if isIngress { policyAllowedConnectionsPerDirection, err = policy.GetIngressAllowedConns(src, dst) @@ -337,29 +338,28 @@ func (pe *PolicyEngine) checkIfAllowedNew(src, dst, protocol, port string) (bool // allAllowedConnections: returns allowed connection between input strings of src and dst // currently used only for testing -func (pe *PolicyEngine) allAllowedConnections(src, dst string) (k8s.ConnectionSet, error) { - res := k8s.ConnectionSet{} +func (pe *PolicyEngine) allAllowedConnections(src, dst string) (*common.ConnectionSet, error) { srcPeer, err := pe.getPeer(src) if err != nil { - return res, err + return nil, err } dstPeer, err := pe.getPeer(dst) if err != nil { - return res, err + return nil, err } allowedConns, err := pe.allAllowedConnectionsBetweenPeers(srcPeer.(Peer), dstPeer.(Peer)) - return allowedConns.(*k8sConnectionSetWrapper).ConnectionSet(), err + return allowedConns, err } -// GetPeerExposedProtocolsAndPorts returns the protocols and ports exposed by a workload/pod peer -func (pe *PolicyEngine) GetPeerExposedProtocolsAndPorts(peer Peer) Connection { +// GetPeerExposedTCPConnections returns the tcp connection (ports) exposed by a workload/pod peer +func GetPeerExposedTCPConnections(peer Peer) *common.ConnectionSet { switch currPeer := peer.(type) { case *k8s.IPBlockPeer: return nil case *k8s.WorkloadPeer: - return getConnectionObject(currPeer.Pod.PodExposedProtocolsAndPorts()) + return currPeer.Pod.PodExposedTCPConnections() case *k8s.PodPeer: - return getConnectionObject(currPeer.Pod.PodExposedProtocolsAndPorts()) + return currPeer.Pod.PodExposedTCPConnections() default: return nil } diff --git a/pkg/netpol/eval/connection.go b/pkg/netpol/eval/connection.go deleted file mode 100644 index 84a45839..00000000 --- a/pkg/netpol/eval/connection.go +++ /dev/null @@ -1,65 +0,0 @@ -package eval - -import ( - v1 "k8s.io/api/core/v1" - - "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" -) - -// 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 - // AllConnections returns true if all ports are allowed for all protocols - AllConnections() bool - // IsEmpty returns true if no connection is allowed - IsEmpty() bool -} - -// PortRange describes a port or a range of ports for allowed traffic -// If start port equals end port, it represents a single port -type PortRange interface { - // Start is the start port - Start() int64 - // End is the end port - End() int64 - // String returns a string representation of the PortRange object - String() string -} - -// k8sConnectionSetWrapper implements the Connection interface -type k8sConnectionSetWrapper struct { - protocolsAndPortsMap map[v1.Protocol][]PortRange - connectionSet k8s.ConnectionSet -} - -func (c *k8sConnectionSetWrapper) ProtocolsAndPortsMap() map[v1.Protocol][]PortRange { - return c.protocolsAndPortsMap -} - -func (c *k8sConnectionSetWrapper) AllConnections() bool { - return c.connectionSet.AllowAll -} -func (c *k8sConnectionSetWrapper) IsEmpty() bool { - return c.connectionSet.IsEmpty() -} - -func (c *k8sConnectionSetWrapper) ConnectionSet() k8s.ConnectionSet { - return c.connectionSet -} - -// convert an input k8s.ConnectionSet object to a connectionObj that implements Connection interface -func getConnectionObject(conn k8s.ConnectionSet) Connection { - protocolsMap := conn.ProtocolsAndPortsMap() - res := &k8sConnectionSetWrapper{ - protocolsAndPortsMap: make(map[v1.Protocol][]PortRange, len(protocolsMap)), - connectionSet: conn, - } - for protocol, ports := range protocolsMap { - res.protocolsAndPortsMap[protocol] = make([]PortRange, len(ports)) - for i, portRange := range ports { - res.protocolsAndPortsMap[protocol][i] = portRange - } - } - return res -} diff --git a/pkg/netpol/eval/eval_test.go b/pkg/netpol/eval/eval_test.go index b53be71d..72fb00db 100644 --- a/pkg/netpol/eval/eval_test.go +++ b/pkg/netpol/eval/eval_test.go @@ -18,7 +18,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/yaml" - "github.com/np-guard/netpol-analyzer/pkg/netpol/eval/internal/k8s" + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/testutils" "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" @@ -1697,7 +1697,7 @@ func connectionsString(pe *PolicyEngine, srcPod, dstPod, protocol, port string, var allowedConnectionsStr string var err error if allConnections { - var allowedConnections k8s.ConnectionSet + var allowedConnections *common.ConnectionSet allowedConnections, err = pe.allAllowedConnections(srcPod, dstPod) if err == nil { allowedConnectionsStr = allowedConnections.String() diff --git a/pkg/netpol/eval/internal/k8s/ipBlock.go b/pkg/netpol/eval/internal/k8s/ipBlock.go index f2fece4f..a8faf1d0 100644 --- a/pkg/netpol/eval/internal/k8s/ipBlock.go +++ b/pkg/netpol/eval/internal/k8s/ipBlock.go @@ -7,6 +7,8 @@ import ( "sort" "strconv" "strings" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" ) const ( @@ -20,7 +22,7 @@ const ( // IPBlock captures a set of ip ranges type IPBlock struct { - ipRange CanonicalIntervalSet + ipRange common.CanonicalIntervalSet } // ToIPRanges returns a string of the ip ranges in the current IPBlock object @@ -64,7 +66,7 @@ func (b *IPBlock) split() []*IPBlock { res := make([]*IPBlock, len(b.ipRange.IntervalSet)) for index, ipr := range b.ipRange.IntervalSet { newBlock := IPBlock{} - newBlock.ipRange.IntervalSet = append(newBlock.ipRange.IntervalSet, Interval{Start: ipr.Start, End: ipr.End}) + newBlock.ipRange.IntervalSet = append(newBlock.ipRange.IntervalSet, common.Interval{Start: ipr.Start, End: ipr.End}) res[index] = &newBlock } return res @@ -132,7 +134,7 @@ func addIntervalToList(ipbNew *IPBlock, ipbList []*IPBlock) []*IPBlock { // NewIPBlock returns an IPBlock object from input cidr str an exceptions cidr str func NewIPBlock(cidr string, exceptions []string) (*IPBlock, error) { - res := IPBlock{ipRange: CanonicalIntervalSet{}} + res := IPBlock{ipRange: common.CanonicalIntervalSet{}} interval, err := cidrToInterval(cidr) if err != nil { return nil, err @@ -170,10 +172,10 @@ func cidrToIPRange(cidr string) (beginning, end int64, err error) { return int64(start), int64(finish), nil } -func cidrToInterval(cidr string) (*Interval, error) { +func cidrToInterval(cidr string) (*common.Interval, error) { start, end, err := cidrToIPRange(cidr) if err != nil { return nil, err } - return &Interval{Start: start, End: end}, nil + return &common.Interval{Start: start, End: end}, nil } diff --git a/pkg/netpol/eval/internal/k8s/netpol.go b/pkg/netpol/eval/internal/k8s/netpol.go index 3e6bd06d..adfba628 100644 --- a/pkg/netpol/eval/internal/k8s/netpol.go +++ b/pkg/netpol/eval/internal/k8s/netpol.go @@ -24,6 +24,8 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" ) // NetworkPolicy is an alias for k8s network policy object @@ -55,13 +57,11 @@ func getProtocolStr(p *v1.Protocol) string { } func (np *NetworkPolicy) convertNamedPort(namedPort string, pod *Pod) (int32, error) { - for _, containerPort := range pod.Ports { - if namedPort == containerPort.Name { - return containerPort.ContainerPort, nil - } + port, err := pod.ConvertPodNamedPort(namedPort) + if err != nil { + return 0, np.netpolErr(namedPortErrTitle, err.Error()) } - errStr := fmt.Sprintf("named port is not defined in a selected workload %s", pod.Owner.Name) - return 0, np.netpolErr(namedPortErrTitle, errStr) + return port, nil } func (np *NetworkPolicy) getPortsRange(port *intstr.IntOrString, endPort *int32, dst Peer) (startNum, endNum int32, err error) { @@ -86,19 +86,20 @@ func (np *NetworkPolicy) getPortsRange(port *intstr.IntOrString, endPort *int32, return start, end, nil } -func (np *NetworkPolicy) ruleConnections(rulePorts []netv1.NetworkPolicyPort, dst Peer) (ConnectionSet, error) { +func (np *NetworkPolicy) ruleConnections(rulePorts []netv1.NetworkPolicyPort, dst Peer) (*common.ConnectionSet, error) { if len(rulePorts) == 0 { - return MakeConnectionSet(true), nil // If this field is empty or missing, this rule matches all ports (traffic not restricted by port) + return common.MakeConnectionSet(true), nil // If this field is empty or missing, this rule matches all ports + // (traffic not restricted by port) } - res := MakeConnectionSet(false) + res := common.MakeConnectionSet(false) for i := range rulePorts { protocol := v1.ProtocolTCP if rulePorts[i].Protocol != nil { protocol = *rulePorts[i].Protocol } - ports := PortSet{} + ports := common.PortSet{} if rulePorts[i].Port == nil { - ports = MakePortSet(true) + ports = common.MakePortSet(true) } else { startPort, endPort, err := np.getPortsRange(rulePorts[i].Port, rulePorts[i].EndPort, dst) if err != nil { @@ -255,8 +256,8 @@ func (np *NetworkPolicy) EgressAllowedConn(dst Peer, protocol, port string) (boo } // GetEgressAllowedConns returns the set of allowed connetions from any captured pod to the destination peer -func (np *NetworkPolicy) GetEgressAllowedConns(dst Peer) (ConnectionSet, error) { - res := MakeConnectionSet(false) +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 @@ -280,8 +281,8 @@ func (np *NetworkPolicy) GetEgressAllowedConns(dst Peer) (ConnectionSet, error) } // GetIngressAllowedConns returns the set of allowed connections to a captured dst pod from the src peer -func (np *NetworkPolicy) GetIngressAllowedConns(src, dst Peer) (ConnectionSet, error) { - res := MakeConnectionSet(false) +func (np *NetworkPolicy) GetIngressAllowedConns(src, dst Peer) (*common.ConnectionSet, error) { + res := common.MakeConnectionSet(false) for _, rule := range np.Spec.Ingress { rulePeers := rule.From rulePorts := rule.Ports diff --git a/pkg/netpol/eval/internal/k8s/peer.go b/pkg/netpol/eval/internal/k8s/peer.go index 86dcc9ad..56bd4234 100644 --- a/pkg/netpol/eval/internal/k8s/peer.go +++ b/pkg/netpol/eval/internal/k8s/peer.go @@ -78,6 +78,9 @@ func (p *WorkloadPeer) Kind() string { } func (p *WorkloadPeer) String() string { + if p.Pod.FakePod { + return "{" + p.Pod.Name + "}" + } return types.NamespacedName{Name: p.Name(), Namespace: p.Namespace()}.String() + "[" + p.Kind() + "]" } diff --git a/pkg/netpol/eval/internal/k8s/pod.go b/pkg/netpol/eval/internal/k8s/pod.go index 303a5793..f2285a21 100644 --- a/pkg/netpol/eval/internal/k8s/pod.go +++ b/pkg/netpol/eval/internal/k8s/pod.go @@ -25,6 +25,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "github.com/np-guard/netpol-analyzer/pkg/netpol/common" "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" ) @@ -34,6 +35,7 @@ const defaultPortsListSize = 8 type Pod struct { Name string Namespace string + FakePod bool // this flag is used to indicate if the pod is created from scanner objects or fake (ingress-controller) Labels map[string]string IPs []corev1.PodIP Ports []corev1.ContainerPort @@ -66,6 +68,7 @@ func PodFromCoreObject(p *corev1.Pod) (*Pod, error) { Ports: make([]corev1.ContainerPort, 0, defaultPortsListSize), HostIP: p.Status.HostIP, Owner: Owner{}, + FakePod: false, } copy(pr.IPs, p.Status.PodIPs) @@ -187,6 +190,7 @@ func PodsFromWorkloadObject(workload interface{}, kind string) ([]*Pod, error) { pod.Ports = make([]corev1.ContainerPort, 0, defaultPortsListSize) pod.HostIP = getFakePodIP() pod.Owner = Owner{Name: workloadName, Kind: kind, APIVersion: APIVersion} + pod.FakePod = false for k, v := range podTemplate.Labels { pod.Labels[k] = v } @@ -212,16 +216,26 @@ func getFakePodIP() string { return scan.IPv4LoopbackAddr } -func (pod *Pod) PodExposedProtocolsAndPorts() ConnectionSet { - res := MakeConnectionSet(false) +// PodExposedTCPConnections returns TCP connections exposed by a pod +func (pod *Pod) PodExposedTCPConnections() *common.ConnectionSet { + res := common.MakeConnectionSet(false) for _, cPort := range pod.Ports { protocol := corev1.ProtocolTCP - if cPort.Protocol != "" { - protocol = cPort.Protocol + if cPort.Protocol == "" || protocol == corev1.ProtocolTCP { + ports := common.PortSet{} + ports.AddPortRange(int64(cPort.ContainerPort), int64(cPort.ContainerPort)) + res.AddConnection(protocol, ports) } - ports := PortSet{} - ports.AddPortRange(int64(cPort.ContainerPort), int64(cPort.ContainerPort)) - res.AddConnection(protocol, ports) } return res } + +// ConvertPodNamedPort returns the ContainerPort number that matches the named port +func (pod *Pod) ConvertPodNamedPort(namedPort string) (int32, error) { + for _, containerPort := range pod.Ports { + if namedPort == containerPort.Name { + return containerPort.ContainerPort, nil + } + } + return 0, errors.New("named port is not defined in a selected workload " + pod.Owner.Name) +} diff --git a/pkg/netpol/eval/resources.go b/pkg/netpol/eval/resources.go index a868e56b..2ddbb4cb 100644 --- a/pkg/netpol/eval/resources.go +++ b/pkg/netpol/eval/resources.go @@ -1,6 +1,7 @@ package eval import ( + "errors" "fmt" appsv1 "k8s.io/api/apps/v1" @@ -71,9 +72,7 @@ func NewPolicyEngineWithObjects(objects []scan.K8sObject) (*PolicyEngine, error) err = pe.UpsertObject(obj.Job) case scan.CronJob: err = pe.UpsertObject(obj.CronJob) - case scan.Service: - continue - case scan.Route: + case scan.Service, scan.Route, scan.Ingress: continue default: err = fmt.Errorf("unsupported kind: %s", obj.Kind) @@ -90,16 +89,7 @@ func (pe *PolicyEngine) resolveMissingNamespaces() error { for _, pod := range pe.podsMap { ns := pod.Namespace if _, ok := pe.namspacesMap[ns]; !ok { - // create a ns object and upsert to PolicyEngine - nsObj := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: ns, - Labels: map[string]string{ - "kubernetes.io/metadata.name": ns, - }, - }, - } - if err := pe.upsertNamespace(nsObj); err != nil { + if err := pe.resolveSingleMissingNamespace(ns); err != nil { return err } } @@ -107,6 +97,22 @@ func (pe *PolicyEngine) resolveMissingNamespaces() error { return nil } +// resolveSingleMissingNamespace create a ns object and upsert to PolicyEngine +func (pe *PolicyEngine) resolveSingleMissingNamespace(ns string) error { + nsObj := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns, + Labels: map[string]string{ + "kubernetes.io/metadata.name": ns, + }, + }, + } + if err := pe.upsertNamespace(nsObj); err != nil { + return err + } + return nil +} + // SetResources: updates the set of all relevant k8s resources // This function *may* be used as convenience to set the initial policy engine state from a // set of resources (e.g., retrieved via List from a cluster). @@ -341,3 +347,30 @@ func (pe *PolicyEngine) GetSelectedPeers(selectors labels.Selector, namespace st } return res } + +// ConvertPeerNamedPort returns the peer.pod.containerPort matching the named port of the peer +func (pe *PolicyEngine) ConvertPeerNamedPort(namedPort string, peer Peer) (int32, error) { + switch currPeer := peer.(type) { + case *k8s.WorkloadPeer: + return currPeer.Pod.ConvertPodNamedPort(namedPort) + case *k8s.PodPeer: + return currPeer.Pod.ConvertPodNamedPort(namedPort) + default: + return 0, errors.New("peer type does not have ports") // should not get here + } +} + +// AddPodByNameAndNamespace adds a new fake pod to the pe.podsMap +func (pe *PolicyEngine) AddPodByNameAndNamespace(name, ns string) (Peer, error) { + podStr := types.NamespacedName{Namespace: ns, Name: name}.String() + newPod := &k8s.Pod{ + Name: name, + Namespace: ns, + FakePod: true, + } + if err := pe.resolveSingleMissingNamespace(ns); err != nil { + return nil, err + } + pe.podsMap[podStr] = newPod + return &k8s.WorkloadPeer{Pod: newPod}, nil +} diff --git a/pkg/netpol/ingressanalyzer/ingress_analyzer.go b/pkg/netpol/ingressanalyzer/ingress_analyzer.go deleted file mode 100644 index a1097244..00000000 --- a/pkg/netpol/ingressanalyzer/ingress_analyzer.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2022 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ingressanalyzer - -import ( - "errors" - - ocroutev1 "github.com/openshift/api/route/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - - "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" - "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" - "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" -) - -// IngressAnalyzer provides API to analyze Ingress/Route resources, to allow inferring potential connectivity -// from ingress-controller to pods in the cluster -type IngressAnalyzer struct { - logger logger.Logger - pe *eval.PolicyEngine // a struct type that includes the podsMap and - // some functionality on pods and namespaces which is required for ingress analyzing - servicesToPeersMap map[string]map[string][]eval.Peer // map from namespace to map from service name to its selected workloads - routesToServicesMap map[string]map[string][]string // map from namespace to map from route name to its target service names -} - -// NewIngressAnalyzerWithObjects returns a new IngressAnalyzer with relevant objects -func NewIngressAnalyzerWithObjects(objects []scan.K8sObject, pe *eval.PolicyEngine, l logger.Logger) (*IngressAnalyzer, error) { - ia := &IngressAnalyzer{ - servicesToPeersMap: make(map[string]map[string][]eval.Peer), - routesToServicesMap: make(map[string]map[string][]string), - logger: l, - pe: pe, - } - - var err error - for _, obj := range objects { - switch obj.Kind { - case scan.Service: - err = ia.mapServiceToPeers(obj.Service) - case scan.Route: - err = ia.mapRouteToServices(obj.Route) - } - if err != nil { - return nil, err - } - } - return ia, err -} - -///////////////////////////////////////////////////////////////////////////////////////////////// -// services analysis - -const ( - selectorError = "selector conversion error" - missingSelectorError = "error : K8s Service without selectors is not supported" -) - -// this function populates servicesToPeersMap -func (ia *IngressAnalyzer) mapServiceToPeers(svc *corev1.Service) error { - // get peers selected by the service selectors - peers, err := ia.getServicePeers(svc) - if err != nil { - return err - } - if _, ok := ia.servicesToPeersMap[svc.Namespace]; !ok { - ia.servicesToPeersMap[svc.Namespace] = make(map[string][]eval.Peer) - } - ia.servicesToPeersMap[svc.Namespace][svc.Name] = peers - return nil -} - -func (ia *IngressAnalyzer) getServicePeers(svc *corev1.Service) ([]eval.Peer, error) { - svcStr := types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}.String() - if svc.Spec.Selector == nil { - return nil, errors.New(scan.Service + " " + svcStr + " " + missingSelectorError) - } - svcLabelsSelector, err := convertServiceSelectorToLabelSelector(svc.Spec.Selector, svcStr) - if err != nil { - return nil, err - } - return ia.pe.GetSelectedPeers(svcLabelsSelector, svc.Namespace), nil -} - -// utility func -func convertServiceSelectorToLabelSelector(svcSelector map[string]string, svcStr string) (labels.Selector, error) { - labelsSelector := metav1.LabelSelector{MatchLabels: svcSelector} - selectorRes, err := metav1.LabelSelectorAsSelector(&labelsSelector) - if err != nil { - return nil, errors.New(scan.Service + " " + svcStr + " " + selectorError) - } - return selectorRes, nil -} - -// ////////////////////////////////////////////////////////////////////////////////////////////// -// routes analysis - -const ( - allowedTargetKind = scan.Service - routeTargetKindErr = "target kind error" - routeBackendsErr = "alternate backends error" -) - -// this function populates routesToServicesMap -func (ia *IngressAnalyzer) mapRouteToServices(rt *ocroutev1.Route) error { - services, err := getRouteServices(rt) - if err != nil { - return err - } - if _, ok := ia.routesToServicesMap[rt.Namespace]; !ok { - ia.routesToServicesMap[rt.Namespace] = make(map[string][]string) - } - ia.routesToServicesMap[rt.Namespace][rt.Name] = services - return nil -} - -func getRouteServices(rt *ocroutev1.Route) ([]string, error) { - routeStr := types.NamespacedName{Namespace: rt.Namespace, Name: rt.Name}.String() - // Currently, only 'Service' is allowed as the kind of target that the route is referring to. - if rt.Spec.To.Kind != "" && rt.Spec.To.Kind != allowedTargetKind { - return nil, errors.New(scan.Route + " " + routeStr + ": " + routeTargetKindErr) - } - - targetServices := make([]string, len(rt.Spec.AlternateBackends)+1) - targetServices[0] = rt.Spec.To.Name - for i, backend := range rt.Spec.AlternateBackends { - if backend.Kind != "" && backend.Kind != allowedTargetKind { - return nil, errors.New(scan.Route + " " + routeStr + ": " + routeBackendsErr) - } - targetServices[i+1] = backend.Name - } - return targetServices, nil -} - -////////////////////////////////////////////////////////////////////////////////////////////// -// Ingress allowed connections - -// AllowedIngressConnections returns a map of the possible connections from ingress-controller pod to workload peers, -// as inferred from Ingress and Route resources. The map is from a workload name to its connection object. -func (ia *IngressAnalyzer) AllowedIngressConnections() map[string]eval.Connection { - // if there is at least one route/ ingress object that targets a service which selects a dst peer, - // then we have ingress connections to that peer - - // get all targeted workload peers and compute allowed conns of each workload peer - res := make(map[string]eval.Connection) - for ns, rtSvcMap := range ia.routesToServicesMap { - // if there are no services in same namespace of the route, the routes in this ns will be skipped - if _, ok := ia.servicesToPeersMap[ns]; !ok { - continue - } - - for _, svcList := range rtSvcMap { - routeTargetPeers := ia.getRouteTargetedPeers(ns, svcList) - // avoid duplicates in the result - for _, peer := range routeTargetPeers { - peerStr := types.NamespacedName{Name: peer.Name(), Namespace: peer.Namespace()}.String() - if _, ok := res[peerStr]; !ok { - res[peerStr] = ia.pe.GetPeerExposedProtocolsAndPorts(peer) - } - } - } - } - - return res -} - -func (ia *IngressAnalyzer) getRouteTargetedPeers(ns string, svcList []string) []eval.Peer { - var res []eval.Peer - for _, svc := range svcList { - peers, ok := ia.servicesToPeersMap[ns][svc] - if !ok { - ia.logger.Warnf("ignoring target service " + svc + " : service not found") - } - res = append(res, peers...) - } - return res -} diff --git a/pkg/netpol/ingressanalyzer/ingress_analyzer_test.go b/pkg/netpol/ingressanalyzer/ingress_analyzer_test.go deleted file mode 100644 index a70d96de..00000000 --- a/pkg/netpol/ingressanalyzer/ingress_analyzer_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package ingressanalyzer - -import ( - "path/filepath" - "testing" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - - "github.com/stretchr/testify/require" - - "github.com/np-guard/netpol-analyzer/pkg/netpol/eval" - "github.com/np-guard/netpol-analyzer/pkg/netpol/internal/testutils" - "github.com/np-guard/netpol-analyzer/pkg/netpol/logger" - "github.com/np-guard/netpol-analyzer/pkg/netpol/scan" -) - -// global scanner object for testing -var scanner = scan.NewResourcesScanner(logger.NewDefaultLogger(), false, filepath.WalkDir) - -func TestIngressAnalyzerWithRoutes(t *testing.T) { - routesNamespace := "frontend" - routeNameExample := "webapp" - path := filepath.Join(testutils.GetTestsDir(), "acs_security_frontend_demos") - objects, processingErrs := scanner.FilesToObjectsList(path) - require.Empty(t, processingErrs) - pe, err := eval.NewPolicyEngineWithObjects(objects) - require.Empty(t, err) - ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger()) - require.Empty(t, err) - // routes map includes 1 namespace - require.Len(t, ia.routesToServicesMap, 1) - // the routes namespace includes 2 different routes - require.Len(t, ia.routesToServicesMap[routesNamespace], 2) - // each route is mapped to 1 service - check 1 for example - require.Len(t, ia.routesToServicesMap[routesNamespace][routeNameExample], 1) -} - -type ingressToPod struct { - podName string - podNamespace string - allConnections bool - port int64 - protocol string -} - -func TestIngressAnalyzerConnectivityToAPod(t *testing.T) { - testingEntries := []ingressToPod{ - { - podName: "asset-cache", - podNamespace: "frontend", - allConnections: false, - port: 8080, - protocol: "TCP", - }, - { - podName: "webapp", - podNamespace: "frontend", - allConnections: false, - port: 8080, - protocol: "TCP", - }, - } - path := filepath.Join(testutils.GetTestsDir(), "acs_security_frontend_demos") - objects, processingErrs := scanner.FilesToObjectsList(path) - require.Empty(t, processingErrs) - pe, err := eval.NewPolicyEngineWithObjects(objects) - require.Empty(t, err) - ia, err := NewIngressAnalyzerWithObjects(objects, pe, logger.NewDefaultLogger()) - require.Empty(t, err) - ingressConns := ia.AllowedIngressConnections() - require.Empty(t, err) - for _, entry := range testingEntries { - peerStr := types.NamespacedName{Name: entry.podName, Namespace: entry.podNamespace}.String() - require.Contains(t, ingressConns, peerStr) - conn := ingressConns[peerStr] - require.Equal(t, conn.AllConnections(), entry.allConnections) - if !conn.AllConnections() { - require.Contains(t, conn.ProtocolsAndPortsMap(), v1.Protocol(entry.protocol)) - connPortRange := conn.ProtocolsAndPortsMap()[v1.Protocol(entry.protocol)] - require.Equal(t, connPortRange[0].Start(), entry.port) - } - } -} diff --git a/tests/acs_security_frontend_demos/asset_cache_route.yaml b/tests/acs_security_frontend_demos/asset_cache_route.yaml index 87d7f4d3..01866585 100644 --- a/tests/acs_security_frontend_demos/asset_cache_route.yaml +++ b/tests/acs_security_frontend_demos/asset_cache_route.yaml @@ -10,6 +10,4 @@ spec: kind: Service name: asset-cache-service weight: 100 - port: - targetPort: http wildcardPolicy: None \ No newline at end of file diff --git a/tests/acs_security_frontend_demos/connlist_output.csv b/tests/acs_security_frontend_demos/connlist_output.csv new file mode 100644 index 00000000..39a740ce --- /dev/null +++ b/tests/acs_security_frontend_demos/connlist_output.csv @@ -0,0 +1 @@ +src,dst,conn diff --git a/tests/acs_security_frontend_demos/connlist_output.dot b/tests/acs_security_frontend_demos/connlist_output.dot new file mode 100644 index 00000000..27f36df1 --- /dev/null +++ b/tests/acs_security_frontend_demos/connlist_output.dot @@ -0,0 +1,2 @@ +digraph { +} \ No newline at end of file diff --git a/tests/acs_security_frontend_demos/connlist_output.json b/tests/acs_security_frontend_demos/connlist_output.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/tests/acs_security_frontend_demos/connlist_output.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/acs_security_frontend_demos/connlist_output.md b/tests/acs_security_frontend_demos/connlist_output.md new file mode 100644 index 00000000..6a8c719f --- /dev/null +++ b/tests/acs_security_frontend_demos/connlist_output.md @@ -0,0 +1,2 @@ +| src | dst | conn | +|-----|-----|------| \ No newline at end of file diff --git a/tests/acs_security_frontend_demos/connlist_output.txt b/tests/acs_security_frontend_demos/connlist_output.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/acs_security_frontend_demos/webapp_route.yaml b/tests/acs_security_frontend_demos/webapp_route.yaml index bae995db..fd5f02f4 100644 --- a/tests/acs_security_frontend_demos/webapp_route.yaml +++ b/tests/acs_security_frontend_demos/webapp_route.yaml @@ -10,6 +10,4 @@ spec: kind: Service name: webapp-service weight: 100 - port: - targetPort: http wildcardPolicy: None \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.csv b/tests/demo_app_with_routes_and_ingress/connlist_output.csv new file mode 100644 index 00000000..7eeb506a --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/connlist_output.csv @@ -0,0 +1,16 @@ +src,dst,conn +0.0.0.0-255.255.255.255,helloworld/hello-world[Deployment],All Connections +0.0.0.0-255.255.255.255,ingressworld/ingress-world[Deployment],All Connections +0.0.0.0-255.255.255.255,routeworld/route-world[Deployment],All Connections +helloworld/hello-world[Deployment],0.0.0.0-255.255.255.255,All Connections +helloworld/hello-world[Deployment],ingressworld/ingress-world[Deployment],All Connections +helloworld/hello-world[Deployment],routeworld/route-world[Deployment],All Connections +ingressworld/ingress-world[Deployment],0.0.0.0-255.255.255.255,All Connections +ingressworld/ingress-world[Deployment],helloworld/hello-world[Deployment],All Connections +ingressworld/ingress-world[Deployment],routeworld/route-world[Deployment],All Connections +routeworld/route-world[Deployment],0.0.0.0-255.255.255.255,All Connections +routeworld/route-world[Deployment],helloworld/hello-world[Deployment],All Connections +routeworld/route-world[Deployment],ingressworld/ingress-world[Deployment],All Connections +{ingress-controller},helloworld/hello-world[Deployment],TCP 8000 +{ingress-controller},ingressworld/ingress-world[Deployment],TCP 8090 +{ingress-controller},routeworld/route-world[Deployment],TCP 8060 diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.dot b/tests/demo_app_with_routes_and_ingress/connlist_output.dot new file mode 100644 index 00000000..04a3b258 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/connlist_output.dot @@ -0,0 +1,22 @@ +digraph { + "0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="red2" fontcolor="red2"] + "helloworld/hello-world[Deployment]" [label="helloworld/hello-world[Deployment]" color="blue" fontcolor="blue"] + "ingressworld/ingress-world[Deployment]" [label="ingressworld/ingress-world[Deployment]" color="blue" fontcolor="blue"] + "routeworld/route-world[Deployment]" [label="routeworld/route-world[Deployment]" color="blue" fontcolor="blue"] + "{ingress-controller}" [label="{ingress-controller}" color="blue" fontcolor="blue"] + "0.0.0.0-255.255.255.255" -> "helloworld/hello-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "ingressworld/ingress-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "routeworld/route-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "helloworld/hello-world[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "helloworld/hello-world[Deployment]" -> "ingressworld/ingress-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "helloworld/hello-world[Deployment]" -> "routeworld/route-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world[Deployment]" -> "helloworld/hello-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world[Deployment]" -> "routeworld/route-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "routeworld/route-world[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "routeworld/route-world[Deployment]" -> "helloworld/hello-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "routeworld/route-world[Deployment]" -> "ingressworld/ingress-world[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "helloworld/hello-world[Deployment]" [label="TCP 8000" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "ingressworld/ingress-world[Deployment]" [label="TCP 8090" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "routeworld/route-world[Deployment]" [label="TCP 8060" color="gold2" fontcolor="darkgreen"] +} \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.dot.png b/tests/demo_app_with_routes_and_ingress/connlist_output.dot.png new file mode 100644 index 00000000..793dd7db Binary files /dev/null and b/tests/demo_app_with_routes_and_ingress/connlist_output.dot.png differ diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.json b/tests/demo_app_with_routes_and_ingress/connlist_output.json new file mode 100644 index 00000000..93374ca6 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/connlist_output.json @@ -0,0 +1,77 @@ +[ + { + "src": "0.0.0.0-255.255.255.255", + "dst": "helloworld/hello-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "ingressworld/ingress-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "routeworld/route-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "helloworld/hello-world[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "helloworld/hello-world[Deployment]", + "dst": "ingressworld/ingress-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "helloworld/hello-world[Deployment]", + "dst": "routeworld/route-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world[Deployment]", + "dst": "helloworld/hello-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world[Deployment]", + "dst": "routeworld/route-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "routeworld/route-world[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "routeworld/route-world[Deployment]", + "dst": "helloworld/hello-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "routeworld/route-world[Deployment]", + "dst": "ingressworld/ingress-world[Deployment]", + "conn": "All Connections" + }, + { + "src": "{ingress-controller}", + "dst": "helloworld/hello-world[Deployment]", + "conn": "TCP 8000" + }, + { + "src": "{ingress-controller}", + "dst": "ingressworld/ingress-world[Deployment]", + "conn": "TCP 8090" + }, + { + "src": "{ingress-controller}", + "dst": "routeworld/route-world[Deployment]", + "conn": "TCP 8060" + } +] \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.md b/tests/demo_app_with_routes_and_ingress/connlist_output.md new file mode 100644 index 00000000..cdfadcf9 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/connlist_output.md @@ -0,0 +1,17 @@ +| src | dst | conn | +|-----|-----|------| +| 0.0.0.0-255.255.255.255 | helloworld/hello-world[Deployment] | All Connections | +| 0.0.0.0-255.255.255.255 | ingressworld/ingress-world[Deployment] | All Connections | +| 0.0.0.0-255.255.255.255 | routeworld/route-world[Deployment] | All Connections | +| helloworld/hello-world[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| helloworld/hello-world[Deployment] | ingressworld/ingress-world[Deployment] | All Connections | +| helloworld/hello-world[Deployment] | routeworld/route-world[Deployment] | All Connections | +| ingressworld/ingress-world[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| ingressworld/ingress-world[Deployment] | helloworld/hello-world[Deployment] | All Connections | +| ingressworld/ingress-world[Deployment] | routeworld/route-world[Deployment] | All Connections | +| routeworld/route-world[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| routeworld/route-world[Deployment] | helloworld/hello-world[Deployment] | All Connections | +| routeworld/route-world[Deployment] | ingressworld/ingress-world[Deployment] | All Connections | +| {ingress-controller} | helloworld/hello-world[Deployment] | TCP 8000 | +| {ingress-controller} | ingressworld/ingress-world[Deployment] | TCP 8090 | +| {ingress-controller} | routeworld/route-world[Deployment] | TCP 8060 | \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/connlist_output.txt b/tests/demo_app_with_routes_and_ingress/connlist_output.txt new file mode 100644 index 00000000..4b6995fe --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/connlist_output.txt @@ -0,0 +1,15 @@ +0.0.0.0-255.255.255.255 => helloworld/hello-world[Deployment] : All Connections +0.0.0.0-255.255.255.255 => ingressworld/ingress-world[Deployment] : All Connections +0.0.0.0-255.255.255.255 => routeworld/route-world[Deployment] : All Connections +helloworld/hello-world[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +helloworld/hello-world[Deployment] => ingressworld/ingress-world[Deployment] : All Connections +helloworld/hello-world[Deployment] => routeworld/route-world[Deployment] : All Connections +ingressworld/ingress-world[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +ingressworld/ingress-world[Deployment] => helloworld/hello-world[Deployment] : All Connections +ingressworld/ingress-world[Deployment] => routeworld/route-world[Deployment] : All Connections +routeworld/route-world[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +routeworld/route-world[Deployment] => helloworld/hello-world[Deployment] : All Connections +routeworld/route-world[Deployment] => ingressworld/ingress-world[Deployment] : All Connections +{ingress-controller} => helloworld/hello-world[Deployment] : TCP 8000 +{ingress-controller} => ingressworld/ingress-world[Deployment] : TCP 8090 +{ingress-controller} => routeworld/route-world[Deployment] : TCP 8060 \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/hello-world-deployment-and-service.yaml b/tests/demo_app_with_routes_and_ingress/hello-world-deployment-and-service.yaml new file mode 100644 index 00000000..c7427777 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/hello-world-deployment-and-service.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: helloworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-world + namespace: helloworld + labels: + app: hello-world +spec: + replicas: 2 + selector: + matchLabels: + app: hello-world + template: + metadata: + labels: + app: hello-world + spec: + containers: + - name: helloworld + image: quay.io/shfa/hello-world:latest + ports: + - containerPort: 8000 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: hello-world + namespace: helloworld +spec: + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + selector: + app: hello-world diff --git a/tests/demo_app_with_routes_and_ingress/hello-world-ingress.yaml b/tests/demo_app_with_routes_and_ingress/hello-world-ingress.yaml new file mode 100644 index 00000000..82c65ccc --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/hello-world-ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hello-world + namespace: helloworld +spec: + rules: + - host: hello.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: hello-world + port: + number: 8000 \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/hello-world-route.yaml b/tests/demo_app_with_routes_and_ingress/hello-world-route.yaml new file mode 100644 index 00000000..d8a9cc5a --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/hello-world-route.yaml @@ -0,0 +1,12 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: hello-world + namespace: helloworld +spec: + port: + targetPort: 8000 + to: + kind: Service + name: hello-world + weight: 100 \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/ingress-world-deployment-and-service.yaml b/tests/demo_app_with_routes_and_ingress/ingress-world-deployment-and-service.yaml new file mode 100644 index 00000000..16bc42d2 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/ingress-world-deployment-and-service.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ingressworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-world + namespace: ingressworld + labels: + app: ingress-world +spec: + replicas: 2 + selector: + matchLabels: + app: ingress-world + template: + metadata: + labels: + app: ingress-world + spec: + containers: + - name: ingressworld + image: quay.io/shfa/ingress-world:latest + ports: + - containerPort: 8090 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-world + namespace: ingressworld +spec: + ports: + - protocol: TCP + port: 8090 + targetPort: 8090 + selector: + app: ingress-world diff --git a/tests/demo_app_with_routes_and_ingress/ingress-world-ingress.yaml b/tests/demo_app_with_routes_and_ingress/ingress-world-ingress.yaml new file mode 100644 index 00000000..c833fef1 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/ingress-world-ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-world + namespace: ingressworld +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world + port: + number: 8090 \ No newline at end of file diff --git a/tests/demo_app_with_routes_and_ingress/route-world-deployment-and-service.yaml b/tests/demo_app_with_routes_and_ingress/route-world-deployment-and-service.yaml new file mode 100644 index 00000000..adaf23e4 --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/route-world-deployment-and-service.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: routeworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: route-world + namespace: routeworld + labels: + app: route-world +spec: + replicas: 2 + selector: + matchLabels: + app: route-world + template: + metadata: + labels: + app: route-world + spec: + containers: + - name: routeworld + image: quay.io/shfa/route-world:latest + ports: + - containerPort: 8060 + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: route-world + namespace: routeworld +spec: + ports: + - protocol: TCP + port: 8060 + targetPort: 8060 + selector: + app: route-world diff --git a/tests/demo_app_with_routes_and_ingress/route-world-route.yaml b/tests/demo_app_with_routes_and_ingress/route-world-route.yaml new file mode 100644 index 00000000..a6ad431d --- /dev/null +++ b/tests/demo_app_with_routes_and_ingress/route-world-route.yaml @@ -0,0 +1,12 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: route-world + namespace: routeworld +spec: + port: + targetPort: 8060 + to: + kind: Service + name: route-world + weight: 100 \ No newline at end of file diff --git a/tests/ingress_example_with_named_port/deployment.yaml b/tests/ingress_example_with_named_port/deployment.yaml new file mode 100644 index 00000000..68746b3e --- /dev/null +++ b/tests/ingress_example_with_named_port/deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Service +metadata: + name: hello-service + namespace: hello +spec: + selector: + app: hello + ports: + - name: http + protocol: TCP + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hello-deployment + namespace: hello + labels: + app: hello +spec: + replicas: 1 + selector: + matchLabels: + app: hello + template: + metadata: + labels: + app: hello + spec: + containers: + - name: hello-container + image: gcr.io/shfa/hello-app:1.0 + ports: + - name: http + containerPort: 8080 \ No newline at end of file diff --git a/tests/ingress_example_with_named_port/ingress.yaml b/tests/ingress_example_with_named_port/ingress.yaml new file mode 100644 index 00000000..c5166091 --- /dev/null +++ b/tests/ingress_example_with_named_port/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hello-ingress + namespace: hello +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: hello-service + port: + name: http \ No newline at end of file diff --git a/tests/k8s_ingress_test/connlist_output.csv b/tests/k8s_ingress_test/connlist_output.csv new file mode 100644 index 00000000..e1c0a73b --- /dev/null +++ b/tests/k8s_ingress_test/connlist_output.csv @@ -0,0 +1,44 @@ +src,dst,conn +0.0.0.0-255.255.255.255,default/details-v1-79f774bdb9[ReplicaSet],All Connections +0.0.0.0-255.255.255.255,default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +0.0.0.0-255.255.255.255,default/ratings-v1-b6994bb9[ReplicaSet],All Connections +0.0.0.0-255.255.255.255,default/reviews-v1-545db77b95[ReplicaSet],All Connections +0.0.0.0-255.255.255.255,default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +0.0.0.0-255.255.255.255,default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/details-v1-79f774bdb9[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/details-v1-79f774bdb9[ReplicaSet],default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +default/details-v1-79f774bdb9[ReplicaSet],default/ratings-v1-b6994bb9[ReplicaSet],All Connections +default/details-v1-79f774bdb9[ReplicaSet],default/reviews-v1-545db77b95[ReplicaSet],All Connections +default/details-v1-79f774bdb9[ReplicaSet],default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +default/details-v1-79f774bdb9[ReplicaSet],default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],default/details-v1-79f774bdb9[ReplicaSet],All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],default/ratings-v1-b6994bb9[ReplicaSet],All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],default/reviews-v1-545db77b95[ReplicaSet],All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +default/productpage-v1-6b746f74dc[ReplicaSet],default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/ratings-v1-b6994bb9[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/ratings-v1-b6994bb9[ReplicaSet],default/details-v1-79f774bdb9[ReplicaSet],All Connections +default/ratings-v1-b6994bb9[ReplicaSet],default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +default/ratings-v1-b6994bb9[ReplicaSet],default/reviews-v1-545db77b95[ReplicaSet],All Connections +default/ratings-v1-b6994bb9[ReplicaSet],default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +default/ratings-v1-b6994bb9[ReplicaSet],default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/reviews-v1-545db77b95[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/reviews-v1-545db77b95[ReplicaSet],default/details-v1-79f774bdb9[ReplicaSet],All Connections +default/reviews-v1-545db77b95[ReplicaSet],default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +default/reviews-v1-545db77b95[ReplicaSet],default/ratings-v1-b6994bb9[ReplicaSet],All Connections +default/reviews-v1-545db77b95[ReplicaSet],default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +default/reviews-v1-545db77b95[ReplicaSet],default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],default/details-v1-79f774bdb9[ReplicaSet],All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],default/ratings-v1-b6994bb9[ReplicaSet],All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],default/reviews-v1-545db77b95[ReplicaSet],All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet],default/reviews-v3-84779c7bbc[ReplicaSet],All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],0.0.0.0-255.255.255.255,All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],default/details-v1-79f774bdb9[ReplicaSet],All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],default/productpage-v1-6b746f74dc[ReplicaSet],All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],default/ratings-v1-b6994bb9[ReplicaSet],All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],default/reviews-v1-545db77b95[ReplicaSet],All Connections +default/reviews-v3-84779c7bbc[ReplicaSet],default/reviews-v2-7bf8c9648f[ReplicaSet],All Connections +{ingress-controller},default/details-v1-79f774bdb9[ReplicaSet],TCP 9080 diff --git a/tests/k8s_ingress_test/connlist_output.dot b/tests/k8s_ingress_test/connlist_output.dot new file mode 100644 index 00000000..fb8931d3 --- /dev/null +++ b/tests/k8s_ingress_test/connlist_output.dot @@ -0,0 +1,53 @@ +digraph { + "0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="red2" fontcolor="red2"] + "default/details-v1-79f774bdb9[ReplicaSet]" [label="default/details-v1-79f774bdb9[ReplicaSet]" color="blue" fontcolor="blue"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="default/productpage-v1-6b746f74dc[ReplicaSet]" color="blue" fontcolor="blue"] + "default/ratings-v1-b6994bb9[ReplicaSet]" [label="default/ratings-v1-b6994bb9[ReplicaSet]" color="blue" fontcolor="blue"] + "default/reviews-v1-545db77b95[ReplicaSet]" [label="default/reviews-v1-545db77b95[ReplicaSet]" color="blue" fontcolor="blue"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="default/reviews-v2-7bf8c9648f[ReplicaSet]" color="blue" fontcolor="blue"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="default/reviews-v3-84779c7bbc[ReplicaSet]" color="blue" fontcolor="blue"] + "{ingress-controller}" [label="{ingress-controller}" color="blue" fontcolor="blue"] + "0.0.0.0-255.255.255.255" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "0.0.0.0-255.255.255.255" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/details-v1-79f774bdb9[ReplicaSet]" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/productpage-v1-6b746f74dc[ReplicaSet]" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/ratings-v1-b6994bb9[ReplicaSet]" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v1-545db77b95[ReplicaSet]" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v2-7bf8c9648f[ReplicaSet]" -> "default/reviews-v3-84779c7bbc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "default/productpage-v1-6b746f74dc[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "default/ratings-v1-b6994bb9[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "default/reviews-v1-545db77b95[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "default/reviews-v3-84779c7bbc[ReplicaSet]" -> "default/reviews-v2-7bf8c9648f[ReplicaSet]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "default/details-v1-79f774bdb9[ReplicaSet]" [label="TCP 9080" color="gold2" fontcolor="darkgreen"] +} \ No newline at end of file diff --git a/tests/k8s_ingress_test/connlist_output.dot.png b/tests/k8s_ingress_test/connlist_output.dot.png new file mode 100644 index 00000000..cb41eb22 Binary files /dev/null and b/tests/k8s_ingress_test/connlist_output.dot.png differ diff --git a/tests/k8s_ingress_test/connlist_output.json b/tests/k8s_ingress_test/connlist_output.json new file mode 100644 index 00000000..403cca18 --- /dev/null +++ b/tests/k8s_ingress_test/connlist_output.json @@ -0,0 +1,217 @@ +[ + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "0.0.0.0-255.255.255.255", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/details-v1-79f774bdb9[ReplicaSet]", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/ratings-v1-b6994bb9[ReplicaSet]", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v1-545db77b95[ReplicaSet]", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "dst": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "default/productpage-v1-6b746f74dc[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "default/ratings-v1-b6994bb9[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "default/reviews-v1-545db77b95[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "default/reviews-v3-84779c7bbc[ReplicaSet]", + "dst": "default/reviews-v2-7bf8c9648f[ReplicaSet]", + "conn": "All Connections" + }, + { + "src": "{ingress-controller}", + "dst": "default/details-v1-79f774bdb9[ReplicaSet]", + "conn": "TCP 9080" + } +] \ No newline at end of file diff --git a/tests/k8s_ingress_test/connlist_output.md b/tests/k8s_ingress_test/connlist_output.md new file mode 100644 index 00000000..605aa37a --- /dev/null +++ b/tests/k8s_ingress_test/connlist_output.md @@ -0,0 +1,45 @@ +| src | dst | conn | +|-----|-----|------| +| 0.0.0.0-255.255.255.255 | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| 0.0.0.0-255.255.255.255 | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| 0.0.0.0-255.255.255.255 | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| 0.0.0.0-255.255.255.255 | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| 0.0.0.0-255.255.255.255 | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| 0.0.0.0-255.255.255.255 | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| default/details-v1-79f774bdb9[ReplicaSet] | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| default/productpage-v1-6b746f74dc[ReplicaSet] | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| default/ratings-v1-b6994bb9[ReplicaSet] | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| default/reviews-v1-545db77b95[ReplicaSet] | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| default/reviews-v2-7bf8c9648f[ReplicaSet] | default/reviews-v3-84779c7bbc[ReplicaSet] | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | 0.0.0.0-255.255.255.255 | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | default/details-v1-79f774bdb9[ReplicaSet] | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | default/productpage-v1-6b746f74dc[ReplicaSet] | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | default/ratings-v1-b6994bb9[ReplicaSet] | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | default/reviews-v1-545db77b95[ReplicaSet] | All Connections | +| default/reviews-v3-84779c7bbc[ReplicaSet] | default/reviews-v2-7bf8c9648f[ReplicaSet] | All Connections | +| {ingress-controller} | default/details-v1-79f774bdb9[ReplicaSet] | TCP 9080 | \ No newline at end of file diff --git a/tests/k8s_ingress_test/connlist_output.txt b/tests/k8s_ingress_test/connlist_output.txt new file mode 100644 index 00000000..f67a30f5 --- /dev/null +++ b/tests/k8s_ingress_test/connlist_output.txt @@ -0,0 +1,43 @@ +0.0.0.0-255.255.255.255 => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +0.0.0.0-255.255.255.255 => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +0.0.0.0-255.255.255.255 => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +0.0.0.0-255.255.255.255 => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +0.0.0.0-255.255.255.255 => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +0.0.0.0-255.255.255.255 => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +default/details-v1-79f774bdb9[ReplicaSet] => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +default/productpage-v1-6b746f74dc[ReplicaSet] => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +default/ratings-v1-b6994bb9[ReplicaSet] => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +default/reviews-v1-545db77b95[ReplicaSet] => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +default/reviews-v2-7bf8c9648f[ReplicaSet] => default/reviews-v3-84779c7bbc[ReplicaSet] : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => 0.0.0.0-255.255.255.255 : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => default/details-v1-79f774bdb9[ReplicaSet] : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => default/productpage-v1-6b746f74dc[ReplicaSet] : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => default/ratings-v1-b6994bb9[ReplicaSet] : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => default/reviews-v1-545db77b95[ReplicaSet] : All Connections +default/reviews-v3-84779c7bbc[ReplicaSet] => default/reviews-v2-7bf8c9648f[ReplicaSet] : All Connections +{ingress-controller} => default/details-v1-79f774bdb9[ReplicaSet] : TCP 9080 \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.csv b/tests/multiple_ingress_objects_with_different_ports/connlist_output.csv new file mode 100644 index 00000000..3606ba62 --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/connlist_output.csv @@ -0,0 +1,4 @@ +src,dst,conn +0.0.0.0-255.255.255.255,ingressworld/ingress-world-multiple-ports[Deployment],All Connections +ingressworld/ingress-world-multiple-ports[Deployment],0.0.0.0-255.255.255.255,All Connections +{ingress-controller},ingressworld/ingress-world-multiple-ports[Deployment],"TCP 8050,8090" diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot b/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot new file mode 100644 index 00000000..bcba1047 --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot @@ -0,0 +1,8 @@ +digraph { + "0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="red2" fontcolor="red2"] + "ingressworld/ingress-world-multiple-ports[Deployment]" [label="ingressworld/ingress-world-multiple-ports[Deployment]" color="blue" fontcolor="blue"] + "{ingress-controller}" [label="{ingress-controller}" color="blue" fontcolor="blue"] + "0.0.0.0-255.255.255.255" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world-multiple-ports[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="TCP 8050,8090" color="gold2" fontcolor="darkgreen"] +} \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot.png b/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot.png new file mode 100644 index 00000000..deb3e458 Binary files /dev/null and b/tests/multiple_ingress_objects_with_different_ports/connlist_output.dot.png differ diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.json b/tests/multiple_ingress_objects_with_different_ports/connlist_output.json new file mode 100644 index 00000000..29a82449 --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/connlist_output.json @@ -0,0 +1,17 @@ +[ + { + "src": "0.0.0.0-255.255.255.255", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world-multiple-ports[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "{ingress-controller}", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "TCP 8050,8090" + } +] \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.md b/tests/multiple_ingress_objects_with_different_ports/connlist_output.md new file mode 100644 index 00000000..5ec5053d --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/connlist_output.md @@ -0,0 +1,5 @@ +| src | dst | conn | +|-----|-----|------| +| 0.0.0.0-255.255.255.255 | ingressworld/ingress-world-multiple-ports[Deployment] | All Connections | +| ingressworld/ingress-world-multiple-ports[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| {ingress-controller} | ingressworld/ingress-world-multiple-ports[Deployment] | TCP 8050,8090 | \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/connlist_output.txt b/tests/multiple_ingress_objects_with_different_ports/connlist_output.txt new file mode 100644 index 00000000..a445a311 --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/connlist_output.txt @@ -0,0 +1,3 @@ +0.0.0.0-255.255.255.255 => ingressworld/ingress-world-multiple-ports[Deployment] : All Connections +ingressworld/ingress-world-multiple-ports[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +{ingress-controller} => ingressworld/ingress-world-multiple-ports[Deployment] : TCP 8050,8090 \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/deployment_with_multiple_ports.yaml b/tests/multiple_ingress_objects_with_different_ports/deployment_with_multiple_ports.yaml new file mode 100644 index 00000000..39fd1e52 --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/deployment_with_multiple_ports.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ingressworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-world-multiple-ports + namespace: ingressworld + labels: + app: ingress-world +spec: + replicas: 2 + selector: + matchLabels: + app: ingress-world + template: + metadata: + labels: + app: ingress-world + spec: + containers: + - name: ingressworld + image: quay.io/shfa/ingress-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-world + namespace: ingressworld +spec: + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + - protocol: TCP + port: 8050 + targetPort: 8050 + - protocol: TCP + port: 8090 + targetPort: 8090 + selector: + app: ingress-world diff --git a/tests/multiple_ingress_objects_with_different_ports/ingress-1.yaml b/tests/multiple_ingress_objects_with_different_ports/ingress-1.yaml new file mode 100644 index 00000000..edbd594f --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/ingress-1.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-1 + namespace: ingressworld +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world + port: + number: 8090 \ No newline at end of file diff --git a/tests/multiple_ingress_objects_with_different_ports/ingress-2.yaml b/tests/multiple_ingress_objects_with_different_ports/ingress-2.yaml new file mode 100644 index 00000000..1a4c8a5b --- /dev/null +++ b/tests/multiple_ingress_objects_with_different_ports/ingress-2.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-2 + namespace: ingressworld +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world + port: + number: 8050 \ No newline at end of file diff --git a/tests/one_ingress_multiple_ports/connlist_output.csv b/tests/one_ingress_multiple_ports/connlist_output.csv new file mode 100644 index 00000000..d3cc1cbc --- /dev/null +++ b/tests/one_ingress_multiple_ports/connlist_output.csv @@ -0,0 +1,4 @@ +src,dst,conn +0.0.0.0-255.255.255.255,ingressworld/ingress-world-multiple-ports[Deployment],All Connections +ingressworld/ingress-world-multiple-ports[Deployment],0.0.0.0-255.255.255.255,All Connections +{ingress-controller},ingressworld/ingress-world-multiple-ports[Deployment],"TCP 8000,8090" diff --git a/tests/one_ingress_multiple_ports/connlist_output.dot b/tests/one_ingress_multiple_ports/connlist_output.dot new file mode 100644 index 00000000..2a96f26e --- /dev/null +++ b/tests/one_ingress_multiple_ports/connlist_output.dot @@ -0,0 +1,8 @@ +digraph { + "0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="red2" fontcolor="red2"] + "ingressworld/ingress-world-multiple-ports[Deployment]" [label="ingressworld/ingress-world-multiple-ports[Deployment]" color="blue" fontcolor="blue"] + "{ingress-controller}" [label="{ingress-controller}" color="blue" fontcolor="blue"] + "0.0.0.0-255.255.255.255" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world-multiple-ports[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="TCP 8000,8090" color="gold2" fontcolor="darkgreen"] +} \ No newline at end of file diff --git a/tests/one_ingress_multiple_ports/connlist_output.dot.png b/tests/one_ingress_multiple_ports/connlist_output.dot.png new file mode 100644 index 00000000..d5b250f9 Binary files /dev/null and b/tests/one_ingress_multiple_ports/connlist_output.dot.png differ diff --git a/tests/one_ingress_multiple_ports/connlist_output.json b/tests/one_ingress_multiple_ports/connlist_output.json new file mode 100644 index 00000000..b5e9998b --- /dev/null +++ b/tests/one_ingress_multiple_ports/connlist_output.json @@ -0,0 +1,17 @@ +[ + { + "src": "0.0.0.0-255.255.255.255", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world-multiple-ports[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "{ingress-controller}", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "TCP 8000,8090" + } +] \ No newline at end of file diff --git a/tests/one_ingress_multiple_ports/connlist_output.md b/tests/one_ingress_multiple_ports/connlist_output.md new file mode 100644 index 00000000..847bae56 --- /dev/null +++ b/tests/one_ingress_multiple_ports/connlist_output.md @@ -0,0 +1,5 @@ +| src | dst | conn | +|-----|-----|------| +| 0.0.0.0-255.255.255.255 | ingressworld/ingress-world-multiple-ports[Deployment] | All Connections | +| ingressworld/ingress-world-multiple-ports[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| {ingress-controller} | ingressworld/ingress-world-multiple-ports[Deployment] | TCP 8000,8090 | \ No newline at end of file diff --git a/tests/one_ingress_multiple_ports/connlist_output.txt b/tests/one_ingress_multiple_ports/connlist_output.txt new file mode 100644 index 00000000..4e3cbdc5 --- /dev/null +++ b/tests/one_ingress_multiple_ports/connlist_output.txt @@ -0,0 +1,3 @@ +0.0.0.0-255.255.255.255 => ingressworld/ingress-world-multiple-ports[Deployment] : All Connections +ingressworld/ingress-world-multiple-ports[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +{ingress-controller} => ingressworld/ingress-world-multiple-ports[Deployment] : TCP 8000,8090 \ No newline at end of file diff --git a/tests/one_ingress_multiple_ports/deployment_with_multiple_ports.yaml b/tests/one_ingress_multiple_ports/deployment_with_multiple_ports.yaml new file mode 100644 index 00000000..39fd1e52 --- /dev/null +++ b/tests/one_ingress_multiple_ports/deployment_with_multiple_ports.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ingressworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-world-multiple-ports + namespace: ingressworld + labels: + app: ingress-world +spec: + replicas: 2 + selector: + matchLabels: + app: ingress-world + template: + metadata: + labels: + app: ingress-world + spec: + containers: + - name: ingressworld + image: quay.io/shfa/ingress-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-world + namespace: ingressworld +spec: + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + - protocol: TCP + port: 8050 + targetPort: 8050 + - protocol: TCP + port: 8090 + targetPort: 8090 + selector: + app: ingress-world diff --git a/tests/one_ingress_multiple_ports/ingress_multiple_rules.yaml b/tests/one_ingress_multiple_ports/ingress_multiple_rules.yaml new file mode 100644 index 00000000..44ee5819 --- /dev/null +++ b/tests/one_ingress_multiple_ports/ingress_multiple_rules.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-world + namespace: ingressworld +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world + port: + number: 8090 + - host: ingress-2.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world + port: + number: 8000 \ No newline at end of file diff --git a/tests/one_ingress_multiple_services/connlist_output.csv b/tests/one_ingress_multiple_services/connlist_output.csv new file mode 100644 index 00000000..d3cc1cbc --- /dev/null +++ b/tests/one_ingress_multiple_services/connlist_output.csv @@ -0,0 +1,4 @@ +src,dst,conn +0.0.0.0-255.255.255.255,ingressworld/ingress-world-multiple-ports[Deployment],All Connections +ingressworld/ingress-world-multiple-ports[Deployment],0.0.0.0-255.255.255.255,All Connections +{ingress-controller},ingressworld/ingress-world-multiple-ports[Deployment],"TCP 8000,8090" diff --git a/tests/one_ingress_multiple_services/connlist_output.dot b/tests/one_ingress_multiple_services/connlist_output.dot new file mode 100644 index 00000000..2a96f26e --- /dev/null +++ b/tests/one_ingress_multiple_services/connlist_output.dot @@ -0,0 +1,8 @@ +digraph { + "0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="red2" fontcolor="red2"] + "ingressworld/ingress-world-multiple-ports[Deployment]" [label="ingressworld/ingress-world-multiple-ports[Deployment]" color="blue" fontcolor="blue"] + "{ingress-controller}" [label="{ingress-controller}" color="blue" fontcolor="blue"] + "0.0.0.0-255.255.255.255" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "ingressworld/ingress-world-multiple-ports[Deployment]" -> "0.0.0.0-255.255.255.255" [label="All Connections" color="gold2" fontcolor="darkgreen"] + "{ingress-controller}" -> "ingressworld/ingress-world-multiple-ports[Deployment]" [label="TCP 8000,8090" color="gold2" fontcolor="darkgreen"] +} \ No newline at end of file diff --git a/tests/one_ingress_multiple_services/connlist_output.dot.png b/tests/one_ingress_multiple_services/connlist_output.dot.png new file mode 100644 index 00000000..d5b250f9 Binary files /dev/null and b/tests/one_ingress_multiple_services/connlist_output.dot.png differ diff --git a/tests/one_ingress_multiple_services/connlist_output.json b/tests/one_ingress_multiple_services/connlist_output.json new file mode 100644 index 00000000..b5e9998b --- /dev/null +++ b/tests/one_ingress_multiple_services/connlist_output.json @@ -0,0 +1,17 @@ +[ + { + "src": "0.0.0.0-255.255.255.255", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "All Connections" + }, + { + "src": "ingressworld/ingress-world-multiple-ports[Deployment]", + "dst": "0.0.0.0-255.255.255.255", + "conn": "All Connections" + }, + { + "src": "{ingress-controller}", + "dst": "ingressworld/ingress-world-multiple-ports[Deployment]", + "conn": "TCP 8000,8090" + } +] \ No newline at end of file diff --git a/tests/one_ingress_multiple_services/connlist_output.md b/tests/one_ingress_multiple_services/connlist_output.md new file mode 100644 index 00000000..847bae56 --- /dev/null +++ b/tests/one_ingress_multiple_services/connlist_output.md @@ -0,0 +1,5 @@ +| src | dst | conn | +|-----|-----|------| +| 0.0.0.0-255.255.255.255 | ingressworld/ingress-world-multiple-ports[Deployment] | All Connections | +| ingressworld/ingress-world-multiple-ports[Deployment] | 0.0.0.0-255.255.255.255 | All Connections | +| {ingress-controller} | ingressworld/ingress-world-multiple-ports[Deployment] | TCP 8000,8090 | \ No newline at end of file diff --git a/tests/one_ingress_multiple_services/connlist_output.txt b/tests/one_ingress_multiple_services/connlist_output.txt new file mode 100644 index 00000000..4e3cbdc5 --- /dev/null +++ b/tests/one_ingress_multiple_services/connlist_output.txt @@ -0,0 +1,3 @@ +0.0.0.0-255.255.255.255 => ingressworld/ingress-world-multiple-ports[Deployment] : All Connections +ingressworld/ingress-world-multiple-ports[Deployment] => 0.0.0.0-255.255.255.255 : All Connections +{ingress-controller} => ingressworld/ingress-world-multiple-ports[Deployment] : TCP 8000,8090 \ No newline at end of file diff --git a/tests/one_ingress_multiple_services/deployment_with_multiple_services.yaml b/tests/one_ingress_multiple_services/deployment_with_multiple_services.yaml new file mode 100644 index 00000000..2ddf0381 --- /dev/null +++ b/tests/one_ingress_multiple_services/deployment_with_multiple_services.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ingressworld +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ingress-world-multiple-ports + namespace: ingressworld + labels: + app: ingress-world +spec: + replicas: 2 + selector: + matchLabels: + app: ingress-world + template: + metadata: + labels: + app: ingress-world + spec: + containers: + - name: ingressworld + image: quay.io/shfa/ingress-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-world-svc1 + namespace: ingressworld +spec: + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + - protocol: TCP + port: 8050 + targetPort: 8050 + selector: + app: ingress-world +--- +apiVersion: v1 +kind: Service +metadata: + name: ingress-world-svc2 + namespace: ingressworld +spec: + ports: + - protocol: TCP + port: 8090 + targetPort: 8090 + selector: + app: ingress-world + diff --git a/tests/one_ingress_multiple_services/ingress_multiple_rules.yaml b/tests/one_ingress_multiple_services/ingress_multiple_rules.yaml new file mode 100644 index 00000000..a7aec99e --- /dev/null +++ b/tests/one_ingress_multiple_services/ingress_multiple_rules.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-world + namespace: ingressworld +spec: + rules: + - host: ingress.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world-svc2 + port: + number: 8090 + - host: ingress-2.nginx.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: ingress-world-svc1 + port: + number: 8000 \ No newline at end of file diff --git a/tests/route_example_with_target_port/deployment_with_multiple_ports.yaml b/tests/route_example_with_target_port/deployment_with_multiple_ports.yaml new file mode 100644 index 00000000..b8b57fd9 --- /dev/null +++ b/tests/route_example_with_target_port/deployment_with_multiple_ports.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: routes-world +spec: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: workload-with-multiple-ports + namespace: routes-world + labels: + app: routes-world +spec: + replicas: 2 + selector: + matchLabels: + app: routes-world + template: + metadata: + labels: + app: routes-world + spec: + containers: + - name: routes-world + image: quay.io/shfa/routes-world:latest + ports: + - containerPort: 8000 # containerport1 + - containerPort: 8050 # containerport2 + - containerPort: 8090 # containerport3 +--- +apiVersion: v1 +kind: Service +metadata: + name: routes-world-svc + namespace: routes-world +spec: + ports: + - name: first-port + protocol: TCP + port: 8000 + targetPort: 8000 + - name: second-port + protocol: TCP + port: 8050 + targetPort: 8050 + - name: third-port + protocol: TCP + port: 8090 + targetPort: 8090 + selector: + app: routes-world diff --git a/tests/route_example_with_target_port/route-1.yaml b/tests/route_example_with_target_port/route-1.yaml new file mode 100644 index 00000000..aa970fdb --- /dev/null +++ b/tests/route_example_with_target_port/route-1.yaml @@ -0,0 +1,15 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: route-1 + namespace: routes-world + labels: + app: routes-world +spec: + to: + kind: Service + name: routes-world-svc + weight: 100 + port: + targetPort: first-port + wildcardPolicy: None \ No newline at end of file diff --git a/tests/route_example_with_target_port/route-2.yaml b/tests/route_example_with_target_port/route-2.yaml new file mode 100644 index 00000000..0cea6cd3 --- /dev/null +++ b/tests/route_example_with_target_port/route-2.yaml @@ -0,0 +1,15 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: route-2 + namespace: routes-world + labels: + app: routes-world +spec: + to: + kind: Service + name: routes-world-svc + weight: 100 + port: + targetPort: 8090 + wildcardPolicy: None \ No newline at end of file