diff --git a/cmd/main.go b/cmd/main.go index 53d2ae664..18110a427 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -74,6 +74,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() @@ -115,6 +116,7 @@ func main() { risksOverviewWrapper, riskManagementWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, authWrapper, logsWrapper, groupsWrapper, diff --git a/go.mod b/go.mod index c5bff6624..1b73d5931 100644 --- a/go.mod +++ b/go.mod @@ -141,8 +141,8 @@ require ( github.com/gitleaks/go-gitdiff v0.9.1 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.16.5 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-git/v5 v5.17.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index b57201c3f..e65c26d17 100644 --- a/go.sum +++ b/go.sum @@ -386,12 +386,12 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk= +github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/internal/commands/result.go b/internal/commands/result.go index f37c5e46b..11318b399 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -187,6 +187,7 @@ func NewResultsCommand( risksOverviewWrapper wrappers.RisksOverviewWrapper, riskManagementWrapper wrappers.RiskManagementWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -203,7 +204,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) @@ -263,6 +264,7 @@ func resultShowSubCommand( resultsJSONReportsWrapper wrappers.ResultsJSONWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -276,7 +278,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( @@ -691,6 +693,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, @@ -720,6 +723,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) } @@ -888,6 +900,10 @@ func writeConsoleSummary(summary *wrappers.ResultSummary, featureFlagsWrapper wr printSCSSummary(summary.SCSOverview.MicroEngineOverviews, featureFlagsWrapper) } + 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") @@ -977,6 +993,15 @@ func printSCSTableRow(microEngineOverview *wrappers.MicroEngineOverview, feature } } +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 @@ -1027,6 +1052,7 @@ func runGetResultCommand( resultsJSONReportsWrapper wrappers.ResultsJSONWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyWrapper wrappers.PolicyWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, jwtWrapper wrappers.JWTWrapper, @@ -1093,7 +1119,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 @@ -1183,6 +1209,7 @@ func CreateScanReport( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, exportWrapper wrappers.ExportWrapper, policyResponseModel *wrappers.PolicyResponseModel, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, @@ -1220,7 +1247,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 } @@ -1376,6 +1403,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, "Completed") || strings.EqualFold( scanStatus, diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index dc0063351..42b0b4954 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" @@ -1740,3 +1741,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 6f3503036..b42e1fc5f 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 cb17fe099..9553f3245 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 b0d346a6d..e3c6e3bb1 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -179,6 +179,7 @@ func NewScanCommand( groupsWrapper wrappers.GroupsWrapper, riskOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, scaRealTimeWrapper wrappers.ScaRealTimeWrapper, policyWrapper wrappers.PolicyWrapper, @@ -213,6 +214,7 @@ func NewScanCommand( groupsWrapper, riskOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -671,6 +673,7 @@ func scanCreateSubCommand( groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -705,6 +708,7 @@ func scanCreateSubCommand( groupsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, jwtWrapper, policyWrapper, accessManagementWrapper, @@ -1037,6 +1041,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) @@ -1164,6 +1173,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{}) @@ -1504,6 +1532,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) @@ -2392,6 +2421,7 @@ func runCreateScanCommand( groupsWrapper wrappers.GroupsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, jwtWrapper wrappers.JWTWrapper, policyWrapper wrappers.PolicyWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, @@ -2449,6 +2479,7 @@ func runCreateScanCommand( jwtWrapper, tenantWrapper, ) + defer cleanUpTempZip(zipFilePath) if err != nil { return errors.Errorf("%s", err) @@ -2484,6 +2515,7 @@ func runCreateScanCommand( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, featureFlagsWrapper, ignorePolicyFlagOmit) if err != nil { @@ -2498,7 +2530,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 } @@ -2510,7 +2542,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 } @@ -2562,6 +2594,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) } @@ -2672,6 +2705,7 @@ func handleWait( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, ) error { @@ -2686,6 +2720,7 @@ func handleWait( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, cmd, featureFlagsWrapper, ignorePolicyFlagOmit) @@ -2711,6 +2746,7 @@ func createReportsAfterScan( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, policyResponseModel *wrappers.PolicyResponseModel, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, @@ -2748,6 +2784,7 @@ func createReportsAfterScan( resultsWrapper, risksOverviewWrapper, scsScanOverviewWrapper, + scanSummaryWrapper, exportWrapper, policyResponseModel, resultsPdfReportsWrapper, @@ -2923,6 +2960,7 @@ func waitForScanCompletion( resultsWrapper wrappers.ResultsWrapper, risksOverviewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, cmd *cobra.Command, featureFlagsWrapper wrappers.FeatureFlagsWrapper, ignorePolicyFlagOmit bool, @@ -2940,7 +2978,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 } @@ -2972,6 +3010,7 @@ func isScanRunning( resultsWrapper wrappers.ResultsWrapper, risksOverViewWrapper wrappers.RisksOverviewWrapper, scsScanOverviewWrapper wrappers.ScanOverviewWrapper, + scanSummaryWrapper wrappers.ScanSummaryWrapper, scanID string, cmd *cobra.Command, featureFlagsWrapper wrappers.FeatureFlagsWrapper, @@ -3007,6 +3046,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 6d029bb34..6c188c776 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 622c8a010..091f64601 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -309,6 +309,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 798256a4b..a5718fbd4 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 63b1defb5..e684c6891 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 000000000..7aba26aa0 --- /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 26c47bdf3..2e3bc3032 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 80e2b2b1d..e868103cd 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 000000000..c25ea8c15 --- /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 000000000..c97f4ff2e --- /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 71c878a0b..f7277c78f 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 ce09dca46..3193c894b 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,44 @@ 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.ScanTypes), params.AiscType, + flag(params.BranchFlag), "main", + flag(params.ScanInfoFormatFlag), printer.FormatJSON, + } + 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() + 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 57de7958b..45cc1ed72 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,