diff --git a/CHANGELOG.md b/CHANGELOG.md index e6957ef3..cf99aa47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.16.0 + +* New Optional REST API +* New Optional Policy Reporter UI Helm SubChart + ## 0.15.1 * Add a checksum for the target configuration secret to the deployment. This enforces a pod recreation when the configuration changed by a Helm upgrade. diff --git a/README.md b/README.md index 69d43d07..2a1cd5f5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This project is in an early stage. Please let me know if anything did not work a * [Customization](#customization) * [Configure Policy Priorities](#configure-policy-priorities) * [Configure Monitoring](#monitoring) +* [Policy Report UI](#policy-report-ui) ## Installation with Helm v3 @@ -236,6 +237,36 @@ If you are not using the MonitoringStack you can import the dashboards from [Gra ![ClusterPolicyReporter Details Grafana Dashboard](https://github.com/fjogeleit/policy-reporter/blob/main/docs/images/cluster-policy-details.png?raw=true) +## Policy Report UI + +If you don't have any supported Monitoring solution running, you can use the standalone Policy Report UI. + +The UI is provided as optional Helm Sub Chart and can be enabled by setting `ui.enabled` to `true`. + +### Installation + +```bash +helm install policy-reporter policy-reporter/policy-reporter --set ui.enabled=true -n policy-reporter --create-namespace +``` + +### Access it with Port Forward on localhost + +```bash +kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter +``` + +Open `http://localhost:8082/` in your browser. + +### Exmaple + +The UI is an optional application and provides three different views with informations about the validation status of your audit policies. + +![Dashboard](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/dashboard.png?raw=true) + +![Policy Reports](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/policy-report.png?raw=true) + +![ClusterPolicyReports](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/cluster-policy-report.png?raw=true) + # Todos * ~~Support for ClusterPolicyReports~~ * ~~Additional Targets~~ diff --git a/charts/policy-reporter/Chart.yaml b/charts/policy-reporter/Chart.yaml index 0da3f6bc..e8ccc654 100644 --- a/charts/policy-reporter/Chart.yaml +++ b/charts/policy-reporter/Chart.yaml @@ -3,9 +3,11 @@ name: policy-reporter description: K8s PolicyReporter watches for wgpolicyk8s.io/v1alpha1.PolicyReport resources. It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord type: application -version: 0.15.1 -appVersion: 0.11.1 +version: 0.16.0 +appVersion: 0.12.0 dependencies: - name: monitoring - condition: monitoring.enabled \ No newline at end of file + condition: monitoring.enabled + - name: policy-reporter-ui + condition: ui.enabled \ No newline at end of file diff --git a/charts/policy-reporter/charts/policy-reporter-ui/Chart.yaml b/charts/policy-reporter/charts/policy-reporter-ui/Chart.yaml new file mode 100644 index 00000000..dd77d873 --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: policy-reporter-ui +description: Policy Reporter UI + +type: application +version: 0.1.0 +appVersion: 0.1.0 diff --git a/charts/policy-reporter/charts/policy-reporter-ui/templates/_helpers.tpl b/charts/policy-reporter/charts/policy-reporter-ui/templates/_helpers.tpl new file mode 100644 index 00000000..750a79e2 --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ui.fullname" -}} +{{- $name := .Chart.Name }} +{{- if contains .Release.Name $name }} +{{- $name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{- define "ui.name" -}} +{{- "policy-reporter-ui" }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ui.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ui.labels" -}} +helm.sh/chart: {{ include "ui.chart" . }} +{{ include "ui.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ui.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ui.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ui.serviceAccountName" -}} +{{ include "ui.fullname" . }}-sa +{{- end }} diff --git a/charts/policy-reporter/charts/policy-reporter-ui/templates/deployment.yaml b/charts/policy-reporter/charts/policy-reporter-ui/templates/deployment.yaml new file mode 100644 index 00000000..a161039c --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/templates/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ui.fullname" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "ui.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "ui.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ui.serviceAccountName" . }} + automountServiceAccountToken: true + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - -backend={{ .Values.backend }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} diff --git a/charts/policy-reporter/charts/policy-reporter-ui/templates/service.yaml b/charts/policy-reporter/charts/policy-reporter-ui/templates/service.yaml new file mode 100644 index 00000000..6675fa73 --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ui.fullname" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ui.selectorLabels" . | nindent 4 }} diff --git a/charts/policy-reporter/charts/policy-reporter-ui/templates/serviceaccount.yaml b/charts/policy-reporter/charts/policy-reporter-ui/templates/serviceaccount.yaml new file mode 100644 index 00000000..ed4f92ef --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ui.serviceAccountName" . }} + labels: + {{- include "ui.labels" . | nindent 4 }} diff --git a/charts/policy-reporter/charts/policy-reporter-ui/values.yaml b/charts/policy-reporter/charts/policy-reporter-ui/values.yaml new file mode 100644 index 00000000..9828ec40 --- /dev/null +++ b/charts/policy-reporter/charts/policy-reporter-ui/values.yaml @@ -0,0 +1,18 @@ +enabled: false + +image: + repository: fjogeleit/policy-reporter-ui + pullPolicy: IfNotPresent + tag: 0.1.0 + +imagePullSecrets: [] + +backend: http://policy-reporter:8080 + +resources: + requests: + memory: 50Mi + cpu: 10m + limits: + memory: 100Mi + cpu: 50m \ No newline at end of file diff --git a/charts/policy-reporter/templates/deployment.yaml b/charts/policy-reporter/templates/deployment.yaml index 51bdad72..80fb1cf0 100644 --- a/charts/policy-reporter/templates/deployment.yaml +++ b/charts/policy-reporter/templates/deployment.yaml @@ -32,10 +32,18 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} args: - --config=/app/config.yaml + {{- if or .Values.api.enabled .Values.ui.enabled }} + - --apiPort=8080 + {{- end }} ports: - name: http containerPort: 2112 protocol: TCP + {{- if or .Values.api.enabled .Values.ui.enabled }} + - name: rest + containerPort: 8080 + protocol: TCP + {{- end }} livenessProbe: httpGet: path: /metrics diff --git a/charts/policy-reporter/templates/service.yaml b/charts/policy-reporter/templates/service.yaml index 1ac39744..3de55a98 100644 --- a/charts/policy-reporter/templates/service.yaml +++ b/charts/policy-reporter/templates/service.yaml @@ -5,11 +5,17 @@ metadata: labels: {{- include "policyreporter.labels" . | nindent 4 }} spec: - type: {{ .Values.service.type }} + type: ClusterIP ports: - - port: {{ .Values.service.port }} + - port: 2112 targetPort: http protocol: TCP name: http + {{- if or .Values.api.enabled .Values.ui.enabled }} + - port: 8080 + targetPort: rest + protocol: TCP + name: rest + {{- end }} selector: {{- include "policyreporter.selectorLabels" . | nindent 4 }} diff --git a/charts/policy-reporter/values.yaml b/charts/policy-reporter/values.yaml index ef0a4ac7..3233059a 100644 --- a/charts/policy-reporter/values.yaml +++ b/charts/policy-reporter/values.yaml @@ -1,24 +1,20 @@ image: repository: fjogeleit/policy-reporter pullPolicy: IfNotPresent - tag: 0.11.1 + tag: 0.12.0 imagePullSecrets: [] deployment: annotations: {} -service: - type: ClusterIP - port: 2112 - resources: requests: - memory: 50Mi - cpu: 10m + memory: 20Mi + cpu: 5m limits: - memory: 100Mi - cpu: 50m + memory: 30Mi + cpu: 10m monitoring: enabled: false @@ -28,6 +24,9 @@ monitoring: # labels to match the serviceMonitorSelector of the Prometheus Resource labels: {} +api: + enabled: false + loki: # loki host address host: "" diff --git a/cmd/root.go b/cmd/root.go index f14f1ebe..95568fe1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,7 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) { v := viper.New() v.SetDefault("namespace", "policy-reporter") + v.SetDefault("api.port", 8080) cfgFile := "" @@ -62,6 +63,11 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) { v.BindPFlag("kubeconfig", flag) } + if flag := cmd.Flags().Lookup("apiPort"); flag != nil { + v.BindPFlag("api.port", flag) + v.BindPFlag("api.enabled", flag) + } + c := &config.Config{} err := v.Unmarshal(c) diff --git a/cmd/run.go b/cmd/run.go index c1293bfc..b8903693 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -75,6 +75,11 @@ func newRunCMD() *cobra.Command { } g := new(errgroup.Group) + + if c.API.Enabled { + g.Go(resolver.APIServer().Start) + } + g.Go(cpClient.StartWatching) g.Go(pClient.StartWatching) g.Go(func() error { @@ -90,6 +95,7 @@ func newRunCMD() *cobra.Command { // For local usage cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file") cmd.PersistentFlags().StringP("config", "c", "", "target configuration file") + cmd.PersistentFlags().IntP("apiPort", "a", 0, "http port for the optional rest api") cmd.PersistentFlags().String("loki", "", "loki host: http://loki:3100") cmd.PersistentFlags().String("loki-minimum-priority", "", "Minimum Priority to send Results to Loki (info < warning < error)") diff --git a/pkg/api/handler.go b/pkg/api/handler.go new file mode 100644 index 00000000..18ad6525 --- /dev/null +++ b/pkg/api/handler.go @@ -0,0 +1,78 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/fjogeleit/policy-reporter/pkg/report" +) + +// PolicyReportHandler for the PolicyReport REST API +func PolicyReportHandler(s *report.PolicyReportStore) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + reports := s.List() + if len(reports) == 0 { + fmt.Fprint(w, "[]") + + return + } + + apiReports := make([]PolicyReport, 0, len(reports)) + for _, r := range reports { + apiReports = append(apiReports, mapPolicyReport(r)) + } + + if err := json.NewEncoder(w).Encode(apiReports); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{ "message": "%s" }`, err.Error()) + } + } +} + +// ClusterPolicyReportHandler for the ClusterPolicyReport REST API +func ClusterPolicyReportHandler(s *report.ClusterPolicyReportStore) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + reports := s.List() + if len(reports) == 0 { + fmt.Fprint(w, "[]") + + return + } + + apiReports := make([]ClusterPolicyReport, 0, len(reports)) + for _, r := range reports { + apiReports = append(apiReports, mapClusterPolicyReport(r)) + } + + if err := json.NewEncoder(w).Encode(apiReports); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{ "message": "%s" }`, err.Error()) + } + } +} + +// TargetsHandler for the Targets REST API +func TargetsHandler(targets []Target) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + + if len(targets) == 0 { + fmt.Fprint(w, "[]") + + return + } + + if err := json.NewEncoder(w).Encode(targets); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, `{ "message": "%s" }`, err.Error()) + } + } +} diff --git a/pkg/api/handler_test.go b/pkg/api/handler_test.go new file mode 100644 index 00000000..214169d3 --- /dev/null +++ b/pkg/api/handler_test.go @@ -0,0 +1,201 @@ +package api_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/fjogeleit/policy-reporter/pkg/api" + "github.com/fjogeleit/policy-reporter/pkg/report" +) + +func Test_TargetsAPI(t *testing.T) { + t.Run("Empty Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/targets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.TargetsHandler(make([]api.Target, 0))) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[]` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) + t.Run("Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/targets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.TargetsHandler([]api.Target{ + {Name: "Loki", MinimumPriority: "debug", SkipExistingOnStartup: true}, + })) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[{"name":"Loki","minimumPriority":"debug","skipExistingOnStartup":true}]` + if !strings.Contains(rr.Body.String(), expected) { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) +} + +func Test_PolicyReportAPI(t *testing.T) { + t.Run("Empty Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/policy-reports", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.PolicyReportHandler(report.NewPolicyReportStore())) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[]` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) + t.Run("Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/policy-reports", nil) + if err != nil { + t.Fatal(err) + } + + result := 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.ErrorPriority, + Status: report.Fail, + Category: "resources", + Scored: true, + Resources: []report.Resource{ + { + APIVersion: "v1", + Kind: "Deployment", + Name: "nginx", + Namespace: "test", + UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409", + }, + }, + } + + preport := report.PolicyReport{ + Name: "polr-test", + Namespace: "test", + Results: map[string]report.Result{"": result}, + Summary: report.Summary{}, + CreationTimestamp: time.Now(), + } + + store := report.NewPolicyReportStore() + store.Add(preport) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.PolicyReportHandler(store)) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[{"name":"polr-test","namespace":"test","results":[{"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":"error","status":"fail","category":"resources","scored":true,"resource":{"apiVersion":"v1","kind":"Deployment","name":"nginx","namespace":"test","uid":"536ab69f-1b3c-4bd9-9ba4-274a56188409"}}],"summary":{"pass":0,"skip":0,"warn":0,"error":0,"fail":0}` + if !strings.Contains(rr.Body.String(), expected) { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) +} + +func Test_ClusterPolicyReportAPI(t *testing.T) { + t.Run("Empty Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/cluster-policy-reports", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.ClusterPolicyReportHandler(report.NewClusterPolicyReportStore())) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[]` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) + t.Run("Respose", func(t *testing.T) { + req, err := http.NewRequest("GET", "/cluster-policy-reports", nil) + if err != nil { + t.Fatal(err) + } + + result := report.Result{ + Message: "validation error: Namespace label missing", + Policy: "ns-label-env-required", + Rule: "ns-label-required", + Priority: report.ErrorPriority, + Status: report.Fail, + Category: "resources", + Scored: true, + Resources: []report.Resource{ + { + APIVersion: "v1", + Kind: "Namespace", + Name: "dev", + UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409", + }, + }, + } + + creport := report.ClusterPolicyReport{ + Name: "cpolr-test", + Summary: report.Summary{}, + CreationTimestamp: time.Now(), + Results: map[string]report.Result{"": result}, + } + + store := report.NewClusterPolicyReportStore() + store.Add(creport) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(api.ClusterPolicyReportHandler(store)) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := `[{"name":"cpolr-test","results":[{"message":"validation error: Namespace label missing","policy":"ns-label-env-required","rule":"ns-label-required","priority":"error","status":"fail","category":"resources","scored":true,"resource":{"apiVersion":"v1","kind":"Namespace","name":"dev","uid":"536ab69f-1b3c-4bd9-9ba4-274a56188409"}}],"summary":{"pass":0,"skip":0,"warn":0,"error":0,"fail":0}` + if !strings.Contains(rr.Body.String(), expected) { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } + }) +} diff --git a/pkg/api/model.go b/pkg/api/model.go new file mode 100644 index 00000000..fe32ca16 --- /dev/null +++ b/pkg/api/model.go @@ -0,0 +1,152 @@ +package api + +import ( + "time" + + "github.com/fjogeleit/policy-reporter/pkg/report" + "github.com/fjogeleit/policy-reporter/pkg/target" +) + +// Resource API Model +type Resource struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + UID string `json:"uid"` +} + +// Result API Model +type Result struct { + Message string `json:"message"` + Policy string `json:"policy"` + Rule string `json:"rule"` + Priority string `json:"priority"` + Status string `json:"status"` + Severity string `json:"severity,omitempty"` + Category string `json:"category,omitempty"` + Scored bool `json:"scored"` + Resource Resource `json:"resource"` +} + +// Summary API Model +type Summary struct { + Pass int `json:"pass"` + Skip int `json:"skip"` + Warn int `json:"warn"` + Error int `json:"error"` + Fail int `json:"fail"` +} + +// PolicyReport API Model +type PolicyReport struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Results []Result `json:"results"` + Summary Summary `json:"summary"` + CreationTimestamp time.Time `json:"creationTimestamp"` +} + +// ClusterPolicyReport API Model +type ClusterPolicyReport struct { + Name string `json:"name"` + Results []Result `json:"results"` + Summary Summary `json:"summary"` + CreationTimestamp time.Time `json:"creationTimestamp"` +} + +func mapPolicyReport(p report.PolicyReport) PolicyReport { + results := make([]Result, 0, len(p.Results)) + + for _, r := range p.Results { + + results = append(results, Result{ + Message: r.Message, + Policy: r.Policy, + Rule: r.Rule, + Priority: r.Priority.String(), + Status: r.Status, + Severity: r.Severity, + Category: r.Category, + Scored: r.Scored, + Resource: Resource{ + Namespace: r.Resources[0].Namespace, + APIVersion: r.Resources[0].APIVersion, + Kind: r.Resources[0].Kind, + Name: r.Resources[0].Name, + UID: r.Resources[0].UID, + }, + }) + } + + return PolicyReport{ + Name: p.Name, + Namespace: p.Namespace, + CreationTimestamp: p.CreationTimestamp, + Summary: Summary{ + Skip: p.Summary.Skip, + Pass: p.Summary.Pass, + Warn: p.Summary.Warn, + Fail: p.Summary.Fail, + Error: p.Summary.Error, + }, + Results: results, + } +} + +func mapClusterPolicyReport(c report.ClusterPolicyReport) ClusterPolicyReport { + results := make([]Result, 0, len(c.Results)) + + for _, r := range c.Results { + results = append(results, Result{ + Message: r.Message, + Policy: r.Policy, + Rule: r.Rule, + Priority: r.Priority.String(), + Status: r.Status, + Severity: r.Severity, + Category: r.Category, + Scored: r.Scored, + Resource: Resource{ + Namespace: r.Resources[0].Namespace, + APIVersion: r.Resources[0].APIVersion, + Kind: r.Resources[0].Kind, + Name: r.Resources[0].Name, + UID: r.Resources[0].UID, + }, + }) + } + + return ClusterPolicyReport{ + Name: c.Name, + CreationTimestamp: c.CreationTimestamp, + Summary: Summary{ + Skip: c.Summary.Skip, + Pass: c.Summary.Pass, + Warn: c.Summary.Warn, + Fail: c.Summary.Fail, + Error: c.Summary.Error, + }, + Results: results, + } +} + +// Target API Model +type Target struct { + Name string `json:"name"` + MinimumPriority string `json:"minimumPriority"` + SkipExistingOnStartup bool `json:"skipExistingOnStartup"` +} + +func mapTarget(t target.Client) Target { + minPrio := t.MinimumPriority() + if minPrio == "" { + minPrio = report.Priority(report.DebugPriority).String() + } + + return Target{ + Name: t.Name(), + MinimumPriority: minPrio, + SkipExistingOnStartup: t.SkipExistingOnStartup(), + } +} diff --git a/pkg/api/server.go b/pkg/api/server.go new file mode 100644 index 00000000..650b7a3e --- /dev/null +++ b/pkg/api/server.go @@ -0,0 +1,58 @@ +package api + +import ( + "fmt" + "net/http" + + "github.com/fjogeleit/policy-reporter/pkg/report" + "github.com/fjogeleit/policy-reporter/pkg/target" +) + +// Server for the optional HTTP REST API +type Server interface { + // Start the HTTP REST API + Start() error +} + +type httpServer struct { + port int + mux *http.ServeMux + pStore *report.PolicyReportStore + cStore *report.ClusterPolicyReportStore + targets []Target +} + +func (s *httpServer) registerHandler() { + s.mux.HandleFunc("/policy-reports", PolicyReportHandler(s.pStore)) + s.mux.HandleFunc("/cluster-policy-reports", ClusterPolicyReportHandler(s.cStore)) + s.mux.HandleFunc("/targets", TargetsHandler(s.targets)) +} + +func (s *httpServer) Start() error { + server := http.Server{ + Addr: fmt.Sprintf(":%d", s.port), + Handler: s.mux, + } + + return server.ListenAndServe() +} + +// NewServer constructor for a new API Server +func NewServer(pStore *report.PolicyReportStore, cStore *report.ClusterPolicyReportStore, targets []target.Client, port int) Server { + apiTargets := make([]Target, 0, len(targets)) + for _, t := range targets { + apiTargets = append(apiTargets, mapTarget(t)) + } + + s := &httpServer{ + port: port, + targets: apiTargets, + cStore: cStore, + pStore: pStore, + mux: http.NewServeMux(), + } + + s.registerHandler() + + return s +} diff --git a/pkg/api/server_test.go b/pkg/api/server_test.go new file mode 100644 index 00000000..7d9ccdbd --- /dev/null +++ b/pkg/api/server_test.go @@ -0,0 +1,26 @@ +package api_test + +import ( + "net/http" + "testing" + + "github.com/fjogeleit/policy-reporter/pkg/api" + "github.com/fjogeleit/policy-reporter/pkg/report" + "github.com/fjogeleit/policy-reporter/pkg/target" + "github.com/fjogeleit/policy-reporter/pkg/target/discord" + "github.com/fjogeleit/policy-reporter/pkg/target/loki" +) + +func Test_NewServer(t *testing.T) { + server := api.NewServer( + report.NewPolicyReportStore(), + report.NewClusterPolicyReportStore(), + []target.Client{ + loki.NewClient("http://localhost:3100", "debug", true, &http.Client{}), + discord.NewClient("http://webhook:2000", "", false, &http.Client{}), + }, + 8080, + ) + + go server.Start() +} diff --git a/pkg/config/config.go b/pkg/config/config.go index dcf28f98..c54138e2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,12 +30,19 @@ type Discord struct { MinimumPriority string `mapstructure:"minimumPriority"` } +// Server configuration +type API struct { + Enabled bool `mapstructure:"enabled"` + Port int `mapstructure:"port"` +} + // Config of the PolicyReporter type Config struct { Loki Loki `mapstructure:"loki"` Elasticsearch Elasticsearch `mapstructure:"elasticsearch"` Slack Slack `mapstructure:"slack"` Discord Discord `mapstructure:"discord"` + API API `mapstructure:"api"` Kubeconfig string `mapstructure:"kubeconfig"` Namespace string `mapstructure:"namespace"` } diff --git a/pkg/config/resolver.go b/pkg/config/resolver.go index 7f7101cd..3d50c401 100644 --- a/pkg/config/resolver.go +++ b/pkg/config/resolver.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/fjogeleit/policy-reporter/pkg/api" "github.com/fjogeleit/policy-reporter/pkg/kubernetes" "github.com/fjogeleit/policy-reporter/pkg/report" "github.com/fjogeleit/policy-reporter/pkg/target" @@ -23,6 +24,8 @@ type Resolver struct { config *Config k8sConfig *rest.Config mapper kubernetes.Mapper + policyStore *report.PolicyReportStore + clusterPolicyStore *report.ClusterPolicyReportStore resultClient report.ResultClient policyClient report.PolicyClient clusterPolicyClient report.ClusterPolicyClient @@ -32,6 +35,16 @@ type Resolver struct { discordClient target.Client } +// APIServer resolver method +func (r *Resolver) APIServer() api.Server { + return api.NewServer( + r.PolicyReportStore(), + r.ClusterPolicyReportStore(), + r.TargetClients(), + r.config.API.Port, + ) +} + // PolicyResultClient resolver method func (r *Resolver) PolicyResultClient(ctx context.Context) (report.ResultClient, error) { if r.resultClient != nil { @@ -48,11 +61,31 @@ func (r *Resolver) PolicyResultClient(ctx context.Context) (report.ResultClient, return nil, err } - client := kubernetes.NewPolicyResultClient(pClient, cpClient) + r.resultClient = kubernetes.NewPolicyResultClient(pClient, cpClient) - r.resultClient = client + return r.resultClient, nil +} - return client, nil +// PolicyReportStore resolver method +func (r *Resolver) PolicyReportStore() *report.PolicyReportStore { + if r.policyStore != nil { + return r.policyStore + } + + r.policyStore = report.NewPolicyReportStore() + + return r.policyStore +} + +// PolicyReportStore resolver method +func (r *Resolver) ClusterPolicyReportStore() *report.ClusterPolicyReportStore { + if r.clusterPolicyStore != nil { + return r.clusterPolicyStore + } + + r.clusterPolicyStore = report.NewClusterPolicyReportStore() + + return r.clusterPolicyStore } // PolicyReportClient resolver method @@ -73,6 +106,7 @@ func (r *Resolver) PolicyReportClient(ctx context.Context) (report.PolicyClient, client := kubernetes.NewPolicyReportClient( policyAPI, + r.PolicyReportStore(), mapper, time.Now(), ) @@ -98,15 +132,14 @@ func (r *Resolver) ClusterPolicyReportClient(ctx context.Context) (report.Cluste return nil, err } - client := kubernetes.NewClusterPolicyReportClient( + r.clusterPolicyClient = kubernetes.NewClusterPolicyReportClient( policyAPI, + r.ClusterPolicyReportStore(), mapper, time.Now(), ) - r.clusterPolicyClient = client - - return client, nil + return r.clusterPolicyClient, nil } // Mapper resolver method diff --git a/pkg/config/resolver_test.go b/pkg/config/resolver_test.go index 5240b144..632f234b 100644 --- a/pkg/config/resolver_test.go +++ b/pkg/config/resolver_test.go @@ -224,6 +224,15 @@ func Test_ResolveClusterPolicyClient(t *testing.T) { } } +func Test_ResolveAPIServer(t *testing.T) { + resolver := config.NewResolver(testConfig, &rest.Config{}) + + server := resolver.APIServer() + if server == nil { + t.Error("Error: Should return API Server") + } +} + func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) { k8sConfig := &rest.Config{} k8sConfig.Host = "invalid/url" diff --git a/pkg/kubernetes/cluster_policy_report_client.go b/pkg/kubernetes/cluster_policy_report_client.go index 3ff7e1bd..d2d78952 100644 --- a/pkg/kubernetes/cluster_policy_report_client.go +++ b/pkg/kubernetes/cluster_policy_report_client.go @@ -13,7 +13,7 @@ import ( type clusterPolicyReportClient struct { policyAPI PolicyReportAdapter - cache map[string]report.ClusterPolicyReport + store *report.ClusterPolicyReportStore callbacks []report.ClusterPolicyReportCallback resultCallbacks []report.PolicyResultCallback mapper Mapper @@ -91,7 +91,7 @@ func (c *clusterPolicyReportClient) StartWatching() error { func (c *clusterPolicyReportClient) executeClusterPolicyReportHandler(e watch.EventType, cpr report.ClusterPolicyReport) { opr := report.ClusterPolicyReport{} if e != watch.Added { - opr = c.cache[cpr.GetIdentifier()] + opr, _ = c.store.Get(cpr.GetIdentifier()) } wg := sync.WaitGroup{} @@ -112,11 +112,11 @@ func (c *clusterPolicyReportClient) executeClusterPolicyReportHandler(e watch.Ev wg.Wait() if e == watch.Deleted { - delete(c.cache, cpr.GetIdentifier()) + c.store.Remove(cpr.GetIdentifier()) return } - c.cache[cpr.GetIdentifier()] = cpr + c.store.Add(cpr) } func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting bool) { @@ -145,7 +145,7 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo wg.Wait() case watch.Modified: - diff := cpr.GetNewResults(c.cache[cpr.GetIdentifier()]) + diff := cpr.GetNewResults(opr) wg := sync.WaitGroup{} wg.Add(len(diff) * len(c.resultCallbacks)) @@ -165,10 +165,10 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo } // NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client -func NewClusterPolicyReportClient(client PolicyReportAdapter, mapper Mapper, startUp time.Time) report.ClusterPolicyClient { +func NewClusterPolicyReportClient(client PolicyReportAdapter, store *report.ClusterPolicyReportStore, mapper Mapper, startUp time.Time) report.ClusterPolicyClient { return &clusterPolicyReportClient{ policyAPI: client, - cache: make(map[string]report.ClusterPolicyReport), + store: store, mapper: mapper, startUp: startUp, } diff --git a/pkg/kubernetes/cluster_policy_report_client_test.go b/pkg/kubernetes/cluster_policy_report_client_test.go index f4c34037..02d3afdb 100644 --- a/pkg/kubernetes/cluster_policy_report_client_test.go +++ b/pkg/kubernetes/cluster_policy_report_client_test.go @@ -22,6 +22,7 @@ func Test_FetchClusterPolicyReports(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -54,6 +55,7 @@ func Test_FetchClusterPolicyReportsError(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -75,6 +77,7 @@ func Test_FetchClusterPolicyResults(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -99,6 +102,7 @@ func Test_FetchClusterPolicyResultsError(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -116,6 +120,7 @@ func Test_ClusterPolicyWatcher(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -150,6 +155,7 @@ func Test_ClusterPolicyWatcherTwice(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -204,6 +210,7 @@ func Test_SkipExisting(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -243,6 +250,7 @@ func Test_WatcherError(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -262,6 +270,7 @@ func Test_WatchDeleteEvent(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -297,6 +306,7 @@ func Test_WatchModifiedEvent(t *testing.T) { client := kubernetes.NewClusterPolicyReportClient( fakeAdapter, + report.NewClusterPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) diff --git a/pkg/kubernetes/policy_report_client.go b/pkg/kubernetes/policy_report_client.go index 35a5c7c4..33832a11 100644 --- a/pkg/kubernetes/policy_report_client.go +++ b/pkg/kubernetes/policy_report_client.go @@ -13,7 +13,7 @@ import ( type policyReportClient struct { policyAPI PolicyReportAdapter - cache map[string]report.PolicyReport + store *report.PolicyReportStore callbacks []report.PolicyReportCallback resultCallbacks []report.PolicyResultCallback mapper Mapper @@ -91,7 +91,7 @@ func (c *policyReportClient) StartWatching() error { func (c *policyReportClient) executePolicyReportHandler(e watch.EventType, pr report.PolicyReport) { opr := report.PolicyReport{} if e != watch.Added { - opr = c.cache[pr.GetIdentifier()] + opr, _ = c.store.Get(pr.GetIdentifier()) } wg := sync.WaitGroup{} @@ -112,11 +112,11 @@ func (c *policyReportClient) executePolicyReportHandler(e watch.EventType, pr re wg.Wait() if e == watch.Deleted { - delete(c.cache, pr.GetIdentifier()) + c.store.Remove(pr.GetIdentifier()) return } - c.cache[pr.GetIdentifier()] = pr + c.store.Add(pr) } func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) { @@ -149,10 +149,10 @@ func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) { } // NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client -func NewPolicyReportClient(client PolicyReportAdapter, mapper Mapper, startUp time.Time) report.PolicyClient { +func NewPolicyReportClient(client PolicyReportAdapter, store *report.PolicyReportStore, mapper Mapper, startUp time.Time) report.PolicyClient { return &policyReportClient{ policyAPI: client, - cache: make(map[string]report.PolicyReport), + store: store, mapper: mapper, startUp: startUp, } diff --git a/pkg/kubernetes/policy_report_client_test.go b/pkg/kubernetes/policy_report_client_test.go index 616ec56c..a793d0ea 100644 --- a/pkg/kubernetes/policy_report_client_test.go +++ b/pkg/kubernetes/policy_report_client_test.go @@ -21,6 +21,7 @@ func Test_FetchPolicyReports(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -53,6 +54,7 @@ func Test_FetchPolicyReportsError(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -71,6 +73,7 @@ func Test_FetchPolicyResults(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -95,6 +98,7 @@ func Test_FetchPolicyResultsError(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -112,6 +116,7 @@ func Test_PolicyWatcher(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -146,6 +151,7 @@ func Test_PolicyWatcherTwice(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -202,6 +208,7 @@ func Test_PolicySkipExisting(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -242,6 +249,7 @@ func Test_PolicyWatcherError(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -261,6 +269,7 @@ func Test_PolicyWatchDeleteEvent(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) @@ -296,6 +305,7 @@ func Test_PolicyWatchModifiedEvent(t *testing.T) { client := kubernetes.NewPolicyReportClient( fakeAdapter, + report.NewPolicyReportStore(), NewMapper(k8sCMClient), time.Now(), ) diff --git a/pkg/kubernetes/report_client_test.go b/pkg/kubernetes/report_client_test.go index 9c3b786f..20278158 100644 --- a/pkg/kubernetes/report_client_test.go +++ b/pkg/kubernetes/report_client_test.go @@ -65,8 +65,8 @@ func Test_ResultClient_FetchPolicyResults(t *testing.T) { mapper := NewMapper(k8sCMClient) client := kubernetes.NewPolicyResultClient( - kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()), - kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()), + kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()), + kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()), ) fakeAdapter.policies = append(fakeAdapter.policies, unstructured.Unstructured{Object: policyMap}) @@ -92,8 +92,8 @@ func Test_ResultClient_FetchPolicyResultsPolicyReportError(t *testing.T) { mapper := NewMapper(k8sCMClient) client := kubernetes.NewPolicyResultClient( - kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()), - kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()), + kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()), + kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()), ) _, err := client.FetchPolicyResults() @@ -112,8 +112,8 @@ func Test_ResultClient_FetchPolicyResultsClusterPolicyReportError(t *testing.T) mapper := NewMapper(k8sCMClient) client := kubernetes.NewPolicyResultClient( - kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()), - kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()), + kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()), + kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()), ) _, err := client.FetchPolicyResults() @@ -129,8 +129,8 @@ func Test_ResultClient_RegisterPolicyResultWatcher(t *testing.T) { mapper := NewMapper(k8sCMClient) - pClient := kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()) - cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()) + pClient := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()) + cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()) client := kubernetes.NewPolicyResultClient(pClient, cpClient) diff --git a/pkg/report/store.go b/pkg/report/store.go new file mode 100644 index 00000000..4ff7899f --- /dev/null +++ b/pkg/report/store.go @@ -0,0 +1,91 @@ +package report + +import "sync" + +type PolicyReportStore struct { + store map[string]PolicyReport + rwm *sync.RWMutex +} + +func (s *PolicyReportStore) Get(id string) (PolicyReport, bool) { + s.rwm.RLock() + r, ok := s.store[id] + s.rwm.RUnlock() + + return r, ok +} + +func (s *PolicyReportStore) List() []PolicyReport { + s.rwm.RLock() + list := make([]PolicyReport, 0, len(s.store)) + + for _, r := range s.store { + list = append(list, r) + } + s.rwm.RUnlock() + + return list +} + +func (s *PolicyReportStore) Add(r PolicyReport) { + s.rwm.Lock() + s.store[r.GetIdentifier()] = r + s.rwm.Unlock() +} + +func (s *PolicyReportStore) Remove(id string) { + s.rwm.Lock() + delete(s.store, id) + s.rwm.Unlock() +} + +func NewPolicyReportStore() *PolicyReportStore { + return &PolicyReportStore{ + store: map[string]PolicyReport{}, + rwm: new(sync.RWMutex), + } +} + +type ClusterPolicyReportStore struct { + store map[string]ClusterPolicyReport + rwm *sync.RWMutex +} + +func (s *ClusterPolicyReportStore) Get(id string) (ClusterPolicyReport, bool) { + s.rwm.RLock() + r, ok := s.store[id] + s.rwm.RUnlock() + + return r, ok +} + +func (s *ClusterPolicyReportStore) List() []ClusterPolicyReport { + s.rwm.RLock() + list := make([]ClusterPolicyReport, 0, len(s.store)) + + for _, r := range s.store { + list = append(list, r) + } + s.rwm.RUnlock() + + return list +} + +func (s *ClusterPolicyReportStore) Add(r ClusterPolicyReport) { + s.rwm.Lock() + s.store[r.GetIdentifier()] = r + s.rwm.Unlock() +} + +func (s *ClusterPolicyReportStore) Remove(id string) { + s.rwm.Lock() + delete(s.store, id) + s.rwm.Unlock() +} + +func NewClusterPolicyReportStore() *ClusterPolicyReportStore { + return &ClusterPolicyReportStore{ + store: map[string]ClusterPolicyReport{}, + rwm: new(sync.RWMutex), + } +} diff --git a/pkg/report/store_test.go b/pkg/report/store_test.go new file mode 100644 index 00000000..50cca655 --- /dev/null +++ b/pkg/report/store_test.go @@ -0,0 +1,81 @@ +package report_test + +import ( + "testing" + + "github.com/fjogeleit/policy-reporter/pkg/report" +) + +func Test_PolicyReportStore(t *testing.T) { + store := report.NewPolicyReportStore() + + t.Run("Add/Get", func(t *testing.T) { + _, ok := store.Get(preport.GetIdentifier()) + if ok == true { + t.Fatalf("Should not be found in empty Store") + } + + store.Add(preport) + _, ok = store.Get(preport.GetIdentifier()) + if ok == false { + t.Errorf("Should be found in Store after adding report to the store") + } + }) + + t.Run("List", func(t *testing.T) { + items := store.List() + if len(items) != 1 { + t.Errorf("Should return List with the added Report") + } + }) + + t.Run("Delete/Get", func(t *testing.T) { + _, ok := store.Get(preport.GetIdentifier()) + if ok == false { + t.Errorf("Should be found in Store after adding report to the store") + } + + store.Remove(preport.GetIdentifier()) + _, ok = store.Get(preport.GetIdentifier()) + if ok == true { + t.Fatalf("Should not be found after Remove report from Store") + } + }) +} + +func Test_ClusterPolicyReportStore(t *testing.T) { + store := report.NewClusterPolicyReportStore() + + t.Run("Add/Get", func(t *testing.T) { + _, ok := store.Get(creport.GetIdentifier()) + if ok == true { + t.Fatalf("Should not be found in empty Store") + } + + store.Add(creport) + _, ok = store.Get(creport.GetIdentifier()) + if ok == false { + t.Errorf("Should be found in Store after adding report to the store") + } + }) + + t.Run("List", func(t *testing.T) { + items := store.List() + if len(items) != 1 { + t.Errorf("Should return List with the added Report") + } + }) + + t.Run("Delete/Get", func(t *testing.T) { + _, ok := store.Get(creport.GetIdentifier()) + if ok == false { + t.Errorf("Should be found in Store after adding report to the store") + } + + store.Remove(creport.GetIdentifier()) + _, ok = store.Get(creport.GetIdentifier()) + if ok == true { + t.Fatalf("Should not be found after Remove report from Store") + } + }) +} diff --git a/pkg/target/client.go b/pkg/target/client.go index 7451f1cd..c123eb0f 100644 --- a/pkg/target/client.go +++ b/pkg/target/client.go @@ -10,4 +10,8 @@ type Client interface { Send(result report.Result) // SkipExistingOnStartup skips already existing PolicyReportResults on startup SkipExistingOnStartup() bool + // Name is a unique identifier for each Target + Name() string + // MinimumPriority for a triggered Result to send to this target + MinimumPriority() string } diff --git a/pkg/target/discord/discord.go b/pkg/target/discord/discord.go index c5d189f0..10b9800e 100644 --- a/pkg/target/discord/discord.go +++ b/pkg/target/discord/discord.go @@ -127,6 +127,14 @@ func (d *client) SkipExistingOnStartup() bool { return d.skipExistingOnStartup } +func (d *client) Name() string { + return "Discord" +} + +func (d *client) MinimumPriority() string { + return d.minimumPriority +} + // NewClient creates a new loki.client to send Results to Loki func NewClient(webhook, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { return &client{ diff --git a/pkg/target/discord/discord_test.go b/pkg/target/discord/discord_test.go index 0b6a4438..80d58ef4 100644 --- a/pkg/target/discord/discord_test.go +++ b/pkg/target/discord/discord_test.go @@ -106,4 +106,18 @@ func Test_LokiTarget(t *testing.T) { t.Error("Should return configured SkipExistingOnStartup") } }) + t.Run("Name", func(t *testing.T) { + client := discord.NewClient("http://localhost:9200", "", true, testClient{}) + + if client.Name() != "Discord" { + t.Errorf("Unexpected Name %s", client.Name()) + } + }) + t.Run("MinimumPriority", func(t *testing.T) { + client := discord.NewClient("http://localhost:9200", "debug", true, testClient{}) + + if client.MinimumPriority() != "debug" { + t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority()) + } + }) } diff --git a/pkg/target/elasticsearch/elasticsearch.go b/pkg/target/elasticsearch/elasticsearch.go index a33b6985..56a23f04 100644 --- a/pkg/target/elasticsearch/elasticsearch.go +++ b/pkg/target/elasticsearch/elasticsearch.go @@ -76,6 +76,14 @@ func (e *client) SkipExistingOnStartup() bool { return e.skipExistingOnStartup } +func (e *client) Name() string { + return "Elasticsearch" +} + +func (e *client) MinimumPriority() string { + return e.minimumPriority +} + // NewClient creates a new loki.client to send Results to Loki func NewClient(host, index, rotation, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { return &client{ diff --git a/pkg/target/elasticsearch/elasticsearch_test.go b/pkg/target/elasticsearch/elasticsearch_test.go index fbd82675..d7010c62 100644 --- a/pkg/target/elasticsearch/elasticsearch_test.go +++ b/pkg/target/elasticsearch/elasticsearch_test.go @@ -110,4 +110,18 @@ func Test_ElasticsearchTarget(t *testing.T) { t.Error("Should return configured SkipExistingOnStartup") } }) + t.Run("Name", func(t *testing.T) { + client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", true, testClient{}) + + if client.Name() != "Elasticsearch" { + t.Errorf("Unexpected Name %s", client.Name()) + } + }) + t.Run("MinimumPriority", func(t *testing.T) { + client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "debug", true, testClient{}) + + if client.MinimumPriority() != "debug" { + t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority()) + } + }) } diff --git a/pkg/target/loki/loki.go b/pkg/target/loki/loki.go index cb714e40..019f67d8 100644 --- a/pkg/target/loki/loki.go +++ b/pkg/target/loki/loki.go @@ -107,6 +107,14 @@ func (l *client) SkipExistingOnStartup() bool { return l.skipExistingOnStartup } +func (l *client) Name() string { + return "Loki" +} + +func (l *client) MinimumPriority() string { + return l.minimumPriority +} + // NewClient creates a new loki.client to send Results to Loki func NewClient(host, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { return &client{ diff --git a/pkg/target/loki/loki_test.go b/pkg/target/loki/loki_test.go index c037617b..ea8878cc 100644 --- a/pkg/target/loki/loki_test.go +++ b/pkg/target/loki/loki_test.go @@ -190,6 +190,20 @@ func Test_LokiTarget(t *testing.T) { t.Error("Should return configured SkipExistingOnStartup") } }) + t.Run("Name", func(t *testing.T) { + client := loki.NewClient("http://localhost:9200", "", true, testClient{}) + + if client.Name() != "Loki" { + t.Errorf("Unexpected Name %s", client.Name()) + } + }) + t.Run("MinimumPriority", func(t *testing.T) { + client := loki.NewClient("http://localhost:9200", "debug", true, testClient{}) + + if client.MinimumPriority() != "debug" { + t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority()) + } + }) } func convertAndValidateBody(req *http.Request, t *testing.T) (string, string) { diff --git a/pkg/target/slack/slack.go b/pkg/target/slack/slack.go index 2d25970f..030cdffc 100644 --- a/pkg/target/slack/slack.go +++ b/pkg/target/slack/slack.go @@ -180,6 +180,14 @@ func (s *client) SkipExistingOnStartup() bool { return s.skipExistingOnStartup } +func (s *client) Name() string { + return "Slack" +} + +func (s *client) MinimumPriority() string { + return s.minimumPriority +} + // NewClient creates a new slack.client to send Results to Loki func NewClient(host, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { return &client{ diff --git a/pkg/target/slack/slack_test.go b/pkg/target/slack/slack_test.go index 4ce1592b..91235ff6 100644 --- a/pkg/target/slack/slack_test.go +++ b/pkg/target/slack/slack_test.go @@ -106,4 +106,18 @@ func Test_LokiTarget(t *testing.T) { t.Error("Should return configured SkipExistingOnStartup") } }) + t.Run("Name", func(t *testing.T) { + client := slack.NewClient("http://localhost:9200", "", true, testClient{}) + + if client.Name() != "Slack" { + t.Errorf("Unexpected Name %s", client.Name()) + } + }) + t.Run("MinimumPriority", func(t *testing.T) { + client := slack.NewClient("http://localhost:9200", "debug", true, testClient{}) + + if client.MinimumPriority() != "debug" { + t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority()) + } + }) }