diff --git a/armometadata/k8sutils.go b/armometadata/k8sutils.go index 15b9476..89b561d 100644 --- a/armometadata/k8sutils.go +++ b/armometadata/k8sutils.go @@ -106,7 +106,7 @@ func LoadConfig(configPath string) (*ClusterConfig, error) { } // 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, string, string) { +func ExtractMetadataFromJsonBytes(input []byte) (error, map[string]string, map[string]string, map[string]string, string, string, string, string, map[string]string) { // output values annotations := map[string]string{} labels := map[string]string{} @@ -115,8 +115,9 @@ func ExtractMetadataFromJsonBytes(input []byte) (error, map[string]string, map[s resourceVersion := "" kind := "" apiVersion := "" + podSelectorMatchLabels := map[string]string{} // ujson parsing - var parent string + var parent, subParent, subParent2 string err := ujson.Walk(input, func(level int, key, value []byte) bool { switch level { case 1: @@ -129,39 +130,54 @@ func ExtractMetadataFromJsonBytes(input []byte) (error, map[string]string, map[s } // skip everything except metadata - if !bytes.EqualFold(key, []byte(`"metadata"`)) { + if !bytes.EqualFold(key, []byte(`"metadata"`)) && !bytes.EqualFold(key, []byte(`"spec"`)) { return false } + + parent = unquote(key) case 2: - // read creationTimestamp - if bytes.EqualFold(key, []byte(`"creationTimestamp"`)) { - creationTs = unquote(value) - } - // read resourceVersion - if bytes.EqualFold(key, []byte(`"resourceVersion"`)) { - resourceVersion = unquote(value) + if parent == "metadata" { + // 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) + subParent = unquote(key) + case 3: // read annotations - if parent == "annotations" { + if subParent == "annotations" { annotations[unquote(key)] = unquote(value) } // read labels - if parent == "labels" { + if subParent == "labels" { labels[unquote(key)] = unquote(value) } + + subParent2 = unquote(key) + case 4: // read ownerReferences - if parent == "ownerReferences" { + if subParent == "ownerReferences" { ownerReferences[unquote(key)] = unquote(value) } + if subParent2 == "matchLabels" { + podSelectorMatchLabels[unquote(key)] = unquote(value) + + } + } return true }) - return err, annotations, labels, ownerReferences, creationTs, resourceVersion, kind, apiVersion + return err, annotations, labels, ownerReferences, creationTs, resourceVersion, kind, apiVersion, podSelectorMatchLabels } func unquote(value []byte) string { diff --git a/armometadata/k8sutils_test.go b/armometadata/k8sutils_test.go index 0e4cab3..08f646b 100644 --- a/armometadata/k8sutils_test.go +++ b/armometadata/k8sutils_test.go @@ -124,16 +124,41 @@ func BoolPtr(b bool) *bool { 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 - kind string - apiVersion string + name string + want error + annotations map[string]string + labels map[string]string + ownerReferences map[string]string + creationTs string + resourceVersion string + kind string + apiVersion string + podSelectorMatchLabels map[string]string }{ + { + name: "networkpolicy_withoutmatching_labels", + annotations: map[string]string{}, + labels: map[string]string{}, + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:12:35Z", + resourceVersion: "", + kind: "NetworkPolicy", + apiVersion: "networking.k8s.io/v1", + podSelectorMatchLabels: map[string]string{}, + }, + { + name: "networkpolicy_withmatching_labels", + annotations: map[string]string{}, + labels: map[string]string{}, + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:12:35Z", + resourceVersion: "", + kind: "NetworkPolicy", + apiVersion: "networking.k8s.io/v1", + podSelectorMatchLabels: map[string]string{ + "role": "frontend", + }, + }, { name: "applicationactivity", annotations: map[string]string{ @@ -147,11 +172,12 @@ func TestExtractMetadataFromJsonBytes(t *testing.T) { "kubescape.io/workload-name": "storage", "kubescape.io/workload-namespace": "kubescape", }, - ownerReferences: map[string]string{}, - creationTs: "2023-11-16T10:15:05Z", - resourceVersion: "1", - kind: "ApplicationActivity", - apiVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:15:05Z", + resourceVersion: "1", + kind: "ApplicationActivity", + apiVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + podSelectorMatchLabels: map[string]string{}, }, { name: "pod", @@ -178,10 +204,11 @@ func TestExtractMetadataFromJsonBytes(t *testing.T) { "name": "kubescape-549f95c69", "uid": "c0ff7d3b-4183-482c-81c5-998faf0b6150", }, - creationTs: "2023-11-16T10:12:35Z", - resourceVersion: "59348379", - kind: "Pod", - apiVersion: "v1", + creationTs: "2023-11-16T10:12:35Z", + resourceVersion: "59348379", + kind: "Pod", + apiVersion: "v1", + podSelectorMatchLabels: map[string]string{}, }, { name: "sbom", @@ -193,18 +220,19 @@ func TestExtractMetadataFromJsonBytes(t *testing.T) { "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", - kind: "SBOMSPDXv2p3", - apiVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + ownerReferences: map[string]string{}, + creationTs: "2023-11-16T10:13:40Z", + resourceVersion: "1", + kind: "SBOMSPDXv2p3", + apiVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + podSelectorMatchLabels: map[string]string{}, }, } 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, kind, apiVersion := ExtractMetadataFromJsonBytes(input) + got, annotations, labels, ownerReferences, creationTs, resourceVersion, kind, apiVersion, podSelectorMatchLabels := ExtractMetadataFromJsonBytes(input) assert.Equal(t, tt.want, got) assert.Equal(t, tt.annotations, annotations) assert.Equal(t, tt.labels, labels) @@ -213,6 +241,7 @@ func TestExtractMetadataFromJsonBytes(t *testing.T) { assert.Equal(t, tt.resourceVersion, resourceVersion) assert.Equal(t, tt.kind, kind) assert.Equal(t, tt.apiVersion, apiVersion) + assert.Equal(t, tt.podSelectorMatchLabels, podSelectorMatchLabels) }) } } @@ -221,6 +250,6 @@ 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) + _, _, _, _, _, _, _, _, _ = ExtractMetadataFromJsonBytes(input) } } diff --git a/armometadata/parser.go b/armometadata/parser.go new file mode 100644 index 0000000..c7ac2e3 --- /dev/null +++ b/armometadata/parser.go @@ -0,0 +1,107 @@ +package armometadata + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + AnnotationKeyStatus = "kubescape.io/status" + AnnotationValueIncomplete = "incomplete" + + MetadataKeyResourceVersion = "resourceVersion" +) + +type KubernetesObjectParser struct { + resourceVersion string + labels map[string]string + annotations map[string]string + creationStamp time.Time + ownerReferences metav1.OwnerReference + kind string + apiVersion string + podSelectorMatchLabels map[string]string +} + +func NewKubernetesResourceParser(input []byte) (*KubernetesObjectParser, error) { + err, annotations, labels, ownerReferences, creationStamp, resourceVersion, kind, apiVersion, podSelectorMatchLabels := ExtractMetadataFromJsonBytes(input) + + if err != nil { + return nil, err + } + + creationStampTime, err := time.Parse(time.RFC3339, creationStamp) + if err != nil { + return nil, err + } + + newOwnerReferences := metav1.OwnerReference{} + + if len(ownerReferences) > 0 { + if value, ok := ownerReferences["name"]; ok { + newOwnerReferences.Name = value + } + + if value, ok := ownerReferences["kind"]; ok { + newOwnerReferences.Kind = value + } + + } + + newKubernetesResourceParser := &KubernetesObjectParser{} + newKubernetesResourceParser.resourceVersion = resourceVersion + newKubernetesResourceParser.labels = labels + newKubernetesResourceParser.annotations = annotations + newKubernetesResourceParser.creationStamp = creationStampTime + newKubernetesResourceParser.ownerReferences = newOwnerReferences + newKubernetesResourceParser.kind = kind + newKubernetesResourceParser.apiVersion = apiVersion + newKubernetesResourceParser.podSelectorMatchLabels = podSelectorMatchLabels + + return newKubernetesResourceParser, nil +} + +func (k *KubernetesObjectParser) GetLabels() map[string]string { + return k.labels +} + +func (k *KubernetesObjectParser) GetLabel(label string) string { + return k.labels[label] +} + +func (k *KubernetesObjectParser) GetAnnotation(annotation string) string { + return k.annotations[annotation] +} + +func (k *KubernetesObjectParser) GetCreationTimestamp() time.Time { + return k.creationStamp +} + +func (k *KubernetesObjectParser) GetResourceVersion() string { + return k.resourceVersion +} + +func (k *KubernetesObjectParser) GetOwnerReferencesKind() string { + return k.ownerReferences.Kind +} + +func (k *KubernetesObjectParser) GetOwnerReferencesName() string { + return k.ownerReferences.Name +} + +func (k *KubernetesObjectParser) GetStatus() string { + return k.annotations[AnnotationKeyStatus] +} + +func (k *KubernetesObjectParser) GetKind() string { + return k.kind +} + +func (k *KubernetesObjectParser) GetApiVersion() string { + return k.apiVersion +} + +func (k *KubernetesObjectParser) GetPodSelectorMatchLabels() map[string]string { + return k.podSelectorMatchLabels +} diff --git a/armometadata/parser_test.go b/armometadata/parser_test.go new file mode 100644 index 0000000..e6b90c2 --- /dev/null +++ b/armometadata/parser_test.go @@ -0,0 +1,76 @@ +package armometadata + +import ( + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNewKubernetesResourceParser(t *testing.T) { + // Test case with valid JSON input + t.Run("valid input", func(t *testing.T) { + validInput := []byte(`{ + "metadata": { + "annotations": {"kubescape.io/status": "active"}, + "labels": {"kubescape.io/workload-name": "example"}, + "ownerReferences": [{"name": "ownerName", "kind": "ownerKind"}], + "creationTimestamp": "2023-03-15T08:00:00Z", + "resourceVersion": "12345" + } + }`) + + expectedCreationTimestamp, _ := time.Parse(time.RFC3339, "2023-03-15T08:00:00Z") + expectedParser := &KubernetesObjectParser{ + resourceVersion: "12345", + labels: map[string]string{"kubescape.io/workload-name": "example"}, + annotations: map[string]string{"kubescape.io/status": "active"}, + creationStamp: expectedCreationTimestamp, + ownerReferences: metav1.OwnerReference{Name: "ownerName", Kind: "ownerKind"}, + podSelectorMatchLabels: map[string]string{}, + } + + parser, err := NewKubernetesResourceParser(validInput) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if !reflect.DeepEqual(parser, expectedParser) { + t.Errorf("Expected parser %+v, got %+v", expectedParser, parser) + } + }) + + // Test case with invalid JSON input + t.Run("invalid json input", func(t *testing.T) { + invalidInput := []byte(`invalid json`) + + _, err := NewKubernetesResourceParser(invalidInput) + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + // Test case with invalid date format + t.Run("invalid date format", func(t *testing.T) { + invalidDateInput := []byte(`{ + "metadata": { + "creationTimestamp": "invalid-date-format" + } + }`) + + _, err := NewKubernetesResourceParser(invalidDateInput) + if err == nil { + t.Errorf("Expected error parsing date, got nil") + } + }) + + // Test case with empty JSON + t.Run("empty json", func(t *testing.T) { + emptyJSON := []byte(`{}`) + + _, err := NewKubernetesResourceParser(emptyJSON) + if err == nil { + t.Errorf("Expected error due to missing metadata, got nil") + } + }) +} diff --git a/armometadata/testdata/networkpolicy_withmatching_labels.json b/armometadata/testdata/networkpolicy_withmatching_labels.json new file mode 100644 index 0000000..caabc92 --- /dev/null +++ b/armometadata/testdata/networkpolicy_withmatching_labels.json @@ -0,0 +1,31 @@ +{ + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "creationTimestamp": "2023-11-16T10:12:35Z", + "name": "allow-frontend-backend", + "namespace": "default" + }, + "spec": { + "podSelector": { + "matchLabels": { + "role": "frontend" + } + }, + "policyTypes": ["Ingress"], + "ingress": [ + { + "from": [ + { + "podSelector": { + "matchLabels": { + "role": "backend" + } + } + } + ] + } + ] + } + } + \ No newline at end of file diff --git a/armometadata/testdata/networkpolicy_withoutmatching_labels.json b/armometadata/testdata/networkpolicy_withoutmatching_labels.json new file mode 100644 index 0000000..640dba3 --- /dev/null +++ b/armometadata/testdata/networkpolicy_withoutmatching_labels.json @@ -0,0 +1,23 @@ +{ + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "creationTimestamp": "2023-11-16T10:12:35Z", + "name": "allow-all-in-namespace", + "namespace": "default" + }, + "spec": { + "podSelector": {}, + "policyTypes": ["Ingress"], + "ingress": [ + { + "from": [ + { + "podSelector": {} + } + ] + } + ] + } + } + \ No newline at end of file