diff --git a/cmd/polaris/audit.go b/cmd/polaris/audit.go index ca06f6c9..2a4ac8de 100644 --- a/cmd/polaris/audit.go +++ b/cmd/polaris/audit.go @@ -51,6 +51,7 @@ var ( helmValues string checks []string auditNamespace string + severityLevel string skipSslValidation bool uploadInsights bool clusterName string @@ -72,6 +73,7 @@ func init() { auditCmd.PersistentFlags().StringVar(&helmValues, "helm-values", "", "Optional flag to add helm values") auditCmd.PersistentFlags().StringSliceVar(&checks, "checks", []string{}, "Optional flag to specify specific checks to check") auditCmd.PersistentFlags().StringVar(&auditNamespace, "namespace", "", "Namespace to audit. Only applies to in-cluster audits") + auditCmd.PersistentFlags().StringVar(&severityLevel, "severity", "", "Severity level used to filter results. Behaves like log levels. 'danger' is the least verbose (warning, danger)") auditCmd.PersistentFlags().BoolVar(&skipSslValidation, "skip-ssl-validation", false, "Skip https certificate verification") auditCmd.PersistentFlags().BoolVar(&uploadInsights, "upload-insights", false, "Upload scan results to Fairwinds Insights") auditCmd.PersistentFlags().StringVar(&clusterName, "cluster-name", "", "Set --cluster-name to a descriptive name for the cluster you're auditing") @@ -175,7 +177,7 @@ var auditCmd = &cobra.Command{ logrus.Println("Success! You can see your results at:") logrus.Printf("%s/orgs/%s/clusters/%s/action-items\n", insightsHost, auth.Organization, clusterName) } else { - outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests) + outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests, severityLevel) } summary := auditData.GetSummary() @@ -223,10 +225,20 @@ func ProcessHelmTemplates(helmChart, helmValues string) (string, error) { return dir, nil } -func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFormat string, useColor bool, onlyShowFailedTests bool) { +func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFormat string, useColor bool, onlyShowFailedTests bool, severityLevel string) { if onlyShowFailedTests { auditData = auditData.RemoveSuccessfulResults() } + + if severityLevel != "" { + switch severityLevel { + case "danger": + auditData = auditData.FilterResultsBySeverityLevel(cfg.SeverityDanger) + case "warning": + auditData = auditData.FilterResultsBySeverityLevel(cfg.SeverityWarning) + } + } + var outputBytes []byte var err error if outputFormat == "score" { diff --git a/docs/cli.md b/docs/cli.md index 1212747d..4baadf57 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -52,6 +52,9 @@ webhook --resource string Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend. --set-exit-code-below-score int Set an exit code of 4 when the score is below this threshold (1-100). --set-exit-code-on-danger Set an exit code of 3 when the audit contains danger-level issues. + --severity string Severity level used to filter results. Behaves like log levels. 'danger' is the least verbose (warning, danger) + --skip-ssl-validation Skip https certificate verification + --upload-insights Upload scan results to Fairwinds Insights # webhook flags --disable-webhook-config-installer disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping. diff --git a/pkg/validator/output.go b/pkg/validator/output.go index 1f996e04..86808577 100644 --- a/pkg/validator/output.go +++ b/pkg/validator/output.go @@ -53,6 +53,26 @@ type AuditData struct { Score uint } +// FilterResultsBySeverityLevel includes results according to the provided severity level: +// 'danger' is the least verbose, 'warning' is medium verbosity, default behavior will +// include all results, which currently also includes 'ignore' +func (res AuditData) FilterResultsBySeverityLevel(severityLevel config.Severity) AuditData { + resCopy := res + resCopy.Results = []Result{} + + filteredResults := funk.Map(res.Results, func(auditDataResult Result) Result { + return auditDataResult.filterResultsBySeverityLevel(severityLevel) + }).([]Result) + + for _, result := range filteredResults { + if result.isNotEmpty() { + resCopy.Results = append(resCopy.Results, result) + } + } + + return resCopy +} + // RemoveSuccessfulResults removes all tests that have passed func (res AuditData) RemoveSuccessfulResults() AuditData { resCopy := res @@ -108,6 +128,25 @@ func (res ResultSet) removeSuccessfulResults() ResultSet { return newResults } +func (res ResultSet) filterResultsBySeverityLevel(severityLevel config.Severity) ResultSet { + newResults := ResultSet{} + for k, resultMessage := range res { + switch severityLevel { + case config.SeverityDanger: + if resultMessage.Severity == config.SeverityDanger { + newResults[k] = resultMessage + } + case config.SeverityWarning: + if resultMessage.Severity == config.SeverityDanger || resultMessage.Severity == config.SeverityWarning { + newResults[k] = resultMessage + } + default: + return res + } + } + return newResults +} + // Result provides results for a Kubernetes object type Result struct { Name string @@ -128,6 +167,16 @@ func (res Result) removeSuccessfulResults() Result { return resCopy } +func (res Result) filterResultsBySeverityLevel(severityLevel config.Severity) Result { + resCopy := res + resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel) + if res.PodResult != nil { + podCopy := res.PodResult.filterResultsBySeverityLevel(severityLevel) + resCopy.PodResult = &podCopy + } + return resCopy +} + func (res Result) isNotEmpty() bool { if res.PodResult != nil { return res.PodResult.isNotEmpty() @@ -151,6 +200,15 @@ func (res PodResult) removeSuccessfulResults() PodResult { return resCopy } +func (res PodResult) filterResultsBySeverityLevel(severityLevel config.Severity) PodResult { + resCopy := PodResult{} + resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel) + resCopy.ContainerResults = funk.Map(res.ContainerResults, func(containerResult ContainerResult) ContainerResult { + return containerResult.filterResultsBySeverityLevel(severityLevel) + }).([]ContainerResult) + return resCopy +} + func (res PodResult) isNotEmpty() bool { for _, cr := range res.ContainerResults { if cr.isNotEmpty() { @@ -172,6 +230,12 @@ func (res ContainerResult) removeSuccessfulResults() ContainerResult { return resCopy } +func (res ContainerResult) filterResultsBySeverityLevel(severityLevel config.Severity) ContainerResult { + resCopy := res + resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel) + return resCopy +} + func (res ContainerResult) isNotEmpty() bool { return res.Results.isNotEmpty() }