diff --git a/cmd/probe.go b/cmd/probe.go index 1278ee87..e1a4aa18 100644 --- a/cmd/probe.go +++ b/cmd/probe.go @@ -35,4 +35,5 @@ func init() { probeCmd.Flags().StringVarP(&probeInstallOptions.Namespace, "namespace", "n", "kube-system", "Namespace for resources") probeCmd.Flags().BoolVar(&probeInstallOptions.Full, "full", false, `If KubeArmor is not running, it deploys a daemonset to have access to more information on KubeArmor support in the environment and deletes daemonset after probing`) + probeCmd.Flags().StringVarP(&probeInstallOptions.Output, "format", "f", "text", " Format: json or text ") } diff --git a/probe/probe.go b/probe/probe.go index a90037e5..897a7ed9 100644 --- a/probe/probe.go +++ b/probe/probe.go @@ -7,6 +7,7 @@ package probe import ( "bytes" "context" + "encoding/json" "fmt" "io" "log" @@ -47,12 +48,7 @@ var itwhite = color.New(color.Italic).Add(color.Italic).SprintFunc() type Options struct { Namespace string Full bool -} - -// To store container default posture and visibility data -type PostureAndVisibility struct { - DefaultPosture string - Visibility string + Output string } // K8sInstaller for karmor install @@ -87,23 +83,40 @@ func PrintProbeResult(c *k8s.Client, o Options) error { } } if isSystemdMode() { - err := probeSystemdMode() + err := probeSystemdMode(o) if err != nil { return err } return nil } - if isKubeArmorRunning(c, o) { - getKubeArmorDeployments(c, o) - getKubeArmorContainers(c, o) - postureData, err := probeRunningKubeArmorNodes(c, o) + isRunning, daemonsetStatus := isKubeArmorRunning(c, o) + if isRunning { + deploymentData := getKubeArmorDeployments(c, o) + containerData := getKubeArmorContainers(c, o) + nodeData, postureData, err := probeRunningKubeArmorNodes(c, o) if err != nil { log.Println("error occured when probing kubearmor nodes", err) } - err = getAnnotatedPods(c, postureData) + armoredPodData, err := getAnnotatedPods(c, o, postureData) if err != nil { log.Println("error occured when getting annotated pods", err) } + + if o.Output == "json" { + ProbeData := map[string]interface{}{"Probe Data": map[string]interface{}{ + "DaemonsetStatus": daemonsetStatus, + "Deployments": deploymentData, + "Containers": containerData, + "Nodes": nodeData, + "ArmoredPods": armoredPodData, + }, + } + out, err := json.Marshal(ProbeData) + if err != nil { + return err + } + fmt.Println(string(out)) + } return nil } @@ -232,7 +245,7 @@ func checkKernelHeaderPresent() bool { return false } -func execIntoPod(c *k8s.Client, podname, namespace, containername string, cmd string) (string, error) { +func execIntoPod(c *k8s.Client, podname, namespace, cmd string) (string, error) { buf := &bytes.Buffer{} errBuf := &bytes.Buffer{} request := c.K8sClientset.CoreV1().RESTClient(). @@ -261,8 +274,8 @@ func execIntoPod(c *k8s.Client, podname, namespace, containername string, cmd st return buf.String(), nil } -func findFileInDir(c *k8s.Client, podname, namespace, containername string, cmd string) bool { - s, err := execIntoPod(c, podname, namespace, containername, cmd) +func findFileInDir(c *k8s.Client, podname, namespace, cmd string) bool { + s, err := execIntoPod(c, podname, namespace, cmd) if err != nil { return false } @@ -282,10 +295,12 @@ func checkNodeKernelHeaderPresent(c *k8s.Client, o Options, nodeName string, ker if err != nil { return false } - - if findFileInDir(c, pods.Items[0].Name, o.Namespace, pods.Items[0].Spec.Containers[0].Name, "find /sys/kernel/btf/ -name vmlinux") || - findFileInDir(c, pods.Items[0].Name, o.Namespace, pods.Items[0].Spec.Containers[0].Name, "find -L /usr/src -maxdepth 2 -path \"*$(uname -r)*\" -name \"Kconfig\"") || - findFileInDir(c, pods.Items[0].Name, o.Namespace, pods.Items[0].Spec.Containers[0].Name, "find -L /lib/modules/ -maxdepth 3 -path \"*$(uname -r)*\" -name \"Kconfig\"") { + cmd1 := "find /sys/kernel/btf/ -name vmlinux" + cmd2 := "find -L /usr/src -maxdepth 2 -path \"*$(uname -r)*\" -name \"Kconfig\"" + cmd3 := "find -L /lib/modules/ -maxdepth 3 -path \"*$(uname -r)*\" -name \"Kconfig\"" + if findFileInDir(c, pods.Items[0].Name, o.Namespace, cmd1) || + findFileInDir(c, pods.Items[0].Name, o.Namespace, cmd2) || + findFileInDir(c, pods.Items[0].Name, o.Namespace, cmd3) { return true } @@ -321,7 +336,7 @@ func getNodeLsmSupport(c *k8s.Client, o Options, nodeName string) (string, error return "none", err } - s, err := execIntoPod(c, pods.Items[0].Name, o.Namespace, pods.Items[0].Spec.Containers[0].Name, "cat "+srcPath) + s, err := execIntoPod(c, pods.Items[0].Name, o.Namespace, "cat "+srcPath) if err != nil { return "none", err } @@ -366,111 +381,189 @@ type KubeArmorProbeData struct { HostVisibility string } -func isKubeArmorRunning(c *k8s.Client, o Options) bool { - _, err := getKubeArmorDaemonset(c, o) - return err == nil +func isKubeArmorRunning(c *k8s.Client, o Options) (bool, *Status) { + isRunning, DaemonsetStatus := getKubeArmorDaemonset(c, o) + return isRunning, DaemonsetStatus + +} +// Status data +type Status struct { + Desired string `json:"desired"` + Ready string `json:"ready"` + Available string `json:"available"` } -func getKubeArmorDaemonset(c *k8s.Client, o Options) (bool, error) { +func getKubeArmorDaemonset(c *k8s.Client, o Options) (bool, *Status) { var data [][]string // KubeArmor DaemonSet w, err := c.K8sClientset.AppsV1().DaemonSets(o.Namespace).Get(context.Background(), "kubearmor", metav1.GetOptions{}) if err != nil { - return false, err + log.Println("error when getting kubearmor daemonset", err) + return false, nil } - color.Green("\nFound KubeArmor running in Kubernetes\n\n") - _, err = boldWhite.Printf("Daemonset :\n") - if err != nil { - color.Red(" Error while printing") + if o.Output == "text" { + color.Green("\nFound KubeArmor running in Kubernetes\n\n") + _, err = boldWhite.Printf("Daemonset :\n") + if err != nil { + color.Red(" Error while printing") + } } + desired, ready, available := w.Status.DesiredNumberScheduled, w.Status.NumberReady, w.Status.NumberAvailable if desired != ready && desired != available { + data = append(data, []string{" ", "kubearmor ", "Desired: " + strconv.Itoa(int(desired)), "Ready: " + strconv.Itoa(int(ready)), "Available: " + strconv.Itoa(int(available))}) return false, nil } + if o.Output == "json" { + DaemonSetStatus := Status{ + Desired: strconv.Itoa(int(desired)), + Ready: strconv.Itoa(int(ready)), + Available: strconv.Itoa(int(available)), + } + return true, &DaemonSetStatus + } data = append(data, []string{" ", "kubearmor ", "Desired: " + strconv.Itoa(int(desired)), "Ready: " + strconv.Itoa(int(ready)), "Available: " + strconv.Itoa(int(available))}) renderOutputInTableWithNoBorders(data) return true, nil } -func getKubeArmorDeployments(c *k8s.Client, o Options) { - var data [][]string - _, err := boldWhite.Printf("Deployments : \n") - if err != nil { - color.Red(" Error while printing") + +func getKubeArmorDeployments(c *k8s.Client, o Options) map[string]*Status { + + if o.Output == "text" { + _, err := boldWhite.Printf("Deployments : \n") + if err != nil { + color.Red(" Error while printing") + } } kubearmorDeployments, err := c.K8sClientset.AppsV1().Deployments(o.Namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: "kubearmor-app", }) - if err != nil { - return + log.Println("error while getting kubearmor deployments", err) + return nil } + if len(kubearmorDeployments.Items) > 0 { + + DeploymentsData := make(map[string]*Status) + var data [][]string for _, kubearmorDeploymentItem := range kubearmorDeployments.Items { desired, ready, available := kubearmorDeploymentItem.Status.UpdatedReplicas, kubearmorDeploymentItem.Status.ReadyReplicas, kubearmorDeploymentItem.Status.AvailableReplicas - if desired != ready && desired != available { - continue - } else { - data = append(data, []string{" ", kubearmorDeploymentItem.Name, "Desired: " + strconv.Itoa(int(desired)), "Ready: " + strconv.Itoa(int(ready)), "Available: " + strconv.Itoa(int(available))}) + if desired == ready && desired == available { + if o.Output == "json" { + DeploymentsData[kubearmorDeploymentItem.Name] = &Status{ + Desired: strconv.Itoa(int(desired)), + Ready: strconv.Itoa(int(ready)), + Available: strconv.Itoa(int(available)), + } + } else { + data = append(data, []string{" ", kubearmorDeploymentItem.Name, "Desired: " + strconv.Itoa(int(desired)), "Ready: " + strconv.Itoa(int(ready)), "Available: " + strconv.Itoa(int(available))}) + } } } + if o.Output == "json" { + return DeploymentsData + } + + renderOutputInTableWithNoBorders(data) } - renderOutputInTableWithNoBorders(data) + return nil } -func getKubeArmorContainers(c *k8s.Client, o Options) { +// KubeArmorPodSpec structure definition +type KubeArmorPodSpec struct { + Running string `json:"running"` + Image_Version string `json:"image_version"` +} + +func getKubeArmorContainers(c *k8s.Client, o Options) map[string]*KubeArmorPodSpec { var data [][]string - _, err := boldWhite.Printf("Containers : \n") - if err != nil { - color.Red(" Error while printing") + + if o.Output == "text" { + _, err := boldWhite.Printf("Containers : \n") + if err != nil { + color.Red(" Error while printing") + } } kubearmorPods, err := c.K8sClientset.CoreV1().Pods(o.Namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: "kubearmor-app", }) + if err != nil { log.Println("error occured when getting kubearmor pods", err) - return + return nil } - + KAContainerData := make(map[string]*KubeArmorPodSpec) if len(kubearmorPods.Items) > 0 { for _, kubearmorPodItem := range kubearmorPods.Items { - data = append(data, []string{" ", kubearmorPodItem.Name, "Running: " + strconv.Itoa(len(kubearmorPodItem.Spec.Containers)), "Image Version: " + kubearmorPodItem.Spec.Containers[0].Image}) + if o.Output == "json" { + KAContainerData[kubearmorPodItem.Name] = &KubeArmorPodSpec{ + Running: strconv.Itoa(len(kubearmorPodItem.Spec.Containers)), + Image_Version: kubearmorPodItem.Spec.Containers[0].Image, + } + } else { + data = append(data, []string{" ", kubearmorPodItem.Name, "Running: " + strconv.Itoa(len(kubearmorPodItem.Spec.Containers)), "Image Version: " + kubearmorPodItem.Spec.Containers[0].Image}) + } } - } - renderOutputInTableWithNoBorders(data) + if o.Output == "json" { + return KAContainerData + } + + renderOutputInTableWithNoBorders(data) + } + return nil } -func probeRunningKubeArmorNodes(c *k8s.Client, o Options) (map[string]string, error) { +func probeRunningKubeArmorNodes(c *k8s.Client, o Options) (map[string]*KubeArmorProbeData, map[string]string, error) { // // KubeArmor Nodes postureData := make(map[string]string) nodes, err := c.K8sClientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) if err != nil { log.Println("error getting nodes", err) - return nil, err + return nil, nil, err } + + NodeData := make(map[string]*KubeArmorProbeData) + if len(nodes.Items) > 0 { for i, item := range nodes.Items { - _, err := boldWhite.Printf("Node %d : \n", i+1) - if err != nil { - color.Red(" Error while printing") + if o.Output == "text" { + _, err := boldWhite.Printf("Node %d : \n", i+1) + if err != nil { + color.Red(" Error while printing") + } } - err = readDataFromKubeArmor(c, o, item.Name, postureData) + kd, err := readDataFromKubeArmor(c, o, item.Name) if err != nil { - return nil, err + return nil, nil, err + } + if o.Output != "json" { + err = printKubeArmorProbeOutput(kd) + if err != nil { + log.Println("an error occured when printing output", err) + return nil, nil, err + } + } else { + NodeData["Node"+strconv.Itoa(i+1)] = kd } + postureData["filePosture"] = kd.ContainerDefaultPosture.FileAction + postureData["capabilitiesPosture"] = kd.ContainerDefaultPosture.CapabilitiesAction + postureData["networkPosture"] = kd.ContainerDefaultPosture.NetworkAction + } } else { fmt.Println("No kubernetes environment found") } - return postureData, nil + return NodeData, postureData, nil } -func readDataFromKubeArmor(c *k8s.Client, o Options, nodeName string, postureData map[string]string) error { +func readDataFromKubeArmor(c *k8s.Client, o Options, nodeName string) (*KubeArmorProbeData, error) { srcPath := "/tmp/karmorProbeData.cfg" pods, err := c.K8sClientset.CoreV1().Pods(o.Namespace).List(context.Background(), metav1.ListOptions{ LabelSelector: "kubearmor-app=kubearmor", @@ -478,7 +571,7 @@ func readDataFromKubeArmor(c *k8s.Client, o Options, nodeName string, postureDat }) if err != nil { log.Println("error occured while getting kubeArmor pods", err) - return err + return nil, err } reader, outStream := io.Pipe() cmdArr := []string{"cat", srcPath} @@ -499,7 +592,7 @@ func readDataFromKubeArmor(c *k8s.Client, o Options, nodeName string, postureDat exec, err := remotecommand.NewSPDYExecutor(c.Config, "POST", req.URL()) if err != nil { - return err + return nil, err } go func() { defer outStream.Close() @@ -513,25 +606,15 @@ func readDataFromKubeArmor(c *k8s.Client, o Options, nodeName string, postureDat buf, err := io.ReadAll(reader) if err != nil { log.Println("an error occured when reading file", err) - return err + return nil, err } kd, err := parseJsonData(buf) if err != nil { - return err - } - - err = printKubeArmorProbeOutput(kd) - if err != nil { - log.Println("an error occured when printing output", err) - return err + return nil, err } - postureData["filePosture"] = kd.ContainerDefaultPosture.FileAction - postureData["capabilitiesPosture"] = kd.ContainerDefaultPosture.CapabilitiesAction - postureData["networkPosture"] = kd.ContainerDefaultPosture.NetworkAction - - return nil + return kd, nil } func parseJsonData(buf []byte) (*KubeArmorProbeData, error) { // Parsing JSON encoded data @@ -572,7 +655,7 @@ func isSystemdMode() bool { return true } -func probeSystemdMode() error { +func probeSystemdMode(o Options) error { jsonFile, err := os.Open("/tmp/karmorProbeData.cfg") if err != nil { log.Println(err) @@ -584,16 +667,25 @@ func probeSystemdMode() error { log.Println("an error occured when reading file", err) return err } - _, err = boldWhite.Printf("Host : \n") - if err != nil { - color.Red(" Error") - } - kd, err := parseJsonData(buf) if err != nil { return err } + if o.Output == "json" { + out, err := json.Marshal(map[string]interface{}{ + "Host": kd, + }) + if err != nil { + return err + } + fmt.Println(string(out)) + return nil + } + _, err = boldWhite.Printf("Host : \n") + if err != nil { + color.Red(" Error") + } err = printKubeArmorProbeOutput(kd) if err != nil { log.Println("an error occured when printing output", err) @@ -611,16 +703,15 @@ func getAnnotatedPodLabels(m map[string]string) mapset.Set[string] { return b } -func getNsSecurityPostureAndVisibility(c *k8s.Client, postureData map[string]string) (map[string]*PostureAndVisibility, error) { +func getNsSecurityPostureAndVisibility(c *k8s.Client, postureData map[string]string) (map[string]*NamespaceData, error) { // Namespace/host security posture and visibility setting - mp := make(map[string]*PostureAndVisibility) + mp := make(map[string]*NamespaceData) namespaces, err := c.K8sClientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{}) if err != nil { return mp, err } - for _, ns := range namespaces.Items { filePosture := postureData["filePosture"] @@ -638,23 +729,64 @@ func getNsSecurityPostureAndVisibility(c *k8s.Client, postureData map[string]str if len(ns.Annotations["kubearmor-network-posture"]) > 0 { networkPosture = ns.Annotations["kubearmor-network-posture"] } - mp[ns.Name] = &PostureAndVisibility{DefaultPosture: "file(" + filePosture + "), capabilities(" + capabilityPosture + "), network(" + networkPosture + ")", Visibility: ns.Annotations["kubearmor-visibility"]} - } + mp[ns.Name] = &NamespaceData{ + NsDefaultPosture: tp.DefaultPosture{FileAction: filePosture, CapabilitiesAction: capabilityPosture, NetworkAction: networkPosture}, + NsVisibilityString: ns.Annotations["kubearmor-visibility"], + NsVisibility: Visibility{ + Process: strings.Contains(ns.Annotations["kubearmor-visibility"], "process"), + File: strings.Contains(ns.Annotations["kubearmor-visibility"], "file"), + Network: strings.Contains(ns.Annotations["kubearmor-visibility"], "network"), + Capabilities: strings.Contains(ns.Annotations["kubearmor-visibility"], "capabilities"), + }, + NsPostureString: " file (" + filePosture + "), capabilities (" + capabilityPosture + "), Network (" + networkPosture + ")", + } + } return mp, err } -func getAnnotatedPods(c *k8s.Client, postureData map[string]string) error { +// NamespaceData structure definition +type NamespaceData struct { + NsPostureString string `json:"-"` + NsVisibilityString string `json:"-"` + NsDefaultPosture tp.DefaultPosture `json:"default_posture"` + NsVisibility Visibility `json:"visibility"` + NsPodList []PodInfo `json:"pod_list"` +} + +// Visibility data structure definition +type Visibility struct { + File bool `json:"file"` + Capabilities bool `json:"capabilities"` + Process bool `json:"process"` + Network bool `json:"network"` +} + +// PodInfo structure definition +type PodInfo struct { + PodName string `json:"pod_name"` + Policy string `json:"policy"` +} + +func getAnnotatedPods(c *k8s.Client, o Options, postureData map[string]string) (map[string]interface{}, error) { + + if o.Output == "text" { + _, err := boldWhite.Printf("Armored Up pods : \n") + if err != nil { + color.Red(" Error printing bold text") + } + } // Annotated Pods Description var data [][]string pods, err := c.K8sClientset.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{}) if err != nil { - return err + return nil, err } + armoredPodData := make(map[string]*NamespaceData) mp, err := getNsSecurityPostureAndVisibility(c, postureData) if err != nil { - return err + return nil, err } policyMap, err := getPoliciesOnAnnotatedPods(c) @@ -667,44 +799,51 @@ func getAnnotatedPods(c *k8s.Client, postureData map[string]string) error { if p.Annotations["kubearmor-policy"] == "enabled" { armoredPod, err := c.K8sClientset.CoreV1().Pods(p.Namespace).Get(context.Background(), p.Name, metav1.GetOptions{}) if err != nil { - return err + return nil, err } - data = append(data, []string{armoredPod.Namespace, mp[armoredPod.Namespace].DefaultPosture, mp[armoredPod.Namespace].Visibility, armoredPod.Name, ""}) + data = append(data, []string{armoredPod.Namespace, mp[armoredPod.Namespace].NsPostureString, mp[armoredPod.Namespace].NsVisibilityString, armoredPod.Name, ""}) labels := getAnnotatedPodLabels(armoredPod.Labels) for policyKey, policyValue := range policyMap { s2 := sliceToSet(policyValue) if s2.IsSubset(labels) { - if checkIfDataAlreadyContainsPodName(data, armoredPod.Name, policyKey) { - continue - } else { - data = append(data, []string{armoredPod.Namespace, mp[armoredPod.Namespace].DefaultPosture, mp[armoredPod.Namespace].Visibility, armoredPod.Name, policyKey}) + if !checkIfDataAlreadyContainsPodName(data, armoredPod.Name, policyKey) { + + data = append(data, []string{armoredPod.Namespace, mp[armoredPod.Namespace].NsPostureString, mp[armoredPod.Namespace].NsVisibilityString, armoredPod.Name, policyKey}) } } } } } - _, err = boldWhite.Printf("Armored Up pods : \n") - if err != nil { - color.Red(" Error printing bold text") - } - // sorting according to namespaces, for merging of cells with same namespaces sort.SliceStable(data, func(i, j int) bool { return data[i][0] < data[j][0] }) + if o.Output == "json" { + for _, v := range data { + + if _, exists := armoredPodData[v[0]]; !exists { + armoredPodData[v[0]] = &NamespaceData{ + NsDefaultPosture: mp[v[0]].NsDefaultPosture, + NsVisibility: mp[v[0]].NsVisibility, + } + } + armoredPodData[v[0]].NsPodList = append(armoredPodData[v[0]].NsPodList, PodInfo{PodName: v[3], Policy: v[4]}) + } + return map[string]interface{}{"Namespaces": armoredPodData}, nil + } + table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"NAMESPACE", "DEFAULT POSTURE", "VISIBILITY", "NAME", "POLICY"}) - for _, v := range data { table.Append(v) } table.SetRowLine(true) table.SetAutoMergeCellsByColumnIndex([]int{0, 1, 2}) table.Render() - return nil + return nil, nil } func getPoliciesOnAnnotatedPods(c *k8s.Client) (map[string][]string, error) {