mirror of
https://github.com/kubescape/kubescape.git
synced 2026-04-15 06:58:11 +00:00
Merge pull request #532 from vladklokun/print-results-to-html
feat: Print results to html
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
JunitResultFormat string = "junit"
|
||||
PrometheusFormat string = "prometheus"
|
||||
PdfFormat string = "pdf"
|
||||
HtmlFormat string = "html"
|
||||
)
|
||||
|
||||
type IPrinter interface {
|
||||
|
||||
154
core/pkg/resultshandling/printer/v2/html/report.gohtml
Normal file
154
core/pkg/resultshandling/printer/v2/html/report.gohtml
Normal file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kubescape Scan Report</title>
|
||||
</head>
|
||||
<style>
|
||||
:root {
|
||||
--cell-padding-vertical: 0.25em;
|
||||
--cell-padding-horizontal: 0.25em;
|
||||
--font-family-sans: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
body {
|
||||
max-width: 60em;
|
||||
margin: auto;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-top: 0.1em solid black;
|
||||
border-bottom: 0.1em solid black;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
td, th {
|
||||
padding-top: var(--cell-padding-vertical);
|
||||
padding-bottom: var(--cell-padding-vertical);
|
||||
padding-right: var(--cell-padding-horizontal);
|
||||
vertical-align: top;
|
||||
}
|
||||
td > p {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
hyphens: auto;
|
||||
}
|
||||
thead {
|
||||
border-bottom: 0.01em solid black;
|
||||
}
|
||||
.numericCell {
|
||||
text-align: right;
|
||||
}
|
||||
.controlSeverityCell {
|
||||
width: 10%;
|
||||
}
|
||||
.controlNameCell {
|
||||
width: 50%;
|
||||
}
|
||||
.controlRiskCell {
|
||||
width: 10%;
|
||||
}
|
||||
.resourceSeverityCell {
|
||||
width: 10%;
|
||||
}
|
||||
.resourceNameCell {
|
||||
width: 30%;
|
||||
}
|
||||
.resourceURLCell {
|
||||
width: 10%;
|
||||
}
|
||||
.resourceRemediationCell {
|
||||
width: 50%;
|
||||
}
|
||||
.logo {
|
||||
width: 25%;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<img class="logo" src="https://raw.githubusercontent.com/armosec/kubescape/master/core/pkg/resultshandling/printer/v2/pdf/logo.png">
|
||||
<h1>Kubescape Scan Report</h1>
|
||||
{{ with .OPASessionObj.Report.SummaryDetails }}
|
||||
<h2>By Controls</h2>
|
||||
<h3>Summary</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>All</th>
|
||||
<th>Failed</th>
|
||||
<th>Excluded</th>
|
||||
<th>Skipped</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ .NumberOfControls.All }}</td>
|
||||
<td>{{ .NumberOfControls.Failed }}</td>
|
||||
<td>{{ .NumberOfControls.Excluded }}</td>
|
||||
<td>{{ .NumberOfControls.Skipped }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Details</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="controlSeverityCell">Severity</th>
|
||||
<th class="controlNameCell">Control Name</th>
|
||||
<th class="controlRiskCell">Failed Resources</th>
|
||||
<th class="controlRiskCell">Excluded Resources</th>
|
||||
<th class="controlRiskCell">All Resources</th>
|
||||
<th class="controlRiskCell">Risk Score, %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ $sorted := sortBySeverityName .Controls }}
|
||||
{{ range $control := $sorted }}
|
||||
<tr>
|
||||
<td class="controlSeverityCell">{{ controlSeverityToString $control.ScoreFactor }}</td>
|
||||
<td class="controlNameCell">{{ $control.Name }}</td>
|
||||
<td class="controlRiskCell numericCell">{{ $control.ResourceCounters.FailedResources }}</td>
|
||||
<td class="controlRiskCell numericCell">{{ $control.ResourceCounters.ExcludedResources }}</td>
|
||||
<td class="controlRiskCell numericCell">{{ sum $control.ResourceCounters.ExcludedResources $control.ResourceCounters.FailedResources $control.ResourceCounters.PassedResources }}</td>
|
||||
<td class="controlRiskCell numericCell">{{ float32ToInt $control.Score }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
<h2>By Resource</h2>
|
||||
{{ $sortedResourceTableView := sortByNamespace .ResourceTableView }}
|
||||
{{ range $sortedResourceTableView }}
|
||||
<h3>Name: {{ .Resource.GetName }}</h3>
|
||||
<p>ApiVersion: {{ .Resource.GetApiVersion }}</p>
|
||||
<p>Kind: {{ .Resource.GetKind }}</p>
|
||||
<p>Name: {{ .Resource.GetName }}</p>
|
||||
<p>Namespace: {{ .Resource.GetNamespace }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="resourceSeverityCell">Severity</th>
|
||||
<th class="resourceNameCell">Name</th>
|
||||
<th class="resourceURLCell">Docs</th>
|
||||
<th class="resourceRemediationCell">Assistant Remediation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .ControlsResult }}
|
||||
<tr>
|
||||
<td class="resourceSeverityCell">{{ .Severity }}</td>
|
||||
<td class="resourceNameCell">{{ .Name }}</td>
|
||||
<td class="resourceURLCell"><a href="https://hub.armo.cloud/docs/{{ lower .URL }}">{{ .URL }}</a></td>
|
||||
<td class="resourceRemediationCell">{{ range .FailedPaths }} <p>{{ . }}</p> {{ end }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
151
core/pkg/resultshandling/printer/v2/htmlprinter.go
Normal file
151
core/pkg/resultshandling/printer/v2/htmlprinter.go
Normal file
@@ -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
|
||||
}
|
||||
19
core/pkg/resultshandling/printer/v2/reportingstructs.go
Normal file
19
core/pkg/resultshandling/printer/v2/reportingstructs.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user