diff --git a/internal/commands/cx_result_sonar.json b/internal/commands/cx_result_sonar.json deleted file mode 100644 index d4cf0d4a1..000000000 --- a/internal/commands/cx_result_sonar.json +++ /dev/null @@ -1 +0,0 @@ -{"issues":[{"engineId":"sast","type":"VULNERABILITY","primaryLocation":{"filePath":"dummy-file-name","textRange":{"startLine":10,"startColumn":9,"endColumn":10}},"secondaryLocations":[{"filePath":"dummy-file-name","textRange":{"startColumn":2,"endColumn":3}}]},{"engineId":"kics","type":"VULNERABILITY","primaryLocation":{"textRange":{"startColumn":1,"endColumn":2}},"secondaryLocations":null}]} diff --git a/internal/commands/result.go b/internal/commands/result.go index ced20f065..7264416b2 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -52,6 +52,11 @@ const ( lowCx = "LOW" mediumCx = "MEDIUM" highCx = "HIGH" + tableResultsFormat = " | %-10s %4d %6d %4d %4d %-9s |\n" + stringTableResultsFormat = " | %-10s %4s %6s %4s %4s %5s |\n" + TableTitleFormat = " | %-11s %4s %6s %4s %4s %6s |\n" + twoNewLines = "\n\n" + tableLine = " --------------------------------------------------------- " codeBashingKey = "cb-url" failedGettingBfl = "Failed getting BFL" notAvailableString = "-" @@ -138,6 +143,10 @@ var sonarSeverities = map[string]string{ highCx: highSonar, } +var containerSupportedAgents = []string{ + commonParams.DefaultAgent, +} + func NewResultsCommand( resultsWrapper wrappers.ResultsWrapper, scanWrapper wrappers.ScansWrapper, @@ -356,12 +365,18 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr sastIssues := 0 scaIssues := 0 kicsIssues := 0 + var containersIssues *int enginesStatusCode := map[string]int{ commonParams.SastType: 0, commonParams.ScaType: 0, commonParams.KicsType: 0, commonParams.APISecType: 0, } + if wrappers.IsContainersEnabled { + containersIssues = new(int) + *containersIssues = 0 + enginesStatusCode[commonParams.ContainersType] = 0 + } if len(scanInfo.StatusDetails) > 0 { for _, statusDetailItem := range scanInfo.StatusDetails { @@ -372,6 +387,8 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr scaIssues = notAvailableNumber } else if statusDetailItem.Name == commonParams.KicsType { kicsIssues = notAvailableNumber + } else if statusDetailItem.Name == commonParams.ContainersType && wrappers.IsContainersEnabled { + *containersIssues = notAvailableNumber } } switch statusDetailItem.Status { @@ -383,23 +400,24 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr } } summary := &wrappers.ResultSummary{ - ScanID: scanInfo.ID, - Status: string(scanInfo.Status), - CreatedAt: scanInfo.CreatedAt.Format("2006-01-02, 15:04:05"), - ProjectID: scanInfo.ProjectID, - RiskStyle: "", - RiskMsg: "", - HighIssues: 0, - MediumIssues: 0, - LowIssues: 0, - InfoIssues: 0, - SastIssues: sastIssues, - KicsIssues: kicsIssues, - ScaIssues: scaIssues, - Tags: scanInfo.Tags, - ProjectName: scanInfo.ProjectName, - BranchName: scanInfo.Branch, - EnginesEnabled: scanInfo.Engines, + ScanID: scanInfo.ID, + Status: string(scanInfo.Status), + CreatedAt: scanInfo.CreatedAt.Format("2006-01-02, 15:04:05"), + ProjectID: scanInfo.ProjectID, + RiskStyle: "", + RiskMsg: "", + HighIssues: 0, + MediumIssues: 0, + LowIssues: 0, + InfoIssues: 0, + SastIssues: sastIssues, + KicsIssues: kicsIssues, + ScaIssues: scaIssues, + ContainersIssues: containersIssues, + Tags: scanInfo.Tags, + ProjectName: scanInfo.ProjectName, + BranchName: scanInfo.Branch, + EnginesEnabled: scanInfo.Engines, EnginesResult: map[string]*wrappers.EngineResultSummary{ commonParams.SastType: {StatusCode: enginesStatusCode[commonParams.SastType]}, commonParams.ScaType: {StatusCode: enginesStatusCode[commonParams.ScaType]}, @@ -407,7 +425,9 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr commonParams.APISecType: {StatusCode: enginesStatusCode[commonParams.APISecType]}, }, } - + if wrappers.IsContainersEnabled { + summary.EnginesResult[commonParams.ContainersType] = &wrappers.EngineResultSummary{StatusCode: enginesStatusCode[commonParams.ContainersType]} + } baseURI, err := resultsWrapper.GetResultsURL(summary.ProjectID) if err != nil { return nil, err @@ -451,6 +471,9 @@ func summaryReport( setNotAvailableNumberIfZero(summary, &summary.SastIssues, commonParams.SastType) setNotAvailableNumberIfZero(summary, &summary.ScaIssues, commonParams.ScaType) setNotAvailableNumberIfZero(summary, &summary.KicsIssues, commonParams.KicsType) + if wrappers.IsContainersEnabled { + setNotAvailableNumberIfZero(summary, summary.ContainersIssues, commonParams.ContainersType) + } setRiskMsgAndStyle(summary) setNotAvailableEnginesStatusCode(summary) @@ -494,6 +517,9 @@ func enhanceWithScanSummary(summary *wrappers.ResultSummary, results *wrappers.S summary.EnginesResult[commonParams.APISecType].High = summary.APISecurity.Risks[1] } summary.TotalIssues = summary.SastIssues + summary.ScaIssues + summary.KicsIssues + summary.GetAPISecurityDocumentationTotal() + if wrappers.IsContainersEnabled { + summary.TotalIssues += *summary.ContainersIssues + } } func writeHTMLSummary(targetFile string, summary *wrappers.ResultSummary) error { @@ -559,7 +585,7 @@ func writeConsoleSummary(summary *wrappers.ResultSummary) error { } func printPoliciesSummary(summary *wrappers.ResultSummary) { - fmt.Printf(" -------------------------------------- \n") + fmt.Printf(tableLine + "\n") if summary.Policies.BreakBuild { fmt.Printf(" Policy Management Violation - Break Build Enabled: \n") } else { @@ -585,22 +611,19 @@ func printAPIsSecuritySummary(summary *wrappers.ResultSummary) { if summary.HasAPISecurityDocumentation() { fmt.Printf(" APIS DOCUMENTATION: %*d \n", defaultPaddingSize, summary.GetAPISecurityDocumentationTotal()) } - fmt.Printf(" -------------------------------------------------- \n\n") + fmt.Printf(tableLine + twoNewLines) } func printTableRow(title string, counts *wrappers.EngineResultSummary, statusNumber int) { - formatString := " | %-4s %4d %6d %4d %4d %-9s |\n" - notAvailableFormatString := " | %-4s %4s %6s %4s %4s %5s |\n" - switch statusNumber { case notAvailableNumber: - fmt.Printf(notAvailableFormatString, title, notAvailableString, notAvailableString, notAvailableString, notAvailableString, notAvailableString) + fmt.Printf(stringTableResultsFormat, title, notAvailableString, notAvailableString, notAvailableString, notAvailableString, notAvailableString) case scanFailedNumber: - fmt.Printf(formatString, title, counts.High, counts.Medium, counts.Low, counts.Info, scanFailedString) + fmt.Printf(tableResultsFormat, title, counts.High, counts.Medium, counts.Low, counts.Info, scanFailedString) case scanCanceledNumber: - fmt.Printf(formatString, title, counts.High, counts.Medium, counts.Low, counts.Info, scanCanceledString) + fmt.Printf(tableResultsFormat, title, counts.High, counts.Medium, counts.Low, counts.Info, scanCanceledString) default: - fmt.Printf(formatString, title, counts.High, counts.Medium, counts.Low, counts.Info, scanSuccessString) + fmt.Printf(tableResultsFormat, title, counts.High, counts.Medium, counts.Low, counts.Info, scanSuccessString) } } @@ -609,20 +632,23 @@ func printResultsSummaryTable(summary *wrappers.ResultSummary) { totalMediumIssues := summary.EnginesResult.GetMediumIssues() totalLowIssues := summary.EnginesResult.GetLowIssues() totalInfoIssues := summary.EnginesResult.GetInfoIssues() - fmt.Printf(" --------------------------------------------------- \n\n") + fmt.Printf(tableLine + twoNewLines) fmt.Printf(" Total Results: %d \n", summary.TotalIssues) - fmt.Println(" --------------------------------------------------- ") - fmt.Println(" | High Medium Low Info Status |") + fmt.Println(tableLine) + fmt.Printf(TableTitleFormat, " ", "High", "Medium", "Low", "Info", "Status") printTableRow("APIs", summary.EnginesResult[commonParams.APISecType], summary.EnginesResult[commonParams.APISecType].StatusCode) printTableRow("IAC", summary.EnginesResult[commonParams.KicsType], summary.EnginesResult[commonParams.KicsType].StatusCode) printTableRow("SAST", summary.EnginesResult[commonParams.SastType], summary.EnginesResult[commonParams.SastType].StatusCode) printTableRow("SCA", summary.EnginesResult[commonParams.ScaType], summary.EnginesResult[commonParams.ScaType].StatusCode) + if wrappers.IsContainersEnabled { + printTableRow("CONTAINERS", summary.EnginesResult[commonParams.ContainersType], summary.EnginesResult[commonParams.ContainersType].StatusCode) + } - fmt.Println(" --------------------------------------------------- ") - fmt.Printf(" | %-4s %4d %6d %4d %4d %-9s |\n", - fmt.Sprintf(boldFormat, "TOTAL"), totalHighIssues, totalMediumIssues, totalLowIssues, totalInfoIssues, summary.Status) - fmt.Printf(" --------------------------------------------------- \n\n") + fmt.Println(tableLine) + fmt.Printf(tableResultsFormat, + "TOTAL", totalHighIssues, totalMediumIssues, totalLowIssues, totalInfoIssues, summary.Status) + fmt.Printf(tableLine + twoNewLines) } func generateScanSummaryURL(summary *wrappers.ResultSummary) string { @@ -651,6 +677,7 @@ func runGetResultCommand( useSCALocalFlow, _ := cmd.Flags().GetBool(commonParams.ReportSbomFormatLocalFlowFlag) retrySBOM, _ := cmd.Flags().GetInt(commonParams.RetrySBOMFlag) sastRedundancy, _ := cmd.Flags().GetBool(commonParams.SastRedundancyFlag) + agent, _ := cmd.Flags().GetString(commonParams.AgentFlag) scanID, _ := cmd.Flags().GetString(commonParams.ScanIDFlag) if scanID == "" { @@ -683,7 +710,6 @@ func runGetResultCommand( } else { logger.PrintIfVerbose("Skipping policy evaluation") } - if sastRedundancy { params[commonParams.SastRedundancyFlag] = "" } @@ -703,6 +729,7 @@ func runGetResultCommand( formatSbomOptions, targetFile, targetPath, + agent, params) } } @@ -746,7 +773,10 @@ func runGetCodeBashingCommand( return nil } } - +func setIsContainersEnabled(agent string) { + agentSupported := contains(containerSupportedAgents, agent) + wrappers.IsContainersEnabled = wrappers.FeatureFlags[wrappers.ContainerEngineCLIEnabled] && agentSupported +} func CreateScanReport( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, @@ -762,11 +792,12 @@ func CreateScanReport( formatSbomOptions, targetFile, targetPath string, + agent string, params map[string]string, ) error { reportList := strings.Split(reportTypes, ",") results := &wrappers.ScanResultsCollection{} - + setIsContainersEnabled(agent) summary, err := convertScanToResultsSummary(scan, resultsWrapper) if err != nil { return err @@ -814,14 +845,22 @@ func countResult(summary *wrappers.ResultSummary, result *wrappers.ScanResult) { } else if engineType == commonParams.KicsType { summary.KicsIssues++ summary.TotalIssues++ + } else if engineType == commonParams.ContainersType { + if wrappers.IsContainersEnabled { + *summary.ContainersIssues++ + summary.TotalIssues++ + } else { + return + } } - if severity == highLabel { + switch severity { + case highLabel: summary.HighIssues++ - } else if severity == lowLabel { - summary.LowIssues++ - } else if severity == mediumLabel { + case mediumLabel: summary.MediumIssues++ - } else if severity == infoLabel { + case lowLabel: + summary.LowIssues++ + case infoLabel: summary.InfoIssues++ } summary.UpdateEngineResultSummary(engineType, severity) @@ -1039,9 +1078,24 @@ func enrichScaResults( // Compute SAST results redundancy resultsModel = ComputeRedundantSastResults(resultsModel) } + if util.Contains(scan.Engines, commonParams.ContainersType) && !wrappers.IsContainersEnabled { + resultsModel = removeContainerResults(resultsModel) + } return resultsModel, nil } +func removeContainerResults(model *wrappers.ScanResultsCollection) *wrappers.ScanResultsCollection { + var newResults []*wrappers.ScanResult + for _, result := range model.Results { + if result.Type != commonParams.ContainersType { + newResults = append(newResults, result) + } + } + model.Results = newResults + model.TotalCount = uint(len(newResults)) + return model +} + func exportSarifResults(targetFile string, results *wrappers.ScanResultsCollection) error { var err error var resultsJSON []byte @@ -1446,12 +1500,28 @@ func parseResultsSonar(results *wrappers.ScanResultsCollection) []wrappers.Sonar } else if engineType == commonParams.ScaType { sonarIssuesByLocation := parseScaSonarLocations(result) sonarIssues = append(sonarIssues, sonarIssuesByLocation...) + } else if wrappers.IsContainersEnabled && engineType == commonParams.ContainersType { + auxIssue.PrimaryLocation = parseContainersSonar(result) + sonarIssues = append(sonarIssues, auxIssue) } } } return sonarIssues } +func parseContainersSonar(result *wrappers.ScanResult) wrappers.SonarLocation { + var auxLocation wrappers.SonarLocation + auxLocation.FilePath = result.ScanResultData.ImageFilePath + auxLocation.Message = result.Description + var textRange wrappers.SonarTextRange + textRange.StartColumn = 1 + textRange.EndColumn = 2 + textRange.StartLine = 1 + textRange.EndLine = 2 + auxLocation.TextRange = textRange + return auxLocation +} + func initSonarIssue(result *wrappers.ScanResult) wrappers.SonarIssues { var sonarIssue wrappers.SonarIssues sonarIssue.Severity = sonarSeverities[result.Severity] @@ -1645,6 +1715,8 @@ func findResult(result *wrappers.ScanResult) []wrappers.SarifScanResult { scanResults = parseSarifResultKics(result, scanResults) } else if result.Type == commonParams.ScaType { scanResults = parseSarifResultsSca(result, scanResults) + } else if result.Type == commonParams.ContainersType && wrappers.IsContainersEnabled { + scanResults = parseSarifResultsContainers(result, scanResults) } if len(scanResults) > 0 { @@ -1653,6 +1725,21 @@ func findResult(result *wrappers.ScanResult) []wrappers.SarifScanResult { return nil } +func parseSarifResultsContainers(result *wrappers.ScanResult, scanResults []wrappers.SarifScanResult) []wrappers.SarifScanResult { + var scanResult = initSarifResult(result) + var scanLocation wrappers.SarifLocation + + scanLocation.PhysicalLocation.ArtifactLocation.URI = result.ScanResultData.ImageFilePath + scanLocation.PhysicalLocation.Region = &wrappers.SarifRegion{} + scanLocation.PhysicalLocation.Region.StartLine = 1 + scanLocation.PhysicalLocation.Region.StartColumn = 1 + scanLocation.PhysicalLocation.Region.EndColumn = 2 + scanResult.Locations = append(scanResult.Locations, scanLocation) + + scanResults = append(scanResults, scanResult) + return scanResults +} + func parseSarifResultsSca(result *wrappers.ScanResult, scanResults []wrappers.SarifScanResult) []wrappers.SarifScanResult { if result == nil || result.ScanResultData.ScaPackageCollection == nil || result.ScanResultData.ScaPackageCollection.Locations == nil { return scanResults @@ -1728,6 +1815,8 @@ func convertNotAvailableNumberToZero(summary *wrappers.ResultSummary) { summary.SastIssues = 0 } else if summary.ScaIssues == notAvailableNumber { summary.ScaIssues = 0 + } else if wrappers.IsContainersEnabled && *summary.ContainersIssues == notAvailableNumber { + *summary.ContainersIssues = 0 } } diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index 148bf5db3..4156d102d 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -3,6 +3,7 @@ package commands import ( + "encoding/json" "fmt" "os" "testing" @@ -10,6 +11,7 @@ import ( "github.com/checkmarx/ast-cli/internal/commands/util/printer" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" "gotest.tools/assert" ) @@ -37,7 +39,13 @@ func TestResultHelp(t *testing.T) { func TestRunGetResultsByScanIdSarifFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sarif") // Remove generated sarif file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatSarif)) + removeFileBySuffix(t, printer.FormatSarif) +} +func TestRunGetResultsByScanIdSarifFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sarif") + // Remove generated sarif file + removeFileBySuffix(t, printer.FormatSarif) } func TestParseSarifEmptyResultSast(t *testing.T) { @@ -50,49 +58,115 @@ func TestParseSarifEmptyResultSast(t *testing.T) { func TestRunGetResultsByScanIdSonarFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sonar") + // Remove generated sonar file + removeFile(t, fileName+"_"+printer.FormatSonar, printer.FormatJSON) +} +func TestRunGetResultsByScanIdSonarFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sonar") // Remove generated sonar file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatSonar)) + removeFile(t, fileName+"_"+printer.FormatSonar, printer.FormatJSON) } func TestRunGetResultsByScanIdJsonFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json") // Remove generated json file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatJSON)) + removeFileBySuffix(t, printer.FormatJSON) +} + +func TestRunGetResultsByScanIdJsonFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json") + + // Remove generated json file + removeFileBySuffix(t, printer.FormatJSON) } func TestRunGetResultsByScanIdJsonFormatWithSastRedundancy(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json", "--sast-redundancy") // Remove generated json file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatJSON)) + removeFileBySuffix(t, printer.FormatJSON) } func TestRunGetResultsByScanIdSummaryJsonFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryJSON") // Remove generated json file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatJSON)) + removeFileBySuffix(t, printer.FormatJSON) +} + +func TestRunGetResultsByScanIdSummaryJsonFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryJSON") + + // Remove generated json file + removeFileBySuffix(t, printer.FormatJSON) } func TestRunGetResultsByScanIdSummaryHtmlFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryHTML") // Remove generated html file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatHTML)) + removeFileBySuffix(t, printer.FormatHTML) +} + +func TestRunGetResultsByScanIdSummaryHtmlFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryHTML") + + // Remove generated html file + removeFileBySuffix(t, printer.FormatHTML) } func TestRunGetResultsByScanIdSummaryConsoleFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryConsole") } +func TestRunGetResultsByScanIdSummaryMarkdownFormatWithContainers(t *testing.T) { + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "markdown") + // Remove generated md file + removeFileBySuffix(t, "md") +} + +func TestRunGetResultsByScanIdSummaryConsoleFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "summaryConsole") +} + +func TestRunGetResultsByScanIdSummaryMarkdownFormat(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "markdown") + // Remove generated md file + removeFileBySuffix(t, "md") +} + +func removeFileBySuffix(t *testing.T, suffix string) { + removeFile(t, fileName, suffix) +} + +func removeFile(t *testing.T, prefix, suffix string) { + err := os.Remove(fmt.Sprintf("%s.%s", prefix, suffix)) + assert.NilError(t, err, "Error removing file, check if report file created") +} + func TestRunGetResultsByScanIdPDFFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "pdf") _, err := os.Stat(fmt.Sprintf("%s.%s", fileName, printer.FormatPDF)) assert.NilError(t, err, "Report file should exist for extension "+printer.FormatPDF) // Remove generated pdf file - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatPDF)) + removeFileBySuffix(t, printer.FormatPDF) +} + +func TestRunGetResultsByScanIdPDFFormatWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "pdf") + _, err := os.Stat(fmt.Sprintf("%s.%s", fileName, printer.FormatPDF)) + assert.NilError(t, err, "Report file should exist for extension "+printer.FormatPDF) + // Remove generated pdf file + removeFileBySuffix(t, printer.FormatPDF) } func TestRunGetResultsByScanIdWrongFormat(t *testing.T) { @@ -286,7 +360,7 @@ func TestRunGetResultsGeneratingPdfReporWithOptions(t *testing.T) { "--output-name", fileName, "--report-pdf-options", "Iac-Security,Sast,Sca,ScanSummary") defer func() { - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatPDF)) + removeFileBySuffix(t, printer.FormatPDF) fmt.Println("test file removed!") }() assert.NilError(t, err) @@ -319,6 +393,24 @@ func TestSBOMReportXML(t *testing.T) { os.Remove(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML)) } +func TestSBOMReportJsonWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sbom") + _, err := os.Stat(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatJSON)) + assert.NilError(t, err, "Report file should exist for extension "+printer.FormatJSON) + // Remove generated json file + os.Remove(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatJSON)) +} + +func TestSBOMReportXMLWithContainers(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sbom", "--report-sbom-format", "CycloneDxXml") + _, err := os.Stat(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML)) + assert.NilError(t, err, "Report file should exist for extension "+printer.FormatXML) + // Remove generated json file + os.Remove(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML)) +} + func TestSBOMReportXMLWithProxy(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sbom", "--report-sbom-format", "CycloneDxXml", "--report-sbom-local-flow") _, err := os.Stat(fmt.Sprintf("%s.%s", fileName+"_"+printer.FormatSbom, printer.FormatXML)) @@ -330,5 +422,49 @@ func TestSBOMReportXMLWithProxy(t *testing.T) { func TestRunGetResultsByScanIdGLFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "gl-sast") // Run test for gl-sast report type - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatGL)) + removeFile(t, "cx_result.gl-sast-report", "json") +} +func TestRunResultsShow_ContainersFFIsOn_includeContainersResult(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json") + assertContainersPresent(t, true) + // Remove generated json file + removeFileBySuffix(t, printer.FormatJSON) +} +func TestRunResultsShow_ContainersFFIsOff_excludeContainersResult(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: false}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json") + assertContainersPresent(t, false) + // Remove generated json file + removeFileBySuffix(t, printer.FormatJSON) +} +func TestRunResultsShow_AgentIsNotSupported_excludeContainersResult(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: true}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "json", "--agent", "jet-brains") + assertContainersPresent(t, false) + // Remove generated json file + removeFileBySuffix(t, printer.FormatJSON) +} + +func assertContainersPresent(t *testing.T, isContainersEnabled bool) { + bytes, err := os.ReadFile(fileName + "." + printer.FormatJSON) + assert.NilError(t, err, "Error reading file") + // Unmarshal the JSON data into the ScanResultsCollection struct + var scanResultsCollection *wrappers.ScanResultsCollection + err = json.Unmarshal(bytes, &scanResultsCollection) + assert.NilError(t, err, "Error unmarshalling JSON data") + for _, scanResult := range scanResultsCollection.Results { + if !isContainersEnabled && scanResult.Type == params.ContainersType { + assert.Assert(t, false, "Containers result should not be present") + } else if isContainersEnabled && scanResult.Type == params.ContainersType { + return + } + } + if isContainersEnabled { + assert.Assert(t, false, "Containers result should be present") + } +} +func TestRunGetResultsShow_ContainersFFOffAndResultsHasContainersResultsOnly_NilAssertion(t *testing.T) { + mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: wrappers.ContainerEngineCLIEnabled, Status: false}} + execCmdNilAssertion(t, "results", "show", "--scan-id", "CONTAINERS_ONLY", "--report-format", "summaryConsole") } diff --git a/internal/commands/scan.go b/internal/commands/scan.go index f3b7b098a..c61ccd931 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -1784,6 +1784,7 @@ func createReportsAfterScan( formatSbomOptions, _ := cmd.Flags().GetString(commonParams.ReportSbomFormatFlag) useSCALocalFlow, _ := cmd.Flags().GetBool(commonParams.ReportSbomFormatLocalFlowFlag) retrySBOM, _ := cmd.Flags().GetInt(commonParams.RetrySBOMFlag) + agent, _ := cmd.Flags().GetString(commonParams.AgentFlag) params, err := getFilters(cmd) if err != nil { @@ -1814,6 +1815,7 @@ func createReportsAfterScan( formatSbomOptions, targetFile, targetPath, + agent, params, ) } diff --git a/internal/params/flags.go b/internal/params/flags.go index 8efeeb7d3..051b1773d 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -224,13 +224,13 @@ const ( SastType = "sast" KicsType = "kics" APISecurityType = "api-security" - ContainersType = "containers" APIDocumentationFlag = "apisec-swagger-filter" IacType = "iac-security" IacLabel = "IaC Security" APISecurityLabel = "API Security" ScaType = "sca" APISecType = "apisec" + ContainersType = "containers" Success = "success" ) diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index 21c5fe877..0dc3638e2 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -26,6 +26,9 @@ var FeatureFlagsBaseMap = []CommandFlags{ { CommandName: "cx project create", }, + { + CommandName: "cx results show", + }, } var FeatureFlags = map[string]bool{} diff --git a/internal/wrappers/mock/results-mock.go b/internal/wrappers/mock/results-mock.go index 731b65d0b..3af261886 100644 --- a/internal/wrappers/mock/results-mock.go +++ b/internal/wrappers/mock/results-mock.go @@ -42,16 +42,46 @@ func (r ResultsMockWrapper) GetAllResultsPackageByScanID(params map[string]strin return &scaPackages, nil, nil } -func (r ResultsMockWrapper) GetAllResultsByScanID(_ map[string]string) ( +var containersResults = &wrappers.ScanResult{ + Type: "containers", + Severity: "medium", + ScanResultData: wrappers.ScanResultData{ + PackageName: "image-mock", + PackageVersion: "1.1", + ImageName: "image-mock", + ImageTag: "1.1", + ImageFilePath: "DockerFile", + ImageOrigin: "Docker", + PackageIdentifier: "mock", + QueryID: 12.4, + QueryName: "mock-query-name", + }, + Description: "mock-description", + VulnerabilityDetails: wrappers.VulnerabilityDetails{ + CvssScore: 4.5, + CveName: "CVE-2021-1234", + CweID: "CWE-1234", + }, +} + +func (r ResultsMockWrapper) GetAllResultsByScanID(params map[string]string) ( *wrappers.ScanResultsCollection, *wrappers.WebError, error, ) { + if params["scan-id"] == "CONTAINERS_ONLY" { + return &wrappers.ScanResultsCollection{ + TotalCount: 1, + Results: []*wrappers.ScanResult{ + containersResults, + }, + }, nil, nil + } const mock = "mock" var dependencyPath = wrappers.DependencyPath{ID: mock, Name: mock, Version: mock, IsResolved: true, IsDevelopment: false, Locations: nil} var dependencyArray = [][]wrappers.DependencyPath{{dependencyPath}} return &wrappers.ScanResultsCollection{ - TotalCount: 7, + TotalCount: 8, Results: []*wrappers.ScanResult{ { Type: "sast", @@ -204,6 +234,7 @@ func (r ResultsMockWrapper) GetAllResultsByScanID(_ map[string]string) ( }, }, }, + containersResults, { Type: "kics", Severity: "low", diff --git a/internal/wrappers/mock/scans-mock.go b/internal/wrappers/mock/scans-mock.go index 088e4d9fe..9c9999235 100644 --- a/internal/wrappers/mock/scans-mock.go +++ b/internal/wrappers/mock/scans-mock.go @@ -83,7 +83,7 @@ func (m *ScansMockWrapper) GetByID(scanID string) (*wrappers.ScanResponseModel, return &wrappers.ScanResponseModel{ ID: scanID, Status: status, - Engines: []string{params.ScaType, params.SastType, params.KicsType}, + Engines: []string{params.ScaType, params.SastType, params.KicsType, params.ContainersType}, }, nil, nil } diff --git a/internal/wrappers/results-json.go b/internal/wrappers/results-json.go index 2697cd857..2d6497894 100644 --- a/internal/wrappers/results-json.go +++ b/internal/wrappers/results-json.go @@ -108,4 +108,11 @@ type ScanResultData struct { ExpectedValue string `json:"expectedValue,omitempty"` Value string `json:"value,omitempty"` Filename string `json:"filename,omitempty"` + // Added to support containers results + PackageName string `json:"packageName,omitempty"` + PackageVersion string `json:"packageVersion,omitempty"` + ImageName string `json:"imageName,omitempty"` + ImageTag string `json:"imageTag,omitempty"` + ImageFilePath string `json:"imageFilePath,omitempty"` + ImageOrigin string `json:"imageOrigin,omitempty"` } diff --git a/internal/wrappers/results-summary.go b/internal/wrappers/results-summary.go index 599111344..8c40573be 100644 --- a/internal/wrappers/results-summary.go +++ b/internal/wrappers/results-summary.go @@ -8,31 +8,32 @@ import ( ) type ResultSummary struct { - TotalIssues int - HighIssues int - MediumIssues int - LowIssues int - InfoIssues int - SastIssues int - KicsIssues int - ScaIssues int - APISecurity APISecResult - RiskStyle string - RiskMsg string - Status string - ScanID string - ScanDate string - ScanTime string - CreatedAt string - ProjectID string - BaseURI string - Tags map[string]string - ProjectName string - BranchName string - ScanInfoMessage string - EnginesEnabled []string - Policies *PolicyResponseModel - EnginesResult EnginesResultsSummary + TotalIssues int + HighIssues int + MediumIssues int + LowIssues int + InfoIssues int + SastIssues int + KicsIssues int + ScaIssues int + ContainersIssues *int `json:"ContainersIssues,omitempty"` + APISecurity APISecResult + RiskStyle string + RiskMsg string + Status string + ScanID string + ScanDate string + ScanTime string + CreatedAt string + ProjectID string + BaseURI string + Tags map[string]string + ProjectName string + BranchName string + ScanInfoMessage string + EnginesEnabled []string + Policies *PolicyResponseModel + EnginesResult EnginesResultsSummary } // nolint: govet @@ -57,6 +58,8 @@ type EngineResultSummary struct { type EnginesResultsSummary map[string]*EngineResultSummary +var IsContainersEnabled bool + func (engineSummary *EnginesResultsSummary) GetHighIssues() int { highIssues := 0 for _, v := range *engineSummary { @@ -102,8 +105,8 @@ func (engineSummary *EngineResultSummary) Increment(level string) { } } -func (summary *ResultSummary) UpdateEngineResultSummary(engineType, severity string) { - summary.EnginesResult[engineType].Increment(severity) +func (r *ResultSummary) UpdateEngineResultSummary(engineType, severity string) { + r.EnginesResult[engineType].Increment(severity) } func (r *ResultSummary) HasEngine(engine string) bool { @@ -118,7 +121,12 @@ func (r *ResultSummary) HasEngine(engine string) bool { func (r *ResultSummary) HasAPISecurity() bool { return r.HasEngine(params.APISecType) } - +func (r *ResultSummary) ContainersEnabled() bool { + return IsContainersEnabled +} +func (r *ResultSummary) ContainersIssuesValue() int { + return *r.ContainersIssues +} func (r *ResultSummary) getRiskFromAPISecurity(origin string) *riskDistribution { for _, risk := range r.APISecurity.RiskDistribution { if strings.EqualFold(risk.Origin, origin) { @@ -235,7 +243,7 @@ const summaryTemplateHeader = `{{define "SummaryTemplate"}} } .bg-kicks { - background-color: #008e96 !important; + background-color: #079E9E !important; } .bg-red { @@ -243,16 +251,19 @@ const summaryTemplateHeader = `{{define "SummaryTemplate"}} } .bg-sast { - background-color: #1165b4 !important; + background-color: #0356A5 !important; } .bg-sca { - background-color: #0fcdc2 !important; + background-color: #15D7D7 !important; } .bg-api-sec { background-color: #bdbdbd !important; } + .bg-containers { + background-color: #70F9CC !important; + } .header-row .cx-info .data .calendar-svg { margin-right: 8px; @@ -707,6 +718,9 @@ const nonAsyncSummary = `
SCA
+ {{if .ContainersEnabled}}
Containers +
+
{{end}}
@@ -714,6 +728,7 @@ const nonAsyncSummary = `
{{if lt .SastIssues 0}}N/A{{else}}{{.SastIssues}}{{end}}
{{if lt .KicsIssues 0}}N/A{{else}}{{.KicsIssues}}{{end}}
{{if lt .ScaIssues 0}}N/A{{else}}{{.ScaIssues}}{{end}}
+ {{if .ContainersEnabled}}
{{if lt .ContainersIssuesValue 0}}N/A{{else}}{{.ContainersIssuesValue}}{{end}}
{{end}}
@@ -785,9 +800,9 @@ const SummaryMarkdownCompletedTemplate = ` ### Vulnerabilities per Scan Type -| SAST | IaC Security | SCA | -|:----------:|:----------:|:---------:| -| {{if lt .SastIssues 0}}N/A{{else}}{{.SastIssues}}{{end}} | {{if lt .KicsIssues 0}}N/A{{else}}{{.KicsIssues}}{{end}} | {{if lt .ScaIssues 0}}N/A{{else}}{{.ScaIssues}}{{end}} | +| SAST | IaC Security | SCA |{{if .ContainersEnabled}} Containers |{{end}} +|:----------:|:----------:|:---------:|{{if .ContainersEnabled}} :----------:|{{end}} +| {{if lt .SastIssues 0}}N/A{{else}}{{.SastIssues}}{{end}} | {{if lt .KicsIssues 0}}N/A{{else}}{{.KicsIssues}}{{end}} | {{if lt .ScaIssues 0}}N/A{{else}}{{.ScaIssues}}{{end}} | {{if .ContainersEnabled}}{{if lt .ScaIssues 0}}N/A{{else}}{{.ContainersIssuesValue}}{{end}} | {{end}} {{if .HasAPISecurity}} ### API Security diff --git a/test/integration/result_test.go b/test/integration/result_test.go index b3711da84..b667d0f97 100644 --- a/test/integration/result_test.go +++ b/test/integration/result_test.go @@ -23,11 +23,9 @@ const ( // Create a scan and test getting its results func TestResultListJson(t *testing.T) { - assertRequiredParameter(t, "Please provide a scan ID", "results", "show") scanID, _ := getRootScan(t) - outputBuffer := executeCmdNilAssertion( t, "Getting results should pass", "results", diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index c8b6055de..5de4baa0a 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -463,7 +463,7 @@ func executeScanAssertions(t *testing.T, projectID, scanID string, tags map[stri } func createScan(t *testing.T, source string, tags map[string]string) (string, string) { - return executeCreateScan(t, getCreateArgs(source, tags, "sast , sca , iac-security , api-security ")) + return executeCreateScan(t, getCreateArgs(source, tags, "sast , sca , iac-security , api-security , container-security ")) } func createScanNoWait(t *testing.T, source string, tags map[string]string) (string, string) {