diff --git a/cmd/main.go b/cmd/main.go index 8ce4a8aa5..2dca11d2f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,6 +37,7 @@ func main() { scanSummary := viper.GetString(params.ScanSummaryPathKey) scaPackage := viper.GetString(params.ScaPackagePathKey) risksOverview := viper.GetString(params.RisksOverviewPathKey) + scsScanOverview := viper.GetString(params.ScsScanOverviewPathKey) uploads := viper.GetString(params.UploadsPathKey) codebashing := viper.GetString(params.CodeBashingPathKey) bfl := viper.GetString(params.BflPathKey) @@ -61,6 +62,7 @@ func main() { projectsWrapper := wrappers.NewHTTPProjectsWrapper(projects) applicationsWrapper := wrappers.NewApplicationsHTTPWrapper(applications) risksOverviewWrapper := wrappers.NewHTTPRisksOverviewWrapper(risksOverview) + scsScanOverviewWrapper := wrappers.NewHTTPScanOverviewWrapper(scsScanOverview) resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scaPackage, scanSummary) authWrapper := wrappers.NewAuthHTTPWrapper() resultsPredicatesWrapper := wrappers.NewResultsPredicatesHTTPWrapper() @@ -93,6 +95,7 @@ func main() { projectsWrapper, resultsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, authWrapper, logsWrapper, groupsWrapper, diff --git a/internal/commands/result.go b/internal/commands/result.go index 597e9c8ec..92217cf03 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -58,9 +58,12 @@ const ( scanFailedString = "Failed" scanCanceledString = "Canceled" scanSuccessString = "Completed" + scanPartialString = "Partial" + scsScanUnavailableString = "" notAvailableNumber = -1 scanFailedNumber = -2 scanCanceledNumber = -3 + scanPartialNumber = -4 defaultPaddingSize = -13 boldFormat = "\033[1m%s\033[0m" scanPendingMessage = "Scan triggered in asynchronous mode or still running. Click more details to get the full status." @@ -148,6 +151,7 @@ func NewResultsCommand( codeBashingWrapper wrappers.CodeBashingWrapper, bflWrapper wrappers.BflWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, policyWrapper wrappers.PolicyWrapper, ) *cobra.Command { resultCmd := &cobra.Command{ @@ -161,7 +165,7 @@ func NewResultsCommand( ), }, } - showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper, policyWrapper) + showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, policyWrapper) codeBashingCmd := resultCodeBashing(codeBashingWrapper) bflResultCmd := resultBflSubCommand(bflWrapper) resultCmd.AddCommand( @@ -176,6 +180,7 @@ func resultShowSubCommand( resultsSbomWrapper wrappers.ResultsSbomWrapper, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, policyWrapper wrappers.PolicyWrapper, ) *cobra.Command { resultShowCmd := &cobra.Command{ @@ -187,7 +192,7 @@ func resultShowSubCommand( $ cx results show --scan-id `, ), - RunE: runGetResultCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper, policyWrapper), + RunE: runGetResultCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, policyWrapper), } addScanIDFlag(resultShowCmd, "ID to report on.") addResultFormatFlag( @@ -358,11 +363,13 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr sastIssues := 0 scaIssues := 0 kicsIssues := 0 + scsIssues := 0 enginesStatusCode := map[string]int{ commonParams.SastType: 0, commonParams.ScaType: 0, commonParams.KicsType: 0, commonParams.APISecType: 0, + commonParams.ScsType: 0, } if len(scanInfo.StatusDetails) > 0 { @@ -374,6 +381,8 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr scaIssues = notAvailableNumber } else if statusDetailItem.Name == commonParams.KicsType { kicsIssues = notAvailableNumber + } else if statusDetailItem.Name == commonParams.ScsType { + scsIssues = notAvailableNumber } } switch statusDetailItem.Status { @@ -398,6 +407,7 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr SastIssues: sastIssues, KicsIssues: kicsIssues, ScaIssues: scaIssues, + ScsIssues: scsIssues, Tags: scanInfo.Tags, ProjectName: scanInfo.ProjectName, BranchName: scanInfo.Branch, @@ -407,6 +417,7 @@ func convertScanToResultsSummary(scanInfo *wrappers.ScanResponseModel, resultsWr commonParams.ScaType: {StatusCode: enginesStatusCode[commonParams.ScaType]}, commonParams.KicsType: {StatusCode: enginesStatusCode[commonParams.KicsType]}, commonParams.APISecType: {StatusCode: enginesStatusCode[commonParams.APISecType]}, + commonParams.ScsType: {StatusCode: enginesStatusCode[commonParams.ScsType]}, }, } @@ -434,6 +445,7 @@ func summaryReport( summary *wrappers.ResultSummary, policies *wrappers.PolicyResponseModel, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, results *wrappers.ScanResultsCollection, ) (*wrappers.ResultSummary, error) { if summary.HasAPISecurity() { @@ -444,6 +456,14 @@ func summaryReport( summary.APISecurity = *apiSecRisks } + if summary.HasSCS() { + SCSOverview, err := getScanOverviewForSCSScanner(scsScanOverviewWrapper, summary.ScanID) + if err != nil { + return nil, err + } + summary.SCSOverview = *SCSOverview + } + if policies != nil { summary.Policies = filterViolatedRules(*policies) } @@ -453,6 +473,7 @@ func summaryReport( setNotAvailableNumberIfZero(summary, &summary.SastIssues, commonParams.SastType) setNotAvailableNumberIfZero(summary, &summary.ScaIssues, commonParams.ScaType) setNotAvailableNumberIfZero(summary, &summary.KicsIssues, commonParams.KicsType) + setNotAvailableNumberIfZero(summary, &summary.ScsIssues, commonParams.ScsType) setRiskMsgAndStyle(summary) setNotAvailableEnginesStatusCode(summary) @@ -495,7 +516,21 @@ func enhanceWithScanSummary(summary *wrappers.ResultSummary, results *wrappers.S summary.EnginesResult[commonParams.APISecType].Medium = summary.APISecurity.Risks[2] summary.EnginesResult[commonParams.APISecType].High = summary.APISecurity.Risks[1] } - summary.TotalIssues = summary.SastIssues + summary.ScaIssues + summary.KicsIssues + summary.GetAPISecurityDocumentationTotal() + + if summary.HasSCS() { + summary.EnginesResult[commonParams.ScsType].Info = summary.SCSOverview.RiskSummary[infoLabel] + summary.EnginesResult[commonParams.ScsType].Low = summary.SCSOverview.RiskSummary[lowLabel] + summary.EnginesResult[commonParams.ScsType].Medium = summary.SCSOverview.RiskSummary[mediumLabel] + summary.EnginesResult[commonParams.ScsType].High = summary.SCSOverview.RiskSummary[highLabel] + + summary.ScsIssues = summary.SCSOverview.TotalRisksCount + + // Special case for SCS where status is partial if any microengines failed + if summary.SCSOverview.Status == scanPartialString { + summary.EnginesResult[commonParams.ScsType].StatusCode = scanPartialNumber + } + } + summary.TotalIssues = summary.SastIssues + summary.ScaIssues + summary.KicsIssues + summary.GetAPISecurityDocumentationTotal() + summary.ScsIssues } func writeHTMLSummary(targetFile string, summary *wrappers.ResultSummary) error { @@ -552,6 +587,10 @@ func writeConsoleSummary(summary *wrappers.ResultSummary) error { printAPIsSecuritySummary(summary) } + if summary.HasSCS() { + printSCSSummary(summary.SCSOverview.MicroEngineOverviews) + } + fmt.Printf(" Checkmarx One - Scan Summary & Details: %s\n", summary.BaseURI) } else { fmt.Printf("Scan executed in asynchronous mode or still running. Hence, no results generated.\n") @@ -601,11 +640,38 @@ func printTableRow(title string, counts *wrappers.EngineResultSummary, statusNum fmt.Printf(formatString, 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) + case scanPartialNumber: + fmt.Printf(formatString, title, counts.High, counts.Medium, counts.Low, counts.Info, scanPartialString) default: fmt.Printf(formatString, title, counts.High, counts.Medium, counts.Low, counts.Info, scanSuccessString) } } +func printSCSSummary(microEngineOverviews []*wrappers.MicroEngineOverview) { + fmt.Printf(" Supply Chain Security Results\n") + fmt.Printf(" --------------------------------------------------------------- \n") + fmt.Println(" | High Medium Low Info Status |") + for _, microEngineOverview := range microEngineOverviews { + printSCSTableRow(microEngineOverview) + } + fmt.Printf(" --------------------------------------------------------------- \n\n") +} + +func printSCSTableRow(microEngineOverview *wrappers.MicroEngineOverview) { + formatString := " | %-16s %4d %6d %4d %4d %-9s |\n" + notAvailableFormatString := " | %-16s %4s %6s %4s %4s %5s |\n" + + riskSummary := microEngineOverview.RiskSummary + microEngineName := microEngineOverview.FullName + + switch microEngineOverview.Status { + case scsScanUnavailableString: + fmt.Printf(notAvailableFormatString, microEngineName, notAvailableString, notAvailableString, notAvailableString, notAvailableString, notAvailableString) + default: + fmt.Printf(formatString, microEngineName, riskSummary[highLabel], riskSummary[mediumLabel], riskSummary[lowLabel], riskSummary[infoLabel], microEngineOverview.Status) + } +} + func printResultsSummaryTable(summary *wrappers.ResultSummary) { totalHighIssues := summary.EnginesResult.GetHighIssues() totalMediumIssues := summary.EnginesResult.GetMediumIssues() @@ -620,6 +686,7 @@ func printResultsSummaryTable(summary *wrappers.ResultSummary) { 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) + printTableRow("SCS", summary.EnginesResult[commonParams.ScsType], summary.EnginesResult[commonParams.ScsType].StatusCode) fmt.Println(" --------------------------------------------------- ") fmt.Printf(" | %-4s %4d %6d %4d %4d %-9s |\n", @@ -641,6 +708,7 @@ func runGetResultCommand( resultsSbomWrapper wrappers.ResultsSbomWrapper, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, policyWrapper wrappers.PolicyWrapper, ) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { @@ -693,6 +761,7 @@ func runGetResultCommand( return CreateScanReport( resultsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, resultsSbomWrapper, policyResponseModel, useSCALocalFlow, @@ -752,6 +821,7 @@ func runGetCodeBashingCommand( func CreateScanReport( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, resultsSbomWrapper wrappers.ResultsSbomWrapper, policyResponseModel *wrappers.PolicyResponseModel, useSCALocalFlow bool, @@ -788,7 +858,7 @@ func CreateScanReport( } isSummaryNeeded := verifyFormatsByReportList(reportList, summaryFormats...) if isSummaryNeeded && !scanPending { - summary, err = summaryReport(summary, policyResponseModel, risksOverviewWrapper, results) + summary, err = summaryReport(summary, policyResponseModel, risksOverviewWrapper, scsScanOverviewWrapper, results) if err != nil { return err } @@ -875,6 +945,25 @@ func getResultsForAPISecScanner( return nil, nil } +func getScanOverviewForSCSScanner( + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanID string, +) (results *wrappers.SCSOverview, err error) { + var scsOverview *wrappers.SCSOverview + var errorModel *wrappers.WebError + + scsOverview, errorModel, err = scsScanOverviewWrapper.GetSCSOverviewByScanID(scanID) + if err != nil { + return nil, errors.Wrapf(err, "SCS: %s", failedListingResults) + } + if errorModel != nil { + return nil, errors.Errorf("SCS: %s: CODE: %d, %s", failedListingResults, errorModel.Code, errorModel.Message) + } else if scsOverview != nil { + return scsOverview, nil + } + return nil, nil +} + func isScanPending(scanStatus string) bool { return !(strings.EqualFold(scanStatus, "Completed") || strings.EqualFold( scanStatus, diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index 6f2c6f157..54fd2af18 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -5,6 +5,8 @@ package commands import ( "fmt" "os" + "regexp" + "strings" "testing" "github.com/checkmarx/ast-cli/internal/commands/util/printer" @@ -17,14 +19,15 @@ import ( const fileName = "cx_result" const ( - resultsCommand = "results" - codeBashingCommand = "codebashing" - vulnerabilityValue = "Reflected XSS All Clients" - languageValue = "PHP" - cweValue = "79" - jsonValue = "json" - tableValue = "table" - listValue = "list" + resultsCommand = "results" + codeBashingCommand = "codebashing" + vulnerabilityValue = "Reflected XSS All Clients" + languageValue = "PHP" + cweValue = "79" + jsonValue = "json" + tableValue = "table" + listValue = "list" + secretDetectionLine = "| Secret Detection 5 3 2 0 Completed |" ) func flag(f string) string { @@ -406,3 +409,68 @@ func Test_addPackageInformation(t *testing.T) { actualFixLink := resultsModel.Results[0].ScanResultData.ScaPackageCollection.FixLink assert.Equal(t, expectedFixLink, actualFixLink, "FixLink should match the result ID") } + +func TestRunGetResultsByScanIdSummaryConsoleFormatWithScsNotScanned(t *testing.T) { + buffer, err := executeRedirectedOsStdoutTestCommand(createASTTestCommandWithScs(false, false, false), + "results", "show", "--scan-id", "MOCK", "--report-format", "summaryConsole") + assert.NilError(t, err) + + stdoutString := buffer.String() + fmt.Print(stdoutString) + + scsSummary := "| SCS - - - - - |" + assert.Equal(t, strings.Contains(stdoutString, scsSummary), true, + "Expected SCS summary:"+scsSummary) + secretDetectionSummary := "Secret Detection" + assert.Equal(t, !strings.Contains(stdoutString, secretDetectionSummary), true, + "Expected Secret Detection summary to be missing:"+secretDetectionSummary) + scorecardSummary := "Scorecard" + assert.Equal(t, !strings.Contains(stdoutString, scorecardSummary), true, + "Expected Scorecard summary to be missing:"+scorecardSummary) +} + +func TestRunGetResultsByScanIdSummaryConsoleFormatWithScsPartial(t *testing.T) { + buffer, err := executeRedirectedOsStdoutTestCommand(createASTTestCommandWithScs(true, true, true), + "results", "show", "--scan-id", "MOCK", "--report-format", "summaryConsole") + assert.NilError(t, err) + + stdoutString := buffer.String() + ansiRegexp := regexp.MustCompile("\x1b\\[[0-9;]*[mK]") + cleanString := ansiRegexp.ReplaceAllString(stdoutString, "") + fmt.Print(stdoutString) + + TotalResults := "Total Results: 17" + assert.Equal(t, strings.Contains(cleanString, TotalResults), true, + "Expected: "+TotalResults) + TotalSummary := "| TOTAL 10 4 3 0 Completed |" + assert.Equal(t, strings.Contains(cleanString, TotalSummary), true, + "Expected TOTAL summary: "+TotalSummary) + scsSummary := "| SCS 5 3 2 0 Partial |" + assert.Equal(t, strings.Contains(cleanString, scsSummary), true, + "Expected SCS summary:"+scsSummary) + secretDetectionSummary := secretDetectionLine + assert.Equal(t, strings.Contains(cleanString, secretDetectionSummary), true, + "Expected Secret Detection summary:"+secretDetectionSummary) + scorecardSummary := "| Scorecard 0 0 0 0 Failed |" + assert.Equal(t, strings.Contains(cleanString, scorecardSummary), true, + "Expected Scorecard summary:"+scorecardSummary) +} + +func TestRunGetResultsByScanIdSummaryConsoleFormatWithScsScorecardNotScanned(t *testing.T) { + buffer, err := executeRedirectedOsStdoutTestCommand(createASTTestCommandWithScs(true, false, false), + "results", "show", "--scan-id", "MOCK", "--report-format", "summaryConsole") + assert.NilError(t, err) + + stdoutString := buffer.String() + fmt.Print(stdoutString) + + scsSummary := "| SCS 5 3 2 0 Completed |" + assert.Equal(t, strings.Contains(stdoutString, scsSummary), true, + "Expected SCS summary:"+scsSummary) + secretDetectionSummary := secretDetectionLine + assert.Equal(t, strings.Contains(stdoutString, secretDetectionSummary), true, + "Expected Secret Detection summary:"+secretDetectionSummary) + scorecardSummary := "| Scorecard - - - - - |" + assert.Equal(t, strings.Contains(stdoutString, scorecardSummary), true, + "Expected Scorecard summary:"+scorecardSummary) +} diff --git a/internal/commands/root.go b/internal/commands/root.go index ed879366f..040a39060 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -33,6 +33,7 @@ func NewAstCLI( projectsWrapper wrappers.ProjectsWrapper, resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, authWrapper wrappers.AuthWrapper, logsWrapper wrappers.LogsWrapper, groupsWrapper wrappers.GroupsWrapper, @@ -155,6 +156,7 @@ func NewAstCLI( logsWrapper, groupsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, jwtWrapper, scaRealTimeWrapper, policyWrapper, @@ -170,6 +172,7 @@ func NewAstCLI( codeBashingWrapper, bflWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, policyWrapper, ) diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index e301c4676..15e87d391 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -3,6 +3,7 @@ package commands import ( "bytes" "fmt" + "io" "log" "os" "strings" @@ -41,6 +42,7 @@ func createASTTestCommand() *cobra.Command { projectsMockWrapper := &mock.ProjectsMockWrapper{} resultsMockWrapper := &mock.ResultsMockWrapper{} risksOverviewMockWrapper := &mock.RisksOverviewMockWrapper{} + scsScanOverviewMockWrapper := &mock.ScanOverviewMockWrapper{} authWrapper := &mock.AuthMockWrapper{} logsWrapper := &mock.LogsMockWrapper{} codeBashingWrapper := &mock.CodeBashingMockWrapper{} @@ -71,6 +73,73 @@ func createASTTestCommand() *cobra.Command { projectsMockWrapper, resultsMockWrapper, risksOverviewMockWrapper, + scsScanOverviewMockWrapper, + authWrapper, + logsWrapper, + groupsMockWrapper, + gitHubWrapper, + azureWrapper, + bitBucketWrapper, + nil, + gitLabWrapper, + bflMockWrapper, + prMockWrapper, + learnMoreMockWrapper, + tenantConfigurationMockWrapper, + jwtWrapper, + scaRealtimeMockWrapper, + chatWrapper, + featureFlagsMockWrapper, + policyWrapper, + sastMetadataWrapper, + accessManagementWrapper, + ) +} + +func createASTTestCommandWithScs(scsScanned, scsScanPartial, scorecardScanned bool) *cobra.Command { + applicationWrapper := &mock.ApplicationsMockWrapper{} + scansMockWrapper := &mock.ScansMockWrapper{HasSCS: scsScanned} + resultsSbomWrapper := &mock.ResultsSbomWrapper{} + resultsPdfWrapper := &mock.ResultsPdfWrapper{} + scansMockWrapper.Running = true + resultsPredicatesMockWrapper := &mock.ResultsPredicatesMockWrapper{} + groupsMockWrapper := &mock.GroupsMockWrapper{} + uploadsMockWrapper := &mock.UploadsMockWrapper{} + projectsMockWrapper := &mock.ProjectsMockWrapper{} + resultsMockWrapper := &mock.ResultsMockWrapper{} + risksOverviewMockWrapper := &mock.RisksOverviewMockWrapper{} + scsScanOverviewMockWrapper := &mock.ScanOverviewMockWrapper{ScanPartial: scsScanPartial, ScorecardScanned: scorecardScanned} + authWrapper := &mock.AuthMockWrapper{} + logsWrapper := &mock.LogsMockWrapper{} + codeBashingWrapper := &mock.CodeBashingMockWrapper{} + gitHubWrapper := &mock.GitHubMockWrapper{} + azureWrapper := &mock.AzureMockWrapper{} + bitBucketWrapper := &mock.BitBucketMockWrapper{} + gitLabWrapper := &mock.GitLabMockWrapper{} + bflMockWrapper := &mock.BflMockWrapper{} + learnMoreMockWrapper := &mock.LearnMoreMockWrapper{} + prMockWrapper := &mock.PRMockWrapper{} + tenantConfigurationMockWrapper := &mock.TenantConfigurationMockWrapper{} + jwtWrapper := &mock.JWTMockWrapper{} + scaRealtimeMockWrapper := &mock.ScaRealTimeHTTPMockWrapper{} + chatWrapper := &mock.ChatMockWrapper{} + featureFlagsMockWrapper := &mock.FeatureFlagsMockWrapper{} + policyWrapper := &mock.PolicyMockWrapper{} + sastMetadataWrapper := &mock.SastMetadataMockWrapper{} + accessManagementWrapper := &mock.AccessManagementMockWrapper{} + + return NewAstCLI( + applicationWrapper, + scansMockWrapper, + resultsSbomWrapper, + resultsPdfWrapper, + resultsPredicatesMockWrapper, + codeBashingWrapper, + uploadsMockWrapper, + projectsMockWrapper, + resultsMockWrapper, + risksOverviewMockWrapper, + scsScanOverviewMockWrapper, authWrapper, logsWrapper, groupsMockWrapper, @@ -122,6 +191,27 @@ func executeRedirectedTestCommand(args ...string) (*bytes.Buffer, error) { return buffer, cmd.Execute() } +func executeRedirectedOsStdoutTestCommand(cmd *cobra.Command, args ...string) (bytes.Buffer, error) { + // Writing os stdout to file + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + cmd.SetArgs(args) + cmd.SilenceUsage = true + err := cmd.Execute() + + // Writing output to buffer + w.Close() + os.Stdout = old + var buffer bytes.Buffer + _, errCopy := io.Copy(&buffer, r) + if errCopy != nil { + return buffer, errCopy + } + return buffer, err +} + func execCmdNilAssertion(t *testing.T, args ...string) { err := executeTestCommand(createASTTestCommand(), args...) assert.NilError(t, err) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index e614c2bf7..99c5fdb8c 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -134,6 +134,7 @@ func NewScanCommand( logsWrapper wrappers.LogsWrapper, groupsWrapper wrappers.GroupsWrapper, riskOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, jwtWrapper wrappers.JWTWrapper, scaRealTimeWrapper wrappers.ScaRealTimeWrapper, policyWrapper wrappers.PolicyWrapper, @@ -162,6 +163,7 @@ func NewScanCommand( projectsWrapper, groupsWrapper, riskOverviewWrapper, + scsScanOverviewWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -414,6 +416,7 @@ func scanCreateSubCommand( projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -444,6 +447,7 @@ func scanCreateSubCommand( projectsWrapper, groupsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -1507,6 +1511,7 @@ func runCreateScanCommand( projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -1565,7 +1570,8 @@ func runCreateScanCommand( resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, - risksOverviewWrapper) + risksOverviewWrapper, + scsScanOverviewWrapper) if err != nil { return err } @@ -1583,7 +1589,8 @@ func runCreateScanCommand( } else { logger.PrintIfVerbose("Skipping policy evaluation") } - err = createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, risksOverviewWrapper, policyResponseModel) + err = createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, + risksOverviewWrapper, scsScanOverviewWrapper, policyResponseModel) if err != nil { return err } @@ -1593,7 +1600,8 @@ func runCreateScanCommand( return err } } else { - err = createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, risksOverviewWrapper, nil) + err = createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, + risksOverviewWrapper, scsScanOverviewWrapper, nil) if err != nil { return err } @@ -1750,6 +1758,7 @@ func handleWait( resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, ) error { err := waitForScanCompletion( scanResponseModel, @@ -1760,6 +1769,7 @@ func handleWait( resultsPdfReportsWrapper, resultsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, cmd) if err != nil { verboseFlag, _ := cmd.Flags().GetBool(commonParams.DebugFlag) @@ -1781,6 +1791,7 @@ func createReportsAfterScan( resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, policyResponseModel *wrappers.PolicyResponseModel, ) error { // Create the required reports @@ -1810,6 +1821,7 @@ func createReportsAfterScan( return CreateScanReport( resultsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, resultsSbomWrapper, policyResponseModel, useSCALocalFlow, @@ -1925,6 +1937,7 @@ func waitForScanCompletion( resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, cmd *cobra.Command, ) error { log.Println("Wait for scan to complete", scanResponseModel.ID, scanResponseModel.Status) @@ -1939,7 +1952,7 @@ func waitForScanCompletion( waitDuration := fixedWait + variableWait logger.PrintfIfVerbose("Sleeping %v before polling", waitDuration) time.Sleep(waitDuration) - running, err := isScanRunning(scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, risksOverviewWrapper, scanResponseModel.ID, cmd) + running, err := isScanRunning(scansWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, scanResponseModel.ID, cmd) if err != nil { return err } @@ -1968,6 +1981,7 @@ func isScanRunning( resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, resultsWrapper wrappers.ResultsWrapper, risksOverViewWrapper wrappers.RisksOverviewWrapper, + scsScanOverviewWrapper wrappers.ScanOverviewWrapper, scanID string, cmd *cobra.Command, ) (bool, error) { @@ -1996,7 +2010,9 @@ func isScanRunning( resultsSbomWrapper, resultsPdfReportsWrapper, resultsWrapper, - risksOverViewWrapper, nil) // check this partial case, how to handle it + risksOverViewWrapper, + scsScanOverviewWrapper, + nil) // check this partial case, how to handle it if reportErr != nil { return false, errors.New("unable to create report for partial scan") } diff --git a/internal/params/binds.go b/internal/params/binds.go index f522daf70..f766b264f 100644 --- a/internal/params/binds.go +++ b/internal/params/binds.go @@ -20,6 +20,7 @@ var EnvVarsBinds = []struct { {ResultsPathKey, ResultsPathEnv, "api/results"}, {ScanSummaryPathKey, ScanSummaryPathEnv, "api/scan-summary"}, {RisksOverviewPathKey, RisksOverviewPathEnv, "api/apisec/static/api/scan/%s/risks-overview"}, + {ScsScanOverviewPathKey, ScsScanOverviewPathEnv, "api/micro-engines/scans/%s/scan-overview"}, {ScaPackagePathKey, ScaPackagePathEnv, "api/sca/risk-management/risk-reports/"}, {SastResultsPathKey, SastResultsPathEnv, "api/sast-results"}, {SastResultsPredicatesPathKey, SastResultsPredicatesPathEnv, "api/sast-results-predicates"}, diff --git a/internal/params/envs.go b/internal/params/envs.go index 0d1a426d7..6541bca55 100644 --- a/internal/params/envs.go +++ b/internal/params/envs.go @@ -23,6 +23,7 @@ const ( ScanSummaryPathEnv = "CX_SCAN_SUMMARY_PATH" ScaPackagePathEnv = "CX_SCA_PACKAGE_PATH" RisksOverviewPathEnv = "CX_RISKS_OVERVIEW_PATH" + ScsScanOverviewPathEnv = "CX_SCS_SCAN_OVERVIEW_PATH" SastResultsPathEnv = "CX_SAST_RESULTS_PATH" SastResultsPredicatesPathEnv = "CX_SAST_RESULTS_PREDICATES_PATH" KicsResultsPathEnv = "CX_KICS_RESULTS_PATH" diff --git a/internal/params/flags.go b/internal/params/flags.go index 4d3c6b707..5faf66358 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -233,6 +233,7 @@ const ( APISecurityLabel = "API Security" ScaType = "sca" APISecType = "apisec" + ScsType = "scs" Success = "success" ) diff --git a/internal/params/keys.go b/internal/params/keys.go index 0ed4b1f98..7957154a1 100644 --- a/internal/params/keys.go +++ b/internal/params/keys.go @@ -22,6 +22,7 @@ var ( ResultsPathKey = strings.ToLower(ResultsPathEnv) ScanSummaryPathKey = strings.ToLower(ScanSummaryPathEnv) RisksOverviewPathKey = strings.ToLower(RisksOverviewPathEnv) + ScsScanOverviewPathKey = strings.ToLower(ScsScanOverviewPathEnv) SastResultsPathKey = strings.ToLower(SastResultsPathEnv) KicsResultsPathKey = strings.ToLower(KicsResultsPathEnv) BflPathKey = strings.ToLower(BflPathEnv) diff --git a/internal/wrappers/mock/scan-overview-mock.go b/internal/wrappers/mock/scan-overview-mock.go new file mode 100644 index 000000000..afec58d61 --- /dev/null +++ b/internal/wrappers/mock/scan-overview-mock.go @@ -0,0 +1,133 @@ +package mock + +import ( + "github.com/checkmarx/ast-cli/internal/wrappers" +) + +type ScanOverviewMockWrapper struct { + ScanPartial bool + ScorecardScanned bool +} + +func (s ScanOverviewMockWrapper) GetSCSOverviewByScanID(scanID string) ( + *wrappers.SCSOverview, + *wrappers.WebError, + error, +) { + if s.ScanPartial { + return &wrappers.SCSOverview{ + Status: "Partial", + TotalRisksCount: 10, + RiskSummary: map[string]int{ + "critical": 0, + "high": 5, + "medium": 3, + "low": 2, + "info": 0, + }, + MicroEngineOverviews: []*wrappers.MicroEngineOverview{ + { + Name: "2ms", + FullName: "Secret Detection", + Status: "Completed", + TotalRisks: 10, + RiskSummary: map[string]int{ + "critical": 0, + "high": 5, + "medium": 3, + "low": 2, + "info": 0, + }, + }, + { + Name: "Scorecard", + FullName: "Scorecard", + Status: "Failed", + TotalRisks: 0, + RiskSummary: map[string]int{ + "critical": 0, + "high": 0, + "medium": 0, + "low": 0, + "info": 0, + }, + }, + }, + }, nil, nil + } + if s.ScorecardScanned { + return &wrappers.SCSOverview{ + Status: "Completed", + TotalRisksCount: 14, + RiskSummary: map[string]int{ + "critical": 0, + "high": 7, + "medium": 4, + "low": 3, + "info": 0, + }, + MicroEngineOverviews: []*wrappers.MicroEngineOverview{ + { + Name: "2ms", + FullName: "Secret Detection", + Status: "Completed", + TotalRisks: 10, + RiskSummary: map[string]int{ + "critical": 0, + "high": 5, + "medium": 3, + "low": 2, + "info": 0, + }, + }, + { + Name: "Scorecard", + FullName: "Scorecard", + Status: "Completed", + TotalRisks: 4, + RiskSummary: map[string]int{ + "critical": 0, + "high": 2, + "medium": 1, + "low": 1, + "info": 0, + }, + }, + }, + }, nil, nil + } + // default Overview + return &wrappers.SCSOverview{ + Status: "Completed", + TotalRisksCount: 10, + RiskSummary: map[string]int{ + "critical": 0, + "high": 5, + "medium": 3, + "low": 2, + "info": 0, + }, + MicroEngineOverviews: []*wrappers.MicroEngineOverview{ + { + Name: "2ms", + FullName: "Secret Detection", + Status: "Completed", + TotalRisks: 10, + RiskSummary: map[string]int{ + "critical": 0, + "high": 5, + "medium": 3, + "low": 2, + "info": 0, + }, + }, + { + Name: "Scorecard", + FullName: "Scorecard", + Status: "", + TotalRisks: 0, + RiskSummary: map[string]int{}, + }, + }, + }, nil, nil +} diff --git a/internal/wrappers/mock/scans-mock.go b/internal/wrappers/mock/scans-mock.go index 088e4d9fe..f69b70339 100644 --- a/internal/wrappers/mock/scans-mock.go +++ b/internal/wrappers/mock/scans-mock.go @@ -11,6 +11,7 @@ import ( type ScansMockWrapper struct { Running bool + HasSCS bool } func (m *ScansMockWrapper) GetWorkflowByID(_ string) ([]*wrappers.ScanTaskResponseModel, *wrappers.ErrorModel, error) { @@ -80,10 +81,14 @@ func (m *ScansMockWrapper) GetByID(scanID string) (*wrappers.ScanResponseModel, fmt.Println("Called GetByID in ScansMockWrapper") var status wrappers.ScanStatus = "Completed" m.Running = !m.Running + engines := []string{params.ScaType, params.SastType, params.KicsType} + if m.HasSCS { + engines = append(engines, params.ScsType) + } return &wrappers.ScanResponseModel{ ID: scanID, Status: status, - Engines: []string{params.ScaType, params.SastType, params.KicsType}, + Engines: engines, }, nil, nil } diff --git a/internal/wrappers/results-summary.go b/internal/wrappers/results-summary.go index 599111344..05e90b3f5 100644 --- a/internal/wrappers/results-summary.go +++ b/internal/wrappers/results-summary.go @@ -16,7 +16,9 @@ type ResultSummary struct { SastIssues int KicsIssues int ScaIssues int + ScsIssues int APISecurity APISecResult + SCSOverview SCSOverview RiskStyle string RiskMsg string Status string @@ -43,10 +45,27 @@ type APISecResult struct { RiskDistribution []riskDistribution `json:"risk_distribution,omitempty"` StatusCode int } + type riskDistribution struct { Origin string `json:"origin,omitempty"` Total int `json:"total,omitempty"` } + +type SCSOverview struct { + Status ScanStatus `json:"status"` + TotalRisksCount int `json:"totalRisks"` + RiskSummary map[string]int `json:"riskSummary"` + MicroEngineOverviews []*MicroEngineOverview `json:"engineOverviews"` +} + +type MicroEngineOverview struct { + Name string `json:"name"` + FullName string `json:"fullName"` + Status ScanStatus `json:"status"` + TotalRisks int `json:"totalRisks"` + RiskSummary map[string]int `json:"riskSummary"` +} + type EngineResultSummary struct { High int Medium int @@ -102,8 +121,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 { @@ -119,6 +138,10 @@ func (r *ResultSummary) HasAPISecurity() bool { return r.HasEngine(params.APISecType) } +func (r *ResultSummary) HasSCS() bool { + return r.HasEngine(params.ScsType) +} + func (r *ResultSummary) getRiskFromAPISecurity(origin string) *riskDistribution { for _, risk := range r.APISecurity.RiskDistribution { if strings.EqualFold(risk.Origin, origin) { diff --git a/internal/wrappers/scan-overview-http.go b/internal/wrappers/scan-overview-http.go new file mode 100644 index 000000000..9768d8c00 --- /dev/null +++ b/internal/wrappers/scan-overview-http.go @@ -0,0 +1,60 @@ +package wrappers + +import ( + "encoding/json" + "fmt" + "net/http" + + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +type ScanOverviewHTTPWrapper struct { + path string +} + +func NewHTTPScanOverviewWrapper(path string) ScanOverviewWrapper { + return &ScanOverviewHTTPWrapper{ + path: path, + } +} + +func (r *ScanOverviewHTTPWrapper) GetSCSOverviewByScanID(scanID string) ( + *SCSOverview, + *WebError, + error, +) { + clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) + path := fmt.Sprintf(r.path, scanID) + resp, err := SendHTTPRequest(http.MethodGet, path, http.NoBody, true, clientTimeout) + if err != nil { + return nil, nil, err + } + + defer func() { + if err == nil { + _ = resp.Body.Close() + } + }() + decoder := json.NewDecoder(resp.Body) + + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := WebError{} + err = decoder.Decode(&errorModel) + if err != nil { + return nil, nil, errors.Wrapf(err, failedToParseGetResults) + } + return nil, &errorModel, nil + case http.StatusOK: + model := SCSOverview{} + err = decoder.Decode(&model) + if err != nil { + return nil, nil, errors.Wrapf(err, failedToParseGetResults) + } + return &model, nil, nil + default: + return nil, nil, errors.Errorf("response status code %d", resp.StatusCode) + } +} diff --git a/internal/wrappers/scan-overview.go b/internal/wrappers/scan-overview.go new file mode 100644 index 000000000..ded345579 --- /dev/null +++ b/internal/wrappers/scan-overview.go @@ -0,0 +1,5 @@ +package wrappers + +type ScanOverviewWrapper interface { + GetSCSOverviewByScanID(scanID string) (*SCSOverview, *WebError, error) +} diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 0ecec9c58..cc813eca4 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -67,6 +67,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { scanSummmaryPath := viper.GetString(params.ScanSummaryPathKey) scaPackage := viper.GetString(params.ScaPackagePathKey) risksOverview := viper.GetString(params.RisksOverviewPathKey) + scsScanOverviewPath := viper.GetString(params.ScsScanOverviewPathKey) uploads := viper.GetString(params.UploadsPathKey) logs := viper.GetString(params.LogsPathKey) codebashing := viper.GetString(params.CodeBashingPathKey) @@ -94,6 +95,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { projectsWrapper := wrappers.NewHTTPProjectsWrapper(projects) resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scaPackage, scanSummmaryPath) risksOverviewWrapper := wrappers.NewHTTPRisksOverviewWrapper(risksOverview) + scsScanOverviewWrapper := wrappers.NewHTTPScanOverviewWrapper(scsScanOverviewPath) authWrapper := wrappers.NewAuthHTTPWrapper() logsWrapper := wrappers.NewLogsWrapper(logs) codeBashingWrapper := wrappers.NewCodeBashingHTTPWrapper(codebashing) @@ -124,6 +126,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { projectsWrapper, resultsWrapper, risksOverviewWrapper, + scsScanOverviewWrapper, authWrapper, logsWrapper, groupsWrapper,