diff --git a/pkg/reconciler/reconciler_test.go b/pkg/reconciler/reconciler_test.go index 5b0a637a..2edd1a3f 100644 --- a/pkg/reconciler/reconciler_test.go +++ b/pkg/reconciler/reconciler_test.go @@ -378,7 +378,7 @@ func TestReconcilerComplete(t *testing.T) { // Checks the count of nodes and edges based on the JSON files in pkg/test-data // Update counts when the test data is changed // We don't create Nodes for kind = Event - const Nodes = 41 + const Nodes = 43 const Edges = 51 if len(com.Edges) != Edges || com.TotalEdges != Edges || len(com.Nodes) != Nodes || com.TotalNodes != Nodes { ns := tr.NodeStore{ diff --git a/pkg/transforms/kyverno.go b/pkg/transforms/kyverno.go new file mode 100644 index 00000000..5272e41c --- /dev/null +++ b/pkg/transforms/kyverno.go @@ -0,0 +1,54 @@ +// Copyright Contributors to the Open Cluster Management project + +package transforms + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type KyvernoPolicyResource struct { + node Node +} + +// KyvernoPolicyResourceBuilder handles Kyverno Policy and ClusterPolicy objects. See: +// +// https://github.com/kyverno/kyverno/blob/main/config/crds/kyverno/kyverno.io_policies.yaml +// https://github.com/kyverno/kyverno/blob/main/config/crds/kyverno/kyverno.io_clusterpolicies.yaml +func KyvernoPolicyResourceBuilder(p *unstructured.Unstructured) *KyvernoPolicyResource { + node := transformCommon(p) // Start off with the common properties + apiGroupVersion(metav1.TypeMeta{Kind: p.GetKind(), APIVersion: p.GetAPIVersion()}, &node) + node.Properties["_isExternal"] = getIsPolicyExternal(p) + if validationFailureAction, ok, _ := unstructured.NestedString(p.Object, "spec", "validationFailureAction"); ok { + node.Properties["validationFailureAction"] = validationFailureAction + } else { + // Audit is the default value and this makes the indexing easier + node.Properties["validationFailureAction"] = "Audit" + } + + if background, ok, _ := unstructured.NestedBool(p.Object, "spec", "background"); ok { + node.Properties["background"] = background + } else { + // true is the default value and this makes the indexing easier + node.Properties["background"] = true + } + + if admission, ok, _ := unstructured.NestedBool(p.Object, "spec", "admission"); ok { + node.Properties["admission"] = admission + } else { + // true is the default value and this makes the indexing easier + node.Properties["admission"] = true + } + + node.Properties["severity"] = p.GetAnnotations()["policies.kyverno.io/severity"] + + return &KyvernoPolicyResource{node: node} +} + +func (p KyvernoPolicyResource) BuildNode() Node { + return p.node +} + +func (p KyvernoPolicyResource) BuildEdges(ns NodeStore) []Edge { + return []Edge{} +} diff --git a/pkg/transforms/kyverno_test.go b/pkg/transforms/kyverno_test.go new file mode 100644 index 00000000..2fb94463 --- /dev/null +++ b/pkg/transforms/kyverno_test.go @@ -0,0 +1,71 @@ +// Copyright Contributors to the Open Cluster Management project + +package transforms + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestTransformKyvernoPolicy(t *testing.T) { + p := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kyverno.io/v1", + "kind": "Policy", + "metadata": map[string]interface{}{ + "name": "my-policy", + "namespace": "my-app", + "annotations": map[string]interface{}{ + "policies.kyverno.io/severity": "medium", + }, + }, + "spec": map[string]interface{}{ + "validationFailureAction": "Deny", + "random": "value", + }, + }, + } + rv := KyvernoPolicyResourceBuilder(&p) + node := rv.node + + AssertEqual("validationFailureAction", node.Properties["validationFailureAction"], "Deny", t) + AssertEqual("background", node.Properties["background"], true, t) + AssertEqual("admission", node.Properties["admission"], true, t) + AssertEqual("severity", node.Properties["severity"], "medium", t) + + // Check the default value for spec.validationFailureAction + unstructured.RemoveNestedField(p.Object, "spec", "validationFailureAction") + + rv = KyvernoPolicyResourceBuilder(&p) + node = rv.node + AssertEqual("validationFailureAction", node.Properties["validationFailureAction"], "Audit", t) +} + +func TestTransformKyvernoClusterPolicy(t *testing.T) { + p := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": map[string]interface{}{ + "name": "my-policy", + "annotations": map[string]interface{}{ + "policies.kyverno.io/severity": "critical", + }, + }, + "spec": map[string]interface{}{ + "validationFailureAction": "Deny", + "random": "value", + "background": false, + "admission": false, + }, + }, + } + rv := KyvernoPolicyResourceBuilder(&p) + node := rv.node + + AssertEqual("validationFailureAction", node.Properties["validationFailureAction"], "Deny", t) + AssertEqual("background", node.Properties["background"], false, t) + AssertEqual("admission", node.Properties["admission"], false, t) + AssertEqual("severity", node.Properties["severity"], "critical", t) +} diff --git a/pkg/transforms/policyreport.go b/pkg/transforms/policyreport.go index 5fa3714d..601712ca 100644 --- a/pkg/transforms/policyreport.go +++ b/pkg/transforms/policyreport.go @@ -4,16 +4,21 @@ package transforms import ( + "sort" "strings" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/stolostron/search-collector/pkg/config" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" ) +const managedByLabel = "app.kubernetes.io/managed-by" + // PolicyReport report type PolicyReport struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` Results []ReportResults `json:"results"` Scope corev1.ObjectReference `json:"scope"` } @@ -51,30 +56,40 @@ func PolicyReportResourceBuilder(pr *PolicyReport) *PolicyReportResource { node.Properties["apiversion"] = gvk.Version node.Properties["apigroup"] = gvk.Group + isKyverno := pr.Labels[managedByLabel] == "kyverno" + numRuleViolations := 0 + // Filter GRC sourced policy violations from node results // Policy details are displayed elsewhere in the UI, displaying them in the PR will result in double counts on pages results := []ReportResults{} for _, result := range pr.Results { - if result.Source == "insights" { + // Include all the results for Kyverno generated policy reports + if isKyverno || result.Source == "insights" { results = append(results, result) } + + if result.Result == "fail" || result.Result == "error" { + numRuleViolations++ + } } // Total number of policies in the report - node.Properties["numRuleViolations"] = len(results) + node.Properties["numRuleViolations"] = numRuleViolations // Extract the properties specific to this type categoryMap := make(map[string]struct{}) - policies := make([]string, 0, len(results)) - var critical = 0 - var important = 0 - var moderate = 0 - var low = 0 + policies := sets.Set[string]{} + critical := 0 + important := 0 + moderate := 0 + low := 0 + + policyViolationCounts := map[string]int{} for _, result := range results { for _, category := range strings.Split(result.Category, ",") { categoryMap[category] = struct{}{} } - policies = append(policies, result.Policy) + policies.Insert(result.Policy) switch result.Properties.TotalRisk { case "4": critical++ @@ -85,12 +100,25 @@ func PolicyReportResourceBuilder(pr *PolicyReport) *PolicyReportResource { case "1": low++ } + + if _, ok := policyViolationCounts[result.Policy]; !ok { + policyViolationCounts[result.Policy] = 0 + } + + if result.Result == "fail" || result.Result == "error" { + policyViolationCounts[result.Policy]++ + } } categories := make([]string, 0, len(categoryMap)) for k := range categoryMap { categories = append(categories, k) } - node.Properties["rules"] = policies + + policyList := policies.UnsortedList() + sort.Strings(policyList) + + // "rules" is incorrect since there is a "rule" field in the results, but this is kept for backwards compatibility + node.Properties["rules"] = policyList node.Properties["category"] = categories node.Properties["critical"] = critical node.Properties["important"] = important @@ -98,6 +126,7 @@ func PolicyReportResourceBuilder(pr *PolicyReport) *PolicyReportResource { node.Properties["low"] = low // extract the cluster scope from the PolicyReport resource node.Properties["scope"] = string(pr.Scope.Name) + node.Properties["_policyViolationCounts"] = policyViolationCounts return &PolicyReportResource{node: node} } @@ -108,7 +137,63 @@ func (pr PolicyReportResource) BuildNode() Node { // BuildEdges builds any necessary edges to related resources func (pr PolicyReportResource) BuildEdges(ns NodeStore) []Edge { - // TODO What edges does PolicyReport need - ret := []Edge{} - return ret + edges := []Edge{} + + labels, ok := pr.node.Properties["label"].(map[string]string) + if !ok || labels[managedByLabel] != "kyverno" { + return edges + } + + // "rules" represents the policies + for _, policy := range pr.node.Properties["rules"].([]string) { + var kind, namespace, name string + + splitPolicy := strings.SplitN(policy, "/", 2) + + // Detect if it's a Policy or ClusterPolicy based on the presence of a namespace + if len(splitPolicy) == 2 { + kind = "Policy" + namespace = splitPolicy[0] + name = splitPolicy[1] + } else { + kind = "ClusterPolicy" + namespace = "_NONE" + name = policy + } + + policyNode, ok := ns.ByKindNamespaceName[kind][namespace][name] + if !ok { + continue + } + + edges = append(edges, Edge{ + SourceKind: policyNode.Properties["kind"].(string), + SourceUID: policyNode.UID, + EdgeType: "reports", + DestKind: pr.node.Properties["kind"].(string), + DestUID: pr.node.UID, + }) + + // The PolicyReport name is the UID of the violating object + violatingObject, ok := ns.ByUID[config.Cfg.ClusterName+"/"+pr.node.Properties["name"].(string)] + if !ok { + continue + } + + edges = append(edges, Edge{ + SourceKind: policyNode.Properties["kind"].(string), + SourceUID: policyNode.UID, + EdgeType: "appliesTo", + DestUID: violatingObject.UID, + DestKind: violatingObject.Properties["kind"].(string), + }, Edge{ + SourceKind: pr.node.Properties["kind"].(string), + SourceUID: pr.node.UID, + EdgeType: "reportsOn", + DestUID: violatingObject.UID, + DestKind: violatingObject.Properties["kind"].(string), + }) + } + + return edges } diff --git a/pkg/transforms/policyreport_test.go b/pkg/transforms/policyreport_test.go index 9bf9a194..5c017704 100644 --- a/pkg/transforms/policyreport_test.go +++ b/pkg/transforms/policyreport_test.go @@ -4,7 +4,10 @@ package transforms import ( + "sort" "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestTransformPolicyReport(t *testing.T) { @@ -22,6 +25,253 @@ func TestTransformPolicyReport(t *testing.T) { AssertDeepEqual("low", node.Properties["low"], 1, t) AssertDeepEqual("scope", node.Properties["scope"], "test-cluster", t) + expected := map[string]int{ + "policyreport testing risk 1 policy": 1, + "policyreport testing risk 2 policy": 1, + } + AssertDeepEqual("_policyViolationCounts", node.Properties["_policyViolationCounts"].(map[string]int), expected, t) +} + +func TestTransformKyvernoClusterPolicyReport(t *testing.T) { + var pr PolicyReport + UnmarshalFile("kyverno-clusterpolicyreport.json", &pr, t) + node := PolicyReportResourceBuilder(&pr).BuildNode() + + AssertDeepEqual("category", node.Properties["category"].([]string), []string{"Kubecost"}, t) + AssertDeepEqual("rules", node.Properties["rules"], []string{"no-label-of-monkey", "require-kubecost-labels"}, t) + // 1 failure and 1 error + AssertDeepEqual("numRuleViolations", node.Properties["numRuleViolations"], 2, t) + expected := map[string]int{"require-kubecost-labels": 2, "no-label-of-monkey": 0} + AssertDeepEqual("_policyViolationCounts", node.Properties["_policyViolationCounts"].(map[string]int), expected, t) +} + +func TestTransformKyvernoPolicyReport(t *testing.T) { + var pr PolicyReport + UnmarshalFile("kyverno-policyreport.json", &pr, t) + node := PolicyReportResourceBuilder(&pr).BuildNode() + + AssertDeepEqual("category", node.Properties["category"].([]string), []string{"Kubecost"}, t) + AssertDeepEqual( + "rules", + node.Properties["rules"], + []string{"open-cluster-management-agent-addon/require-kubecost-labels", "require-kubecost-labels"}, + t, + ) + AssertDeepEqual("numRuleViolations", node.Properties["numRuleViolations"], 2, t) + expected := map[string]int{ + "require-kubecost-labels": 1, + "open-cluster-management-agent-addon/require-kubecost-labels": 1, + } + AssertDeepEqual("_policyViolationCounts", node.Properties["_policyViolationCounts"].(map[string]int), expected, t) +} + +func TestKyvernoClusterPolicyReportBuildEdges(t *testing.T) { + p := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": map[string]interface{}{ + "name": "require-kubecost-labels", + "annotations": map[string]interface{}{ + "policies.kyverno.io/severity": "critical", + }, + "uid": "132ec5b8-892b-40da-8b92-af141c377dfe", + }, + "spec": map[string]interface{}{ + "validationFailureAction": "deny", + "random": "value", + }, + }, + } + + ns := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "kube-public", + "uid": "13967788-e842-4662-9630-a9e9c39fa199", + }, + }, + } + + nodes := []Node{KyvernoPolicyResourceBuilder(p).node, GenericResourceBuilder(ns).node} + nodeStore := BuildFakeNodeStore(nodes) + + var pr PolicyReport + UnmarshalFile("kyverno-clusterpolicyreport.json", &pr, t) + + edges := PolicyReportResourceBuilder(&pr).BuildEdges(nodeStore) + + if len(edges) != 3 { + t.Fatalf("Expected two edges but got %d", len(edges)) + } + + edge := edges[0] + expectedEdge := Edge{ + SourceUID: "local-cluster/132ec5b8-892b-40da-8b92-af141c377dfe", + SourceKind: "ClusterPolicy", + EdgeType: "reports", + DestUID: "local-cluster/509de4c9-ed73-4309-9764-c88334781eae", + DestKind: "ClusterPolicyReport", + } + AssertDeepEqual("edge", edge, expectedEdge, t) + + edge2 := edges[1] + expectedEdge2 := Edge{ + SourceUID: "local-cluster/132ec5b8-892b-40da-8b92-af141c377dfe", + SourceKind: "ClusterPolicy", + EdgeType: "appliesTo", + DestUID: "local-cluster/13967788-e842-4662-9630-a9e9c39fa199", + DestKind: "Namespace", + } + AssertDeepEqual("edge", edge2, expectedEdge2, t) + + edge3 := edges[2] + expectedEdge3 := Edge{ + SourceUID: "local-cluster/509de4c9-ed73-4309-9764-c88334781eae", + SourceKind: "ClusterPolicyReport", + EdgeType: "reportsOn", + DestUID: "local-cluster/13967788-e842-4662-9630-a9e9c39fa199", + DestKind: "Namespace", + } + AssertDeepEqual("edge", edge3, expectedEdge3, t) +} + +func TestKyvernoPolicyReportBuildEdges(t *testing.T) { + policy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kyverno.io/v1", + "kind": "Policy", + "metadata": map[string]interface{}{ + "name": "require-kubecost-labels", + "namespace": "open-cluster-management-agent-addon", + "annotations": map[string]interface{}{ + "policies.kyverno.io/severity": "medium", + }, + "uid": "132ec5b8-892b-40da-8b92-af141c377dfe", + }, + "spec": map[string]interface{}{ + "validationFailureAction": "deny", + "random": "value", + }, + }, + } + + clusterPolicy := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": map[string]interface{}{ + "name": "require-kubecost-labels", + "annotations": map[string]interface{}{ + "policies.kyverno.io/severity": "medium", + }, + "uid": "162ec5b8-892b-40da-8b92-af141c377ddd", + }, + "spec": map[string]interface{}{ + "validationFailureAction": "deny", + "random": "value", + }, + }, + } + + violatingPod := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "require-kubecost-labels", + "namespace": "open-cluster-management-agent-addon", + "uid": "019db722-d5c7-4085-9778-d8ebc33e95f2", + }, + }, + } + + nodes := []Node{ + KyvernoPolicyResourceBuilder(policy).node, + KyvernoPolicyResourceBuilder(clusterPolicy).node, + GenericResourceBuilder(violatingPod).node, + } + nodeStore := BuildFakeNodeStore(nodes) + + var pr PolicyReport + UnmarshalFile("kyverno-policyreport.json", &pr, t) + + // Test a PolicyReport generated by a Policy kind + edges := PolicyReportResourceBuilder(&pr).BuildEdges(nodeStore) + + if len(edges) != 6 { + t.Fatalf("Expected 6 edges but got %d", len(edges)) + } + + sort.Slice(edges, func(i, j int) bool { + if edges[i].SourceUID == edges[j].SourceUID { + return edges[i].DestUID < edges[j].DestUID + } + + return edges[i].SourceUID < edges[j].SourceUID + }) + + edge1 := edges[0] + expectedEdge1 := Edge{ + SourceUID: "local-cluster/132ec5b8-892b-40da-8b92-af141c377dfe", + SourceKind: "Policy", + EdgeType: "appliesTo", + DestUID: "local-cluster/019db722-d5c7-4085-9778-d8ebc33e95f2", + DestKind: "Pod", + } + AssertDeepEqual("edge", edge1, expectedEdge1, t) + + edge2 := edges[1] + expectedEdge2 := Edge{ + SourceUID: "local-cluster/132ec5b8-892b-40da-8b92-af141c377dfe", + SourceKind: "Policy", + EdgeType: "reports", + DestUID: "local-cluster/53cd0e2e-34e0-454b-a0c4-e4dbf9306470", + DestKind: "PolicyReport", + } + AssertDeepEqual("edge", edge2, expectedEdge2, t) + + edge3 := edges[2] + expectedEdge3 := Edge{ + SourceUID: "local-cluster/162ec5b8-892b-40da-8b92-af141c377ddd", + SourceKind: "ClusterPolicy", + EdgeType: "appliesTo", + DestUID: "local-cluster/019db722-d5c7-4085-9778-d8ebc33e95f2", + DestKind: "Pod", + } + AssertDeepEqual("edge", edge3, expectedEdge3, t) + + edge4 := edges[3] + expectedEdge4 := Edge{ + SourceUID: "local-cluster/162ec5b8-892b-40da-8b92-af141c377ddd", + SourceKind: "ClusterPolicy", + EdgeType: "reports", + DestUID: "local-cluster/53cd0e2e-34e0-454b-a0c4-e4dbf9306470", + DestKind: "PolicyReport", + } + AssertDeepEqual("edge", edge4, expectedEdge4, t) + + edge5 := edges[4] + expectedEdge5 := Edge{ + SourceUID: "local-cluster/53cd0e2e-34e0-454b-a0c4-e4dbf9306470", + SourceKind: "PolicyReport", + EdgeType: "reportsOn", + DestUID: "local-cluster/019db722-d5c7-4085-9778-d8ebc33e95f2", + DestKind: "Pod", + } + AssertDeepEqual("edge", edge5, expectedEdge5, t) + + edge6 := edges[5] + expectedEdge6 := Edge{ + SourceUID: "local-cluster/53cd0e2e-34e0-454b-a0c4-e4dbf9306470", + SourceKind: "PolicyReport", + EdgeType: "reportsOn", + DestUID: "local-cluster/019db722-d5c7-4085-9778-d8ebc33e95f2", + DestKind: "Pod", + } + AssertDeepEqual("edge", edge6, expectedEdge6, t) } func TestPolicyReportBuildEdges(t *testing.T) { diff --git a/pkg/transforms/testutils.go b/pkg/transforms/testutils.go index 479b146a..98902c4a 100644 --- a/pkg/transforms/testutils.go +++ b/pkg/transforms/testutils.go @@ -61,13 +61,19 @@ func BuildFakeNodeStore(nodes []Node) NodeStore { for _, n := range nodes { byUID[n.UID] = n kind := n.Properties["kind"].(string) - var namespace = "_NONE" + namespace := "_NONE" if n.Properties["namespace"] != nil { namespace = n.Properties["namespace"].(string) } - byKindNameNamespace[kind] = make(map[string]map[string]Node) - byKindNameNamespace[kind][namespace] = make(map[string]Node) + if byKindNameNamespace[kind] == nil { + byKindNameNamespace[kind] = map[string]map[string]Node{} + } + + if byKindNameNamespace[kind][namespace] == nil { + byKindNameNamespace[kind][namespace] = map[string]Node{} + } + byKindNameNamespace[kind][namespace][n.Properties["name"].(string)] = n } diff --git a/pkg/transforms/transformer.go b/pkg/transforms/transformer.go index 5e24eff9..addf93a2 100644 --- a/pkg/transforms/transformer.go +++ b/pkg/transforms/transformer.go @@ -403,7 +403,7 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { } trans = SubscriptionResourceBuilder(&typedResource) - case [2]string{"PolicyReport", "wgpolicyk8s.io"}: + case [2]string{"PolicyReport", "wgpolicyk8s.io"}, [2]string{"ClusterPolicyReport", "wgpolicyk8s.io"}: typedResource := PolicyReport{} err := runtime.DefaultUnstructuredConverter. FromUnstructured(event.Resource.UnstructuredContent(), &typedResource) @@ -415,6 +415,9 @@ func TransformRoutine(input chan *Event, output chan NodeEvent) { case [2]string{"ValidatingAdmissionPolicyBinding", "admissionregistration.k8s.io"}: trans = VapBindingResourceBuilder(event.Resource) + case [2]string{"Policy", "kyverno.io"}, [2]string{"ClusterPolicy", "kyverno.io"}: + trans = KyvernoPolicyResourceBuilder(event.Resource) + default: generic := GenericResourceBuilder(event.Resource, event.AdditionalPrinterColumns...) diff --git a/test-data/kyverno-clusterpolicyreport.json b/test-data/kyverno-clusterpolicyreport.json new file mode 100644 index 00000000..e3abe680 --- /dev/null +++ b/test-data/kyverno-clusterpolicyreport.json @@ -0,0 +1,79 @@ +{ + "apiVersion": "wgpolicyk8s.io/v1alpha2", + "kind": "ClusterPolicyReport", + "metadata": { + "creationTimestamp": "2024-10-24T19:21:36Z", + "generation": 2, + "labels": { + "app.kubernetes.io/managed-by": "kyverno" + }, + "name": "13967788-e842-4662-9630-a9e9c39fa199", + "ownerReferences": [ + { + "apiVersion": "v1", + "kind": "Namespace", + "name": "kube-public", + "uid": "13967788-e842-4662-9630-a9e9c39fa199" + } + ], + "resourceVersion": "1540000", + "uid": "509de4c9-ed73-4309-9764-c88334781eae" + }, + "results": [ + { + "category": "Kubecost", + "message": "validation error: The Kubecost label `owner` is required for Namespaces. rule require-labels failed at path /metadata/labels/owner/", + "policy": "require-kubecost-labels", + "result": "fail", + "rule": "require-labels", + "scored": true, + "severity": "medium", + "source": "kyverno", + "timestamp": { + "nanos": 0, + "seconds": 1729797707 + } + }, + { + "category": "Kubecost", + "message": "Some error", + "policy": "require-kubecost-labels", + "result": "error", + "rule": "require-labels", + "scored": true, + "severity": "medium", + "source": "kyverno", + "timestamp": { + "nanos": 0, + "seconds": 1729797707 + } + }, + { + "category": "Kubecost", + "message": "LGTM", + "policy": "no-label-of-monkey", + "result": "pass", + "rule": "no-monkey", + "scored": true, + "severity": "medium", + "source": "kyverno", + "timestamp": { + "nanos": 0, + "seconds": 1729797707 + } + } + ], + "scope": { + "apiVersion": "v1", + "kind": "Namespace", + "name": "kube-public", + "uid": "13967788-e842-4662-9630-a9e9c39fa199" + }, + "summary": { + "error": 1, + "fail": 1, + "pass": 0, + "skip": 0, + "warn": 0 + } +} diff --git a/test-data/kyverno-policyreport.json b/test-data/kyverno-policyreport.json new file mode 100644 index 00000000..6ba6b308 --- /dev/null +++ b/test-data/kyverno-policyreport.json @@ -0,0 +1,67 @@ +{ + "apiVersion": "wgpolicyk8s.io/v1beta1", + "kind": "PolicyReport", + "metadata": { + "creationTimestamp": "2024-10-24T18:49:30Z", + "generation": 3, + "labels": { + "app.kubernetes.io/managed-by": "kyverno" + }, + "name": "019db722-d5c7-4085-9778-d8ebc33e95f2", + "namespace": "open-cluster-management-agent-addon", + "ownerReferences": [ + { + "apiVersion": "v1", + "kind": "Pod", + "name": "hypershift-install-job-9ct24-46dq9", + "uid": "019db722-d5c7-4085-9778-d8ebc33e95f2" + } + ], + "resourceVersion": "1539107", + "uid": "53cd0e2e-34e0-454b-a0c4-e4dbf9306470" + }, + "results": [ + { + "category": "Kubecost", + "message": "validation error: The Kubecost labels `owner`, `team`, `department`, `app`, and `env` are all required for Namespaces. rule require-labels failed at path /metadata/labels/app/", + "policy": "open-cluster-management-agent-addon/require-kubecost-labels", + "result": "fail", + "rule": "require-labels", + "scored": true, + "severity": "medium", + "source": "kyverno", + "timestamp": { + "nanos": 0, + "seconds": 1729797680 + } + }, + { + "category": "Kubecost", + "message": "validation error: The Kubecost label `owner` is required for Namespaces. rule require-labels failed at path /metadata/labels/owner/", + "policy": "require-kubecost-labels", + "result": "fail", + "rule": "require-labels", + "scored": true, + "severity": "medium", + "source": "kyverno", + "timestamp": { + "nanos": 0, + "seconds": 1729795789 + } + } + ], + "scope": { + "apiVersion": "v1", + "kind": "Pod", + "name": "hypershift-install-job-9ct24-46dq9", + "namespace": "open-cluster-management-agent-addon", + "uid": "019db722-d5c7-4085-9778-d8ebc33e95f2" + }, + "summary": { + "error": 0, + "fail": 2, + "pass": 0, + "skip": 0, + "warn": 0 + } +}