1
0
Fork 0
mirror of https://github.com/kyverno/policy-reporter.git synced 2024-12-14 11:57:32 +00:00

ID Generation (#428)

Signed-off-by: Frank Jogeleit <frank.jogeleit@lovoo.com>
This commit is contained in:
Frank Jogeleit 2024-05-02 12:22:59 +02:00 committed by GitHub
parent 1edff60f57
commit dd150ee3b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 716 additions and 130 deletions

View file

@ -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" . }}

View file

@ -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

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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,32 +129,21 @@ 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"}}
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())
}
})
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"}}
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"}}}

View file

@ -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,
})
}

View file

@ -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,

View file

@ -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
}

View file

@ -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()

View file

@ -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,
}
}

View file

@ -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}
}

View file

@ -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)
}
})
}

View file

@ -1,4 +1,4 @@
package report
package result
import (
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"

View file

@ -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,
})

View file

@ -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,
}
}

View file

@ -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")
}
})
}

View file

@ -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{}
}

View file

@ -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")
}
})
}