From 03562cc379c5db344a2f8824c3f775634f64fcdc Mon Sep 17 00:00:00 2001 From: Frank Jogeleit Date: Mon, 29 Apr 2024 08:35:23 +0200 Subject: [PATCH] source filter Signed-off-by: Frank Jogeleit --- charts/policy-reporter/README.md | 45 ++++-- charts/policy-reporter/configs/core.tmpl | 5 + .../templates/clusterrole.yaml | 12 ++ charts/policy-reporter/values.yaml | 37 ++++- pkg/config/config.go | 14 ++ pkg/config/resolver.go | 53 ++++++- pkg/kubernetes/jobs/client.go | 46 ++++++ pkg/kubernetes/pods/client.go | 45 ++++++ pkg/kubernetes/policy_report_client.go | 4 +- pkg/kubernetes/policy_report_client_test.go | 5 +- pkg/kubernetes/queue.go | 8 +- pkg/report/filter.go | 77 --------- pkg/report/filter_test.go | 94 ----------- pkg/report/meta_filter.go | 26 +++ pkg/report/meta_filter_test.go | 60 +++++++ pkg/report/report_filter.go | 29 ++++ pkg/report/report_filter_test.go | 24 +++ pkg/report/result_filter.go | 31 ++++ pkg/report/result_filter_test.go | 25 +++ pkg/report/source_filter.go | 150 ++++++++++++++++++ pkg/target/http/logroundtripper_test.go | 3 +- pkg/target/http/utils_test.go | 6 +- pkg/validate/model.go | 4 + 23 files changed, 604 insertions(+), 199 deletions(-) create mode 100644 pkg/kubernetes/jobs/client.go create mode 100644 pkg/kubernetes/pods/client.go delete mode 100644 pkg/report/filter.go delete mode 100644 pkg/report/filter_test.go create mode 100644 pkg/report/meta_filter.go create mode 100644 pkg/report/meta_filter_test.go create mode 100644 pkg/report/report_filter.go create mode 100644 pkg/report/report_filter_test.go create mode 100644 pkg/report/result_filter.go create mode 100644 pkg/report/result_filter_test.go create mode 100644 pkg/report/source_filter.go diff --git a/charts/policy-reporter/README.md b/charts/policy-reporter/README.md index 24c0413f..59662b5a 100644 --- a/charts/policy-reporter/README.md +++ b/charts/policy-reporter/README.md @@ -110,17 +110,15 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | reportFilter.namespaces.include | list | `[]` | | | reportFilter.namespaces.exclude | list | `[]` | | | reportFilter.clusterReports.disabled | bool | `false` | | -| database.type | string | `""` | | -| database.database | string | `""` | | -| database.username | string | `""` | | -| database.password | string | `""` | | -| database.host | string | `""` | | -| database.enableSSL | bool | `false` | | -| database.dsn | string | `""` | | -| database.secretRef | string | `""` | | -| database.mountedSecret | string | `""` | | +| sourceFilters | list | `[{"disableClusterReports":false,"kinds":{"exclude":["ReplicaSet"]},"removeControlled":true,"selector":{"source":"kyverno"}}]` | Source based PolicyReport filter | +| sourceFilters[0] | object | `{"disableClusterReports":false,"kinds":{"exclude":["ReplicaSet"]},"removeControlled":true,"selector":{"source":"kyverno"}}` | PolicyReport selector. | +| sourceFilters[0].selector.source | string | `"kyverno"` | select PolicyReport by source | +| sourceFilters[0].removeControlled | bool | `true` | Filter out PolicyReports of controlled Pods and Jobs | +| sourceFilters[0].disableClusterReports | bool | `false` | Filter out ClusterPolicyReports | +| sourceFilters[0].kinds | object | `{"exclude":["ReplicaSet"]}` | Filter out PolicyReports based on the scope resource kind | +| kyverno-plugin.enabled | bool | `false` | | +| trivy-plugin.enabled | bool | `false` | | | global.labels | object | `{}` | | -| policyPriorities | object | `{}` | | | basicAuth.username | string | `""` | | | basicAuth.password | string | `""` | | | basicAuth.secretRef | string | `""` | | @@ -134,6 +132,8 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | emailReports.smtp.password | string | `""` | | | emailReports.smtp.from | string | `""` | | | emailReports.smtp.encryption | string | `""` | | +| emailReports.smtp.skipTLS | bool | `false` | | +| emailReports.smtp.certificate | string | `""` | | | emailReports.summary.enabled | bool | `false` | | | emailReports.summary.schedule | string | `"0 8 * * *"` | | | emailReports.summary.activeDeadlineSeconds | int | `300` | | @@ -165,6 +165,9 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | target.loki.sources | list | `[]` | | | target.loki.skipExistingOnStartup | bool | `true` | | | target.loki.customFields | object | `{}` | | +| target.loki.headers | object | `{}` | | +| target.loki.username | string | `""` | | +| target.loki.password | string | `""` | | | target.loki.filter | object | `{}` | | | target.loki.channels | list | `[]` | | | target.elasticsearch.host | string | `""` | | @@ -173,12 +176,14 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | target.elasticsearch.index | string | `"policy-reporter"` | | | target.elasticsearch.username | string | `""` | | | target.elasticsearch.password | string | `""` | | +| target.elasticsearch.apiKey | string | `""` | | | target.elasticsearch.secretRef | string | `""` | | | target.elasticsearch.mountedSecret | string | `""` | | | target.elasticsearch.rotation | string | `"daily"` | | | target.elasticsearch.minimumPriority | string | `""` | | | target.elasticsearch.sources | list | `[]` | | | target.elasticsearch.skipExistingOnStartup | bool | `true` | | +| target.elasticsearch.typelessApi | bool | `false` | | | target.elasticsearch.customFields | object | `{}` | | | target.elasticsearch.filter | object | `{}` | | | target.elasticsearch.channels | list | `[]` | | @@ -286,9 +291,12 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | target.securityHub.region | string | `""` | | | target.securityHub.endpoint | string | `""` | | | target.securityHub.accountID | string | `""` | | +| target.securityHub.productName | string | `""` | | | target.securityHub.minimumPriority | string | `""` | | | target.securityHub.sources | list | `[]` | | | target.securityHub.skipExistingOnStartup | bool | `true` | | +| target.securityHub.cleanup | bool | `false` | | +| target.securityHub.delayInSeconds | int | `2` | | | target.securityHub.customFields | object | `{}` | | | target.securityHub.filter | object | `{}` | | | target.securityHub.channels | list | `[]` | | @@ -313,6 +321,15 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | redis.prefix | string | `"policy-reporter"` | | | redis.username | string | `""` | | | redis.password | string | `""` | | +| database.type | string | `""` | | +| database.database | string | `""` | | +| database.username | string | `""` | | +| database.password | string | `""` | | +| database.host | string | `""` | | +| database.enableSSL | bool | `false` | | +| database.dsn | string | `""` | | +| database.secretRef | string | `""` | | +| database.mountedSecret | string | `""` | | | podDisruptionBudget.minAvailable | int | `1` | Configures the minimum available pods for policy-reporter disruptions. Cannot be used if `maxUnavailable` is set. | | podDisruptionBudget.maxUnavailable | string | `nil` | Configures the maximum unavailable pods for policy-reporter disruptions. Cannot be used if `minAvailable` is set. | | nodeSelector | object | `{}` | | @@ -332,7 +349,7 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | ui.image.registry | string | `"ghcr.io"` | Image registry | | ui.image.repository | string | `"kyverno/policy-reporter-ui"` | Image repository | | ui.image.pullPolicy | string | `"IfNotPresent"` | Image PullPolicy | -| ui.image.tag | string | `"2.0.0-alpha.37"` | Image tag Defaults to `Chart.AppVersion` if omitted | +| ui.image.tag | string | `"2.0.0-alpha.47"` | Image tag Defaults to `Chart.AppVersion` if omitted | | ui.replicaCount | int | `1` | Deployment replica count | | ui.tempDir | string | `"/tmp"` | Temporary Directory to persist session data for authentication | | ui.logging.encoding | string | `"console"` | log encoding possible encodings are console and json | @@ -356,10 +373,10 @@ Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-get | ui.oauth.secretRef | string | `""` | Provide OpenID Connect configuration via Secret supported keys: `provider`, `clientId`, `clientSecret` | | ui.displayMode | string | `""` | DisplayMode dark/light uses the OS configured prefered color scheme as default | | ui.customBoards | list | `[]` | Additional customizable dashboards | -| ui.sources | list | `[{"exceptions":false,"excludes":{"namespaceKinds":["Pod","Job","ReplicaSet"],"results":["warn","error"]},"name":"kyverno"}]` | source specific configurations | -| ui.sources[0] | object | `{"exceptions":false,"excludes":{"namespaceKinds":["Pod","Job","ReplicaSet"],"results":["warn","error"]},"name":"kyverno"}` | kyverno specific UI confiurations | +| ui.sources | list | `[{"exceptions":false,"excludes":{"results":["warn","error"]},"name":"kyverno"}]` | source specific configurations | +| ui.sources[0] | object | `{"exceptions":false,"excludes":{"results":["warn","error"]},"name":"kyverno"}` | kyverno specific UI confiurations | | ui.sources[0].exceptions | bool | `false` | enabled action button to generate PolicyExceptions from the UI | -| ui.sources[0].excludes | object | `{"namespaceKinds":["Pod","Job","ReplicaSet"],"results":["warn","error"]}` | exclude Pod, Job and Replica resources from kyverno results by default if no kinds are specified | +| ui.sources[0].excludes | object | `{"results":["warn","error"]}` | exclude Pod, Job and Replica resources from kyverno results by default if no kinds are specified | | ui.clusters | list | `[{"name":"Default","secretRef":"policy-report-ui-default-cluster"}]` | Connected Policy Reporter APIs | | ui.imagePullSecrets | list | `[]` | Image pull secrets for image verification policies, this will define the `--imagePullSecrets` argument | | ui.serviceAccount.create | bool | `true` | Create ServiceAccount | diff --git a/charts/policy-reporter/configs/core.tmpl b/charts/policy-reporter/configs/core.tmpl index b570e2e7..909fec6b 100644 --- a/charts/policy-reporter/configs/core.tmpl +++ b/charts/policy-reporter/configs/core.tmpl @@ -150,6 +150,11 @@ reportFilter: clusterReports: disabled: {{ .Values.reportFilter.clusterReports.disabled }} +{{- with .Values.sourceFilters }} +sourceFilters: + {{- toYaml . | nindent 2 }} +{{- end }} + leaderElection: enabled: {{ or .Values.leaderElection.enabled (gt (int .Values.replicaCount) 1) }} releaseOnCancel: {{ .Values.leaderElection.releaseOnCancel }} diff --git a/charts/policy-reporter/templates/clusterrole.yaml b/charts/policy-reporter/templates/clusterrole.yaml index 689ccb30..41c5f56d 100644 --- a/charts/policy-reporter/templates/clusterrole.yaml +++ b/charts/policy-reporter/templates/clusterrole.yaml @@ -28,4 +28,16 @@ rules: - namespaces verbs: - list +- apiGroups: + - '' + resources: + - pods + verbs: + - get +- apiGroups: + - 'batch' + resources: + - jobs + verbs: + - get {{- end -}} diff --git a/charts/policy-reporter/values.yaml b/charts/policy-reporter/values.yaml index 41da2d66..a5c17918 100644 --- a/charts/policy-reporter/values.yaml +++ b/charts/policy-reporter/values.yaml @@ -166,6 +166,20 @@ reportFilter: # Disable the processing of ClusterPolicyReports disabled: false +# -- Source based PolicyReport filter +sourceFilters: + # -- PolicyReport selector. +- selector: + # -- select PolicyReport by source + source: kyverno + # -- Filter out PolicyReports of controlled Pods and Jobs + uncontrolledOnly: true + # -- Filter out ClusterPolicyReports + disableClusterReports: false + # -- Filter out PolicyReports based on the scope resource kind + kinds: + exclude: [ReplicaSet] + kyverno-plugin: enabled: false @@ -655,6 +669,25 @@ redis: username: "" password: "" +database: + # Database Type, supported: mysql, postgres, mariadb + type: "" + database: "" # Database Name + username: "" + password: "" + host: "" + enableSSL: false + # instead of configure the individual values you can also provide an DSN string + # example postgres: postgres://postgres:password@localhost:5432/postgres?sslmode=disable + # example mysql: root:password@tcp(localhost:3306)/test?tls=false + dsn: "" + # configure an existing secret as source for your values + # supported fields: username, password, host, dsn, database + secretRef: "" + # use an mounted secret as source for your values, required the information in JSON format + # supported fields: username, password, host, dsn, database + mountedSecret: "" + # enabled if replicaCount > 1 podDisruptionBudget: # -- Configures the minimum available pods for policy-reporter disruptions. @@ -804,10 +837,6 @@ ui: exceptions: false # -- exclude Pod, Job and Replica resources from kyverno results by default if no kinds are specified excludes: - namespaceKinds: - - Pod - - Job - - ReplicaSet results: - warn - error diff --git a/pkg/config/config.go b/pkg/config/config.go index 2dd6b15b..2567fe24 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,6 +27,19 @@ type MetricsFilter struct { Sources ValueFilter `mapstructure:"sources"` } +type ReportSelector struct { + Source string `mapstructure:"source"` +} + +type SourceFilter struct { + Selector ReportSelector `mapstructure:"selector"` + Kinds ValueFilter `mapstructure:"kinds"` + Sources ValueFilter `mapstructure:"sources"` + Namespaces ValueFilter `mapstructure:"namespaces"` + UncontrolledOnly bool `mapstructure:"uncontrolledOnly"` + DisableClusterReports bool `mapstructure:"disableClusterReports"` +} + // SMTP configuration type SMTP struct { Host string `mapstructure:"host"` @@ -165,6 +178,7 @@ type Config struct { Metrics Metrics `mapstructure:"metrics"` REST REST `mapstructure:"rest"` ReportFilter ReportFilter `mapstructure:"reportFilter"` + SourceFilters []SourceFilter `mapstructure:"sourceFilters"` Redis Redis `mapstructure:"redis"` Profiling Profiling `mapstructure:"profiling"` EmailReports EmailReports `mapstructure:"emailReports"` diff --git a/pkg/config/resolver.go b/pkg/config/resolver.go index 816d5d34..bc57ce36 100644 --- a/pkg/config/resolver.go +++ b/pkg/config/resolver.go @@ -29,8 +29,11 @@ import ( "github.com/kyverno/policy-reporter/pkg/email" "github.com/kyverno/policy-reporter/pkg/email/summary" "github.com/kyverno/policy-reporter/pkg/email/violations" + "github.com/kyverno/policy-reporter/pkg/helper" "github.com/kyverno/policy-reporter/pkg/kubernetes" + "github.com/kyverno/policy-reporter/pkg/kubernetes/jobs" "github.com/kyverno/policy-reporter/pkg/kubernetes/namespaces" + "github.com/kyverno/policy-reporter/pkg/kubernetes/pods" "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" "github.com/kyverno/policy-reporter/pkg/leaderelection" "github.com/kyverno/policy-reporter/pkg/listener" @@ -190,10 +193,30 @@ func (r *Resolver) Queue() (*kubernetes.Queue, error) { return nil, err } + pods, err := r.PodClient() + if err != nil { + return nil, err + } + + jobs, err := r.JobClient() + if err != nil { + return nil, err + } + return kubernetes.NewQueue( kubernetes.NewDebouncer(1*time.Minute, r.EventPublisher()), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "report-queue"), client, + report.NewSourceFilter(pods, jobs, helper.Map(r.config.SourceFilters, func(f SourceFilter) report.SourceValidation { + return report.SourceValidation{ + Selector: report.ReportSelector{Source: f.Selector.Source}, + Kinds: ToRuleSet(f.Kinds), + Sources: ToRuleSet(f.Sources), + Namespaces: ToRuleSet(f.Namespaces), + UncontrolledOnly: f.UncontrolledOnly, + DisableClusterReports: f.DisableClusterReports, + } + })), ), nil } @@ -302,6 +325,32 @@ func (r *Resolver) NamespaceClient() (namespaces.Client, error) { ), nil } +// PodClient resolver method +func (r *Resolver) PodClient() (pods.Client, error) { + clientset, err := r.Clientset() + if err != nil { + return nil, err + } + + return pods.NewClient( + clientset.CoreV1(), + gocache.New(15*time.Second, 5*time.Second), + ), nil +} + +// JobClient resolver method +func (r *Resolver) JobClient() (jobs.Client, error) { + clientset, err := r.Clientset() + if err != nil { + return nil, err + } + + return jobs.NewClient( + clientset.BatchV1(), + gocache.New(15*time.Second, 5*time.Second), + ), nil +} + func (r *Resolver) TargetFactory() *TargetFactory { ns, err := r.NamespaceClient() if err != nil { @@ -469,8 +518,8 @@ func (r *Resolver) PolicyReportClient() (report.PolicyReportClient, error) { return r.policyReportClient, nil } -func (r *Resolver) ReportFilter() *report.Filter { - return report.NewFilter( +func (r *Resolver) ReportFilter() *report.MetaFilter { + return report.NewMetaFilter( r.config.ReportFilter.ClusterReports.Disabled, ToRuleSet(r.config.ReportFilter.Namespaces), ) diff --git a/pkg/kubernetes/jobs/client.go b/pkg/kubernetes/jobs/client.go new file mode 100644 index 00000000..7313fcbe --- /dev/null +++ b/pkg/kubernetes/jobs/client.go @@ -0,0 +1,46 @@ +package jobs + +import ( + "context" + + gocache "github.com/patrickmn/go-cache" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/batch/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +type Client interface { + Get(scope *corev1.ObjectReference) (*batchv1.Job, error) +} + +type k8sClient struct { + client v1.BatchV1Interface + cache *gocache.Cache +} + +func (c *k8sClient) Get(scope *corev1.ObjectReference) (*batchv1.Job, error) { + if cached, ok := c.cache.Get(string(scope.UID)); ok { + return cached.(*batchv1.Job), nil + } + + pod, err := kubernetes.Retry(func() (*batchv1.Job, error) { + return c.client.Jobs(scope.Namespace).Get(context.Background(), scope.Name, metav1.GetOptions{}) + }) + if err != nil { + return nil, err + } + + c.cache.Set(string(scope.UID), pod, 0) + + return pod, nil +} + +func NewClient(client v1.BatchV1Interface, cache *gocache.Cache) Client { + return &k8sClient{ + client: client, + cache: cache, + } +} diff --git a/pkg/kubernetes/pods/client.go b/pkg/kubernetes/pods/client.go new file mode 100644 index 00000000..590e2bea --- /dev/null +++ b/pkg/kubernetes/pods/client.go @@ -0,0 +1,45 @@ +package pods + +import ( + "context" + + gocache "github.com/patrickmn/go-cache" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + + "github.com/kyverno/policy-reporter/pkg/kubernetes" +) + +type Client interface { + Get(scope *corev1.ObjectReference) (*corev1.Pod, error) +} + +type k8sClient struct { + client v1.CoreV1Interface + cache *gocache.Cache +} + +func (c *k8sClient) Get(scope *corev1.ObjectReference) (*corev1.Pod, error) { + if cached, ok := c.cache.Get(string(scope.UID)); ok { + return cached.(*corev1.Pod), nil + } + + pod, err := kubernetes.Retry(func() (*corev1.Pod, error) { + return c.client.Pods(scope.Namespace).Get(context.Background(), scope.Name, metav1.GetOptions{}) + }) + if err != nil { + return nil, err + } + + c.cache.Set(string(scope.UID), pod, 0) + + return pod, nil +} + +func NewClient(client v1.CoreV1Interface, cache *gocache.Cache) Client { + return &k8sClient{ + client: client, + cache: cache, + } +} diff --git a/pkg/kubernetes/policy_report_client.go b/pkg/kubernetes/policy_report_client.go index 44435ec2..e4a65f03 100644 --- a/pkg/kubernetes/policy_report_client.go +++ b/pkg/kubernetes/policy_report_client.go @@ -25,7 +25,7 @@ type k8sPolicyReportClient struct { metaClient metadata.Interface synced bool mx *sync.Mutex - reportFilter *report.Filter + reportFilter *report.MetaFilter stopChan chan struct{} } @@ -110,7 +110,7 @@ func (k *k8sPolicyReportClient) configureInformer(informer cache.SharedIndexInfo } // NewPolicyReportClient new Client for Policy Report Kubernetes API -func NewPolicyReportClient(metaClient metadata.Interface, reportFilter *report.Filter, queue *Queue) report.PolicyReportClient { +func NewPolicyReportClient(metaClient metadata.Interface, reportFilter *report.MetaFilter, queue *Queue) report.PolicyReportClient { return &k8sPolicyReportClient{ metaClient: metaClient, mx: &sync.Mutex{}, diff --git a/pkg/kubernetes/policy_report_client_test.go b/pkg/kubernetes/policy_report_client_test.go index b75c2c61..4a90fd94 100644 --- a/pkg/kubernetes/policy_report_client_test.go +++ b/pkg/kubernetes/policy_report_client_test.go @@ -15,7 +15,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/validate" ) -var filter = report.NewFilter(false, validate.RuleSets{}) +var filter = report.NewMetaFilter(false, validate.RuleSets{}) func Test_PolicyReportWatcher(t *testing.T) { ctx := context.Background() @@ -38,6 +38,7 @@ func Test_PolicyReportWatcher(t *testing.T) { kubernetes.NewDebouncer(0, publisher), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), ) kclient, rclient, _ := NewFakeMetaClient() @@ -88,6 +89,7 @@ func Test_ClusterPolicyReportWatcher(t *testing.T) { kubernetes.NewDebouncer(0, publisher), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), ) kclient, _, rclient := NewFakeMetaClient() @@ -128,6 +130,7 @@ func Test_HasSynced(t *testing.T) { kubernetes.NewDebouncer(0, report.NewEventPublisher()), workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-queue"), restClient.Wgpolicyk8sV1alpha2(), + report.NewSourceFilter(nil, nil, []report.SourceValidation{}), ) kclient, _, _ := NewFakeMetaClient() diff --git a/pkg/kubernetes/queue.go b/pkg/kubernetes/queue.go index f77ca07b..a9b38a3c 100644 --- a/pkg/kubernetes/queue.go +++ b/pkg/kubernetes/queue.go @@ -25,6 +25,7 @@ type Queue struct { debouncer Debouncer lock *sync.Mutex cache sets.Set[string] + filter *report.SourceFilter } func (q *Queue) Add(obj *v1.PartialObjectMetadata) error { @@ -101,6 +102,10 @@ func (q *Queue) processNextItem() bool { return true } + if ok := q.filter.Validate(polr); !ok { + return true + } + event := func() report.Event { q.lock.Lock() defer q.lock.Unlock() @@ -157,12 +162,13 @@ 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, filter *report.SourceFilter) *Queue { return &Queue{ debouncer: debouncer, queue: queue, client: client, cache: sets.New[string](), lock: &sync.Mutex{}, + filter: filter, } } diff --git a/pkg/report/filter.go b/pkg/report/filter.go deleted file mode 100644 index 56c3c3de..00000000 --- a/pkg/report/filter.go +++ /dev/null @@ -1,77 +0,0 @@ -package report - -import ( - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -type Namespaced interface { - GetNamespace() string -} - -type Filter struct { - disbaleClusterReports bool - namespace validate.RuleSets -} - -func (f *Filter) DisableClusterReports() bool { - return f.disbaleClusterReports -} - -func (f *Filter) AllowReport(report Namespaced) bool { - return validate.Namespace(report.GetNamespace(), f.namespace) -} - -func NewFilter(disableClusterReports bool, namespace validate.RuleSets) *Filter { - return &Filter{disableClusterReports, namespace} -} - -type ResultValidation = func(v1alpha2.PolicyReportResult) bool - -type ResultFilter struct { - validations []ResultValidation - Sources []string - MinimumPriority string -} - -func (rf *ResultFilter) AddValidation(v ResultValidation) { - rf.validations = append(rf.validations, v) -} - -func (rf *ResultFilter) Validate(result v1alpha2.PolicyReportResult) bool { - for _, validation := range rf.validations { - if !validation(result) { - return false - } - } - - return true -} - -func NewResultFilter() *ResultFilter { - return &ResultFilter{} -} - -type ReportValidation = func(v1alpha2.ReportInterface) bool - -type ReportFilter struct { - validations []ReportValidation -} - -func (rf *ReportFilter) AddValidation(v ReportValidation) { - rf.validations = append(rf.validations, v) -} - -func (rf *ReportFilter) Validate(report v1alpha2.ReportInterface) bool { - for _, validation := range rf.validations { - if !validation(report) { - return false - } - } - - return true -} - -func NewReportFilter() *ReportFilter { - return &ReportFilter{} -} diff --git a/pkg/report/filter_test.go b/pkg/report/filter_test.go deleted file mode 100644 index dbdfb250..00000000 --- a/pkg/report/filter_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package report_test - -import ( - "testing" - - "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" - "github.com/kyverno/policy-reporter/pkg/fixtures" - "github.com/kyverno/policy-reporter/pkg/report" - "github.com/kyverno/policy-reporter/pkg/validate" -) - -func Test_DisableClusterReports(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{}) - - if !filter.DisableClusterReports() { - t.Error("Expected EnableClusterReports to return true as configured") - } -} - -func Test_AllowReport(t *testing.T) { - t.Run("Allow ClusterReport", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Exclude: []string{"*"}}) - if !filter.AllowReport(creport) { - t.Error("Expected AllowReport returns true if Report is a ClusterPolicyReport without namespace") - } - }) - - t.Run("Allow Report with matching include Namespace", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"patch", "te*"}}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true if Report namespace matches include pattern") - } - }) - - t.Run("Disallow Report with matching exclude Namespace", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Exclude: []string{"patch", "te*"}}) - if filter.AllowReport(preport) { - t.Error("Expected AllowReport returns false if Report namespace matches exclude pattern") - } - }) - - t.Run("Ignores exclude pattern if include namespaces provided", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"*"}, Exclude: []string{"te*"}}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true because exclude patterns ignored if include patterns provided") - } - }) - - t.Run("Allow Report when no configuration exists", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{}) - if !filter.AllowReport(preport) { - t.Error("Expected AllowReport returns true if no namespace patterns configured") - } - }) - - t.Run("Disallow Report if no include namespace matches", func(t *testing.T) { - filter := report.NewFilter(true, validate.RuleSets{Include: []string{"patch", "dev"}}) - if filter.AllowReport(preport) { - t.Error("Expected AllowReport returns false if no namespace pattern matches") - } - }) -} - -func Test_ResultFilter(t *testing.T) { - t.Run("don't filter any result without validations", func(t *testing.T) { - filter := report.NewResultFilter() - if !filter.Validate(fixtures.FailResult) { - t.Error("Expected result validates to true") - } - }) - t.Run("filter result with a false validation", func(t *testing.T) { - filter := report.NewResultFilter() - filter.AddValidation(func(r v1alpha2.PolicyReportResult) bool { return false }) - if filter.Validate(fixtures.FailResult) { - t.Error("Expected result validates to false") - } - }) -} - -func Test_ReportFilter(t *testing.T) { - t.Run("don't filter any result without validations", func(t *testing.T) { - filter := report.NewReportFilter() - if !filter.Validate(preport) { - t.Error("Expected result validates to true") - } - }) - t.Run("filter result with a false validation", func(t *testing.T) { - filter := report.NewReportFilter() - filter.AddValidation(func(r v1alpha2.ReportInterface) bool { return false }) - if filter.Validate(preport) { - t.Error("Expected result validates to false") - } - }) -} diff --git a/pkg/report/meta_filter.go b/pkg/report/meta_filter.go new file mode 100644 index 00000000..445b03d1 --- /dev/null +++ b/pkg/report/meta_filter.go @@ -0,0 +1,26 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/validate" +) + +type Namespaced interface { + GetNamespace() string +} + +type MetaFilter struct { + disbaleClusterReports bool + namespace validate.RuleSets +} + +func (f *MetaFilter) DisableClusterReports() bool { + return f.disbaleClusterReports +} + +func (f *MetaFilter) AllowReport(report Namespaced) bool { + return validate.Namespace(report.GetNamespace(), f.namespace) +} + +func NewMetaFilter(disableClusterReports bool, namespace validate.RuleSets) *MetaFilter { + return &MetaFilter{disableClusterReports, namespace} +} diff --git a/pkg/report/meta_filter_test.go b/pkg/report/meta_filter_test.go new file mode 100644 index 00000000..5d35ce60 --- /dev/null +++ b/pkg/report/meta_filter_test.go @@ -0,0 +1,60 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/report" + "github.com/kyverno/policy-reporter/pkg/validate" +) + +func TestMetaFilter(t *testing.T) { + t.Run("disable cluster reports", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{}) + + if !filter.DisableClusterReports() { + t.Error("Expected EnableClusterReports to return true as configured") + } + }) + + t.Run("Allow ClusterReport", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Exclude: []string{"*"}}) + if !filter.AllowReport(creport) { + t.Error("Expected AllowReport returns true if Report is a ClusterPolicyReport without namespace") + } + }) + + t.Run("Allow Report with matching include Namespace", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"patch", "te*"}}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true if Report namespace matches include pattern") + } + }) + + t.Run("Disallow Report with matching exclude Namespace", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Exclude: []string{"patch", "te*"}}) + if filter.AllowReport(preport) { + t.Error("Expected AllowReport returns false if Report namespace matches exclude pattern") + } + }) + + t.Run("Ignores exclude pattern if include namespaces provided", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"*"}, Exclude: []string{"te*"}}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true because exclude patterns ignored if include patterns provided") + } + }) + + t.Run("Allow Report when no configuration exists", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{}) + if !filter.AllowReport(preport) { + t.Error("Expected AllowReport returns true if no namespace patterns configured") + } + }) + + t.Run("Disallow Report if no include namespace matches", func(t *testing.T) { + filter := report.NewMetaFilter(true, validate.RuleSets{Include: []string{"patch", "dev"}}) + if filter.AllowReport(preport) { + t.Error("Expected AllowReport returns false if no namespace pattern matches") + } + }) +} diff --git a/pkg/report/report_filter.go b/pkg/report/report_filter.go new file mode 100644 index 00000000..671ebbe9 --- /dev/null +++ b/pkg/report/report_filter.go @@ -0,0 +1,29 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" +) + +type ReportValidation = func(v1alpha2.ReportInterface) bool + +type ReportFilter struct { + validations []ReportValidation +} + +func (rf *ReportFilter) AddValidation(v ReportValidation) { + rf.validations = append(rf.validations, v) +} + +func (rf *ReportFilter) Validate(report v1alpha2.ReportInterface) bool { + for _, validation := range rf.validations { + if !validation(report) { + return false + } + } + + return true +} + +func NewReportFilter() *ReportFilter { + return &ReportFilter{} +} diff --git a/pkg/report/report_filter_test.go b/pkg/report/report_filter_test.go new file mode 100644 index 00000000..0a2e40f7 --- /dev/null +++ b/pkg/report/report_filter_test.go @@ -0,0 +1,24 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/report" +) + +func Test_ReportFilter(t *testing.T) { + t.Run("don't filter any result without validations", func(t *testing.T) { + filter := report.NewReportFilter() + if !filter.Validate(preport) { + t.Error("Expected result validates to true") + } + }) + t.Run("filter result with a false validation", func(t *testing.T) { + filter := report.NewReportFilter() + filter.AddValidation(func(r v1alpha2.ReportInterface) bool { return false }) + if filter.Validate(preport) { + t.Error("Expected result validates to false") + } + }) +} diff --git a/pkg/report/result_filter.go b/pkg/report/result_filter.go new file mode 100644 index 00000000..7f9bbcab --- /dev/null +++ b/pkg/report/result_filter.go @@ -0,0 +1,31 @@ +package report + +import ( + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" +) + +type ResultValidation = func(v1alpha2.PolicyReportResult) bool + +type ResultFilter struct { + validations []ResultValidation + Sources []string + MinimumPriority string +} + +func (rf *ResultFilter) AddValidation(v ResultValidation) { + rf.validations = append(rf.validations, v) +} + +func (rf *ResultFilter) Validate(result v1alpha2.PolicyReportResult) bool { + for _, validation := range rf.validations { + if !validation(result) { + return false + } + } + + return true +} + +func NewResultFilter() *ResultFilter { + return &ResultFilter{} +} diff --git a/pkg/report/result_filter_test.go b/pkg/report/result_filter_test.go new file mode 100644 index 00000000..bfcc0b8a --- /dev/null +++ b/pkg/report/result_filter_test.go @@ -0,0 +1,25 @@ +package report_test + +import ( + "testing" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/report" +) + +func Test_ResultFilter(t *testing.T) { + t.Run("don't filter any result without validations", func(t *testing.T) { + filter := report.NewResultFilter() + if !filter.Validate(fixtures.FailResult) { + t.Error("Expected result validates to true") + } + }) + t.Run("filter result with a false validation", func(t *testing.T) { + filter := report.NewResultFilter() + filter.AddValidation(func(r v1alpha2.PolicyReportResult) bool { return false }) + if filter.Validate(fixtures.FailResult) { + t.Error("Expected result validates to false") + } + }) +} diff --git a/pkg/report/source_filter.go b/pkg/report/source_filter.go new file mode 100644 index 00000000..3d3bf72c --- /dev/null +++ b/pkg/report/source_filter.go @@ -0,0 +1,150 @@ +package report + +import ( + "strings" + + "go.uber.org/zap" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/helper" + "github.com/kyverno/policy-reporter/pkg/validate" +) + +type PodClient interface { + Get(res *corev1.ObjectReference) (*corev1.Pod, error) +} + +type JobClient interface { + Get(res *corev1.ObjectReference) (*batchv1.Job, error) +} + +type ReportSelector struct { + Source string +} + +type SourceValidation struct { + Selector ReportSelector + Kinds validate.RuleSets + Sources validate.RuleSets + Namespaces validate.RuleSets + UncontrolledOnly bool + DisableClusterReports bool +} + +type SourceFilter struct { + pods PodClient + jobs JobClient + validations []SourceValidation +} + +func (s *SourceFilter) Validate(polr v1alpha2.ReportInterface) bool { + for _, validation := range s.validations { + if ok := s.run(polr, validation); !ok { + return false + } + } + + return true +} + +func (s *SourceFilter) run(polr v1alpha2.ReportInterface, options SourceValidation) bool { + logger := zap.L().With( + zap.String("namespace", polr.GetNamespace()), + zap.String("report", polr.GetName()), + ) + + if !Match(polr, options.Selector) { + return true + } + + if options.DisableClusterReports && polr.GetNamespace() == "" { + logger.Debug("filter cluster report") + return false + } + + if options.Sources.Enabled() && !validate.MatchRuleSet(polr.GetSource(), options.Sources) { + logger.Debug("filter report source") + return false + } + + scope := polr.GetScope() + if scope == nil { + return true + } + + logger = logger.With(zap.Any("scope", scope)) + + if options.Kinds.Enabled() && !validate.MatchRuleSet(scope.Kind, options.Kinds) { + logger.Debug("filter scope resource kind") + return false + } + + if options.Namespaces.Enabled() && !validate.MatchRuleSet(scope.Namespace, options.Namespaces) { + logger.Debug("filter scope resource namespace") + return false + } + + if options.UncontrolledOnly && s.pods != nil && scope.Kind == "Pod" { + pod, err := s.pods.Get(scope) + if err != nil { + zap.L().Error("failed to get pod", zap.Error(err), zap.Any("resource", scope)) + return true + } + + if ok := Uncontrolled(pod.OwnerReferences); ok { + return true + } + + logger.Debug("filter controlled pod resource") + return false + } + + if options.UncontrolledOnly && s.jobs != nil && scope.Kind == "Job" { + job, err := s.jobs.Get(scope) + if err != nil { + zap.L().Error("failed to get job", zap.Error(err), zap.Any("resource", scope)) + return true + } + + if ok := Uncontrolled(job.OwnerReferences); ok { + return true + } + + logger.Debug("filter controlled job resource") + return false + } + + return true +} + +func NewSourceFilter(pods PodClient, jobs JobClient, validations []SourceValidation) *SourceFilter { + return &SourceFilter{pods: pods, jobs: jobs, validations: validations} +} + +var controller = []string{"ReplicaSet", "DaemonSet", "CronJob", "Job"} + +func Uncontrolled(owner []metav1.OwnerReference) bool { + if len(owner) == 0 { + return true + } + + for _, o := range owner { + isController := o.Controller + if isController == nil { + continue + } + + if *isController == true && helper.Contains(o.Kind, controller) { + return false + } + } + + return true +} + +func Match(polr v1alpha2.ReportInterface, selector ReportSelector) bool { + return selector.Source == "" || strings.ToLower(selector.Source) == strings.ToLower(polr.GetSource()) +} diff --git a/pkg/target/http/logroundtripper_test.go b/pkg/target/http/logroundtripper_test.go index ec6441c8..3afc24bd 100644 --- a/pkg/target/http/logroundtripper_test.go +++ b/pkg/target/http/logroundtripper_test.go @@ -5,10 +5,11 @@ import ( "net/http/httptest" "testing" - "github.com/kyverno/policy-reporter/pkg/target/http" "github.com/stretchr/testify/assert" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" + + "github.com/kyverno/policy-reporter/pkg/target/http" ) type mock struct{} diff --git a/pkg/target/http/utils_test.go b/pkg/target/http/utils_test.go index 8b0d736e..b9dfb23b 100644 --- a/pkg/target/http/utils_test.go +++ b/pkg/target/http/utils_test.go @@ -6,12 +6,12 @@ import ( "net/http/httptest" "testing" - "github.com/kyverno/policy-reporter/pkg/fixtures" - "github.com/kyverno/policy-reporter/pkg/target/http" "github.com/stretchr/testify/assert" "go.uber.org/zap" - "go.uber.org/zap/zaptest/observer" + + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/target/http" ) func TestResultMapping(t *testing.T) { diff --git a/pkg/validate/model.go b/pkg/validate/model.go index 9093d670..54ffc0a5 100644 --- a/pkg/validate/model.go +++ b/pkg/validate/model.go @@ -9,3 +9,7 @@ type RuleSets struct { func (r RuleSets) Count() int { return len(r.Exclude) + len(r.Include) } + +func (r RuleSets) Enabled() bool { + return r.Count() > 0 +}