diff --git a/internal/commands/hooks.go b/internal/commands/hooks.go index 0651f8629..cfb304b19 100644 --- a/internal/commands/hooks.go +++ b/internal/commands/hooks.go @@ -9,7 +9,7 @@ import ( ) // NewHooksCommand creates the hooks command with pre-commit subcommand -func NewHooksCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func NewHooksCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { hooksCmd := &cobra.Command{ Use: "hooks", Short: "Manage Git hooks", @@ -31,19 +31,27 @@ func NewHooksCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { } // Add pre-commit and pre-receive subcommand - hooksCmd.AddCommand(PreCommitCommand(jwtWrapper)) - hooksCmd.AddCommand(PreReceiveCommand(jwtWrapper)) + hooksCmd.AddCommand(PreCommitCommand(jwtWrapper, featureFlagsWrapper)) + hooksCmd.AddCommand(PreReceiveCommand(jwtWrapper, featureFlagsWrapper)) return hooksCmd } -func validateLicense(jwtWrapper wrappers.JWTWrapper) error { - allowed, err := jwtWrapper.IsAllowedEngine(params.EnterpriseSecretsLabel) +func validateLicense(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) error { + scsLicensingV2Flag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.ScsLicensingV2Enabled) + var licenseName string + if scsLicensingV2Flag.Status { + licenseName = params.SecretDetectionLabel + } else { + licenseName = params.EnterpriseSecretsLabel + } + + allowed, err := jwtWrapper.IsAllowedEngine(licenseName) if err != nil { return errors.Wrapf(err, "Failed checking license") } if !allowed { - return errors.New("Error: License validation failed. Please verify your CxOne license includes Enterprise Secrets.") + return errors.Errorf("Error: License validation failed. Please verify your CxOne license includes %s.", licenseName) } return nil } diff --git a/internal/commands/pre-receive.go b/internal/commands/pre-receive.go index 9e58f8a1d..4b7457c8d 100644 --- a/internal/commands/pre-receive.go +++ b/internal/commands/pre-receive.go @@ -15,7 +15,7 @@ const ( SuccessFullSecretsLicenceValidation = "License for pre-receive secret detection has been validated successfully" ) -func PreReceiveCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func PreReceiveCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { preReceiveCmd := &cobra.Command{ Use: "pre-receive", Short: "Manage pre-receive hooks and run secret detection scans", @@ -27,7 +27,7 @@ func PreReceiveCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { ), } preReceiveCmd.AddCommand(scanSecretsPreReceiveCommand()) - preReceiveCmd.AddCommand(validateSecretsLicence(jwtWrapper)) + preReceiveCmd.AddCommand(validateSecretsLicence(jwtWrapper, featureFlagsWrapper)) return preReceiveCmd } @@ -54,7 +54,7 @@ func scanSecretsPreReceiveCommand() *cobra.Command { return scanPrereceiveCmd } -func validateSecretsLicence(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func validateSecretsLicence(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { validateLicence := &cobra.Command{ Use: "validate", Short: "Validates the license for pre-receive secret detection", @@ -64,19 +64,27 @@ func validateSecretsLicence(jwtWrapper wrappers.JWTWrapper) *cobra.Command { $ cx hooks pre-receive validate `, ), - RunE: checkLicence(jwtWrapper), + RunE: checkLicence(jwtWrapper, featureFlagsWrapper), } return validateLicence } -func checkLicence(jwtWrapper wrappers.JWTWrapper) func(cmd *cobra.Command, args []string) error { +func checkLicence(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - isAllowed, err := jwtWrapper.IsAllowedEngine(params.EnterpriseSecretsLabel) + scsLicensingV2Flag, _ := wrappers.GetSpecificFeatureFlag(featureFlagsWrapper, wrappers.ScsLicensingV2Enabled) + var licenseName string + if scsLicensingV2Flag.Status { + licenseName = params.SecretDetectionLabel + } else { + licenseName = params.EnterpriseSecretsLabel + } + + isAllowed, err := jwtWrapper.IsAllowedEngine(licenseName) if err != nil { log.Fatalf("%s: %s", "Failed the licence check", err) } if !isAllowed { - log.Fatalf("Error: License validation failed. Please ensure that your Checkmarx One license includes Enterprise Secrets") + log.Fatalf("Error: License validation failed. Please ensure that your Checkmarx One license includes %s", licenseName) } _, _ = fmt.Fprintln(cmd.OutOrStdout(), SuccessFullSecretsLicenceValidation) return nil diff --git a/internal/commands/pre_commit.go b/internal/commands/pre_commit.go index 2509efed4..44ff7c3ec 100644 --- a/internal/commands/pre_commit.go +++ b/internal/commands/pre_commit.go @@ -12,7 +12,7 @@ import ( // PreCommitCommand creates the pre-commit subcommand -func PreCommitCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func PreCommitCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { preCommitCmd := &cobra.Command{ Use: "pre-commit", Short: "Manage pre-commit hooks and run secret detection scans", @@ -26,11 +26,11 @@ func PreCommitCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { } preCommitCmd.PersistentFlags().Bool("global", false, "Install the hook globally for all repositories") - preCommitCmd.AddCommand(secretsInstallGitHookCommand(jwtWrapper)) - preCommitCmd.AddCommand(secretsUninstallGitHookCommand(jwtWrapper)) - preCommitCmd.AddCommand(secretsUpdateGitHookCommand(jwtWrapper)) - preCommitCmd.AddCommand(secretsScanCommand(jwtWrapper)) - preCommitCmd.AddCommand(secretsIgnoreCommand(jwtWrapper)) + preCommitCmd.AddCommand(secretsInstallGitHookCommand(jwtWrapper, featureFlagsWrapper)) + preCommitCmd.AddCommand(secretsUninstallGitHookCommand()) + preCommitCmd.AddCommand(secretsUpdateGitHookCommand(jwtWrapper, featureFlagsWrapper)) + preCommitCmd.AddCommand(secretsScanCommand(jwtWrapper, featureFlagsWrapper)) + preCommitCmd.AddCommand(secretsIgnoreCommand(jwtWrapper, featureFlagsWrapper)) preCommitCmd.AddCommand(secretsHelpCommand()) return preCommitCmd @@ -38,7 +38,7 @@ func PreCommitCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { // / validateLicense verifies the user has the required license for secret detection -func secretsInstallGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func secretsInstallGitHookCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { cmd := &cobra.Command{ Use: "secrets-install-git-hook", Short: "Install the pre-commit hook", @@ -49,7 +49,7 @@ func secretsInstallGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command `, ), PreRunE: func(cmd *cobra.Command, args []string) error { - return validateLicense(jwtWrapper) + return validateLicense(jwtWrapper, featureFlagsWrapper) }, RunE: func(cmd *cobra.Command, args []string) error { global, _ := cmd.Flags().GetBool("global") @@ -60,7 +60,7 @@ func secretsInstallGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command return cmd } -func secretsUninstallGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func secretsUninstallGitHookCommand() *cobra.Command { cmd := &cobra.Command{ Use: "secrets-uninstall-git-hook", Short: "Uninstall the pre-commit hook", @@ -79,7 +79,7 @@ func secretsUninstallGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Comma return cmd } -func secretsUpdateGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func secretsUpdateGitHookCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { cmd := &cobra.Command{ Use: "secrets-update-git-hook", Short: "Update the pre-commit hook", @@ -90,7 +90,7 @@ func secretsUpdateGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command `, ), PreRunE: func(cmd *cobra.Command, args []string) error { - return validateLicense(jwtWrapper) + return validateLicense(jwtWrapper, featureFlagsWrapper) }, RunE: func(cmd *cobra.Command, args []string) error { global, _ := cmd.Flags().GetBool("global") @@ -101,7 +101,7 @@ func secretsUpdateGitHookCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command return cmd } -func secretsScanCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func secretsScanCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { return &cobra.Command{ Use: "secrets-scan", Short: "Run the real-time secret detection scan", @@ -112,7 +112,7 @@ func secretsScanCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { `, ), PreRunE: func(cmd *cobra.Command, args []string) error { - return validateLicense(jwtWrapper) + return validateLicense(jwtWrapper, featureFlagsWrapper) }, RunE: func(cmd *cobra.Command, args []string) error { return precommit.Scan() @@ -120,7 +120,7 @@ func secretsScanCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { } } -func secretsIgnoreCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { +func secretsIgnoreCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrappers.FeatureFlagsWrapper) *cobra.Command { var resultIds string var all bool @@ -135,7 +135,7 @@ func secretsIgnoreCommand(jwtWrapper wrappers.JWTWrapper) *cobra.Command { `, ), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := validateLicense(jwtWrapper); err != nil { + if err := validateLicense(jwtWrapper, featureFlagsWrapper); err != nil { return err } diff --git a/internal/commands/pre_commit_test.go b/internal/commands/pre_commit_test.go index 5e20d8308..c77c54c50 100644 --- a/internal/commands/pre_commit_test.go +++ b/internal/commands/pre_commit_test.go @@ -3,13 +3,15 @@ package commands import ( "testing" + "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/checkmarx/ast-cli/internal/wrappers/mock" "github.com/stretchr/testify/assert" ) func TestNewHooksCommand(t *testing.T) { mockJWT := &mock.JWTMockWrapper{} - cmd := NewHooksCommand(mockJWT) + mockFF := &mock.FeatureFlagsMockWrapper{} + cmd := NewHooksCommand(mockJWT, mockFF) assert.NotNil(t, cmd) assert.Equal(t, "hooks", cmd.Use) @@ -18,7 +20,8 @@ func TestNewHooksCommand(t *testing.T) { func TestPreCommitCommand(t *testing.T) { mockJWT := &mock.JWTMockWrapper{} - cmd := PreCommitCommand(mockJWT) + mockFF := &mock.FeatureFlagsMockWrapper{} + cmd := PreCommitCommand(mockJWT, mockFF) assert.NotNil(t, cmd) assert.Equal(t, "pre-commit", cmd.Use) @@ -45,32 +48,69 @@ func TestPreCommitCommand(t *testing.T) { func TestValidateLicense(t *testing.T) { tests := []struct { - name string - aiEnabled int - wantError bool + name string + scsLicensingV2 bool + allowSecretDetection int + allowEnterpriseSecrets int + aiEnabled int + wantErr bool }{ { - name: "License is valid", - aiEnabled: 0, - wantError: false, + name: "scsLicensing V2 ON and Secret Detection allowed", + scsLicensingV2: true, + allowSecretDetection: 0, + allowEnterpriseSecrets: mock.EnterpriseSecretsDisabled, + wantErr: false, }, { - name: "License is invalid", - aiEnabled: mock.AIProtectionDisabled, - wantError: true, + name: "scsLicensing V2 ON and Secret Detection denied", + scsLicensingV2: true, + allowSecretDetection: mock.SecretDetectionDisabled, + allowEnterpriseSecrets: 0, + wantErr: true, + }, + { + name: "scsLicensing V2 OFF and Enterprise Secrets allowed", + scsLicensingV2: false, + allowSecretDetection: mock.SecretDetectionDisabled, + allowEnterpriseSecrets: 0, + wantErr: false, + }, + { + name: "scsLicensing V2 OFF and Enterprise Secrets denied", + scsLicensingV2: false, + allowSecretDetection: 0, + allowEnterpriseSecrets: mock.EnterpriseSecretsDisabled, + wantErr: true, + }, + { + name: "AI enabled and secrets license disabled", + aiEnabled: 1, + allowEnterpriseSecrets: mock.EnterpriseSecretsDisabled, + allowSecretDetection: mock.SecretDetectionDisabled, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockJWT := &mock.JWTMockWrapper{ - AIEnabled: tt.aiEnabled, + wrappers.ClearCache() + mockFF := &mock.FeatureFlagsMockWrapper{} + mock.Flag = wrappers.FeatureFlagResponseModel{ + Name: wrappers.ScsLicensingV2Enabled, + Status: tt.scsLicensingV2, } - err := validateLicense(mockJWT) + mockJWT := &mock.JWTMockWrapper{} + mockJWT.AIEnabled = tt.aiEnabled + mockJWT.EnterpriseSecretsEnabled = tt.allowEnterpriseSecrets + mockJWT.SecretDetectionEnabled = tt.allowSecretDetection + + err := validateLicense(mockJWT, mockFF) - if tt.wantError { + if tt.wantErr { assert.Error(t, err) + assert.Contains(t, err.Error(), "License validation failed") } else { assert.NoError(t, err) } @@ -80,7 +120,8 @@ func TestValidateLicense(t *testing.T) { func TestSecretsIgnoreCommand(t *testing.T) { mockJWT := &mock.JWTMockWrapper{} - cmd := secretsIgnoreCommand(mockJWT) + mockFF := &mock.FeatureFlagsMockWrapper{} + cmd := secretsIgnoreCommand(mockJWT, mockFF) assert.NotNil(t, cmd) resultIdsFlag := cmd.Flag("resultIds") diff --git a/internal/commands/pre_receive_test.go b/internal/commands/pre_receive_test.go index e6618c68c..f141cd478 100644 --- a/internal/commands/pre_receive_test.go +++ b/internal/commands/pre_receive_test.go @@ -11,7 +11,8 @@ import ( func TestPreReceiveCommand(t *testing.T) { mockJWT := &mock.JWTMockWrapper{} - cmd := PreReceiveCommand(mockJWT) + mockFF := &mock.FeatureFlagsMockWrapper{} + cmd := PreReceiveCommand(mockJWT, mockFF) assert.NotNil(t, cmd) assert.Equal(t, "pre-receive", cmd.Use) subCmds := cmd.Commands() diff --git a/internal/commands/root.go b/internal/commands/root.go index dfeb2de76..1613c5fbd 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -225,7 +225,7 @@ func NewAstCLI( triageCmd := NewResultsPredicatesCommand(resultsPredicatesWrapper, featureFlagsWrapper, customStatesWrapper) chatCmd := NewChatCommand(chatWrapper, tenantWrapper) - hooksCmd := NewHooksCommand(jwtWrapper) + hooksCmd := NewHooksCommand(jwtWrapper, featureFlagsWrapper) telemetryCmd := NewTelemetryCommand(telemetryWrapper) rootCmd.AddCommand( scanCmd, diff --git a/internal/wrappers/mock/jwt-helper-mock.go b/internal/wrappers/mock/jwt-helper-mock.go index 2ef794c48..1286b4530 100644 --- a/internal/wrappers/mock/jwt-helper-mock.go +++ b/internal/wrappers/mock/jwt-helper-mock.go @@ -9,12 +9,16 @@ import ( type JWTMockWrapper struct { AIEnabled int + EnterpriseSecretsEnabled int + SecretDetectionEnabled int CheckmarxOneAssistEnabled int CustomGetAllowedEngines func(wrappers.FeatureFlagsWrapper) (map[string]bool, error) } const AIProtectionDisabled = 1 const CheckmarxOneAssistDisabled = 1 +const EnterpriseSecretsDisabled = 1 +const SecretDetectionDisabled = 1 var engines = []string{"sast", "sca", "api-security", "iac-security", "scs", "containers", "enterprise-secrets"} @@ -38,13 +42,27 @@ func (*JWTMockWrapper) ExtractTenantFromToken() (tenant string, err error) { // IsAllowedEngine mock for tests func (j *JWTMockWrapper) IsAllowedEngine(engine string) (bool, error) { - if engine == params.AiProviderFlag || engine == params.EnterpriseSecretsLabel { + if engine == params.AiProviderFlag { if j.AIEnabled == AIProtectionDisabled { return false, nil } return true, nil } + if engine == params.EnterpriseSecretsLabel { + if j.EnterpriseSecretsEnabled == EnterpriseSecretsDisabled { + return false, nil + } + return true, nil + } + + if engine == params.SecretDetectionLabel { + if j.SecretDetectionEnabled == SecretDetectionDisabled { + return false, nil + } + return true, nil + } + if engine == params.CheckmarxOneAssistType { if j.CheckmarxOneAssistEnabled == CheckmarxOneAssistDisabled { return false, nil