Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add network maps to dashboard #13

Merged
merged 12 commits into from
Dec 7, 2023
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Using Netfetch

This project aims to simplify the mapping of network policies in a Kubernetes cluster. It's a work in progress!

The `netfetch` tool is designed to scan Kubernetes namespaces for network policies, checking whether implicit default deny policies are in place and examining if there are any other policies targeting the pods.

This document guides you on how to use `netfetch` to perform these scans.
Expand Down
64 changes: 63 additions & 1 deletion backend/cmd/dash.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package cmd

import (
"encoding/json"
"fmt"
"log"
"net/http"

_ "github.com/deggja/netfetch/backend/statik"
"github.com/rakyll/statik/fs"

"github.com/deggja/netfetch/backend/pkg/k8s"
"github.com/rakyll/statik/fs"
"github.com/rs/cors"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -38,6 +39,9 @@ func startDashboardServer() {
http.HandleFunc("/scan", k8s.HandleScanRequest)
http.HandleFunc("/namespaces", k8s.HandleNamespaceListRequest)
http.HandleFunc("/add-policy", k8s.HandleAddPolicyRequest)
http.HandleFunc("/visualization", k8s.HandleVisualizationRequest)
http.HandleFunc("/visualization/cluster", handleClusterVisualizationRequest)
http.HandleFunc("/namespaces-with-policies", handleNamespacesWithPoliciesRequest)

// Wrap the default serve mux with the CORS middleware
handler := c.Handler(http.DefaultServeMux)
Expand All @@ -50,6 +54,23 @@ func startDashboardServer() {
}
}

// func dashboardHandler(w http.ResponseWriter, r *http.Request) {
// // Check if we are in development mode
// isDevelopment := true // You can use an environment variable or a config flag to set this
// if isDevelopment {
// // Redirect to the Vue dev server
// vueDevServer := "http://localhost:8081"
// http.Redirect(w, r, vueDevServer+r.RequestURI, http.StatusTemporaryRedirect)
// } else {
// // Serve the embedded frontend using statik
// statikFS, err := fs.New()
// if err != nil {
// log.Fatal(err)
// }
// http.FileServer(statikFS).ServeHTTP(w, r)
// }
// }

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
statikFS, err := fs.New()
if err != nil {
Expand All @@ -59,3 +80,44 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
// Serve the embedded frontend
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
}

namespaces, err := k8s.GatherNamespacesWithPolicies()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

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)
}
}

// 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
}

// Call the function to gather cluster-wide visualization data
clusterVizData, err := k8s.GatherClusterVisualizationData()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

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)
}
}
Binary file modified backend/netfetch
Binary file not shown.
4 changes: 2 additions & 2 deletions backend/pkg/k8s/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ func HandleScanRequest(w http.ResponseWriter, r *http.Request) {

// HandleNamespaceListRequest lists all non-system Kubernetes namespaces
func HandleNamespaceListRequest(w http.ResponseWriter, r *http.Request) {
clientset, err := getClientset()
clientset, err := GetClientset()
if err != nil {
http.Error(w, "Failed to create Kubernetes client: "+err.Error(), http.StatusInternalServerError)
return
Expand Down Expand Up @@ -367,7 +367,7 @@ func HandleNamespaceListRequest(w http.ResponseWriter, r *http.Request) {
}

// getClientset creates a new Kubernetes clientset
func getClientset() (*kubernetes.Clientset, error) {
func GetClientset() (*kubernetes.Clientset, error) {
kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
Expand Down
145 changes: 145 additions & 0 deletions backend/pkg/k8s/visualizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package k8s

import (
"context"
"encoding/json"
"log"
"net/http"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// VisualizationData represents the structure of network policy and pod data for visualization.
type VisualizationData struct {
Policies []PolicyVisualization `json:"policies"`
}

// PolicyVisualization represents a network policy and the pods it affects for visualization purposes.
type PolicyVisualization struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
TargetPods []string `json:"targetPods"`
}

// gatherVisualizationData retrieves network policies and associated pods for visualization.
func gatherVisualizationData(namespace string) (*VisualizationData, error) {
clientset, err := GetClientset()
if err != nil {
return nil, err
}

// Retrieve all network policies in the specified namespace
policies, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}

vizData := &VisualizationData{
Policies: make([]PolicyVisualization, 0), // Initialize as empty slice
}

// Iterate over the retrieved policies to build the visualization data
for _, policy := range policies.Items {
// For each policy, find the pods that match its pod selector
selector, err := metav1.LabelSelectorAsSelector(&policy.Spec.PodSelector)
if err != nil {
log.Printf("Error parsing selector for policy %s: %v\n", policy.Name, err)
continue
}

pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: selector.String(),
})
if err != nil {
log.Printf("Error listing pods for policy %s: %v\n", policy.Name, err)
continue
}

podNames := make([]string, 0, len(pods.Items))
for _, pod := range pods.Items {
podNames = append(podNames, pod.Name)
}

vizData.Policies = append(vizData.Policies, PolicyVisualization{
Name: policy.Name,
Namespace: policy.Namespace,
TargetPods: podNames,
})
}

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")

vizData, err := gatherVisualizationData(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() ([]string, error) {
clientset, err := GetClientset()
if err != nil {
return nil, err
}

// Retrieve all namespaces
namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}

var namespacesWithPolicies []string

// Check each namespace for network policies
for _, ns := range namespaces.Items {
policies, err := clientset.NetworkingV1().NetworkPolicies(ns.Name).List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Printf("Error listing policies in namespace %s: %v\n", ns.Name, err)
continue
}

if len(policies.Items) > 0 {
namespacesWithPolicies = append(namespacesWithPolicies, ns.Name)
}
}

return namespacesWithPolicies, nil
}

// gatherClusterVisualizationData retrieves visualization data for all namespaces with network policies.
func GatherClusterVisualizationData() ([]VisualizationData, error) {
namespacesWithPolicies, err := GatherNamespacesWithPolicies()
if err != nil {
return nil, err
}

// Slice to hold the visualization data for the entire cluster
var clusterVizData []VisualizationData

for _, namespace := range namespacesWithPolicies {
vizData, err := gatherVisualizationData(namespace)
if err != nil {
log.Printf("Error gathering visualization data for namespace %s: %v\n", namespace, err)
continue
}
clusterVizData = append(clusterVizData, *vizData)
}

return clusterVizData, nil
}
2 changes: 1 addition & 1 deletion backend/statik/statik.go

Large diffs are not rendered by default.

Loading