diff --git a/cmd/policy-assistant/examples/traffic.json b/cmd/policy-assistant/examples/traffic.json index 6240cbb6..58d30317 100644 --- a/cmd/policy-assistant/examples/traffic.json +++ b/cmd/policy-assistant/examples/traffic.json @@ -71,5 +71,83 @@ "Protocol": "TCP", "ResolvedPort": 80, "ResolvedPortName": "serve-80-tcp" + },{ + "Source": { + "Internal": { + "Workload": "kube-system/daemonset/azure-npm", + "Namespace": "kube-system" + } + }, + "Destination": { + "Internal": { + "Workload": "default/deployment/aks-helloworld-one", + "Namespace": "default" + } + }, + "Protocol": "TCP", + "ResolvedPort": 80, + "ResolvedPortName": "serve-80-tcp" + },{ + "Source": { + "Internal": { + "Workload": "kube-system/daemonset/azure-npm", + "Namespace": "kube-system" + } + }, + "Destination": { + "Internal": { + "PodLabels": {"pod": "b"}, + "NamespaceLabels": {"ns": "y"}, + "Namespace": "y" + }, + "IP": "192.168.1.100" + }, + "Protocol": "TCP", + "ResolvedPort": 80, + "ResolvedPortName": "serve-80-tcp" + },{ + "Source": { + "Internal": { + "PodLabels": {"app": "c"}, + "NamespaceLabels": {"ns": "y"}, + "Namespace": "y" + }, + "IP": "192.168.1.99" + }, + "Destination": { + "Internal": { + "Workload": "default/deployment/nginx-deployment2", + "Namespace": "default" + } + }, + "Protocol": "TCP", + "ResolvedPort": 80, + "ResolvedPortName": "serve-80-tcp" + },{ + "Source": { + "IP": "192.168.1.99" + }, + "Destination": { + "Internal": { + "Workload": "default/deployment/nginx-deployment2", + "Namespace": "default" + } + }, + "Protocol": "TCP", + "ResolvedPort": 80, + "ResolvedPortName": "serve-80-tcp" + },{ + "Source": { + "Internal": { + "Workload": "default/deployment/nginx-deployment2", + "Namespace": "default" + } + }, + "Destination": { + "IP": "192.168.1.99" + }, + "Protocol": "TCP", + "ResolvedPort": 80, + "ResolvedPortName": "serve-80-tcp" } ] \ No newline at end of file diff --git a/cmd/policy-assistant/pkg/cli/analyze.go b/cmd/policy-assistant/pkg/cli/analyze.go index 322e90f4..6ef5247e 100644 --- a/cmd/policy-assistant/pkg/cli/analyze.go +++ b/cmd/policy-assistant/pkg/cli/analyze.go @@ -69,6 +69,14 @@ type AnalyzeArgs struct { ProbePath string Timeout time.Duration + + SourceWorkloadTraffic string + + DestinationWorkloadTraffic string + + Port int + + Protocol string } func SetupAnalyzeCommand() *cobra.Command { @@ -96,6 +104,10 @@ func SetupAnalyzeCommand() *cobra.Command { command.Flags().StringVar(&args.TrafficPath, "traffic-path", "", "path to json traffic file, containing of a list of traffic objects") command.Flags().StringVar(&args.ProbePath, "probe-path", "", "path to json model file for synthetic probe") command.Flags().DurationVar(&args.Timeout, "kube-client-timeout", DefaultTimeout, "kube client timeout") + command.Flags().StringVar(&args.SourceWorkloadTraffic, "src-workload", "", "Source workload traffic in this form namespace/workloadType/workloadName") + command.Flags().StringVar(&args.DestinationWorkloadTraffic, "dst-workload", "", "Destination workload traffic Name in this form namespace/workloadType/workloadName") + command.Flags().IntVar(&args.Port, "port", 0, "port used for testing network policies") + command.Flags().StringVar(&args.Protocol, "protocol", "", "protocol used for testing network policies") return command } @@ -173,7 +185,7 @@ func RunAnalyzeCommand(args *AnalyzeArgs) { ProbeSyntheticConnectivity(policies, args.ProbePath, kubePods, kubeNamespaces) case VerdictWalkthroughMode: fmt.Println("verdict walkthrough:") - VerdictWalkthrough(policies) + VerdictWalkthrough(policies, args.SourceWorkloadTraffic, args.DestinationWorkloadTraffic, args.Port, args.Protocol, args.TrafficPath) default: panic(errors.Errorf("unrecognized mode %s", mode)) } @@ -296,7 +308,106 @@ func shouldIncludeANPandBANP(client *kubernetes.Clientset) (bool, bool) { return includeANP, includeBANP } -func VerdictWalkthrough(policies *matcher.Policy) { +func VerdictWalkthrough(policies *matcher.Policy, sourceWorkloadTraffic string, destinationWorkloadTraffic string, port int, protocol string, trafficPath string) { + var sourceWorkloadInfo matcher.TrafficPeer + var destinationWorkloadInfo matcher.TrafficPeer + var allTraffic []*matcher.Traffic + + if trafficPath != "" && (sourceWorkloadTraffic != "" || destinationWorkloadTraffic != "" || port != 0 || protocol != "") { + logrus.Fatalf("%+v", errors.Errorf("If using traffic path, you can't input traffic via CLI and viceversa")) + } else if trafficPath == "" && (sourceWorkloadTraffic == "" || destinationWorkloadTraffic == "" || port == 0 || protocol == "") { + logrus.Fatalf("%+v", errors.Errorf("For this mode, you must either set --traffic-path or set all of --src-workload (//workloadName), --dst-workload (//workloadName), --port (integer from 0 to 65535) and --protocol (TCP, UDP and SCTP) parameters")) + } + + if trafficPath != "" { + allTraffics, err := json.ParseFile[[]*matcher.Traffic](trafficPath) + utils.DoOrDie(err) + for _, traffic := range *allTraffics { + var podA, podB *matcher.TrafficPeer + + // Determine source and destination peer information + sourceInternal := traffic.Source.Internal + destinationInternal := traffic.Destination.Internal + + podA = matcher.CreateTrafficPeer(traffic.Source.IP, nil) + podB = matcher.CreateTrafficPeer(traffic.Destination.IP, nil) + + // Update podA and podB if internal information is available + if sourceInternal != nil { + podA = matcher.CreateTrafficPeer(traffic.Source.IP, &matcher.InternalPeer{ + PodLabels: sourceInternal.PodLabels, + NamespaceLabels: sourceInternal.NamespaceLabels, + Namespace: sourceInternal.Namespace, + Workload: sourceInternal.Workload, + }) + } + + if destinationInternal != nil { + podB = matcher.CreateTrafficPeer(traffic.Destination.IP, &matcher.InternalPeer{ + PodLabels: destinationInternal.PodLabels, + NamespaceLabels: destinationInternal.NamespaceLabels, + Namespace: destinationInternal.Namespace, + Workload: destinationInternal.Workload, + }) + } + + // Special case handling for workload-specific traffic (internal vs. external) + if sourceInternal != nil { + if sourceInternal.Workload != "" { + podA = matcher.GetInternalPeerInfo(sourceInternal.Workload) + } + } + + if destinationInternal != nil { + if destinationInternal.Workload != "" { + podB = matcher.GetInternalPeerInfo(destinationInternal.Workload) + } + } + + // Append the resolved traffic to the allTraffic slice + allTraffic = append(allTraffic, matcher.CreateTraffic(podA, podB, traffic.ResolvedPort, string(traffic.Protocol))) + } + } else { + + if protocol != "TCP" && protocol != "UDP" && protocol != "SCTP" { + logrus.Fatalf("Bad Protocol Value: protocols supported are TCP, UDP and SCTP") + } + + sourceWorkloadInfo = matcher.WorkloadStringToTrafficPeer(sourceWorkloadTraffic) + destinationWorkloadInfo = matcher.WorkloadStringToTrafficPeer(destinationWorkloadTraffic) + + if sourceWorkloadInfo.Internal.Pods == nil || destinationWorkloadInfo.Internal.Pods == nil { + return + } + + podA := &matcher.TrafficPeer{ + Internal: &matcher.InternalPeer{ + PodLabels: sourceWorkloadInfo.Internal.PodLabels, + NamespaceLabels: sourceWorkloadInfo.Internal.NamespaceLabels, + Namespace: sourceWorkloadInfo.Internal.Namespace, + Workload: sourceWorkloadInfo.Internal.Workload, + }, + IP: sourceWorkloadInfo.Internal.Pods[0].IP, + } + podB := &matcher.TrafficPeer{ + Internal: &matcher.InternalPeer{ + PodLabels: destinationWorkloadInfo.Internal.PodLabels, + NamespaceLabels: destinationWorkloadInfo.Internal.NamespaceLabels, + Namespace: destinationWorkloadInfo.Internal.Namespace, + Workload: destinationWorkloadInfo.Internal.Workload, + }, + IP: destinationWorkloadInfo.Internal.Pods[0].IP, + } + allTraffic = []*matcher.Traffic{ + { + Source: podA, + Destination: podB, + ResolvedPort: port, + Protocol: v1.Protocol(protocol), + }, + } + } + tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) table.SetAutoWrapText(false) @@ -304,51 +415,6 @@ func VerdictWalkthrough(policies *matcher.Policy) { table.SetAutoMergeCells(true) table.SetHeader([]string{"Traffic", "Verdict", "Ingress Walkthrough", "Egress Walkthrough"}) - - // FIXME: use pod resources from CLI arguments or JSON - podA := &matcher.TrafficPeer{ - Internal: &matcher.InternalPeer{ - PodLabels: map[string]string{"pod": "a"}, - NamespaceLabels: map[string]string{"kubernetes.io/metadata.name": "demo"}, - Namespace: "demo", - }, - IP: "10.0.0.4", - } - podB := &matcher.TrafficPeer{ - Internal: &matcher.InternalPeer{ - PodLabels: map[string]string{"pod": "b"}, - NamespaceLabels: map[string]string{"kubernetes.io/metadata.name": "demo"}, - Namespace: "demo", - }, - IP: "10.0.0.5", - } - allTraffic := []*matcher.Traffic{ - { - Source: podA, - Destination: podB, - ResolvedPort: 80, - Protocol: v1.ProtocolTCP, - }, - { - Source: podA, - Destination: podB, - ResolvedPort: 81, - Protocol: v1.ProtocolTCP, - }, - { - Source: podB, - Destination: podA, - ResolvedPort: 80, - Protocol: v1.ProtocolTCP, - }, - { - Source: podB, - Destination: podA, - ResolvedPort: 81, - Protocol: v1.ProtocolTCP, - }, - } - for _, traffic := range allTraffic { trafficResult := policies.IsTrafficAllowed(traffic) ingressFlow := trafficResult.Ingress.Flow() diff --git a/cmd/policy-assistant/pkg/matcher/traffic.go b/cmd/policy-assistant/pkg/matcher/traffic.go index 855caeb8..14f4d526 100644 --- a/cmd/policy-assistant/pkg/matcher/traffic.go +++ b/cmd/policy-assistant/pkg/matcher/traffic.go @@ -58,29 +58,32 @@ func labelsToString(labels map[string]string) string { return strings.Join(slice.Map(format, slice.Sort(maps.Keys(labels))), "\n") } -func (t *Traffic) PrettyString() string { - if t == nil || t.Source == nil || t.Destination == nil { - return "" +// Helper function to generate the string for source or destination +func (t *Traffic) formatPeer(peer *TrafficPeer) string { + if peer.Internal == nil { + return fmt.Sprintf("%s", peer.IP) } - src := t.Source.Internal.Workload - if src == "" { - if t.Source.Internal == nil { - return "" - } - - src = fmt.Sprintf("%s/%s", t.Source.Internal.Namespace, labelsToStringSlim(t.Source.Internal.PodLabels)) + // If there is a workload, return it + if peer.Internal.Workload != "" { + return peer.Internal.Workload } - dst := t.Destination.Internal.Workload - if dst == "" { - if t.Destination.Internal == nil { - return "" - } + // Otherwise, return namespace and labels + return fmt.Sprintf("%s/%s", peer.Internal.Namespace, labelsToStringSlim(peer.Internal.PodLabels)) +} - dst = fmt.Sprintf("%s/%s", t.Destination.Internal.Namespace, labelsToStringSlim(t.Destination.Internal.PodLabels)) +// PrettyString refactor to reduce nested if/else blocks +func (t *Traffic) PrettyString() string { + if t == nil || t.Source == nil || t.Destination == nil { + return "" } + // Format source and destination peers + src := t.formatPeer(t.Source) + dst := t.formatPeer(t.Destination) + + // If both source and destination are internal, we need to check the workload conditions return fmt.Sprintf("%s -> %s:%d (%s)", src, dst, t.ResolvedPort, t.Protocol) } @@ -107,6 +110,50 @@ func (p *TrafficPeer) IsExternal() bool { return p.Internal == nil } +func CreateTrafficPeer(ip string, internal *InternalPeer) *TrafficPeer { + return &TrafficPeer{ + IP: ip, + Internal: internal, + } +} + +// Helper function to create Traffic objects +func CreateTraffic(source, destination *TrafficPeer, resolvedPort int, protocol string) *Traffic { + return &Traffic{ + Source: source, + Destination: destination, + ResolvedPort: resolvedPort, + Protocol: v1.Protocol(protocol), + } +} + +// Helper function to get internal TrafficPeer info from workload string +func GetInternalPeerInfo(workload string) *TrafficPeer { + if workload == "" { + return nil + } + workloadInfo := WorkloadStringToTrafficPeer(workload) + if workloadInfo.Internal.Pods == nil { + return &TrafficPeer{ + Internal: &InternalPeer{ + PodLabels: workloadInfo.Internal.PodLabels, + NamespaceLabels: workloadInfo.Internal.NamespaceLabels, + Namespace: workloadInfo.Internal.Namespace, + Workload: workloadInfo.Internal.Workload, + }, + } + } + return &TrafficPeer{ + Internal: &InternalPeer{ + PodLabels: workloadInfo.Internal.PodLabels, + NamespaceLabels: workloadInfo.Internal.NamespaceLabels, + Namespace: workloadInfo.Internal.Namespace, + Workload: workloadInfo.Internal.Workload, + }, + IP: workloadInfo.Internal.Pods[0].IP, + } +} + func (p *TrafficPeer) Translate() TrafficPeer { //Translates kubernetes workload types to TrafficPeers. var podsNetworking []*PodNetworking @@ -159,7 +206,7 @@ func (p *TrafficPeer) Translate() TrafficPeer { } if !workloadOwnerExists { - logrus.Infof("workload not found on the cluster") + logrus.Infof(workloadMetadata[0] + "/" + workloadMetadata[1] + "/" + workloadMetadata[2] + " workload not found on the cluster") internalPeer = InternalPeer{ Workload: "", } @@ -179,6 +226,24 @@ func (p *TrafficPeer) Translate() TrafficPeer { return TranslatedPeer } +func WorkloadStringToTrafficPeer(workloadString string) TrafficPeer { + //Translates a Workload string to a TrafficPeer. + //var deploymentPeers []TrafficPeer + + tmpInternalPeer := InternalPeer{ + Workload: workloadString, + } + tmpPeer := TrafficPeer{ + Internal: &tmpInternalPeer, + } + tmpPeerTranslated := tmpPeer.Translate() + //if tmpPeerTranslated.Internal.Workload != "" { + // deploymentPeers = append(deploymentPeers, tmpPeerTranslated) + //} + + return tmpPeerTranslated +} + func DeploymentsToTrafficPeers() []TrafficPeer { //Translates all pods associated with deployments to TrafficPeers. var deploymentPeers []TrafficPeer