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
+
+
+
+ | All |
+ Failed |
+ Excluded |
+ Skipped |
+
+
+
+
+ | {{ .NumberOfControls.All }} |
+ {{ .NumberOfControls.Failed }} |
+ {{ .NumberOfControls.Excluded }} |
+ {{ .NumberOfControls.Skipped }} |
+
+
+
+ Details
+
+
+
+ | Severity |
+ Control Name |
+ Failed Resources |
+ Excluded Resources |
+ All Resources |
+ Risk Score, % |
+
+
+
+ {{ $sorted := sortBySeverityName .Controls }}
+ {{ range $control := $sorted }}
+
+ | {{ 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 }}
+
+
+ {{ end }}
+ By Resource
+ {{ $sortedResourceTableView := sortByNamespace .ResourceTableView }}
+ {{ range $sortedResourceTableView }}
+ Name: {{ .Resource.GetName }}
+ ApiVersion: {{ .Resource.GetApiVersion }}
+ Kind: {{ .Resource.GetKind }}
+ Name: {{ .Resource.GetName }}
+ Namespace: {{ .Resource.GetNamespace }}
+
+
+
+ | Severity |
+ Name |
+ Docs |
+ Assistant Remediation |
+
+
+
+ {{ range .ControlsResult }}
+
+ | {{ .Severity }} |
+ {{ .Name }} |
+ {{ .URL }} |
+ {{ range .FailedPaths }} {{ . }} {{ end }} |
+
+ {{ 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)
}