From cda9bb0e45de9b8e859762900453ee2564cac212 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 3 Mar 2022 12:34:03 +0200 Subject: [PATCH 1/2] Update junit support --- README.md | 23 -- clihandler/initcliutils.go | 7 +- examples/exceptions/README.md | 1 + resultshandling/printer/printresults.go | 2 +- resultshandling/printer/v2/junit.go | 256 +++++++----------- resultshandling/reporter/v1/mockreporter.go | 30 -- resultshandling/reporter/v2/mockreporter.go | 44 +++ .../reporter/v2/reporteventreceiver.go | 38 --- 8 files changed, 145 insertions(+), 256 deletions(-) delete mode 100644 resultshandling/reporter/v1/mockreporter.go create mode 100644 resultshandling/reporter/v2/mockreporter.go diff --git a/README.md b/README.md index ef679e9d..0d194e32 100644 --- a/README.md +++ b/README.md @@ -96,29 +96,6 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser brew install kubescape ``` -## Flags - -| flag | default | description | options | -|-----------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------| -| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | | -| `--include-namespaces` | Scan all namespaces | Scan specific namespaces | | -| `-s`/`--silent` | Display progress messages | Silent progress messages | | -| `-t`/`--fail-threshold` | `100` (do not fail) | fail command (return exit code 1) if result is above threshold | `0` -> `100` | -| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus`/`pdf` | -| `-o`/`--output` | print to stdout | Save scan result in file | | -| `--use-from` | | Load local framework object from specified path. If not used will download latest || -| `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | | -| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | -| `--exceptions` | | Path to an exceptions obj, [examples](https://github.com/armosec/kubescape/tree/master/examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || -| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal. [docs](https://hub.armo.cloud/docs/configuration-parameters) | | -| `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` | -| `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` | -| `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | | -| `--kube-context` | current-context | Cluster context to scan | | -| `--verbose` | `false` | Display all of the input resources and not only failed resources | `true`/`false` | -| `--logger` | `info` | Set the logger level | `debug`/`info`/`success`/`warning`/`error`/`fatal` | - - ## Usage & Examples ### Examples diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index a466690d..cbb68eaf 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -12,7 +12,6 @@ import ( "github.com/armosec/kubescape/hostsensorutils" "github.com/armosec/kubescape/resourcehandler" "github.com/armosec/kubescape/resultshandling/reporter" - reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" reporterv2 "github.com/armosec/kubescape/resultshandling/reporter/v2" "github.com/armosec/opa-utils/reporthandling" @@ -51,10 +50,12 @@ func getRBACHandler(tenantConfig cautils.ITenantConfig, k8s *k8sinterface.Kubern func getReporter(tenantConfig cautils.ITenantConfig, submit bool) reporter.IReport { if submit { - // return reporterv1.NewReportEventReceiver(tenantConfig.GetConfigObj()) return reporterv2.NewReportEventReceiver(tenantConfig.GetConfigObj()) } - return reporterv1.NewReportMock() + if tenantConfig.GetAccountID() == "" { + return reporterv2.NewReportMock(reporterv2.NO_SUBMIT_QUERY) + } + return reporterv2.NewReportMock("") } func getResourceHandler(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, hostSensorHandler hostsensorutils.IHostSensor, registryAdaptors *resourcehandler.RegistryAdaptors) resourcehandler.IResourceHandler { diff --git a/examples/exceptions/README.md b/examples/exceptions/README.md index 6cad5627..2edb3d29 100644 --- a/examples/exceptions/README.md +++ b/examples/exceptions/README.md @@ -24,6 +24,7 @@ e.g. When a `kube-system` resource fails and it is ok, simply add the resource t * `controlID` - Control ID can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported) * `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules) (regex supported) +You can find [here](https://github.com/armosec/kubescape/tree/master/examples/exceptions) some examples of exceptions files ## Usage diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go index 30aaf302..5f4b948a 100644 --- a/resultshandling/printer/printresults.go +++ b/resultshandling/printer/printresults.go @@ -30,7 +30,7 @@ func GetWriter(outputFile string) *os.File { f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { logger.L().Error(fmt.Sprintf("failed to open file for writing, reason: %s", err.Error())) - return os.Stderr + return os.Stdout } return f } diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 7b06e503..4069c597 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -4,12 +4,16 @@ import ( "encoding/xml" "fmt" "os" + "sort" "strings" + "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/armosec/opa-utils/shared" ) /* @@ -104,178 +108,108 @@ func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASession } func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - junitResult, err := junitPrinter.convertPostureReportToJunitResult(opaSessionObj) - if err != nil { - logger.L().Fatal("failed to build xml result object", helpers.Error(err)) - } + junitResult := testsSuites(opaSessionObj) postureReportStr, err := xml.Marshal(junitResult) if err != nil { logger.L().Fatal("failed to Marshal xml result object", helpers.Error(err)) } + + logOUtputFile(junitPrinter.writer.Name()) + junitPrinter.writer.Write(postureReportStr) } -func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { - - // // Frameworks - // for _, frameworksReports := range results.Report.ListFrameworks().All() { - // fw := JUnitFrameworks{} - // fw.Name = frameworksReports.GetName() - // fw.RiskScore = frameworksReports.GetScore() - // fw.Status = string(frameworksReports.GetStatus().Status()) - // juResult.Frameworks = append(juResult.Frameworks, fw) - // } - testSuites := JUnitTestSuites{ - XMLName: xml.Name{ - Local: "Kubescape scan results", - }, +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(), } - testSuites.Failures = results.Report.SummaryDetails.NumberOfResources().Failed() - testSuites.Tests = results.Report.SummaryDetails.NumberOfResources().All() - testSuites.Disabled = results.Report.SummaryDetails.NumberOfResources().Skipped() - // summary.errors = - // summary.Name = "?" +} +func listTestsSuite(results *cautils.OPASessionObj) []JUnitTestSuite { + var testSuites []JUnitTestSuite - // resources - counter := 0 - for resourceID, resourceResult := range results.ResourcesResult { - counter++ - - // resource data - testSuite := JUnitTestSuite{ - XMLName: xml.Name{ - Local: resourceID, - }, - } - testSuite.Name = resourceID - testSuite.Disabled = 0 - testSuite.Errors = 0 - testSuite.Failures = len(resourceResult.ListControlsIDs(nil).Failed()) - testSuite.Hostname = "" - testSuite.ID = counter - // testSuite.Skipped = "" - testSuite.Time = "" - testSuite.Timestamp = results.PostureReport.ReportGenerationTime.String() - - testSuite.Properties = []JUnitProperty{ - { - Name: "ID", - Value: resourceID, - }, - } - - // controls - for _, control := range resourceResult.ListControls() { - testCase := JUnitTestCase{ - XMLName: xml.Name{ - Local: control.GetName(), - Space: getControlURL(control.GetID()), - }, - } - testCase.Name = control.GetName() - testCase.Classname = control.GetID() - testCase.Status = string(control.GetStatus(nil).Status()) - - if control.GetStatus(nil).IsFailed() { - paths := failedPathsToString(&control) - - testCaseFailure := JUnitFailure{} - testCaseFailure.Contents = fmt.Sprintf("More deatiles: %s", getControlURL(control.GetID())) - testCaseFailure.Message = strings.Join(paths, ";") - testCaseFailure.Type = "" // TODO - suppot add/modify - - testCase.Failure = &testCaseFailure - } - - testSuite.TestCases = append(testSuite.TestCases, testCase) - } - - testSuites.Suites = append(testSuites.Suites, testSuite) + // control scan + if len(results.Report.SummaryDetails.ListFrameworks().All()) == 0 { + 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 } - return &testSuites, nil + 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.ListControls(), f.GetName()) + testSuites = append(testSuites, testSuite) + } + + return testSuites +} +func testsCases(results *cautils.OPASessionObj, controls reportsummary.IControlsSummaries, classname string) []JUnitTestCase { + var testCases []JUnitTestCase + + for _, cID := range controls.ListControlsIDs().All() { + 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 + } + 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(), getControlURL(control.GetID()), strings.Join(resourcesStr, "\n")) + + testCase.Failure = &testCaseFailure + } else if control.GetStatus().IsSkipped() { + testCase.SkipMessage = &JUnitSkipMessage{ + Message: "", // TODO - fill after statusInfo is supportred + } + + } + testCases = append(testCases, testCase) + } + return testCases } -// func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { +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 + } + s += fmt.Sprintf("name: %s", resource.GetName()) + return s +} -// // // Frameworks -// // for _, frameworksReports := range results.Report.ListFrameworks().All() { -// // fw := JUnitFrameworks{} -// // fw.Name = frameworksReports.GetName() -// // fw.RiskScore = frameworksReports.GetScore() -// // fw.Status = string(frameworksReports.GetStatus().Status()) -// // juResult.Frameworks = append(juResult.Frameworks, fw) -// // } -// testSuites := JUnitTestSuites{ -// XMLName: xml.Name{ -// Local: "Kubescape scan results", -// }, -// } -// testSuites.Failures = results.Report.SummaryDetails.NumberOfControls().Failed() -// testSuites.Tests = results.Report.SummaryDetails.NumberOfControls().All() -// testSuites.Disabled = results.Report.SummaryDetails.NumberOfControls().Skipped() -// // summary.errors = -// // summary.Name = "?" - -// // controls -// for _, controlIDs := range results.Report.ListControlsIDs().All() { -// controlReport := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controlIDs) - -// // control data -// testSuite := JUnitTestSuite{ -// XMLName: xml.Name{ -// Local: controlReport.GetName(), -// Space: getControlURL(controlReport.GetID()), -// }, -// } -// testSuite.Name = controlReport.GetName() -// testSuite.Disabled = 0 -// testSuite.Errors = 0 -// testSuite.Failures = controlReport.NumberOfResources().Failed() -// testSuite.Hostname = "" -// testSuite.ID = 0 -// // testSuite.Skipped = "" -// testSuite.Time = "" -// testSuite.Timestamp = "" - -// testCase.Classname = controlReport.GetID() -// testCase.Url = getControlURL(controlReport.GetID()) -// testCase.Name = controlReport.GetName() -// testCase.Status = string(controlReport.GetStatus().Status()) - -// // resources counters -// testCase.AllResources = controlReport.NumberOfResources().All() -// testCase.Excluded = controlReport.NumberOfResources().Excluded() -// testCase.Failed = controlReport.NumberOfResources().Failed() - -// // resources -// var jUnitResources []JUnitResource -// for _, resourceID := range controlReport.ListResourcesIDs().All() { -// result, ok := results.ResourcesResult[resourceID] -// if !ok { -// continue -// } -// if result.GetStatus(nil).IsPassed() && !junitPrinter.verbose { // add passed resources only in verbose mode -// continue -// } - -// jUnitResource := JUnitResource{} -// rules := result.ListRulesOfControl(controlReport.GetID(), "") -// for _, rule := range rules { -// jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) -// } -// if resource, ok := results.AllResources[resourceID]; ok { -// jUnitResource.Name = resource.GetName() -// jUnitResource.Namespace = resource.GetNamespace() -// jUnitResource.Kind = resource.GetKind() -// jUnitResource.ApiVersion = resource.GetApiVersion() -// } - -// jUnitResources = append(jUnitResources, jUnitResource) -// } -// testCase.Resources = jUnitResources -// juResult.Suites = append(juResult.Suites, testCase) -// } - -// return &juResult, nil -// } +func properties(riskScore float32) []JUnitProperty { + return []JUnitProperty{ + { + Name: "riskScore", + Value: fmt.Sprintf("%.2f", riskScore), + }, + } +} diff --git a/resultshandling/reporter/v1/mockreporter.go b/resultshandling/reporter/v1/mockreporter.go deleted file mode 100644 index 8ffc01fe..00000000 --- a/resultshandling/reporter/v1/mockreporter.go +++ /dev/null @@ -1,30 +0,0 @@ -package v1 - -import ( - "fmt" - "os" - - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/getter" -) - -type ReportMock struct { -} - -func NewReportMock() *ReportMock { - return &ReportMock{} -} -func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error { - return nil -} - -func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) { -} - -func (reportMock *ReportMock) SetClusterName(clusterName string) { -} - -func (reportMock *ReportMock) DisplayReportURL() { - message := fmt.Sprintf("\nScan results have not been submitted.\nYou can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here: https://%s/cli-signup \n", getter.GetArmoAPIConnector().GetFrontendURL()) - cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n%s\n", message)) -} diff --git a/resultshandling/reporter/v2/mockreporter.go b/resultshandling/reporter/v2/mockreporter.go new file mode 100644 index 00000000..7837f3fd --- /dev/null +++ b/resultshandling/reporter/v2/mockreporter.go @@ -0,0 +1,44 @@ +package v2 + +import ( + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/getter" +) + +const NO_SUBMIT_QUERY = "utm_source=GitHub&utm_medium=CLI&utm_campaign=no_submit" + +type ReportMock struct { + query string +} + +func NewReportMock(query string) *ReportMock { + return &ReportMock{ + query: query, + } +} +func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error { + return nil +} + +func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) { +} + +func (reportMock *ReportMock) SetClusterName(clusterName string) { +} + +func (reportMock *ReportMock) DisplayReportURL() { + u := fmt.Sprintf("https://%s/account/login", getter.GetArmoAPIConnector().GetFrontendURL()) + if reportMock.query != "" { + u += fmt.Sprintf("?%s", reportMock.query) + } + sep := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + message := sep + "\n" + message += "Scan results have not been submitted." + "\n" + message += "Sign up for free: " + message += u + "\n" + message += sep + "\n" + cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n%s\n", message)) +} diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index 5d50403e..421cf6a5 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -87,12 +87,6 @@ func (report *ReportEventReceiver) prepareReport(postureReport *reporthandlingv2 if err := report.sendResources(host, postureReport, &reportCounter, false); err != nil { return err } - // reportCounter++ - - // // send results - // if err := report.sendResults(host, postureReport, &reportCounter, true); err != nil { - // return err - // } return nil } @@ -156,38 +150,6 @@ func (report *ReportEventReceiver) sendResources(host string, postureReport *rep return report.sendReport(host, splittedPostureReport, *reportCounter, true) } -// func (report *ReportEventReceiver) sendResults(host string, postureReport *reporthandlingv2.PostureReport, reportCounter *int, isLastReport bool) error { -// splittedPostureReport := setSubReport(postureReport) -// counter := 0 - -// for _, v := range postureReport.Results { -// r, err := json.Marshal(v) -// if err != nil { -// return fmt.Errorf("failed to unmarshal resource '%s', reason: %v", v.GetResourceID(), err) -// } - -// if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Resources) > 0 { - -// // send report -// if err := report.sendReport(host, splittedPostureReport, *reportCounter, false); err != nil { -// return err -// } -// *reportCounter++ - -// // delete results -// splittedPostureReport.Results = []resourcesresults.Result{} - -// // restart counter -// counter = 0 -// } - -// counter += len(r) -// splittedPostureReport.Results = append(splittedPostureReport.Results, v) -// } - -// return report.sendReport(host, splittedPostureReport, *reportCounter, isLastReport) -// } - func (report *ReportEventReceiver) sendReport(host string, postureReport *reporthandlingv2.PostureReport, counter int, isLastReport bool) error { postureReport.PaginationInfo = reporthandlingv2.PaginationMarks{ ReportNumber: counter, From ec30ed8439a52d1002ef36fbb8f11e3c8ce5a20f Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 3 Mar 2022 13:05:36 +0200 Subject: [PATCH 2/2] json v2 support --- README.md | 2 +- cautils/scaninfo.go | 2 +- clihandler/cmd/scan.go | 4 +-- clihandler/initcli.go | 2 +- resultshandling/printer/v2/jsonprinter.go | 36 +++++++++++++++++++++ resultshandling/printer/v2/junit.go | 4 --- resultshandling/printer/v2/prettyprinter.go | 17 +++++----- resultshandling/printer/v2/resourcetable.go | 4 +-- resultshandling/printer/v2/utils.go | 23 ++++++++----- resultshandling/results.go | 11 +++++-- 10 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 resultshandling/printer/v2/jsonprinter.go diff --git a/README.md b/README.md index 0d194e32..07c4d229 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ kubescape scan --verbose #### Output in `json` format ``` -kubescape scan --format json --output results.json +kubescape scan --format json --format-version v2 --output results.json ``` #### Output in `junit xml` format diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 7f21ff3d..f8b7e7f7 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -68,7 +68,7 @@ type ScanInfo struct { VerboseMode bool // Display all of the input resources and not only failed resources Format string // Format results (table, json, junit ...) Output string // Store results in an output file, Output file name - OutputVersion string // Output object can be differnet between versions, this is for testing and backward compatibility + FormatVersion string // Output object can be differnet between versions, this is for testing and backward compatibility ExcludedNamespaces string // used for host sensor namespace IncludeNamespaces string // DEPRECATED? InputPatterns []string // Yaml files input patterns diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 1835321d..d2aabd74 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -79,12 +79,12 @@ func init() { scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") - scanCmd.PersistentFlags().StringVar(&scanInfo.OutputVersion, "output-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") + scanCmd.PersistentFlags().StringVar(&scanInfo.FormatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") // hidden flags scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") // this flag should be used very cautiously. We prefer users will not use it at all unless the DaemoSet can not run pods on the nodes scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support - scanCmd.PersistentFlags().MarkHidden("output-version") // meant for testing different output approaches and not for common use + scanCmd.PersistentFlags().MarkHidden("format-version") // meant for testing different output approaches and not for common use hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") hostF.NoOptDefVal = "true" diff --git a/clihandler/initcli.go b/clihandler/initcli.go index 0a96a51f..def16384 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -83,7 +83,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler := getReporter(tenantConfig, scanInfo.Submit) // setup printer - printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.OutputVersion, scanInfo.VerboseMode) + printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.FormatVersion, scanInfo.VerboseMode) printerHandler.SetWriter(scanInfo.Output) // ================== return interface ====================================== diff --git a/resultshandling/printer/v2/jsonprinter.go b/resultshandling/printer/v2/jsonprinter.go new file mode 100644 index 00000000..d9b1235c --- /dev/null +++ b/resultshandling/printer/v2/jsonprinter.go @@ -0,0 +1,36 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/resultshandling/printer" +) + +type JsonPrinter struct { + writer *os.File +} + +func NewJsonPrinter() *JsonPrinter { + return &JsonPrinter{} +} + +func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) { + jsonPrinter.writer = printer.GetWriter(outputFile) +} + +func (jsonPrinter *JsonPrinter) Score(score float32) { + fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) +} + +func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + finalizeJson(opaSessionObj) + r, err := json.Marshal(opaSessionObj.Report) + if err != nil { + logger.L().Fatal("failed to Marshal posture report object") + } + jsonPrinter.writer.Write(r) +} diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 4069c597..503b7eaf 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -103,10 +103,6 @@ func (junitPrinter *JunitPrinter) Score(score float32) { fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) } -func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) { - finalizeReport(opaSessionObj) -} - func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { junitResult := testsSuites(opaSessionObj) postureReportStr, err := xml.Marshal(junitResult) diff --git a/resultshandling/printer/v2/prettyprinter.go b/resultshandling/printer/v2/prettyprinter.go index 75e7053b..314149f5 100644 --- a/resultshandling/printer/v2/prettyprinter.go +++ b/resultshandling/printer/v2/prettyprinter.go @@ -17,24 +17,27 @@ import ( ) type PrettyPrinter struct { - outputVersion string + formatVersion string writer *os.File verboseMode bool sortedControlNames []string } -func NewPrettyPrinter(verboseMode bool, outputVersion string) *PrettyPrinter { +func NewPrettyPrinter(verboseMode bool, formatVersion string) *PrettyPrinter { return &PrettyPrinter{ verboseMode: verboseMode, - outputVersion: outputVersion, + formatVersion: formatVersion, } } func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All()) - prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) - prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) + if prettyPrinter.formatVersion == "v1" { + prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) + } else if prettyPrinter.formatVersion == "v2" { + prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) + } prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails) } @@ -47,10 +50,6 @@ func (prettyPrinter *PrettyPrinter) Score(score float32) { } func (prettyPrinter *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries, allResources map[string]workloadinterface.IMetadata) { - if prettyPrinter.outputVersion != "v1" { - return - } - for i := 0; i < len(prettyPrinter.sortedControlNames); i++ { controlSummary := controls.GetControl(reportsummary.EControlCriteriaName, prettyPrinter.sortedControlNames[i]) // summaryDetails.Controls ListControls().All() Controls.GetControl(ca) diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go index 1689ed3d..b1555fc1 100644 --- a/resultshandling/printer/v2/resourcetable.go +++ b/resultshandling/printer/v2/resourcetable.go @@ -11,9 +11,7 @@ import ( ) func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { - if prettyPrinter.outputVersion != "v2" { - return - } + summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(true) summaryTable.SetAutoMergeCells(true) diff --git a/resultshandling/printer/v2/utils.go b/resultshandling/printer/v2/utils.go index c432bcac..31c4372b 100644 --- a/resultshandling/printer/v2/utils.go +++ b/resultshandling/printer/v2/utils.go @@ -3,22 +3,22 @@ package v2 import ( "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/opa-utils/reporthandling" "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" ) // finalizeV2Report finalize the results objects by copying data from map to lists -func finalizeReport(opaSessionObj *cautils.OPASessionObj) { +func finalizeJson(opaSessionObj *cautils.OPASessionObj) { if len(opaSessionObj.Report.Results) == 0 { opaSessionObj.Report.Results = make([]resourcesresults.Result, len(opaSessionObj.ResourcesResult)) finalizeResults(opaSessionObj.Report.Results, opaSessionObj.ResourcesResult) - opaSessionObj.ResourcesResult = nil } if len(opaSessionObj.Report.Resources) == 0 { opaSessionObj.Report.Resources = make([]reporthandling.Resource, len(opaSessionObj.AllResources)) - finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.AllResources) - opaSessionObj.AllResources = nil + finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.Report.Results, opaSessionObj.AllResources) } } @@ -30,15 +30,22 @@ func finalizeResults(results []resourcesresults.Result, resourcesResult map[stri } } -func finalizeResources(resources []reporthandling.Resource, allResources map[string]workloadinterface.IMetadata) { +func finalizeResources(resources []reporthandling.Resource, results []resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { index := 0 - for resourceID := range allResources { - if obj, ok := allResources[resourceID]; ok { + for i := range results { + if obj, ok := allResources[results[i].ResourceID]; ok { r := *reporthandling.NewResource(obj.GetObject()) - r.ResourceID = resourceID + r.ResourceID = results[i].ResourceID resources[index] = r } index++ } } + +func logOUtputFile(fileName string) { + if fileName != "/dev/stdout" && fileName != "/dev/stderr" { + logger.L().Success("Scan results saved", helpers.String("filename", fileName)) + } + +} diff --git a/resultshandling/results.go b/resultshandling/results.go index e5ec8ec5..b74f04d9 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -53,11 +53,16 @@ func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 return (float32(len(allResources)) - float32(len(failedResources))) / float32(len(allResources)) } -func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPrinter { +func NewPrinter(printFormat, formatVersion string, verboseMode bool) printer.IPrinter { switch printFormat { case printer.JsonFormat: - return printerv1.NewJsonPrinter() + switch formatVersion { + case "v2": + return printerv2.NewJsonPrinter() + default: + return printerv1.NewJsonPrinter() + } case printer.JunitResultFormat: return printerv2.NewJunitPrinter(verboseMode) case printer.PrometheusFormat: @@ -65,6 +70,6 @@ func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPr case printer.PdfFormat: return printerv2.NewPdfPrinter() default: - return printerv2.NewPrettyPrinter(verboseMode, outputVersion) + return printerv2.NewPrettyPrinter(verboseMode, formatVersion) } }