diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdb0d87ee..1938aff9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,10 @@ jobs: BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }} BITBUCKET_PASSWORD: ${{ secrets.BITBUCKET_PASSWORD }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} + PR_BITBUCKET_TOKEN: ${{ secrets.PR_BITBUCKET_TOKEN }} + PR_BITBUCKET_NAMESPACE: "AstSystemTest" + PR_BITBUCKET_REPO_NAME: "cliIntegrationTest" + PR_BITBUCKET_ID: 1 run: | sudo chmod +x ./internal/commands/.scripts/integration_up.sh ./internal/commands/.scripts/integration_down.sh ./internal/commands/.scripts/integration_up.sh diff --git a/.github/workflows/manual-integration-test.yml b/.github/workflows/manual-integration-test.yml index 59a30b05d..cf3eebbdd 100644 --- a/.github/workflows/manual-integration-test.yml +++ b/.github/workflows/manual-integration-test.yml @@ -87,6 +87,10 @@ jobs: BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }} BITBUCKET_PASSWORD: ${{ secrets.BITBUCKET_PASSWORD }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} + PR_BITBUCKET_TOKEN: ${{ secrets.PR_BITBUCKET_TOKEN }} + PR_BITBUCKET_NAMESPACE: "AstSystemTest" + PR_BITBUCKET_REPO_NAME: "cliIntegrationTest" + PR_BITBUCKET_ID: 1 run: | sudo chmod +x ./internal/commands/.scripts/integration_up.sh ./internal/commands/.scripts/integration_down.sh ./internal/commands/.scripts/integration_up.sh diff --git a/cmd/main.go b/cmd/main.go index 36ee14044..b809e61fc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -42,6 +42,8 @@ func main() { bfl := viper.GetString(params.BflPathKey) prDecorationGithubPath := viper.GetString(params.PRDecorationGithubPathKey) prDecorationGitlabPath := viper.GetString(params.PRDecorationGitlabPathKey) + bitbucketServerPath := viper.GetString(params.PRDecorationBitbucketServerPathKey) + bitbucketCloudPath := viper.GetString(params.PRDecorationBitbucketCloudPathKey) prDecorationAzurePath := viper.GetString(params.PRDecorationAzurePathKey) descriptionsPath := viper.GetString(params.DescriptionsPathKey) tenantConfigurationPath := viper.GetString(params.TenantConfigurationPathKey) @@ -73,7 +75,7 @@ func main() { bitBucketServerWrapper := bitbucketserver.NewBitbucketServerWrapper() gitLabWrapper := wrappers.NewGitLabWrapper() bflWrapper := wrappers.NewBflHTTPWrapper(bfl) - prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, prDecorationAzurePath) + prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, bitbucketCloudPath, bitbucketServerPath, prDecorationAzurePath) learnMoreWrapper := wrappers.NewHTTPLearnMoreWrapper(descriptionsPath) tenantConfigurationWrapper := wrappers.NewHTTPTenantConfigurationWrapper(tenantConfigurationPath) jwtWrapper := wrappers.NewJwtWrapper() diff --git a/internal/commands/util/pr.go b/internal/commands/util/pr.go index 4060e3217..e5b2dcdfc 100644 --- a/internal/commands/util/pr.go +++ b/internal/commands/util/pr.go @@ -3,6 +3,7 @@ package util import ( "fmt" "log" + "strings" "github.com/MakeNowJust/heredoc" "github.com/checkmarx/ast-cli/internal/commands/policymanagement" @@ -15,21 +16,23 @@ import ( ) const ( - failedCreatingGithubPrDecoration = "Failed creating github PR Decoration" - failedCreatingAzurePrDecoration = "Failed creating azure PR Decoration" - failedCreatingGitlabPrDecoration = "Failed creating gitlab MR Decoration" - errorCodeFormat = "%s: CODE: %d, %s\n" - policyErrorFormat = "%s: Failed to get scanID policy information" - waitDelayDefault = 5 - resultPolicyDefaultTimeout = 1 - failedGettingScanError = "Failed showing a scan" - noPRDecorationCreated = "A PR couldn't be created for this scan because it is still in progress." - githubOnPremURLSuffix = "/api/v3/repos/" - gitlabOnPremURLSuffix = "/api/v4/" - githubCloudURL = "https://api.github.com/repos/" - gitlabCloudURL = "https://gitlab.com" + gitlabOnPremURLSuffix - azureCloudURL = "https://dev.azure.com/" - errorAzureOnPremParams = "code-repository-url must be set when code-repository-username is set" + failedCreatingGithubPrDecoration = "Failed creating github PR Decoration" + failedCreatingGitlabPrDecoration = "Failed creating gitlab MR Decoration" + failedCreatingBitbucketPrDecoration = "Failed creating bitbucket PR Decoration" + failedCreatingAzurePrDecoration = "Failed creating azure PR Decoration" + errorCodeFormat = "%s: CODE: %d, %s\n" + policyErrorFormat = "%s: Failed to get scanID policy information" + waitDelayDefault = 5 + resultPolicyDefaultTimeout = 1 + failedGettingScanError = "Failed showing a scan" + noPRDecorationCreated = "A PR couldn't be created for this scan because it is still in progress." + githubOnPremURLSuffix = "/api/v3/repos/" + gitlabOnPremURLSuffix = "/api/v4/" + githubCloudURL = "https://api.github.com/repos/" + gitlabCloudURL = "https://gitlab.com" + gitlabOnPremURLSuffix + azureCloudURL = "https://dev.azure.com/" + bitbucketCloudURL = "bitbucket.org" + errorAzureOnPremParams = "code-repository-url must be set when code-repository-username is set" ) func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { @@ -45,10 +48,12 @@ func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers prDecorationGithub := PRDecorationGithub(prWrapper, policyWrapper, scansWrapper) prDecorationGitlab := PRDecorationGitlab(prWrapper, policyWrapper, scansWrapper) + prDecorationBitbucket := PRDecorationBitbucket(prWrapper, policyWrapper, scansWrapper) prDecorationAzure := PRDecorationAzure(prWrapper, policyWrapper, scansWrapper) cmd.AddCommand(prDecorationGithub) cmd.AddCommand(prDecorationGitlab) + cmd.AddCommand(prDecorationBitbucket) cmd.AddCommand(prDecorationAzure) return cmd } @@ -205,6 +210,46 @@ func PRDecorationAzure(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Poli return prDecorationAzure } +func PRDecorationBitbucket(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { + prDecorationBitbucket := &cobra.Command{ + Use: "bitbucket ", + Short: "Decorate bitbucket PR with vulnerabilities", + Long: "Decorate bitbucket PR with vulnerabilities", + Example: heredoc.Doc( + ` + $ cx utils pr bitbucket --scan-id --token --namespace --repo-name + --pr-id --code-repository-url + `, + ), + Annotations: map[string]string{ + "command:doc": heredoc.Doc( + ` + `, + ), + }, + RunE: runPRDecorationBitbucket(prWrapper, policyWrapper, scansWrapper), + } + + prDecorationBitbucket.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from") + prDecorationBitbucket.Flags().String(params.SCMTokenFlag, "", params.BitbucketTokenUsage) + prDecorationBitbucket.Flags().String(params.NamespaceFlag, "", fmt.Sprintf(params.NamespaceFlagUsage, "Bitbucket")) + prDecorationBitbucket.Flags().String(params.RepoNameFlag, "", fmt.Sprintf(params.RepoNameFlagUsage, "Bitbucket")) + prDecorationBitbucket.Flags().Int(params.PRBBIDFlag, 0, params.PRBBIDFlagUsage) + prDecorationBitbucket.Flags().String(params.ProjectKeyFlag, "", params.ProjectKeyFlagUsage) + prDecorationBitbucket.Flags().String(params.CodeRepositoryFlag, "", params.CodeRepositoryFlagUsage) + + // Set the value for token to mask the scm token + _ = viper.BindPFlag(params.SCMTokenFlag, prDecorationBitbucket.Flags().Lookup(params.SCMTokenFlag)) + + // mark all fields as required\ + _ = prDecorationBitbucket.MarkFlagRequired(params.ScanIDFlag) + _ = prDecorationBitbucket.MarkFlagRequired(params.SCMTokenFlag) + _ = prDecorationBitbucket.MarkFlagRequired(params.RepoNameFlag) + _ = prDecorationBitbucket.MarkFlagRequired(params.PRBBIDFlag) + + return prDecorationBitbucket +} + func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { scanID, _ := cmd.Flags().GetString(params.ScanIDFlag) @@ -320,7 +365,7 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers. APIURL: updatedAPIURL, } - prResponse, errorModel, err := prWrapper.PostGitlabPRDecoration(prModel) + prResponse, errorModel, err := prWrapper.PostPRDecoration(prModel) if err != nil { return err @@ -336,6 +381,53 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers. } } +func runPRDecorationBitbucket(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + scanID, _ := cmd.Flags().GetString(params.ScanIDFlag) + scmTokenFlag, _ := cmd.Flags().GetString(params.SCMTokenFlag) + namespaceFlag, _ := cmd.Flags().GetString(params.NamespaceFlag) + repoNameFlag, _ := cmd.Flags().GetString(params.RepoNameFlag) + prIDFlag, _ := cmd.Flags().GetInt(params.PRBBIDFlag) + apiURL, _ := cmd.Flags().GetString(params.CodeRepositoryFlag) + projectKey, _ := cmd.Flags().GetString(params.ProjectKeyFlag) + + isCloud, flagRequiredErr := checkIsCloudAndValidateFlag(apiURL, namespaceFlag, projectKey) + if flagRequiredErr != nil { + return flagRequiredErr + } + + scanRunningOrQueued, err := IsScanRunningOrQueued(scansWrapper, scanID) + + if err != nil { + return err + } + + if scanRunningOrQueued { + log.Println(noPRDecorationCreated) + return nil + } + + policies, policyError := getScanViolatedPolicies(scansWrapper, policyWrapper, scanID, cmd) + if policyError != nil { + return errors.Errorf(policyErrorFormat, failedCreatingBitbucketPrDecoration) + } + + prModel := createBBPRModel(isCloud, scanID, scmTokenFlag, namespaceFlag, repoNameFlag, prIDFlag, apiURL, projectKey, policies) + prResponse, errorModel, err := prWrapper.PostPRDecoration(prModel) + + if err != nil { + return err + } + + if errorModel != nil { + return errors.Errorf(errorCodeFormat, failedCreatingBitbucketPrDecoration, errorModel.Code, errorModel.Message) + } + + logger.Print(prResponse) + return nil + } +} + func runPRDecorationAzure(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { scanID, _ := cmd.Flags().GetString(params.ScanIDFlag) @@ -381,7 +473,7 @@ func runPRDecorationAzure(prWrapper wrappers.PRWrapper, policyWrapper wrappers.P Policies: policies, APIURL: updatedAPIURL, } - prResponse, errorModel, err := prWrapper.PostAzurePRDecoration(prModel) + prResponse, errorModel, err := prWrapper.PostPRDecoration(prModel) if err != nil { return err } @@ -415,6 +507,61 @@ func updateScmTokenForAzure(scmTokenFlag, codeRepositoryUserName string) string return scmTokenFlag } +func formatRepoNameSlugBB(repoName string) string { + repoSlug := strings.Replace(strings.TrimSpace(repoName), " ", "-", -1) + return repoSlug +} + +func checkIsCloudAndValidateFlag(apiURL, namespaceFlag, projectKey string) (bool, error) { + isCloud := isBitbucketCloud(apiURL) + flagRequiredErr := validateBitbucketFlags(isCloud, namespaceFlag, projectKey) + return isCloud, flagRequiredErr +} + +func validateBitbucketFlags(isCloud bool, namespaceFlag, projectKey string) error { + if isCloud { + if namespaceFlag == "" { + return errors.New("namespace is required for Bitbucket Cloud") + } + } else { + if projectKey == "" { + return errors.New("project key is required for Bitbucket Server") + } + } + return nil +} + +func isBitbucketCloud(apiURL string) bool { + if apiURL == "" || strings.Contains(apiURL, bitbucketCloudURL) { + return true + } + return false +} + +func createBBPRModel(isCloud bool, scanID, scmTokenFlag, namespaceFlag, repoNameFlag string, prIDFlag int, apiURL, projectKey string, policies []wrappers.PrPolicy) interface{} { + formattedRepoNameSlug := formatRepoNameSlugBB(repoNameFlag) + + if isCloud { + return &wrappers.BitbucketCloudPRModel{ + ScanID: scanID, + ScmToken: scmTokenFlag, + Namespace: namespaceFlag, + RepoName: formattedRepoNameSlug, + PRID: prIDFlag, + Policies: policies, + } + } + return &wrappers.BitbucketServerPRModel{ + ScanID: scanID, + ScmToken: scmTokenFlag, + ProjectKey: projectKey, + RepoName: formattedRepoNameSlug, + PRID: prIDFlag, + Policies: policies, + ServerURL: apiURL, + } +} + func getScanViolatedPolicies(scansWrapper wrappers.ScansWrapper, policyWrapper wrappers.PolicyWrapper, scanID string, cmd *cobra.Command) ([]wrappers.PrPolicy, error) { // retrieve scan model to get the projectID scanResponseModel, errorScanModel, err := scansWrapper.GetByID(scanID) diff --git a/internal/commands/util/pr_test.go b/internal/commands/util/pr_test.go index 96add7129..349092c84 100644 --- a/internal/commands/util/pr_test.go +++ b/internal/commands/util/pr_test.go @@ -79,6 +79,124 @@ func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL( asserts.Equal(t, gitlabCloudURL, cloudAPIURL) } +func TestCheckIsCloudAndValidateFlag(t *testing.T) { + tests := []struct { + name string + apiURL string + namespaceFlag string + projectKey string + expectedCloud bool + expectedError string + }{ + { + name: "Bitbucket Cloud", + apiURL: "", + namespaceFlag: "namespace", + projectKey: "", + expectedCloud: true, + expectedError: "", + }, + { + name: "Bitbucket Cloud without https", + apiURL: "bitbucket.org", + namespaceFlag: "namespace", + projectKey: "", + expectedCloud: true, + expectedError: "", + }, + { + name: "Bitbucket Cloud with namespace", + apiURL: "https://bitbucket.org", + namespaceFlag: "namespace", + projectKey: "", + expectedCloud: true, + expectedError: "", + }, + { + name: "Bitbucket Cloud without namespace", + apiURL: "https://bitbucket.org", + namespaceFlag: "", + projectKey: "", + expectedCloud: true, + expectedError: "namespace is required for Bitbucket Cloud", + }, + { + name: "Bitbucket Server with project key and API URL", + apiURL: "https://bitbucket.example.com", + namespaceFlag: "", + projectKey: "projectKey", + expectedCloud: false, + expectedError: "", + }, + { + name: "Bitbucket Server without project key", + apiURL: "https://bitbucket.example.com", + namespaceFlag: "", + projectKey: "", + expectedCloud: false, + expectedError: "project key is required for Bitbucket Server", + }, + { + name: "Bitbucket Cloud with URL and project key", + apiURL: "https://bitbucket.org", + namespaceFlag: "", + projectKey: "projectKey", + expectedCloud: true, + expectedError: "namespace is required for Bitbucket Cloud", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + isCloud, err := checkIsCloudAndValidateFlag(tt.apiURL, tt.namespaceFlag, tt.projectKey) + asserts.Equal(t, tt.expectedCloud, isCloud) + if tt.expectedError != "" { + asserts.EqualError(t, err, tt.expectedError) + } else { + asserts.NoError(t, err) + } + }) + } +} + +func TestRepoSlugFormatBB(t *testing.T) { + tests := []struct { + name string + repoNameFlag string + expectedSlug string + }{ + { + name: "Single word repo name", + repoNameFlag: "repository", + expectedSlug: "repository", + }, + { + name: "Repo name with spaces", + repoNameFlag: "my repository", + expectedSlug: "my-repository", + }, + { + name: "Repo name with multiple spaces", + repoNameFlag: "my awesome repository", + expectedSlug: "my-awesome-repository", + }, + { + name: "Repo name with leading and trailing spaces", + repoNameFlag: " my repository ", + expectedSlug: "my-repository", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + slug := formatRepoNameSlugBB(tt.repoNameFlag) + asserts.Equal(t, tt.expectedSlug, slug) + }) + } +} + func TestGetAzureAPIURL_whenAPIURLIsSet_ShouldUpdateAPIURL(t *testing.T) { selfHostedURL := "https://azure.example.com" updatedAPIURL := getAzureAPIURL(selfHostedURL) diff --git a/internal/params/binds.go b/internal/params/binds.go index 71b359d1d..cdceebd90 100644 --- a/internal/params/binds.go +++ b/internal/params/binds.go @@ -28,6 +28,8 @@ var EnvVarsBinds = []struct { {BflPathKey, BflPathEnv, "api/bfl"}, {PRDecorationGithubPathKey, PRDecorationGithubPathEnv, "api/flow-publisher/pr/github"}, {PRDecorationGitlabPathKey, PRDecorationGitlabPathEnv, "api/flow-publisher/pr/gitlab"}, + {PRDecorationBitbucketCloudPathKey, PRDecorationBitbucketCloudPathEnv, "api/flow-publisher/pr/bitbucket"}, + {PRDecorationBitbucketServerPathKey, PRDecorationBitbucketServerPathEnv, "api/flow-publisher/pr/bitbucket-server"}, {PRDecorationAzurePathKey, PRDecorationAzurePathEnv, "api/flow-publisher/pr/azure"}, {DescriptionsPathKey, DescriptionsPathEnv, "api/queries/descriptions"}, {TenantConfigurationPathKey, TenantConfigurationPathEnv, "api/configuration/tenant"}, diff --git a/internal/params/envs.go b/internal/params/envs.go index cd29f1081..337030699 100644 --- a/internal/params/envs.go +++ b/internal/params/envs.go @@ -31,6 +31,8 @@ const ( PRDecorationGithubPathEnv = "CX_PR_DECORATION_GITHUB_PATH" PRDecorationGitlabPathEnv = "CX_PR_DECORATION_GITLAB_PATH" PRDecorationAzurePathEnv = "CX_PR_DECORATION_AZURE_PATH" + PRDecorationBitbucketCloudPathEnv = "CX_PR_DECORATION_BITBUCKET_CLOUD_PATH" + PRDecorationBitbucketServerPathEnv = "CX_PR_DECORATION_BITBUCKET_SERVER_PATH" SastRmPathEnv = "CX_SAST_RM_PATH" UploadsPathEnv = "CX_UPLOADS_PATH" TokenExpirySecondsEnv = "CX_TOKEN_EXPIRY_SECONDS" diff --git a/internal/params/flags.go b/internal/params/flags.go index 8c364f1f7..c1c00d6ab 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -34,7 +34,7 @@ const ( BranchFlagSh = "b" ScanIDFlag = "scan-id" CodeRepositoryFlag = "code-repository-url" - CodeRepositoryFlagUsage = "Code repository URL (optional for self-hosted SCMs)" + CodeRepositoryFlagUsage = "Code repository URL (required for self-hosted SCMs)" BranchFlagUsage = "Branch to scan" MainBranchFlag = "branch" ScaResolverFlag = "sca-resolver" @@ -121,6 +121,7 @@ const ( AzureTokenUsage = "Azure DevOps personal access token. Requires “Connected server” and “Code“ scope." GithubTokenUsage = "GitHub OAuth token. Requires “Repo” scope and organization SSO authorization, if enforced by the organization" GitLabTokenUsage = "GitLab OAuth token" + BitbucketTokenUsage = "Bitbucket OAuth token" BotCount = "Note: dependabot is not counted but other bots might be considered as contributors." DisabledReposCount = "Note: Disabled repositories are not counted." URLFlag = "url" @@ -175,6 +176,10 @@ const ( AzureProjectFlagUsage = "Azure project name or project ID" CodeRespositoryUsernameFlag = "code-repository-username" CodeRespositoryUsernameFlagUsage = "Azure username for code repository" + ProjectKeyFlag = "project-key" + ProjectKeyFlagUsage = "Key of the project containing the repository" + PRBBIDFlag = "pr-id" + PRBBIDFlagUsage = "Bitbucket PR ID" // Chat (General) ChatAPIKey = "chat-apikey" diff --git a/internal/params/keys.go b/internal/params/keys.go index ed23c6b9a..6ac7f3215 100644 --- a/internal/params/keys.go +++ b/internal/params/keys.go @@ -28,6 +28,8 @@ var ( BflPathKey = strings.ToLower(BflPathEnv) PRDecorationGithubPathKey = strings.ToLower(PRDecorationGithubPathEnv) PRDecorationGitlabPathKey = strings.ToLower(PRDecorationGitlabPathEnv) + PRDecorationBitbucketCloudPathKey = strings.ToLower(PRDecorationBitbucketCloudPathEnv) + PRDecorationBitbucketServerPathKey = strings.ToLower(PRDecorationBitbucketServerPathEnv) PRDecorationAzurePathKey = strings.ToLower(PRDecorationAzurePathEnv) UploadsPathKey = strings.ToLower(UploadsPathEnv) SastRmPathKey = strings.ToLower(SastRmPathEnv) diff --git a/internal/wrappers/mock/pr-mock.go b/internal/wrappers/mock/pr-mock.go index e608fec9b..00289ae09 100644 --- a/internal/wrappers/mock/pr-mock.go +++ b/internal/wrappers/mock/pr-mock.go @@ -2,23 +2,32 @@ package mock import ( "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" ) +const prCommentSuccess = "PR comment created successfully." + type PRMockWrapper struct { } -func (pr *PRMockWrapper) PostPRDecoration(model *wrappers.PRModel) ( +func (pr *PRMockWrapper) PostPRDecoration(model interface{}) ( string, *wrappers.WebError, error, ) { - return "PR comment created successfully.", nil, nil -} - -func (pr *PRMockWrapper) PostGitlabPRDecoration(model *wrappers.GitlabPRModel) (string, *wrappers.WebError, error) { - return "MR comment created successfully.", nil, nil -} + switch model.(type) { + case *wrappers.PRModel: + return prCommentSuccess, nil, nil + case *wrappers.GitlabPRModel: + return "MR comment created successfully.", nil, nil + case *wrappers.BitbucketCloudPRModel: + return "Bitbucket Cloud PR comment created successfully.", nil, nil + case *wrappers.BitbucketServerPRModel: + return "Bitbucket Server PR comment created successfully.", nil, nil + case *wrappers.AzurePRModel: + return prCommentSuccess, nil, nil -func (pr *PRMockWrapper) PostAzurePRDecoration(model *wrappers.AzurePRModel) (string, *wrappers.WebError, error) { - return "PR comment created successfully.", nil, nil + default: + return "", nil, errors.New("unsupported model type") + } } diff --git a/internal/wrappers/pr-http.go b/internal/wrappers/pr-http.go index e114cea51..c74fe8be7 100644 --- a/internal/wrappers/pr-http.go +++ b/internal/wrappers/pr-http.go @@ -16,52 +16,35 @@ const ( ) type PRHTTPWrapper struct { - githubPath string - gitlabPath string - azurePath string + githubPath string + gitlabPath string + azurePath string + bitbucketCloudPath string + bitbucketServerPath string } -func NewHTTPPRWrapper(githubPath, gitlabPath, azurePath string) PRWrapper { +func NewHTTPPRWrapper(githubPath, gitlabPath, bitbucketCloudPath, bitbucketServerPath, azurePath string) PRWrapper { return &PRHTTPWrapper{ - githubPath: githubPath, - gitlabPath: gitlabPath, - azurePath: azurePath, + githubPath: githubPath, + gitlabPath: gitlabPath, + azurePath: azurePath, + bitbucketCloudPath: bitbucketCloudPath, + bitbucketServerPath: bitbucketServerPath, } } -func (r *PRHTTPWrapper) PostPRDecoration(model *PRModel) ( - string, - *WebError, - error, -) { - clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) - jsonBytes, err := json.Marshal(model) +func (r *PRHTTPWrapper) PostPRDecoration(model interface{}) (string, *WebError, error) { + url, err := r.getPRDecorationURL(model) if err != nil { return "", nil, err } - resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, r.githubPath, bytes.NewBuffer(jsonBytes), true, clientTimeout) - if err != nil { - return "", nil, err - } - defer func() { - if err == nil { - _ = resp.Body.Close() - } - }() - return handlePRResponseWithBody(resp, err) -} -func (r *PRHTTPWrapper) PostGitlabPRDecoration(model *GitlabPRModel) ( - string, - *WebError, - error, -) { clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) jsonBytes, err := json.Marshal(model) if err != nil { return "", nil, err } - resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, r.gitlabPath, bytes.NewBuffer(jsonBytes), true, clientTimeout) + resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, url, bytes.NewBuffer(jsonBytes), true, clientTimeout) if err != nil { return "", nil, err } @@ -73,26 +56,21 @@ func (r *PRHTTPWrapper) PostGitlabPRDecoration(model *GitlabPRModel) ( return handlePRResponseWithBody(resp, err) } -func (r *PRHTTPWrapper) PostAzurePRDecoration(model *AzurePRModel) ( - string, - *WebError, - error, -) { - clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) - jsonBytes, err := json.Marshal(model) - if err != nil { - return "", nil, err - } - resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, r.azurePath, bytes.NewBuffer(jsonBytes), true, clientTimeout) - if err != nil { - return "", nil, err +func (r *PRHTTPWrapper) getPRDecorationURL(model interface{}) (string, error) { + switch model.(type) { + case *PRModel: + return r.githubPath, nil + case *GitlabPRModel: + return r.gitlabPath, nil + case *BitbucketCloudPRModel: + return r.bitbucketCloudPath, nil + case *BitbucketServerPRModel: + return r.bitbucketServerPath, nil + case *AzurePRModel: + return r.azurePath, nil + default: + return "", errors.New("unsupported model type") } - defer func() { - if err == nil { - _ = resp.Body.Close() - } - }() - return handlePRResponseWithBody(resp, err) } func handlePRResponseWithBody(resp *http.Response, err error) (string, *WebError, error) { diff --git a/internal/wrappers/pr.go b/internal/wrappers/pr.go index 376541d33..c52f80c62 100644 --- a/internal/wrappers/pr.go +++ b/internal/wrappers/pr.go @@ -34,8 +34,24 @@ type AzurePRModel struct { APIURL string `json:"apiUrl"` } +type BitbucketCloudPRModel struct { + ScanID string `json:"scanId"` + ScmToken string `json:"scmToken"` + Namespace string `json:"namespace"` + RepoName string `json:"repoName"` + PRID int `json:"prId"` + Policies []PrPolicy `json:"violatedPolicyList"` +} + +type BitbucketServerPRModel struct { + ScanID string `json:"scanId"` + ScmToken string `json:"scmToken"` + ServerURL string `json:"apiUrl"` + ProjectKey string `json:"namespace"` + RepoName string `json:"repoName"` + PRID int `json:"prNumber"` + Policies []PrPolicy `json:"violatedPolicyList"` +} type PRWrapper interface { - PostPRDecoration(model *PRModel) (string, *WebError, error) - PostGitlabPRDecoration(model *GitlabPRModel) (string, *WebError, error) - PostAzurePRDecoration(model *AzurePRModel) (string, *WebError, error) + PostPRDecoration(model interface{}) (string, *WebError, error) } diff --git a/test/integration/pr_test.go b/test/integration/pr_test.go index 55560cb8e..b95c6125e 100644 --- a/test/integration/pr_test.go +++ b/test/integration/pr_test.go @@ -31,10 +31,15 @@ const ( prAzureOrganization = "AZURE_NEW_ORG" prAzureProject = "AZURE_PROJECT_NAME" prAzureNumber = "AZURE_PR_NUMBER" + prBBToken = "PR_BITBUCKET_TOKEN" + prBBNamespace = "PR_BITBUCKET_NAMESPACE" + prBBId = "PR_BITBUCKET_ID" + prBBRepoName = "PR_BITBUCKET_REPO_NAME" prdDecorationForbiddenMessage = "A PR couldn't be created for this scan because it is still in progress." failedGettingScanError = "Failed showing a scan" githubPRCommentCreated = "github PR comment created successfully." gitlabPRCommentCreated = "gitlab PR comment created successfully." + BBPRCommentCreated = "bitbucket PR comment created successfully." azurePRCommentCreated = "azure PR comment created successfully." outputFileName = "test_output.log" scans = "api/scans" @@ -118,7 +123,7 @@ func TestPRGithubDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T "https://github.example.com", } - monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, *wrappers.PRModel) (string, *wrappers.WebError, error) { + monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, interface{}) (string, *wrappers.WebError, error) { return githubPRCommentCreated, nil, nil }) defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostPRDecoration) @@ -201,10 +206,10 @@ func TestPRGitlabDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T "https://gitlab.example.com", } - monkey.Patch((*wrappers.PRHTTPWrapper).PostGitlabPRDecoration, func(*wrappers.PRHTTPWrapper, *wrappers.GitlabPRModel) (string, *wrappers.WebError, error) { + monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, interface{}) (string, *wrappers.WebError, error) { return gitlabPRCommentCreated, nil, nil }) - defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostGitlabPRDecoration) + defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostPRDecoration) file := createOutputFile(t, outputFileName) defer deleteOutputFile(t, file) @@ -284,10 +289,10 @@ func TestPRAzureDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T) "https://azure.example.com", } - monkey.Patch((*wrappers.PRHTTPWrapper).PostAzurePRDecoration, func(*wrappers.PRHTTPWrapper, *wrappers.AzurePRModel) (string, *wrappers.WebError, error) { + monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, interface{}) (string, *wrappers.WebError, error) { return azurePRCommentCreated, nil, nil }) - defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostAzurePRDecoration) + defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostPRDecoration) file := createOutputFile(t, outputFileName) defer deleteOutputFile(t, file) @@ -416,3 +421,104 @@ func runPRTestForRunningScan(t *testing.T, args []string) { defer deleteOutputFile(t, file) defer logger.SetOutput(os.Stdout) } + +func TestPRBBOnCloudDecorationSuccessCase(t *testing.T) { + args := []string{ + "utils", + "pr", + "bitbucket", + flag(params.ScanIDFlag), + getCompletedScanID(t), + flag(params.SCMTokenFlag), + os.Getenv(prBBToken), + flag(params.NamespaceFlag), + os.Getenv(prBBNamespace), + flag(params.PRBBIDFlag), + os.Getenv(prBBId), + flag(params.RepoNameFlag), + os.Getenv(prBBRepoName), + "--debug", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "Error should be nil") +} + +func TestPRBBBDecorationFailure(t *testing.T) { + args := []string{ + "utils", + "pr", + "bitbucket", + flag(params.ScanIDFlag), + "fakeScanID", + flag(params.SCMTokenFlag), + os.Getenv(prBBToken), + flag(params.NamespaceFlag), + os.Getenv(prBBNamespace), + flag(params.PRBBIDFlag), + os.Getenv(prBBId), + flag(params.RepoNameFlag), + os.Getenv(prBBRepoName), + "--debug", + } + + err, _ := executeCommand(t, args...) + assert.ErrorContains(t, err, "scan not found") +} + +func TestPRBBDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T) { + args := []string{ + "utils", + "pr", + "bitbucket", + flag(params.ScanIDFlag), + getCompletedScanID(t), + flag(params.SCMTokenFlag), + "Token", + flag(params.ProjectKeyFlag), + "PROJECTKEY", + flag(params.PRBBIDFlag), + os.Getenv(prBBId), + flag(params.RepoNameFlag), + os.Getenv(prBBRepoName), + flag(params.CodeRepositoryFlag), + "https://bitbucket.example.com", + } + + monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, interface{}) (string, *wrappers.WebError, error) { + return BBPRCommentCreated, nil, nil + }) + defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostPRDecoration) + + file := createOutputFile(t, outputFileName) + defer deleteOutputFile(t, file) + defer logger.SetOutput(os.Stdout) + + err, _ := executeCommand(t, args...) + assert.NilError(t, err, "Error should be nil") + + stdoutString, err := util.ReadFileAsString(file.Name()) + if err != nil { + t.Fatalf("Failed to read log file: %v", err) + } + + assert.Equal(t, strings.Contains(stdoutString, BBPRCommentCreated), true, "Expected output: %s", BBPRCommentCreated) +} +func TestPRBitbucketDecoration_WhenScanIsRunning_ShouldAvoidPRDecorationCommand(t *testing.T) { + args := []string{ + "utils", + "pr", + "bitbucket", + flag(params.ScanIDFlag), + getRunningScanId(t), + flag(params.SCMTokenFlag), + os.Getenv(prBBToken), + flag(params.PRBBIDFlag), + os.Getenv(prBBId), + flag(params.RepoNameFlag), + os.Getenv(prBBRepoName), + flag(params.NamespaceFlag), + os.Getenv(prBBNamespace), + } + runPRTestForRunningScan(t, args) +} diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 3655a0ee1..b51abd0ca 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -74,6 +74,8 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { learnMore := viper.GetString(params.DescriptionsPathKey) prDecorationGithubPath := viper.GetString(params.PRDecorationGithubPathKey) prDecorationGitlabPath := viper.GetString(params.PRDecorationGitlabPathKey) + prDecorationBitbucketCloudPath := viper.GetString(params.PRDecorationBitbucketCloudPathKey) + prDecorationBitbucketServerPath := viper.GetString(params.PRDecorationBitbucketServerPathKey) prDecorationAzurePath := viper.GetString(params.PRDecorationAzurePathKey) tenantConfigurationPath := viper.GetString(params.TenantConfigurationPathKey) resultsPdfPath := viper.GetString(params.ResultsPdfReportPathKey) @@ -105,7 +107,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { bitBucketWrapper := wrappers.NewBitbucketWrapper() bflWrapper := wrappers.NewBflHTTPWrapper(bfl) learnMoreWrapper := wrappers.NewHTTPLearnMoreWrapper(learnMore) - prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, prDecorationAzurePath) + prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, prDecorationBitbucketCloudPath, prDecorationBitbucketServerPath, prDecorationAzurePath) tenantConfigurationWrapper := wrappers.NewHTTPTenantConfigurationWrapper(tenantConfigurationPath) jwtWrapper := wrappers.NewJwtWrapper() scaRealtimeWrapper := wrappers.NewHTTPScaRealTimeWrapper()