diff --git a/armometadata/k8sutils.go b/armometadata/k8sutils.go index f922cc4..c9481f2 100644 --- a/armometadata/k8sutils.go +++ b/armometadata/k8sutils.go @@ -1,6 +1,7 @@ package armometadata import ( + "bytes" "encoding/json" "fmt" "hash/fnv" @@ -8,6 +9,7 @@ import ( "strings" "github.com/armosec/utils-k8s-go/wlid" + "github.com/olvrng/ujson" "github.com/spf13/viper" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -102,3 +104,60 @@ func LoadConfig(configPath string) (*ClusterConfig, error) { err = json.Unmarshal(res, &config) return config, err } + +// ExtractMetadataFromBytes extracts metadata from the JSON bytes of a Kubernetes object +func ExtractMetadataFromJsonBytes(input []byte) (error, map[string]string, map[string]string, map[string]string, string, string) { + // output values + annotations := map[string]string{} + labels := map[string]string{} + ownerReferences := map[string]string{} + creationTs := "" + resourceVersion := "" + // ujson parsing + var parent string + err := ujson.Walk(input, func(level int, key, value []byte) bool { + switch level { + case 1: + // skip everything except metadata + if !bytes.EqualFold(key, []byte(`"metadata"`)) { + return false + } + case 2: + // read creationTimestamp + if bytes.EqualFold(key, []byte(`"creationTimestamp"`)) { + creationTs = unquote(value) + } + // read resourceVersion + if bytes.EqualFold(key, []byte(`"resourceVersion"`)) { + resourceVersion = unquote(value) + } + // record parent for level 3 + parent = unquote(key) + case 3: + // read annotations + if parent == "annotations" { + annotations[unquote(key)] = unquote(value) + } + // read labels + if parent == "labels" { + labels[unquote(key)] = unquote(value) + } + case 4: + // read ownerReferences + if parent == "ownerReferences" { + ownerReferences[unquote(key)] = unquote(value) + } + + } + return true + }) + return err, annotations, labels, ownerReferences, creationTs, resourceVersion +} + +func unquote(value []byte) string { + buf, err := ujson.Unquote(value) + if err != nil { + return string(value) + } + return string(buf) +} diff --git a/armometadata/k8sutils_test.go b/armometadata/k8sutils_test.go index ea165bb..ec3a96b 100644 --- a/armometadata/k8sutils_test.go +++ b/armometadata/k8sutils_test.go @@ -1,6 +1,8 @@ package armometadata import ( + "fmt" + "os" "testing" "github.com/armosec/armoapi-go/armotypes" @@ -119,3 +121,96 @@ func TestLoadClusterConfig(t *testing.T) { func BoolPtr(b bool) *bool { return &b } + +func TestExtractMetadataFromJsonBytes(t *testing.T) { + tests := []struct { + name string + want error + annotations map[string]string + labels map[string]string + ownerReferences map[string]string + creationTs string + resourceVersion string + }{ + { + name: "applicationactivity", + annotations: map[string]string{ + "kubescape.io/status": "", + "kubescape.io/wlid": "wlid://cluster-gke_armo-test-clusters_us-central1-c_danielg/namespace-kubescape/deployment-storage", + }, + labels: map[string]string{ + "kubescape.io/workload-api-group": "apps", + "kubescape.io/workload-api-version": "v1", + "kubescape.io/workload-kind": "Deployment", + "kubescape.io/workload-name": "storage", + "kubescape.io/workload-namespace": "kubescape", + }, + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:15:05Z", + resourceVersion: "1", + }, + { + name: "pod", + annotations: map[string]string{ + "cni.projectcalico.org/containerID": "d2e279e2ac8fda015bce3d0acf86121f9df8fdf9bf9e028d99d41110ab1b81dc", + "cni.projectcalico.org/podIP": "10.0.2.169/32", + "cni.projectcalico.org/podIPs": "10.0.2.169/32", + }, + labels: map[string]string{ + "app": "kubescape", + "app.kubernetes.io/instance": "kubescape", + "app.kubernetes.io/name": "kubescape", + "helm.sh/chart": "kubescape-operator-1.16.2", + "helm.sh/revision": "1", + "otel": "enabled", + "pod-template-hash": "549f95c69", + "tier": "ks-control-plane", + }, + ownerReferences: map[string]string{ + "apiVersion": "apps/v1", + "blockOwnerDeletion": "true", + "controller": "true", + "kind": "ReplicaSet", + "name": "kubescape-549f95c69", + "uid": "c0ff7d3b-4183-482c-81c5-998faf0b6150", + }, + creationTs: "2023-11-16T10:12:35Z", + resourceVersion: "59348379", + }, + { + name: "sbom", + annotations: map[string]string{ + "kubescape.io/image-id": "quay.io/kubescape/kubescape@sha256:608b85d3de51caad84a2bfe089ec2c5dbc192dbe9dc319849834bf0e678e0523", + "kubescape.io/status": "", + }, + labels: map[string]string{ + "kubescape.io/image-id": "quay-io-kubescape-kubescape-sha256-608b85d3de51caad84a2bfe089ec", + "kubescape.io/image-name": "quay-io-kubescape-kubescape", + }, + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:13:40Z", + resourceVersion: "1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", tt.name)) + assert.NoError(t, err) + got, annotations, labels, ownerReferences, creationTs, resourceVersion := ExtractMetadataFromJsonBytes(input) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.annotations, annotations) + assert.Equal(t, tt.labels, labels) + assert.Equal(t, tt.ownerReferences, ownerReferences) + assert.Equal(t, tt.creationTs, creationTs) + assert.Equal(t, tt.resourceVersion, resourceVersion) + }) + } +} + +func BenchmarkExtractMetadataFromJsonBytes(b *testing.B) { + input, err := os.ReadFile("testdata/applicationactivity.json") + assert.NoError(b, err) + for i := 0; i < b.N; i++ { + _, _, _, _, _, _ = ExtractMetadataFromJsonBytes(input) + } +} diff --git a/armometadata/testdata/applicationactivity.json b/armometadata/testdata/applicationactivity.json new file mode 100644 index 0000000..3bf276c --- /dev/null +++ b/armometadata/testdata/applicationactivity.json @@ -0,0 +1,88 @@ +{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "ApplicationActivity", + "metadata": { + "annotations": { + "kubescape.io/status": "", + "kubescape.io/wlid": "wlid://cluster-gke_armo-test-clusters_us-central1-c_danielg/namespace-kubescape/deployment-storage" + }, + "creationTimestamp": "2023-11-16T10:15:05Z", + "labels": { + "kubescape.io/workload-api-group": "apps", + "kubescape.io/workload-api-version": "v1", + "kubescape.io/workload-kind": "Deployment", + "kubescape.io/workload-name": "storage", + "kubescape.io/workload-namespace": "kubescape" + }, + "name": "kubescape-replicaset-storage-76d8b6ddfc-aced-6fcf", + "namespace": "kubescape", + "resourceVersion": "1", + "uid": "1e06cb11-4bab-4bed-bca1-8e95982a0d2e" + }, + "spec": { + "syscalls": [ + "newfstatat", + "setgroups", + "getpeername", + "mmap", + "getdents64", + "rt_sigreturn", + "epoll_pwait", + "execve", + "fcntl", + "getsockopt", + "prctl", + "sched_getaffinity", + "accept4", + "connect", + "pread64", + "sendto", + "uname", + "epoll_create1", + "getppid", + "mkdirat", + "readlinkat", + "madvise", + "recvfrom", + "rt_sigprocmask", + "close", + "gettid", + "setgid", + "statfs", + "getrlimit", + "nanosleep", + "getrandom", + "inotify_init1", + "clone", + "futex", + "fchown", + "fstatfs", + "getpid", + "read", + "tgkill", + "arch_prctl", + "faccessat2", + "ioctl", + "listen", + "openat", + "capset", + "getuid", + "pipe2", + "setuid", + "sigaltstack", + "bind", + "fstat", + "write", + "inotify_add_watch", + "sched_yield", + "getsockname", + "setsockopt", + "socket", + "capget", + "epoll_ctl", + "rt_sigaction", + "chdir" + ] + }, + "status": {} +} diff --git a/armometadata/testdata/pod.json b/armometadata/testdata/pod.json new file mode 100644 index 0000000..d30ddf0 --- /dev/null +++ b/armometadata/testdata/pod.json @@ -0,0 +1,368 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "cni.projectcalico.org/containerID": "d2e279e2ac8fda015bce3d0acf86121f9df8fdf9bf9e028d99d41110ab1b81dc", + "cni.projectcalico.org/podIP": "10.0.2.169/32", + "cni.projectcalico.org/podIPs": "10.0.2.169/32" + }, + "creationTimestamp": "2023-11-16T10:12:35Z", + "generateName": "kubescape-549f95c69-", + "labels": { + "app": "kubescape", + "app.kubernetes.io/instance": "kubescape", + "app.kubernetes.io/name": "kubescape", + "helm.sh/chart": "kubescape-operator-1.16.2", + "helm.sh/revision": "1", + "otel": "enabled", + "pod-template-hash": "549f95c69", + "tier": "ks-control-plane" + }, + "name": "kubescape-549f95c69-pfvm7", + "namespace": "kubescape", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "kubescape-549f95c69", + "uid": "c0ff7d3b-4183-482c-81c5-998faf0b6150" + } + ], + "resourceVersion": "59348379", + "uid": "833c4131-2996-49b0-88e7-59d7e78c00fb" + }, + "spec": { + "automountServiceAccountToken": true, + "containers": [ + { + "command": [ + "ksserver" + ], + "env": [ + { + "name": "GOMEMLIMIT", + "value": "400MiB" + }, + { + "name": "KS_LOGGER_LEVEL", + "value": "debug" + }, + { + "name": "KS_LOGGER_NAME", + "value": "zap" + }, + { + "name": "KS_DOWNLOAD_ARTIFACTS", + "value": "true" + }, + { + "name": "RULE_PROCESSING_GOMAXPROCS" + }, + { + "name": "KS_DEFAULT_CONFIGMAP_NAME", + "value": "kubescape-config" + }, + { + "name": "KS_DEFAULT_CONFIGMAP_NAMESPACE", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + } + } + }, + { + "name": "KS_CONTEXT", + "value": "gke_armo-test-clusters_us-central1-c_danielg" + }, + { + "name": "KS_DEFAULT_CLOUD_CONFIGMAP_NAME", + "value": "ks-cloud-config" + }, + { + "name": "KS_ENABLE_HOST_SCANNER", + "value": "true" + }, + { + "name": "KS_SKIP_UPDATE_CHECK", + "value": "false" + }, + { + "name": "KS_HOST_SCAN_YAML", + "value": "/home/nonroot/.kubescape/host-scanner.yaml" + }, + { + "name": "LARGE_CLUSTER_SIZE", + "value": "1500" + }, + { + "name": "ACCOUNT_ID", + "valueFrom": { + "secretKeyRef": { + "key": "account", + "name": "cloud-secret" + } + } + }, + { + "name": "OTEL_COLLECTOR_SVC", + "value": "otel-collector:4317" + } + ], + "image": "quay.io/kubescape/kubescape:v3.0.1", + "imagePullPolicy": "IfNotPresent", + "livenessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/livez", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 3, + "periodSeconds": 3, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "name": "kubescape", + "ports": [ + { + "containerPort": 8080, + "name": "http", + "protocol": "TCP" + } + ], + "readinessProbe": { + "failureThreshold": 3, + "httpGet": { + "path": "/readyz", + "port": 8080, + "scheme": "HTTP" + }, + "initialDelaySeconds": 3, + "periodSeconds": 3, + "successThreshold": 1, + "timeoutSeconds": 1 + }, + "resources": { + "limits": { + "memory": "1Gi" + }, + "requests": { + "memory": "400Mi" + } + }, + "securityContext": { + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true, + "runAsNonRoot": true + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/etc/credentials", + "name": "cloud-secret", + "readOnly": true + }, + { + "mountPath": "/home/nonroot/.kubescape", + "name": "kubescape-volume", + "subPath": "config.json" + }, + { + "mountPath": "/home/nonroot/.kubescape/host-scanner.yaml", + "name": "host-scanner-definition", + "subPath": "host-scanner-yaml" + }, + { + "mountPath": "/home/nonroot/results", + "name": "results" + }, + { + "mountPath": "/home/nonroot/failed", + "name": "failed" + }, + { + "mountPath": "/etc/config", + "name": "ks-cloud-config", + "readOnly": true + }, + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "kube-api-access-64hdm", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "gke-danielg-default-pool-8ca9aa65-fshh", + "preemptionPolicy": "PreemptLowerPriority", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": { + "fsGroup": 65532, + "runAsUser": 65532 + }, + "serviceAccount": "kubescape", + "serviceAccountName": "kubescape", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "cloud-secret", + "secret": { + "defaultMode": 420, + "secretName": "cloud-secret" + } + }, + { + "configMap": { + "defaultMode": 420, + "items": [ + { + "key": "clusterData", + "path": "clusterData.json" + }, + { + "key": "services", + "path": "services.json" + } + ], + "name": "ks-cloud-config" + }, + "name": "ks-cloud-config" + }, + { + "configMap": { + "defaultMode": 420, + "name": "host-scanner-definition" + }, + "name": "host-scanner-definition" + }, + { + "emptyDir": {}, + "name": "kubescape-volume" + }, + { + "emptyDir": {}, + "name": "results" + }, + { + "emptyDir": {}, + "name": "failed" + }, + { + "name": "kube-api-access-64hdm", + "projected": { + "defaultMode": 420, + "sources": [ + { + "serviceAccountToken": { + "expirationSeconds": 3607, + "path": "token" + } + }, + { + "configMap": { + "items": [ + { + "key": "ca.crt", + "path": "ca.crt" + } + ], + "name": "kube-root-ca.crt" + } + }, + { + "downwardAPI": { + "items": [ + { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + }, + "path": "namespace" + } + ] + } + } + ] + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2023-11-16T10:12:35Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2023-11-16T10:12:39Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2023-11-16T10:12:39Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2023-11-16T10:12:35Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://e100754284db718d949f90ac10c3bd7b7abae1238db791d41cd7245e4ee382ff", + "image": "quay.io/kubescape/kubescape:v3.0.1", + "imageID": "quay.io/kubescape/kubescape@sha256:608b85d3de51caad84a2bfe089ec2c5dbc192dbe9dc319849834bf0e678e0523", + "lastState": {}, + "name": "kubescape", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2023-11-16T10:12:36Z" + } + } + } + ], + "hostIP": "10.128.0.99", + "phase": "Running", + "podIP": "10.0.2.169", + "podIPs": [ + { + "ip": "10.0.2.169" + } + ], + "qosClass": "Burstable", + "startTime": "2023-11-16T10:12:35Z" + } +} diff --git a/armometadata/testdata/sbom.json b/armometadata/testdata/sbom.json new file mode 100644 index 0000000..bc7b666 --- /dev/null +++ b/armometadata/testdata/sbom.json @@ -0,0 +1,71 @@ +{ + "apiVersion": "spdx.softwarecomposition.kubescape.io/v1beta1", + "kind": "SBOMSPDXv2p3", + "metadata": { + "annotations": { + "kubescape.io/image-id": "quay.io/kubescape/kubescape@sha256:608b85d3de51caad84a2bfe089ec2c5dbc192dbe9dc319849834bf0e678e0523", + "kubescape.io/status": "" + }, + "creationTimestamp": "2023-11-16T10:13:40Z", + "labels": { + "kubescape.io/image-id": "quay-io-kubescape-kubescape-sha256-608b85d3de51caad84a2bfe089ec", + "kubescape.io/image-name": "quay-io-kubescape-kubescape" + }, + "name": "quay.io-kubescape-kubescape-v3.0.1-8e0523", + "namespace": "kubescape", + "resourceVersion": "1", + "uid": "760b36e2-19ce-47bb-93be-afefcb09b66c" + }, + "spec": { + "metadata": { + "report": { + "createdAt": null + }, + "tool": { + "name": "", + "version": "v0.76.0" + } + }, + "spdx": { + "SPDXID": "SPDXRef-DOCUMENT", + "annotations": null, + "comment": "", + "creationInfo": { + "comment": "", + "created": "2023-11-16T10:13:39Z", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-v0.76.0" + ], + "licenseListVersion": "3.20" + }, + "dataLicense": "CC0-1.0", + "documentDescribes": null, + "documentNamespace": "https://anchore.com/syft/image/quay.io/kubescape/kubescape@sha256-608b85d3de51caad84a2bfe089ec2c5dbc192dbe9dc319849834bf0e678e0523-fe03ff99-d7cf-4b09-be11-9cd3dbf0f60b", + "externalDocumentRefs": null, + "files": null, + "hasExtractedLicensingInfos": [ + { + "comment": "", + "extractedText": "NONE", + "licenseId": "LicenseRef-GPL", + "name": "GPL", + "seeAlsos": null + } + ], + "name": "quay.io/kubescape/kubescape@sha256:608b85d3de51caad84a2bfe089ec2c5dbc192dbe9dc319849834bf0e678e0523", + "packages": [], + "relationships": [ + { + "comment": "", + "relatedSpdxElement": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "spdxElementId": "SPDXRef-DOCUMENT" + } + ], + "snippets": null, + "spdxVersion": "SPDX-2.3" + } + }, + "status": {} +} diff --git a/go.mod b/go.mod index 15f7f84..58b6473 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/armosec/utils-go v0.0.20 github.com/docker/docker v24.0.5+incompatible github.com/francoispqt/gojay v1.2.13 + github.com/olvrng/ujson v1.1.0 github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.22.0 diff --git a/go.sum b/go.sum index 3c7858e..9253f00 100644 --- a/go.sum +++ b/go.sum @@ -391,6 +391,8 @@ github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olvrng/ujson v1.1.0 h1:8xVUzVlqwdMVWh5d1UHBtLQ1D50nxoPuPEq9Wozs8oA= +github.com/olvrng/ujson v1.1.0/go.mod h1:Mz4G3RODTUfbkKyvi0lgmPx/7vd3Saksk+1jgk8s9xo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=