mirror of
https://github.com/kyverno/policy-reporter.git
synced 2024-12-14 11:57:32 +00:00
Policy report crd update (#24)
* Add Support for Properties and Timestamp * Prevent resending violations after Kyverno cleanup
This commit is contained in:
parent
15ad03d552
commit
4a436eb2a8
23 changed files with 527 additions and 180 deletions
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## 1.2.0
|
||||
|
||||
* Support for (Cluster)PolicyReport CRD Properties in Target Output
|
||||
* Support for (Cluster)PolicyReport CRD Timestamp in Target Output
|
||||
* Fix resend violations after Kyverno Cleanup with ResultHashes
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Added PolicyReport Category to Metrics
|
||||
|
|
|
@ -5,8 +5,8 @@ description: |
|
|||
It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord
|
||||
|
||||
type: application
|
||||
version: 1.1.0
|
||||
appVersion: 1.1.0
|
||||
version: 1.2.0
|
||||
appVersion: 1.2.0
|
||||
|
||||
dependencies:
|
||||
- name: monitoring
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
image:
|
||||
repository: fjogeleit/policy-reporter
|
||||
pullPolicy: IfNotPresent
|
||||
tag: 1.1.0
|
||||
tag: 1.2.0
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ require (
|
|||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -292,6 +292,8 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
|
|||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.1 h1:L60q1+q7cXE4JeEJJKMnh2brFIe3rZxCihYAB61ypAY=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.1/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
|
|
|
@ -20,6 +20,7 @@ type clusterPolicyReportClient struct {
|
|||
startUp time.Time
|
||||
skipExisting bool
|
||||
started bool
|
||||
modifyHash map[string]uint64
|
||||
}
|
||||
|
||||
func (c *clusterPolicyReportClient) RegisterCallback(cb report.ClusterPolicyReportCallback) {
|
||||
|
@ -125,6 +126,12 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
c.RegisterCallback(func(s watch.EventType, cpr report.ClusterPolicyReport, opr report.ClusterPolicyReport) {
|
||||
switch s {
|
||||
case watch.Added:
|
||||
if len(cpr.Results) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
c.modifyHash[cpr.GetIdentifier()] = cpr.ResultHash()
|
||||
|
||||
preExisted := cpr.CreationTimestamp.Before(c.startUp)
|
||||
|
||||
if c.skipExisting && preExisted {
|
||||
|
@ -145,6 +152,19 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
|
||||
wg.Wait()
|
||||
case watch.Modified:
|
||||
if len(cpr.Results) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
newHash := cpr.ResultHash()
|
||||
if hash, ok := c.modifyHash[cpr.GetIdentifier()]; ok {
|
||||
if newHash == hash {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.modifyHash[cpr.GetIdentifier()] = newHash
|
||||
|
||||
diff := cpr.GetNewResults(opr)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -160,6 +180,10 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
}
|
||||
|
||||
wg.Wait()
|
||||
case watch.Deleted:
|
||||
if _, ok := c.modifyHash[cpr.GetIdentifier()]; ok {
|
||||
delete(c.modifyHash, cpr.GetIdentifier())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -167,9 +191,10 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
// NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client
|
||||
func NewClusterPolicyReportClient(client PolicyReportAdapter, store *report.ClusterPolicyReportStore, mapper Mapper, startUp time.Time) report.ClusterPolicyClient {
|
||||
return &clusterPolicyReportClient{
|
||||
policyAPI: client,
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
policyAPI: client,
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
modifyHash: make(map[string]uint64),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,12 +140,13 @@ func (m *mapper) mapResult(result map[string]interface{}) report.Result {
|
|||
status := result["status"].(report.Status)
|
||||
|
||||
r := report.Result{
|
||||
Message: result["message"].(string),
|
||||
Policy: result["policy"].(string),
|
||||
Status: status,
|
||||
Scored: result["scored"].(bool),
|
||||
Priority: report.PriorityFromStatus(status),
|
||||
Resources: resources,
|
||||
Message: result["message"].(string),
|
||||
Policy: result["policy"].(string),
|
||||
Status: status,
|
||||
Scored: result["scored"].(bool),
|
||||
Priority: report.PriorityFromStatus(status),
|
||||
Resources: resources,
|
||||
Properties: make(map[string]string, 0),
|
||||
}
|
||||
|
||||
if severity, ok := result["severity"]; ok {
|
||||
|
@ -164,6 +165,21 @@ func (m *mapper) mapResult(result map[string]interface{}) report.Result {
|
|||
r.Category = category.(string)
|
||||
}
|
||||
|
||||
if created, ok := result["timestamp"]; ok {
|
||||
time, err := time.Parse("2006-01-02T15:04:05Z", created.(string))
|
||||
if err == nil {
|
||||
r.Timestamp = time
|
||||
}
|
||||
}
|
||||
|
||||
if props, ok := result["properties"]; ok {
|
||||
if properties, ok := props.(map[string]interface{}); ok {
|
||||
for property, value := range properties {
|
||||
r.Properties[property] = value.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
|
|
@ -28,13 +28,14 @@ var policyMap = map[string]interface{}{
|
|||
},
|
||||
"results": []interface{}{
|
||||
map[string]interface{}{
|
||||
"message": "message",
|
||||
"status": "fail",
|
||||
"scored": true,
|
||||
"policy": "required-label",
|
||||
"rule": "app-label-required",
|
||||
"category": "test",
|
||||
"severity": "high",
|
||||
"message": "message",
|
||||
"status": "fail",
|
||||
"scored": true,
|
||||
"policy": "required-label",
|
||||
"rule": "app-label-required",
|
||||
"timestamp": "2021-02-23T15:10:00Z",
|
||||
"category": "test",
|
||||
"severity": "high",
|
||||
"resources": []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
|
@ -44,6 +45,9 @@ var policyMap = map[string]interface{}{
|
|||
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
"properties": map[string]interface{}{
|
||||
"version": "1.2.0",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"message": "message 2",
|
||||
|
@ -163,6 +167,12 @@ func Test_MapPolicyReport(t *testing.T) {
|
|||
if result1.Severity != report.High {
|
||||
t.Errorf("Expected Severity '%s' (acutal %s)", report.High, result1.Severity)
|
||||
}
|
||||
if result1.Timestamp.Format("2006-01-02T15:04:05Z") != "2021-02-23T15:10:00Z" {
|
||||
t.Errorf("Expected Timestamp '2021-02-23T15:10:00Z' (acutal %s)", result1.Timestamp.Format("2006-01-02T15:04:05Z"))
|
||||
}
|
||||
if result1.Properties["version"] != "1.2.0" {
|
||||
t.Errorf("Expected Property '1.2.0' (acutal %s)", result1.Properties["version"])
|
||||
}
|
||||
|
||||
resource := result1.Resources[0]
|
||||
if resource.APIVersion != "v1" {
|
||||
|
|
|
@ -20,6 +20,7 @@ type policyReportClient struct {
|
|||
startUp time.Time
|
||||
skipExisting bool
|
||||
started bool
|
||||
modifyHash map[string]uint64
|
||||
}
|
||||
|
||||
func (c *policyReportClient) RegisterCallback(cb report.PolicyReportCallback) {
|
||||
|
@ -126,24 +127,64 @@ func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
|
|||
func(e watch.EventType, pr report.PolicyReport, or report.PolicyReport) {
|
||||
switch e {
|
||||
case watch.Added:
|
||||
if len(pr.Results) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
c.modifyHash[pr.GetIdentifier()] = pr.ResultHash()
|
||||
|
||||
preExisted := pr.CreationTimestamp.Before(c.startUp)
|
||||
|
||||
if c.skipExisting && preExisted {
|
||||
break
|
||||
}
|
||||
|
||||
for _, result := range pr.Results {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(pr.Results) * len(c.resultCallbacks))
|
||||
|
||||
for _, r := range pr.Results {
|
||||
for _, cb := range c.resultCallbacks {
|
||||
cb(result, preExisted)
|
||||
go func(callback report.PolicyResultCallback, result report.Result) {
|
||||
callback(result, preExisted)
|
||||
wg.Done()
|
||||
}(cb, r)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
case watch.Modified:
|
||||
diff := pr.GetNewResults(or)
|
||||
for _, result := range diff {
|
||||
for _, cb := range c.resultCallbacks {
|
||||
cb(result, false)
|
||||
if len(pr.Results) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
newHash := pr.ResultHash()
|
||||
if hash, ok := c.modifyHash[pr.GetIdentifier()]; ok {
|
||||
if newHash == hash {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.modifyHash[pr.GetIdentifier()] = newHash
|
||||
|
||||
diff := pr.GetNewResults(or)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(diff) * len(c.resultCallbacks))
|
||||
|
||||
for _, r := range diff {
|
||||
for _, cb := range c.resultCallbacks {
|
||||
go func(callback report.PolicyResultCallback, result report.Result) {
|
||||
callback(result, false)
|
||||
wg.Done()
|
||||
}(cb, r)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
case watch.Deleted:
|
||||
if _, ok := c.modifyHash[pr.GetIdentifier()]; ok {
|
||||
delete(c.modifyHash, pr.GetIdentifier())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -151,9 +192,10 @@ func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
|
|||
// NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client
|
||||
func NewPolicyReportClient(client PolicyReportAdapter, store *report.PolicyReportStore, mapper Mapper, startUp time.Time) report.PolicyClient {
|
||||
return &policyReportClient{
|
||||
policyAPI: client,
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
policyAPI: client,
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
modifyHash: make(map[string]uint64),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,3 +158,149 @@ func Test_ResultClient_RegisterPolicyResultWatcher(t *testing.T) {
|
|||
t.Error("Should receive 3 Result from all PolicyReports")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResultClient_SkipReportsWithoutResults(t *testing.T) {
|
||||
_, k8sCMClient := newFakeAPI()
|
||||
k8sCMClient.Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
fakeAdapter := NewPolicyReportAdapter()
|
||||
|
||||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
pClient := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now())
|
||||
cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now())
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(pClient, cpClient)
|
||||
|
||||
client.RegisterPolicyResultWatcher(false)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
results := make([]report.Result, 0, 3)
|
||||
|
||||
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
|
||||
results = append(results, r)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
go pClient.StartWatching()
|
||||
go cpClient.StartWatching()
|
||||
|
||||
var policyMap2 = map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "policy-report",
|
||||
"namespace": "test",
|
||||
"creationTimestamp": "2021-02-23T15:00:00Z",
|
||||
},
|
||||
"summary": map[string]interface{}{
|
||||
"pass": int64(1),
|
||||
"skip": int64(2),
|
||||
"warn": int64(3),
|
||||
"fail": int64(4),
|
||||
"error": int64(5),
|
||||
},
|
||||
"results": []interface{}{},
|
||||
}
|
||||
|
||||
var clusterPolicyMap2 = map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "clusterpolicy-report",
|
||||
"creationTimestamp": "2021-02-23T15:00:00Z",
|
||||
},
|
||||
"summary": map[string]interface{}{
|
||||
"pass": int64(0),
|
||||
"skip": int64(0),
|
||||
"warn": int64(0),
|
||||
"fail": int64(0),
|
||||
"error": int64(0),
|
||||
},
|
||||
"results": []interface{}{},
|
||||
}
|
||||
|
||||
fakeAdapter.clusterPolicyWatcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap2})
|
||||
fakeAdapter.clusterPolicyWatcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap2})
|
||||
fakeAdapter.clusterPolicyWatcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap})
|
||||
|
||||
fakeAdapter.policyWatcher.Add(&unstructured.Unstructured{Object: policyMap2})
|
||||
fakeAdapter.policyWatcher.Modify(&unstructured.Unstructured{Object: policyMap2})
|
||||
fakeAdapter.policyWatcher.Modify(&unstructured.Unstructured{Object: policyMap})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Error("Should receive 3 Result from none empty PolicyReport and ClusterPolicyReport Modify")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResultClient_SkipReportsCleanUpEvents(t *testing.T) {
|
||||
_, k8sCMClient := newFakeAPI()
|
||||
k8sCMClient.Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
fakeAdapter := NewPolicyReportAdapter()
|
||||
|
||||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
pClient := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now())
|
||||
cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now())
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(pClient, cpClient)
|
||||
|
||||
client.RegisterPolicyResultWatcher(false)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
|
||||
results := make([]report.Result, 0, 3)
|
||||
|
||||
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
|
||||
results = append(results, r)
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
go pClient.StartWatching()
|
||||
go cpClient.StartWatching()
|
||||
|
||||
var policyMap2 = map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "policy-report",
|
||||
"namespace": "test",
|
||||
"creationTimestamp": "2021-02-23T15:00:00Z",
|
||||
},
|
||||
"summary": map[string]interface{}{
|
||||
"pass": int64(0),
|
||||
"skip": int64(0),
|
||||
"warn": int64(0),
|
||||
"fail": int64(0),
|
||||
"error": int64(0),
|
||||
},
|
||||
"results": []interface{}{},
|
||||
}
|
||||
|
||||
var clusterPolicyMap2 = map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "clusterpolicy-report",
|
||||
"creationTimestamp": "2021-02-23T15:00:00Z",
|
||||
},
|
||||
"summary": map[string]interface{}{
|
||||
"pass": int64(0),
|
||||
"skip": int64(0),
|
||||
"warn": int64(0),
|
||||
"fail": int64(0),
|
||||
"error": int64(0),
|
||||
},
|
||||
"results": []interface{}{},
|
||||
}
|
||||
|
||||
fakeAdapter.clusterPolicyWatcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
|
||||
fakeAdapter.clusterPolicyWatcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap2})
|
||||
fakeAdapter.clusterPolicyWatcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap})
|
||||
|
||||
fakeAdapter.policyWatcher.Add(&unstructured.Unstructured{Object: policyMap})
|
||||
fakeAdapter.policyWatcher.Modify(&unstructured.Unstructured{Object: policyMap2})
|
||||
fakeAdapter.policyWatcher.Modify(&unstructured.Unstructured{Object: policyMap})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Error("Should receive 3 Results from the initial add events, not from the cleanup modify events")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,49 +28,17 @@ func CreateClusterPolicyReportMetricsCallback() report.ClusterPolicyReportCallba
|
|||
updateClusterPolicyGauge(policyGauge, report)
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.WithLabelValues(
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
).Set(1)
|
||||
ruleGauge.With(generateClusterResultLabels(report, rule)).Set(1)
|
||||
}
|
||||
case watch.Modified:
|
||||
updateClusterPolicyGauge(policyGauge, report)
|
||||
|
||||
for _, rule := range oldReport.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.DeleteLabelValues(
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
)
|
||||
ruleGauge.Delete(generateClusterResultLabels(oldReport, rule))
|
||||
}
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.
|
||||
WithLabelValues(
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
).
|
||||
Set(1)
|
||||
ruleGauge.With(generateClusterResultLabels(report, rule)).Set(1)
|
||||
}
|
||||
case watch.Deleted:
|
||||
policyGauge.DeleteLabelValues(report.Name, "Pass")
|
||||
|
@ -80,22 +48,34 @@ func CreateClusterPolicyReportMetricsCallback() report.ClusterPolicyReportCallba
|
|||
policyGauge.DeleteLabelValues(report.Name, "Skip")
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.DeleteLabelValues(
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
)
|
||||
ruleGauge.Delete(generateClusterResultLabels(report, rule))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateClusterResultLabels(report report.ClusterPolicyReport, result report.Result) prometheus.Labels {
|
||||
labels := prometheus.Labels{
|
||||
"rule": result.Rule,
|
||||
"policy": result.Policy,
|
||||
"report": report.Name,
|
||||
"kind": "",
|
||||
"name": "",
|
||||
"status": result.Status,
|
||||
"severity": result.Severity,
|
||||
"category": result.Category,
|
||||
}
|
||||
|
||||
if len(result.Resources) > 0 {
|
||||
res := result.Resources[0]
|
||||
|
||||
labels["kind"] = res.Kind
|
||||
labels["name"] = res.Name
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func updateClusterPolicyGauge(policyGauge *prometheus.GaugeVec, report report.ClusterPolicyReport) {
|
||||
policyGauge.
|
||||
WithLabelValues(report.Name, "Pass").
|
||||
|
|
|
@ -28,54 +28,17 @@ func CreatePolicyReportMetricsCallback() report.PolicyReportCallback {
|
|||
updatePolicyGauge(policyGauge, report)
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.
|
||||
WithLabelValues(
|
||||
report.Namespace,
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
).
|
||||
Set(1)
|
||||
ruleGauge.With(generateResultLabels(report, rule)).Set(1)
|
||||
}
|
||||
case watch.Modified:
|
||||
updatePolicyGauge(policyGauge, report)
|
||||
|
||||
for _, rule := range oldReport.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.DeleteLabelValues(
|
||||
report.Namespace,
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
)
|
||||
ruleGauge.Delete(generateResultLabels(oldReport, rule))
|
||||
}
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
ruleGauge.
|
||||
WithLabelValues(
|
||||
report.Namespace,
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
).
|
||||
Set(1)
|
||||
ruleGauge.With(generateResultLabels(report, rule)).Set(1)
|
||||
}
|
||||
case watch.Deleted:
|
||||
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Pass")
|
||||
|
@ -85,24 +48,35 @@ func CreatePolicyReportMetricsCallback() report.PolicyReportCallback {
|
|||
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Skip")
|
||||
|
||||
for _, rule := range report.Results {
|
||||
res := rule.Resources[0]
|
||||
|
||||
ruleGauge.DeleteLabelValues(
|
||||
report.Namespace,
|
||||
rule.Rule,
|
||||
rule.Policy,
|
||||
report.Name,
|
||||
res.Kind,
|
||||
res.Name,
|
||||
rule.Status,
|
||||
rule.Severity,
|
||||
rule.Category,
|
||||
)
|
||||
ruleGauge.Delete(generateResultLabels(report, rule))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateResultLabels(report report.PolicyReport, result report.Result) prometheus.Labels {
|
||||
labels := prometheus.Labels{
|
||||
"namespace": report.Namespace,
|
||||
"rule": result.Rule,
|
||||
"policy": result.Policy,
|
||||
"report": report.Name,
|
||||
"kind": "",
|
||||
"name": "",
|
||||
"status": result.Status,
|
||||
"severity": result.Severity,
|
||||
"category": result.Category,
|
||||
}
|
||||
|
||||
if len(result.Resources) > 0 {
|
||||
res := result.Resources[0]
|
||||
|
||||
labels["kind"] = res.Kind
|
||||
labels["name"] = res.Name
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
func updatePolicyGauge(policyGauge *prometheus.GaugeVec, report report.PolicyReport) {
|
||||
policyGauge.
|
||||
WithLabelValues(report.Namespace, report.Name, "Pass").
|
||||
|
|
|
@ -3,7 +3,10 @@ package report
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
)
|
||||
|
||||
// Status Enum defined for PolicyReport
|
||||
|
@ -129,15 +132,17 @@ type Resource struct {
|
|||
|
||||
// Result from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReportResult
|
||||
type Result struct {
|
||||
Message string
|
||||
Policy string
|
||||
Rule string
|
||||
Priority Priority
|
||||
Status Status
|
||||
Severity Severity `json:",omitempty"`
|
||||
Category string `json:",omitempty"`
|
||||
Scored bool
|
||||
Resources []Resource
|
||||
Message string
|
||||
Policy string
|
||||
Rule string
|
||||
Priority Priority
|
||||
Status Status
|
||||
Severity Severity `json:",omitempty"`
|
||||
Category string `json:",omitempty"`
|
||||
Scored bool
|
||||
Timestamp time.Time
|
||||
Resources []Resource
|
||||
Properties map[string]string
|
||||
}
|
||||
|
||||
// GetIdentifier returns a global unique Result identifier
|
||||
|
@ -173,6 +178,24 @@ func (pr PolicyReport) GetIdentifier() string {
|
|||
return fmt.Sprintf("%s__%s", pr.Namespace, pr.Name)
|
||||
}
|
||||
|
||||
// ResultHash generates a has of the current result set
|
||||
func (pr PolicyReport) ResultHash() uint64 {
|
||||
list := make([]string, 0, len(pr.Results))
|
||||
|
||||
for id := range pr.Results {
|
||||
list = append(list, id)
|
||||
}
|
||||
|
||||
sort.Strings(list)
|
||||
|
||||
hash, err := hashstructure.Hash(list, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
// GetNewResults filters already existing Results from the old PolicyReport and returns only the diff with new Results
|
||||
func (pr PolicyReport) GetNewResults(or PolicyReport) []Result {
|
||||
diff := make([]Result, 0)
|
||||
|
@ -215,3 +238,21 @@ func (cr ClusterPolicyReport) GetNewResults(cor ClusterPolicyReport) []Result {
|
|||
|
||||
return diff
|
||||
}
|
||||
|
||||
// ResultHash generates a has of the current result set
|
||||
func (cr ClusterPolicyReport) ResultHash() uint64 {
|
||||
list := make([]string, 0, len(cr.Results))
|
||||
|
||||
for id := range cr.Results {
|
||||
list = append(list, id)
|
||||
}
|
||||
|
||||
sort.Strings(list)
|
||||
|
||||
hash, err := hashstructure.Hash(list, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return hash
|
||||
}
|
||||
|
|
|
@ -83,6 +83,28 @@ func Test_PolicyReport(t *testing.T) {
|
|||
t.Error("Expected 1 new result in diff")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Check PolicyReport.ResultHash", func(t *testing.T) {
|
||||
preport := preport
|
||||
preport.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
|
||||
|
||||
hash := preport.ResultHash()
|
||||
if hash != 5971778764232883205 {
|
||||
t.Error("Expected '5971778764232883205' new result in diff")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Check PolicyReport.ResultHash same with different order", func(t *testing.T) {
|
||||
preport1 := preport
|
||||
preport2 := preport
|
||||
|
||||
preport1.Results = map[string]report.Result{result2.GetIdentifier(): result2, result1.GetIdentifier(): result1}
|
||||
preport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
|
||||
|
||||
if preport2.ResultHash() != preport1.ResultHash() {
|
||||
t.Error("Expected same hash with different order")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ClusterPolicyReport(t *testing.T) {
|
||||
|
@ -104,6 +126,28 @@ func Test_ClusterPolicyReport(t *testing.T) {
|
|||
t.Error("Expected 1 new result in diff")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Check PolicyReport.ResultHash", func(t *testing.T) {
|
||||
report1 := creport
|
||||
report1.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
|
||||
|
||||
hash := report1.ResultHash()
|
||||
if hash != 5971778764232883205 {
|
||||
t.Error("Expected '5971778764232883205' new result in diff")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Check PolicyReport.ResultHash same with different order", func(t *testing.T) {
|
||||
report1 := creport
|
||||
report2 := creport
|
||||
|
||||
report1.Results = map[string]report.Result{result2.GetIdentifier(): result2, result1.GetIdentifier(): result1}
|
||||
report2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
|
||||
|
||||
if report2.ResultHash() != report1.ResultHash() {
|
||||
t.Error("Expected same hash with different order")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Result(t *testing.T) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
|
@ -74,6 +75,10 @@ func newPayload(result report.Result) payload {
|
|||
embedFields = append(embedFields, embedField{"API Version", res.APIVersion, true})
|
||||
}
|
||||
|
||||
for property, value := range result.Properties {
|
||||
embedFields = append(embedFields, embedField{strings.Title(property), value, true})
|
||||
}
|
||||
|
||||
embeds := make([]embed, 0, 1)
|
||||
embeds = append(embeds, embed{
|
||||
Title: "New Policy Report Result",
|
||||
|
|
|
@ -3,20 +3,22 @@ package discord_test
|
|||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/discord"
|
||||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Timestamp: time.Date(2021, time.February, 23, 15, 10, 0, 0, time.UTC),
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
|
@ -26,6 +28,7 @@ var completeResult = report.Result{
|
|||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
}
|
||||
|
||||
var minimalResult = report.Result{
|
||||
|
|
|
@ -10,14 +10,15 @@ import (
|
|||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Timestamp: time.Date(2021, time.February, 23, 15, 10, 0, 0, time.UTC),
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
|
@ -27,6 +28,7 @@ var completeResult = report.Result{
|
|||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
|
|
|
@ -32,7 +32,12 @@ type entry struct {
|
|||
}
|
||||
|
||||
func newLokiPayload(result report.Result) payload {
|
||||
le := entry{Ts: time.Now().Format(time.RFC3339), Line: "[" + strings.ToUpper(result.Priority.String()) + "] " + result.Message}
|
||||
timestamp := time.Now()
|
||||
if !result.Timestamp.IsZero() {
|
||||
timestamp = result.Timestamp
|
||||
}
|
||||
|
||||
le := entry{Ts: timestamp.Format(time.RFC3339), Line: "[" + strings.ToUpper(result.Priority.String()) + "] " + result.Message}
|
||||
ls := stream{Entries: []entry{le}}
|
||||
|
||||
res := report.Resource{}
|
||||
|
@ -65,6 +70,10 @@ func newLokiPayload(result report.Result) payload {
|
|||
labels = append(labels, "namespace=\""+res.Namespace+"\"")
|
||||
}
|
||||
|
||||
for property, value := range result.Properties {
|
||||
labels = append(labels, property+"=\""+strings.ReplaceAll(value, "\"", "")+"\"")
|
||||
}
|
||||
|
||||
ls.Labels = "{" + strings.Join(labels, ",") + "}"
|
||||
|
||||
return payload{Streams: []stream{ls}}
|
||||
|
|
|
@ -6,20 +6,22 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/loki"
|
||||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Timestamp: time.Date(2021, time.February, 23, 15, 10, 0, 0, time.UTC),
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
|
@ -29,6 +31,7 @@ var completeResult = report.Result{
|
|||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
}
|
||||
|
||||
var minimalResult = report.Result{
|
||||
|
@ -107,6 +110,9 @@ func Test_LokiTarget(t *testing.T) {
|
|||
if !strings.Contains(labels, "namespace=\""+res.Namespace+"\"") {
|
||||
t.Error("Missing Content for Label 'namespace'")
|
||||
}
|
||||
if !strings.Contains(labels, "version=\""+completeResult.Properties["version"]+"\"") {
|
||||
t.Error("Missing Content for Label 'version'")
|
||||
}
|
||||
}
|
||||
|
||||
loki := loki.NewClient("http://localhost:3100", "", false, testClient{callback, 200})
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
|
@ -148,6 +149,24 @@ func (s *client) newPayload(result report.Result) payload {
|
|||
att.Blocks = append(att.Blocks, block{Type: "section", Fields: []field{{Type: "mrkdwn", Text: "*Namespace*\n" + res.Namespace}}})
|
||||
}
|
||||
|
||||
if len(result.Properties) > 0 {
|
||||
att.Blocks = append(
|
||||
att.Blocks,
|
||||
block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Properties*"}},
|
||||
)
|
||||
|
||||
propBlock := block{
|
||||
Type: "section",
|
||||
Fields: []field{},
|
||||
}
|
||||
|
||||
for property, value := range result.Properties {
|
||||
propBlock.Fields = append(propBlock.Fields, field{Type: "mrkdwn", Text: "*" + strings.Title(property) + "*\n" + value})
|
||||
}
|
||||
|
||||
att.Blocks = append(att.Blocks, propBlock)
|
||||
}
|
||||
|
||||
p.Attachments = append(p.Attachments, att)
|
||||
|
||||
return p
|
||||
|
|
|
@ -3,20 +3,22 @@ package slack_test
|
|||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/slack"
|
||||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Timestamp: time.Date(2021, time.February, 23, 15, 10, 0, 0, time.UTC),
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
|
@ -26,6 +28,7 @@ var completeResult = report.Result{
|
|||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
}
|
||||
|
||||
var minimalResult = report.Result{
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
|
@ -85,10 +86,19 @@ func newPayload(result report.Result) payload {
|
|||
facts = append(facts, fact{"API Version", res.APIVersion})
|
||||
}
|
||||
|
||||
for property, value := range result.Properties {
|
||||
facts = append(facts, fact{strings.Title(property), value})
|
||||
}
|
||||
|
||||
timestamp := time.Now()
|
||||
if !result.Timestamp.IsZero() {
|
||||
timestamp = result.Timestamp
|
||||
}
|
||||
|
||||
sections := make([]section, 0, 1)
|
||||
sections = append(sections, section{
|
||||
Title: "New Policy Report Result",
|
||||
SubTitle: time.Now().Format(time.RFC3339),
|
||||
SubTitle: timestamp.Format(time.RFC3339),
|
||||
Text: result.Message,
|
||||
Facts: facts,
|
||||
})
|
||||
|
|
|
@ -4,20 +4,22 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/teams"
|
||||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.High,
|
||||
Timestamp: time.Date(2021, time.February, 23, 15, 10, 0, 0, time.UTC),
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
|
@ -27,6 +29,7 @@ var completeResult = report.Result{
|
|||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
}
|
||||
|
||||
var minimalErrorResult = report.Result{
|
||||
|
|
Loading…
Reference in a new issue