diff --git a/core/pkg/resultshandling/printer/v2/htmlprinter.go b/core/pkg/resultshandling/printer/v2/htmlprinter.go index 5e0dfe13..958c6e25 100644 --- a/core/pkg/resultshandling/printer/v2/htmlprinter.go +++ b/core/pkg/resultshandling/printer/v2/htmlprinter.go @@ -131,7 +131,7 @@ func buildResourceControlResult(resourceControl resourcesresults.ResourceAssocia ctlSeverity := apis.ControlSeverityToString(control.GetScoreFactor()) ctlName := resourceControl.GetName() ctlURL := resourceControl.GetID() - failedPaths := failedPathsToString(&resourceControl) + failedPaths := append(failedPathsToString(&resourceControl), fixPathsToString(&resourceControl)...) return ResourceControlResult{ctlSeverity, ctlName, ctlURL, failedPaths} } diff --git a/core/pkg/resultshandling/printer/v2/junit.go b/core/pkg/resultshandling/printer/v2/junit.go index a7a53d57..cbc2842b 100644 --- a/core/pkg/resultshandling/printer/v2/junit.go +++ b/core/pkg/resultshandling/printer/v2/junit.go @@ -4,7 +4,6 @@ import ( "encoding/xml" "fmt" "os" - "sort" "strings" logger "github.com/kubescape/go-logger" @@ -12,8 +11,9 @@ import ( "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/apis" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" - "github.com/kubescape/opa-utils/shared" + "github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults" ) /* @@ -55,6 +55,7 @@ type JUnitTestSuite struct { 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) + File string `xml:"file,attr"` // The file be tested Properties []JUnitProperty `xml:"properties>property,omitempty"` TestCases []JUnitTestCase `xml:"testcase"` } @@ -88,6 +89,11 @@ type JUnitFailure struct { Contents string `xml:",chardata"` } +const ( + lineSeparator = "\n===================================================================================================================\n\n" + testCaseTypeResources = "Resources" +) + func NewJunitPrinter(verbose bool) *JunitPrinter { return &JunitPrinter{ verbose: verbose, @@ -118,96 +124,119 @@ func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionO func testsSuites(results *cautils.OPASessionObj) *JUnitTestSuites { return &JUnitTestSuites{ Suites: listTestsSuite(results), - Tests: results.Report.SummaryDetails.NumberOfControls().All(), + Tests: results.Report.SummaryDetails.NumberOfResources().All(), Name: "Kubescape Scanning", - Failures: results.Report.SummaryDetails.NumberOfControls().Failed(), + Failures: results.Report.SummaryDetails.NumberOfResources().Failed(), } } + +// aggregate resources source to a list of resources results +func sourceToResourcesResults(results *cautils.OPASessionObj) map[string][]resourcesresults.Result { + resourceResults := make(map[string][]resourcesresults.Result) + for i := range results.ResourceSource { + if r, ok := results.ResourcesResult[i]; ok { + if _, ok := resourceResults[results.ResourceSource[i].RelativePath]; !ok { + resourceResults[results.ResourceSource[i].RelativePath] = []resourcesresults.Result{} + } + resourceResults[results.ResourceSource[i].RelativePath] = append(resourceResults[results.ResourceSource[i].RelativePath], r) + } + } + return resourceResults +} + +// listTestsSuite returns a list of testsuites func listTestsSuite(results *cautils.OPASessionObj) []JUnitTestSuite { var testSuites []JUnitTestSuite - + resourceResults := sourceToResourcesResults(results) + counter := 0 // control scan - if len(results.Report.SummaryDetails.ListFrameworks()) == 0 { + for path, resourcesResult := range resourceResults { testSuite := JUnitTestSuite{} - 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.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) + testSuite.ID = counter + counter++ + testSuite.File = path + testSuite.TestCases = testsCases(results, resourcesResult) + if len(testSuite.TestCases) > 0 { + 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 - testCase.Status = string(control.GetStatus().Status()) - - if control.GetStatus().IsFailed() { - resources := map[string]interface{}{} - resourceIDs := control.ListResourcesIDs().Failed() - for j := range resourceIDs { - resource := results.AllResources[resourceIDs[j]] - resources[resourceToString(resource)] = nil +func failedControlsToFailureMessage(results *cautils.OPASessionObj, controls []resourcesresults.ResourceAssociatedControl, severityCounter []int) string { + msg := "" + for _, c := range controls { + control := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, c.GetID()) + if c.GetStatus(nil).IsFailed() { + msg += fmt.Sprintf("Test: %s\n", control.GetName()) + msg += fmt.Sprintf("Severity: %s\n", apis.ControlSeverityToString(control.GetScoreFactor())) + msg += fmt.Sprintf("Remediation: %s\n", control.GetRemediation()) + msg += fmt.Sprintf("Link: %s\n", getControlLink(control.GetID())) + if failedPaths := failedPathsToString(&c); len(failedPaths) > 0 { + msg += fmt.Sprintf("Failed paths: \n - %s\n", strings.Join(failedPaths, "\n - ")) } - 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(), 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 + if fixPaths := fixPathsToString(&c); len(fixPaths) > 0 { + msg += fmt.Sprintf("Available fix: \n - %s\n", strings.Join(fixPaths, "\n - ")) } + msg += "\n" + severityCounter[apis.ControlSeverityToInt(control.GetScoreFactor())] += 1 } + } + return msg +} + +// Every testCase includes a file (even if the file contains several resources) +func testsCases(results *cautils.OPASessionObj, resourcesResult []resourcesresults.Result) []JUnitTestCase { + var testCases []JUnitTestCase + testCase := JUnitTestCase{} + testCaseFailure := JUnitFailure{} + testCaseFailure.Type = testCaseTypeResources + message := "" + + // severityCounter represents the severities, 0: Unknown, 1: Low, 2: Medium, 3: High, 4: Critical + severityCounter := make([]int, apis.NumberOfSeverities, apis.NumberOfSeverities) + + for i := range resourcesResult { + message += lineSeparator + if failedControls := failedControlsToFailureMessage(results, resourcesResult[i].ListControls(), severityCounter); failedControls != "" { + message += fmt.Sprintf("Resource: %s\n\n%s", resourceNameToString(results.AllResources[resourcesResult[i].GetResourceID()]), failedControls) + } + } + testCaseFailure.Message += fmt.Sprintf("%s\n%s", getSummaryMessage(severityCounter), message) + + testCase.Failure = &testCaseFailure + if testCase.Failure.Message != "" { testCases = append(testCases, testCase) } + return testCases } -func resourceToString(resource workloadinterface.IMetadata) 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 +func getSummaryMessage(severityCounter []int) string { + total := 0 + severities := "" + for i, count := range severityCounter { + if apis.SeverityNumberToString(i) == apis.SeverityNumberToString(apis.SeverityUnknown) { + continue + } + severities += fmt.Sprintf("%s: %d, ", apis.SeverityNumberToString(i), count) + total += count } - s += fmt.Sprintf("name: %s", resource.GetName()) - return s + if len(severities) == 0 { + return "" + } + return fmt.Sprintf("Total: %d (%s)", total, severities[:len(severities)-2]) } -func properties(riskScore float32) []JUnitProperty { - return []JUnitProperty{ - { - Name: "riskScore", - Value: fmt.Sprintf("%.2f", riskScore), - }, +func resourceNameToString(resource workloadinterface.IMetadata) string { + s := "" + s += fmt.Sprintf("kind=%s/", resource.GetKind()) + if resource.GetNamespace() != "" { + s += fmt.Sprintf("namespace=%s/", resource.GetNamespace()) } + s += fmt.Sprintf("name=%s", resource.GetName()) + return s } diff --git a/core/pkg/resultshandling/printer/v2/resourcetable.go b/core/pkg/resultshandling/printer/v2/resourcetable.go index 28951ec0..27ffa3d1 100644 --- a/core/pkg/resultshandling/printer/v2/resourcetable.go +++ b/core/pkg/resultshandling/printer/v2/resourcetable.go @@ -74,7 +74,7 @@ func generateResourceRows(controls []resourcesresults.ResourceAssociatedControl, } row[resourceColumnURL] = fmt.Sprintf("https://hub.armosec.io/docs/%s", strings.ToLower(controls[i].GetID())) - row[resourceColumnPath] = strings.Join(failedPathsToString(&controls[i]), "\n") + row[resourceColumnPath] = strings.Join(append(failedPathsToString(&controls[i]), fixPathsToString(&controls[i])...), "\n") row[resourceColumnName] = controls[i].GetName() if c := summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaName, controls[i].GetName()); c != nil { @@ -120,6 +120,16 @@ func failedPathsToString(control *resourcesresults.ResourceAssociatedControl) [] if p := control.ResourceAssociatedRules[j].Paths[k].FailedPath; p != "" { paths = append(paths, p) } + } + } + return paths +} + +func fixPathsToString(control *resourcesresults.ResourceAssociatedControl) []string { + var paths []string + + for j := range control.ResourceAssociatedRules { + for k := range control.ResourceAssociatedRules[j].Paths { if p := control.ResourceAssociatedRules[j].Paths[k].FixPath.Path; p != "" { v := control.ResourceAssociatedRules[j].Paths[k].FixPath.Value paths = append(paths, fmt.Sprintf("%s=%s", p, v)) diff --git a/go.mod b/go.mod index af064f47..e89e12ed 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/johnfercher/maroto v0.37.0 github.com/kubescape/go-logger v0.0.6 github.com/kubescape/k8s-interface v0.0.83 - github.com/kubescape/opa-utils v0.0.183 + github.com/kubescape/opa-utils v0.0.186 github.com/kubescape/rbac-utils v0.0.17 github.com/libgit2/git2go/v33 v33.0.9 github.com/mattn/go-isatty v0.0.14 diff --git a/go.sum b/go.sum index 4308189c..07a20d66 100644 --- a/go.sum +++ b/go.sum @@ -839,8 +839,8 @@ github.com/kubescape/go-logger v0.0.6 h1:ynhAmwrz0O7Jtqq1CdmCZUrKveji25hVP+B/FAb github.com/kubescape/go-logger v0.0.6/go.mod h1:DnVWEvC90LFY1nNMaNo6nBVOcqkLMK3S0qzXP1fzRvI= github.com/kubescape/k8s-interface v0.0.83 h1:yQ1kWNZmKfBim/+NmxpPI/j7L9ASDq2h3mCNdmYgzqY= github.com/kubescape/k8s-interface v0.0.83/go.mod h1:ihX96yqar+xogHl45mFE8zT9DLI06iy7XQPAP+j5KJE= -github.com/kubescape/opa-utils v0.0.183 h1:Qiosb2dV6QIIM8vIbjiIRkxYNiHFbDTvcOVfKa8u5mE= -github.com/kubescape/opa-utils v0.0.183/go.mod h1:jC5QrhS6WiFj/tXP2/YSDBnFlGsYUQokDGbKwMBgMpw= +github.com/kubescape/opa-utils v0.0.186 h1:3T0lD3x1/hweXY/HeNM2I4h8Ugh9SacXPo1AaOPBrAs= +github.com/kubescape/opa-utils v0.0.186/go.mod h1:jC5QrhS6WiFj/tXP2/YSDBnFlGsYUQokDGbKwMBgMpw= github.com/kubescape/rbac-utils v0.0.17 h1:B78kjlTKqjYK/PXwmi4GPysHsFxIwVz1KFb4+IGT29w= github.com/kubescape/rbac-utils v0.0.17/go.mod h1:pBwjpcrVeuH/no+DiCZWvlhYtCDzd3U0o/hEZKi+eM8= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= diff --git a/httphandler/go.mod b/httphandler/go.mod index c283493f..d8411424 100644 --- a/httphandler/go.mod +++ b/httphandler/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/schema v1.2.0 github.com/kubescape/go-logger v0.0.6 github.com/kubescape/kubescape/v2 v2.0.0-00010101000000-000000000000 - github.com/kubescape/opa-utils v0.0.183 + github.com/kubescape/opa-utils v0.0.186 github.com/stretchr/testify v1.8.0 k8s.io/utils v0.0.0-20220706174534-f6158b442e7c ) diff --git a/httphandler/go.sum b/httphandler/go.sum index 49c20842..99bb4f24 100644 --- a/httphandler/go.sum +++ b/httphandler/go.sum @@ -895,8 +895,8 @@ github.com/kubescape/go-logger v0.0.6 h1:ynhAmwrz0O7Jtqq1CdmCZUrKveji25hVP+B/FAb github.com/kubescape/go-logger v0.0.6/go.mod h1:DnVWEvC90LFY1nNMaNo6nBVOcqkLMK3S0qzXP1fzRvI= github.com/kubescape/k8s-interface v0.0.83 h1:yQ1kWNZmKfBim/+NmxpPI/j7L9ASDq2h3mCNdmYgzqY= github.com/kubescape/k8s-interface v0.0.83/go.mod h1:ihX96yqar+xogHl45mFE8zT9DLI06iy7XQPAP+j5KJE= -github.com/kubescape/opa-utils v0.0.183 h1:Qiosb2dV6QIIM8vIbjiIRkxYNiHFbDTvcOVfKa8u5mE= -github.com/kubescape/opa-utils v0.0.183/go.mod h1:jC5QrhS6WiFj/tXP2/YSDBnFlGsYUQokDGbKwMBgMpw= +github.com/kubescape/opa-utils v0.0.186 h1:3T0lD3x1/hweXY/HeNM2I4h8Ugh9SacXPo1AaOPBrAs= +github.com/kubescape/opa-utils v0.0.186/go.mod h1:jC5QrhS6WiFj/tXP2/YSDBnFlGsYUQokDGbKwMBgMpw= github.com/kubescape/rbac-utils v0.0.17 h1:B78kjlTKqjYK/PXwmi4GPysHsFxIwVz1KFb4+IGT29w= github.com/kubescape/rbac-utils v0.0.17/go.mod h1:pBwjpcrVeuH/no+DiCZWvlhYtCDzd3U0o/hEZKi+eM8= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=