Skip to content

Commit

Permalink
Merge pull request #942 from Checkmarx/feature/MiryamFoifer/PRDecorat…
Browse files Browse the repository at this point in the history
…ionSupportOnADO

Add Pr-Decoration Support On ADO (AST-70135)
  • Loading branch information
miryamfoiferCX authored Nov 14, 2024
2 parents bfae48f + 2dedbbd commit e55d4cc
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 38 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ jobs:
AZURE_PROJECT: ${{ secrets.AZURE_PROJECT }}
AZURE_REPOS: ${{ secrets.AZURE_REPOS }}
AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }}
AZURE_NEW_ORG: "azureAccountTests"
AZURE_PROJECT_NAME: "testsProject"
AZURE_PR_NUMBER: 1
AZURE_NEW_TOKEN: ${{ secrets.AZURE_NEW_TOKEN }}
BITBUCKET_WORKSPACE: ${{ secrets.BITBUCKET_WORKSPACE }}
BITBUCKET_REPOS: ${{ secrets.BITBUCKET_REPOS }}
BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }}
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 @@ -78,6 +78,10 @@ jobs:
AZURE_PROJECT: ${{ secrets.AZURE_PROJECT }}
AZURE_REPOS: ${{ secrets.AZURE_REPOS }}
AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }}
AZURE_NEW_ORG: "azureAccountTests"
AZURE_PROJECT_NAME: "testsProject"
AZURE_PR_NUMBER: 1
AZURE_NEW_TOKEN: ${{ secrets.AZURE_NEW_TOKEN }}
BITBUCKET_WORKSPACE: ${{ secrets.BITBUCKET_WORKSPACE }}
BITBUCKET_REPOS: ${{ secrets.BITBUCKET_REPOS }}
BITBUCKET_USERNAME: ${{ secrets.BITBUCKET_USERNAME }}
Expand Down
3 changes: 2 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
bfl := viper.GetString(params.BflPathKey)
prDecorationGithubPath := viper.GetString(params.PRDecorationGithubPathKey)
prDecorationGitlabPath := viper.GetString(params.PRDecorationGitlabPathKey)
prDecorationAzurePath := viper.GetString(params.PRDecorationAzurePathKey)
descriptionsPath := viper.GetString(params.DescriptionsPathKey)
tenantConfigurationPath := viper.GetString(params.TenantConfigurationPathKey)
resultsPdfPath := viper.GetString(params.ResultsPdfReportPathKey)
Expand Down Expand Up @@ -72,7 +73,7 @@ func main() {
bitBucketServerWrapper := bitbucketserver.NewBitbucketServerWrapper()
gitLabWrapper := wrappers.NewGitLabWrapper()
bflWrapper := wrappers.NewBflHTTPWrapper(bfl)
prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath)
prWrapper := wrappers.NewHTTPPRWrapper(prDecorationGithubPath, prDecorationGitlabPath, prDecorationAzurePath)
learnMoreWrapper := wrappers.NewHTTPLearnMoreWrapper(descriptionsPath)
tenantConfigurationWrapper := wrappers.NewHTTPTenantConfigurationWrapper(tenantConfigurationPath)
jwtWrapper := wrappers.NewJwtWrapper()
Expand Down
132 changes: 132 additions & 0 deletions internal/commands/util/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ 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"
Expand All @@ -27,6 +28,8 @@ const (
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"
)

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)
prDecorationAzure := PRDecorationAzure(prWrapper, policyWrapper, scansWrapper)

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

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

func PRDecorationAzure(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command {
prDecorationAzure := &cobra.Command{
Use: "azure",
Short: "Decorate azure PR with vulnerabilities",
Long: "Decorate azure PR with vulnerabilities",
Example: heredoc.Doc(
`
$ cx utils pr azure --scan-id <scan-id> --token <AAD> --namespace <organization> --project <project-name or project-id>
--pr-number <pr number> --code-repository-url <code-repository-url>
`,
),
Annotations: map[string]string{
"command:doc": heredoc.Doc(
`https://checkmarx.com/resource/documents/en/34965-68653-utils.html
`,
),
},
RunE: runPRDecorationAzure(prWrapper, policyWrapper, scansWrapper),
}

prDecorationAzure.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from")
prDecorationAzure.Flags().String(params.SCMTokenFlag, "", params.AzureTokenUsage)
prDecorationAzure.Flags().String(params.NamespaceFlag, "", fmt.Sprintf(params.NamespaceFlagUsage, "Azure"))
prDecorationAzure.Flags().String(params.AzureProjectFlag, "", fmt.Sprintf(params.AzureProjectFlagUsage))
prDecorationAzure.Flags().Int(params.PRNumberFlag, 0, params.PRNumberFlagUsage)
prDecorationAzure.Flags().String(params.CodeRepositoryFlag, "", params.CodeRepositoryFlagUsage)
prDecorationAzure.Flags().String(params.CodeRespositoryUsernameFlag, "", fmt.Sprintf(params.CodeRespositoryUsernameFlagUsage))

// Set the value for token to mask the scm token
_ = viper.BindPFlag(params.SCMTokenFlag, prDecorationAzure.Flags().Lookup(params.SCMTokenFlag))

// mark some fields as required\
_ = prDecorationAzure.MarkFlagRequired(params.ScanIDFlag)
_ = prDecorationAzure.MarkFlagRequired(params.SCMTokenFlag)
_ = prDecorationAzure.MarkFlagRequired(params.NamespaceFlag)
_ = prDecorationAzure.MarkFlagRequired(params.AzureProjectFlag)
_ = prDecorationAzure.MarkFlagRequired(params.PRNumberFlag)

return prDecorationAzure
}

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 @@ -226,6 +272,13 @@ func updateAPIURLForGitlabOnPrem(apiURL string) string {
return gitlabCloudURL
}

func getAzureAPIURL(apiURL string) string {
if apiURL != "" {
return apiURL
}
return azureCloudURL
}

func runPRDecorationGitlab(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 @@ -283,6 +336,85 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.
}
}

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)
scmTokenFlag, _ := cmd.Flags().GetString(params.SCMTokenFlag)
namespaceFlag, _ := cmd.Flags().GetString(params.NamespaceFlag)
projectNameFlag, _ := cmd.Flags().GetString(params.AzureProjectFlag)
prNumberFlag, _ := cmd.Flags().GetInt(params.PRNumberFlag)
apiURL, _ := cmd.Flags().GetString(params.CodeRepositoryFlag)
codeRepositoryUserName, _ := cmd.Flags().GetString(params.CodeRespositoryUsernameFlag)

errParams := validateAzureOnPremParameters(apiURL, codeRepositoryUserName)
if errParams != nil {
return errParams
}

scanRunningOrQueued, err := IsScanRunningOrQueued(scansWrapper, scanID)

if err != nil {
return err
}

if scanRunningOrQueued {
log.Println(noPRDecorationCreated)
return nil
}

// Retrieve policies related to the scan and project to include in the PR decoration
policies, policyError := getScanViolatedPolicies(scansWrapper, policyWrapper, scanID, cmd)
if policyError != nil {
return errors.Errorf(policyErrorFormat, failedCreatingAzurePrDecoration)
}

// Build and post the pr decoration
updatedAPIURL := getAzureAPIURL(apiURL)
updatedScmToken := updateScmTokenForAzure(scmTokenFlag, codeRepositoryUserName)
azureNameSpace := createAzureNameSpace(namespaceFlag, projectNameFlag)

prModel := &wrappers.AzurePRModel{
ScanID: scanID,
ScmToken: updatedScmToken,
Namespace: azureNameSpace,
PrNumber: prNumberFlag,
Policies: policies,
APIURL: updatedAPIURL,
}
prResponse, errorModel, err := prWrapper.PostAzurePRDecoration(prModel)
if err != nil {
return err
}

if errorModel != nil {
return errors.Errorf(errorCodeFormat, failedCreatingAzurePrDecoration, errorModel.Code, errorModel.Message)
}

logger.Print(prResponse)

return nil
}
}

func validateAzureOnPremParameters(apiURL, codeRepositoryUserName string) error {
if apiURL == "" && codeRepositoryUserName != "" {
log.Println(errorAzureOnPremParams)
return errors.New(errorAzureOnPremParams)
}
return nil
}

func createAzureNameSpace(namespace, projectName string) string {
return fmt.Sprintf("%s/%s", namespace, projectName)
}

func updateScmTokenForAzure(scmTokenFlag, codeRepositoryUserName string) string {
if codeRepositoryUserName != "" {
return fmt.Sprintf("%s:%s", codeRepositoryUserName, scmTokenFlag)
}
return scmTokenFlag
}

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
56 changes: 54 additions & 2 deletions internal/commands/util/pr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@ import (
"gotest.tools/assert"
)

func TestNewPRDecorationCommandMustExist(t *testing.T) {
const (
token = "token"
)

func TestNewGithubPRDecorationCommandMustExist(t *testing.T) {
cmd := PRDecorationGithub(nil, nil, nil)
assert.Assert(t, cmd != nil, "PR decoration command must exist")

err := cmd.Execute()
assert.ErrorContains(t, err, "scan-id")
}

func TestNewMRDecorationCommandMustExist(t *testing.T) {
func TestNewGitlabMRDecorationCommandMustExist(t *testing.T) {
cmd := PRDecorationGitlab(nil, nil, nil)
assert.Assert(t, cmd != nil, "MR decoration command must exist")

err := cmd.Execute()
assert.ErrorContains(t, err, "scan-id")
}

func TestNewAzurePRDecorationCommandMustExist(t *testing.T) {
cmd := PRDecorationAzure(nil, nil, nil)
assert.Assert(t, cmd != nil, "PR decoration command must exist")

err := cmd.Execute()
assert.ErrorContains(t, err, "scan-id")
}

func TestIsScanRunning_WhenScanRunning_ShouldReturnTrue(t *testing.T) {
scansMockWrapper := &mock.ScansMockWrapper{Running: true}

Expand Down Expand Up @@ -66,3 +78,43 @@ func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(
cloudAPIURL := updateAPIURLForGitlabOnPrem("")
asserts.Equal(t, gitlabCloudURL, cloudAPIURL)
}

func TestGetAzureAPIURL_whenAPIURLIsSet_ShouldUpdateAPIURL(t *testing.T) {
selfHostedURL := "https://azure.example.com"
updatedAPIURL := getAzureAPIURL(selfHostedURL)
asserts.Equal(t, selfHostedURL, updatedAPIURL)
}

func TestGetAzureAPIURL_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(t *testing.T) {
cloudAPIURL := getAzureAPIURL("")
asserts.Equal(t, azureCloudURL, cloudAPIURL)
}

func TestUpdateScmTokenForAzureOnPrem_whenUserNameIsSet_ShouldUpdateToken(t *testing.T) {
username := "username"
expectedToken := username + ":" + token
updatedToken := updateScmTokenForAzure(token, username)
asserts.Equal(t, expectedToken, updatedToken)
}

func TestUpdateScmTokenForAzureOnPrem_whenUserNameNotSet_ShouldNotUpdateToken(t *testing.T) {
username := ""
expectedToken := token
updatedToken := updateScmTokenForAzure(token, username)
asserts.Equal(t, expectedToken, updatedToken)
}

func TestCreateAzureNameSpace_ShouldCreateNamespace(t *testing.T) {
azureNamespace := createAzureNameSpace("organization", "project")
asserts.Equal(t, "organization/project", azureNamespace)
}

func TestValidateAzureOnPremParameters_WhenParametersAreValid_ShouldReturnNil(t *testing.T) {
err := validateAzureOnPremParameters("https://azure.example.com", "username")
asserts.Nil(t, err)
}

func TestValidateAzureOnPremParameters_WhenParametersAreNotValid_ShouldReturnError(t *testing.T) {
err := validateAzureOnPremParameters("", "username")
asserts.NotNil(t, err)
}
1 change: 1 addition & 0 deletions internal/params/binds.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var EnvVarsBinds = []struct {
{BflPathKey, BflPathEnv, "api/bfl"},
{PRDecorationGithubPathKey, PRDecorationGithubPathEnv, "api/flow-publisher/pr/github"},
{PRDecorationGitlabPathKey, PRDecorationGitlabPathEnv, "api/flow-publisher/pr/gitlab"},
{PRDecorationAzurePathKey, PRDecorationAzurePathEnv, "api/flow-publisher/pr/azure"},
{DescriptionsPathKey, DescriptionsPathEnv, "api/queries/descriptions"},
{TenantConfigurationPathKey, TenantConfigurationPathEnv, "api/configuration/tenant"},
{UploadsPathKey, UploadsPathEnv, "api/uploads"},
Expand Down
1 change: 1 addition & 0 deletions internal/params/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
BflPathEnv = "CX_BFL_PATH"
PRDecorationGithubPathEnv = "CX_PR_DECORATION_GITHUB_PATH"
PRDecorationGitlabPathEnv = "CX_PR_DECORATION_GITLAB_PATH"
PRDecorationAzurePathEnv = "CX_PR_DECORATION_AZURE_PATH"
SastRmPathEnv = "CX_SAST_RM_PATH"
UploadsPathEnv = "CX_UPLOADS_PATH"
TokenExpirySecondsEnv = "CX_TOKEN_EXPIRY_SECONDS"
Expand Down
24 changes: 14 additions & 10 deletions internal/params/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,20 @@ const (
ScaFilterUsage = "SCA filter"

// PR decoration flags
NamespaceFlag = "namespace"
NamespaceFlagUsage = "%s namespace is required to post the comments"
RepoNameFlag = "repo-name"
RepoNameFlagUsage = "%s repository details"
PRNumberFlag = "pr-number"
PRNumberFlagUsage = "Pull Request number for posting notifications and comments"
PRIidFlag = "mr-iid"
PRIidFlagUsage = "Gitlab IID (internal ID) of the merge request"
PRGitlabProjectFlag = "gitlab-project-id"
PRGitlabProjectFlagUsage = "Gitlab project ID"
NamespaceFlag = "namespace"
NamespaceFlagUsage = "%s namespace is required to post the comments"
RepoNameFlag = "repo-name"
RepoNameFlagUsage = "%s repository details"
PRNumberFlag = "pr-number"
PRNumberFlagUsage = "Pull Request number for posting notifications and comments"
PRIidFlag = "mr-iid"
PRIidFlagUsage = "Gitlab IID (internal ID) of the merge request"
PRGitlabProjectFlag = "gitlab-project-id"
PRGitlabProjectFlagUsage = "Gitlab project ID"
AzureProjectFlag = "project"
AzureProjectFlagUsage = "Azure project name or project ID"
CodeRespositoryUsernameFlag = "code-repository-username"
CodeRespositoryUsernameFlagUsage = "Azure username for code repository"

// Chat (General)
ChatAPIKey = "chat-apikey"
Expand Down
1 change: 1 addition & 0 deletions internal/params/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (
BflPathKey = strings.ToLower(BflPathEnv)
PRDecorationGithubPathKey = strings.ToLower(PRDecorationGithubPathEnv)
PRDecorationGitlabPathKey = strings.ToLower(PRDecorationGitlabPathEnv)
PRDecorationAzurePathKey = strings.ToLower(PRDecorationAzurePathEnv)
UploadsPathKey = strings.ToLower(UploadsPathEnv)
SastRmPathKey = strings.ToLower(SastRmPathEnv)
AccessKeyIDConfigKey = strings.ToLower(AccessKeyIDEnv)
Expand Down
4 changes: 4 additions & 0 deletions internal/wrappers/mock/pr-mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ func (pr *PRMockWrapper) PostPRDecoration(model *wrappers.PRModel) (
func (pr *PRMockWrapper) PostGitlabPRDecoration(model *wrappers.GitlabPRModel) (string, *wrappers.WebError, error) {
return "MR comment created successfully.", nil, nil
}

func (pr *PRMockWrapper) PostAzurePRDecoration(model *wrappers.AzurePRModel) (string, *wrappers.WebError, error) {
return "PR comment created successfully.", nil, nil
}
Loading

0 comments on commit e55d4cc

Please sign in to comment.