From 93680bf4e107e5f433c06440a533222283fae9d8 Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Wed, 6 Mar 2024 12:19:44 +0200 Subject: [PATCH 1/7] Support for user-specified file with connections to add Signed-off-by: ZIV NEVO --- cmd/nettop/main.go | 6 +- cmd/nettop/parse_args.go | 2 + pkg/analyzer/connections.go | 188 +++++++++++++++++++++++++-- pkg/analyzer/policies_synthesizer.go | 28 +++- 4 files changed, 208 insertions(+), 16 deletions(-) diff --git a/cmd/nettop/main.go b/cmd/nettop/main.go index eec2264..725c972 100644 --- a/cmd/nettop/main.go +++ b/cmd/nettop/main.go @@ -90,7 +90,11 @@ func getVerbosity(args *inArgs) analyzer.Verbosity { // (or NetworkPolicies to allow only this connectivity) func detectTopology(args *inArgs) error { logger := analyzer.NewDefaultLoggerWithVerbosity(getVerbosity(args)) - synth := analyzer.NewPoliciesSynthesizer(analyzer.WithLogger(logger), analyzer.WithDNSPort(*args.DNSPort)) + opts := []analyzer.PoliciesSynthesizerOption{analyzer.WithLogger(logger), analyzer.WithDNSPort(*args.DNSPort)} + if *args.connsFile != "" { + opts = append(opts, analyzer.WithConnectionsFile(*args.connsFile)) + } + synth := analyzer.NewPoliciesSynthesizer(opts...) var content interface{} if args.SynthNetpols != nil && *args.SynthNetpols { diff --git a/cmd/nettop/parse_args.go b/cmd/nettop/parse_args.go index e330df5..979995a 100644 --- a/cmd/nettop/parse_args.go +++ b/cmd/nettop/parse_args.go @@ -34,6 +34,7 @@ type inArgs struct { OutputFile *string OutputFormat *string DNSPort *int + connsFile *string SynthNetpols *bool Quiet *bool Verbose *bool @@ -47,6 +48,7 @@ func parseInArgs(cmdlineArgs []string) (*inArgs, error) { args.OutputFormat = flagset.String("format", jsonFormat, "output format; must be either \"json\" or \"yaml\"") args.SynthNetpols = flagset.Bool("netpols", false, "whether to synthesize NetworkPolicies to allow only the discovered connections") args.DNSPort = flagset.Int("dnsport", analyzer.DefaultDNSPort, "DNS port to be used in egress rules of synthesized NetworkPolicies") + args.connsFile = flagset.String("conns", "", "a file specifying connections to enable") args.Quiet = flagset.Bool("q", false, "runs quietly, reports only severe errors and results") args.Verbose = flagset.Bool("v", false, "runs with more informative messages printed to log") err := flagset.Parse(cmdlineArgs) diff --git a/pkg/analyzer/connections.go b/pkg/analyzer/connections.go index e8de197..d0bc809 100644 --- a/pkg/analyzer/connections.go +++ b/pkg/analyzer/connections.go @@ -7,22 +7,31 @@ SPDX-License-Identifier: Apache-2.0 package analyzer import ( + "bufio" "fmt" + "os" + "strings" ) +type connectionExtractor struct { + resources []*Resource + links []*Service + logger Logger +} + // This function is at the core of the topology analysis // For each resource, it finds other resources that may use it and compiles a list of connections holding these dependencies -func discoverConnections(resources []*Resource, links []*Service, logger Logger) []*Connections { +func (ce *connectionExtractor) discoverConnections() []*Connections { connections := []*Connections{} - for _, destRes := range resources { - deploymentServices := findServices(destRes, links) - logger.Debugf("services matched to %v: %v", destRes.Resource.Name, deploymentServices) + for _, destRes := range ce.resources { + deploymentServices := ce.findServices(destRes) + ce.logger.Debugf("services matched to %v: %v", destRes.Resource.Name, deploymentServices) for _, svc := range deploymentServices { - srcRes := findSource(resources, svc) + srcRes := ce.findSource(svc) if len(srcRes) > 0 { for _, r := range srcRes { if !r.equals(destRes) { - logger.Debugf("source: %s target: %s link: %s", r.Resource.Name, destRes.Resource.Name, svc.Resource.Name) + ce.logger.Debugf("source: %s target: %s link: %s", r.Resource.Name, destRes.Resource.Name, svc.Resource.Name) connections = append(connections, &Connections{Source: r, Target: destRes, Link: svc}) } } @@ -51,9 +60,9 @@ func areSelectorsContained(selectors1 map[string]string, selectors2 []string) bo } // findServices returns a list of services that may be in front of a given workload resource -func findServices(resource *Resource, links []*Service) []*Service { +func (ce *connectionExtractor) findServices(resource *Resource) []*Service { var matchedSvc []*Service - for _, link := range links { + for _, link := range ce.links { if link.Resource.Namespace != resource.Resource.Namespace { continue } @@ -68,9 +77,9 @@ func findServices(resource *Resource, links []*Service) []*Service { } // findSource returns a list of resources that are likely trying to connect to the given service -func findSource(resources []*Resource, service *Service) []*Resource { +func (ce *connectionExtractor) findSource(service *Service) []*Resource { tRes := []*Resource{} - for _, resource := range resources { + for _, resource := range ce.resources { serviceAddresses := getPossibleServiceAddresses(service, resource) foundSrc := *resource // We copy the resource so we can specify the ports used by the source found matched := false @@ -122,3 +131,162 @@ func envValueMatchesService(envVal string, service *Service, serviceAddresses [] } return false, SvcNetworkAttr{} } + +const ( + srcDstDelim = "->" + endpointsPortDelim = "|" + commentToken = "#" + wildcardToken = "*" +) + +type workloadAndService struct { + resource *Resource + service *Service +} + +func (ce *connectionExtractor) connectionsFromFile(filename string) ([]*Connections, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + conns := []*Connections{} + + scanner := bufio.NewScanner(file) + lineNum := 0 + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + lineNum += 1 + if line == "" || strings.HasPrefix(line, commentToken) { + continue + } + lineConns, err := ce.parseConnectionLine(line, lineNum) + if err != nil { + return nil, err + } + conns = append(conns, lineConns...) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return conns, nil +} + +func (ce *connectionExtractor) parseConnectionLine(line string, lineNum int) ([]*Connections, error) { + // Take only the part before # starts a comment + parts := strings.Split(line, commentToken) + if len(parts) == 0 { + return nil, syntaxError("unexpected comment", lineNum) + } + + line = parts[0] + + parts = strings.Split(line, srcDstDelim) + if len(parts) != 2 { + return nil, syntaxError("connection line must have exactly one -> separator", lineNum) + } + + src := strings.TrimSpace(parts[0]) + srcWorkloads, err := ce.parseEndpoints(src, lineNum) + if err != nil { + return nil, err + } + + parts = strings.Split(parts[1], endpointsPortDelim) + if len(parts) == 0 { + return nil, syntaxError("missing destination", lineNum) + } + if len(parts) > 2 { + return nil, syntaxError("connection line must have at most one | separator", lineNum) + } + dst := strings.TrimSpace(parts[0]) + dstWorkloads, err := ce.parseEndpoints(dst, lineNum) + if err != nil { + return nil, err + } + + conns := []*Connections{} + for _, srcWl := range srcWorkloads { + for _, dstWl := range dstWorkloads { + ce.logger.Debugf("src: %+v, dst: %+v\n", *srcWl.resource, *dstWl.resource) + conns = append(conns, &Connections{ + Source: srcWl.resource, + Target: dstWl.resource, + Link: dstWl.service, + }) + } + } + return conns, nil +} + +func (ce *connectionExtractor) parseEndpoints(endpoint string, lineNum int) ([]workloadAndService, error) { + parts := strings.Split(endpoint, "/") + if len(parts) != 3 { + return nil, syntaxError("source and destination must be of the form namespace/kind/name", lineNum) + } + ns, kind, name := parts[0], parts[1], parts[2] + kind = strings.ToUpper(kind[:1]) + kind[1:] // Capitalize kind's first letter + + var res []workloadAndService + switch kind { + case service: + res = ce.getMatchingServices(ns, name) + case wildcardToken: + res = append(ce.getMatchingServices(ns, name), ce.getMatchingWorkloads(ns, kind, name)...) + default: + res = ce.getMatchingWorkloads(ns, kind, name) + } + if len(res) == 0 { + return nil, fmt.Errorf("no matching endpoints for %s in the provided manifests", endpoint) + } + return res, nil +} + +func (ce *connectionExtractor) getMatchingServices(ns, name string) []workloadAndService { + services := []workloadAndService{} + for _, svc := range ce.links { + if strMatch(svc.Resource.Namespace, ns) && strMatch(svc.Resource.Name, name) { + services = append(services, ce.workloadsOfSvc(svc)...) + } + } + return services +} + +func (ce *connectionExtractor) workloadsOfSvc(svc *Service) []workloadAndService { + svcWorkloads := []workloadAndService{} + for _, workload := range ce.resources { + if workload.Resource.Namespace == svc.Resource.Namespace && + areSelectorsContained(workload.Resource.Labels, svc.Resource.Selectors) { + svcWorkloads = append(svcWorkloads, workloadAndService{workload, svc}) + } + } + return svcWorkloads +} + +func (ce *connectionExtractor) getMatchingWorkloads(ns, kind, name string) []workloadAndService { + workloads := []workloadAndService{} + for _, workload := range ce.resources { + if strMatch(workload.Resource.Namespace, ns) && strMatch(workload.Resource.Kind, kind) && + strMatch(workload.Resource.Name, name) { + services := ce.findServices(workload) + if len(services) == 0 { + ce.logger.Infof("workload %s is not exposed by any service", workload.Resource.Name) + } + for _, svc := range services { + workloads = append(workloads, workloadAndService{workload, svc}) + } + } + } + return workloads +} + +func strMatch(str, pattern string) bool { + return pattern == wildcardToken || str == pattern +} + +func syntaxError(errorStr string, lineNum int) error { + return fmt.Errorf("syntax error in line %d: %s", lineNum, errorStr) +} diff --git a/pkg/analyzer/policies_synthesizer.go b/pkg/analyzer/policies_synthesizer.go index f8b930d..1895f9c 100644 --- a/pkg/analyzer/policies_synthesizer.go +++ b/pkg/analyzer/policies_synthesizer.go @@ -31,10 +31,11 @@ type WalkFunction func(root string, fn fs.WalkDirFunc) error // It is possible to get either a slice with all the discovered connections or a slice with K8s NetworkPolicies // that allow only the discovered connections and nothing more. type PoliciesSynthesizer struct { - logger Logger - stopOnError bool - walkFn WalkFunction - dnsPort int + logger Logger + stopOnError bool + walkFn WalkFunction + dnsPort int + connectionsFile string errors []FileProcessingError } @@ -74,6 +75,12 @@ func WithDNSPort(dnsPort int) PoliciesSynthesizerOption { } } +func WithConnectionsFile(filename string) PoliciesSynthesizerOption { + return func(p *PoliciesSynthesizer) { + p.connectionsFile = filename + } +} + // NewPoliciesSynthesizer creates a new instance of PoliciesSynthesizer, and applies the provided functional options. func NewPoliciesSynthesizer(options ...PoliciesSynthesizerOption) *PoliciesSynthesizer { // object with default behavior options @@ -227,7 +234,18 @@ func (ps *PoliciesSynthesizer) extractConnections(resAcc *resourceAccumulator) ( resAcc.exposeServices() // Discover all connections between resources - connections := discoverConnections(resAcc.workloads, resAcc.services, ps.logger) + ce := connectionExtractor{resAcc.workloads, resAcc.services, ps.logger} + connections := ce.discoverConnections() + + // If user specified a file with extra connections, add them too + if ps.connectionsFile != "" { + fileConns, err := ce.connectionsFromFile(ps.connectionsFile) + if err != nil { + fpErr := failedReadingFile(ps.connectionsFile, err) + return nil, nil, appendAndLogNewError(fileErrors, fpErr, ps.logger) + } + connections = append(connections, fileConns...) + } return resAcc.workloads, connections, fileErrors } From 9a3b9d6e938d221f87f176cae38f0fb53992a928 Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Wed, 6 Mar 2024 12:22:19 +0200 Subject: [PATCH 2/7] lint Signed-off-by: ZIV NEVO --- pkg/analyzer/connections.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/analyzer/connections.go b/pkg/analyzer/connections.go index d0bc809..1ae23b3 100644 --- a/pkg/analyzer/connections.go +++ b/pkg/analyzer/connections.go @@ -137,6 +137,7 @@ const ( endpointsPortDelim = "|" commentToken = "#" wildcardToken = "*" + endpointParts = 3 ) type workloadAndService struct { @@ -224,7 +225,7 @@ func (ce *connectionExtractor) parseConnectionLine(line string, lineNum int) ([] func (ce *connectionExtractor) parseEndpoints(endpoint string, lineNum int) ([]workloadAndService, error) { parts := strings.Split(endpoint, "/") - if len(parts) != 3 { + if len(parts) != endpointParts { return nil, syntaxError("source and destination must be of the form namespace/kind/name", lineNum) } ns, kind, name := parts[0], parts[1], parts[2] From 1f67b1cc64aba3a68fea802195fdd09fcc2e7a02 Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Thu, 28 Mar 2024 13:39:20 +0200 Subject: [PATCH 3/7] test moved Signed-off-by: ZIV NEVO --- cmd/nettop/main_test.go | 2 +- .../sockshop/{ => expected_netpols}/expected_netpol_output.json | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/sockshop/{ => expected_netpols}/expected_netpol_output.json (100%) diff --git a/cmd/nettop/main_test.go b/cmd/nettop/main_test.go index 928de3e..541049e 100644 --- a/cmd/nettop/main_test.go +++ b/cmd/nettop/main_test.go @@ -99,7 +99,7 @@ var ( true, []string{"-v"}, false, - []string{"sockshop", "expected_netpol_output.json"}, + []string{"sockshop", "expected_netpols", "expected_netpol_output.json"}, }, { "NetpolsK8sWordpress", diff --git a/tests/sockshop/expected_netpol_output.json b/tests/sockshop/expected_netpols/expected_netpol_output.json similarity index 100% rename from tests/sockshop/expected_netpol_output.json rename to tests/sockshop/expected_netpols/expected_netpol_output.json From 2b7fa5dcc559d91b49d2534de68fe77c077ba4b9 Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Thu, 28 Mar 2024 14:54:37 +0200 Subject: [PATCH 4/7] Support exposing a service to selected namespaces Signed-off-by: ZIV NEVO --- pkg/analyzer/types.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/analyzer/types.go b/pkg/analyzer/types.go index ad65454..0467eda 100644 --- a/pkg/analyzer/types.go +++ b/pkg/analyzer/types.go @@ -64,8 +64,9 @@ type Service struct { FilePath string `json:"filepath,omitempty"` Kind string `json:"kind,omitempty"` Network []SvcNetworkAttr `json:"network,omitempty"` - ExposeToCluster bool `json:"-"` - ExposeExternally bool `json:"-"` + exposeToNS []string + ExposeToCluster bool `json:"-"` + ExposeExternally bool `json:"-"` } `json:"resource,omitempty"` } From 3c8d48b5b32541110fd6397c66069d1ca47d784b Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Thu, 28 Mar 2024 14:57:14 +0200 Subject: [PATCH 5/7] Support exposing to selected namespaces Signed-off-by: ZIV NEVO --- pkg/analyzer/synth_netpols.go | 61 +++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/pkg/analyzer/synth_netpols.go b/pkg/analyzer/synth_netpols.go index 7345910..667e5a9 100644 --- a/pkg/analyzer/synth_netpols.go +++ b/pkg/analyzer/synth_netpols.go @@ -17,7 +17,6 @@ import ( ) const ( - networkAPIVersion = "networking.k8s.io/v1" networkPolicyKind = "NetworkPolicy" ) @@ -58,7 +57,7 @@ func getNsDefaultDenyPolicy(namespace string) *network.NetworkPolicy { return &network.NetworkPolicy{ TypeMeta: metaV1.TypeMeta{ Kind: networkPolicyKind, - APIVersion: networkAPIVersion, + APIVersion: network.SchemeGroupVersion.String(), }, ObjectMeta: metaV1.ObjectMeta{ Name: policyName, @@ -105,19 +104,17 @@ func determineConnectivityPerDeployment(connections []*Connections) []*deploymen } if srcDeploy != nil { - netpolPeer := getNetpolPeer(srcDeploy, dstDeploy) - srcDeploy.addEgressRule([]network.NetworkPolicyPeer{netpolPeer}, targetPorts) + netpolPeers := getNetpolPeers(srcDeploy, dstDeploy, conn.Link) + if netpolPeers != nil { + srcDeploy.addEgressRule(netpolPeers, targetPorts) + } } - switch { - case conn.Link.Resource.ExposeExternally: - dstDeploy.addIngressRule([]network.NetworkPolicyPeer{}, targetPorts) // allowing traffic from all sources - case conn.Link.Resource.ExposeToCluster: - peer := network.NetworkPolicyPeer{NamespaceSelector: &metaV1.LabelSelector{}} - dstDeploy.addIngressRule([]network.NetworkPolicyPeer{peer}, targetPorts) // allowing traffic from all cluster sources - case conn.Source != nil: - netpolPeer := getNetpolPeer(dstDeploy, srcDeploy) - dstDeploy.addIngressRule([]network.NetworkPolicyPeer{netpolPeer}, targetPorts) // allow traffic only from this specific source + if dstDeploy != nil { + netpolPeers := getNetpolPeers(dstDeploy, srcDeploy, conn.Link) + if netpolPeers != nil { + dstDeploy.addIngressRule(netpolPeers, targetPorts) + } } } @@ -145,16 +142,32 @@ func findOrAddDeploymentConn(resource *Resource, deployConns map[string]*deploym return &deploy } -func getNetpolPeer(netpolDeploy, otherDeploy *deploymentConnectivity) network.NetworkPolicyPeer { - netpolPeer := network.NetworkPolicyPeer{PodSelector: getDeployConnSelector(otherDeploy)} - if netpolDeploy.Resource.Resource.Namespace != otherDeploy.Resource.Resource.Namespace { - if otherDeploy.Resource.Resource.Namespace != "" { - netpolPeer.NamespaceSelector = &metaV1.LabelSelector{ - MatchLabels: map[string]string{"kubernetes.io/metadata.name": otherDeploy.Resource.Resource.Namespace}, - } - } // if otherDeploy has no namespace specified, we assume it is in the same namespace as the netpolDeploy +func getNetpolPeers(netpolDeploy, otherDeploy *deploymentConnectivity, link *Service) []network.NetworkPolicyPeer { + switch { + case link.Resource.ExposeExternally: // allowing traffic to/from all endpoints + return []network.NetworkPolicyPeer{} + case link.Resource.ExposeToCluster: // allowing traffic to/from all cluster endpoints + return []network.NetworkPolicyPeer{{NamespaceSelector: &metaV1.LabelSelector{}}} + case len(link.Resource.exposeToNS) > 0: // allowing traffic to/from endpoints in selected namespaces + res := []network.NetworkPolicyPeer{} + for _, ns := range link.Resource.exposeToNS { + labelMap := map[string]string{core.LabelMetadataName: ns} + peer := network.NetworkPolicyPeer{NamespaceSelector: &metaV1.LabelSelector{MatchLabels: labelMap}} + res = append(res, peer) + } + return res + case otherDeploy != nil: // allowing traffic to the specified deplotment only + netpolPeer := network.NetworkPolicyPeer{PodSelector: getDeployConnSelector(otherDeploy)} + if netpolDeploy.Resource.Resource.Namespace != otherDeploy.Resource.Resource.Namespace { + if otherDeploy.Resource.Resource.Namespace != "" { + netpolPeer.NamespaceSelector = &metaV1.LabelSelector{ + MatchLabels: map[string]string{core.LabelMetadataName: otherDeploy.Resource.Resource.Namespace}, + } + } // if otherDeploy has no namespace specified, we assume it is in the same namespace as the netpolDeploy + } + return []network.NetworkPolicyPeer{netpolPeer} } - return netpolPeer + return nil } func getDeployConnSelector(deployConn *deploymentConnectivity) *metaV1.LabelSelector { @@ -191,7 +204,7 @@ func (ps *PoliciesSynthesizer) buildNetpolPerDeployment(deployConnectivity []*de netpol := network.NetworkPolicy{ TypeMeta: metaV1.TypeMeta{ Kind: networkPolicyKind, - APIVersion: networkAPIVersion, + APIVersion: network.SchemeGroupVersion.String(), }, ObjectMeta: metaV1.ObjectMeta{ Name: deployConn.Resource.Resource.Name + "-netpol", @@ -228,7 +241,7 @@ func NetpolListFromNetpolSlice(netpols []*network.NetworkPolicy) network.Network netpolList := network.NetworkPolicyList{ TypeMeta: metaV1.TypeMeta{ Kind: "NetworkPolicyList", - APIVersion: networkAPIVersion, + APIVersion: network.SchemeGroupVersion.String(), }, Items: netpols2, } From cfa6718e749f0251f8189dd46f8e828c0b5cdf3b Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Thu, 28 Mar 2024 14:58:53 +0200 Subject: [PATCH 6/7] Support for strong wildcards Signed-off-by: ZIV NEVO --- pkg/analyzer/connections.go | 50 +++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/pkg/analyzer/connections.go b/pkg/analyzer/connections.go index 1ae23b3..5565abb 100644 --- a/pkg/analyzer/connections.go +++ b/pkg/analyzer/connections.go @@ -11,6 +11,8 @@ import ( "fmt" "os" "strings" + + "k8s.io/apimachinery/pkg/util/validation" ) type connectionExtractor struct { @@ -133,11 +135,12 @@ func envValueMatchesService(envVal string, service *Service, serviceAddresses [] } const ( - srcDstDelim = "->" - endpointsPortDelim = "|" - commentToken = "#" - wildcardToken = "*" - endpointParts = 3 + srcDstDelim = "->" + endpointsPortDelim = "|" + commentToken = "#" + wildcardToken = "_" + strongWildcardToken = "*" + endpointParts = 3 ) type workloadAndService struct { @@ -212,17 +215,26 @@ func (ce *connectionExtractor) parseConnectionLine(line string, lineNum int) ([] conns := []*Connections{} for _, srcWl := range srcWorkloads { for _, dstWl := range dstWorkloads { - ce.logger.Debugf("src: %+v, dst: %+v\n", *srcWl.resource, *dstWl.resource) + svc := mergeSrcAndDstSvcs(srcWl.service, dstWl.service) conns = append(conns, &Connections{ Source: srcWl.resource, Target: dstWl.resource, - Link: dstWl.service, + Link: svc, }) } } return conns, nil } +func mergeSrcAndDstSvcs(srcSvc, dstSvc *Service) *Service { + retSvc := *dstSvc + retSvc.Resource.ExposeExternally = srcSvc.Resource.ExposeExternally || dstSvc.Resource.ExposeExternally + retSvc.Resource.ExposeToCluster = srcSvc.Resource.ExposeToCluster || dstSvc.Resource.ExposeToCluster + retSvc.Resource.exposeToNS = dstSvc.Resource.exposeToNS + retSvc.Resource.exposeToNS = append(retSvc.Resource.exposeToNS, srcSvc.Resource.exposeToNS...) + return &retSvc +} + func (ce *connectionExtractor) parseEndpoints(endpoint string, lineNum int) ([]workloadAndService, error) { parts := strings.Split(endpoint, "/") if len(parts) != endpointParts { @@ -231,6 +243,10 @@ func (ce *connectionExtractor) parseEndpoints(endpoint string, lineNum int) ([]w ns, kind, name := parts[0], parts[1], parts[2] kind = strings.ToUpper(kind[:1]) + kind[1:] // Capitalize kind's first letter + if ns == strongWildcardToken || kind == strongWildcardToken || name == strongWildcardToken { + return ce.parseEndpointWithStrongWildcard(ns, kind, name) + } + var res []workloadAndService switch kind { case service: @@ -246,6 +262,26 @@ func (ce *connectionExtractor) parseEndpoints(endpoint string, lineNum int) ([]w return res, nil } +func (ce *connectionExtractor) parseEndpointWithStrongWildcard(ns, kind, name string) ([]workloadAndService, error) { + if kind != strongWildcardToken || name != strongWildcardToken { + return nil, fmt.Errorf("bad endpoint pattern %s/%s/%s. Patterns with '*' should either equal '*/*/*' "+ + "or have the form '/*/*'", ns, kind, name) + } + + svc := Service{} + if ns != strongWildcardToken { + if len(validation.IsDNS1123Subdomain(ns)) != 0 { + return nil, fmt.Errorf("%s is not a proper namespace name", ns) + } + svc.Resource.exposeToNS = append(svc.Resource.exposeToNS, ns) + } else { + svc.Resource.ExposeToCluster = true + } + wlSvc := workloadAndService{resource: nil, service: &svc} + + return []workloadAndService{wlSvc}, nil +} + func (ce *connectionExtractor) getMatchingServices(ns, name string) []workloadAndService { services := []workloadAndService{} for _, svc := range ce.links { From 2477eea031702685ba07ed56c45ad766362c493c Mon Sep 17 00:00:00 2001 From: ZIV NEVO Date: Thu, 28 Mar 2024 15:07:19 +0200 Subject: [PATCH 7/7] Initial test for connection file Signed-off-by: ZIV NEVO --- pkg/analyzer/policies_synthesizer_test.go | 9 +++++++++ tests/sockshop/connections.txt | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/sockshop/connections.txt diff --git a/pkg/analyzer/policies_synthesizer_test.go b/pkg/analyzer/policies_synthesizer_test.go index c5ec0ff..42e9bd0 100644 --- a/pkg/analyzer/policies_synthesizer_test.go +++ b/pkg/analyzer/policies_synthesizer_test.go @@ -131,6 +131,15 @@ func TestPoliciesSynthesizerAPIDnsPort(t *testing.T) { } } +func TestPoliciesSynthesizerConnectionsFile(t *testing.T) { + dirPath := filepath.Join(getTestsDir(), "sockshop", "manifests") + connFilePath := filepath.Join(getTestsDir(), "sockshop", "connections.txt") + synthesizer := NewPoliciesSynthesizer(WithConnectionsFile(connFilePath)) + netpols, err := synthesizer.PoliciesFromFolderPaths([]string{dirPath}) + require.Nil(t, err) + require.Len(t, netpols, 15) +} + func TestPoliciesSynthesizerAPIFatalError(t *testing.T) { dirPath1 := filepath.Join(getTestsDir(), "k8s_wordpress_example") dirPath2 := filepath.Join(getTestsDir(), "badPath") diff --git a/tests/sockshop/connections.txt b/tests/sockshop/connections.txt new file mode 100644 index 0000000..67039da --- /dev/null +++ b/tests/sockshop/connections.txt @@ -0,0 +1,21 @@ +sock-shop/deployment/orders -> sock-shop/service/catalogue | TCP:80 +sock-shop/deployment/catalogue -> sock-shop/deployment/catalogue-db +# sock-shop/deployment/orders -> 59.48.20.0/24 | TCP:443 + +# Known workloads in NS ns1, can connect to known workloads in NS sock-shop, named session-db +sock-shop/_/_ -> sock-shop/_/session-db + +# All workloads in NS ns1, can connect to the deployment catalogue-db in NS sock-shop +ns1/*/* -> sock-shop/deployment/shipping + +# All workloads in the cluster, can connect to the deployment catalogue-db in NS sock-shop +*/*/* -> sock-shop/deployment/orders + +# 0.0.0.0/0 -> sock-shop/deployment/catalogue-db + + + +# ns1/*/session-db # not supported +# ns1/deployment/* # not supported +# */deployment/* # not supported +# */deployment/session-db # not supported