mirror of
https://github.com/kyverno/policy-reporter.git
synced 2024-12-14 11:57:32 +00:00
Email Reports (#164)
* add E-Mail reports Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>
This commit is contained in:
parent
7d0440fbca
commit
3e443f126a
48 changed files with 3892 additions and 143 deletions
|
@ -1,7 +1,16 @@
|
|||
# Changelog
|
||||
|
||||
# 2.10.0
|
||||
* Policy Reporter
|
||||
* Email Reports
|
||||
* Send Summary Reports over SMTP to different E-Mails
|
||||
* Supports channels and filters to send different subsets of Namespaces or Sources to dedicated E-Mails
|
||||
* Reports are generated and send over dedicated CronJobs, this makes it easy to send the reports as often as needed
|
||||
* Currently a basic summary and a more detailed violation report is available and can be separatly enabled and configured
|
||||
|
||||
# 2.9.5
|
||||
* Fix Policy Reporter Version in the Helm Chart values.yaml
|
||||
|
||||
# 2.9.4
|
||||
* Policy Reporter
|
||||
* Add [AWS Kinesis](https://aws.amazon.com/kinesis) compatible target
|
||||
|
|
|
@ -24,6 +24,7 @@ WORKDIR /app
|
|||
USER 1234
|
||||
|
||||
COPY --from=builder /app/LICENSE.md .
|
||||
COPY --from=builder /app/templates /app/templates
|
||||
COPY --from=builder /app/build/policyreporter /app/policyreporter
|
||||
# copy the debian's trusted root CA's to the final image
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
|
|
@ -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: 2.9.5
|
||||
appVersion: 2.6.2
|
||||
version: 2.10.0
|
||||
appVersion: 2.7.0
|
||||
|
||||
icon: https://github.com/kyverno/kyverno/raw/main/img/logo.png
|
||||
home: https://kyverno.github.io/policy-reporter
|
||||
|
|
34
charts/policy-reporter/config-email-reports.yaml
Normal file
34
charts/policy-reporter/config-email-reports.yaml
Normal file
|
@ -0,0 +1,34 @@
|
|||
emailReports:
|
||||
clusterName: {{ .Values.emailReports.clusterName }}
|
||||
{{- with .Values.emailReports.smtp }}
|
||||
smtp:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
summary:
|
||||
{{- with .Values.emailReports.summary.to }}
|
||||
to:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.emailReports.summary.filter }}
|
||||
filter:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.emailReports.violations.channels }}
|
||||
channels:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
violations:
|
||||
{{- with .Values.emailReports.violations.to }}
|
||||
to:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.emailReports.violations.filter }}
|
||||
filter:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with .Values.emailReports.violations.channels }}
|
||||
channels:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
|
@ -78,8 +78,8 @@ Create UI target host based on configuration
|
|||
|
||||
{{- define "kyverno.securityContext" -}}
|
||||
{{- if semverCompare "<1.19" .Capabilities.KubeVersion.Version }}
|
||||
{{ toYaml (omit .Values.securityContext "seccompProfile") }}
|
||||
{{- toYaml (omit .Values.securityContext "seccompProfile") }}
|
||||
{{- else }}
|
||||
{{ toYaml .Values.securityContext }}
|
||||
{{- toYaml .Values.securityContext }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{{- if or .Values.emailReports.summary.enabled .Values.emailReports.violations.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "policyreporter.fullname" . }}-config-email-reports
|
||||
{{- if .Values.annotations }}
|
||||
annotations:
|
||||
{{- toYaml .Values.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "policyreporter.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
config.yaml: {{ tpl (.Files.Get "config-email-reports.yaml") . | b64enc }}
|
||||
{{- end }}
|
94
charts/policy-reporter/templates/cronjob-summary-report.yaml
Normal file
94
charts/policy-reporter/templates/cronjob-summary-report.yaml
Normal file
|
@ -0,0 +1,94 @@
|
|||
{{- if .Values.emailReports.summary.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "policyreporter.fullname" . }}-summary-report
|
||||
labels:
|
||||
{{- include "policyreporter.labels" . | nindent 4 }}
|
||||
{{- if .Values.annotations }}
|
||||
annotations:
|
||||
{{- toYaml .Values.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
schedule: {{ .Values.emailReports.summary.schedule | quote }}
|
||||
jobTemplate:
|
||||
spec:
|
||||
activeDeadlineSeconds: {{ .Values.emailReports.summary.activeDeadlineSeconds }}
|
||||
backoffLimit: {{ .Values.emailReports.summary.backoffLimit }}
|
||||
{{- if gt (.Values.emailReports.summary.ttlSecondsAfterFinished | toString | atoi) 0 }}
|
||||
ttlSecondsAfterFinished: {{ .Values.emailReports.summary.ttlSecondsAfterFinished }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "policyreporter.selectorLabels" . | nindent 12 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.global.labels }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
checksum/secret: {{ include (print .Template.BasePath "/config-email-reports-secret.yaml") . | sha256sum | quote }}
|
||||
{{- with .Values.annotations }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "policyreporter.serviceAccountName" . }}
|
||||
automountServiceAccountToken: true
|
||||
{{- if .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 12 }}
|
||||
{{- end }}
|
||||
restartPolicy: {{ .Values.emailReports.summary.restartPolicy }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if .Values.securityContext }}
|
||||
securityContext: {{ include "kyverno.securityContext" . | nindent 16 }}
|
||||
{{- end }}
|
||||
command:
|
||||
- /app/policyreporter
|
||||
- send
|
||||
- summary
|
||||
args:
|
||||
- --config=/app/config.yaml
|
||||
- --template-dir=/app/templates
|
||||
volumeMounts:
|
||||
- name: sqlite
|
||||
mountPath: /sqlite
|
||||
- name: config-file
|
||||
mountPath: /app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
value: {{ .Release.Namespace }}
|
||||
volumes:
|
||||
- name: sqlite
|
||||
emptyDir: {}
|
||||
- name: config-file
|
||||
secret:
|
||||
secretName: {{ include "policyreporter.fullname" . }}-config-email-reports
|
||||
optional: true
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -0,0 +1,94 @@
|
|||
{{- if .Values.emailReports.violations.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ include "policyreporter.fullname" . }}-violations-report
|
||||
labels:
|
||||
{{- include "policyreporter.labels" . | nindent 4 }}
|
||||
{{- if .Values.annotations }}
|
||||
annotations:
|
||||
{{- toYaml .Values.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
schedule: {{ .Values.emailReports.violations.schedule | quote }}
|
||||
jobTemplate:
|
||||
spec:
|
||||
activeDeadlineSeconds: {{ .Values.emailReports.violations.activeDeadlineSeconds }}
|
||||
backoffLimit: {{ .Values.emailReports.violations.backoffLimit }}
|
||||
{{- if gt (.Values.emailReports.violations.ttlSecondsAfterFinished | toString | atoi) 0 }}
|
||||
ttlSecondsAfterFinished: {{ .Values.emailReports.violations.ttlSecondsAfterFinished }}
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "policyreporter.selectorLabels" . | nindent 12 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.global.labels }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
checksum/secret: {{ include (print .Template.BasePath "/config-email-reports-secret.yaml") . | sha256sum | quote }}
|
||||
{{- with .Values.annotations }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "policyreporter.serviceAccountName" . }}
|
||||
automountServiceAccountToken: true
|
||||
{{- if .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 12 }}
|
||||
{{- end }}
|
||||
restartPolicy: {{ .Values.emailReports.violations.restartPolicy }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if .Values.securityContext }}
|
||||
securityContext: {{ include "kyverno.securityContext" . | nindent 16 }}
|
||||
{{- end }}
|
||||
command:
|
||||
- /app/policyreporter
|
||||
- send
|
||||
- violations
|
||||
args:
|
||||
- --config=/app/config.yaml
|
||||
- --template-dir=/app/templates
|
||||
volumeMounts:
|
||||
- name: sqlite
|
||||
mountPath: /sqlite
|
||||
- name: config-file
|
||||
mountPath: /app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
value: {{ .Release.Namespace }}
|
||||
volumes:
|
||||
- name: sqlite
|
||||
emptyDir: {}
|
||||
- name: config-file
|
||||
secret:
|
||||
secretName: {{ include "policyreporter.fullname" . }}-config-email-reports
|
||||
optional: true
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -2,7 +2,7 @@ image:
|
|||
registry: ghcr.io
|
||||
repository: kyverno/policy-reporter
|
||||
pullPolicy: IfNotPresent
|
||||
tag: 2.6.2
|
||||
tag: 2.7.0
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
|
@ -154,6 +154,50 @@ global:
|
|||
# require-ns-labels: error
|
||||
policyPriorities: {}
|
||||
|
||||
emailReports:
|
||||
clusterName: "" # (optional) - displayed in the E-Mail Report if configured
|
||||
smtp:
|
||||
host: ""
|
||||
port: 465
|
||||
username: ""
|
||||
password: ""
|
||||
from: "" # Displayed From E-Mail Address
|
||||
encryption: "" # ssl/tls / starttls
|
||||
|
||||
# basic summary report
|
||||
summary:
|
||||
enabled: false
|
||||
schedule: "* 8 * * *" # CronJob schedule defines when the report will be send
|
||||
activeDeadlineSeconds: 300 # timeout in seconds
|
||||
backoffLimit: 3 # retry counter
|
||||
ttlSecondsAfterFinished: 0
|
||||
restartPolicy: Never # pod restart policy
|
||||
|
||||
to: [] # list of receiver e-mail addresses
|
||||
filter: {} # optional filters
|
||||
# namespaces:
|
||||
# include: []
|
||||
# exclude: []
|
||||
# sources: ['Kyverno']
|
||||
channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses
|
||||
|
||||
# violation summary report
|
||||
violations:
|
||||
enabled: false
|
||||
schedule: "* 8 * * *" # CronJob schedule defines when the report will be send
|
||||
activeDeadlineSeconds: 300 # timeout in seconds
|
||||
backoffLimit: 3 # retry counter
|
||||
ttlSecondsAfterFinished: 0
|
||||
restartPolicy: Never # pod restart policy
|
||||
|
||||
to: [] # list of receiver e-mail addresses
|
||||
filter: {} # optional filters
|
||||
# namespaces:
|
||||
# include: []
|
||||
# exclude: []
|
||||
# sources: ['Kyverno']
|
||||
channels: [] # (optional) channels can be used to to send only a subset of namespaces / sources to dedicated email addresses
|
||||
|
||||
# Reference a configuration which already exists instead of creating one
|
||||
existingTargetConfig:
|
||||
enabled: false
|
||||
|
|
63
cmd/root.go
63
cmd/root.go
|
@ -1,11 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// NewCLI creates a new instance of the root CLI
|
||||
|
@ -18,64 +14,7 @@ func NewCLI() *cobra.Command {
|
|||
}
|
||||
|
||||
rootCmd.AddCommand(newRunCMD())
|
||||
rootCmd.AddCommand(newSendCMD())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
func loadConfig(cmd *cobra.Command) (*config.Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
cfgFile := ""
|
||||
|
||||
configFlag := cmd.Flags().Lookup("config")
|
||||
if configFlag != nil {
|
||||
cfgFile = configFlag.Value.String()
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
v.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName("config")
|
||||
}
|
||||
|
||||
v.AutomaticEnv()
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
log.Println("[INFO] No configuration file found")
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("kubeconfig"); flag != nil {
|
||||
v.BindPFlag("kubeconfig", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("port"); flag != nil {
|
||||
v.BindPFlag("api.port", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("rest-enabled"); flag != nil {
|
||||
v.BindPFlag("rest.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("metrics-enabled"); flag != nil {
|
||||
v.BindPFlag("metrics.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("profile"); flag != nil {
|
||||
v.BindPFlag("profiling.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("dbfile"); flag != nil {
|
||||
v.BindPFlag("dbfile", flag)
|
||||
}
|
||||
|
||||
c := &config.Config{}
|
||||
|
||||
err := v.Unmarshal(c)
|
||||
|
||||
if c.DBFile == "" {
|
||||
c.DBFile = "sqlite-database.db"
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import (
|
|||
"flag"
|
||||
"log"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
@ -17,7 +17,7 @@ func newRunCMD() *cobra.Command {
|
|||
Use: "run",
|
||||
Short: "Run PolicyReporter Watcher & HTTP Metrics Server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := loadConfig(cmd)
|
||||
c, err := config.Load(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
26
cmd/send.go
Normal file
26
cmd/send.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/kyverno/policy-reporter/cmd/send"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newSendCMD() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "send",
|
||||
Short: "Send different kinds of email reports",
|
||||
}
|
||||
|
||||
// For local usage
|
||||
cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file")
|
||||
cmd.PersistentFlags().StringP("config", "c", "", "target configuration file")
|
||||
cmd.PersistentFlags().StringP("template-dir", "t", "./templates", "template directory for email reports")
|
||||
cmd.AddCommand(send.NewSummaryCMD())
|
||||
cmd.AddCommand(send.NewViolationsCMD())
|
||||
|
||||
flag.Parse()
|
||||
|
||||
return cmd
|
||||
}
|
106
cmd/send/summary.go
Normal file
106
cmd/send/summary.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package send
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/config"
|
||||
"github.com/kyverno/policy-reporter/pkg/email/summary"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func NewSummaryCMD() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "summary",
|
||||
Short: "Send a summary e-mail to the configured emails",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := config.Load(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var k8sConfig *rest.Config
|
||||
if c.Kubeconfig != "" {
|
||||
k8sConfig, err = clientcmd.BuildConfigFromFlags("", c.Kubeconfig)
|
||||
} else {
|
||||
k8sConfig, err = rest.InClusterConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolver := config.NewResolver(c, k8sConfig)
|
||||
|
||||
generator, err := resolver.SummaryGenerator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := generator.GenerateData(cmd.Context())
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to generate report data: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
reporter := resolver.SummaryReporter()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1 + len(c.EmailReports.Summary.Channels))
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if len(c.EmailReports.Summary.To) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
report, err := reporter.Report(data, c.EmailReports.Summary.Format)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to create report: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = resolver.EmailClient().Send(report, c.EmailReports.Summary.To)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to send report: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, ch := range c.EmailReports.Violations.Channels {
|
||||
go func(channel config.EmailReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(channel.To) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sources := summary.FilterSources(data, config.EmailReportFilterFromConfig(channel.Filter), !channel.DisableClusterReports)
|
||||
if len(sources) == 0 {
|
||||
log.Printf("[INFO] skip email - no results to send")
|
||||
return
|
||||
}
|
||||
|
||||
report, err := reporter.Report(sources, channel.Format)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to create report: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = resolver.EmailClient().Send(report, channel.To)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to send report: %s\n", err)
|
||||
}
|
||||
}(ch)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
105
cmd/send/violations.go
Normal file
105
cmd/send/violations.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package send
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/config"
|
||||
"github.com/kyverno/policy-reporter/pkg/email/violations"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func NewViolationsCMD() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "violations",
|
||||
Short: "Send a violations e-mail to the configured emails",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := config.Load(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var k8sConfig *rest.Config
|
||||
if c.Kubeconfig != "" {
|
||||
k8sConfig, err = clientcmd.BuildConfigFromFlags("", c.Kubeconfig)
|
||||
} else {
|
||||
k8sConfig, err = rest.InClusterConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolver := config.NewResolver(c, k8sConfig)
|
||||
|
||||
generator, err := resolver.ViolationsGenerator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := generator.GenerateData(cmd.Context())
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to generate report data: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
reporter := resolver.ViolationsReporter()
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1 + len(c.EmailReports.Violations.Channels))
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if len(c.EmailReports.Violations.To) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
report, err := reporter.Report(data, c.EmailReports.Violations.Format)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to create report: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = resolver.EmailClient().Send(report, c.EmailReports.Violations.To)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to send report: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, ch := range c.EmailReports.Violations.Channels {
|
||||
go func(channel config.EmailReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(channel.To) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sources := violations.FilterSources(data, config.EmailReportFilterFromConfig(channel.Filter), !channel.DisableClusterReports)
|
||||
if len(sources) == 0 {
|
||||
log.Printf("[INFO] skip email - no results to send")
|
||||
}
|
||||
|
||||
report, err := reporter.Report(sources, channel.Format)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to create report: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = resolver.EmailClient().Send(report, channel.To)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to send report: %s\n", err)
|
||||
}
|
||||
}(ch)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
2
go.mod
2
go.mod
|
@ -54,6 +54,8 @@ require (
|
|||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -504,9 +504,13 @@ github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs
|
|||
github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
|
@ -5,6 +5,11 @@ type ValueFilter struct {
|
|||
Exclude []string `mapstructure:"exclude"`
|
||||
}
|
||||
|
||||
type EmailReportFilter struct {
|
||||
Namespaces ValueFilter `mapstructure:"namespaces"`
|
||||
Sources []string `mapstructure:"sources"`
|
||||
}
|
||||
|
||||
type TargetFilter struct {
|
||||
Namespaces ValueFilter `mapstructure:"namespaces"`
|
||||
Priorities ValueFilter `mapstructure:"priorities"`
|
||||
|
@ -97,6 +102,7 @@ type Webhook struct {
|
|||
Channels []Webhook `mapstructure:"channels"`
|
||||
}
|
||||
|
||||
// S3 configuration
|
||||
type S3 struct {
|
||||
Name string `mapstructure:"name"`
|
||||
AccessKeyID string `mapstructure:"accessKeyID"`
|
||||
|
@ -112,6 +118,7 @@ type S3 struct {
|
|||
Channels []S3 `mapstructure:"channels"`
|
||||
}
|
||||
|
||||
// Kinesis configuration
|
||||
type Kinesis struct {
|
||||
Name string `mapstructure:"name"`
|
||||
AccessKeyID string `mapstructure:"accessKeyID"`
|
||||
|
@ -126,6 +133,39 @@ type Kinesis struct {
|
|||
Channels []Kinesis `mapstructure:"channels"`
|
||||
}
|
||||
|
||||
// SMTP configuration
|
||||
type SMTP struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
From string `mapstructure:"from"`
|
||||
Encryption string `mapstructure:"encryption"`
|
||||
}
|
||||
|
||||
// EmailReport configuration
|
||||
type EmailReport struct {
|
||||
To []string `mapstructure:"to"`
|
||||
Format string `mapstructure:"format"`
|
||||
Filter EmailReportFilter `mapstructure:"filter"`
|
||||
Channels []EmailReport `mapstructure:"channels"`
|
||||
DisableClusterReports bool `mapstructure:"disableClusterReports"`
|
||||
}
|
||||
|
||||
// EmailReport configuration
|
||||
type EmailTemplates struct {
|
||||
Dir string `mapstructure:"dir"`
|
||||
}
|
||||
|
||||
// EmailReports configuration
|
||||
type EmailReports struct {
|
||||
SMTP SMTP `mapstructure:"smtp"`
|
||||
Templates EmailTemplates `mapstructure:"templates"`
|
||||
Summary EmailReport `mapstructure:"summary"`
|
||||
Violations EmailReport `mapstructure:"violations"`
|
||||
ClusterName string `mapstructure:"clusterName"`
|
||||
}
|
||||
|
||||
// API configuration
|
||||
type API struct {
|
||||
Port int `mapstructure:"port"`
|
||||
|
@ -191,4 +231,5 @@ type Config struct {
|
|||
ReportFilter ReportFilter `mapstructure:"reportFilter"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
Profiling Profiling `mapstructure:"profiling"`
|
||||
EmailReports EmailReports `mapstructure:"emailReports"`
|
||||
}
|
||||
|
|
70
pkg/config/load.go
Normal file
70
pkg/config/load.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func Load(cmd *cobra.Command) (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
cfgFile := ""
|
||||
|
||||
configFlag := cmd.Flags().Lookup("config")
|
||||
if configFlag != nil {
|
||||
cfgFile = configFlag.Value.String()
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
v.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
v.AddConfigPath(".")
|
||||
v.SetConfigName("config")
|
||||
}
|
||||
|
||||
v.AutomaticEnv()
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
log.Println("[INFO] No configuration file found")
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("kubeconfig"); flag != nil {
|
||||
v.BindPFlag("kubeconfig", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("port"); flag != nil {
|
||||
v.BindPFlag("api.port", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("rest-enabled"); flag != nil {
|
||||
v.BindPFlag("rest.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("metrics-enabled"); flag != nil {
|
||||
v.BindPFlag("metrics.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("profile"); flag != nil {
|
||||
v.BindPFlag("profiling.enabled", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("dbfile"); flag != nil {
|
||||
v.BindPFlag("dbfile", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("template-dir"); flag != nil {
|
||||
v.BindPFlag("emailReports.templates.dir", flag)
|
||||
}
|
||||
|
||||
c := &Config{}
|
||||
|
||||
err := v.Unmarshal(c)
|
||||
|
||||
if c.DBFile == "" {
|
||||
c.DBFile = "sqlite-database.db"
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
62
pkg/config/load_test.go
Normal file
62
pkg/config/load_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func createCMD() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
|
||||
cmd.Flags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file")
|
||||
cmd.Flags().StringP("config", "c", "", "target configuration file")
|
||||
cmd.Flags().IntP("port", "p", 8080, "http port for the optional rest api")
|
||||
cmd.Flags().StringP("dbfile", "d", "sqlite-database.db", "path to the SQLite DB File")
|
||||
cmd.Flags().BoolP("metrics-enabled", "m", false, "Enable Policy Reporter's Metrics API")
|
||||
cmd.Flags().BoolP("rest-enabled", "r", false, "Enable Policy Reporter's REST API")
|
||||
cmd.Flags().Bool("profile", false, "Enable application profiling with pprof")
|
||||
cmd.Flags().StringP("template-dir", "t", "./templates", "template directory for email reports")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func Test_Load(t *testing.T) {
|
||||
cmd := createCMD()
|
||||
|
||||
_ = cmd.Flags().Set("kubeconfig", "./config")
|
||||
_ = cmd.Flags().Set("port", "8081")
|
||||
_ = cmd.Flags().Set("rest-enabled", "1")
|
||||
_ = cmd.Flags().Set("metrics-enabled", "1")
|
||||
_ = cmd.Flags().Set("profile", "1")
|
||||
_ = cmd.Flags().Set("template-dir", "/app/templates")
|
||||
_ = cmd.Flags().Set("dbfile", "")
|
||||
|
||||
c, err := config.Load(cmd)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected Error: %s", err)
|
||||
}
|
||||
|
||||
if c.Kubeconfig != "./config" {
|
||||
t.Errorf("Unexpected TemplateDir Config: %s", c.Kubeconfig)
|
||||
}
|
||||
if c.API.Port != 8081 {
|
||||
t.Errorf("Unexpected Port Config: %d", c.API.Port)
|
||||
}
|
||||
if c.REST.Enabled != true {
|
||||
t.Errorf("Unexpected REST Config: %v", c.REST.Enabled)
|
||||
}
|
||||
if c.Metrics.Enabled != true {
|
||||
t.Errorf("Unexpected Metrics Config: %v", c.Metrics.Enabled)
|
||||
}
|
||||
if c.Profiling.Enabled != true {
|
||||
t.Errorf("Unexpected Profiling Config: %v", c.Profiling.Enabled)
|
||||
}
|
||||
if c.EmailReports.Templates.Dir != "/app/templates" {
|
||||
t.Errorf("Unexpected TemplateDir Config: %s", c.EmailReports.Templates.Dir)
|
||||
}
|
||||
if c.DBFile != "sqlite-database.db" {
|
||||
t.Errorf("Unexpected DBFile Config: %s", c.DBFile)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,10 @@ import (
|
|||
|
||||
"github.com/kyverno/policy-reporter/pkg/api"
|
||||
"github.com/kyverno/policy-reporter/pkg/cache"
|
||||
"github.com/kyverno/policy-reporter/pkg/email"
|
||||
"github.com/kyverno/policy-reporter/pkg/email/summary"
|
||||
"github.com/kyverno/policy-reporter/pkg/email/violations"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
"github.com/kyverno/policy-reporter/pkg/helper"
|
||||
"github.com/kyverno/policy-reporter/pkg/kubernetes"
|
||||
"github.com/kyverno/policy-reporter/pkg/listener"
|
||||
|
@ -26,9 +30,11 @@ import (
|
|||
"github.com/kyverno/policy-reporter/pkg/target/teams"
|
||||
"github.com/kyverno/policy-reporter/pkg/target/ui"
|
||||
"github.com/kyverno/policy-reporter/pkg/target/webhook"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
|
||||
goredis "github.com/go-redis/redis/v8"
|
||||
"github.com/kyverno/kyverno/pkg/client/clientset/versioned"
|
||||
wgpolicyk8sv1alpha2 "github.com/kyverno/kyverno/pkg/client/clientset/versioned/typed/policyreport/v1alpha2"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
@ -376,6 +382,72 @@ func (r *Resolver) SkipExistingOnStartup() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (r *Resolver) CRDClient() (wgpolicyk8sv1alpha2.Wgpolicyk8sV1alpha2Interface, error) {
|
||||
client, err := versioned.NewForConfig(r.k8sConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.Wgpolicyk8sV1alpha2(), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) SummaryGenerator() (*summary.Generator, error) {
|
||||
client, err := r.CRDClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return summary.NewGenerator(
|
||||
client,
|
||||
EmailReportFilterFromConfig(r.config.EmailReports.Summary.Filter),
|
||||
!r.config.EmailReports.Summary.DisableClusterReports,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) SummaryReporter() *summary.Reporter {
|
||||
return summary.NewReporter(
|
||||
r.config.EmailReports.Templates.Dir,
|
||||
r.config.EmailReports.ClusterName,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Resolver) ViolationsGenerator() (*violations.Generator, error) {
|
||||
client, err := r.CRDClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return violations.NewGenerator(
|
||||
client,
|
||||
EmailReportFilterFromConfig(r.config.EmailReports.Violations.Filter),
|
||||
!r.config.EmailReports.Violations.DisableClusterReports,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) ViolationsReporter() *violations.Reporter {
|
||||
return violations.NewReporter(
|
||||
r.config.EmailReports.Templates.Dir,
|
||||
r.config.EmailReports.ClusterName,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Resolver) SMTPServer() *mail.SMTPServer {
|
||||
server := mail.NewSMTPClient()
|
||||
server.Host = r.config.EmailReports.SMTP.Host
|
||||
server.Port = r.config.EmailReports.SMTP.Port
|
||||
server.Username = r.config.EmailReports.SMTP.Username
|
||||
server.Password = r.config.EmailReports.SMTP.Password
|
||||
server.ConnectTimeout = 10 * time.Second
|
||||
server.SendTimeout = 10 * time.Second
|
||||
server.Encryption = email.EncryptionFromString(r.config.EmailReports.SMTP.Encryption)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (r *Resolver) EmailClient() *email.Client {
|
||||
return email.NewClient(r.config.EmailReports.SMTP.From, r.SMTPServer())
|
||||
}
|
||||
|
||||
func (r *Resolver) PolicyReportClient() (report.PolicyReportClient, error) {
|
||||
if r.policyReportClient != nil {
|
||||
return r.policyReportClient, nil
|
||||
|
@ -736,21 +808,31 @@ func createKinesisClient(config Kinesis, parent Kinesis) target.Client {
|
|||
)
|
||||
}
|
||||
|
||||
func createTargetFilter(filter TargetFilter, minimumPriority string, sources []string) *target.Filter {
|
||||
func createTargetFilter(fil TargetFilter, minimumPriority string, sources []string) *target.Filter {
|
||||
return &target.Filter{
|
||||
MinimumPriority: minimumPriority,
|
||||
Sources: sources,
|
||||
Namespace: target.Rules{
|
||||
Include: filter.Namespaces.Include,
|
||||
Exclude: filter.Namespaces.Exclude,
|
||||
Namespace: filter.Rules{
|
||||
Include: fil.Namespaces.Include,
|
||||
Exclude: fil.Namespaces.Exclude,
|
||||
},
|
||||
Priority: target.Rules{
|
||||
Include: filter.Priorities.Include,
|
||||
Exclude: filter.Priorities.Exclude,
|
||||
Priority: filter.Rules{
|
||||
Include: fil.Priorities.Include,
|
||||
Exclude: fil.Priorities.Exclude,
|
||||
},
|
||||
Policy: target.Rules{
|
||||
Include: filter.Policies.Include,
|
||||
Exclude: filter.Policies.Exclude,
|
||||
Policy: filter.Rules{
|
||||
Include: fil.Policies.Include,
|
||||
Exclude: fil.Policies.Exclude,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func EmailReportFilterFromConfig(config EmailReportFilter) filter.Filter {
|
||||
return filter.New(
|
||||
filter.Rules{
|
||||
Include: config.Namespaces.Include,
|
||||
Exclude: config.Namespaces.Exclude,
|
||||
},
|
||||
config.Sources,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -94,6 +94,19 @@ var testConfig = &config.Config{
|
|||
Region: "ru-central1",
|
||||
Channels: []config.Kinesis{{}},
|
||||
},
|
||||
EmailReports: config.EmailReports{
|
||||
Templates: config.EmailTemplates{
|
||||
Dir: "../../templates",
|
||||
},
|
||||
SMTP: config.SMTP{
|
||||
Host: "localhost",
|
||||
Port: 465,
|
||||
Username: "policy-reporter@kyverno.io",
|
||||
Password: "password",
|
||||
From: "policy-reporter@kyverno.io",
|
||||
Encryption: "ssl/tls",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_ResolveTarget(t *testing.T) {
|
||||
|
@ -487,6 +500,27 @@ func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_ResolveCRDClient(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
|
||||
_, err := resolver.CRDClient()
|
||||
if err != nil {
|
||||
t.Error("unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResolveCRDClientWithInvalidK8sConfig(t *testing.T) {
|
||||
k8sConfig := &rest.Config{}
|
||||
k8sConfig.Host = "invalid/url"
|
||||
|
||||
resolver := config.NewResolver(testConfig, k8sConfig)
|
||||
|
||||
_, err := resolver.CRDClient()
|
||||
if err == nil {
|
||||
t.Error("Error: 'host must be a URL or a host:port pair' was expected")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RegisterStoreListener(t *testing.T) {
|
||||
t.Run("Register StoreListener", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
|
@ -528,3 +562,82 @@ func Test_RegisterSendResultListener(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_SummaryReportServices(t *testing.T) {
|
||||
t.Run("Generator", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
generator, err := resolver.SummaryGenerator()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if generator == nil {
|
||||
t.Error("Should return Generator Pointer")
|
||||
}
|
||||
})
|
||||
t.Run("Generator.Error", func(t *testing.T) {
|
||||
k8sConfig := &rest.Config{}
|
||||
k8sConfig.Host = "invalid/url"
|
||||
|
||||
resolver := config.NewResolver(testConfig, k8sConfig)
|
||||
|
||||
_, err := resolver.SummaryGenerator()
|
||||
if err == nil {
|
||||
t.Error("Error: 'host must be a URL or a host:port pair' was expected")
|
||||
}
|
||||
})
|
||||
t.Run("Reporter", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
reporter := resolver.SummaryReporter()
|
||||
if reporter == nil {
|
||||
t.Error("Should return Reporter Pointer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ViolationReportServices(t *testing.T) {
|
||||
t.Run("Generator", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
generator, err := resolver.ViolationsGenerator()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if generator == nil {
|
||||
t.Error("Should return Generator Pointer")
|
||||
}
|
||||
})
|
||||
t.Run("Generator.Error", func(t *testing.T) {
|
||||
k8sConfig := &rest.Config{}
|
||||
k8sConfig.Host = "invalid/url"
|
||||
|
||||
resolver := config.NewResolver(testConfig, k8sConfig)
|
||||
|
||||
_, err := resolver.ViolationsGenerator()
|
||||
if err == nil {
|
||||
t.Error("Error: 'host must be a URL or a host:port pair' was expected")
|
||||
}
|
||||
})
|
||||
t.Run("Reporter", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
reporter := resolver.ViolationsReporter()
|
||||
if reporter == nil {
|
||||
t.Error("Should return Reporter Pointer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_SMTP(t *testing.T) {
|
||||
t.Run("SMTP", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
smtp := resolver.SMTPServer()
|
||||
if smtp == nil {
|
||||
t.Error("Should return SMTP Pointer")
|
||||
}
|
||||
})
|
||||
t.Run("EmailClient", func(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
client := resolver.EmailClient()
|
||||
if client == nil {
|
||||
t.Error("Should return EmailClient Pointer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
63
pkg/email/client.go
Normal file
63
pkg/email/client.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
func EncryptionFromString(enc string) mail.Encryption {
|
||||
switch strings.ToLower(enc) {
|
||||
case "ssl/tls":
|
||||
return mail.EncryptionSSLTLS
|
||||
case "starttls":
|
||||
return mail.EncryptionSTARTTLS
|
||||
default:
|
||||
return mail.EncryptionNone
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
server *mail.SMTPServer
|
||||
from string
|
||||
}
|
||||
|
||||
func (c *Client) Send(report Report, to []string) error {
|
||||
if len(to) > 1 {
|
||||
c.server.KeepAlive = true
|
||||
}
|
||||
|
||||
client, err := c.server.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, to := range to {
|
||||
msg := mail.NewMSG().
|
||||
SetFrom(fmt.Sprintf("Policy Reporter <%s>", c.from)).
|
||||
AddTo(to).
|
||||
SetSubject(report.Title)
|
||||
|
||||
if strings.ToLower(report.Format) == "html" || report.Format == "" {
|
||||
msg.SetBody(mail.TextHTML, report.Message)
|
||||
} else {
|
||||
msg.SetBody(mail.TextPlain, report.Message)
|
||||
}
|
||||
|
||||
if msg.Error != nil {
|
||||
return msg.Error
|
||||
}
|
||||
|
||||
err = msg.Send(client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClient(from string, server *mail.SMTPServer) *Client {
|
||||
return &Client{server: server, from: from}
|
||||
}
|
36
pkg/email/client_test.go
Normal file
36
pkg/email/client_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package email_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
func Test_EncryptionFromString(t *testing.T) {
|
||||
t.Run("EncryptionFromString.SSLTLS", func(t *testing.T) {
|
||||
encryption := email.EncryptionFromString("ssl/tls")
|
||||
if encryption != mail.EncryptionSSLTLS {
|
||||
t.Errorf("Unexpected encryption mapping: %d", encryption)
|
||||
}
|
||||
})
|
||||
t.Run("EncryptionFromString.STARTTLS", func(t *testing.T) {
|
||||
encryption := email.EncryptionFromString("starttls")
|
||||
if encryption != mail.EncryptionSTARTTLS {
|
||||
t.Errorf("Unexpected encryption mapping: %d", encryption)
|
||||
}
|
||||
})
|
||||
t.Run("EncryptionFromString.Default", func(t *testing.T) {
|
||||
encryption := email.EncryptionFromString("")
|
||||
if encryption != mail.EncryptionNone {
|
||||
t.Errorf("Unexpected encryption mapping: %d", encryption)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_NewClient(t *testing.T) {
|
||||
client := email.NewClient("policy-reporter@kyverno.io", nil)
|
||||
if client == nil {
|
||||
t.Errorf("Unexpected client result")
|
||||
}
|
||||
}
|
26
pkg/email/functions.go
Normal file
26
pkg/email/functions.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package email
|
||||
|
||||
import "github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
|
||||
const (
|
||||
PassColor = "#198754"
|
||||
WarnColor = "#fd7e14"
|
||||
FailColor = "#dc3545"
|
||||
ErrorColor = "#b02a37"
|
||||
DefaultColor = "#cccccc"
|
||||
)
|
||||
|
||||
func ColorFromStatus(status string) string {
|
||||
switch status {
|
||||
case v1alpha2.StatusPass:
|
||||
return PassColor
|
||||
case v1alpha2.StatusWarn:
|
||||
return WarnColor
|
||||
case v1alpha2.StatusFail:
|
||||
return FailColor
|
||||
case v1alpha2.StatusError:
|
||||
return ErrorColor
|
||||
default:
|
||||
return DefaultColor
|
||||
}
|
||||
}
|
41
pkg/email/functions_test.go
Normal file
41
pkg/email/functions_test.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package email_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/email"
|
||||
)
|
||||
|
||||
func Test_ColorFromStatus(t *testing.T) {
|
||||
t.Run("ColorFromStatus.Pass", func(t *testing.T) {
|
||||
color := email.ColorFromStatus(v1alpha2.StatusPass)
|
||||
if color != email.PassColor {
|
||||
t.Errorf("Unexpected pass color: %s", color)
|
||||
}
|
||||
})
|
||||
t.Run("ColorFromStatus.Warn", func(t *testing.T) {
|
||||
color := email.ColorFromStatus(v1alpha2.StatusWarn)
|
||||
if color != email.WarnColor {
|
||||
t.Errorf("Unexpected warn color: %s", color)
|
||||
}
|
||||
})
|
||||
t.Run("ColorFromStatus.Fail", func(t *testing.T) {
|
||||
color := email.ColorFromStatus(v1alpha2.StatusFail)
|
||||
if color != email.FailColor {
|
||||
t.Errorf("Unexpected fail color: %s", color)
|
||||
}
|
||||
})
|
||||
t.Run("ColorFromStatus.Error", func(t *testing.T) {
|
||||
color := email.ColorFromStatus(v1alpha2.StatusError)
|
||||
if color != email.ErrorColor {
|
||||
t.Errorf("Unexpected error color: %s", color)
|
||||
}
|
||||
})
|
||||
t.Run("ColorFromStatus.Default", func(t *testing.T) {
|
||||
color := email.ColorFromStatus("")
|
||||
if color != email.DefaultColor {
|
||||
t.Errorf("Unexpected error color: %s", color)
|
||||
}
|
||||
})
|
||||
}
|
16
pkg/email/model.go
Normal file
16
pkg/email/model.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
Title string
|
||||
Message string
|
||||
Format string
|
||||
ClusterName string
|
||||
}
|
||||
|
||||
type Reporter interface {
|
||||
Report(ctx context.Context) (Report, error)
|
||||
}
|
222
pkg/email/summary/fixtures_test.go
Normal file
222
pkg/email/summary/fixtures_test.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package summary_test
|
||||
|
||||
import (
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
"github.com/kyverno/kyverno/pkg/client/clientset/versioned/fake"
|
||||
v1alpha2client "github.com/kyverno/kyverno/pkg/client/clientset/versioned/typed/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var Filter = filter.New(filter.Rules{}, make([]string, 0, 0))
|
||||
|
||||
func NewFakeCilent() (v1alpha2client.Wgpolicyk8sV1alpha2Interface, v1alpha2client.PolicyReportInterface, v1alpha2client.ClusterPolicyReportInterface) {
|
||||
client := fake.NewSimpleClientset().Wgpolicyk8sV1alpha2()
|
||||
|
||||
return client, client.PolicyReports("test"), client.ClusterPolicyReports()
|
||||
}
|
||||
|
||||
var PolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "policy-report",
|
||||
Namespace: "test",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 0,
|
||||
Skip: 0,
|
||||
Warn: 0,
|
||||
Fail: 3,
|
||||
Error: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "required-label",
|
||||
Rule: "app-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
},
|
||||
{
|
||||
Message: "message 2",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "priority-test",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
},
|
||||
{
|
||||
Message: "message 3",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "required-label",
|
||||
Rule: "app-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "name",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988b3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var KyvernoPolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "kyverno-policy-report",
|
||||
Namespace: "kyverno",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 1,
|
||||
Skip: 0,
|
||||
Warn: 0,
|
||||
Fail: 0,
|
||||
Error: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "required-limit",
|
||||
Rule: "resource-limit-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093003},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "kyverno",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var EmptyPolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "empty-policy-report",
|
||||
Namespace: "test",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{},
|
||||
}
|
||||
|
||||
var ClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 1,
|
||||
Warn: 2,
|
||||
Error: 3,
|
||||
Pass: 4,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "cluster-required-label",
|
||||
Rule: "ns-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "policy-reporter",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var KyvernoClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "kyverno-cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 0,
|
||||
Warn: 0,
|
||||
Error: 0,
|
||||
Pass: 1,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "cluster-required-quota",
|
||||
Rule: "ns-quota-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "kyverno",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var MinClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 1,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "cluster-policy",
|
||||
Rule: "cluster-role",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var EmptyClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "empty-cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{},
|
||||
}
|
147
pkg/email/summary/generator.go
Normal file
147
pkg/email/summary/generator.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
api "github.com/kyverno/kyverno/pkg/client/clientset/versioned/typed/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
client api.Wgpolicyk8sV1alpha2Interface
|
||||
filter filter.Filter
|
||||
clusterReports bool
|
||||
}
|
||||
|
||||
func (o *Generator) GenerateData(ctx context.Context) ([]Source, error) {
|
||||
mx := &sync.Mutex{}
|
||||
|
||||
sources := make(map[string]*Source)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
if o.clusterReports {
|
||||
clusterReports, err := o.client.ClusterPolicyReports().List(ctx, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return make([]Source, 0, 0), err
|
||||
}
|
||||
|
||||
wg.Add(len(clusterReports.Items))
|
||||
|
||||
for _, rep := range clusterReports.Items {
|
||||
go func(report v1alpha2.ClusterPolicyReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(report.Results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rs := report.Results[0].Source
|
||||
if !o.filter.ValidateSource(rs) {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
s, ok := sources[rs]
|
||||
if !ok {
|
||||
s = NewSource(rs, o.clusterReports)
|
||||
sources[rs] = s
|
||||
}
|
||||
mx.Unlock()
|
||||
|
||||
s.AddClusterSummary(report.Summary)
|
||||
}(rep)
|
||||
}
|
||||
}
|
||||
|
||||
reports, err := o.client.PolicyReports(v1.NamespaceAll).List(ctx, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return make([]Source, 0, 0), err
|
||||
}
|
||||
|
||||
wg.Add(len(reports.Items))
|
||||
|
||||
for _, rep := range reports.Items {
|
||||
go func(report v1alpha2.PolicyReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(report.Results) == 0 || !o.filter.ValidateNamespace(report.Namespace) {
|
||||
return
|
||||
}
|
||||
|
||||
rs := report.Results[0].Source
|
||||
if !o.filter.ValidateSource(rs) {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
s, ok := sources[rs]
|
||||
if !ok {
|
||||
s = NewSource(rs, o.clusterReports)
|
||||
sources[rs] = s
|
||||
}
|
||||
mx.Unlock()
|
||||
|
||||
s.AddNamespacedSummary(report.Namespace, report.Summary)
|
||||
}(rep)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
list := make([]Source, 0, len(sources))
|
||||
for _, s := range sources {
|
||||
list = append(list, *s)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func NewGenerator(client api.Wgpolicyk8sV1alpha2Interface, filter filter.Filter, clusterReports bool) *Generator {
|
||||
return &Generator{client, filter, clusterReports}
|
||||
}
|
||||
|
||||
func FilterSources(sources []Source, filter filter.Filter, clusterReports bool) []Source {
|
||||
newSources := make([]Source, 0)
|
||||
|
||||
mx := sync.Mutex{}
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(sources))
|
||||
|
||||
for _, s := range sources {
|
||||
go func(source Source) {
|
||||
defer wg.Done()
|
||||
|
||||
if !filter.ValidateSource(source.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
newSource := NewSource(source.Name, clusterReports)
|
||||
|
||||
if clusterReports {
|
||||
newSource.ClusterScopeSummary = source.ClusterScopeSummary
|
||||
}
|
||||
|
||||
for ns, results := range source.NamespaceScopeSummary {
|
||||
if !filter.ValidateNamespace(ns) {
|
||||
continue
|
||||
}
|
||||
|
||||
newSource.NamespaceScopeSummary[ns] = results
|
||||
}
|
||||
|
||||
if !clusterReports && len(newSource.NamespaceScopeSummary) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
newSources = append(newSources, *newSource)
|
||||
mx.Unlock()
|
||||
}(s)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return newSources
|
||||
}
|
174
pkg/email/summary/generator_test.go
Normal file
174
pkg/email/summary/generator_test.go
Normal file
|
@ -0,0 +1,174 @@
|
|||
package summary_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email/summary"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func Test_GenerateDataWithSingleSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source got: %d", len(data))
|
||||
}
|
||||
|
||||
source := data[0]
|
||||
if source.Name != "test" {
|
||||
t.Fatalf("expected source name 'test', got: %s", source.Name)
|
||||
}
|
||||
if source.ClusterScopeSummary.Fail != 1 {
|
||||
t.Fatalf("unexpected Summary Mapping: %d", source.ClusterScopeSummary.Fail)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Fail != 3 {
|
||||
t.Fatalf("unexpected Summary Mapping: %d", source.NamespaceScopeSummary["test"].Fail)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GenerateDataWithMultipleSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("expected two sources, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GenerateDataWithSourceFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, filter.New(filter.Rules{}, []string{"test"}), true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterSourcesBySource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = summary.FilterSources(data, filter.New(filter.Rules{}, []string{"Kyverno"}), true)
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source left, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterSourcesByNamespace(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = summary.FilterSources(data, filter.New(filter.Rules{Exclude: []string{"kyverno"}}, []string{}), true)
|
||||
source := data[0]
|
||||
if source.Name != "Kyverno" {
|
||||
source = data[1]
|
||||
}
|
||||
|
||||
if _, ok := source.NamespaceScopeSummary["kyverno"]; ok {
|
||||
t.Fatal("expected namespace kyverno to be excluded")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RemoveEmptySource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = summary.FilterSources(data, filter.New(filter.Rules{Exclude: []string{"kyverno"}}, []string{}), false)
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source left, got: %d", len(data))
|
||||
}
|
||||
}
|
62
pkg/email/summary/model.go
Normal file
62
pkg/email/summary/model.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
Skip int
|
||||
Pass int
|
||||
Warn int
|
||||
Fail int
|
||||
Error int
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Name string
|
||||
ClusterScopeSummary *Summary
|
||||
NamespaceScopeSummary map[string]*Summary
|
||||
ClusterReports bool
|
||||
|
||||
mx *sync.Mutex
|
||||
}
|
||||
|
||||
func (s *Source) AddClusterSummary(sum v1alpha2.PolicyReportSummary) {
|
||||
s.ClusterScopeSummary.Skip += sum.Skip
|
||||
s.ClusterScopeSummary.Pass += sum.Pass
|
||||
s.ClusterScopeSummary.Warn += sum.Warn
|
||||
s.ClusterScopeSummary.Fail += sum.Fail
|
||||
s.ClusterScopeSummary.Error += sum.Error
|
||||
}
|
||||
|
||||
func (s *Source) AddNamespacedSummary(ns string, sum v1alpha2.PolicyReportSummary) {
|
||||
s.mx.Lock()
|
||||
if d, ok := s.NamespaceScopeSummary[ns]; ok {
|
||||
d.Skip += sum.Skip
|
||||
d.Pass += sum.Pass
|
||||
d.Warn += sum.Warn
|
||||
d.Fail += sum.Fail
|
||||
d.Error += sum.Error
|
||||
} else {
|
||||
s.NamespaceScopeSummary[ns] = &Summary{
|
||||
Skip: sum.Skip,
|
||||
Pass: sum.Pass,
|
||||
Fail: sum.Fail,
|
||||
Warn: sum.Warn,
|
||||
Error: sum.Error,
|
||||
}
|
||||
}
|
||||
s.mx.Unlock()
|
||||
}
|
||||
|
||||
func NewSource(name string, clusterReports bool) *Source {
|
||||
return &Source{
|
||||
Name: name,
|
||||
ClusterScopeSummary: &Summary{},
|
||||
NamespaceScopeSummary: map[string]*Summary{},
|
||||
ClusterReports: clusterReports,
|
||||
mx: new(sync.Mutex),
|
||||
}
|
||||
}
|
79
pkg/email/summary/model_test.go
Normal file
79
pkg/email/summary/model_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package summary_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/email/summary"
|
||||
)
|
||||
|
||||
func Test_Source(t *testing.T) {
|
||||
source := summary.NewSource("kyverno", true)
|
||||
t.Run("Source.ClusterReports", func(t *testing.T) {
|
||||
if !source.ClusterReports {
|
||||
t.Errorf("Expected Surce.ClusterReports to be true")
|
||||
}
|
||||
})
|
||||
t.Run("Source.AddClusterSummary", func(t *testing.T) {
|
||||
source.AddClusterSummary(v1alpha2.PolicyReportSummary{
|
||||
Pass: 1,
|
||||
Warn: 2,
|
||||
Fail: 4,
|
||||
Error: 3,
|
||||
})
|
||||
|
||||
if source.ClusterScopeSummary.Pass != 1 {
|
||||
t.Errorf("Unexpected Pass Summary: %d", source.ClusterScopeSummary.Pass)
|
||||
}
|
||||
if source.ClusterScopeSummary.Warn != 2 {
|
||||
t.Errorf("Unexpected Warn Summary: %d", source.ClusterScopeSummary.Warn)
|
||||
}
|
||||
if source.ClusterScopeSummary.Fail != 4 {
|
||||
t.Errorf("Unexpected Fail Summary: %d", source.ClusterScopeSummary.Fail)
|
||||
}
|
||||
if source.ClusterScopeSummary.Error != 3 {
|
||||
t.Errorf("Unexpected Errpr Summary: %d", source.ClusterScopeSummary.Error)
|
||||
}
|
||||
})
|
||||
t.Run("Source.AddNamespacedSummary", func(t *testing.T) {
|
||||
source.AddNamespacedSummary("test", v1alpha2.PolicyReportSummary{
|
||||
Pass: 5,
|
||||
Warn: 6,
|
||||
Fail: 7,
|
||||
Error: 8,
|
||||
})
|
||||
|
||||
if source.NamespaceScopeSummary["test"].Pass != 5 {
|
||||
t.Errorf("Unexpected Pass Summary: %d", source.ClusterScopeSummary.Pass)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Warn != 6 {
|
||||
t.Errorf("Unexpected Warn Summary: %d", source.ClusterScopeSummary.Warn)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Fail != 7 {
|
||||
t.Errorf("Unexpected Fail Summary: %d", source.ClusterScopeSummary.Fail)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Error != 8 {
|
||||
t.Errorf("Unexpected Errpr Summary: %d", source.ClusterScopeSummary.Error)
|
||||
}
|
||||
|
||||
source.AddNamespacedSummary("test", v1alpha2.PolicyReportSummary{
|
||||
Pass: 2,
|
||||
Warn: 1,
|
||||
Fail: 0,
|
||||
Error: 3,
|
||||
})
|
||||
|
||||
if source.NamespaceScopeSummary["test"].Pass != 7 {
|
||||
t.Errorf("Unexpected Pass Summary: %d", source.ClusterScopeSummary.Pass)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Warn != 7 {
|
||||
t.Errorf("Unexpected Warn Summary: %d", source.ClusterScopeSummary.Warn)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Fail != 7 {
|
||||
t.Errorf("Unexpected Fail Summary: %d", source.ClusterScopeSummary.Fail)
|
||||
}
|
||||
if source.NamespaceScopeSummary["test"].Error != 11 {
|
||||
t.Errorf("Unexpected Errpr Summary: %d", source.ClusterScopeSummary.Error)
|
||||
}
|
||||
})
|
||||
}
|
42
pkg/email/summary/reporter.go
Normal file
42
pkg/email/summary/reporter.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
templateDir string
|
||||
clusterName string
|
||||
}
|
||||
|
||||
func (o *Reporter) Report(sources []Source, format string) (email.Report, error) {
|
||||
b := new(strings.Builder)
|
||||
|
||||
templ, err := template.ParseFiles(o.templateDir + "/summary.html")
|
||||
if err != nil {
|
||||
return email.Report{}, err
|
||||
}
|
||||
|
||||
err = templ.Execute(b, struct {
|
||||
Sources []Source
|
||||
ClusterName string
|
||||
}{Sources: sources, ClusterName: o.clusterName})
|
||||
if err != nil {
|
||||
return email.Report{}, err
|
||||
}
|
||||
|
||||
return email.Report{
|
||||
ClusterName: o.clusterName,
|
||||
Title: "Summary Report from " + time.Now().Format("2006-02-01"),
|
||||
Message: b.String(),
|
||||
Format: format,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewReporter(templateDir, clusterName string) *Reporter {
|
||||
return &Reporter{templateDir, clusterName}
|
||||
}
|
54
pkg/email/summary/reporter_test.go
Normal file
54
pkg/email/summary/reporter_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package summary_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email/summary"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func Test_CreateReport(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := summary.NewGenerator(client, Filter, true)
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Println(path)
|
||||
|
||||
reporter := summary.NewReporter("../../../templates", "Cluster")
|
||||
report, err := reporter.Report(data, "html")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if report.Message == "" {
|
||||
t.Fatal("expected validate report message")
|
||||
}
|
||||
if report.ClusterName != "Cluster" {
|
||||
t.Fatal("expected clustername to be set")
|
||||
}
|
||||
if report.Format != "html" {
|
||||
t.Fatal("expected format to be set")
|
||||
}
|
||||
}
|
321
pkg/email/violations/fixtures_test.go
Normal file
321
pkg/email/violations/fixtures_test.go
Normal file
|
@ -0,0 +1,321 @@
|
|||
package violations_test
|
||||
|
||||
import (
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
"github.com/kyverno/kyverno/pkg/client/clientset/versioned/fake"
|
||||
v1alpha2client "github.com/kyverno/kyverno/pkg/client/clientset/versioned/typed/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
var Filter = filter.New(filter.Rules{}, make([]string, 0, 0))
|
||||
|
||||
func NewFakeCilent() (v1alpha2client.Wgpolicyk8sV1alpha2Interface, v1alpha2client.PolicyReportInterface, v1alpha2client.ClusterPolicyReportInterface) {
|
||||
client := fake.NewSimpleClientset().Wgpolicyk8sV1alpha2()
|
||||
|
||||
return client, client.PolicyReports("test"), client.ClusterPolicyReports()
|
||||
}
|
||||
|
||||
var PolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "policy-report",
|
||||
Namespace: "test",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 0,
|
||||
Skip: 0,
|
||||
Warn: 0,
|
||||
Fail: 3,
|
||||
Error: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "required-label",
|
||||
Rule: "app-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
Properties: map[string]string{"version": "1.2.0"},
|
||||
},
|
||||
{
|
||||
Message: "message 2",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "priority-test",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
},
|
||||
{
|
||||
Message: "message 3",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "required-label",
|
||||
Rule: "app-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "name",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988b3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var KyvernoPolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "kyverno-policy-report",
|
||||
Namespace: "kyverno",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 1,
|
||||
Skip: 0,
|
||||
Warn: 1,
|
||||
Fail: 0,
|
||||
Error: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "required-limit",
|
||||
Rule: "resource-limit-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093003},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "kyverno",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusWarn,
|
||||
Scored: true,
|
||||
Policy: "required-limit",
|
||||
Rule: "resource-limit-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093003},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx2",
|
||||
Namespace: "kyverno",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var PassPolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "pass-policy-report",
|
||||
Namespace: "test",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 1,
|
||||
Skip: 0,
|
||||
Warn: 0,
|
||||
Fail: 0,
|
||||
Error: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "required-limit",
|
||||
Rule: "resource-limit-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093003},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var EmptyPolicyReportCRD = &v1alpha2.PolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "empty-policy-report",
|
||||
Namespace: "test",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{},
|
||||
}
|
||||
|
||||
var ClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 1,
|
||||
Warn: 0,
|
||||
Error: 0,
|
||||
Pass: 1,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "cluster-required-label",
|
||||
Rule: "ns-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "policy-reporter",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "cluster-required-label",
|
||||
Rule: "ns-label-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "test",
|
||||
Namespace: "test",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var KyvernoClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "kyverno-cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 1,
|
||||
Warn: 0,
|
||||
Error: 0,
|
||||
Pass: 0,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "cluster-required-quota",
|
||||
Rule: "ns-quota-required",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "Kyverno",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
Resources: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "kyverno",
|
||||
UID: "dfd57c50-f30c-4729-b63f-b1954d8988d1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var MinClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Fail: 1,
|
||||
Pass: 1,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusFail,
|
||||
Scored: true,
|
||||
Policy: "cluster-policy",
|
||||
Rule: "cluster-role",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var PassClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "pass-cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{
|
||||
Pass: 1,
|
||||
},
|
||||
Results: []v1alpha2.PolicyReportResult{
|
||||
{
|
||||
Message: "message",
|
||||
Result: v1alpha2.StatusPass,
|
||||
Scored: true,
|
||||
Policy: "cluster-policy-pass",
|
||||
Rule: "cluster-role-pass",
|
||||
Timestamp: v1.Timestamp{Seconds: 1614093000},
|
||||
Source: "test",
|
||||
Category: "test",
|
||||
Severity: v1alpha2.SeverityHigh,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var EmptyClusterPolicyReportCRD = &v1alpha2.ClusterPolicyReport{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "empty-cluster-policy-report",
|
||||
},
|
||||
Summary: v1alpha2.PolicyReportSummary{},
|
||||
}
|
184
pkg/email/violations/generator.go
Normal file
184
pkg/email/violations/generator.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package violations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
api "github.com/kyverno/kyverno/pkg/client/clientset/versioned/typed/policyreport/v1alpha2"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
client api.Wgpolicyk8sV1alpha2Interface
|
||||
filter filter.Filter
|
||||
clusterReports bool
|
||||
}
|
||||
|
||||
func (o *Generator) GenerateData(ctx context.Context) ([]Source, error) {
|
||||
mx := &sync.Mutex{}
|
||||
|
||||
sources := make(map[string]*Source)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
if o.clusterReports {
|
||||
clusterReports, err := o.client.ClusterPolicyReports().List(ctx, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return make([]Source, 0, 0), err
|
||||
}
|
||||
|
||||
wg.Add(len(clusterReports.Items))
|
||||
|
||||
for _, rep := range clusterReports.Items {
|
||||
go func(report v1alpha2.ClusterPolicyReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(report.Results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rs := report.Results[0].Source
|
||||
|
||||
if !o.filter.ValidateSource(rs) {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
s, ok := sources[rs]
|
||||
if !ok {
|
||||
s = NewSource(rs, o.clusterReports)
|
||||
sources[rs] = s
|
||||
}
|
||||
mx.Unlock()
|
||||
|
||||
s.AddClusterPassed(report.Summary.Pass)
|
||||
|
||||
length := len(report.Results)
|
||||
if length == 0 || length == report.Summary.Pass+report.Summary.Skip {
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range report.Results {
|
||||
if result.Result == v1alpha2.StatusPass || result.Result == v1alpha2.StatusSkip {
|
||||
continue
|
||||
}
|
||||
|
||||
s.AddClusterResults(mapResult(result))
|
||||
}
|
||||
}(rep)
|
||||
}
|
||||
}
|
||||
|
||||
reports, err := o.client.PolicyReports(v1.NamespaceAll).List(ctx, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return make([]Source, 0, 0), err
|
||||
}
|
||||
|
||||
wg.Add(len(reports.Items))
|
||||
|
||||
for _, rep := range reports.Items {
|
||||
go func(report v1alpha2.PolicyReport) {
|
||||
defer wg.Done()
|
||||
|
||||
if len(report.Results) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rs := report.Results[0].Source
|
||||
|
||||
if !o.filter.ValidateSource(rs) || !o.filter.ValidateNamespace(report.Namespace) {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
s, ok := sources[rs]
|
||||
if !ok {
|
||||
s = NewSource(rs, o.clusterReports)
|
||||
sources[rs] = s
|
||||
}
|
||||
mx.Unlock()
|
||||
|
||||
s.AddNamespacedPassed(report.Namespace, report.Summary.Pass)
|
||||
|
||||
length := len(report.Results)
|
||||
if length == 0 || length == report.Summary.Pass+report.Summary.Skip {
|
||||
s.InitResults(report.Namespace)
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range report.Results {
|
||||
if result.Result == v1alpha2.StatusPass || result.Result == v1alpha2.StatusSkip {
|
||||
continue
|
||||
}
|
||||
s.AddNamespacedResults(report.Namespace, mapResult(result))
|
||||
}
|
||||
}(rep)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
list := make([]Source, 0, len(sources))
|
||||
for _, s := range sources {
|
||||
list = append(list, *s)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func NewGenerator(client api.Wgpolicyk8sV1alpha2Interface, filter filter.Filter, clusterReports bool) *Generator {
|
||||
return &Generator{client, filter, clusterReports}
|
||||
}
|
||||
|
||||
func FilterSources(sources []Source, filter filter.Filter, clusterReports bool) []Source {
|
||||
newSources := make([]Source, 0)
|
||||
|
||||
mx := sync.Mutex{}
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(sources))
|
||||
|
||||
for _, s := range sources {
|
||||
go func(source Source) {
|
||||
defer wg.Done()
|
||||
|
||||
if !filter.ValidateSource(source.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
newSource := NewSource(source.Name, clusterReports)
|
||||
|
||||
if clusterReports {
|
||||
newSource.ClusterPassed = source.ClusterPassed
|
||||
newSource.ClusterResults = source.ClusterResults
|
||||
}
|
||||
|
||||
for ns, passed := range source.NamespacePassed {
|
||||
if !filter.ValidateNamespace(ns) {
|
||||
continue
|
||||
}
|
||||
|
||||
newSource.AddNamespacedPassed(ns, passed)
|
||||
}
|
||||
|
||||
for ns, results := range source.NamespaceResults {
|
||||
if !filter.ValidateNamespace(ns) {
|
||||
continue
|
||||
}
|
||||
|
||||
newSource.NamespaceResults[ns] = results
|
||||
}
|
||||
|
||||
if !clusterReports && len(newSource.NamespaceResults) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
mx.Lock()
|
||||
newSources = append(newSources, *newSource)
|
||||
mx.Unlock()
|
||||
}(s)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return newSources
|
||||
}
|
193
pkg/email/violations/generator_test.go
Normal file
193
pkg/email/violations/generator_test.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
package violations_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email/violations"
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func Test_GenerateDataWithSingleSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source got: %d", len(data))
|
||||
}
|
||||
|
||||
source := data[0]
|
||||
if source.Name != "test" {
|
||||
t.Fatalf("expected source name 'test', got: %s", source.Name)
|
||||
}
|
||||
if source.ClusterPassed != 1 {
|
||||
t.Fatalf("unexpected Summary Mapping: %d", source.ClusterPassed)
|
||||
}
|
||||
if len(source.NamespaceResults["test"]["fail"]) != 3 {
|
||||
t.Fatalf("unexpected Summary Mapping: %d", len(source.NamespaceResults["test"]["fail"]))
|
||||
}
|
||||
|
||||
result := source.NamespaceResults["test"]["fail"][0]
|
||||
if result.Kind != "Deployment" {
|
||||
t.Fatalf("unexpected kind: %s", result.Kind)
|
||||
}
|
||||
if result.Name != "nginx" {
|
||||
t.Fatalf("unexpected name: %s", result.Kind)
|
||||
}
|
||||
if result.Policy != "required-label" {
|
||||
t.Fatalf("unexpected policy: %s", result.Kind)
|
||||
}
|
||||
if result.Rule != "app-label-required" {
|
||||
t.Fatalf("unexpected rule: %s", result.Kind)
|
||||
}
|
||||
if result.Status != "fail" {
|
||||
t.Fatalf("unexpected status: %s", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GenerateDataWithMultipleSource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, PassPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, PassClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 2 {
|
||||
t.Fatalf("expected two sources, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GenerateDataWithSourceFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, filter.New(filter.Rules{}, []string{"test"}), true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterSourcesBySource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = violations.FilterSources(data, filter.New(filter.Rules{}, []string{"Kyverno"}), true)
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source left, got: %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_FilterSourcesByNamespace(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = violations.FilterSources(data, filter.New(filter.Rules{Exclude: []string{"kyverno"}}, []string{}), true)
|
||||
source := data[0]
|
||||
if source.Name != "Kyverno" {
|
||||
source = data[1]
|
||||
}
|
||||
|
||||
if _, ok := source.NamespaceResults["kyverno"]; ok {
|
||||
t.Fatal("expected namespace kyverno to be excluded")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RemoveEmptySource(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
data = violations.FilterSources(data, filter.New(filter.Rules{Exclude: []string{"kyverno"}}, []string{}), false)
|
||||
if len(data) != 1 {
|
||||
t.Fatalf("expected one source left, got: %d", len(data))
|
||||
}
|
||||
}
|
114
pkg/email/violations/model.go
Normal file
114
pkg/email/violations/model.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package violations
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Policy string
|
||||
Rule string
|
||||
Kind string
|
||||
Name string
|
||||
Status string
|
||||
}
|
||||
|
||||
func mapResult(res v1alpha2.PolicyReportResult) []Result {
|
||||
count := len(res.Resources)
|
||||
|
||||
if count == 0 {
|
||||
return []Result{{
|
||||
Policy: res.Policy,
|
||||
Rule: res.Rule,
|
||||
Status: string(res.Result),
|
||||
}}
|
||||
}
|
||||
|
||||
list := make([]Result, 0, count)
|
||||
for _, re := range res.Resources {
|
||||
list = append(list, Result{
|
||||
Policy: res.Policy,
|
||||
Rule: res.Rule,
|
||||
Name: re.Name,
|
||||
Kind: re.Kind,
|
||||
Status: string(res.Result),
|
||||
})
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Name string
|
||||
ClusterPassed int
|
||||
NamespacePassed map[string]int
|
||||
ClusterResults map[string][]Result
|
||||
NamespaceResults map[string]map[string][]Result
|
||||
ClusterReports bool
|
||||
|
||||
passMX *sync.Mutex
|
||||
crMX *sync.Mutex
|
||||
nrMX *sync.Mutex
|
||||
}
|
||||
|
||||
func (s *Source) AddClusterResults(result []Result) {
|
||||
s.crMX.Lock()
|
||||
s.ClusterResults[result[0].Status] = append(s.ClusterResults[result[0].Status], result...)
|
||||
s.crMX.Unlock()
|
||||
}
|
||||
|
||||
func (s *Source) AddClusterPassed(results int) {
|
||||
s.ClusterPassed += results
|
||||
}
|
||||
|
||||
func (s *Source) AddNamespacedPassed(ns string, results int) {
|
||||
s.passMX.Lock()
|
||||
s.NamespacePassed[ns] += results
|
||||
s.passMX.Unlock()
|
||||
}
|
||||
|
||||
func (s *Source) AddNamespacedResults(ns string, result []Result) {
|
||||
s.nrMX.Lock()
|
||||
if nr, ok := s.NamespaceResults[ns]; ok {
|
||||
s.NamespaceResults[ns][result[0].Status] = append(nr[result[0].Status], result...)
|
||||
} else {
|
||||
s.NamespaceResults[ns] = map[string][]Result{
|
||||
v1alpha2.StatusWarn: make([]Result, 0),
|
||||
v1alpha2.StatusFail: make([]Result, 0),
|
||||
v1alpha2.StatusError: make([]Result, 0),
|
||||
}
|
||||
|
||||
s.NamespaceResults[ns][result[0].Status] = result
|
||||
}
|
||||
s.nrMX.Unlock()
|
||||
}
|
||||
|
||||
func (s Source) InitResults(ns string) {
|
||||
s.nrMX.Lock()
|
||||
if _, ok := s.NamespaceResults[ns]; !ok {
|
||||
s.NamespaceResults[ns] = map[string][]Result{
|
||||
v1alpha2.StatusWarn: make([]Result, 0),
|
||||
v1alpha2.StatusFail: make([]Result, 0),
|
||||
v1alpha2.StatusError: make([]Result, 0),
|
||||
}
|
||||
}
|
||||
s.nrMX.Unlock()
|
||||
}
|
||||
|
||||
func NewSource(name string, clusterReports bool) *Source {
|
||||
return &Source{
|
||||
Name: name,
|
||||
ClusterReports: clusterReports,
|
||||
ClusterResults: map[string][]Result{
|
||||
v1alpha2.StatusWarn: make([]Result, 0),
|
||||
v1alpha2.StatusFail: make([]Result, 0),
|
||||
v1alpha2.StatusError: make([]Result, 0),
|
||||
},
|
||||
NamespaceResults: map[string]map[string][]Result{},
|
||||
NamespacePassed: map[string]int{},
|
||||
passMX: new(sync.Mutex),
|
||||
crMX: new(sync.Mutex),
|
||||
nrMX: new(sync.Mutex),
|
||||
}
|
||||
}
|
101
pkg/email/violations/model_test.go
Normal file
101
pkg/email/violations/model_test.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package violations_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email/violations"
|
||||
)
|
||||
|
||||
func Test_Source(t *testing.T) {
|
||||
source := violations.NewSource("kyverno", true)
|
||||
t.Run("Source.ClusterReports", func(t *testing.T) {
|
||||
if !source.ClusterReports {
|
||||
t.Errorf("Expected Surce.ClusterReports to be true")
|
||||
}
|
||||
})
|
||||
t.Run("Source.AddClusterPassed", func(t *testing.T) {
|
||||
source.AddClusterPassed(3)
|
||||
|
||||
if source.ClusterPassed != 3 {
|
||||
t.Errorf("Unexpected Summary: %d", source.ClusterPassed)
|
||||
}
|
||||
})
|
||||
t.Run("Source.AddClusterResults", func(t *testing.T) {
|
||||
source.AddClusterResults([]violations.Result{{
|
||||
Name: "policy-reporter",
|
||||
Kind: "Namespace",
|
||||
Policy: "require-label",
|
||||
Rule: "require-label",
|
||||
Status: "fail",
|
||||
}, {
|
||||
Name: "develop",
|
||||
Kind: "Namespace",
|
||||
Policy: "require-label",
|
||||
Rule: "require-label",
|
||||
Status: "fail",
|
||||
}})
|
||||
|
||||
if len(source.ClusterResults["fail"]) != 2 {
|
||||
t.Errorf("Unexpected amount of failing Cluster Results: %d", len(source.ClusterResults["fail"]))
|
||||
}
|
||||
})
|
||||
t.Run("Source.AddNamespacedPassed", func(t *testing.T) {
|
||||
source.AddNamespacedPassed("test", 2)
|
||||
|
||||
if source.NamespacePassed["test"] != 2 {
|
||||
t.Errorf("Unexpected amount of passed Results in Namespace: %d", source.NamespacePassed["test"])
|
||||
}
|
||||
|
||||
source.AddNamespacedPassed("test", 3)
|
||||
|
||||
if source.NamespacePassed["test"] != 5 {
|
||||
t.Errorf("Unexpected amount of passed Results in Namespace: %d", source.NamespacePassed["test"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Source.AddNamespacedResults", func(t *testing.T) {
|
||||
source.AddNamespacedResults("test", []violations.Result{{
|
||||
Name: "policy-reporter",
|
||||
Kind: "Deployment",
|
||||
Policy: "require-label",
|
||||
Rule: "require-label",
|
||||
Status: "fail",
|
||||
}})
|
||||
|
||||
if len(source.NamespaceResults["test"]["fail"]) != 1 {
|
||||
t.Errorf("Unexpected amount of failing Results in namespace after init: %d", len(source.ClusterResults["fail"]))
|
||||
}
|
||||
if source.NamespaceResults["test"]["warn"] == nil {
|
||||
t.Errorf("Expected warn map is initialized")
|
||||
}
|
||||
if source.NamespaceResults["test"]["error"] == nil {
|
||||
t.Errorf("Expected warn map is initialized")
|
||||
}
|
||||
|
||||
source.AddNamespacedResults("test", []violations.Result{{
|
||||
Name: "policy-reporter-ui",
|
||||
Kind: "Deployment",
|
||||
Policy: "require-label",
|
||||
Rule: "require-label",
|
||||
Status: "fail",
|
||||
}})
|
||||
|
||||
if len(source.NamespaceResults["test"]["fail"]) != 2 {
|
||||
t.Errorf("Unexpected amount of failing Results in namespace after add: %d", len(source.ClusterResults["fail"]))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Source.InitResults", func(t *testing.T) {
|
||||
source.InitResults("test")
|
||||
|
||||
if source.NamespaceResults["test"]["fail"] == nil {
|
||||
t.Errorf("Expected fail map is initialized")
|
||||
}
|
||||
if source.NamespaceResults["test"]["warn"] == nil {
|
||||
t.Errorf("Expected warn map is initialized")
|
||||
}
|
||||
if source.NamespaceResults["test"]["error"] == nil {
|
||||
t.Errorf("Expected warn map is initialized")
|
||||
}
|
||||
})
|
||||
}
|
54
pkg/email/violations/reporter.go
Normal file
54
pkg/email/violations/reporter.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package violations
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
templateDir string
|
||||
clusterName string
|
||||
}
|
||||
|
||||
func (o *Reporter) Report(sources []Source, format string) (email.Report, error) {
|
||||
b := new(strings.Builder)
|
||||
|
||||
vioTempl := template.New("violations.html").Funcs(template.FuncMap{
|
||||
"color": email.ColorFromStatus,
|
||||
"title": strings.Title,
|
||||
"hasViolations": func(results map[string][]Result) bool {
|
||||
return (len(results["warn"]) + len(results["fail"]) + len(results["error"])) > 0
|
||||
},
|
||||
"lenNamespaceResults": func(source Source, ns, status string) int {
|
||||
return len(source.NamespaceResults[ns][status])
|
||||
},
|
||||
})
|
||||
|
||||
templ, err := vioTempl.ParseFiles(o.templateDir + "/violations.html")
|
||||
if err != nil {
|
||||
return email.Report{}, err
|
||||
}
|
||||
|
||||
err = templ.Execute(b, struct {
|
||||
Sources []Source
|
||||
Status []string
|
||||
ClusterName string
|
||||
}{Sources: sources, Status: []string{"warn", "fail", "error"}, ClusterName: o.clusterName})
|
||||
if err != nil {
|
||||
return email.Report{}, err
|
||||
}
|
||||
|
||||
return email.Report{
|
||||
ClusterName: o.clusterName,
|
||||
Title: "Summary Report from " + time.Now().Format("2006-02-01"),
|
||||
Message: b.String(),
|
||||
Format: format,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewReporter(templateDir string, clusterName string) *Reporter {
|
||||
return &Reporter{templateDir, clusterName}
|
||||
}
|
54
pkg/email/violations/reporter_test.go
Normal file
54
pkg/email/violations/reporter_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package violations_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/email/violations"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func Test_CreateReport(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
client, pClient, cClient := NewFakeCilent()
|
||||
|
||||
_, _ = pClient.Create(ctx, PolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = pClient.Create(ctx, EmptyPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = client.PolicyReports("kyverno").Create(ctx, KyvernoPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
_, _ = cClient.Create(ctx, ClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, EmptyClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
_, _ = cClient.Create(ctx, KyvernoClusterPolicyReportCRD, v1.CreateOptions{})
|
||||
|
||||
generator := violations.NewGenerator(client, Filter, true)
|
||||
data, err := generator.GenerateData(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Println(path)
|
||||
|
||||
reporter := violations.NewReporter("../../../templates", "Cluster")
|
||||
report, err := reporter.Report(data, "html")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if report.Message == "" {
|
||||
t.Fatal("expected validate report message")
|
||||
}
|
||||
if report.ClusterName != "Cluster" {
|
||||
t.Fatal("expected clustername to be set")
|
||||
}
|
||||
if report.Format != "html" {
|
||||
t.Fatal("expected format to be set")
|
||||
}
|
||||
}
|
67
pkg/filter/filter.go
Normal file
67
pkg/filter/filter.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"github.com/kyverno/go-wildcard"
|
||||
"github.com/kyverno/policy-reporter/pkg/helper"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
namespace Rules
|
||||
sources []string
|
||||
}
|
||||
|
||||
func (f Filter) ValidateSource(source string) bool {
|
||||
return ValidateSource(source, f.sources)
|
||||
}
|
||||
|
||||
func (f Filter) ValidateNamespace(namespace string) bool {
|
||||
return ValidateNamespace(namespace, f.namespace)
|
||||
}
|
||||
|
||||
func New(namespaces Rules, sources []string) Filter {
|
||||
return Filter{namespaces, sources}
|
||||
}
|
||||
|
||||
func ValidateNamespace(namespace string, namespaces Rules) bool {
|
||||
if namespace != "" && len(namespaces.Include) > 0 {
|
||||
for _, ns := range namespaces.Include {
|
||||
if wildcard.Match(ns, namespace) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else if namespace != "" && len(namespaces.Exclude) > 0 {
|
||||
for _, ns := range namespaces.Exclude {
|
||||
if wildcard.Match(ns, namespace) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ValidateRule(value string, rules Rules) bool {
|
||||
if len(rules.Include) > 0 {
|
||||
for _, ns := range rules.Include {
|
||||
if wildcard.Match(ns, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else if len(rules.Exclude) > 0 {
|
||||
for _, ns := range rules.Exclude {
|
||||
if wildcard.Match(ns, value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ValidateSource(source string, sources []string) bool {
|
||||
return len(sources) == 0 || helper.Contains(source, sources)
|
||||
}
|
88
pkg/filter/filter_test.go
Normal file
88
pkg/filter/filter_test.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package filter_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
)
|
||||
|
||||
func Test_BaseClient(t *testing.T) {
|
||||
t.Run("Validate Default", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{}, []string{})
|
||||
|
||||
if !filter.ValidateNamespace("test") {
|
||||
t.Errorf("Unexpected Validation Result without configured rules")
|
||||
}
|
||||
if !filter.ValidateSource("Kyverno") {
|
||||
t.Errorf("Unexpected Validation Result without configured rules")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Source", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{}, []string{"jsPolicy"})
|
||||
|
||||
if filter.ValidateSource("test") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
|
||||
if !filter.ValidateSource("jsPolicy") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Validate Exclude Namespace match", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{Exclude: []string{"default"}}, []string{})
|
||||
|
||||
if filter.ValidateNamespace("default") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{Exclude: []string{"team-a"}}, []string{})
|
||||
|
||||
if !filter.ValidateNamespace("default") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Namespace match", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{Include: []string{"default"}}, []string{})
|
||||
|
||||
if !filter.ValidateNamespace("default") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) {
|
||||
filter := filter.New(filter.Rules{Include: []string{"team-a"}}, []string{})
|
||||
|
||||
if filter.ValidateNamespace("default") {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Rule match", func(t *testing.T) {
|
||||
result := filter.ValidateRule("test", filter.Rules{Exclude: []string{"team-a"}})
|
||||
|
||||
if !result {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Rule mismatch", func(t *testing.T) {
|
||||
result := filter.ValidateRule("test", filter.Rules{Exclude: []string{"test"}})
|
||||
|
||||
if result {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Rule match", func(t *testing.T) {
|
||||
result := filter.ValidateRule("test", filter.Rules{Include: []string{"test"}})
|
||||
|
||||
if !result {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Rule mismatch", func(t *testing.T) {
|
||||
result := filter.ValidateRule("test", filter.Rules{Include: []string{"team-a"}})
|
||||
|
||||
if result {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
}
|
6
pkg/filter/model.go
Normal file
6
pkg/filter/model.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package filter
|
||||
|
||||
type Rules struct {
|
||||
Exclude []string
|
||||
Include []string
|
||||
}
|
13
pkg/helper/utils.go
Normal file
13
pkg/helper/utils.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package helper
|
||||
|
||||
import "strings"
|
||||
|
||||
func Contains(source string, sources []string) bool {
|
||||
for _, s := range sources {
|
||||
if strings.EqualFold(s, source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
package target
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
"github.com/kyverno/policy-reporter/pkg/helper"
|
||||
"github.com/kyverno/policy-reporter/pkg/report"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
// Client for a provided Target
|
||||
|
@ -29,15 +28,15 @@ type Rules struct {
|
|||
}
|
||||
|
||||
type Filter struct {
|
||||
Namespace Rules
|
||||
Priority Rules
|
||||
Policy Rules
|
||||
Namespace filter.Rules
|
||||
Priority filter.Rules
|
||||
Policy filter.Rules
|
||||
MinimumPriority string
|
||||
Sources []string
|
||||
}
|
||||
|
||||
func (f *Filter) Validate(result report.Result) bool {
|
||||
if len(f.Sources) > 0 && !contains(result.Source, f.Sources) {
|
||||
if len(f.Sources) > 0 && !helper.Contains(result.Source, f.Sources) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -61,49 +60,21 @@ func (f *Filter) Validate(result report.Result) bool {
|
|||
}
|
||||
|
||||
func (f *Filter) validateNamespaceRules(result report.Result) bool {
|
||||
if result.HasResource() && len(f.Namespace.Include) > 0 {
|
||||
for _, ns := range f.Namespace.Include {
|
||||
if wildcard.Match(ns, result.Resource.Namespace) {
|
||||
if !result.HasResource() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else if result.HasResource() && len(f.Namespace.Exclude) > 0 {
|
||||
for _, ns := range f.Namespace.Exclude {
|
||||
if wildcard.Match(ns, result.Resource.Namespace) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return filter.ValidateNamespace(result.Resource.Namespace, f.Namespace)
|
||||
}
|
||||
|
||||
func (f *Filter) validatePolicyRules(result report.Result) bool {
|
||||
if len(f.Policy.Include) > 0 {
|
||||
for _, ns := range f.Policy.Include {
|
||||
if wildcard.Match(ns, result.Policy) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else if len(f.Policy.Exclude) > 0 {
|
||||
for _, ns := range f.Policy.Exclude {
|
||||
if wildcard.Match(ns, result.Policy) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return filter.ValidateRule(result.Policy, f.Policy)
|
||||
}
|
||||
|
||||
func (f *Filter) validatePriorityRules(result report.Result) bool {
|
||||
if len(f.Priority.Include) > 0 {
|
||||
return contains(result.Priority.String(), f.Priority.Include)
|
||||
} else if len(f.Priority.Exclude) > 0 && contains(result.Priority.String(), f.Priority.Exclude) {
|
||||
return helper.Contains(result.Priority.String(), f.Priority.Include)
|
||||
} else if len(f.Priority.Exclude) > 0 && helper.Contains(result.Priority.String(), f.Priority.Exclude) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -139,13 +110,3 @@ func (c *BaseClient) SkipExistingOnStartup() bool {
|
|||
func NewBaseClient(name string, skipExistingOnStartup bool, filter *Filter) BaseClient {
|
||||
return BaseClient{name, skipExistingOnStartup, filter}
|
||||
}
|
||||
|
||||
func contains(source string, sources []string) bool {
|
||||
for _, s := range sources {
|
||||
if strings.EqualFold(s, source) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package target_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/policy-reporter/pkg/filter"
|
||||
"github.com/kyverno/policy-reporter/pkg/report"
|
||||
"github.com/kyverno/policy-reporter/pkg/target"
|
||||
)
|
||||
|
@ -26,6 +27,18 @@ var result = report.Result{
|
|||
},
|
||||
}
|
||||
|
||||
var result2 = 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,
|
||||
Source: "Kyverno",
|
||||
}
|
||||
|
||||
func Test_BaseClient(t *testing.T) {
|
||||
t.Run("Validate Default", func(t *testing.T) {
|
||||
filter := &target.Filter{}
|
||||
|
@ -49,29 +62,37 @@ func Test_BaseClient(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("Validate ClusterResult", func(t *testing.T) {
|
||||
filter := &target.Filter{Namespace: filter.Rules{Include: []string{"default"}}}
|
||||
|
||||
if !filter.Validate(result2) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Validate Exclude Namespace match", func(t *testing.T) {
|
||||
filter := &target.Filter{Namespace: target.Rules{Exclude: []string{"default"}}}
|
||||
filter := &target.Filter{Namespace: filter.Rules{Exclude: []string{"default"}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Namespace: target.Rules{Exclude: []string{"team-a"}}}
|
||||
filter := &target.Filter{Namespace: filter.Rules{Exclude: []string{"team-a"}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Namespace match", func(t *testing.T) {
|
||||
filter := &target.Filter{Namespace: target.Rules{Include: []string{"default"}}}
|
||||
filter := &target.Filter{Namespace: filter.Rules{Include: []string{"default"}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Namespace mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Namespace: target.Rules{Include: []string{"team-a"}}}
|
||||
filter := &target.Filter{Namespace: filter.Rules{Include: []string{"team-a"}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
|
@ -79,28 +100,28 @@ func Test_BaseClient(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Validate Exclude Priority match", func(t *testing.T) {
|
||||
filter := &target.Filter{Priority: target.Rules{Exclude: []string{report.WarningPriority.String()}}}
|
||||
filter := &target.Filter{Priority: filter.Rules{Exclude: []string{report.WarningPriority.String()}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Priority mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Priority: target.Rules{Exclude: []string{report.ErrorPriority.String()}}}
|
||||
filter := &target.Filter{Priority: filter.Rules{Exclude: []string{report.ErrorPriority.String()}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Priority match", func(t *testing.T) {
|
||||
filter := &target.Filter{Priority: target.Rules{Include: []string{report.WarningPriority.String()}}}
|
||||
filter := &target.Filter{Priority: filter.Rules{Include: []string{report.WarningPriority.String()}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Priority mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Priority: target.Rules{Include: []string{report.ErrorPriority.String()}}}
|
||||
filter := &target.Filter{Priority: filter.Rules{Include: []string{report.ErrorPriority.String()}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
|
@ -108,28 +129,28 @@ func Test_BaseClient(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Validate Exclude Policy match", func(t *testing.T) {
|
||||
filter := &target.Filter{Policy: target.Rules{Exclude: []string{"require-requests-and-limits-required"}}}
|
||||
filter := &target.Filter{Policy: filter.Rules{Exclude: []string{"require-requests-and-limits-required"}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Policy mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Policy: target.Rules{Exclude: []string{"policy-test"}}}
|
||||
filter := &target.Filter{Policy: filter.Rules{Exclude: []string{"policy-test"}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Include Policy match", func(t *testing.T) {
|
||||
filter := &target.Filter{Policy: target.Rules{Include: []string{"require-requests-and-limits-required"}}}
|
||||
filter := &target.Filter{Policy: filter.Rules{Include: []string{"require-requests-and-limits-required"}}}
|
||||
|
||||
if !filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
}
|
||||
})
|
||||
t.Run("Validate Exclude Policy mismatch", func(t *testing.T) {
|
||||
filter := &target.Filter{Policy: target.Rules{Include: []string{"policy-test"}}}
|
||||
filter := &target.Filter{Policy: filter.Rules{Include: []string{"policy-test"}}}
|
||||
|
||||
if filter.Validate(result) {
|
||||
t.Errorf("Unexpected Validation Result")
|
||||
|
|
311
templates/summary.html
Normal file
311
templates/summary.html
Normal file
|
@ -0,0 +1,311 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style type="text/css">
|
||||
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.row-responsive.row{margin-right:0 !important}td.col-lg-3{display:block;width:100% !important;padding-left:0 !important;padding-right:0 !important}.w-full,.w-full>tbody>tr>td{width:100% !important}.p-4:not(table),.p-4:not(.btn)>tbody>tr>td,.p-4.btn td a{padding:16px !important}.pt-6:not(table),.pt-6:not(.btn)>tbody>tr>td,.pt-6.btn td a,.py-6:not(table),.py-6:not(.btn)>tbody>tr>td,.py-6.btn td a{padding-top:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-0>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-8>tbody>tr>td{font-size:32px !important;line-height:32px !important;height:32px !important}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
||||
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
|
||||
<table class="container-fluid" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 0 16px;" align="left">
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ if .ClusterName }}
|
||||
<h1 class="h1" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 36px; line-height: 43.2px; margin: 0;" align="left">{{ .ClusterName}}: Summary Report</h1>
|
||||
{{ else }}
|
||||
<h1 class="h1" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 36px; line-height: 43.2px; margin: 0;" align="left">Summary Report</h1>
|
||||
{{ end }}
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{{ range $key, $source := .Sources }}
|
||||
<table class="card p-4 pt-6 my-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 24px 16px 16px;" align="left" bgcolor="#ffffff">
|
||||
<h2 class="h2" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 32px; line-height: 38.4px; margin: 0;" align="left">{{ $source.Name }}</h2>
|
||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 class="h4" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">ClusterPolicyReport Summary</h3>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ if $source.ClusterReports }}
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="hr" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" align="left">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="container-fluid" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 0 16px;" align="left">
|
||||
<div class="row row-responsive" style="margin-right: -24px;">
|
||||
<table class="" role="presentation" border="0" cellpadding="0" cellspacing="0" style="table-layout: fixed; width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-green-500 text-center" style="color: #198754; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Pass
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ $source.ClusterScopeSummary.Pass }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-orange-500 text-center" style="color: #fd7e14; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Warning
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ $source.ClusterScopeSummary.Warn }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-red-500 text-center" style="color: #dc3545; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Fail
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ $source.ClusterScopeSummary.Fail }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-red-600 text-center" style="color: #b02a37; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Error
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ $source.ClusterScopeSummary.Error }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="hr" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" align="left">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-8 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 32px; font-size: 32px; width: 100%; height: 32px; margin: 0;" align="left" width="100%" height="32">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{if $source.NamespaceScopeSummary }}
|
||||
<h3 class="h4" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">PolicyReport Summary</h3>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-striped thead-default table-bordered" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 100%; border: 1px solid #e2e8f0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Namespace</th>
|
||||
<th class="text-right text-green-500" style="line-height: 24px; font-size: 16px; color: #198754; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="right" valign="top">Pass</th>
|
||||
<th class="text-right text-orange-500" style="line-height: 24px; font-size: 16px; color: #fd7e14; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="right" valign="top">Warning</th>
|
||||
<th class="text-right text-red-500" style="line-height: 24px; font-size: 16px; color: #dc3545; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="right" valign="top">Fail</th>
|
||||
<th class="text-right text-red-600" style="line-height: 24px; font-size: 16px; color: #b02a37; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="right" valign="top">Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $ns, $sum := $source.NamespaceScopeSummary }}
|
||||
<tr style="" bgcolor="#f2f2f2">
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $ns }}</td>
|
||||
<td class="text-right" style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="right" valign="top">{{ $sum.Pass }}</td>
|
||||
<td class="text-right" style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="right" valign="top">{{ $sum.Warn }}</td>
|
||||
<td class="text-right" style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="right" valign="top">{{ $sum.Fail }}</td>
|
||||
<td class="text-right" style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="right" valign="top">{{ $sum.Error }}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
458
templates/violations.html
Normal file
458
templates/violations.html
Normal file
|
@ -0,0 +1,458 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style type="text/css">
|
||||
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.row-responsive.row{margin-right:0 !important}td.col-lg-3{display:block;width:100% !important;padding-left:0 !important;padding-right:0 !important}.w-full,.w-full>tbody>tr>td{width:100% !important}.p-4:not(table),.p-4:not(.btn)>tbody>tr>td,.p-4.btn td a{padding:16px !important}.pt-6:not(table),.pt-6:not(.btn)>tbody>tr>td,.pt-6.btn td a,.py-6:not(table),.py-6:not(.btn)>tbody>tr>td,.py-6.btn td a{padding-top:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-0>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-2>tbody>tr>td{font-size:8px !important;line-height:8px !important;height:8px !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-8>tbody>tr>td{font-size:32px !important;line-height:32px !important;height:32px !important}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
||||
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
|
||||
<table class="container-fluid" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 0 16px;" align="left">
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ if .ClusterName }}
|
||||
<h1 class="h1" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 36px; line-height: 43.2px; margin: 0;" align="left">{{ .ClusterName}}: Summary Report</h1>
|
||||
{{ else }}
|
||||
<h1 class="h1" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 36px; line-height: 43.2px; margin: 0;" align="left">Summary Report</h1>
|
||||
{{ end }}
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ range $key, $source := .Sources }}
|
||||
<table class="card p-4 pt-6" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 24px 16px 16px;" align="left" bgcolor="#ffffff">
|
||||
<h2 class="h2" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 32px; line-height: 38.4px; margin: 0;" align="left">{{ $source.Name }}</h2>
|
||||
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ if $source.ClusterReports }}
|
||||
<h3 class="h4" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">ClusterPolicyReport Summary</h3>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="hr" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" align="left">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 class="h4 text-green-500" style="color: #198754; padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">Pass Results: {{ $source.ClusterPassed }}</h4>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="hr" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" align="left">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ range $status := $.Status }}
|
||||
{{ $results := index $source.ClusterResults $status }}
|
||||
{{ $length := len $results }}
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 class="h4 text-orange-500" style="color: {{ color $status }}; padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">{{ title $status }} Results: {{ $length }} </h4>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ if $results }}
|
||||
<table class="table table-striped thead-default table-bordered" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 100%; border: 1px solid #e2e8f0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Kind</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Name</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Policy</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Valdiation Rule</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $key, $result := $results }}
|
||||
<tr style="" bgcolor="#f2f2f2">
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Kind }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Name }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Policy }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Rule }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Status }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="hr" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; border-top-width: 1px; border-top-color: #e2e8f0; border-top-style: solid; height: 1px; width: 100%; margin: 0;" align="left">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-0 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 0; font-size: 0; width: 100%; height: 0; margin: 0;" align="left" width="100%" height="0">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if or $source.NamespaceResults $source.NamespacePassed }}
|
||||
<table class="s-8 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 32px; font-size: 32px; width: 100%; height: 32px; margin: 0;" align="left" width="100%" height="32">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 class="h4" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="left">PolicyReport Summary</h3>
|
||||
{{ end }}
|
||||
|
||||
{{ range $namespace, $list := $source.NamespaceResults }}
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height:32px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="32">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 class="h5" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 20px; line-height: 24px; margin: 0;" align="left">Namespace: {{ $namespace }}</h3>
|
||||
<table class="s-8 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 32px; font-size: 32px; width: 100%; height: 32px; margin: 0;" align="left" width="100%" height="32">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="card" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left" bgcolor="#ffffff">
|
||||
<table class="container-fluid" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 0 16px;" align="left">
|
||||
<div class="row row-responsive" style="margin-right: -24px;">
|
||||
<table class="" role="presentation" border="0" cellpadding="0" cellspacing="0" style="table-layout: fixed; width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-green-500 text-center" style="color: #198754; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Pass
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{index $source.NamespacePassed $namespace }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-orange-500 text-center" style="color: #fd7e14; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Warning
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ lenNamespaceResults $source $namespace "warn" }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-red-500 text-center" style="color: #dc3545; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Fail
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ lenNamespaceResults $source $namespace "fail" }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="col-lg-3 col-6" style="line-height: 24px; font-size: 16px; min-height: 1px; font-weight: normal; padding-right: 24px; width: 50%; margin: 0;" align="left" valign="top">
|
||||
<table class="p-4" role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 16px;" align="left">
|
||||
<div class="space-y-4">
|
||||
<h1 class="h4 fw-500 text-red-600 text-center" style="color: #b02a37; padding-top: 0; padding-bottom: 0; font-weight: 500 !important; vertical-align: baseline; font-size: 24px; line-height: 28.8px; margin: 0;" align="center">
|
||||
Error
|
||||
</h1>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-4xl text-center" style="line-height: 43.2px; font-size: 36px; width: 100%; margin: 0;" align="center">{{ lenNamespaceResults $source $namespace "error" }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-2 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 8px; font-size: 8px; width: 100%; height: 8px; margin: 0;" align="left" width="100%" height="8">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ if hasViolations $list }}
|
||||
<table class="table table-striped thead-default table-bordered" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 100%; border: 1px solid #e2e8f0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Kind</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Name</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Policy</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Valdiation Rule</th>
|
||||
<th style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border-color: #e2e8f0; border-style: solid; border-width: 1px 1px 2px;" align="left" valign="top">Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $status, $results := $list }}
|
||||
{{ range $key, $result := $results }}
|
||||
<tr style="" bgcolor="#f2f2f2">
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Kind }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Name }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Policy }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">{{ $result.Rule }}</td>
|
||||
<td style="line-height: 24px; font-size: 16px; margin: 0; padding: 12px; border: 1px solid #e2e8f0;" align="left" valign="top">
|
||||
<table class="badge bg-orange-500 text-white" align="left" role="presentation" border="0" cellpadding="0" cellspacing="0" style="color: #ffffff;" bgcolor="{{ color $status }}">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 1; font-size: 75%; display: inline-block; font-weight: 700; white-space: nowrap; border-radius: 4px; color: #ffffff; margin: 0; padding: 4px 6.4px;" align="center" bgcolor="{{ color $status }}" valign="baseline">
|
||||
<span>{{ $result.Status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue