diff --git a/charts/policy-reporter/config.yaml b/charts/policy-reporter/config.yaml index e1fac982..c7729bb4 100644 --- a/charts/policy-reporter/config.yaml +++ b/charts/policy-reporter/config.yaml @@ -385,6 +385,12 @@ redis: {{- toYaml . | nindent 2 }} {{- end }} +{{- with .Values.sourceConfig }} +sourceConfig: + {{- toYaml . | nindent 2 }} +{{- end }} + + logging: encoding: {{ .Values.logging.encoding }} logLevel: {{ include "policyreporter.logLevel" . }} diff --git a/charts/policy-reporter/values.yaml b/charts/policy-reporter/values.yaml index 096c6401..06e6c600 100644 --- a/charts/policy-reporter/values.yaml +++ b/charts/policy-reporter/values.yaml @@ -163,6 +163,13 @@ reportFilter: # Disable the processing of ClusterPolicyReports disabled: false +# customize source specific logic like result ID generation +sourceConfig: {} +# sourcename: +# customID: +# enabled: true +# fields: ["resource", "policy", "rule", "category", "result", "message"] + # enable policy-report-ui ui: enabled: false diff --git a/pkg/config/config.go b/pkg/config/config.go index 20c6dcf3..930dbfbc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -347,34 +347,44 @@ type Database struct { MountedSecret string `mapstructure:"mountedSecret"` } +type CustomID struct { + Enabled bool `mapstructure:"enabled"` + Fields []string `mapstructure:"fields"` +} + +type SourceConfig struct { + CustomID `mapstructure:"customID"` +} + // Config of the PolicyReporter type Config struct { Version string - Namespace string `mapstructure:"namespace"` - Loki *Loki `mapstructure:"loki"` - Elasticsearch *Elasticsearch `mapstructure:"elasticsearch"` - Slack *Slack `mapstructure:"slack"` - Discord *Discord `mapstructure:"discord"` - Teams *Teams `mapstructure:"teams"` - S3 *S3 `mapstructure:"s3"` - Kinesis *Kinesis `mapstructure:"kinesis"` - SecurityHub *SecurityHub `mapstructure:"securityHub"` - GCS *GCS `mapstructure:"gcs"` - UI *UI `mapstructure:"ui"` - Webhook *Webhook `mapstructure:"webhook"` - Telegram *Telegram `mapstructure:"telegram"` - GoogleChat *GoogleChat `mapstructure:"googleChat"` - API API `mapstructure:"api"` - WorkerCount int `mapstructure:"worker"` - DBFile string `mapstructure:"dbfile"` - Metrics Metrics `mapstructure:"metrics"` - REST REST `mapstructure:"rest"` - ReportFilter ReportFilter `mapstructure:"reportFilter"` - Redis Redis `mapstructure:"redis"` - Profiling Profiling `mapstructure:"profiling"` - EmailReports EmailReports `mapstructure:"emailReports"` - LeaderElection LeaderElection `mapstructure:"leaderElection"` - K8sClient K8sClient `mapstructure:"k8sClient"` - Logging Logging `mapstructure:"logging"` - Database Database `mapstructure:"database"` + Namespace string `mapstructure:"namespace"` + Loki *Loki `mapstructure:"loki"` + Elasticsearch *Elasticsearch `mapstructure:"elasticsearch"` + Slack *Slack `mapstructure:"slack"` + Discord *Discord `mapstructure:"discord"` + Teams *Teams `mapstructure:"teams"` + S3 *S3 `mapstructure:"s3"` + Kinesis *Kinesis `mapstructure:"kinesis"` + SecurityHub *SecurityHub `mapstructure:"securityHub"` + GCS *GCS `mapstructure:"gcs"` + UI *UI `mapstructure:"ui"` + Webhook *Webhook `mapstructure:"webhook"` + Telegram *Telegram `mapstructure:"telegram"` + GoogleChat *GoogleChat `mapstructure:"googleChat"` + API API `mapstructure:"api"` + WorkerCount int `mapstructure:"worker"` + DBFile string `mapstructure:"dbfile"` + Metrics Metrics `mapstructure:"metrics"` + REST REST `mapstructure:"rest"` + ReportFilter ReportFilter `mapstructure:"reportFilter"` + Redis Redis `mapstructure:"redis"` + Profiling Profiling `mapstructure:"profiling"` + EmailReports EmailReports `mapstructure:"emailReports"` + LeaderElection LeaderElection `mapstructure:"leaderElection"` + K8sClient K8sClient `mapstructure:"k8sClient"` + Logging Logging `mapstructure:"logging"` + Database Database `mapstructure:"database"` + SourceConfig map[string]SourceConfig `mapstructure:"sourceConfig"` } diff --git a/pkg/config/resolver.go b/pkg/config/resolver.go index 999a6791..38898769 100644 --- a/pkg/config/resolver.go +++ b/pkg/config/resolver.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "os" + "strings" "time" goredis "github.com/go-redis/redis/v8" @@ -33,6 +34,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/listener" "github.com/kyverno/policy-reporter/pkg/listener/metrics" "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" "github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/validate" ) @@ -173,6 +175,19 @@ func (r *Resolver) EventPublisher() report.EventPublisher { return r.publisher } +func (r *Resolver) CustomIDGenerators() map[string]result.IDGenerator { + generators := make(map[string]result.IDGenerator) + for s, c := range r.config.SourceConfig { + if !c.Enabled || len(c.Fields) == 0 { + continue + } + + generators[strings.ToLower(s)] = result.NewIDGenerator(c.Fields) + } + + return generators +} + // EventPublisher resolver method func (r *Resolver) Queue() (*kubernetes.Queue, error) { client, err := r.CRDClient() @@ -184,6 +199,7 @@ func (r *Resolver) Queue() (*kubernetes.Queue, error) { kubernetes.NewDebouncer(1*time.Minute, r.EventPublisher()), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "report-queue"), client, + result.NewReconditioner(r.CustomIDGenerators()), ), nil } diff --git a/pkg/config/resolver_test.go b/pkg/config/resolver_test.go index ed753e45..5de24e9a 100644 --- a/pkg/config/resolver_test.go +++ b/pkg/config/resolver_test.go @@ -190,6 +190,15 @@ var testConfig = &config.Config{ Webhook: "http://localhost:900/webhook", Channels: []*config.GoogleChat{{}}, }, + SourceConfig: map[string]config.SourceConfig{ + "test": { + CustomID: config.CustomID{ + Enabled: true, + Fields: []string{"resource"}, + }, + }, + "default": {}, + }, } func Test_ResolveTargets(t *testing.T) { @@ -580,3 +589,12 @@ func Test_ResolveEnableLeaderElection(t *testing.T) { } }) } + +func Test_ResolveCustomIDGenerators(t *testing.T) { + resolver := config.NewResolver(testConfig, nil) + + generators := resolver.CustomIDGenerators() + if len(generators) != 1 { + t.Error("only enabled custom id config should be mapped") + } +} diff --git a/pkg/crd/api/policyreport/v1alpha2/common.go b/pkg/crd/api/policyreport/v1alpha2/common.go index f041780a..1cb560e3 100644 --- a/pkg/crd/api/policyreport/v1alpha2/common.go +++ b/pkg/crd/api/policyreport/v1alpha2/common.go @@ -16,10 +16,8 @@ package v1alpha2 import ( "bytes" "fmt" - "strconv" "strings" - "github.com/segmentio/fasthash/fnv1a" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -243,32 +241,6 @@ func (r *PolicyReportResult) GetKind() string { } func (r *PolicyReportResult) GetID() string { - if r.ID != "" { - return r.ID - } - - if id, ok := r.Properties[ResultIDKey]; ok { - r.ID = id - - return r.ID - } - - h1 := fnv1a.Init64 - - res := r.GetResource() - if res != nil { - h1 = fnv1a.AddString64(h1, res.Name) - h1 = fnv1a.AddString64(h1, string(res.UID)) - } - - h1 = fnv1a.AddString64(h1, r.Policy) - h1 = fnv1a.AddString64(h1, r.Rule) - h1 = fnv1a.AddString64(h1, string(r.Result)) - h1 = fnv1a.AddString64(h1, r.Category) - h1 = fnv1a.AddString64(h1, r.Message) - - r.ID = strconv.FormatUint(h1, 10) - return r.ID } diff --git a/pkg/crd/api/policyreport/v1alpha2/common_test.go b/pkg/crd/api/policyreport/v1alpha2/common_test.go index 7fd31f01..37a0be2f 100644 --- a/pkg/crd/api/policyreport/v1alpha2/common_test.go +++ b/pkg/crd/api/policyreport/v1alpha2/common_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" corev1 "k8s.io/api/core/v1" ) @@ -98,7 +99,7 @@ func TestCommon(t *testing.T) { }) } -func TestPolicyReportResul(t *testing.T) { +func TestPolicyReportResult(t *testing.T) { t.Run("GetResource Without Resources", func(t *testing.T) { r := &v1alpha2.PolicyReportResult{} @@ -128,31 +129,20 @@ func TestPolicyReportResul(t *testing.T) { } }) t.Run("GetID from Result With Resource", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}} + r := v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}} + r.ID = result.NewIDGenerator(nil).Generate(&v1alpha2.PolicyReport{}, r) if r.GetID() != "18007334074686647077" { t.Errorf("expected result kind to be '18007334074686647077', got :%s", r.GetID()) } }) t.Run("GetID from Result With ID Property", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}, Properties: map[string]string{"resultID": "result-id"}} - - if r.GetID() != "result-id" { - t.Errorf("expected result kind to be 'result-id', got :%s", r.GetID()) - } - }) - t.Run("GetID cached", func(t *testing.T) { - r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}, Properties: map[string]string{"resultID": "result-id"}} + r := v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}, Properties: map[string]string{"resultID": "result-id"}} + r.ID = result.NewIDGenerator(nil).Generate(&v1alpha2.PolicyReport{}, r) if r.GetID() != "result-id" { t.Errorf("expected result kind to be 'result-id', got :%s", r.GetID()) } - - r.Properties["resultID"] = "test" - - if r.GetID() != "result-id" { - t.Errorf("expected result ID doesn't change, got :%s", r.GetID()) - } }) t.Run("ToResourceString with Namespace and Kind", func(t *testing.T) { r := &v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Namespace: "default", Kind: "Pod"}}} diff --git a/pkg/database/model.go b/pkg/database/model.go index 121cd864..4e195721 100644 --- a/pkg/database/model.go +++ b/pkg/database/model.go @@ -5,11 +5,11 @@ import ( "github.com/segmentio/fasthash/fnv1a" "github.com/uptrace/bun" - corev1 "k8s.io/api/core/v1" api "github.com/kyverno/policy-reporter/pkg/api/v1" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" ) type Config struct { @@ -109,13 +109,8 @@ func MapPolicyReport(r v1alpha2.ReportInterface) *PolicyReport { func MapPolicyReportResults(polr v1alpha2.ReportInterface) []*PolicyReportResult { list := make([]*PolicyReportResult, 0, len(polr.GetResults())) - for _, result := range polr.GetResults() { - res := result.GetResource() - if res == nil && polr.GetScope() != nil { - res = polr.GetScope() - } else if res == nil { - res = &corev1.ObjectReference{} - } + for _, r := range polr.GetResults() { + res := result.Resource(polr, r) ns := res.Namespace if ns == "" { @@ -123,7 +118,7 @@ func MapPolicyReportResults(polr v1alpha2.ReportInterface) []*PolicyReportResult } list = append(list, &PolicyReportResult{ - ID: result.GetID(), + ID: r.GetID(), PolicyReportID: polr.GetID(), Resource: Resource{ APIVersion: res.APIVersion, @@ -132,16 +127,16 @@ func MapPolicyReportResults(polr v1alpha2.ReportInterface) []*PolicyReportResult Namespace: ns, UID: string(res.UID), }, - Policy: result.Policy, - Rule: result.Rule, - Source: result.Source, - Scored: result.Scored, - Message: result.Message, - Result: string(result.Result), - Severity: string(result.Severity), - Category: result.Category, - Properties: result.Properties, - Created: result.Timestamp.Seconds, + Policy: r.Policy, + Rule: r.Rule, + Source: r.Source, + Scored: r.Scored, + Message: r.Message, + Result: string(r.Result), + Severity: string(r.Severity), + Category: r.Category, + Properties: r.Properties, + Created: r.Timestamp.Seconds, }) } diff --git a/pkg/fixtures/policy_reports.go b/pkg/fixtures/policy_reports.go index 0e2d1441..398747bc 100644 --- a/pkg/fixtures/policy_reports.go +++ b/pkg/fixtures/policy_reports.go @@ -32,6 +32,7 @@ var DefaultPolicyReport = &v1alpha2.PolicyReport{ }, Results: []v1alpha2.PolicyReportResult{ { + ID: "12348", Message: "message", Result: v1alpha2.StatusFail, Scored: true, @@ -53,6 +54,7 @@ var DefaultPolicyReport = &v1alpha2.PolicyReport{ Properties: map[string]string{"version": "1.2.0"}, }, { + ID: "12346", Message: "message 2", Result: v1alpha2.StatusFail, Scored: true, @@ -60,6 +62,7 @@ var DefaultPolicyReport = &v1alpha2.PolicyReport{ Timestamp: v1.Timestamp{Seconds: 1614093000}, }, { + ID: "12347", Message: "message 3", Result: v1alpha2.StatusFail, Scored: true, diff --git a/pkg/helper/utils.go b/pkg/helper/utils.go index 065136d8..d8ade1f5 100644 --- a/pkg/helper/utils.go +++ b/pkg/helper/utils.go @@ -11,3 +11,11 @@ func Contains(source string, sources []string) bool { return false } + +func Defaults(s, f string) string { + if s != "" { + return s + } + + return f +} diff --git a/pkg/kubernetes/policy_report_client_test.go b/pkg/kubernetes/policy_report_client_test.go index b75c2c61..72e3d281 100644 --- a/pkg/kubernetes/policy_report_client_test.go +++ b/pkg/kubernetes/policy_report_client_test.go @@ -12,6 +12,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/fixtures" "github.com/kyverno/policy-reporter/pkg/kubernetes" "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" "github.com/kyverno/policy-reporter/pkg/validate" ) @@ -38,6 +39,7 @@ func Test_PolicyReportWatcher(t *testing.T) { kubernetes.NewDebouncer(0, publisher), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + result.NewReconditioner(nil), ) kclient, rclient, _ := NewFakeMetaClient() @@ -88,6 +90,7 @@ func Test_ClusterPolicyReportWatcher(t *testing.T) { kubernetes.NewDebouncer(0, publisher), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + result.NewReconditioner(nil), ) kclient, _, rclient := NewFakeMetaClient() @@ -128,6 +131,7 @@ func Test_HasSynced(t *testing.T) { kubernetes.NewDebouncer(0, report.NewEventPublisher()), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + result.NewReconditioner(nil), ) kclient, _, _ := NewFakeMetaClient() diff --git a/pkg/kubernetes/queue.go b/pkg/kubernetes/queue.go index f77ca07b..e1b81aa9 100644 --- a/pkg/kubernetes/queue.go +++ b/pkg/kubernetes/queue.go @@ -17,14 +17,16 @@ import ( pr "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/crd/client/clientset/versioned/typed/policyreport/v1alpha2" "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" ) type Queue struct { - queue workqueue.RateLimitingInterface - client v1alpha2.Wgpolicyk8sV1alpha2Interface - debouncer Debouncer - lock *sync.Mutex - cache sets.Set[string] + queue workqueue.RateLimitingInterface + client v1alpha2.Wgpolicyk8sV1alpha2Interface + reconditioner *result.Reconditioner + debouncer Debouncer + lock *sync.Mutex + cache sets.Set[string] } func (q *Queue) Add(obj *v1.PartialObjectMetadata) error { @@ -96,7 +98,7 @@ func (q *Queue) processNextItem() bool { defer q.lock.Unlock() q.cache.Delete(key) }() - q.debouncer.Add(report.LifecycleEvent{Type: report.Deleted, PolicyReport: updateResults(polr)}) + q.debouncer.Add(report.LifecycleEvent{Type: report.Deleted, PolicyReport: q.reconditioner.Prepare(polr)}) return true } @@ -115,29 +117,11 @@ func (q *Queue) processNextItem() bool { q.handleErr(err, key) - q.debouncer.Add(report.LifecycleEvent{Type: event, PolicyReport: updateResults(polr)}) + q.debouncer.Add(report.LifecycleEvent{Type: event, PolicyReport: q.reconditioner.Prepare(polr)}) return true } -// each result needs to know its resource it belongs to, to generate internal unique IDs -func updateResults(polr pr.ReportInterface) pr.ReportInterface { - results := polr.GetResults() - for i, r := range results { - scope := polr.GetScope() - - if len(r.Resources) == 0 && scope != nil { - r.Resources = append(r.Resources, *scope) - } - - r.Priority = report.ResolvePriority(r) - - results[i] = r - } - - return polr -} - func (q *Queue) handleErr(err error, key interface{}) { if err == nil { q.queue.Forget(key) @@ -157,12 +141,18 @@ func (q *Queue) handleErr(err error, key interface{}) { zap.L().Warn("dropping report out of queue", zap.Any("key", key), zap.Error(err)) } -func NewQueue(debouncer Debouncer, queue workqueue.RateLimitingInterface, client v1alpha2.Wgpolicyk8sV1alpha2Interface) *Queue { +func NewQueue( + debouncer Debouncer, + queue workqueue.RateLimitingInterface, + client v1alpha2.Wgpolicyk8sV1alpha2Interface, + reconditioner *result.Reconditioner, +) *Queue { return &Queue{ - debouncer: debouncer, - queue: queue, - client: client, - cache: sets.New[string](), - lock: &sync.Mutex{}, + debouncer: debouncer, + queue: queue, + client: client, + cache: sets.New[string](), + lock: &sync.Mutex{}, + reconditioner: reconditioner, } } diff --git a/pkg/report/result/id_generator.go b/pkg/report/result/id_generator.go new file mode 100644 index 00000000..77d91f0a --- /dev/null +++ b/pkg/report/result/id_generator.go @@ -0,0 +1,165 @@ +package result + +import ( + "strconv" + "strings" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/segmentio/fasthash/fnv1a" + corev1 "k8s.io/api/core/v1" +) + +type FieldMapperFunc = func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 + +type IDGenerator interface { + Generate(polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) string +} + +var fieldMapper = map[string]FieldMapperFunc{ + "resource": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + var resource *corev1.ObjectReference + + if res.HasResource() { + resource = res.GetResource() + } else if polr.GetScope() != nil { + resource = polr.GetScope() + } + + if resource != nil { + h1 = fnv1a.AddString64(h1, string(resource.UID)) + h1 = fnv1a.AddString64(h1, string(resource.Name)) + } + + return h1 + }, + "namespace": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, polr.GetNamespace()) + }, + "policy": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, res.Policy) + }, + "rule": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, res.Rule) + }, + "result": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, string(res.Result)) + }, + "category": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, res.Category) + }, + "message": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, res.Message) + }, + "created": func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + return fnv1a.AddString64(h1, res.Timestamp.String()) + }, +} + +var ( + propertyResolver = func(field string) FieldMapperFunc { + name := strings.TrimPrefix(field, "property:") + + return func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + if prop, ok := res.Properties[name]; ok { + h1 = fnv1a.AddString64(h1, prop) + } + + return h1 + } + } + + labelResolver = func(field string) FieldMapperFunc { + name := strings.TrimPrefix(field, "label:") + + return func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + if prop, ok := polr.GetLabels()[name]; ok { + h1 = fnv1a.AddString64(h1, prop) + } + + return h1 + } + } + + annotationResolver = func(field string) FieldMapperFunc { + name := strings.TrimPrefix(field, "annotation:") + + return func(h1 uint64, polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) uint64 { + if prop, ok := polr.GetAnnotations()[name]; ok { + h1 = fnv1a.AddString64(h1, prop) + } + + return h1 + } + } +) + +type customIDGenerator struct { + mappings []FieldMapperFunc +} + +func (g *customIDGenerator) Generate(polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) string { + if id, ok := res.Properties[v1alpha2.ResultIDKey]; ok { + return id + } + + h1 := fnv1a.Init64 + for _, mapping := range g.mappings { + h1 = mapping(h1, polr, res) + } + + return strconv.FormatUint(h1, 10) +} + +type defaultIDGenerator struct{} + +func (g *defaultIDGenerator) Generate(polr v1alpha2.ReportInterface, res v1alpha2.PolicyReportResult) string { + if id, ok := res.Properties[v1alpha2.ResultIDKey]; ok { + return id + } + + h1 := fnv1a.Init64 + + resource := polr.GetScope() + if resource == nil { + resource = res.GetResource() + } + + if resource != nil { + h1 = fnv1a.AddString64(h1, resource.Name) + h1 = fnv1a.AddString64(h1, string(resource.UID)) + } + + h1 = fnv1a.AddString64(h1, res.Policy) + h1 = fnv1a.AddString64(h1, res.Rule) + h1 = fnv1a.AddString64(h1, string(res.Result)) + h1 = fnv1a.AddString64(h1, res.Category) + h1 = fnv1a.AddString64(h1, res.Message) + + return strconv.FormatUint(h1, 10) +} + +func NewIDGenerator(config []string) IDGenerator { + if len(config) == 0 { + return &defaultIDGenerator{} + } + + mappings := make([]FieldMapperFunc, 0, len(config)) + for _, field := range config { + if strings.HasPrefix(field, "property:") { + mappings = append(mappings, propertyResolver(field)) + continue + } + if strings.HasPrefix(field, "label:") { + mappings = append(mappings, labelResolver(field)) + continue + } + if strings.HasPrefix(field, "annotation:") { + mappings = append(mappings, annotationResolver(field)) + continue + } + + mappings = append(mappings, fieldMapper[field]) + } + + return &customIDGenerator{mappings} +} diff --git a/pkg/report/result/id_generator_test.go b/pkg/report/result/id_generator_test.go new file mode 100644 index 00000000..0bb8bbad --- /dev/null +++ b/pkg/report/result/id_generator_test.go @@ -0,0 +1,180 @@ +package result_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDefaultGenerator(t *testing.T) { + generator := result.NewIDGenerator(nil) + + t.Run("ID From Property", func(t *testing.T) { + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Properties: map[string]string{"resultID": "12345"}}) + + if id != "12345" { + t.Errorf("expected result id to be '12345', got :%s", id) + } + }) + + t.Run("ID From Resource", func(t *testing.T) { + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}}) + + if id != "18007334074686647077" { + t.Errorf("expected result id to be '18007334074686647077', got :%s", id) + } + }) + + t.Run("ID From Scope", func(t *testing.T) { + id := generator.Generate(&v1alpha2.PolicyReport{Scope: &corev1.ObjectReference{Name: "test", Kind: "Pod"}}, v1alpha2.PolicyReportResult{}) + + if id != "18007334074686647077" { + t.Errorf("expected result id to be '18007334074686647077', got :%s", id) + } + }) +} + +func TestCustomGenerator(t *testing.T) { + t.Run("ID From Property", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"resource"}) + + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Properties: map[string]string{"resultID": "12345"}}) + + if id != "12345" { + t.Errorf("expected result id to be '12345', got :%s", id) + } + }) + + t.Run("ID From Resource", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"resource"}) + + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{{Name: "test", Kind: "Pod"}}}) + + if id != "18007334074686647077" { + t.Errorf("expected result id to be '18007334074686647077', got :%s", id) + } + }) + + t.Run("ID From Scope", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"resource"}) + + id := generator.Generate(&v1alpha2.PolicyReport{Scope: &corev1.ObjectReference{Name: "test", Kind: "Pod"}}, v1alpha2.PolicyReportResult{}) + + if id != "18007334074686647077" { + t.Errorf("expected result id to be '18007334074686647077', got :%s", id) + } + }) + + t.Run("ID From Namespace", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"namespace"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{ObjectMeta: v1.ObjectMeta{Namespace: ""}}, v1alpha2.PolicyReportResult{Message: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{ObjectMeta: v1.ObjectMeta{Namespace: "test"}}, v1alpha2.PolicyReportResult{Message: ""}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Policy", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"policy"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Policy: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Policy: "test"}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Rule", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"rule"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Rule: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Rule: "test"}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Result", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"result"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Result: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Result: "fail"}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Category", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"category"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Category: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Category: "test"}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Message", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"message"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Message: ""}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Message: "test"}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Created", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"created"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Timestamp: v1.Timestamp{Seconds: 0}}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Timestamp: v1.Timestamp{Seconds: 1714641964}}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Property", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"property:id"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{}) + id := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Properties: map[string]string{"id": "1234"}}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Label", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"label:id"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{}) + id := generator.Generate(&v1alpha2.PolicyReport{ObjectMeta: v1.ObjectMeta{Labels: map[string]string{"id": "1234"}}}, v1alpha2.PolicyReportResult{}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) + + t.Run("ID From Annotation", func(t *testing.T) { + generator := result.NewIDGenerator([]string{"annotation:id"}) + + empty := generator.Generate(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{}) + id := generator.Generate(&v1alpha2.PolicyReport{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"id": "1234"}}}, v1alpha2.PolicyReportResult{}) + + if id == empty { + t.Errorf("expected result id different from empty %s, got :%s", empty, id) + } + }) +} diff --git a/pkg/report/mapper.go b/pkg/report/result/mapper.go similarity index 97% rename from pkg/report/mapper.go rename to pkg/report/result/mapper.go index e77ee2da..b2290788 100644 --- a/pkg/report/mapper.go +++ b/pkg/report/result/mapper.go @@ -1,4 +1,4 @@ -package report +package result import ( "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" diff --git a/pkg/report/mapper_test.go b/pkg/report/result/mapper_test.go similarity index 77% rename from pkg/report/mapper_test.go rename to pkg/report/result/mapper_test.go index 8d2754f1..143acfb0 100644 --- a/pkg/report/mapper_test.go +++ b/pkg/report/result/mapper_test.go @@ -1,15 +1,15 @@ -package report_test +package result_test import ( "testing" "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/report/result" ) func Test_ResolvePriority(t *testing.T) { t.Run("Status Skip", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusSkip, Severity: v1alpha2.SeverityHigh, }) @@ -20,7 +20,7 @@ func Test_ResolvePriority(t *testing.T) { }) t.Run("Status Pass", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusPass, Severity: v1alpha2.SeverityHigh, }) @@ -31,7 +31,7 @@ func Test_ResolvePriority(t *testing.T) { }) t.Run("Status Warning", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusWarn, Severity: v1alpha2.SeverityHigh, }) @@ -42,7 +42,7 @@ func Test_ResolvePriority(t *testing.T) { }) t.Run("Status Error", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusError, Severity: v1alpha2.SeverityHigh, }) @@ -53,7 +53,7 @@ func Test_ResolvePriority(t *testing.T) { }) t.Run("Status Fail Fallback", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusFail, }) @@ -63,7 +63,7 @@ func Test_ResolvePriority(t *testing.T) { }) t.Run("Status Severity", func(t *testing.T) { - priority := report.ResolvePriority(v1alpha2.PolicyReportResult{ + priority := result.ResolvePriority(v1alpha2.PolicyReportResult{ Result: v1alpha2.StatusFail, Severity: v1alpha2.SeverityCritical, }) diff --git a/pkg/report/result/reconditioner.go b/pkg/report/result/reconditioner.go new file mode 100644 index 00000000..cb1c3ec2 --- /dev/null +++ b/pkg/report/result/reconditioner.go @@ -0,0 +1,43 @@ +package result + +import ( + "strings" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" +) + +type Reconditioner struct { + defaultIDGenerator IDGenerator + customIDGenerators map[string]IDGenerator +} + +func (r *Reconditioner) Prepare(polr v1alpha2.ReportInterface) v1alpha2.ReportInterface { + generator := r.defaultIDGenerator + if g, ok := r.customIDGenerators[strings.ToLower(polr.GetSource())]; ok { + generator = g + } + + results := polr.GetResults() + for i, r := range results { + r.ID = generator.Generate(polr, r) + r.Priority = ResolvePriority(r) + r.Category = helper.Defaults(r.Category, "Other") + + scope := polr.GetScope() + if len(r.Resources) == 0 && scope != nil { + r.Resources = append(r.Resources, *scope) + } + + results[i] = r + } + + return polr +} + +func NewReconditioner(generators map[string]IDGenerator) *Reconditioner { + return &Reconditioner{ + defaultIDGenerator: NewIDGenerator(nil), + customIDGenerators: generators, + } +} diff --git a/pkg/report/result/reconditioner_test.go b/pkg/report/result/reconditioner_test.go new file mode 100644 index 00000000..9ad52ff9 --- /dev/null +++ b/pkg/report/result/reconditioner_test.go @@ -0,0 +1,126 @@ +package result_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestReconditioner(t *testing.T) { + t.Run("prepare with default generator", func(t *testing.T) { + var report v1alpha2.ReportInterface = &v1alpha2.PolicyReport{ + ObjectMeta: v1.ObjectMeta{ + Name: "policy-report", + Namespace: "test", + }, + Summary: v1alpha2.PolicyReportSummary{ + Pass: 0, + Skip: 0, + Warn: 0, + Fail: 1, + Error: 0, + }, + Scope: &corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "test", + UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", + }, + Results: []v1alpha2.PolicyReportResult{ + { + ID: "12348", + Message: "message", + Result: v1alpha2.StatusFail, + Scored: true, + Policy: "required-label", + Rule: "app-label-required", + Timestamp: v1.Timestamp{Seconds: 1614093000}, + Source: "test", + Category: "", + Severity: v1alpha2.SeverityHigh, + Properties: map[string]string{"version": "1.2.0"}, + }, + }, + } + + rec := result.NewReconditioner(nil) + + report = rec.Prepare(report) + res := report.GetResults()[0] + + if res.ID != "1412073110812056002" { + t.Errorf("result id should be generated from default generator: %s", res.ID) + } + if res.Category != "Other" { + t.Error("result category should default to Other") + } + if res.Priority != v1alpha2.ErrorPriority { + t.Error("result prioriry should be mapped") + } + if len(res.Resources) == 0 || res.Resources[0] != *report.GetScope() { + t.Error("result resource should be mapped to scope") + } + }) + + t.Run("prepare with custom generator", func(t *testing.T) { + var report v1alpha2.ReportInterface = &v1alpha2.PolicyReport{ + ObjectMeta: v1.ObjectMeta{ + Name: "policy-report", + Namespace: "test", + }, + Summary: v1alpha2.PolicyReportSummary{ + Pass: 0, + Skip: 0, + Warn: 0, + Fail: 1, + Error: 0, + }, + Scope: &corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "test", + UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1", + }, + Results: []v1alpha2.PolicyReportResult{ + { + ID: "12348", + Message: "message", + Result: v1alpha2.StatusFail, + Scored: true, + Policy: "required-label", + Rule: "app-label-required", + Timestamp: v1.Timestamp{Seconds: 1614093000}, + Source: "test", + Category: "", + Severity: v1alpha2.SeverityHigh, + Properties: map[string]string{"version": "1.2.0"}, + }, + }, + } + + rec := result.NewReconditioner(map[string]result.IDGenerator{ + "test": result.NewIDGenerator([]string{"resource", "policy", "rule", "result"}), + }) + + report = rec.Prepare(report) + res := report.GetResults()[0] + + if res.ID != "12714703365089292087" { + t.Errorf("result id should be generated from custom generator: %s", res.ID) + } + if res.Category != "Other" { + t.Error("result category should default to Other") + } + if res.Priority != v1alpha2.ErrorPriority { + t.Error("result prioriry should be mapped") + } + if len(res.Resources) == 0 || res.Resources[0] != *report.GetScope() { + t.Error("result resource should be mapped to scope") + } + }) +} diff --git a/pkg/report/result/resource.go b/pkg/report/result/resource.go new file mode 100644 index 00000000..cfa40b2a --- /dev/null +++ b/pkg/report/result/resource.go @@ -0,0 +1,16 @@ +package result + +import ( + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + corev1 "k8s.io/api/core/v1" +) + +func Resource(p v1alpha2.ReportInterface, r v1alpha2.PolicyReportResult) *corev1.ObjectReference { + if r.HasResource() { + return r.GetResource() + } else if p.GetScope() != nil { + return p.GetScope() + } + + return &corev1.ObjectReference{} +} diff --git a/pkg/report/result/resource_test.go b/pkg/report/result/resource_test.go new file mode 100644 index 00000000..b1d9c2b0 --- /dev/null +++ b/pkg/report/result/resource_test.go @@ -0,0 +1,37 @@ +package result_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report/result" + corev1 "k8s.io/api/core/v1" +) + +func TestResource(t *testing.T) { + t.Run("resource from scope", func(t *testing.T) { + resource := &corev1.ObjectReference{Name: "test", Kind: "Pod"} + + res := result.Resource(&v1alpha2.PolicyReport{Scope: resource}, v1alpha2.PolicyReportResult{}) + + if res != resource { + t.Error("expected function to return scope resource") + } + }) + t.Run("resource from result", func(t *testing.T) { + resource := &corev1.ObjectReference{Name: "test", Kind: "Pod"} + + res := result.Resource(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{*resource}}) + + if res.Name != resource.Name { + t.Error("expected function to return result resource") + } + }) + t.Run("empty fallback resource", func(t *testing.T) { + res := result.Resource(&v1alpha2.PolicyReport{}, v1alpha2.PolicyReportResult{Resources: []corev1.ObjectReference{}}) + + if res == nil { + t.Error("expected function to return empty fallback resource") + } + }) +}