From af1d5694dcdf535b5be5a3063c9dce4f04a10933 Mon Sep 17 00:00:00 2001 From: Vlad Klokun Date: Fri, 3 Jun 2022 19:22:12 +0300 Subject: [PATCH] feat: add HTML as an output for scan results --- cmd/scan/scan.go | 2 +- .../resultshandling/printer/printresults.go | 1 + .../printer/v2/html/report.gohtml | 154 ++++++++++++++++++ .../resultshandling/printer/v2/htmlprinter.go | 151 +++++++++++++++++ .../printer/v2/reportingstructs.go | 19 +++ core/pkg/resultshandling/results.go | 2 + 6 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 core/pkg/resultshandling/printer/v2/html/report.gohtml create mode 100644 core/pkg/resultshandling/printer/v2/htmlprinter.go create mode 100644 core/pkg/resultshandling/printer/v2/reportingstructs.go diff --git a/cmd/scan/scan.go b/cmd/scan/scan.go index feaa0142..ccff3a99 100644 --- a/cmd/scan/scan.go +++ b/cmd/scan/scan.go @@ -72,7 +72,7 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command { scanCmd.PersistentFlags().StringVar(&scanInfo.UseArtifactsFrom, "use-artifacts-from", "", "Load artifacts from local directory. If not used will download them") scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system,kube-public") scanCmd.PersistentFlags().Float32VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1") - scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer","json","junit","prometheus","pdf"`) + scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer", "json", "junit", "prometheus", "pdf", "html"`) scanCmd.PersistentFlags().StringVar(&scanInfo.IncludeNamespaces, "include-namespaces", "", "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to ARMO backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") scanCmd.PersistentFlags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") diff --git a/core/pkg/resultshandling/printer/printresults.go b/core/pkg/resultshandling/printer/printresults.go index 9c2c1c20..1202736e 100644 --- a/core/pkg/resultshandling/printer/printresults.go +++ b/core/pkg/resultshandling/printer/printresults.go @@ -17,6 +17,7 @@ const ( JunitResultFormat string = "junit" PrometheusFormat string = "prometheus" PdfFormat string = "pdf" + HtmlFormat string = "html" ) type IPrinter interface { diff --git a/core/pkg/resultshandling/printer/v2/html/report.gohtml b/core/pkg/resultshandling/printer/v2/html/report.gohtml new file mode 100644 index 00000000..784f003a --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/html/report.gohtml @@ -0,0 +1,154 @@ + + + + + Kubescape Scan Report + + + + +

Kubescape Scan Report

+ {{ with .OPASessionObj.Report.SummaryDetails }} +

By Controls

+

Summary

+ + + + + + + + + + + + + + + + + +
AllFailedExcludedSkipped
{{ .NumberOfControls.All }}{{ .NumberOfControls.Failed }}{{ .NumberOfControls.Excluded }}{{ .NumberOfControls.Skipped }}
+

Details

+ + + + + + + + + + + + + {{ $sorted := sortBySeverityName .Controls }} + {{ range $control := $sorted }} + + + + + + + + + + {{ end }} + +
SeverityControl NameFailed ResourcesExcluded ResourcesAll ResourcesRisk Score, %
{{ controlSeverityToString $control.ScoreFactor }}{{ $control.Name }}{{ $control.ResourceCounters.FailedResources }}{{ $control.ResourceCounters.ExcludedResources }}{{ sum $control.ResourceCounters.ExcludedResources $control.ResourceCounters.FailedResources $control.ResourceCounters.PassedResources }}{{ float32ToInt $control.Score }}
+ {{ end }} +

By Resource

+ {{ $sortedResourceTableView := sortByNamespace .ResourceTableView }} + {{ range $sortedResourceTableView }} +

Name: {{ .Resource.GetName }}

+

ApiVersion: {{ .Resource.GetApiVersion }}

+

Kind: {{ .Resource.GetKind }}

+

Name: {{ .Resource.GetName }}

+

Namespace: {{ .Resource.GetNamespace }}

+ + + + + + + + + + + {{ range .ControlsResult }} + + + + + + + {{ end }} + +
SeverityNameDocsAssistant Remediation
{{ .Severity }}{{ .Name }}{{ .URL }}{{ range .FailedPaths }}

{{ . }}

{{ end }}
+ + {{ end }} + + diff --git a/core/pkg/resultshandling/printer/v2/htmlprinter.go b/core/pkg/resultshandling/printer/v2/htmlprinter.go new file mode 100644 index 00000000..c93d0238 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/htmlprinter.go @@ -0,0 +1,151 @@ +package v2 + +import ( + _ "embed" + "html/template" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/armosec/kubescape/v2/core/cautils" + "github.com/armosec/kubescape/v2/core/cautils/logger" + "github.com/armosec/kubescape/v2/core/cautils/logger/helpers" + "github.com/armosec/kubescape/v2/core/pkg/resultshandling/printer" + "github.com/armosec/opa-utils/reporthandling/apis" + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" +) + +const ( + htmlOutputFile = "report" + htmlOutputExt = ".html" +) + +//go:embed html/report.gohtml +var reportTemplate string + +type HTMLReportingCtx struct { + OPASessionObj *cautils.OPASessionObj + ResourceTableView ResourceTableView +} + +type HtmlPrinter struct { + writer *os.File +} + +func NewHtmlPrinter() *HtmlPrinter { + return &HtmlPrinter{} +} + +func (htmlPrinter *HtmlPrinter) SetWriter(outputFile string) { + if outputFile == "" { + outputFile = htmlOutputFile + } + if filepath.Ext(strings.TrimSpace(outputFile)) != htmlOutputExt { + outputFile = outputFile + htmlOutputExt + } + htmlPrinter.writer = printer.GetWriter(outputFile) +} + +func (htmlPrinter *HtmlPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + tplFuncMap := template.FuncMap{ + "sum": func(nums ...int) int { + total := 0 + for _, n := range nums { + total += n + } + return total + }, + "float32ToInt": cautils.Float32ToInt, + "lower": strings.ToLower, + "sortByNamespace": func(resourceTableView ResourceTableView) ResourceTableView { + sortedResourceTableView := make(ResourceTableView, len(resourceTableView)) + copy(sortedResourceTableView, resourceTableView) + + sort.SliceStable( + sortedResourceTableView, + func(i, j int) bool { + return sortedResourceTableView[i].Resource.GetNamespace() < sortedResourceTableView[j].Resource.GetNamespace() + }, + ) + return sortedResourceTableView + }, + "controlSeverityToString": apis.ControlSeverityToString, + "sortBySeverityName": func(controlSummaries map[string]reportsummary.ControlSummary) []reportsummary.ControlSummary { + sortedSlice := make([]reportsummary.ControlSummary, 0, len(controlSummaries)) + for _, val := range controlSummaries { + sortedSlice = append(sortedSlice, val) + } + + sort.SliceStable( + sortedSlice, + func(i, j int) bool { + //First sort by Severity descending + iSeverity := apis.ControlSeverityToInt(sortedSlice[i].GetScoreFactor()) + jSeverity := apis.ControlSeverityToInt(sortedSlice[j].GetScoreFactor()) + if iSeverity > jSeverity { + return true + } + if iSeverity < jSeverity { + return false + } + //And then by Name ascending + return sortedSlice[i].GetName() < sortedSlice[j].GetName() + }, + ) + + return sortedSlice + }, + } + tpl := template.Must( + template.New("htmlReport").Funcs(tplFuncMap).Parse(reportTemplate), + ) + + resourceTableView := bulidResourceTableView(opaSessionObj) + reportingCtx := HTMLReportingCtx{opaSessionObj, resourceTableView} + err := tpl.Execute(htmlPrinter.writer, reportingCtx) + if err != nil { + logger.L().Error("failed to render template", helpers.Error(err)) + } +} + +func (htmlPrinter *HtmlPrinter) Score(score float32) { + return +} + +func bulidResourceTableView(opaSessionObj *cautils.OPASessionObj) ResourceTableView { + resourceTableView := make(ResourceTableView, 0) + for resourceID, result := range opaSessionObj.ResourcesResult { + if result.GetStatus(nil).IsFailed() { + resource := opaSessionObj.AllResources[resourceID] + ctlResults := buildResourceControlResultTable(result.AssociatedControls, &opaSessionObj.Report.SummaryDetails) + resourceTableView = append(resourceTableView, ResourceResult{resource, ctlResults}) + } + } + + return resourceTableView +} + +func buildResourceControlResult(resourceControl resourcesresults.ResourceAssociatedControl, control reportsummary.IControlSummary) ResourceControlResult { + ctlSeverity := apis.ControlSeverityToString(control.GetScoreFactor()) + ctlName := resourceControl.GetName() + ctlURL := resourceControl.GetID() + failedPaths := failedPathsToString(&resourceControl) + + return ResourceControlResult{ctlSeverity, ctlName, ctlURL, failedPaths} +} + +func buildResourceControlResultTable(resourceControls []resourcesresults.ResourceAssociatedControl, summaryDetails *reportsummary.SummaryDetails) []ResourceControlResult { + var ctlResults []ResourceControlResult + for _, resourceControl := range resourceControls { + if resourceControl.GetStatus(nil).IsFailed() { + control := summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaName, resourceControl.GetName()) + ctlResult := buildResourceControlResult(resourceControl, control) + + ctlResults = append(ctlResults, ctlResult) + } + } + + return ctlResults +} diff --git a/core/pkg/resultshandling/printer/v2/reportingstructs.go b/core/pkg/resultshandling/printer/v2/reportingstructs.go new file mode 100644 index 00000000..052d1110 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/reportingstructs.go @@ -0,0 +1,19 @@ +package v2 + +import ( + "github.com/armosec/k8s-interface/workloadinterface" +) + +type ResourceTableView []ResourceResult + +type ResourceResult struct { + Resource workloadinterface.IMetadata + ControlsResult []ResourceControlResult +} + +type ResourceControlResult struct { + Severity string + Name string + URL string + FailedPaths []string +} diff --git a/core/pkg/resultshandling/results.go b/core/pkg/resultshandling/results.go index 9e4d42e5..6d1764e8 100644 --- a/core/pkg/resultshandling/results.go +++ b/core/pkg/resultshandling/results.go @@ -95,6 +95,8 @@ func NewPrinter(printFormat, formatVersion string, verboseMode bool, viewType ca return printerv2.NewPrometheusPrinter(verboseMode) case printer.PdfFormat: return printerv2.NewPdfPrinter() + case printer.HtmlFormat: + return printerv2.NewHtmlPrinter() default: return printerv2.NewPrettyPrinter(verboseMode, formatVersion, viewType) }