Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PR Decoration For BB On-cloud and On-prem(AST-70121) #939

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/manual-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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
Expand Down
4 changes: 3 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
descriptionsPath := viper.GetString(params.DescriptionsPathKey)
tenantConfigurationPath := viper.GetString(params.TenantConfigurationPathKey)
resultsPdfPath := viper.GetString(params.ResultsPdfReportPathKey)
Expand Down Expand Up @@ -72,7 +74,7 @@ func main() {
bitBucketServerWrapper := bitbucketserver.NewBitbucketServerWrapper()
gitLabWrapper := wrappers.NewGitLabWrapper()
bflWrapper := wrappers.NewBflHTTPWrapper(bfl)
prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath)
prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, bitbucketCloudPath, bitbucketServerPath)
learnMoreWrapper := wrappers.NewHTTPLearnMoreWrapper(descriptionsPath)
tenantConfigurationWrapper := wrappers.NewHTTPTenantConfigurationWrapper(tenantConfigurationPath)
jwtWrapper := wrappers.NewJwtWrapper()
Expand Down
173 changes: 160 additions & 13 deletions internal/commands/util/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package util
import (
"fmt"
"log"
"strings"

"github.com/MakeNowJust/heredoc"
"github.com/checkmarx/ast-cli/internal/commands/policymanagement"
Expand All @@ -15,18 +16,20 @@ import (
)

const (
failedCreatingGithubPrDecoration = "Failed creating github 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
failedCreatingGithubPrDecoration = "Failed creating github PR Decoration"
failedCreatingGitlabPrDecoration = "Failed creating gitlab MR Decoration"
failedCreatingBitbucketPrDecoration = "Failed creating bitbucket 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
AlvoBen marked this conversation as resolved.
Show resolved Hide resolved
bitbucketCloudURL = "bitbucket.org"
)

func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command {
Expand All @@ -42,9 +45,11 @@ func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers

prDecorationGithub := PRDecorationGithub(prWrapper, policyWrapper, scansWrapper)
prDecorationGitlab := PRDecorationGitlab(prWrapper, policyWrapper, scansWrapper)
prDecorationBitbucket := PRDecorationBitbucket(prWrapper, policyWrapper, scansWrapper)

cmd.AddCommand(prDecorationGithub)
cmd.AddCommand(prDecorationGitlab)
cmd.AddCommand(prDecorationBitbucket)
return cmd
}

Expand Down Expand Up @@ -159,6 +164,46 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Pol
return prDecorationGitlab
}

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 <scan-id> --token <PAT> --namespace <username (required for cloud services)> --repo-name <repository-slug>
--pr-id <pr number> --code-repository-url <bitbucket-server-url (required for self-hosted)>
`,
),
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)
Expand Down Expand Up @@ -267,7 +312,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
Expand All @@ -283,6 +328,108 @@ 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 := createPRModel(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)
AlvoBen marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
}

func repoSlugFormatBB(repoNameFlag string) string {
repoSlug := strings.Replace(repoNameFlag, " ", "-", -1)
AlvoBen marked this conversation as resolved.
Show resolved Hide resolved
return repoSlug
}

func checkIsCloudAndValidateFlag(apiURL, namespaceFlag, projectKey string) (bool, error) {
elchananarb marked this conversation as resolved.
Show resolved Hide resolved
isCloud := isBitbucketCloud(apiURL)
flagRequiredErr := validateBitbucketFlags(isCloud, namespaceFlag, projectKey)
return isCloud, flagRequiredErr
}

func validateBitbucketFlags(isCloud bool, namespaceFlag, projectKey string) error {
miryamfoiferCX marked this conversation as resolved.
Show resolved Hide resolved
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 {
miryamfoiferCX marked this conversation as resolved.
Show resolved Hide resolved
sarahCx marked this conversation as resolved.
Show resolved Hide resolved
if apiURL == "" || strings.Contains(apiURL, bitbucketCloudURL) {
AlvoBen marked this conversation as resolved.
Show resolved Hide resolved
return true
}
return false
}

func createPRModel(isCloud bool, scanID, scmTokenFlag, namespaceFlag, repoNameFlag string, prIDFlag int, apiURL, projectKey string, policies []wrappers.PrPolicy) interface{} {
miryamfoiferCX marked this conversation as resolved.
Show resolved Hide resolved
repoSlugFormatBB := repoSlugFormatBB(repoNameFlag)

miryamfoiferCX marked this conversation as resolved.
Show resolved Hide resolved
if isCloud {
return &wrappers.BitbucketCloudPRModel{
ScanID: scanID,
ScmToken: scmTokenFlag,
Namespace: namespaceFlag,
RepoName: repoSlugFormatBB,
PRID: prIDFlag,
Policies: policies,
}
}
return &wrappers.BitbucketServerPRModel{
ScanID: scanID,
ScmToken: scmTokenFlag,
ProjectKey: projectKey,
RepoName: repoSlugFormatBB,
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)
Expand Down
105 changes: 105 additions & 0 deletions internal/commands/util/pr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,108 @@ func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(
cloudAPIURL := updateAPIURLForGitlabOnPrem("")
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 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",
},
AlvoBen marked this conversation as resolved.
Show resolved Hide resolved
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
slug := repoSlugFormatBB(tt.repoNameFlag)
asserts.Equal(t, tt.expectedSlug, slug)
})
}
}
2 changes: 2 additions & 0 deletions internal/params/binds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
{DescriptionsPathKey, DescriptionsPathEnv, "api/queries/descriptions"},
{TenantConfigurationPathKey, TenantConfigurationPathEnv, "api/configuration/tenant"},
{UploadsPathKey, UploadsPathEnv, "api/uploads"},
Expand Down
2 changes: 2 additions & 0 deletions internal/params/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (
BflPathEnv = "CX_BFL_PATH"
PRDecorationGithubPathEnv = "CX_PR_DECORATION_GITHUB_PATH"
PRDecorationGitlabPathEnv = "CX_PR_DECORATION_GITLAB_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"
Expand Down
Loading
Loading