From 3e9fa2bc47969cea926c33a1eeb9151a04b88fb0 Mon Sep 17 00:00:00 2001 From: deggja Date: Thu, 16 May 2024 00:25:38 +0200 Subject: [PATCH 1/5] feat: initial code to add table format to target scanners Signed-off-by: deggja --- backend/cmd/scan.go | 111 +++++++++++++++++++++++------- backend/pkg/k8s/target-scanner.go | 10 +-- 2 files changed, 93 insertions(+), 28 deletions(-) diff --git a/backend/cmd/scan.go b/backend/cmd/scan.go index 5d027b2..b63117d 100644 --- a/backend/cmd/scan.go +++ b/backend/cmd/scan.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" "github.com/deggja/netfetch/backend/pkg/k8s" "github.com/spf13/cobra" ) @@ -20,7 +22,9 @@ var scanCmd = &cobra.Command{ Short: "Scan Kubernetes namespaces for network policies", Long: `Scan Kubernetes namespaces for network policies. By default, it scans for native Kubernetes network policies. - Use --cilium to scan for Cilium network policies. You may also target a speecific network policy using --target-policy.`, + Use --cilium to scan for Cilium network policies. + You may also target a specific network policy using the --target flag. + This can be used in combination with --native and --cilium for select policy types.`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { var namespace string @@ -59,40 +63,36 @@ var scanCmd = &cobra.Command{ 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) - } + fmt.Println(createTargetPodsTable(pods, foundNamespace)) } } return } } - // Handle target policy for Cilium network policies and cluster-wide policies + // 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 + // 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) + fmt.Println("Error during Cilium cluster wide network policy search:", err) } else { - fmt.Printf("Found Cilium cluster-wide network policy '%s'.\n", policy.GetName()) + fmt.Printf("Found Cilium clusterwide network policy '%s'.\n", policy.GetName()) - // List the pods targeted by this cluster-wide policy + // 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) + 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()) + 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) - } + fmt.Printf("Pods targeted by cluster wide policy '%s':\n", policy.GetName()) + fmt.Println(createTargetPodsTable(pods, "")) } } } else { @@ -106,9 +106,7 @@ var scanCmd = &cobra.Command{ 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) - } + fmt.Println(createTargetPodsTable(pods, foundNamespace)) } } return @@ -128,9 +126,9 @@ var scanCmd = &cobra.Command{ // Perform Cilium network policy scan if --cilium is used if cilium { - // Perform cluster-wide Cilium scan first if no namespace is specified + // Perform cluster wide Cilium scan first if no namespace is specified if namespace == "" { - fmt.Println("Running cluster-wide Cilium network policies scan...") + fmt.Println("Running cluster wide Cilium network policies scan...") dynamicClient, err := k8s.GetCiliumDynamicClient() if err != nil { fmt.Println("Error obtaining dynamic client:", err) @@ -139,9 +137,9 @@ var scanCmd = &cobra.Command{ clusterwideScanResult, err := k8s.ScanCiliumClusterwideNetworkPolicies(dynamicClient, false, dryRun, true) if err != nil { - fmt.Println("Error during cluster-wide Cilium network policies scan:", err) + fmt.Println("Error during cluster wide Cilium network policies scan:", err) } else { - // Handle the cluster-wide scan result; skip further scanning if all pods are protected + // Handle the cluster wide scan result; skip further scanning if all pods are protected if clusterwideScanResult.AllPodsProtected { fmt.Println("All pods are protected by cluster wide cilium policies.\nYour Netfetch security score is: 42/42") return @@ -166,10 +164,77 @@ func handleScanResult(scanResult *k8s.ScanResult) { // Implement your logic to handle scan results } +var ( + EvenRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) + OddRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) +) + +// Function to create a table for pods +func createTargetPodsTable(pods []string, namespace string) string { + podsInfo := [][]string{} + for _, pod := range pods { + podsInfo = append(podsInfo, []string{namespace, pod, "N/A"}) + } + + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return HeaderStyle + case row%2 == 0: + return EvenRowStyle + default: + return OddRowStyle + } + }). + Headers("Namespace", "Pod Name", "IP Address") + + for _, row := range podsInfo { + formattedRow := make([]string, 3) + for i := 0; i < 3; i++ { + if i < len(row) { + formattedRow[i] = row[i] + } else { + formattedRow[i] = "N/A" + } + } + + t.Row(formattedRow[0], formattedRow[1], formattedRow[2]) + } + + return t.String() +} + +// Function to create a table for policies +func createTargetPoliciesTable(policiesInfo [][]string) string { + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return HeaderStyle + case row%2 == 0: + return EvenRowStyle + default: + return OddRowStyle + } + }). + Headers("Policy Name") + + for _, row := range policiesInfo { + t.Row(row...) + } + + return t.String() +} + func init() { scanCmd.Flags().BoolVarP(&dryRun, "dryrun", "d", false, "Perform a dry run without applying any changes") scanCmd.Flags().BoolVar(&native, "native", false, "Scan only native network policies") - scanCmd.Flags().BoolVar(&cilium, "cilium", false, "Scan only Cilium network policies (includes cluster-wide policies if no namespace is specified)") + scanCmd.Flags().BoolVar(&cilium, "cilium", false, "Scan only Cilium network policies (includes cluster wide policies if no namespace is specified)") scanCmd.Flags().StringVarP(&targetPolicy, "target", "t", "", "Scan a specific network policy by name") scanCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") rootCmd.AddCommand(scanCmd) diff --git a/backend/pkg/k8s/target-scanner.go b/backend/pkg/k8s/target-scanner.go index 8ebd82e..cbf7558 100644 --- a/backend/pkg/k8s/target-scanner.go +++ b/backend/pkg/k8s/target-scanner.go @@ -55,10 +55,10 @@ func FindCiliumNetworkPolicyByName(dynamicClient dynamic.Interface, policyName s return policy, namespace, nil } } - return nil, "", fmt.Errorf("Cilium network policy %s not found in any non-system namespace", policyName) + 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. +// 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", @@ -68,7 +68,7 @@ func FindCiliumClusterWideNetworkPolicyByName(dynamicClient dynamic.Interface, p 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 nil, fmt.Errorf("cilium cluster wide network policy %s not found", policyName) } return policy, nil } @@ -164,12 +164,12 @@ func ListPodsTargetedByCiliumNetworkPolicy(dynamicClient dynamic.Interface, poli return targetedPods, nil } -// ListPodsTargetedByCiliumClusterWideNetworkPolicy lists all pods targeted by the given Cilium cluster-wide network policy. +// 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) + 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 From 40820703c2ae0fc5b88e358905a4d3d2185bdde1 Mon Sep 17 00:00:00 2001 From: deggja Date: Mon, 20 May 2024 20:47:23 +0200 Subject: [PATCH 2/5] feat: updated styling of output for scanners in table format to accomodate look and feel to the rest of the scanners Signed-off-by: deggja --- backend/cmd/scan.go | 81 +++++++++---------------------- backend/pkg/k8s/target-scanner.go | 39 ++++++++------- 2 files changed, 43 insertions(+), 77 deletions(-) diff --git a/backend/cmd/scan.go b/backend/cmd/scan.go index b63117d..dc0f8f4 100644 --- a/backend/cmd/scan.go +++ b/backend/cmd/scan.go @@ -165,70 +165,33 @@ func handleScanResult(scanResult *k8s.ScanResult) { } var ( - EvenRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) - OddRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) + headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6")).Align(lipgloss.Center) + evenRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) + oddRowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) + tableBorderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("99")) ) // Function to create a table for pods -func createTargetPodsTable(pods []string, namespace string) string { - podsInfo := [][]string{} - for _, pod := range pods { - podsInfo = append(podsInfo, []string{namespace, pod, "N/A"}) - } - - t := table.New(). - Border(lipgloss.NormalBorder()). - BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). - StyleFunc(func(row, col int) lipgloss.Style { - switch { - case row == 0: - return HeaderStyle - case row%2 == 0: - return EvenRowStyle - default: - return OddRowStyle - } - }). - Headers("Namespace", "Pod Name", "IP Address") - - for _, row := range podsInfo { - formattedRow := make([]string, 3) - for i := 0; i < 3; i++ { - if i < len(row) { - formattedRow[i] = row[i] - } else { - formattedRow[i] = "N/A" - } - } - - t.Row(formattedRow[0], formattedRow[1], formattedRow[2]) - } - - return t.String() -} - -// Function to create a table for policies -func createTargetPoliciesTable(policiesInfo [][]string) string { - t := table.New(). - Border(lipgloss.NormalBorder()). - BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). - StyleFunc(func(row, col int) lipgloss.Style { - switch { - case row == 0: - return HeaderStyle - case row%2 == 0: - return EvenRowStyle - default: - return OddRowStyle - } - }). - Headers("Policy Name") +func createTargetPodsTable(pods [][]string, namespace string) string { + t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(tableBorderStyle). + StyleFunc(func(row, col int) lipgloss.Style { + if row == 0 { + return headerStyle + } + if row%2 == 0 { + return evenRowStyle + } + return oddRowStyle + }). + Headers("Namespace", "Pod Name", "IP Address") - for _, row := range policiesInfo { - t.Row(row...) - } + for _, podDetails := range pods { + t.Row(podDetails...) + } - return t.String() + return t.String() } func init() { diff --git a/backend/pkg/k8s/target-scanner.go b/backend/pkg/k8s/target-scanner.go index cbf7558..8b341e0 100644 --- a/backend/pkg/k8s/target-scanner.go +++ b/backend/pkg/k8s/target-scanner.go @@ -5,7 +5,6 @@ import ( "fmt" "regexp" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -82,7 +81,7 @@ func GetAllNonSystemNamespaces(dynamicClient dynamic.Interface) ([]string, error Resource: "namespaces", } - namespacesList, err := dynamicClient.Resource(gvr).List(context.TODO(), metav1.ListOptions{}) + namespacesList, err := dynamicClient.Resource(gvr).List(context.TODO(), v1.ListOptions{}) if err != nil { return nil, fmt.Errorf("error listing namespaces: %v", err) } @@ -97,7 +96,7 @@ func GetAllNonSystemNamespaces(dynamicClient dynamic.Interface) ([]string, error } // ListPodsTargetedByNetworkPolicy lists all pods targeted by the given network policy in the specified namespace. -func ListPodsTargetedByNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured, namespace string) ([]string, error) { +func ListPodsTargetedByNetworkPolicy(dynamicClient dynamic.Interface, policy *unstructured.Unstructured, namespace string) ([][]string, error) { // Retrieve the PodSelector (matchLabels) podSelector, found, err := unstructured.NestedMap(policy.Object, "spec", "podSelector", "matchLabels") if err != nil { @@ -117,21 +116,25 @@ func ListPodsTargetedByNetworkPolicy(dynamicClient dynamic.Interface, policy *un } // Fetch pods based on the selector - pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), v1.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) - } + var targetedPods [][]string + for _, pod := range pods.Items { + podDetails := []string{namespace, pod.Name, pod.Status.PodIP} + if pod.Status.PodIP == "" { + podDetails[2] = "N/A" + } + targetedPods = append(targetedPods, podDetails) + } 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) { +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 { @@ -151,21 +154,21 @@ func ListPodsTargetedByCiliumNetworkPolicy(dynamicClient dynamic.Interface, poli } // Fetch pods based on the selector - pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), v1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) if err != nil { return nil, fmt.Errorf("error listing pods in namespace %s: %v", namespace, err) } - var targetedPods []string + var targetedPods [][]string for _, pod := range pods.Items { - targetedPods = append(targetedPods, pod.Name) + targetedPods = append(targetedPods, []string{namespace, pod.Name, pod.Status.PodIP}) } 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) { +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 { @@ -193,15 +196,15 @@ func ListPodsTargetedByCiliumClusterWideNetworkPolicy(dynamicClient dynamic.Inte } // Fetch pods based on the selector - pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) + pods, err := clientset.CoreV1().Pods("").List(context.TODO(), v1.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) - } + var targetedPods [][]string + for _, pod := range pods.Items { + targetedPods = append(targetedPods, []string{"", pod.Name, pod.Status.PodIP}) + } return targetedPods, nil } From 772797206dac4a0684f0ce4035ed56af82f6279a Mon Sep 17 00:00:00 2001 From: deggja Date: Sun, 26 May 2024 11:33:49 +0200 Subject: [PATCH 3/5] chore: update version bumping script and release workflow Signed-off-by: deggja --- .github/scripts/bump_version.sh | 22 +++++++++++++++------- .github/workflows/release.yml | 8 ++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/scripts/bump_version.sh b/.github/scripts/bump_version.sh index 3d2302d..4a88d5f 100755 --- a/.github/scripts/bump_version.sh +++ b/.github/scripts/bump_version.sh @@ -14,6 +14,8 @@ if [ ! -z "$latest_tag" ]; then IFS='.' read -r major minor patch <<< "${latest_tag}" fi +new_tag="$major.$minor.$patch" + # Analyze commit messages since the last tag for versioning for commit in $(git rev-list $latest_tag..HEAD); do message=$(git log --format=%B -n 1 $commit) @@ -22,20 +24,26 @@ for commit in $(git rev-list $latest_tag..HEAD); do let major+=1 minor=0 patch=0 + new_tag="${major}.${minor}.${patch}" break elif [[ $message == *"#minor"* ]]; then let minor+=1 patch=0 + new_tag="${major}.${minor}.${patch}" elif [[ $message == *"#patch"* ]]; then let patch+=1 + new_tag="${major}.${minor}.${patch}" fi done -new_tag="${major}.${minor}.${patch}" - -# Set output for the next steps using environment file -echo "new_tag=$new_tag" >> $GITHUB_ENV +# Check if new version is different from the latest tag +if [ "$new_tag" != "$latest_tag" ]; then + # Set output for the next steps using environment file + echo "new_tag=$new_tag" >> $GITHUB_ENV -# Create the new tag -git tag $new_tag -git push --tags + # Create the new tag + git tag $new_tag + git push --tags +else + echo "new_tag=" >> $GITHUB_ENV +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 745e1e5..fe019f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,8 +33,16 @@ jobs: run: | chmod +x .github/scripts/bump_version.sh .github/scripts/bump_version.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Application + if: env.new_tag != '' + run: | + go build -ldflags "-X 'github.com/deggja/netfetch/backend/cmd.Version=${{ env.new_tag }}'" -o netfetch - name: Run GoReleaser + if: env.new_tag != '' uses: goreleaser/goreleaser-action@v2 with: version: latest From 3deb2dabca2cb44b14b254a9fb98f1b016c4a8e1 Mon Sep 17 00:00:00 2001 From: deggja Date: Sun, 26 May 2024 11:48:24 +0200 Subject: [PATCH 4/5] feat: finished styling output in target scanner #patch Signed-off-by: deggja --- backend/cmd/scan.go | 2 +- backend/pkg/k8s/target-scanner.go | 70 ++++++++++++++++--------------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/backend/cmd/scan.go b/backend/cmd/scan.go index dc0f8f4..d0d4780 100644 --- a/backend/cmd/scan.go +++ b/backend/cmd/scan.go @@ -85,7 +85,7 @@ var scanCmd = &cobra.Command{ fmt.Printf("Found Cilium clusterwide network policy '%s'.\n", policy.GetName()) // List the pods targeted by this cluster wide policy - pods, err := k8s.ListPodsTargetedByCiliumClusterWideNetworkPolicy(dynamicClient, policy) + pods, err := k8s.ListPodsTargetedByCiliumClusterWideNetworkPolicy(clientset, 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 { diff --git a/backend/pkg/k8s/target-scanner.go b/backend/pkg/k8s/target-scanner.go index 8b341e0..1119904 100644 --- a/backend/pkg/k8s/target-scanner.go +++ b/backend/pkg/k8s/target-scanner.go @@ -168,44 +168,46 @@ func ListPodsTargetedByCiliumNetworkPolicy(dynamicClient dynamic.Interface, poli } // 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) - } +func ListPodsTargetedByCiliumClusterWideNetworkPolicy(clientset *kubernetes.Clientset, 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]$`) + // 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()) - } - } - } + // Check if the selector is empty + selector := 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(), v1.ListOptions{LabelSelector: selector.AsSelectorPreValidated().String()}) - if err != nil { - return nil, fmt.Errorf("error listing pods: %v", err) - } + // Fetch pods based on the selector across all namespaces + pods, err := clientset.CoreV1().Pods("").List(context.TODO(), v1.ListOptions{ + LabelSelector: selector.AsSelector().String(), + }) + if err != nil { + return nil, fmt.Errorf("error listing pods for cluster wide policy: %v", err) + } - var targetedPods [][]string + var targetedPods [][]string for _, pod := range pods.Items { - targetedPods = append(targetedPods, []string{"", pod.Name, pod.Status.PodIP}) + podDetails := []string{pod.Namespace, pod.Name, pod.Status.PodIP} + targetedPods = append(targetedPods, podDetails) } - return targetedPods, nil -} - + return targetedPods, nil +} \ No newline at end of file From 2d982c7ea3eb73c6c639fb973e3b848fd7f5bcc7 Mon Sep 17 00:00:00 2001 From: deggja Date: Sun, 26 May 2024 12:20:04 +0200 Subject: [PATCH 5/5] docs: update readme Signed-off-by: deggja --- README.md | 93 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 819a475..a306fed 100644 --- a/README.md +++ b/README.md @@ -40,32 +40,24 @@ This project aims to demystify network policies in Kubernetes. It's a work in progress! -The `netfetch` tool is designed to scan Kubernetes namespaces for network policies, checking if your workloads are targeted by a network policy or not. - -What can I use `netfetch` for? 🤔 - -CLI: -- Scan your Kubernetes cluster or namespace to identify pods running with no ingress and egress restrictions. -- Save the output of your scans in a text file to analyze. -- Create implicit default deny network policies in namespaces that do not have one. -- Get a score calculated for your cluster or namespace based on the findings of the scans. - -Dashboard: -- Scan your cluster or namespace and list pods running without network restrictions in a table. -- Visualise all existing network policies and pods in your cluster or namespace in a network map you can interact with. -- Double click a network policy in a network map to preview the YAML of that policy. -- Create implicit default deny network policies in namespaces that do not have one. -- Get suggestions for network policies that you can edit & apply to your namespaces by analysing existing pods. -- Get a score calculated for your cluster or namespace based on the findings of the scans. +The `netfetch` tool will scan your Kubernetes cluster and let you know if you have any pods running without being targeted by network policies. + +| Feature | CLI | Dashboard | +|------------------------------------------------------------------------|------|-----------| +| Scan cluster identify pods without network policies | ✓ | ✓ | +| Save scan output to a text file | ✓ | | +| Visualize network policies and pods in a interactive network map | | ✓ | +| Create default deny network policies where this is missing | ✓ | ✓ | +| Get suggestions for network policies based on existing workloads | | ✓ | +| Calculate a security score based on scan findings | ✓ | ✓ | +| Scan a specific policy by name to see what pods it targets | ✓ | | ### NetworkPolicy type support in Netfetch -Dashboard: -* Kubernetes - -CLI: -* Kubernetes -* Cilium +| Type | CLI | Dashboard | +|-----------|------|-----------| +| Kubernetes| ✓ | ✓ | +| Cilium | ✓ | | Support for additional types of network policies is in the works. No support for the type you need? Check out [issues](https://github.com/deggja/netfetch/issues) for an existing request or create a new one if there is none. @@ -118,7 +110,7 @@ netfetch scan --dryrun Run `netfetch` in dryrun against a namespace ```sh -netfetch scan production --dryrun +netfetch scan crossplane-system --dryrun ``` ![netfetch-demo](https://github.com/deggja/netfetch/assets/15778492/015e9d9f-a678-4a14-a8bd-607f02c13d9f) @@ -129,61 +121,70 @@ Scan entire cluster. netfetch scan ``` -Scan a namespace called production. +Scan a namespace called crossplane-system. ```sh -netfetch scan production +netfetch scan crossplane-system ``` -Scan entire cluster for Cilium Network Policies. +Scan entire cluster for Cilium Network Policies and or Cluster Wide Cilium Network Policies. ```sh netfetch scan --cilium ``` -Scan a namespace called production. +Scan a namespace called production for regular Cilium Network Policies. ```sh netfetch scan production --cilium ``` -### Using the dashboard 📟 - -Launch the dashboard: +Scan a specific network policy. ```sh -netfetch dash +netfetch scan --target my-policy-name ``` -You may also specify a port for the dashboard to run on (default is 8080). +Scan a specific Cilium Network Policy. ```sh -netfetch dash --port 8081 +netfetch scan --cilium --target default-cilium-default-deny-all ``` -While in the dashboard, you have a couple of options. +[![asciicast](https://asciinema.org/a/661200.svg)](https://asciinema.org/a/661200) -You can use the `Scan cluster` button, which is the equivalent to the CLI `netfetch scan` command. This will populate the table view with all pods not targeted by a network policy. +### Using the dashboard 📟 -Scanning a specific namespace is done by selecting the namespace of choice from the `Select a namespace` dropdown and using the `Scan namespace` button. This is the equivalent to the CLI `netfetch scan namespace` command. +Launch the dashboard: -This will populate the table view with all pods not targeted by a network policy in that specific namespace. In addition to this, if there are any pods in the cluster already targeted by a network policy - it will create a visualisation of this in a network map rendered using [D3](https://d3-graph-gallery.com/network.html) below the table view. +```sh +netfetch dash +``` -![Netfetch Dashboard](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-dash.png) +You may also specify a port for the dashboard to run on (default is 8080). -You can click the `Create cluster map` button to do exactly that. This will render a network map with D3, fetching all pods and policies in all the namespaces you have access to in the cluster. +```sh +netfetch dash --port 8081 +``` -![Cluster map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-clustermap.png) +### Dashboard functionality overview -Inside the network map visualisations, you can double click the network policy nodes to preview the YAML of that policy. +The Netfetch Dashboard offers an intuitive interface for interacting with your Kubernetes cluster's network policies. Below is a detailed overview of the functionalities available through the dashboard: -![Network map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-ns.png) +| Action | Description | Screenshot Link | +|----------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| Scan Cluster | Initiates a cluster-wide scan to identify pods without network policies, similar to `netfetch scan`. | ![Netfetch Dashboard](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-dash.png) | +| Scan Namespace | Scans a selected namespace for pods not covered by network policies, equivalent to `netfetch scan namespace`. | ![Cluster map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-clustermap.png) | +| Create Cluster Map | Generates a D3-rendered network map of all pods and policies across accessible namespaces. | ![Network map](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-ns.png) | +| Suggest Policy | Provides network policy suggestions based on existing workloads within a selected namespace. | ![Suggested policies](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-suggestpolicy.png) | -When scanning a specific namespace using the `Select namespace` dropdown, you may click `Suggest policy` to get network policy suggestions based on your existing workloads. +### Interactive Features -![Suggested policies](https://github.com/deggja/netfetch/blob/main/frontend/dash/src/assets/new-suggestpolicy.png) +- **Table View**: Shows pods not targeted by network policies. It updates based on the cluster or namespace scans. +- **Network Map Visualization**: Rendered using D3 to show how pods and policies interact within the cluster. +- **Policy Preview**: Double-click network policy nodes within the network map to view policy YAML. +- **Policy Editing**: Edit suggested policies directly within the dashboard or copy the YAML for external use. -You may also edit the suggestions inline by using the "Edit" button or copy the YAML of the policy and use it outside of netfetch. ### Netfetch score 🥇