diff --git a/backend/cmd/scan.go b/backend/cmd/scan.go index 40bd8b2..5d027b2 100644 --- a/backend/cmd/scan.go +++ b/backend/cmd/scan.go @@ -68,6 +68,52 @@ var scanCmd = &cobra.Command{ } } + // Handle target policy for Cilium network policies and cluster-wide policies + if targetPolicy != "" && cilium { + fmt.Println("Policy type: Cilium") + fmt.Printf("Searching for Cilium network policy '%s' across all non-system namespaces...\n", targetPolicy) + policy, foundNamespace, err := k8s.FindCiliumNetworkPolicyByName(dynamicClient, targetPolicy) + if err != nil { + // If not found in namespaces, search for cluster-wide policy + fmt.Println("Cilium network policy not found in namespaces, searching for cluster-wide policy...") + policy, err = k8s.FindCiliumClusterWideNetworkPolicyByName(dynamicClient, targetPolicy) + if err != nil { + fmt.Println("Error during Cilium cluster-wide network policy search:", err) + } else { + fmt.Printf("Found Cilium cluster-wide network policy '%s'.\n", policy.GetName()) + + // List the pods targeted by this cluster-wide policy + pods, err := k8s.ListPodsTargetedByCiliumClusterWideNetworkPolicy(dynamicClient, policy) + if err != nil { + fmt.Printf("Error listing pods targeted by cluster-wide policy %s: %v\n", policy.GetName(), err) + } else if len(pods) == 0 { + fmt.Printf("No pods targeted by cluster-wide policy '%s'.\n", policy.GetName()) + } else { + fmt.Printf("Pods targeted by cluster-wide policy '%s':\n", policy.GetName()) + for _, pod := range pods { + fmt.Printf(" - %s\n", pod) + } + } + } + } else { + fmt.Printf("Found Cilium network policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) + + // List the pods targeted by this policy + pods, err := k8s.ListPodsTargetedByCiliumNetworkPolicy(dynamicClient, policy, foundNamespace) + if err != nil { + fmt.Printf("Error listing pods targeted by policy %s: %v\n", policy.GetName(), err) + } else if len(pods) == 0 { + fmt.Printf("No pods targeted by policy '%s' in namespace '%s'.\n", policy.GetName(), foundNamespace) + } else { + fmt.Printf("Pods targeted by policy '%s' in namespace '%s':\n", policy.GetName(), foundNamespace) + for _, pod := range pods { + fmt.Printf(" - %s\n", pod) + } + } + } + return + } + // Default to native scan if no specific type is mentioned or if --native is used if !cilium || native { fmt.Println("Running native network policies scan...") diff --git a/backend/netfetch b/backend/netfetch deleted file mode 100755 index 4073795..0000000 Binary files a/backend/netfetch and /dev/null differ diff --git a/backend/pkg/k8s/target-scanner.go b/backend/pkg/k8s/target-scanner.go index f910851..8ebd82e 100644 --- a/backend/pkg/k8s/target-scanner.go +++ b/backend/pkg/k8s/target-scanner.go @@ -3,6 +3,7 @@ package k8s import ( "context" "fmt" + "regexp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,6 +36,44 @@ func FindNativeNetworkPolicyByName(dynamicClient dynamic.Interface, clientset *k return nil, "", fmt.Errorf("network policy %s not found in any non-system namespace", policyName) } +// FindCiliumNetworkPolicyByName searches for a specific Cilium network policy by name across all non-system namespaces. +func FindCiliumNetworkPolicyByName(dynamicClient dynamic.Interface, policyName string) (*unstructured.Unstructured, string, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumnetworkpolicies", + } + + namespaces, err := GetAllNonSystemNamespaces(dynamicClient) + if err != nil { + return nil, "", fmt.Errorf("error getting namespaces: %v", err) + } + + for _, namespace := range namespaces { + policy, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(context.TODO(), policyName, v1.GetOptions{}) + if err == nil { + return policy, namespace, nil + } + } + return nil, "", fmt.Errorf("Cilium network policy %s not found in any non-system namespace", policyName) +} + +// FindCiliumClusterWideNetworkPolicyByName searches for a specific cluster-wide Cilium network policy by name. +func FindCiliumClusterWideNetworkPolicyByName(dynamicClient dynamic.Interface, policyName string) (*unstructured.Unstructured, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumclusterwidenetworkpolicies", + } + + policy, err := dynamicClient.Resource(gvr).Get(context.TODO(), policyName, v1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Cilium cluster-wide network policy %s not found", policyName) + } + return policy, nil +} + + // GetAllNonSystemNamespaces returns a list of all non-system namespaces using a dynamic client. func GetAllNonSystemNamespaces(dynamicClient dynamic.Interface) ([]string, error) { gvr := schema.GroupVersionResource{ @@ -90,3 +129,80 @@ func ListPodsTargetedByNetworkPolicy(dynamicClient dynamic.Interface, policy *un return targetedPods, nil } + +// ListPodsTargetedByCiliumNetworkPolicy lists all pods targeted by the given Cilium network policy in the specified namespace. +func ListPodsTargetedByCiliumNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured, namespace string) ([]string, error) { + // Retrieve the PodSelector (matchLabels) + podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "endpointSelector", "matchLabels") + if err != nil { + return nil, fmt.Errorf("failed to retrieve pod selector from Cilium network policy %s: %v", policy.GetName(), err) + } + + // Check if the selector is empty + selector := make(labels.Set) + if found && len(podSelector) > 0 { + for key, value := range podSelector { + if strValue, ok := value.(string); ok { + selector[key] = strValue + } else { + return nil, fmt.Errorf("invalid type for selector value %v in policy %s", value, policy.GetName()) + } + } + } + + // Fetch pods based on the selector + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) + if err != nil { + return nil, fmt.Errorf("error listing pods in namespace %s: %v", namespace, err) + } + + var targetedPods []string + for _, pod := range pods.Items { + targetedPods = append(targetedPods, pod.Name) + } + + return targetedPods, nil +} + +// ListPodsTargetedByCiliumClusterWideNetworkPolicy lists all pods targeted by the given Cilium cluster-wide network policy. +func ListPodsTargetedByCiliumClusterWideNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured) ([]string, error) { + // Retrieve the PodSelector (matchLabels) + podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "endpointSelector", "matchLabels") + if err != nil { + return nil, fmt.Errorf("failed to retrieve pod selector from Cilium cluster-wide network policy %s: %v", policy.GetName(), err) + } + + // Regex for valid Kubernetes label keys + validLabelKey := regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`) + + // Check if the selector is empty + selector := make(labels.Set) + if found && len(podSelector) > 0 { + for key, value := range podSelector { + // Skip reserved labels + if !validLabelKey.MatchString(key) { + fmt.Printf("Skipping reserved label key %s in policy %s\n", key, policy.GetName()) + continue + } + if strValue, ok := value.(string); ok { + selector[key] = strValue + } else { + return nil, fmt.Errorf("invalid type for selector value %v in policy %s", value, policy.GetName()) + } + } + } + + // Fetch pods based on the selector + pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) + if err != nil { + return nil, fmt.Errorf("error listing pods: %v", err) + } + + var targetedPods []string + for _, pod := range pods.Items { + targetedPods = append(targetedPods, pod.Name) + } + + return targetedPods, nil +} +