Skip to content

Commit

Permalink
Index Kyverno policies and policy reports (#327)
Browse files Browse the repository at this point in the history
PolicyReport indexing was previously focused on ACM policy results for
Insights. This now also accounts for PolicyReport and
ClusterPolicyReport objects generated by Kyverno by having the correct
numRuleViolations values and edges created between the report and the
Kyverno policy.

Relates:
https://issues.redhat.com/browse/ACM-15127

Signed-off-by: mprahl <[email protected]>
  • Loading branch information
mprahl authored Nov 6, 2024
1 parent dffe7e2 commit eb3ca4f
Show file tree
Hide file tree
Showing 9 changed files with 635 additions and 20 deletions.
2 changes: 1 addition & 1 deletion pkg/reconciler/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
54 changes: 54 additions & 0 deletions pkg/transforms/kyverno.go
Original file line number Diff line number Diff line change
@@ -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{}
}
71 changes: 71 additions & 0 deletions pkg/transforms/kyverno_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
115 changes: 100 additions & 15 deletions pkg/transforms/policyreport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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++
Expand All @@ -85,19 +100,33 @@ 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
node.Properties["moderate"] = moderate
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}
}

Expand All @@ -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
}
Loading

0 comments on commit eb3ca4f

Please sign in to comment.