From 03b1e522ecd12eb54ff50f361b3cbeaaf26f4c71 Mon Sep 17 00:00:00 2001 From: deggja Date: Fri, 15 Nov 2024 14:10:01 +0100 Subject: [PATCH 1/3] feat: add ability to use optional kubeconfig flag to all optional commands Signed-off-by: deggja --- backend/cmd/dash.go | 202 ++---------------- backend/cmd/scan.go | 24 ++- backend/pkg/k8s/cilium-scanner.go | 16 +- backend/pkg/k8s/handlers.go | 343 ++++++++++++++++++++++++++++++ backend/pkg/k8s/scanner.go | 129 +++-------- backend/pkg/k8s/visualizer.go | 61 ------ 6 files changed, 408 insertions(+), 367 deletions(-) create mode 100644 backend/pkg/k8s/handlers.go diff --git a/backend/cmd/dash.go b/backend/cmd/dash.go index 7b44176..6b13a01 100644 --- a/backend/cmd/dash.go +++ b/backend/cmd/dash.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "encoding/json" "fmt" "log" "net/http" @@ -23,25 +22,19 @@ var dashCmd = &cobra.Command{ Short: "Launch the Netfetch interactive dashboard", Run: func(cmd *cobra.Command, args []string) { port, _ := cmd.Flags().GetString("port") - startDashboardServer(port) + startDashboardServer(port, kubeconfigPath) }, } -func init() { - rootCmd.AddCommand(dashCmd) - - dashCmd.Flags().StringP("port", "p", "8080", "Port for the interactive dashboard") -} - func setNoCacheHeaders(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } -func startDashboardServer(port string) { +func startDashboardServer(port string, kubeconfigPath string) { // Verify connection to cluster or throw error - clientset, err := k8s.GetClientset() + clientset, err := k8s.GetClientset(kubeconfigPath) if err != nil { log.Fatalf("You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: %v", err) return @@ -72,16 +65,16 @@ func startDashboardServer(port string) { // Set up handlers http.HandleFunc("/", dashboardHandler) - http.HandleFunc("/scan", k8s.HandleScanRequest) - http.HandleFunc("/namespaces", k8s.HandleNamespaceListRequest) - http.HandleFunc("/add-policy", k8s.HandleAddPolicyRequest) - http.HandleFunc("/create-policy", HandleCreatePolicyRequest) - http.HandleFunc("/namespaces-with-policies", handleNamespacesWithPoliciesRequest) - http.HandleFunc("/namespace-policies", handleNamespacePoliciesRequest) - http.HandleFunc("/visualization", k8s.HandleVisualizationRequest) - http.HandleFunc("/visualization/cluster", handleClusterVisualizationRequest) - http.HandleFunc("/policy-yaml", k8s.HandlePolicyYAMLRequest) - http.HandleFunc("/pod-info", handlePodInfoRequest) + http.HandleFunc("/scan", k8s.HandleScanRequest(kubeconfigPath)) + http.HandleFunc("/namespaces", k8s.HandleNamespaceListRequest(kubeconfigPath)) + http.HandleFunc("/add-policy", k8s.HandleAddPolicyRequest(kubeconfigPath)) + http.HandleFunc("/create-policy", k8s.HandleCreatePolicyRequest(kubeconfigPath)) + http.HandleFunc("/namespaces-with-policies", k8s.HandleNamespacesWithPoliciesRequest(kubeconfigPath)) + http.HandleFunc("/namespace-policies", k8s.HandleNamespacePoliciesRequest(kubeconfigPath)) + http.HandleFunc("/visualization", k8s.HandleVisualizationRequest(kubeconfigPath)) + http.HandleFunc("/visualization/cluster", k8s.HandleClusterVisualizationRequest(kubeconfigPath)) + http.HandleFunc("/policy-yaml", k8s.HandlePolicyYAMLRequest(kubeconfigPath)) + http.HandleFunc("/pod-info", k8s.HandlePodInfoRequest(kubeconfigPath)) // Wrap the default serve mux with the CORS middleware handler := c.Handler(http.DefaultServeMux) @@ -125,169 +118,6 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) { http.FileServer(statikFS).ServeHTTP(w, r) } -// handleNamespacesWithPoliciesRequest handles the HTTP request for serving a list of namespaces with network policies. -func handleNamespacesWithPoliciesRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - clientset, err := k8s.GetClientset() - if err != nil { - log.Fatalf("You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: %v", err) - return - } - - namespaces, err := k8s.GatherNamespacesWithPolicies(clientset) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - setNoCacheHeaders(w) - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(struct { - Namespaces []string `json:"namespaces"` - }{Namespaces: namespaces}); err != nil { - http.Error(w, "Failed to encode namespaces data", http.StatusInternalServerError) - } -} - -// handleNamespacePoliciesRequest handles the HTTP request for serving a list of network policies in a namespace. -func handleNamespacePoliciesRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - // Extract the namespace parameter from the query string - namespace := r.URL.Query().Get("namespace") - if namespace == "" { - http.Error(w, "Namespace parameter is required", http.StatusBadRequest) - return - } - - // Obtain the Kubernetes clientset - clientset, err := k8s.GetClientset() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) - return - } - - // Fetch network policies from the specified namespace - policies, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.Background(), metav1.ListOptions{}) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to get network policies: %v", err), http.StatusInternalServerError) - return - } - - // Convert the list of network policies to a more simple structure if needed or encode directly - // For example, you might want to return only the names and some identifiers of the policies - - setNoCacheHeaders(w) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(policies) -} - -// handleClusterVisualizationRequest handles the HTTP request for serving cluster-wide visualization data. -func handleClusterVisualizationRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - clientset, err := k8s.GetClientset() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Call the function to gather cluster-wide visualization data - clusterVizData, err := k8s.GatherClusterVisualizationData(clientset) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - setNoCacheHeaders(w) - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(clusterVizData); err != nil { - http.Error(w, "Failed to encode cluster visualization data", http.StatusInternalServerError) - } -} - -// handlePodInfoRequest handles the HTTP request for serving pod information. -func handlePodInfoRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - // Extract the namespace parameter from the query string - namespace := r.URL.Query().Get("namespace") - if namespace == "" { - http.Error(w, "Namespace parameter is required", http.StatusBadRequest) - return - } - - // Obtain the Kubernetes clientset - clientset, err := k8s.GetClientset() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) - return - } - - // Fetch pod information from the specified namespace - podInfo, err := k8s.GetPodInfo(clientset, namespace) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to get pod information: %v", err), http.StatusInternalServerError) - return - } - - setNoCacheHeaders(w) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(podInfo) -} - -// HandleCreatePolicyRequest handles the HTTP request to create a network policy from YAML. -func HandleCreatePolicyRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - var policyRequest struct { - YAML string `json:"yaml"` - Namespace string `json:"namespace"` - } - if err := json.NewDecoder(r.Body).Decode(&policyRequest); err != nil { - http.Error(w, fmt.Sprintf("Failed to decode request body: %v", err), http.StatusBadRequest) - return - } - defer r.Body.Close() - - clientset, err := k8s.GetClientset() - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) - return - } - - networkPolicy, err := k8s.YAMLToNetworkPolicy(policyRequest.YAML) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to parse network policy YAML: %v", err), http.StatusBadRequest) - return - } - - createdPolicy, err := clientset.NetworkingV1().NetworkPolicies(policyRequest.Namespace).Create(context.Background(), networkPolicy, metav1.CreateOptions{}) - if err != nil { - http.Error(w, fmt.Sprintf("Failed to create network policy: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(createdPolicy) -} - var HeaderStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("6")). @@ -298,3 +128,9 @@ var HeaderStyle = lipgloss.NewStyle(). PaddingRight(4). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("99")) + +func init() { + dashCmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file (optional)") + dashCmd.Flags().StringP("port", "p", "8080", "Port for the interactive dashboard") + rootCmd.AddCommand(dashCmd) +} \ No newline at end of file diff --git a/backend/cmd/scan.go b/backend/cmd/scan.go index 7fc4bd5..e294f44 100644 --- a/backend/cmd/scan.go +++ b/backend/cmd/scan.go @@ -10,11 +10,12 @@ import ( ) var ( - dryRun bool - native bool - cilium bool - verbose bool - targetPolicy string + dryRun bool + native bool + cilium bool + verbose bool + targetPolicy string + kubeconfigPath string ) var scanCmd = &cobra.Command{ @@ -33,12 +34,12 @@ var scanCmd = &cobra.Command{ } // Initialize the Kubernetes clients - clientset, err := k8s.GetClientset() + clientset, err := k8s.GetClientset(kubeconfigPath) if err != nil { fmt.Println("Error creating Kubernetes client:", err) return } - dynamicClient, err := k8s.GetCiliumDynamicClient() + dynamicClient, err := k8s.GetCiliumDynamicClient(kubeconfigPath) if err != nil { fmt.Println("Error creating Kubernetes dynamic client:", err) return @@ -115,7 +116,7 @@ var scanCmd = &cobra.Command{ // 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...") - nativeScanResult, err := k8s.ScanNetworkPolicies(namespace, dryRun, false, true, true, true) + nativeScanResult, err := k8s.ScanNetworkPolicies(namespace, dryRun, false, true, true, true, kubeconfigPath) if err != nil { fmt.Println("Error during Kubernetes native network policies scan:", err) } else { @@ -129,13 +130,13 @@ var scanCmd = &cobra.Command{ // Perform cluster wide Cilium scan first if no namespace is specified if namespace == "" { fmt.Println("Running cluster wide Cilium network policies scan...") - dynamicClient, err := k8s.GetCiliumDynamicClient() + dynamicClient, err := k8s.GetCiliumDynamicClient(kubeconfigPath) if err != nil { fmt.Println("Error obtaining dynamic client:", err) return } - clusterwideScanResult, err := k8s.ScanCiliumClusterwideNetworkPolicies(dynamicClient, false, dryRun, true) + clusterwideScanResult, err := k8s.ScanCiliumClusterwideNetworkPolicies(dynamicClient, false, dryRun, true, kubeconfigPath) if err != nil { fmt.Println("Error during cluster wide Cilium network policies scan:", err) } else { @@ -150,7 +151,7 @@ var scanCmd = &cobra.Command{ // Proceed with normal Cilium network policy scan fmt.Println("Running cilium network policies scan...") - ciliumScanResult, err := k8s.ScanCiliumNetworkPolicies(namespace, dryRun, false, true, true, true) + ciliumScanResult, err := k8s.ScanCiliumNetworkPolicies(namespace, dryRun, false, true, true, true, kubeconfigPath) if err != nil { fmt.Println("Error during Cilium network policies scan:", err) } else { @@ -196,6 +197,7 @@ func createTargetPodsTable(pods [][]string) string { } func init() { + scanCmd.Flags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file (optional)") 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)") diff --git a/backend/pkg/k8s/cilium-scanner.go b/backend/pkg/k8s/cilium-scanner.go index 98358f7..0ad0f40 100644 --- a/backend/pkg/k8s/cilium-scanner.go +++ b/backend/pkg/k8s/cilium-scanner.go @@ -111,7 +111,7 @@ func createPoliciesTable(policiesInfo [][]string) string { } // GetCiliumDynamicClient returns a dynamic interface to query for Cilium policies -func GetCiliumDynamicClient() (dynamic.Interface, error) { +func GetCiliumDynamicClient(kubeconfigPath string) (dynamic.Interface, error) { config, err := rest.InClusterConfig() if err != nil { kubeconfigPath := os.Getenv("KUBECONFIG") @@ -132,8 +132,8 @@ func GetCiliumDynamicClient() (dynamic.Interface, error) { } // initializeCiliumClients creates and returns initialized dynamic and Kubernetes clientsets. -func initializeCiliumClients() (dynamic.Interface, *kubernetes.Clientset, error) { - dynamicClient, err := GetCiliumDynamicClient() +func initializeCiliumClients(kubeconfigPath string) (dynamic.Interface, *kubernetes.Clientset, error) { + dynamicClient, err := GetCiliumDynamicClient(kubeconfigPath) if err != nil { return nil, nil, fmt.Errorf("error creating dynamic Kubernetes client: %s", err) } @@ -141,7 +141,7 @@ func initializeCiliumClients() (dynamic.Interface, *kubernetes.Clientset, error) return nil, nil, fmt.Errorf("failed to create dynamic client: client is nil") } - clientset, err := GetClientset() + clientset, err := GetClientset(kubeconfigPath) if err != nil { return nil, nil, fmt.Errorf("error creating Kubernetes clientset: %s", err) } @@ -309,7 +309,7 @@ var hasStartedCiliumScan bool = false var globallyProtectedPods = make(map[string]struct{}) // ScanCiliumNetworkPolicies scans namespaces for Cilium network policies -func ScanCiliumNetworkPolicies(specificNamespace string, dryRun bool, returnResult bool, isCLI bool, printScore bool, printMessages bool) (*ScanResult, error) { +func ScanCiliumNetworkPolicies(specificNamespace string, dryRun bool, returnResult bool, isCLI bool, printScore bool, printMessages bool, kubeconfigPath string) (*ScanResult, error) { var output bytes.Buffer unprotectedPodsCount := 0 @@ -317,7 +317,7 @@ func ScanCiliumNetworkPolicies(specificNamespace string, dryRun bool, returnResu writer := bufio.NewWriter(&output) - dynamicClient, clientset, err := initializeCiliumClients() + dynamicClient, clientset, err := initializeCiliumClients(kubeconfigPath) if err != nil { fmt.Println(err) return nil, err @@ -532,7 +532,7 @@ func reportPodProtectionStatus(writer *bufio.Writer, unprotectedPods []string) { } // ScanCiliumClusterwideNetworkPolicies scans the cluster for Cilium Clusterwide Network Policies -func ScanCiliumClusterwideNetworkPolicies(dynamicClient dynamic.Interface, printMessages bool, dryRun bool, isCLI bool) (*ScanResult, error) { +func ScanCiliumClusterwideNetworkPolicies(dynamicClient dynamic.Interface, printMessages bool, dryRun bool, isCLI bool, kubeconfigPath string) (*ScanResult, error) { // Buffer and writer setup to capture output for both console and file. var output bytes.Buffer writer := bufio.NewWriter(&output) @@ -543,7 +543,7 @@ func ScanCiliumClusterwideNetworkPolicies(dynamicClient dynamic.Interface, print return nil, fmt.Errorf("failed to create dynamic client: client is nil") } - dynamicClient, clientset, err := initializeCiliumClients() + dynamicClient, clientset, err := initializeCiliumClients(kubeconfigPath) if err != nil { fmt.Println("Error initializing clients:", err) return nil, err diff --git a/backend/pkg/k8s/handlers.go b/backend/pkg/k8s/handlers.go new file mode 100644 index 0000000..8e60bd2 --- /dev/null +++ b/backend/pkg/k8s/handlers.go @@ -0,0 +1,343 @@ +package k8s + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// INTERACTIVE DASHBOARD LOGIC + +func setNoCacheHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") +} + +// HandleScanRequest handles the HTTP request for scanning network policies +func HandleScanRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + namespace := r.URL.Query().Get("namespace") + + // Perform the scan + result, err := ScanNetworkPolicies(namespace, false, true, false, false, false, kubeconfigPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Respond with JSON + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) + } +} + +// HandleNamespaceListRequest lists all non-system Kubernetes namespaces +func HandleNamespaceListRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError) + return + } + + namespaces, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) + if err != nil { + // Handle forbidden access error specifically + if statusErr, isStatus := err.(*k8serrors.StatusError); isStatus { + if statusErr.Status().Code == http.StatusForbidden { + http.Error(w, "Access forbidden: "+err.Error(), http.StatusForbidden) + return + } + } + http.Error(w, "Failed to list namespaces: "+err.Error(), http.StatusInternalServerError) + return + } + + var namespaceList []string + for _, ns := range namespaces.Items { + if !IsSystemNamespace(ns.Name) { + namespaceList = append(namespaceList, ns.Name) + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string][]string{"namespaces": namespaceList}) + } +} + +func HandleAddPolicyRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Define a struct to parse the incoming request + type request struct { + Namespace string `json:"namespace"` + } + + // Parse the incoming JSON request + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Apply the default deny policy + err := createAndApplyDefaultDenyPolicy(req.Namespace, kubeconfigPath) + if err != nil { + http.Error(w, "Failed to apply default deny policy: "+err.Error(), http.StatusInternalServerError) + return + } + + // Respond with success message + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "message": "Implicit default deny all network policy successfully added to namespace " + req.Namespace, + }) + + // Re-scan the namespace + scanResult, err := ScanNetworkPolicies(req.Namespace, false, true, false, false, false, kubeconfigPath) + if err != nil { + http.Error(w, "Error re-scanning after applying policy: "+err.Error(), http.StatusInternalServerError) + return + } + + // Respond with updated scan results + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(scanResult) + } +} + +// HandleNamespacesWithPoliciesRequest handles the HTTP request for serving a list of namespaces with network policies. +func HandleNamespacesWithPoliciesRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, "You are not connected to a Kubernetes cluster. Please connect to a cluster and re-run the command: "+err.Error(), http.StatusInternalServerError) + return + } + + namespaces, err := GatherNamespacesWithPolicies(clientset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + setNoCacheHeaders(w) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(struct { + Namespaces []string `json:"namespaces"` + }{Namespaces: namespaces}); err != nil { + http.Error(w, "Failed to encode namespaces data", http.StatusInternalServerError) + } + } +} + +// HandleNamespacePoliciesRequest handles the HTTP request for serving a list of network policies in a namespace. +func HandleNamespacePoliciesRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // Extract the namespace parameter from the query string + namespace := r.URL.Query().Get("namespace") + if namespace == "" { + http.Error(w, "Namespace parameter is required", http.StatusBadRequest) + return + } + + // Obtain the Kubernetes clientset + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) + return + } + + // Fetch network policies from the specified namespace + policies, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.Background(), metav1.ListOptions{}) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to get network policies: %v", err), http.StatusInternalServerError) + return + } + + setNoCacheHeaders(w) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(policies) + } +} + +// HandleClusterVisualizationRequest handles the HTTP request for serving cluster-wide visualization data. +func HandleClusterVisualizationRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Call the function to gather cluster-wide visualization data + clusterVizData, err := GatherClusterVisualizationData(clientset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + setNoCacheHeaders(w) + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(clusterVizData); err != nil { + http.Error(w, "Failed to encode cluster visualization data", http.StatusInternalServerError) + } + } +} + +// HandlePodInfoRequest handles the HTTP request for serving pod information. +func HandlePodInfoRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // Extract the namespace parameter from the query string + namespace := r.URL.Query().Get("namespace") + if namespace == "" { + http.Error(w, "Namespace parameter is required", http.StatusBadRequest) + return + } + + // Obtain the Kubernetes clientset + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) + return + } + + // Fetch pod information from the specified namespace + podInfo, err := GetPodInfo(clientset, namespace) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to get pod information: %v", err), http.StatusInternalServerError) + return + } + + setNoCacheHeaders(w) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(podInfo) + } +} + +// HandleCreatePolicyRequest handles the HTTP request to create a network policy from YAML. +func HandleCreatePolicyRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + var policyRequest struct { + YAML string `json:"yaml"` + Namespace string `json:"namespace"` + } + if err := json.NewDecoder(r.Body).Decode(&policyRequest); err != nil { + http.Error(w, fmt.Sprintf("Failed to decode request body: %v", err), http.StatusBadRequest) + return + } + defer r.Body.Close() + + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create Kubernetes client: %v", err), http.StatusInternalServerError) + return + } + + networkPolicy, err := YAMLToNetworkPolicy(policyRequest.YAML) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to parse network policy YAML: %v", err), http.StatusBadRequest) + return + } + + createdPolicy, err := clientset.NetworkingV1().NetworkPolicies(policyRequest.Namespace).Create(context.Background(), networkPolicy, metav1.CreateOptions{}) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to create network policy: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(createdPolicy) + } +} + +func HandleVisualizationRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + namespace := r.URL.Query().Get("namespace") + + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError) + return + } + + vizData, err := gatherVisualizationData(clientset, namespace) + if err != nil { + http.Error(w, "Failed to gather visualization data: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(vizData); err != nil { + http.Error(w, "Failed to encode visualization data: "+err.Error(), http.StatusInternalServerError) + } + } +} + +// HandlePolicyYAMLRequest handles the HTTP request for serving the YAML of a network policy. +func HandlePolicyYAMLRequest(kubeconfigPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + + // Extract the policy name and namespace from query parameters + policyName := r.URL.Query().Get("name") + namespace := r.URL.Query().Get("namespace") + if policyName == "" || namespace == "" { + http.Error(w, "Policy name or namespace not provided", http.StatusBadRequest) + return + } + + // Retrieve the network policy YAML + clientset, err := GetClientset(kubeconfigPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + yamlData, err := getNetworkPolicyYAML(clientset, namespace, policyName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/x-yaml") + w.Write([]byte(yamlData)) + } +} diff --git a/backend/pkg/k8s/scanner.go b/backend/pkg/k8s/scanner.go index ca697cd..7ba35c9 100644 --- a/backend/pkg/k8s/scanner.go +++ b/backend/pkg/k8s/scanner.go @@ -4,11 +4,9 @@ import ( "bufio" "bytes" "context" - "encoding/json" "errors" "fmt" "net" - "net/http" "net/url" "os" "path/filepath" @@ -76,8 +74,8 @@ func isNetworkError(err error) bool { } // Initialize client -func InitializeClient() (*kubernetes.Clientset, error) { - clientset, err := GetClientset() +func InitializeClient(kubeconfigPath string) (*kubernetes.Clientset, error) { + clientset, err := GetClientset(kubeconfigPath) if err != nil { fmt.Printf("Error creating Kubernetes client: %s\n", err) return nil, err @@ -196,7 +194,7 @@ func displayUnprotectedPods(nsName string, unprotectedPods []string, writer *buf } } -func handleCLIInteractions(nsName string, unprotectedPods []string, writer *bufio.Writer, scanResult *ScanResult) { +func handleCLIInteractions(nsName string, unprotectedPods []string, writer *bufio.Writer, scanResult *ScanResult, kubeconfigPath string) { if len(unprotectedPods) > 0 { // Header headerText := fmt.Sprintf("Unprotected pods found in namespace %s:", nsName) @@ -215,7 +213,7 @@ func handleCLIInteractions(nsName string, unprotectedPods []string, writer *bufi // Prompt for applying policies if promptForPolicyApplication(nsName, writer) { - err := createAndApplyDefaultDenyPolicy(nsName) + err := createAndApplyDefaultDenyPolicy(nsName, kubeconfigPath) if err != nil { fmt.Fprintf(writer, "Failed to apply default deny policy in namespace %s: %s\n", nsName, err) } else { @@ -226,7 +224,7 @@ func handleCLIInteractions(nsName string, unprotectedPods []string, writer *bufi } } -func processNamespacePolicies(clientset *kubernetes.Clientset, nsName string, writer *bufio.Writer, isCLI bool, dryRun bool, scanResult *ScanResult) error { +func processNamespacePolicies(clientset *kubernetes.Clientset, nsName string, writer *bufio.Writer, isCLI bool, dryRun bool, scanResult *ScanResult, kubeconfigPath string) error { // Fetch covered pods coveredPods, err := fetchCoveredPods(clientset, nsName, writer) if err != nil { @@ -245,7 +243,7 @@ func processNamespacePolicies(clientset *kubernetes.Clientset, nsName string, wr // Only handle CLI interactions if it's CLI mode and not a dry run if isCLI && !dryRun { - handleCLIInteractions(nsName, unprotectedPods, writer, scanResult) + handleCLIInteractions(nsName, unprotectedPods, writer, scanResult, kubeconfigPath) } else if dryRun { // If it's a dry run, we just display the data without prompting for any actions displayUnprotectedPods(nsName, unprotectedPods, writer) @@ -257,7 +255,7 @@ func processNamespacePolicies(clientset *kubernetes.Clientset, nsName string, wr var hasStartedNativeScan bool = false // ScanNetworkPolicies scans namespaces for network policies -func ScanNetworkPolicies(specificNamespace string, dryRun bool, returnResult bool, isCLI bool, printScore bool, printMessages bool) (*ScanResult, error) { +func ScanNetworkPolicies(specificNamespace string, dryRun bool, returnResult bool, isCLI bool, printScore bool, printMessages bool, kubeconfigPath string) (*ScanResult, error) { var output bytes.Buffer var namespacesToScan []string @@ -266,7 +264,7 @@ func ScanNetworkPolicies(specificNamespace string, dryRun bool, returnResult boo writer := bufio.NewWriter(&output) - clientset, err := InitializeClient() + clientset, err := InitializeClient(kubeconfigPath) if err != nil { return nil, err } @@ -286,7 +284,7 @@ func ScanNetworkPolicies(specificNamespace string, dryRun bool, returnResult boo } for _, nsName := range namespacesToScan { - err := processNamespacePolicies(clientset, nsName, writer, isCLI, dryRun, scanResult) + err := processNamespacePolicies(clientset, nsName, writer, isCLI, dryRun, scanResult, kubeconfigPath) if err != nil { fmt.Printf("Error processing namespace %s: %v\n", nsName, err) continue @@ -342,9 +340,9 @@ func handleOutputAndPrompts(writer *bufio.Writer, output *bytes.Buffer) { } // Function to create the implicit default deny if missing -func createAndApplyDefaultDenyPolicy(namespace string) error { +func createAndApplyDefaultDenyPolicy(namespace string, kubeconfigPath string) error { // Initialize Kubernetes client - clientset, err := GetClientset() + clientset, err := GetClientset(kubeconfigPath) if err != nil { return fmt.Errorf("failed to create Kubernetes client: %v", err) } @@ -417,64 +415,13 @@ func CalculateScore(hasPolicies bool, hasDenyAll bool, unprotectedPodsCount int) return score } -// INTERACTIVE DASHBOARD LOGIC - -// handleScanRequest handles the HTTP request for scanning network policies -func HandleScanRequest(w http.ResponseWriter, r *http.Request) { - // Extract parameters from request, e.g., namespace - namespace := r.URL.Query().Get("namespace") - - // Perform the scan - result, err := ScanNetworkPolicies(namespace, false, true, false, false, false) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Respond with JSON - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(result) -} - -// HandleNamespaceListRequest lists all non-system Kubernetes namespaces -func HandleNamespaceListRequest(w http.ResponseWriter, r *http.Request) { - clientset, err := GetClientset() - if err != nil { - http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError) - return - } - - namespaces, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) - if err != nil { - // Handle forbidden access error specifically - if statusErr, isStatus := err.(*k8serrors.StatusError); isStatus { - if statusErr.Status().Code == http.StatusForbidden { - http.Error(w, "Access forbidden: "+err.Error(), http.StatusForbidden) - return - } - } - http.Error(w, "Failed to list namespaces: "+err.Error(), http.StatusInternalServerError) - return - } - - var namespaceList []string - for _, ns := range namespaces.Items { - if !IsSystemNamespace(ns.Name) { - namespaceList = append(namespaceList, ns.Name) - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string][]string{"namespaces": namespaceList}) -} - var ( isClientInitialized = false clientset *kubernetes.Clientset ) // GetClientset creates a new Kubernetes clientset -func GetClientset() (*kubernetes.Clientset, error) { +func GetClientset(kubeconfigPath string) (*kubernetes.Clientset, error) { if isClientInitialized { return clientset, nil } @@ -482,9 +429,19 @@ func GetClientset() (*kubernetes.Clientset, error) { var config *rest.Config var err error + if kubeconfigPath != "" { + // Use the provided kubeconfig + fmt.Println("Using provided kubeconfig path: ", kubeconfigPath) + config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfigPath, err) + } + } else { // First try to use the in-cluster configuration config, err = rest.InClusterConfig() - if err != nil { + if err == nil { + fmt.Println("Using in-cluster Kubernetes configuration") + } else { fmt.Println("Mode: CLI") // Fallback to kubeconfig @@ -501,9 +458,8 @@ func GetClientset() (*kubernetes.Clientset, error) { config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfig, err) - } - } else { - fmt.Println("Using in-cluster Kubernetes configuration") + } + } } // Create and store the clientset @@ -516,41 +472,6 @@ func GetClientset() (*kubernetes.Clientset, error) { return clientset, nil } -func HandleAddPolicyRequest(w http.ResponseWriter, r *http.Request) { - // Define a struct to parse the incoming request - type request struct { - Namespace string `json:"namespace"` - } - - // Parse the incoming JSON request - var req request - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Apply the default deny policy - err := createAndApplyDefaultDenyPolicy(req.Namespace) - if err != nil { - http.Error(w, "Failed to apply default deny policy: "+err.Error(), http.StatusInternalServerError) - return - } - - // Respond with success message - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"message": "Implicit default deny all network policy successfully added to namespace " + req.Namespace}) - - scanResult, err := ScanNetworkPolicies(req.Namespace, false, true, false, false, false) - if err != nil { - http.Error(w, "Error re-scanning after applying policy: "+err.Error(), http.StatusInternalServerError) - return - } - - // Respond with updated scan results - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(scanResult) -} - // contains checks if a string is present in a slice func contains(slice []string, str string) bool { for _, v := range slice { diff --git a/backend/pkg/k8s/visualizer.go b/backend/pkg/k8s/visualizer.go index 528a1b2..e1f86da 100644 --- a/backend/pkg/k8s/visualizer.go +++ b/backend/pkg/k8s/visualizer.go @@ -2,9 +2,7 @@ package k8s import ( "context" - "encoding/json" "log" - "net/http" "gopkg.in/yaml.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -69,33 +67,6 @@ func gatherVisualizationData(clientset kubernetes.Interface, namespace string) ( return vizData, nil } -// HandleVisualizationRequest handles the HTTP request for serving visualization data. -func HandleVisualizationRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - namespace := r.URL.Query().Get("namespace") - - clientset, err := GetClientset() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - vizData, err := gatherVisualizationData(clientset, namespace) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(vizData); err != nil { - http.Error(w, "Failed to encode visualization data", http.StatusInternalServerError) - } -} - // gatherNamespacesWithPolicies returns a list of all namespaces that contain network policies. func GatherNamespacesWithPolicies(clientset kubernetes.Interface) ([]string, error) { // Retrieve all namespaces @@ -144,38 +115,6 @@ func GatherClusterVisualizationData(clientset kubernetes.Interface) ([]Visualiza return clusterVizData, nil } -// HandlePolicyYAMLRequest handles the HTTP request for serving the YAML of a network policy. -func HandlePolicyYAMLRequest(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) - return - } - - // Extract the policy name and namespace from query parameters - policyName := r.URL.Query().Get("name") - namespace := r.URL.Query().Get("namespace") - if policyName == "" || namespace == "" { - http.Error(w, "Policy name or namespace not provided", http.StatusBadRequest) - return - } - - // Retrieve the network policy YAML - clientset, err := GetClientset() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - yaml, err := getNetworkPolicyYAML(clientset, namespace, policyName) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/x-yaml") - w.Write([]byte(yaml)) -} - // getNetworkPolicyYAML retrieves the YAML representation of a network policy, excluding annotations. func getNetworkPolicyYAML(clientset kubernetes.Interface, namespace string, policyName string) (string, error) { // Get the specified network policy From e61121fbdd68bd221f26bc55831078a082634d2f Mon Sep 17 00:00:00 2001 From: deggja Date: Fri, 15 Nov 2024 17:44:18 +0100 Subject: [PATCH 2/3] feat: format output for when kubeconfig is provided Signed-off-by: deggja --- backend/pkg/k8s/scanner.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/pkg/k8s/scanner.go b/backend/pkg/k8s/scanner.go index 7ba35c9..79aba91 100644 --- a/backend/pkg/k8s/scanner.go +++ b/backend/pkg/k8s/scanner.go @@ -431,7 +431,9 @@ func GetClientset(kubeconfigPath string) (*kubernetes.Clientset, error) { if kubeconfigPath != "" { // Use the provided kubeconfig + fmt.Println("Mode: CLI") fmt.Println("Using provided kubeconfig path: ", kubeconfigPath) + fmt.Printf("\n") config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) if err != nil { return nil, fmt.Errorf("failed to build config from kubeconfig path %s: %v", kubeconfigPath, err) From 60100531659a60754d3afe72b883414455718a55 Mon Sep 17 00:00:00 2001 From: deggja Date: Fri, 15 Nov 2024 17:48:33 +0100 Subject: [PATCH 3/3] docs: updated docs for --kubeconfig flag and updated score section Signed-off-by: deggja --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a306fed..3b32902 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,12 @@ Run `netfetch` in dryrun against a cluster. netfetch scan --dryrun ``` +You can also specify the desired kubeconfig file by using the `--kubeconfig /path/to/config` flag. + +```sh +netfetch scan --kubeconfig /Users/xxx/.kube/config +``` + Run `netfetch` in dryrun against a namespace ```sh @@ -188,7 +194,7 @@ The Netfetch Dashboard offers an intuitive interface for interacting with your K ### Netfetch score 🥇 -The `netfetch` tool provides a basic score at the end of each scan. The score ranges from 1 to 42, with 1 being the lowest and 42 being the highest possible score. +The `netfetch` tool provides a basic score at the end of each scan. The score ranges from 1 to 100, with 1 being the lowest and 100 being the highest possible score. Your score will decrease based on the amount of workloads in your cluster that are running without being targeted by a network policy.