mirror of
https://github.com/kubescape/kubescape.git
synced 2026-04-15 06:58:11 +00:00
233 lines
8.5 KiB
Go
233 lines
8.5 KiB
Go
package printer
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
logger "github.com/kubescape/go-logger"
|
|
"github.com/kubescape/go-logger/helpers"
|
|
"github.com/kubescape/k8s-interface/workloadinterface"
|
|
"github.com/kubescape/kubescape/v2/core/cautils"
|
|
"github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer"
|
|
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
|
|
"github.com/kubescape/opa-utils/shared"
|
|
)
|
|
|
|
const (
|
|
junitOutputFile = "report"
|
|
junitOutputExt = ".xml"
|
|
)
|
|
|
|
/*
|
|
riskScore
|
|
status
|
|
*/
|
|
type JunitPrinter struct {
|
|
writer *os.File
|
|
verbose bool
|
|
}
|
|
|
|
// https://llg.cubic.org/docs/junit/
|
|
|
|
type JUnitXML struct {
|
|
TestSuites JUnitTestSuites `xml:"testsuites"`
|
|
}
|
|
|
|
// JUnitTestSuites represents the test summary
|
|
type JUnitTestSuites struct {
|
|
XMLName xml.Name `xml:"testsuites"`
|
|
Suites []JUnitTestSuite `xml:"testsuite"` // list of controls
|
|
Errors int `xml:"errors,attr"` // total number of tests with error result from all testsuites
|
|
Failures int `xml:"failures,attr"` // total number of failed tests from all testsuites
|
|
Tests int `xml:"tests,attr"` // total number of tests from all testsuites. Some software may expect to only see the number of successful tests from all testsuites though
|
|
Time string `xml:"time,attr"` // time in seconds to execute all test suites
|
|
Name string `xml:"name,attr"` // ? Add framework names ?
|
|
}
|
|
|
|
// JUnitTestSuite represents a single control
|
|
type JUnitTestSuite struct {
|
|
XMLName xml.Name `xml:"testsuite"`
|
|
Tests int `xml:"tests,attr"` // total number of tests from this testsuite. Some software may expect to only see the number of successful tests though
|
|
Name string `xml:"name,attr"` // Full (class) name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents. Required
|
|
Errors int `xml:"errors,attr"` // The total number of tests in the suite that errors
|
|
Failures int `xml:"failures,attr"` // The total number of tests in the suite that failed
|
|
Hostname string `xml:"hostname,attr"` // Host on which the tests were executed ? cluster name ?
|
|
ID int `xml:"id,attr"` // Starts at 0 for the first testsuite and is incremented by 1 for each following testsuite
|
|
Skipped string `xml:"skipped,attr"` // The total number of skipped tests
|
|
Time string `xml:"time,attr"` // Time taken (in seconds) to execute the tests in the suite
|
|
Timestamp string `xml:"timestamp,attr"` // when the test was executed in ISO 8601 format (2014-01-21T16:17:18)
|
|
Properties []JUnitProperty `xml:"properties>property,omitempty"`
|
|
TestCases []JUnitTestCase `xml:"testcase"`
|
|
}
|
|
|
|
// JUnitTestCase represents a single resource
|
|
type JUnitTestCase struct {
|
|
XMLName xml.Name `xml:"testcase"`
|
|
Classname string `xml:"classname,attr"` // Full class name for the class the test method is in. required
|
|
Name string `xml:"name,attr"` // Name of the test method, required
|
|
Time string `xml:"time,attr"` // Time taken (in seconds) to execute the test. optional
|
|
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
|
|
Failure *JUnitFailure `xml:"failure,omitempty"`
|
|
}
|
|
|
|
// JUnitSkipMessage contains the reason why a testcase was skipped.
|
|
type JUnitSkipMessage struct {
|
|
Message string `xml:"message,attr"`
|
|
}
|
|
|
|
// JUnitProperty represents a key/value pair used to define properties.
|
|
type JUnitProperty struct {
|
|
Name string `xml:"name,attr"`
|
|
Value string `xml:"value,attr"`
|
|
}
|
|
|
|
// JUnitFailure contains data related to a failed test.
|
|
type JUnitFailure struct {
|
|
Message string `xml:"message,attr"`
|
|
Type string `xml:"type,attr"`
|
|
Contents string `xml:",chardata"`
|
|
}
|
|
|
|
func NewJunitPrinter(verbose bool) *JunitPrinter {
|
|
return &JunitPrinter{
|
|
verbose: verbose,
|
|
}
|
|
}
|
|
|
|
func (jp *JunitPrinter) SetWriter(outputFile string) {
|
|
if strings.TrimSpace(outputFile) == "" {
|
|
outputFile = junitOutputFile
|
|
}
|
|
if filepath.Ext(strings.TrimSpace(outputFile)) != junitOutputExt {
|
|
outputFile = outputFile + junitOutputExt
|
|
}
|
|
jp.writer = printer.GetWriter(outputFile)
|
|
}
|
|
|
|
func (jp *JunitPrinter) Score(score float32) {
|
|
fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", cautils.Float32ToInt(score))
|
|
}
|
|
|
|
func (jp *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
|
|
junitResult := testsSuites(opaSessionObj)
|
|
postureReportStr, err := xml.Marshal(junitResult)
|
|
if err != nil {
|
|
logger.L().Fatal("failed to Marshal xml result object", helpers.Error(err))
|
|
}
|
|
|
|
if _, err := jp.writer.Write(postureReportStr); err != nil {
|
|
logger.L().Error("failed to write results", helpers.Error(err))
|
|
} else {
|
|
printer.LogOutputFile(jp.writer.Name())
|
|
}
|
|
}
|
|
|
|
func testsSuites(results *cautils.OPASessionObj) *JUnitTestSuites {
|
|
return &JUnitTestSuites{
|
|
Suites: listTestsSuite(results),
|
|
Tests: results.Report.SummaryDetails.NumberOfControls().All(),
|
|
Name: "Kubescape Scanning",
|
|
Failures: results.Report.SummaryDetails.NumberOfControls().Failed(),
|
|
}
|
|
}
|
|
func listTestsSuite(results *cautils.OPASessionObj) []JUnitTestSuite {
|
|
var testSuites []JUnitTestSuite
|
|
|
|
// control scan
|
|
if len(results.Report.SummaryDetails.ListFrameworks()) == 0 {
|
|
testSuite := JUnitTestSuite{}
|
|
testSuite.Tests = results.Report.SummaryDetails.NumberOfControls().All()
|
|
testSuite.Failures = results.Report.SummaryDetails.NumberOfControls().Failed()
|
|
testSuite.Timestamp = results.Report.ReportGenerationTime.String()
|
|
testSuite.ID = 0
|
|
testSuite.Name = "kubescape"
|
|
testSuite.Properties = properties(results.Report.SummaryDetails.Score)
|
|
testSuite.TestCases = testsCases(results, &results.Report.SummaryDetails.Controls, "Kubescape")
|
|
testSuites = append(testSuites, testSuite)
|
|
return testSuites
|
|
}
|
|
|
|
for i, f := range results.Report.SummaryDetails.Frameworks {
|
|
testSuite := JUnitTestSuite{}
|
|
testSuite.Tests = f.NumberOfControls().All()
|
|
testSuite.Failures = f.NumberOfControls().Failed()
|
|
testSuite.Timestamp = results.Report.ReportGenerationTime.String()
|
|
testSuite.ID = i
|
|
testSuite.Name = f.Name
|
|
testSuite.Properties = properties(f.Score)
|
|
testSuite.TestCases = testsCases(results, f.GetControls(), f.GetName())
|
|
testSuites = append(testSuites, testSuite)
|
|
}
|
|
|
|
return testSuites
|
|
}
|
|
func testsCases(results *cautils.OPASessionObj, controls reportsummary.IControlsSummaries, classname string) []JUnitTestCase {
|
|
var testCases []JUnitTestCase
|
|
|
|
iter := controls.ListControlsIDs().All()
|
|
for iter.HasNext() {
|
|
cID := iter.Next()
|
|
testCase := JUnitTestCase{}
|
|
control := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, cID)
|
|
|
|
testCase.Name = control.GetName()
|
|
testCase.Classname = classname
|
|
|
|
if control.GetStatus().IsFailed() {
|
|
resources := map[string]interface{}{}
|
|
resourceIDs := control.ListResourcesIDs().Failed()
|
|
for j := range resourceIDs {
|
|
resource := results.AllResources[resourceIDs[j]]
|
|
sourcePath := ""
|
|
if ResourceSourcePath, ok := results.ResourceSource[resourceIDs[j]]; ok {
|
|
sourcePath = ResourceSourcePath.RelativePath
|
|
}
|
|
resources[resourceToString(resource, sourcePath)] = nil
|
|
}
|
|
resourcesStr := shared.MapStringToSlice(resources)
|
|
sort.Strings(resourcesStr)
|
|
testCaseFailure := JUnitFailure{}
|
|
testCaseFailure.Type = "Control"
|
|
// testCaseFailure.Contents =
|
|
testCaseFailure.Message = fmt.Sprintf("Remediation: %s\nMore details: %s\n\n%s", control.GetRemediation(), cautils.GetControlLink(control.GetID()), strings.Join(resourcesStr, "\n"))
|
|
|
|
testCase.Failure = &testCaseFailure
|
|
} else if control.GetStatus().IsSkipped() {
|
|
testCase.SkipMessage = &JUnitSkipMessage{
|
|
Message: "", // TODO - fill after statusInfo is supported
|
|
}
|
|
|
|
}
|
|
testCases = append(testCases, testCase)
|
|
}
|
|
return testCases
|
|
}
|
|
|
|
func resourceToString(resource workloadinterface.IMetadata, sourcePath string) string {
|
|
sep := "; "
|
|
s := ""
|
|
s += fmt.Sprintf("apiVersion: %s", resource.GetApiVersion()) + sep
|
|
s += fmt.Sprintf("kind: %s", resource.GetKind()) + sep
|
|
if resource.GetNamespace() != "" {
|
|
s += fmt.Sprintf("namespace: %s", resource.GetNamespace()) + sep
|
|
}
|
|
s += fmt.Sprintf("name: %s", resource.GetName())
|
|
if sourcePath != "" {
|
|
s += sep + fmt.Sprintf("sourcePath: %s", sourcePath)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func properties(riskScore float32) []JUnitProperty {
|
|
return []JUnitProperty{
|
|
{
|
|
Name: "riskScore",
|
|
Value: fmt.Sprintf("%.2f", riskScore),
|
|
},
|
|
}
|
|
}
|