diff --git a/internal/commands/groups_test.go b/internal/commands/groups_test.go index 4b507908b..5e5d2d65e 100644 --- a/internal/commands/groups_test.go +++ b/internal/commands/groups_test.go @@ -9,6 +9,7 @@ import ( func TestCreateScanAndProjectWithGroupFFTrue(t *testing.T) { mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + defer clearFlags() execCmdNilAssertion( t, "scan", "create", "--project-name", "new-project", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", @@ -17,6 +18,7 @@ func TestCreateScanAndProjectWithGroupFFTrue(t *testing.T) { func TestCreateScanAndProjectWithGroupFFFalse(t *testing.T) { mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: false}} + defer clearFlags() execCmdNilAssertion( t, "scan", "create", "--project-name", "new-project", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", @@ -24,6 +26,7 @@ func TestCreateScanAndProjectWithGroupFFFalse(t *testing.T) { } func TestCreateProjectWithGroupFFTrue(t *testing.T) { mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + defer clearFlags() execCmdNilAssertion( t, "project", "create", "--project-name", "new-project", "--groups", "group", ) @@ -31,6 +34,7 @@ func TestCreateProjectWithGroupFFTrue(t *testing.T) { func TestCreateProjectWithGroupFFFalse(t *testing.T) { mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: false}} + defer clearFlags() execCmdNilAssertion( t, "project", "create", "--project-name", "new-project", "--groups", "group", @@ -39,6 +43,7 @@ func TestCreateProjectWithGroupFFFalse(t *testing.T) { func TestCreateScanForExistingProjectWithGroupFFTrue(t *testing.T) { mock.Flags = wrappers.FeatureFlagsResponseModel{{Name: "ACCESS_MANAGEMENT_ENABLED", Status: true}} + defer clearFlags() execCmdNilAssertion( t, "scan", "create", "--project-name", "MOCK", "-b", "dummy_branch", "-s", ".", "--project-groups", "group", diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 9331e0b5e..285f282b2 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -124,6 +124,12 @@ const ( "--scs-repo-url your_repo_url --scs-repo-token your_repo_token" ScsScorecardUnsupportedHostWarningMsg = "SCS scan warning: Unable to run Scorecard scanner due to unsupported repo host. Currently, Scorecard can only run on GitHub Cloud repos." + gitCommitHistoryInvalidValueErrorMsg = "Invalid value for --git-commit-history. Valid values are: 'true' or 'false'" + gitCommitHistoryNotAvailableWarningMsg = "Secret Detection scan warning: --git-commit-history flag ignored because git commit history scanning is not available." + gitCommitHistoryNotSelectedWarningMsg = "Secret Detection scan warning: --git-commit-history flag ignored because scs was not specified in scan types." + gitCommitHistoryNotApplicableWarningMsg = "Secret Detection scan warning: --git-commit-history flag ignored because secret detection wasn't run on this scan." + gitCommitHistoryNoGitRepositoryWarningMsg = "Secret Detection scan warning: No Git history found. Secret Detection will scan the working tree only." + jsonExt = ".json" xmlExt = ".xml" sbomScanTypeErrMsg = "The --sbom-only flag can only be used when the scan type is sca" @@ -884,6 +890,7 @@ func scanCreateSubCommand( createScanCmd.PersistentFlags().String(commonParams.SCSRepoTokenFlag, "", "Provide a token with read permission for the repo that you are scanning (for scorecard scans)") createScanCmd.PersistentFlags().String(commonParams.SCSRepoURLFlag, "", "The URL of the repo that you are scanning with scs (for scorecard scans)") createScanCmd.PersistentFlags().String(commonParams.SCSEnginesFlag, "", "Specify which scs engines will run (default: all licensed engines)") + createScanCmd.PersistentFlags().String(commonParams.GitCommitHistoryFlag, "", commonParams.GitCommitHistoryFlagDescription) createScanCmd.PersistentFlags().Bool(commonParams.ScaHideDevAndTestDepFlag, false, scaHideDevAndTestDepFlagDescription) // Container config flags @@ -1010,9 +1017,8 @@ func setupScanTypeProjectAndConfig( configArr = append(configArr, containersConfig) } - scsLicensingV2Flag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.ScsLicensingV2Enabled) - var SCSConfig, scsErr = addSCSScan(cmd, resubmitConfig, scsLicensingV2Flag.Status, userAllowedEngines[commonParams.RepositoryHealthType], - userAllowedEngines[commonParams.SecretDetectionType], userAllowedEngines[commonParams.EnterpriseSecretsType]) + var SCSConfig, scsErr = addSCSScan(cmd, resubmitConfig, userAllowedEngines[commonParams.RepositoryHealthType], + userAllowedEngines[commonParams.SecretDetectionType], userAllowedEngines[commonParams.EnterpriseSecretsType], featureFlagsWrapper) if scsErr != nil { return scsErr } else if SCSConfig != nil { @@ -1388,14 +1394,15 @@ func isScorecardRunnable(isScsEnginesFlagSet, scsScorecardSelected bool, scsRepo return isURLSupportedByScorecard(scsRepoURL), nil } -func addSCSScan(cmd *cobra.Command, resubmitConfig []wrappers.Config, scsLicensingV2, hasRepositoryHealthLicense, - hasSecretDetectionLicense, hasEnterpriseSecretsLicense bool) (map[string]interface{}, error) { - scsEnabled := isScsEnabled(scsLicensingV2) +func addSCSScan(cmd *cobra.Command, resubmitConfig []wrappers.Config, hasRepositoryHealthLicense, + hasSecretDetectionLicense, hasEnterpriseSecretsLicense bool, featureFlagsWrapper wrappers.FeatureFlagsWrapper) (map[string]interface{}, error) { + scsLicensingV2Flag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.ScsLicensingV2Enabled) + scsEnabled := isScsEnabled(scsLicensingV2Flag.Status) if !scsEnabled { return nil, nil } - scsScorecardAllowed := isScsScorecardAllowed(scsLicensingV2, hasRepositoryHealthLicense) - scsSecretDetectionAllowed := isScsSecretDetectionAllowed(scsLicensingV2, hasSecretDetectionLicense, hasEnterpriseSecretsLicense) + scsScorecardAllowed := isScsScorecardAllowed(scsLicensingV2Flag.Status, hasRepositoryHealthLicense) + scsSecretDetectionAllowed := isScsSecretDetectionAllowed(scsLicensingV2Flag.Status, hasSecretDetectionLicense, hasEnterpriseSecretsLicense) if !scsScorecardAllowed && !scsSecretDetectionAllowed { return nil, nil } @@ -1426,6 +1433,12 @@ func addSCSScan(cmd *cobra.Command, resubmitConfig []wrappers.Config, scsLicensi if scsSecretDetectionSelected && scsSecretDetectionAllowed { scsConfig.Twoms = trueString + + // Set git commit history based on FF and validations + commitHistoryFlag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.SscsCommitHistoryEnabled) + if gitCommitHistoryValue := getGitCommitHistoryValue(cmd, commitHistoryFlag.Status); gitCommitHistoryValue != "" { + scsConfig.GitCommitHistory = gitCommitHistoryValue + } } isScsEnginesFlagSet := scsEngines != "" @@ -3512,6 +3525,13 @@ func validateCreateScanFlags(cmd *cobra.Command) error { } } } + + // Validate git-commit-history flag + err = validateGitCommitHistoryFlag(cmd) + if err != nil { + return err + } + return nil } @@ -3783,6 +3803,102 @@ func validateBooleanString(value string) error { return nil } +// validateGitCommitHistoryFlag validates the git-commit-history flag (needed for Secret Detection) +func validateGitCommitHistoryFlag(cmd *cobra.Command) error { + gitCommitHistory, _ := cmd.Flags().GetString(commonParams.GitCommitHistoryFlag) + + err := validateBooleanString(gitCommitHistory) + if err != nil { + return errors.Errorf(gitCommitHistoryInvalidValueErrorMsg) + } + + return nil +} + +// getGitCommitHistoryValue determines the value for git commit history config based on flag and validations +func getGitCommitHistoryValue(cmd *cobra.Command, isFeatureFlagEnabled bool) string { + if !isFeatureFlagEnabled { + fmt.Println(gitCommitHistoryNotAvailableWarningMsg) + return "" + } + + gitCommitHistory, _ := cmd.Flags().GetString(commonParams.GitCommitHistoryFlag) + gitCommitHistoryValue := strings.ToLower(gitCommitHistory) + + if !validateGitCommitHistoryContext(cmd) { + return "" + } + + return gitCommitHistoryValue +} + +// validateGitCommitHistoryContext validates if the context is appropriate for functionality +func validateGitCommitHistoryContext(cmd *cobra.Command) bool { + userScanTypes, _ := cmd.Flags().GetString(commonParams.ScanTypes) + if !strings.Contains(strings.ToLower(userScanTypes), commonParams.ScsType) { + fmt.Println(gitCommitHistoryNotSelectedWarningMsg) + return false + } + + scsEngines, _ := cmd.Flags().GetString(commonParams.SCSEnginesFlag) + scsScoreCardSelected, scsSecretDetectionSelected := getSCSEnginesSelected(scsEngines) + if scsScoreCardSelected && !scsSecretDetectionSelected { + fmt.Println(gitCommitHistoryNotApplicableWarningMsg) + return false + } + + source, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + if !hasGitRepository(source) { + fmt.Println(gitCommitHistoryNoGitRepositoryWarningMsg) + return false + } + + return true +} + +// hasGitRepository checks if the source directory contains a Git repository (skipping validation for git URLs or zip files) +func hasGitRepository(source string) bool { + if source == "" { + return false + } + + sourceTrimmed := strings.TrimSpace(source) + + if util.IsGitURL(sourceTrimmed) || filepath.Ext(sourceTrimmed) == constants.ZipExtension { + return true + } + + info, err := os.Stat(sourceTrimmed) + if err != nil || !info.IsDir() { + return false + } + + // Check if .git exists in the root directory + gitPath := filepath.Join(sourceTrimmed, ".git") + if _, err := os.Stat(gitPath); err == nil { + return true + } + + // fallback: search for .git in subdirectories + return searchGitInSubdirectories(sourceTrimmed) +} + +// searchGitInSubdirectories walks through subdirectories to find a .git folder +func searchGitInSubdirectories(sourcePath string) bool { + found := false + _ = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { + if err != nil || found { + return nil + } + if info.IsDir() && info.Name() == ".git" { + found = true + return filepath.SkipAll + } + return nil + }) + return found +} + func parseArgs(input string) []string { var args []string var current strings.Builder diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index e14142f80..92ff6b243 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -832,6 +832,7 @@ func TestAddSCSScan_ResubmitWithoutScorecardFlags_ShouldPass(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() cmdCommand := &cobra.Command{ Use: "scan", Short: "Scan a project", @@ -856,8 +857,15 @@ func TestAddSCSScan_ResubmitWithoutScorecardFlags_ShouldPass(t *testing.T) { }, } - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) expectedConfig := wrappers.SCSConfig{ Twoms: trueString, @@ -901,6 +909,7 @@ func TestAddSCSScan_ResubmitWithScorecardFlags_ShouldPass(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() cmdCommand := &cobra.Command{ Use: "scan", Short: "Scan a project", @@ -925,8 +934,15 @@ func TestAddSCSScan_ResubmitWithScorecardFlags_ShouldPass(t *testing.T) { }, } - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) expectedConfig := wrappers.SCSConfig{ Twoms: "true", @@ -1185,6 +1201,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecard_scsMapHasBoth(t *testing. for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() var resubmitConfig []wrappers.Config cmdCommand := &cobra.Command{ Use: "scan", @@ -1199,8 +1216,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecard_scsMapHasBoth(t *testing. _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyRepo) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) scsConfig := wrappers.SCSConfig{ Twoms: "true", @@ -1245,6 +1269,7 @@ func TestAddSCSScan_WithoutSCSSecretDetection_scsMapNoSecretDetection(t *testing for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() var resubmitConfig []wrappers.Config cmdCommand := &cobra.Command{ Use: "scan", @@ -1259,8 +1284,15 @@ func TestAddSCSScan_WithoutSCSSecretDetection_scsMapNoSecretDetection(t *testing _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyRepo) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) scsConfig := wrappers.SCSConfig{ Twoms: "", @@ -1305,6 +1337,7 @@ func TestAddSCSScan_WithSCSSecretDetection_scsMapHasSecretDetection(t *testing.T for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() var resubmitConfig []wrappers.Config cmdCommand := &cobra.Command{ Use: "scan", @@ -1315,8 +1348,15 @@ func TestAddSCSScan_WithSCSSecretDetection_scsMapHasSecretDetection(t *testing.T _ = cmdCommand.Execute() _ = cmdCommand.Flags().Set(commonParams.SCSEnginesFlag, "secret-detection") - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) scsConfig := wrappers.SCSConfig{ Twoms: "true", @@ -1332,6 +1372,86 @@ func TestAddSCSScan_WithSCSSecretDetection_scsMapHasSecretDetection(t *testing.T } } +func TestAddSCSScan_WithSCSSecretDetectionAndGitCommitHistoryFlag_scsMapHasSecretDetection(t *testing.T) { + tests := []struct { + name string + scsLicensingV2 bool + hasRepositoryHealthLicense bool + hasSecretDetectionLicense bool + hasEnterpriseSecretsLicense bool + }{ + { + name: "scsLicensingV2 disabled", + scsLicensingV2: false, + hasRepositoryHealthLicense: false, + hasSecretDetectionLicense: false, + hasEnterpriseSecretsLicense: true, + }, + { + name: "scsLicensingV2 enabled", + scsLicensingV2: true, + hasRepositoryHealthLicense: true, + hasSecretDetectionLicense: true, + hasEnterpriseSecretsLicense: false, + }, + } + + // Create a temporary directory with .git for testing + tempDir := t.TempDir() + gitDir := filepath.Join(tempDir, ".git") + _ = os.Mkdir(gitDir, 0755) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() + var resubmitConfig []wrappers.Config + cmdCommand := &cobra.Command{ + Use: "scan", + Short: "Scan a project", + Long: `Scan a project`, + } + cmdCommand.PersistentFlags().String(commonParams.SCSEnginesFlag, "", "SCS Engine flag") + cmdCommand.PersistentFlags().String(commonParams.GitCommitHistoryFlag, "", commonParams.GitCommitHistoryFlagDescription) + cmdCommand.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types") + cmdCommand.PersistentFlags().String(commonParams.SourcesFlag, "", "Sources") + + _ = cmdCommand.Execute() + _ = cmdCommand.Flags().Set(commonParams.SCSEnginesFlag, "secret-detection") + _ = cmdCommand.Flags().Set(commonParams.GitCommitHistoryFlag, "true") + _ = cmdCommand.Flags().Set(commonParams.ScanTypes, "scs") + _ = cmdCommand.Flags().Set(commonParams.SourcesFlag, tempDir) + + mock.Flags = wrappers.FeatureFlagsResponseModel{ + { + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + }, + { + Name: wrappers.SscsCommitHistoryEnabled, + Status: true, + }, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) + + scsConfig := wrappers.SCSConfig{ + Twoms: "true", + GitCommitHistory: "true", + } + scsMapConfig := make(map[string]interface{}) + scsMapConfig[resultsMapType] = commonParams.MicroEnginesType + scsMapConfig[resultsMapValue] = &scsConfig + + if !reflect.DeepEqual(result, scsMapConfig) { + t.Errorf("Expected %+v, but got %+v", scsMapConfig, result) + } + }) + } +} + func TestAddSCSScan_WithSCSSecretDetectionAndScorecardWithScanTypesAndNoScorecardFlags_scsMapHasSecretDetection(t *testing.T) { tests := []struct { name string @@ -1358,6 +1478,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardWithScanTypesAndNoScorecar for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1374,8 +1495,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardWithScanTypesAndNoScorecar _ = cmdCommand.Execute() _ = cmdCommand.Flags().Set(commonParams.ScanTypeFlag, "scs") - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) scsConfig := wrappers.SCSConfig{ Twoms: "true", @@ -1433,6 +1561,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndWithoutScanTypes_scsMapHasSecretDet for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() var resubmitConfig []wrappers.Config cmdCommand := &cobra.Command{ Use: "scan", @@ -1440,8 +1569,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndWithoutScanTypes_scsMapHasSecretDet Long: `Scan a project`, } - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) scsConfig := wrappers.SCSConfig{ Twoms: "true", @@ -1484,6 +1620,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepo_scsMap for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1504,8 +1641,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepo_scsMap _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyShortenedGithubRepo) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1565,6 +1709,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepoWithTok for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1585,8 +1730,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepoWithTok _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyShortenedRepoWithToken) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1646,6 +1798,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGithubRepoWithTokenInURL_s for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1666,8 +1819,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGithubRepoWithTokenInURL_s _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyRepoWithToken) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1727,6 +1887,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGithubRepoWithTokenAndUser for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1747,8 +1908,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGithubRepoWithTokenAndUser _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyRepoWithTokenAndUsername) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1808,6 +1976,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepoWithTok for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1828,8 +1997,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardShortenedGithubRepoWithTok _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyShortenedRepoWithTokenAndUsername) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1889,6 +2065,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGitLabRepo_scsMapHasSecret for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1909,8 +2086,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGitLabRepo_scsMapHasSecret _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummyGitlabRepo) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -1970,6 +2154,7 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGitSSHRepo_scsMapHasSecret for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + wrappers.ClearCache() // Create a pipe for capturing stdout r, w, _ := os.Pipe() oldStdout := os.Stdout @@ -1990,8 +2175,15 @@ func TestAddSCSScan_WithSCSSecretDetectionAndScorecardGitSSHRepo_scsMapHasSecret _ = cmdCommand.Flags().Set(commonParams.SCSRepoTokenFlag, dummyToken) _ = cmdCommand.Flags().Set(commonParams.SCSRepoURLFlag, dummySSHRepo) - result, _ := addSCSScan(cmdCommand, resubmitConfig, tt.scsLicensingV2, - tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense) + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, + } + defer clearFlags() + + featureFlagsWrapper := &mock.FeatureFlagsMockWrapper{} + result, _ := addSCSScan(cmdCommand, resubmitConfig, + tt.hasRepositoryHealthLicense, tt.hasSecretDetectionLicense, tt.hasEnterpriseSecretsLicense, featureFlagsWrapper) // Close the writer to signal that we are done capturing the output w.Close() @@ -3426,6 +3618,7 @@ func TestValidateScanTypes(t *testing.T) { Name: wrappers.ScsLicensingV2Enabled, Status: tt.scsLicensingV2, } + defer clearFlags() cmd := &cobra.Command{} cmd.Flags().String(commonParams.ScanTypes, tt.userScanTypes, "") @@ -4372,3 +4565,356 @@ func TestUploadZip_AsMultipartUpload_when_FF_Enable_ZIP_Exceeds_5GB_Error(t *tes assert.Assert(t, strings.Contains(err.Error(), "error from UploadFileInMultipart"), err.Error()) assert.Equal(t, zipPath, "") } + +func TestValidateGitCommitHistoryFlag(t *testing.T) { + tests := []struct { + name string + flagValue string + expectedErrorMsg string + }{ + { + name: "Valid true value", + flagValue: "true", + }, + { + name: "Valid false value", + flagValue: "false", + }, + { + name: "Valid TRUE value (case insensitive)", + flagValue: "TRUE", + }, + { + name: "Valid FALSE value (case insensitive)", + flagValue: "FALSE", + }, + { + name: "Invalid value 'maybe'", + flagValue: "maybe", + expectedErrorMsg: gitCommitHistoryInvalidValueErrorMsg, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdCommand := &cobra.Command{ + Use: "scan", + Short: "Test scan command", + } + cmdCommand.PersistentFlags().String(commonParams.GitCommitHistoryFlag, "false", commonParams.GitCommitHistoryFlagDescription) + + _ = cmdCommand.Execute() + + _ = cmdCommand.Flags().Set(commonParams.GitCommitHistoryFlag, tt.flagValue) + + err := validateGitCommitHistoryFlag(cmdCommand) + if tt.expectedErrorMsg != "" { + assert.Assert(t, err != nil, "Expected error but got nil") + if err != nil { + assert.Assert(t, err.Error() == tt.expectedErrorMsg, "Expected error: %v, got: %v", tt.expectedErrorMsg, err) + } + } else { + assert.NilError(t, err, "Expected no error, got: %v", err) + } + }) + } +} + +func TestGetGitCommitHistoryValue(t *testing.T) { + // Create a temporary directory with .git for testing + tempDir := t.TempDir() + gitDir := filepath.Join(tempDir, ".git") + _ = os.Mkdir(gitDir, 0755) + + // Create a directory with .git in a subdirectory + tempDirWithSubGit := t.TempDir() + subDir := filepath.Join(tempDirWithSubGit, "project1") + _ = os.Mkdir(subDir, 0755) + gitDirSub := filepath.Join(subDir, ".git") + _ = os.Mkdir(gitDirSub, 0755) + + tests := []struct { + name string + flagValue string + scanTypes string + scsEngines string + source string + ffEnabled bool + expectedValue string + }{ + { + name: "Flag true with valid context - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag false with valid context - returns false", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: true, + expectedValue: "false", + }, + { + name: "Flag true with git in subdirectory - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDirWithSubGit, + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag true with both engines - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection,scorecard", + source: tempDir, + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag false with both engines - returns false", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection,scorecard", + source: tempDir, + ffEnabled: true, + expectedValue: "false", + }, + { + name: "Flag true with HTTPS GitHub URL - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "https://github.com/user/repo.git", + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag true with HTTPS GitLab URL - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "https://gitlab.com/user/repo.git", + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag true with SSH git@ format URL - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "git@github.com:user/repo.git", + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag true with SSH protocol URL - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "ssh://git@github.com/user/repo.git", + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag false with git URL - returns false", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "https://github.com/user/repo.git", + ffEnabled: true, + expectedValue: "false", + }, + { + name: "Flag true with zip file - returns true", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "/path/to/source.zip", + ffEnabled: true, + expectedValue: "true", + }, + { + name: "Flag false with zip file - returns false", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection", + source: "/path/to/source.zip", + ffEnabled: true, + expectedValue: "false", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdCommand := &cobra.Command{ + Use: "scan", + Short: "Scan a project with git commit history", + } + cmdCommand.PersistentFlags().String(commonParams.GitCommitHistoryFlag, "", commonParams.GitCommitHistoryFlagDescription) + cmdCommand.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types") + cmdCommand.PersistentFlags().String(commonParams.SCSEnginesFlag, "", "SCS engines") + cmdCommand.PersistentFlags().String(commonParams.SourcesFlag, "", "Sources") + + _ = cmdCommand.Execute() + + _ = cmdCommand.Flags().Set(commonParams.GitCommitHistoryFlag, tt.flagValue) + _ = cmdCommand.Flags().Set(commonParams.ScanTypes, tt.scanTypes) + _ = cmdCommand.Flags().Set(commonParams.SCSEnginesFlag, tt.scsEngines) + _ = cmdCommand.Flags().Set(commonParams.SourcesFlag, tt.source) + + result := getGitCommitHistoryValue(cmdCommand, tt.ffEnabled) + + assert.Equal(t, tt.expectedValue, result, "Expected value=%s, got=%s", tt.expectedValue, result) + }) + } +} + +func TestGetGitCommitHistoryValue_WithWarnings(t *testing.T) { + // Create a temporary directory with .git for testing + tempDir := t.TempDir() + gitDir := filepath.Join(tempDir, ".git") + _ = os.Mkdir(gitDir, 0755) + + // Create a directory without .git + tempDirNoGit := t.TempDir() + + tests := []struct { + name string + flagValue string + scanTypes string + scsEngines string + source string + ffEnabled bool + expectedValue string + expectWarnings string + }{ + { + name: "Flag true with FF disabled - returns empty with warning", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: false, + expectedValue: "", + expectWarnings: gitCommitHistoryNotAvailableWarningMsg, + }, + { + name: "Flag false with FF disabled - returns empty with warning", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: false, + expectedValue: "", + expectWarnings: gitCommitHistoryNotAvailableWarningMsg, + }, + { + name: "Flag true without SCS scan type - returns empty with warning", + flagValue: "true", + scanTypes: "sast", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNotSelectedWarningMsg, + }, + { + name: "Flag false without SCS scan type - returns empty with warning", + flagValue: "false", + scanTypes: "sast", + scsEngines: "secret-detection", + source: tempDir, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNotSelectedWarningMsg, + }, + { + name: "Flag true with only scorecard engine - returns empty with warning", + flagValue: "true", + scanTypes: "scs", + scsEngines: "scorecard", + source: tempDir, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNotApplicableWarningMsg, + }, + { + name: "Flag false with only scorecard engine - returns empty with warning", + flagValue: "false", + scanTypes: "scs", + scsEngines: "scorecard", + source: tempDir, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNotApplicableWarningMsg, + }, + { + name: "Flag true without git repository - returns empty with warning", + flagValue: "true", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDirNoGit, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNoGitRepositoryWarningMsg, + }, + { + name: "Flag false without git repository - returns empty with warning", + flagValue: "false", + scanTypes: "scs", + scsEngines: "secret-detection", + source: tempDirNoGit, + ffEnabled: true, + expectedValue: "", + expectWarnings: gitCommitHistoryNoGitRepositoryWarningMsg, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdCommand := &cobra.Command{ + Use: "scan", + Short: "Scan a project with git commit history", + } + cmdCommand.PersistentFlags().String(commonParams.GitCommitHistoryFlag, "", commonParams.GitCommitHistoryFlagDescription) + cmdCommand.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types") + cmdCommand.PersistentFlags().String(commonParams.SCSEnginesFlag, "", "SCS engines") + cmdCommand.PersistentFlags().String(commonParams.SourcesFlag, "", "Sources") + + _ = cmdCommand.Execute() + + _ = cmdCommand.Flags().Set(commonParams.GitCommitHistoryFlag, tt.flagValue) + _ = cmdCommand.Flags().Set(commonParams.ScanTypes, tt.scanTypes) + _ = cmdCommand.Flags().Set(commonParams.SCSEnginesFlag, tt.scsEngines) + _ = cmdCommand.Flags().Set(commonParams.SourcesFlag, tt.source) + + // Capture output for warnings + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + result := getGitCommitHistoryValue(cmdCommand, tt.ffEnabled) + + w.Close() + os.Stdout = oldStdout + + // Read captured output + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + r.Close() + output := buf.String() + + assert.Equal(t, tt.expectedValue, result, "Expected value=%s, got=%s", tt.expectedValue, result) + assert.Assert(t, strings.Contains(output, tt.expectWarnings), + "Expected warning containing '%s' not found in output: %s", tt.expectWarnings, output) + }) + } +} diff --git a/internal/params/flags.go b/internal/params/flags.go index 6f83f10ea..85abf3a4c 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -242,6 +242,10 @@ const ( SCSRepoTokenFlag = "scs-repo-token" SCSRepoURLFlag = "scs-repo-url" + // Secret Detection Git Commit History + GitCommitHistoryFlag = "git-commit-history" + GitCommitHistoryFlagDescription = "Enable or disable commit history scan for Secret Detection" + // Containers Config Flags ContainersFileFolderFilterFlag = "containers-file-folder-filter" ContainersImageTagFilterFlag = "containers-image-tag-filter" diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index 6762e860f..ad1b3afab 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -15,6 +15,7 @@ const SastCustomStateEnabled = "SAST_CUSTOM_STATES_ENABLED" const RiskManagementEnabled = "RISK_MANAGEMENT_IDES_PROJECT_RESULTS_SCORES_API_ENABLED" const OssRealtimeEnabled = "OSS_REALTIME_ENABLED" const ScsLicensingV2Enabled = "SSCS_NEW_LICENSING_ENABLED" +const SscsCommitHistoryEnabled = "SSCS_COMMIT_HISTORY_ENABLED" const DirectAssociationEnabled = "DIRECT_APP_ASSOCIATION_ENABLED" const maxRetries = 3 const IncreaseFileUploadLimit = "INCREASE_FILE_UPLOAD_LIMIT" diff --git a/internal/wrappers/mock/feature-flags-mock.go b/internal/wrappers/mock/feature-flags-mock.go index 44eb85db3..b292821b8 100644 --- a/internal/wrappers/mock/feature-flags-mock.go +++ b/internal/wrappers/mock/feature-flags-mock.go @@ -27,6 +27,21 @@ func (f FeatureFlagsMockWrapper) GetSpecificFlag(specificFlag string) (*wrappers fmt.Println(FFErr) return nil, FFErr } + + // If Flags (plural) is set, search for the specific flag by name + if len(Flags) > 0 { + for _, flag := range Flags { + if flag.Name == specificFlag { + fmt.Println("Returning flag from Flags collection:", flag.Status, "for flag name:", flag.Name) + return &wrappers.FeatureFlagResponseModel{Name: flag.Name, Status: flag.Status}, nil + } + } + // If flag not found in collection, return default (false) + fmt.Println("Flag not found in Flags collection, returning default (false) for flag:", specificFlag) + return &wrappers.FeatureFlagResponseModel{Name: specificFlag, Status: false}, nil + } + + // Otherwise, return the single Flag (backward compatibility) fmt.Println("Returning flag:", Flag.Status, "for flag name:", Flag.Name) return &Flag, nil } diff --git a/internal/wrappers/scans.go b/internal/wrappers/scans.go index b1dc5a028..71c878a0b 100644 --- a/internal/wrappers/scans.go +++ b/internal/wrappers/scans.go @@ -157,8 +157,9 @@ type APISecConfig struct { } type SCSConfig struct { - Twoms string `json:"2ms,omitempty"` - Scorecard string `json:"scorecard,omitempty"` - RepoURL string `json:"repoUrl,omitempty"` - RepoToken string `json:"repoToken,omitempty"` + Twoms string `json:"2ms,omitempty"` + Scorecard string `json:"scorecard,omitempty"` + RepoURL string `json:"repoUrl,omitempty"` + RepoToken string `json:"repoToken,omitempty"` + GitCommitHistory string `json:"gitCommitHistory,omitempty"` }