diff --git a/Dockerfile b/Dockerfile index cca0b0fd..495483dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM checkmarx/bash:5.3-r12-f48dd8a45af577@sha256:f48dd8a45af5771e98cb5d56d204ada0e0dc045093ca3272b4c3dbe3f85e6e4f +FROM checkmarx/bash:5.3-r12-02a1aad732e7ab@sha256:02a1aad732e7ab0659b212d83c2a0bb548d9d8bdec23336f6c0b44f8f3435cb8 USER nonroot COPY cx /app/bin/cx diff --git a/cmd/main.go b/cmd/main.go index ae5d46ce..aae6c2c2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -75,6 +75,7 @@ func main() { risksOverviewWrapper := wrappers.NewHTTPRisksOverviewWrapper(risksOverview, apiSecurityResult) riskManagementWrapper := wrappers.NewHTTPRiskManagementWrapper(riskManagement) scsScanOverviewWrapper := wrappers.NewHTTPScanOverviewWrapper(scsScanOverview) + scanSummaryWrapper := wrappers.NewHTTPScanSummaryWrapper(scanSummary) resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scanSummary) authWrapper := wrappers.NewAuthHTTPWrapper() resultsPredicatesWrapper := wrappers.NewResultsPredicatesHTTPWrapper() @@ -116,6 +117,7 @@ func main() { risksOverviewWrapper, riskManagementWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, authWrapper, logsWrapper, groupsWrapper, diff --git a/internal/commands/result.go b/internal/commands/result.go index 51a155a6..378022fe 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -206,6 +206,7 @@ func NewResultsCommand( risksOverviewWrapper wrappers.RisksOverviewWrapper, riskManagementWrapper wrappers.RiskManagementWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -222,7 +223,7 @@ func NewResultsCommand( }, } showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, - risksOverviewWrapper, scsScanOverviewWrapper, policyWrapper, featureFlagsWrapper, jwtWrapper) + risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, policyWrapper, featureFlagsWrapper, jwtWrapper) codeBashingCmd := resultCodeBashing(codeBashingWrapper) bflResultCmd := resultBflSubCommand(bflWrapper) exitCodeSubcommand := exitCodeSubCommand(scanWrapper) @@ -282,6 +283,7 @@ func resultShowSubCommand( resultsJSONReportsWrapper wrappers.ResultsJSONWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -295,7 +297,7 @@ func resultShowSubCommand( $ cx results show --scan-id `, ), - RunE: runGetResultCommand(resultsWrapper, scanWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, policyWrapper, featureFlagsWrapper, jwtWrapper), + RunE: runGetResultCommand(resultsWrapper, scanWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, policyWrapper, featureFlagsWrapper, jwtWrapper), } addScanIDFlag(resultShowCmd, "ID to report on") addResultFormatFlag( @@ -727,6 +729,7 @@ func summaryReport( policies *wrappers.PolicyResponseModel, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, results *wrappers.ScanResultsCollection, resultsParams map[string]string, @@ -756,6 +759,15 @@ func summaryReport( summary.SCSOverview = SCSOverview } + if summary.HasAISC() { + // Getting AISC information from scan-summary API + aiscInfo, err := getAISCInfoFromScanSummary(scanSummaryWrapper, summary.ScanID) + if err != nil { + return nil, err + } + summary.AISCInfo = aiscInfo + } + if policies != nil { summary.Policies = filterViolatedRules(*policies) } @@ -924,6 +936,10 @@ func writeConsoleSummary(summary *wrappers.ResultSummary, ignorePolicyFlagOmit b printSCSSummary(summary.SCSOverview.MicroEngineOverviews) } + if summary.HasAISC() { + printAISCSummary(summary) + } + 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") @@ -1013,6 +1029,15 @@ func printSCSTableRow(microEngineOverview *wrappers.MicroEngineOverview) { } } +func printAISCSummary(summary *wrappers.ResultSummary) { + fmt.Printf(" AI SUPPLY CHAIN ENGINE SUMMARY\n") + fmt.Printf(" --------------------------------------------------------------------- \n") + fmt.Printf(" | %-32s %30s |\n", "Category", "Count") + fmt.Printf(" | %-32s %30d |\n", "Total Assets", summary.AISCAssetsValue()) + fmt.Printf(" | %-32s %30d |\n", "Total Asset Types", summary.AISCAssetTypesValue()) + fmt.Printf(" --------------------------------------------------------------------- \n\n") +} + func getCountValue(count int) interface{} { if count < 0 { return disabledString @@ -1063,6 +1088,7 @@ func runGetResultCommand( resultsJSONReportsWrapper wrappers.ResultsJSONWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -1129,7 +1155,7 @@ func runGetResultCommand( resultsParams[commonParams.SastRedundancyFlag] = "" } - _, err = CreateScanReport(resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, exportWrapper, + _, err = CreateScanReport(resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, exportWrapper, policyResponseModel, resultsPdfReportsWrapper, resultsJSONReportsWrapper, scan, format, formatPdfToEmail, formatPdfOptions, formatSbomOptions, targetFile, targetPath, agent, resultsParams, featureFlagsWrapper, ignorePolicyFlagOmit) return err @@ -1220,6 +1246,7 @@ func CreateScanReport( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, exportWrapper wrappers.ExportWrapper, policyResponseModel *wrappers.PolicyResponseModel, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, @@ -1257,7 +1284,7 @@ func CreateScanReport( } isSummaryNeeded := verifyFormatsByReportList(reportList, summaryFormats...) if isSummaryNeeded && !scanPending { - summary, err = summaryReport(summary, policyResponseModel, risksOverviewWrapper, scsScanOverviewWrapper, featureFlagsWrapper, results, resultsParams) + summary, err = summaryReport(summary, policyResponseModel, risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, featureFlagsWrapper, results, resultsParams) if err != nil { return nil, err } @@ -1413,6 +1440,30 @@ func getScanOverviewForSCSScanner( return nil, nil } +func getAISCInfoFromScanSummary( + scanSummaryWrapper wrappers.ScanSummaryWrapper, + scanID string, +) (*wrappers.AISCInfo, error) { + var scanSummaryModel *wrappers.ScanSummariesModel + var errorModel *wrappers.WebError + + scanSummaryModel, errorModel, err := scanSummaryWrapper.GetScanSummaryByScanID(scanID) + if err != nil { + return nil, errors.Wrapf(err, "AISC: %s", failedListingResults) + } + if errorModel != nil { + return nil, errors.Errorf("AISC: %s: CODE: %d, %s", failedListingResults, errorModel.Code, errorModel.Message) + } + if scanSummaryModel != nil && len(scanSummaryModel.ScansSummaries) > 0 { + aiscCounters := scanSummaryModel.ScansSummaries[0].AiscCounters + return &wrappers.AISCInfo{ + TotalAssets: aiscCounters.AssetsCounter, // Map from API response + TotalAssetTypes: aiscCounters.AssetTypesCounter, // Map from API response + }, nil + } + return nil, nil +} + func isScanPending(scanStatus string) bool { return !strings.EqualFold(scanStatus, statusCompleted) && !strings.EqualFold(scanStatus, statusPartial) && diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index 133d9051..dff116ed 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -18,6 +18,7 @@ import ( "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "github.com/pkg/errors" assertion "github.com/stretchr/testify/assert" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -1741,3 +1742,140 @@ func TestGetFilterResultsForAPISecScanner(t *testing.T) { } } } + +func TestGetAISCInfoFromScanSummary_Success(t *testing.T) { + mockWrapper := &mock.ScanSummaryMockWrapper{} + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.NilError(t, err) + assert.Assert(t, result != nil, "Expected non-nil result") + if result == nil { + return + } + assert.Equal(t, result.TotalAssets, 0, "Expected TotalAssets to be 0") + assert.Equal(t, result.TotalAssetTypes, 0, "Expected TotalAssetTypes to be 0") +} + +func TestGetAISCInfoFromScanSummary_WithNonZeroValues(t *testing.T) { + // Create a custom mock wrapper with non-zero values + mockWrapper := &customScanSummaryMockWrapper{ + assetsCounter: 10, + assetTypesCounter: 5, + } + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.NilError(t, err) + assert.Assert(t, result != nil, "Expected non-nil result") + if result == nil { + return + } + assert.Equal(t, result.TotalAssets, 10, "Expected TotalAssets to be 10") + assert.Equal(t, result.TotalAssetTypes, 5, "Expected TotalAssetTypes to be 5") +} + +func TestGetAISCInfoFromScanSummary_EmptyScansSummaries(t *testing.T) { + mockWrapper := &customScanSummaryMockWrapper{ + emptySummaries: true, + } + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.NilError(t, err) + assert.Assert(t, result == nil, "Expected nil result for empty summaries") +} + +func TestGetAISCInfoFromScanSummary_NilScanSummaryModel(t *testing.T) { + mockWrapper := &customScanSummaryMockWrapper{ + nilModel: true, + } + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.NilError(t, err) + assert.Assert(t, result == nil, "Expected nil result for nil scan summary model") +} + +func TestGetAISCInfoFromScanSummary_Error(t *testing.T) { + mockWrapper := &customScanSummaryMockWrapper{ + returnError: true, + } + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.Assert(t, err != nil, "Expected error") + assert.Assert(t, result == nil, "Expected nil result on error") + if err == nil { + return + } + assert.Assert(t, strings.Contains(err.Error(), "AISC"), "Expected error message to contain 'AISC'") + assert.Assert(t, strings.Contains(err.Error(), failedListingResults), "Expected error message to contain failedListingResults") +} + +func TestGetAISCInfoFromScanSummary_WebError(t *testing.T) { + mockWrapper := &customScanSummaryMockWrapper{ + returnWebError: true, + } + scanID := "test-scan-id" + + result, err := getAISCInfoFromScanSummary(mockWrapper, scanID) + + assert.Assert(t, err != nil, "Expected error") + assert.Assert(t, result == nil, "Expected nil result on web error") + if err == nil { + return + } + assert.Assert(t, strings.Contains(err.Error(), "AISC"), "Expected error message to contain 'AISC'") + assert.Assert(t, strings.Contains(err.Error(), failedListingResults), "Expected error message to contain failedListingResults") + assert.Assert(t, strings.Contains(err.Error(), "CODE: 400"), "Expected error message to contain error code") + assert.Assert(t, strings.Contains(err.Error(), "Bad Request"), "Expected error message to contain error message") +} + +// Custom mock wrapper for testing different scenarios +type customScanSummaryMockWrapper struct { + assetsCounter int + assetTypesCounter int + emptySummaries bool + nilModel bool + returnError bool + returnWebError bool +} + +func (m *customScanSummaryMockWrapper) GetScanSummaryByScanID(scanID string) (*wrappers.ScanSummariesModel, *wrappers.WebError, error) { + if m.returnError { + return nil, nil, errors.New("mock error from GetScanSummaryByScanID") + } + if m.returnWebError { + return nil, &wrappers.WebError{ + Code: 400, + Message: "Bad Request", + }, nil + } + if m.nilModel { + return nil, nil, nil + } + if m.emptySummaries { + return &wrappers.ScanSummariesModel{ + ScansSummaries: []wrappers.ScanSumaries{}, + TotalCount: 0, + }, nil, nil + } + return &wrappers.ScanSummariesModel{ + ScansSummaries: []wrappers.ScanSumaries{ + { + ScanID: scanID, + AiscCounters: wrappers.AiscCounters{ + AssetsCounter: m.assetsCounter, + AssetTypesCounter: m.assetTypesCounter, + }, + }, + }, + TotalCount: 1, + }, nil, nil +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 6f350303..b42e1fc5 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -41,6 +41,7 @@ func NewAstCLI( risksOverviewWrapper wrappers.RisksOverviewWrapper, riskManagementWrapper wrappers.RiskManagementWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, authWrapper wrappers.AuthWrapper, logsWrapper wrappers.LogsWrapper, groupsWrapper wrappers.GroupsWrapper, @@ -189,6 +190,7 @@ func NewAstCLI( groupsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, jwtWrapper, scaRealTimeWrapper, policyWrapper, @@ -213,6 +215,7 @@ func NewAstCLI( risksOverviewWrapper, riskManagementWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, policyWrapper, featureFlagsWrapper, jwtWrapper, diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index cb17fe09..9553f324 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -50,6 +50,7 @@ func createASTTestCommand() *cobra.Command { risksOverviewMockWrapper := &mock.RisksOverviewMockWrapper{} riskManagementMockWrapper := &mock.RiskManagementMockWrapper{} scsScanOverviewMockWrapper := &mock.ScanOverviewMockWrapper{} + scanSummaryMockWrapper := &mock.ScanSummaryMockWrapper{} authWrapper := &mock.AuthMockWrapper{} logsWrapper := &mock.LogsMockWrapper{} codeBashingWrapper := &mock.CodeBashingMockWrapper{} @@ -89,6 +90,7 @@ func createASTTestCommand() *cobra.Command { risksOverviewMockWrapper, riskManagementMockWrapper, scsScanOverviewMockWrapper, + scanSummaryMockWrapper, authWrapper, logsWrapper, groupsMockWrapper, diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 7f8f7f3e..583c596b 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -185,6 +185,7 @@ func NewScanCommand( groupsWrapper wrappers.GroupsWrapper, riskOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, scaRealTimeWrapper wrappers.ScaRealTimeWrapper, policyWrapper wrappers.PolicyWrapper, @@ -219,6 +220,7 @@ func NewScanCommand( groupsWrapper, riskOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -679,6 +681,7 @@ func scanCreateSubCommand( groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -713,6 +716,7 @@ func scanCreateSubCommand( groupsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -788,7 +792,7 @@ func scanCreateSubCommand( ) createScanCmd.PersistentFlags().Bool(commonParams.ContainerResolveLocallyFlag, false, "Execute container resolver locally.") createScanCmd.PersistentFlags().String(commonParams.ContainerImagesFlag, "", "List of container images to scan, ex: manuelbcd/vulnapp:latest,debian:10") - createScanCmd.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types, ex: (sast,iac-security,sca,api-security)") + createScanCmd.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types, ex: (sast,iac-security,sca,api-security,aisc)") createScanCmd.PersistentFlags().String(commonParams.TagList, "", "List of tags, ex: (tagA,tagB:val,etc)") createScanCmd.PersistentFlags().StringP( @@ -1046,6 +1050,11 @@ func setupScanTypeProjectAndConfig( configArr = append(configArr, SCSConfig) } + var aiscConfig = addAiscScan(featureFlagsWrapper, resubmitConfig) + if aiscConfig != nil { + configArr = append(configArr, aiscConfig) + } + info["config"] = configArr var err2 error *input, err2 = json.Marshal(info) @@ -1173,6 +1182,25 @@ func overrideSastConfigValue(sastFastScanChanged, sastIncrementalChanged, sastLi } } +func addAiscScan(featureFlagWrapper wrappers.FeatureFlagsWrapper, resubmitConfig []wrappers.Config) map[string]interface{} { + // Add the aisc resubmit config, currently no value is passed in config + aiSupplyChainEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagWrapper, wrappers.AISupplyChainEnabled) + aiSupplyChainGAEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagWrapper, wrappers.AISupplyChainGAEnabled) + if scanTypeEnabled(commonParams.AiscType) && aiSupplyChainEnabled.Status && aiSupplyChainGAEnabled.Status { + aiscMapConfig := make(map[string]interface{}) + aiscConfig := wrappers.AISCConfig{} + aiscMapConfig[resultsMapType] = commonParams.AiscType + aiscMapConfig[resultsMapValue] = &aiscConfig + for _, config := range resubmitConfig { + if config.Type == commonParams.AiscType && config.Value == nil { + continue + } + } + return aiscMapConfig + } + return nil +} + func addKicsScan(cmd *cobra.Command, resubmitConfig []wrappers.Config) map[string]interface{} { if scanTypeEnabled(commonParams.KicsType) { kicsMapConfig := make(map[string]interface{}) @@ -1513,6 +1541,7 @@ func validateScanTypes(cmd *cobra.Command, jwtWrapper wrappers.JWTWrapper, featu scsLicensingV2Flag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.ScsLicensingV2Enabled) allowedEngines, err := jwtWrapper.GetAllowedEngines(featureFlagsWrapper) + logger.PrintIfVerbose(fmt.Sprintf("Allowed scan types: %v", allowedEngines)) isSbomScan, _ := cmd.PersistentFlags().GetBool(commonParams.SbomFlag) @@ -2544,6 +2573,7 @@ func runCreateScanCommand( groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -2614,6 +2644,7 @@ func runCreateScanCommand( jwtWrapper, tenantWrapper, ) + defer cleanUpTempZip(zipFilePath) if err != nil { return errors.Errorf("%s", err) @@ -2654,6 +2685,7 @@ func runCreateScanCommand( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, featureFlagsWrapper, ignorePolicyFlagOmit) if err != nil { @@ -2668,7 +2700,7 @@ func runCreateScanCommand( } results, reportErr := createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, - resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, policyResponseModel, featureFlagsWrapper, ignorePolicyFlagOmit) + resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, policyResponseModel, featureFlagsWrapper, ignorePolicyFlagOmit) if reportErr != nil { return reportErr } @@ -2680,7 +2712,7 @@ func runCreateScanCommand( } } else { _, err = createReportsAfterScan(cmd, scanResponseModel.ID, scansWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, resultsWrapper, - risksOverviewWrapper, scsScanOverviewWrapper, nil, featureFlagsWrapper, ignorePolicyFlagOmit) + risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, nil, featureFlagsWrapper, ignorePolicyFlagOmit) if err != nil { return err } @@ -2732,6 +2764,7 @@ func createScanModel( scanModel := wrappers.Scan{} // Try to parse to a scan model in order to manipulate the request payload err = json.Unmarshal(input, &scanModel) + if err != nil { return nil, "", errors.Wrapf(err, "%s: Input in bad format", failedCreating) } @@ -2844,6 +2877,7 @@ func handleWait( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, ) error { @@ -2858,6 +2892,7 @@ func handleWait( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, cmd, featureFlagsWrapper, ignorePolicyFlagOmit) @@ -2883,6 +2918,7 @@ func createReportsAfterScan( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyResponseModel *wrappers.PolicyResponseModel, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, @@ -2920,6 +2956,7 @@ func createReportsAfterScan( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, exportWrapper, policyResponseModel, resultsPdfReportsWrapper, @@ -3095,6 +3132,7 @@ func waitForScanCompletion( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, cmd *cobra.Command, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, @@ -3112,7 +3150,7 @@ func waitForScanCompletion( logger.PrintfIfVerbose("Sleeping %v before polling", waitDuration) time.Sleep(waitDuration) running, err := isScanRunning(scansWrapper, exportWrapper, resultsPdfReportsWrapper, resultsJSONReportsWrapper, resultsWrapper, - risksOverviewWrapper, scsScanOverviewWrapper, scanResponseModel.ID, cmd, featureFlagsWrapper, ignorePolicyFlagOmit) + risksOverviewWrapper, scsScanOverviewWrapper, scanSummaryWrapper, scanResponseModel.ID, cmd, featureFlagsWrapper, ignorePolicyFlagOmit) if err != nil { return err } @@ -3144,6 +3182,7 @@ func isScanRunning( resultsWrapper wrappers.ResultsWrapper, risksOverViewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, scanID string, cmd *cobra.Command, featureFlagsWrapper wrappers.FeatureFlagsWrapper, @@ -3179,6 +3218,7 @@ func isScanRunning( resultsWrapper, risksOverViewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, nil, featureFlagsWrapper, ignorePolicyFlagOmit) // 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/commands/scan_test.go b/internal/commands/scan_test.go index 6dda0c5f..971e3d02 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -830,6 +830,117 @@ func TestAddScaScan(t *testing.T) { t.Errorf("Expected %+v, but got %+v", scaMapConfig, result) } } + +func TestAddAiscScan_WhenAiscEnabledAndFeatureFlagEnabled_ShouldReturnConfig(t *testing.T) { + wrappers.ClearCache() + var resubmitConfig []wrappers.Config + + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.AISupplyChainEnabled, + Status: true, + } + defer clearFlags() + originalScanTypes := actualScanTypes + actualScanTypes = commonParams.SastType + "," + commonParams.KicsType + "," + commonParams.ScaType + "," + commonParams.AiscType + defer func() { actualScanTypes = originalScanTypes }() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result := addAiscScan(featureFlagsWrapper, resubmitConfig) + + assert.Assert(t, result != nil, "Expected non-nil result when AISC is enabled and feature flag is true") + + assert.Assert(t, result[resultsMapType] == commonParams.AiscType, + "Expected type '%s', got '%v'", commonParams.AiscType, result[resultsMapType]) + + configValue := result[resultsMapValue] + assert.Assert(t, configValue != nil, "Expected non-nil config value") + _, ok := configValue.(*wrappers.AISCConfig) + assert.Assert(t, ok, "Expected config value to be *wrappers.AISCConfig") +} + +func TestAddAiscScan_WhenAiscDisabled_ShouldReturnNil(t *testing.T) { + wrappers.ClearCache() + var resubmitConfig []wrappers.Config + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.AISupplyChainEnabled, + Status: true, + } + defer clearFlags() + originalScanTypes := actualScanTypes + actualScanTypes = commonParams.SastType + "," + commonParams.KicsType + "," + commonParams.ScaType + defer func() { actualScanTypes = originalScanTypes }() + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result := addAiscScan(featureFlagsWrapper, resubmitConfig) + assert.Assert(t, result == nil, "Expected nil result when AISC is disabled in scan types") +} + +func TestAddAiscScan_WhenFeatureFlagDisabled_ShouldReturnNil(t *testing.T) { + wrappers.ClearCache() + var resubmitConfig []wrappers.Config + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.AISupplyChainEnabled, + Status: false, + } + defer clearFlags() + originalScanTypes := actualScanTypes + actualScanTypes = commonParams.SastType + "," + commonParams.KicsType + "," + commonParams.ScaType + "," + commonParams.AiscType + defer func() { actualScanTypes = originalScanTypes }() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result := addAiscScan(featureFlagsWrapper, resubmitConfig) + assert.Assert(t, result == nil, "Expected nil result when feature flag is disabled") +} + +func TestAddAiscScan_WithResubmitConfig_ShouldHandleCorrectly(t *testing.T) { + wrappers.ClearCache() + resubmitConfig := []wrappers.Config{ + { + Type: commonParams.AiscType, + Value: map[string]interface{}{}, + }, + } + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.AISupplyChainEnabled, + Status: true, + } + defer clearFlags() + originalScanTypes := actualScanTypes + actualScanTypes = commonParams.SastType + "," + commonParams.KicsType + "," + commonParams.ScaType + "," + commonParams.AiscType + defer func() { actualScanTypes = originalScanTypes }() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result := addAiscScan(featureFlagsWrapper, resubmitConfig) + assert.Assert(t, result != nil, "Expected non-nil result with resubmit config") + assert.Assert(t, result[resultsMapType] == commonParams.AiscType, + "Expected type '%s'", commonParams.AiscType) +} + +func TestAddAiscScan_ConfigStructure_ShouldHaveCorrectFormat(t *testing.T) { + wrappers.ClearCache() + var resubmitConfig []wrappers.Config + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.AISupplyChainEnabled, + Status: true, + } + defer clearFlags() + originalScanTypes := actualScanTypes + actualScanTypes = commonParams.SastType + "," + commonParams.KicsType + "," + commonParams.ScaType + "," + commonParams.AiscType + defer func() { actualScanTypes = originalScanTypes }() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result := addAiscScan(featureFlagsWrapper, resubmitConfig) + + expectedMapConfig := make(map[string]interface{}) + expectedMapConfig[resultsMapType] = commonParams.AiscType + expectedMapConfig[resultsMapValue] = &wrappers.AISCConfig{} + + assert.Equal(t, result[resultsMapType], expectedMapConfig[resultsMapType], + "Type field should match") + + _, ok := result[resultsMapValue].(*wrappers.AISCConfig) + assert.Assert(t, ok, "Expected result value to be *wrappers.AISCConfig") +} + func TestAddSCSScan_ResubmitWithoutScorecardFlags_ShouldPass(t *testing.T) { tests := []struct { name string diff --git a/internal/params/flags.go b/internal/params/flags.go index d2b1ccac..896609c6 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -310,6 +310,7 @@ const ( const ( SastType = "sast" KicsType = "kics" + AiscType = "aisc" APISecurityType = "api-security" AIProtectionType = "AI Protection" CheckmarxOneAssistType = "Checkmarx One Assist" diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index 798256a4..a5718fbd 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -22,6 +22,12 @@ const maxRetries = 3 const IncreaseFileUploadLimit = "INCREASE_FILE_UPLOAD_LIMIT" const ScaDeltaScanEnabled = "SCA_DELTASCAN_ENABLED" +// AISupplyChainEnabled is the feature flag for AI Supply Chain Engine. +const AISupplyChainEnabled = "AI_SUPPLY_CHAIN_ENGINE_ENABLED" + +// AISupplyChainGAEnabled is the feature flag for AI Supply Chain Engine GA. +const AISupplyChainGAEnabled = "AI_SUPPLY_CHAIN_ENGINE_GA_ENABLED" + var DefaultFFLoad bool = false var FeatureFlagsBaseMap = []CommandFlags{ diff --git a/internal/wrappers/jwt-helper.go b/internal/wrappers/jwt-helper.go index 8393c6b4..566a8830 100644 --- a/internal/wrappers/jwt-helper.go +++ b/internal/wrappers/jwt-helper.go @@ -41,7 +41,7 @@ func NewJwtWrapper() JWTWrapper { } func getEnabledEngines(scsLicensingV2 bool) (enabledEngines []string) { - enabledEngines = []string{"sast", "sca", "api-security", "iac-security", "containers"} + enabledEngines = []string{"sast", "sca", "api-security", "iac-security", "containers", "aisc"} if scsLicensingV2 { enabledEngines = append(enabledEngines, commonParams.RepositoryHealthType, commonParams.SecretDetectionType) } else { @@ -57,6 +57,7 @@ func getDefaultEngines(scsLicensingV2 bool) (defaultEngines map[string]bool) { "api-security": true, "iac-security": true, "containers": true, + "aisc": true, } if scsLicensingV2 { defaultEngines[commonParams.RepositoryHealthType] = true diff --git a/internal/wrappers/mock/scan-summary-mock.go b/internal/wrappers/mock/scan-summary-mock.go new file mode 100644 index 00000000..7aba26aa --- /dev/null +++ b/internal/wrappers/mock/scan-summary-mock.go @@ -0,0 +1,25 @@ +package mock + +import ( + "github.com/checkmarx/ast-cli/internal/wrappers" +) + +// ScanSummaryMockWrapper is a mock implementation of ScanSummaryWrapper. +type ScanSummaryMockWrapper struct{} + +// GetScanSummaryByScanID returns mock scan summary data with empty AISC counters. +func (s *ScanSummaryMockWrapper) GetScanSummaryByScanID(scanID string) (*wrappers.ScanSummariesModel, *wrappers.WebError, error) { + // Return mock scan summary data with empty AISC counters + return &wrappers.ScanSummariesModel{ + ScansSummaries: []wrappers.ScanSumaries{ + { + ScanID: scanID, + AiscCounters: wrappers.AiscCounters{ + AssetsCounter: 0, + AssetTypesCounter: 0, + }, + }, + }, + TotalCount: 1, + }, nil, nil +} diff --git a/internal/wrappers/results-summary.go b/internal/wrappers/results-summary.go index 26c47bdf..2e3bc303 100644 --- a/internal/wrappers/results-summary.go +++ b/internal/wrappers/results-summary.go @@ -21,6 +21,7 @@ type ResultSummary struct { ScsIssues *int `json:"ScsIssues,omitempty"` SCSOverview *SCSOverview `json:"ScsOverview,omitempty"` APISecurity APISecFilteredResult + AISCInfo *AISCInfo `json:"AiscInfo,omitempty"` RiskStyle string RiskMsg string Status string @@ -74,6 +75,12 @@ type MicroEngineOverview struct { RiskSummary map[string]interface{} `json:"riskSummary"` } +// AISCInfo contains information about AI Supply Chain Engine assets and types. +type AISCInfo struct { + TotalAssets int `json:"TotalAssets"` + TotalAssetTypes int `json:"TotalAssetTypes"` +} + type EngineResultSummary struct { Critical int High int @@ -172,6 +179,27 @@ func (r *ResultSummary) SCSIssuesValue() int { return *r.ScsIssues } +// HasAISC checks if AISC engine is enabled. +func (r *ResultSummary) HasAISC() bool { + return r.HasEngine(params.AiscType) +} + +// AISCAssetsValue returns the total number of AISC assets. +func (r *ResultSummary) AISCAssetsValue() int { + if r.AISCInfo != nil { + return r.AISCInfo.TotalAssets + } + return 0 +} + +// AISCAssetTypesValue returns the total number of AISC asset types. +func (r *ResultSummary) AISCAssetTypesValue() int { + if r.AISCInfo != nil { + return r.AISCInfo.TotalAssetTypes + } + return 0 +} + func (r *ResultSummary) getRiskFromAPISecurity(origin string) *RiskDistributionEntry { for _, risk := range r.APISecurity.RiskDistribution { if strings.EqualFold(risk.Origin, origin) { @@ -816,6 +844,18 @@ const nonAsyncSummary = `
{{.APISecurity.TotalRisksCount}}
+ {{end}} + {{if .HasAISC}} +
+
+
Total Assets
+
{{.AISCAssetsValue}}
+
+
+
Total Asset Types
+
{{.AISCAssetTypesValue}}
+
+
{{end}}` const asyncSummaryTemplate = `
@@ -883,6 +923,14 @@ const SummaryMarkdownCompletedTemplate = ` |:---------:|:---------:| {{if .HasAPISecurityDocumentation}}:---------:|{{end}} | {{.APISecurity.APICount}} | {{.APISecurity.TotalRisksCount}} | {{if .HasAPISecurityDocumentation}} {{.GetAPISecurityDocumentationTotal}} |{{end}} {{end}} + +{{if .HasAISC}} +### AI Supply Chain Engine + +| Total Assets | Total Asset Types | +|:---------:|:---------:| +| {{.AISCAssetsValue}} | {{.AISCAssetTypesValue}} | +{{end}} ` func SummaryMarkdownTemplate(isScanPending bool) string { diff --git a/internal/wrappers/results.go b/internal/wrappers/results.go index 80e2b2b1..e868103c 100644 --- a/internal/wrappers/results.go +++ b/internal/wrappers/results.go @@ -7,41 +7,143 @@ type ResultsWrapper interface { // ScanSummariesModel model used to parse the response from the scan-summary API type ScanSummariesModel struct { - ScansSummaries []ScanSumaries `json:"scansSummaries,omitempty,"` - TotalCount int `json:"totalCount,omitempty,"` + ScansSummaries []ScanSumaries `json:"scansSummaries,omitempty"` + TotalCount int `json:"totalCount,omitempty"` } type ScanSumaries struct { - SastCounters SastCounters `json:"sastCounters,omitempty,"` - KicsCounters KicsCounters `json:"kicsCounters,omitempty,"` - ScaCounters ScaCounters `json:"scaCounters,omitempty,"` - ScaContainersCounters ScaContainersCounters `json:"scaContainersCounters,omitempty,"` + TenantID string `json:"tenantId,omitempty"` + ScanID string `json:"scanId,omitempty"` + SastCounters SastCounters `json:"sastCounters,omitempty"` + KicsCounters KicsCounters `json:"kicsCounters,omitempty"` + ScaCounters ScaCounters `json:"scaCounters,omitempty"` + ScaPackagesCounters ScaPackagesCounters `json:"scaPackagesCounters,omitempty"` + ScaContainersCounters ScaContainersCounters `json:"scaContainersCounters,omitempty"` + ApiSecCounters ApiSecCounters `json:"apiSecCounters,omitempty"` + MicroEnginesCounters MicroEnginesCounters `json:"microEnginesCounters,omitempty"` + ContainersCounters ContainersCounters `json:"containersCounters,omitempty"` + AiscCounters AiscCounters `json:"aiscCounters,omitempty"` } type SastCounters struct { - SeverityCounters []SeverityCounters `json:"SeverityCounters,omitempty,"` - TotalCounter int `json:"totalCounter,omitempty,"` - FilesScannedCounter int `json:"filesScannedCounter,omitempty,"` + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + FilesScannedCounter int `json:"filesScannedCounter,omitempty"` } + type KicsCounters struct { - SeverityCounters []SeverityCounters `json:"SeverityCounters,omitempty,"` - TotalCounter int `json:"totalCounter,omitempty,"` - FilesScannedCounter int `json:"filesScannedCounter,omitempty,"` + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + FilesScannedCounter int `json:"filesScannedCounter,omitempty"` } type ScaCounters struct { - SeverityCounters []SeverityCounters `json:"SeverityCounters,omitempty,"` - TotalCounter int `json:"totalCounter,omitempty,"` - FilesScannedCounter int `json:"filesScannedCounter,omitempty,"` + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + FilesScannedCounter int `json:"filesScannedCounter,omitempty"` +} + +// ScaPackagesCounters contains counters for SCA packages. +type ScaPackagesCounters struct { + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + StatusCounters []StatusCounters `json:"statusCounters,omitempty"` + StateCounters []StateCounters `json:"stateCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + OutdatedCounter int `json:"outdatedCounter,omitempty"` + RiskLevelCounters []RiskLevelCounters `json:"riskLevelCounters,omitempty"` + LicenseCounters []LicenseCounters `json:"licenseCounters,omitempty"` } type ScaContainersCounters struct { - SeverityCounters []SeverityCounters `json:"severityVulnerabilitiesCounters,omitempty,"` - TotalPackagesCounter int `json:"totalPackagesCounter,omitempty,"` - TotalVulnerabilitiesCounter int `json:"totalVulnerabilitiesCounter,omitempty,"` + SeverityCounters []SeverityCounters `json:"severityVulnerabilitiesCounters,omitempty"` + TotalPackagesCounter int `json:"totalPackagesCounter,omitempty"` + TotalVulnerabilitiesCounter int `json:"totalVulnerabilitiesCounter,omitempty"` +} + +// ApiSecCounters contains counters for API Security findings. +type ApiSecCounters struct { + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + StateCounters []StateCounters `json:"stateCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + FilesScannedCounter int `json:"filesScannedCounter,omitempty"` + RiskLevel string `json:"riskLevel,omitempty"` + ApiSecTotal int `json:"apiSecTotal,omitempty"` +} + +// MicroEnginesCounters contains counters for micro engines. +type MicroEnginesCounters struct { + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + StatusCounters []StatusCounters `json:"statusCounters,omitempty"` + StateCounters []StateCounters `json:"stateCounters,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + FilesScannedCounter int `json:"filesScannedCounter,omitempty"` } +// ContainersCounters contains counters for container scanning results. +type ContainersCounters struct { + TotalPackagesCounter int `json:"totalPackagesCounter,omitempty"` + TotalCounter int `json:"totalCounter,omitempty"` + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` + StatusCounters []StatusCounters `json:"statusCounters,omitempty"` + StateCounters []StateCounters `json:"stateCounters,omitempty"` + AgeCounters []AgeCounters `json:"ageCounters,omitempty"` + PackageCounters []PackageCounters `json:"packageCounters,omitempty"` + SeverityStatusCounters []SeverityStatusCounters `json:"severityStatusCounters,omitempty"` +} + +// AiscCounters contains counters for AISC engine scanning. +type AiscCounters struct { + AssetsCounter int `json:"assetsCounter,omitempty"` + AssetTypesCounter int `json:"assetTypesCounter,omitempty"` +} + +// SeverityCounters contains severity level counter information. type SeverityCounters struct { - Severity string `json:"severity,omitempty,"` - Counter int `json:"counter,omitempty,"` + Severity string `json:"severity,omitempty"` + Counter int `json:"counter,omitempty"` +} + +// StatusCounters contains status counter information. +type StatusCounters struct { + Status string `json:"status,omitempty"` + Counter int `json:"counter,omitempty"` +} + +// StateCounters contains state counter information. +type StateCounters struct { + State string `json:"state,omitempty"` + Counter int `json:"counter,omitempty"` +} + +// RiskLevelCounters contains risk level counter information. +type RiskLevelCounters struct { + RiskLevel string `json:"riskLevel,omitempty"` + Counter int `json:"counter,omitempty"` +} + +// LicenseCounters contains license counter information. +type LicenseCounters struct { + License string `json:"license,omitempty"` + Counter int `json:"counter,omitempty"` +} + +// AgeCounters contains age counter information. +type AgeCounters struct { + Age string `json:"age,omitempty"` + Counter int `json:"counter,omitempty"` + SeverityCounters []SeverityCounters `json:"severityCounters,omitempty"` +} + +// PackageCounters contains package counter information. +type PackageCounters struct { + PackageID string `json:"packageId,omitempty"` + Counter int `json:"counter,omitempty"` + IsMalicious bool `json:"isMalicious,omitempty"` +} + +// SeverityStatusCounters contains combined severity and status counter information. +type SeverityStatusCounters struct { + Severity string `json:"severity,omitempty"` + Status string `json:"status,omitempty"` + Counter int `json:"counter,omitempty"` } diff --git a/internal/wrappers/scan-summary-http.go b/internal/wrappers/scan-summary-http.go new file mode 100644 index 00000000..c25ea8c1 --- /dev/null +++ b/internal/wrappers/scan-summary-http.go @@ -0,0 +1,71 @@ +package wrappers + +import ( + "encoding/json" + "fmt" + "net/http" + + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +const ( + failedToParseScanSummary = "Failed to parse scan-summary response" +) + +// ScanSummaryHTTPWrapper is a struct that implements the ScanSummaryWrapper interface for retrieving scan summaries via HTTP requests. +type ScanSummaryHTTPWrapper struct { + path string +} + +// NewHTTPScanSummaryWrapper creates a new instance of ScanSummaryHTTPWrapper with the provided path. +func NewHTTPScanSummaryWrapper(path string) ScanSummaryWrapper { + return &ScanSummaryHTTPWrapper{ + path: path, + } +} + +// GetScanSummaryByScanID retrieves the scan summary for a given scan ID by making an HTTP GET request to the configured path. +func (s *ScanSummaryHTTPWrapper) GetScanSummaryByScanID(scanID string) (*ScanSummariesModel, *WebError, error) { + clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) + + // Construct the path with query parameter + path := fmt.Sprintf("%s?scan-ids=%s", s.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, failedToParseScanSummary) + } + return nil, &errorModel, nil + + case http.StatusOK: + model := ScanSummariesModel{} + err = decoder.Decode(&model) + if err != nil { + return nil, nil, errors.Wrapf(err, failedToParseScanSummary) + } + return &model, nil, nil + + case http.StatusNotFound: + return nil, nil, errors.Errorf("scan summary not found for scan ID: %s", scanID) + + default: + return nil, nil, errors.Errorf("response status code %d", resp.StatusCode) + } +} diff --git a/internal/wrappers/scan-summary.go b/internal/wrappers/scan-summary.go new file mode 100644 index 00000000..c97f4ff2 --- /dev/null +++ b/internal/wrappers/scan-summary.go @@ -0,0 +1,6 @@ +package wrappers + +// ScanSummaryWrapper wraps scan summary logic. +type ScanSummaryWrapper interface { + GetScanSummaryByScanID(scanID string) (*ScanSummariesModel, *WebError, error) +} diff --git a/internal/wrappers/scans.go b/internal/wrappers/scans.go index 71c878a0..f7277c78 100644 --- a/internal/wrappers/scans.go +++ b/internal/wrappers/scans.go @@ -163,3 +163,7 @@ type SCSConfig struct { RepoToken string `json:"repoToken,omitempty"` GitCommitHistory string `json:"gitCommitHistory,omitempty"` } + +// AISCConfig is a placeholder for AISC scan configurations. +type AISCConfig struct { +} diff --git a/test/integration/result_test.go b/test/integration/result_test.go index ce09dca4..4938f717 100644 --- a/test/integration/result_test.go +++ b/test/integration/result_test.go @@ -3,8 +3,10 @@ package integration import ( + "bytes" "encoding/json" "fmt" + "io" "log" "os" "strings" @@ -777,3 +779,46 @@ func findQueryDescriptionLink(glReport wrappers.GlSastResultsCollection) (string } return "", false } + +func TestCreateScan_WithTypeAisc_ConsoleSummaryContainsAISCOutput(t *testing.T) { + // Step 1: Create scan with AISC type + createArgs := []string{ + "scan", "create", + flag(params.ProjectName), getProjectNameForScanTests(), + flag(params.SourcesFlag), Zip, + flag(params.BranchFlag), "main", + flag(params.ScanInfoFormatFlag), printer.FormatJSON, flag(params.DebugFlag), + } + scanID, _ := executeCreateScan(t, createArgs) + assert.Assert(t, scanID != "", "Scan ID should not be empty") + + // Step 2: Redirect os.Stdout to capture fmt.Printf output from printAISCSummary + oldStdout := os.Stdout + r, w, pipeErr := os.Pipe() + assert.NilError(t, pipeErr, "Failed to create os.Pipe") + os.Stdout = w + + _ = executeCmdNilAssertion( + t, "Results show with AISC summary console should pass", + "results", "show", + flag(params.ScanIDFlag), scanID, + flag(params.TargetFormatFlag), printer.FormatSummaryConsole, + ) + + w.Close() + var buf bytes.Buffer + _, copyErr := io.Copy(&buf, r) + os.Stdout = oldStdout + assert.NilError(t, copyErr, "Failed to read captured stdout") + + output := buf.String() + // Check if scan-summary API was called + if strings.Contains(output, "scan-summary") { + assert.Assert(t, strings.Contains(output, "AI SUPPLY CHAIN ENGINE SUMMARY"), + "Console output should contain AISC summary header") + assert.Assert(t, strings.Contains(output, "Total Assets"), + "Console output should contain Total Assets row") + assert.Assert(t, strings.Contains(output, "Total Asset Types"), + "Console output should contain Total Asset Types row") + } +} diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 57de7958..45cc1ed7 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -64,7 +64,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { groups := viper.GetString(params.GroupsPathKey) projects := viper.GetString(params.ProjectsPathKey) results := viper.GetString(params.ResultsPathKey) - scanSummmaryPath := viper.GetString(params.ScanSummaryPathKey) + scanSummaryPath := viper.GetString(params.ScanSummaryPathKey) risksOverview := viper.GetString(params.RisksOverviewPathKey) apiSecurityResult := viper.GetString(params.APISecurityResultPathKey) riskManagement := viper.GetString(params.RiskManagementPathKey) @@ -102,10 +102,11 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { groupsWrapper := wrappers.NewHTTPGroupsWrapper(groups) uploadsWrapper := wrappers.NewUploadsHTTPWrapper(uploads) projectsWrapper := wrappers.NewHTTPProjectsWrapper(projects) - resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scanSummmaryPath) + resultsWrapper := wrappers.NewHTTPResultsWrapper(results, scanSummaryPath) risksOverviewWrapper := wrappers.NewHTTPRisksOverviewWrapper(risksOverview, apiSecurityResult) riskManagementWrapper := wrappers.NewHTTPRiskManagementWrapper(riskManagement) scsScanOverviewWrapper := wrappers.NewHTTPScanOverviewWrapper(scsScanOverviewPath) + scanSummaryWrapper := wrappers.NewHTTPScanSummaryWrapper(scanSummaryPath) authWrapper := wrappers.NewAuthHTTPWrapper() logsWrapper := wrappers.NewLogsWrapper(logs) codeBashingWrapper := wrappers.NewCodeBashingHTTPWrapper(codebashing) @@ -145,6 +146,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { risksOverviewWrapper, riskManagementWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, authWrapper, logsWrapper, groupsWrapper,