Skip to content

Commit

Permalink
pr decoration support for ADO
Browse files Browse the repository at this point in the history
  • Loading branch information
miryamfoiferCX committed Nov 13, 2024
1 parent d8e3069 commit 53a9df4
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 15 deletions.
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
118 changes: 118 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,7 @@ const (
gitlabOnPremURLSuffix = "/api/v4/"
githubCloudURL = "https://api.github.com/repos/"
gitlabCloudURL = "https://gitlab.com" + gitlabOnPremURLSuffix
azureCloudURL = "https://dev.azure.com/"
)

func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command {
Expand All @@ -42,9 +44,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 +163,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>
--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 all 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 +271,13 @@ func updateAPIURLForGitlabOnPrem(apiURL string) string {
return gitlabCloudURL
}

func updateAPIURLForAzureOnPrem(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 +335,72 @@ 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)

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 := updateAPIURLForAzureOnPrem(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 createAzureNameSpace(namespaceFlag string, projectNameFlag string) string {

Check failure on line 393 in internal/commands/util/pr.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(namespaceFlag string, projectNameFlag string) string could be replaced with func(namespaceFlag, projectNameFlag string) string (gocritic)
return fmt.Sprintf("%s/%s", namespaceFlag, projectNameFlag)
}

func updateScmTokenForAzure(scmTokenFlag string, codeRepositoryUserName string) string {

Check failure on line 397 in internal/commands/util/pr.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(scmTokenFlag string, codeRepositoryUserName string) string could be replaced with func(scmTokenFlag, codeRepositoryUserName string) string (gocritic)
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
44 changes: 42 additions & 2 deletions internal/commands/util/pr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,30 @@ import (
"gotest.tools/assert"
)

func TestNewPRDecorationCommandMustExist(t *testing.T) {
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 +74,35 @@ func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(
cloudAPIURL := updateAPIURLForGitlabOnPrem("")
asserts.Equal(t, gitlabCloudURL, cloudAPIURL)
}

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

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

func TestUpdateScmTokenForAzureOnPrem_whenUserNameIsSet_ShouldUpdateToken(t *testing.T) {
token := "token"

Check failure on line 90 in internal/commands/util/pr_test.go

View workflow job for this annotation

GitHub Actions / lint

string `token` has 2 occurrences, make it a constant (goconst)
username := "username"
expectedToken := username + ":" + token
updatedToken := updateScmTokenForAzure(token, username)
asserts.Equal(t, expectedToken, updatedToken)
}

func TestUpdateScmTokenForAzureOnPrem_whenUserNameNotSet_ShouldNotUpdateToken(t *testing.T) {
token := "token"
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)
}
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
}
26 changes: 25 additions & 1 deletion internal/wrappers/pr-http.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ const (
type PRHTTPWrapper struct {
githubPath string
gitlabPath string
azurePath string
}

func NewHTTPPRWrapper(githubPath, gitlabPath string) PRWrapper {
func NewHTTPPRWrapper(githubPath, gitlabPath string, azurePath string) PRWrapper {

Check failure on line 24 in internal/wrappers/pr-http.go

View workflow job for this annotation

GitHub Actions / lint

paramTypeCombine: func(githubPath, gitlabPath string, azurePath string) PRWrapper could be replaced with func(githubPath, gitlabPath, azurePath string) PRWrapper (gocritic)
return &PRHTTPWrapper{
githubPath: githubPath,
gitlabPath: gitlabPath,
azurePath: azurePath,
}
}

Expand Down Expand Up @@ -71,6 +73,28 @@ 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
}
defer func() {
if err == nil {
_ = resp.Body.Close()
}
}()
return handlePRResponseWithBody(resp, err)
}

func handlePRResponseWithBody(resp *http.Response, err error) (string, *WebError, error) {
if err != nil {
return "", nil, err
Expand Down
10 changes: 10 additions & 0 deletions internal/wrappers/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ type GitlabPRModel struct {
APIURL string `json:"apiUrl"`
}

type AzurePRModel struct {
ScanID string `json:"scanId"`
ScmToken string `json:"scmToken"`
Namespace string `json:"namespace"`
PrNumber int `json:"prNumber"`
Policies []PrPolicy `json:"violatedPolicyList"`
APIURL string `json:"apiUrl"`
}

type PRWrapper interface {
PostPRDecoration(model *PRModel) (string, *WebError, error)
PostGitlabPRDecoration(model *GitlabPRModel) (string, *WebError, error)
PostAzurePRDecoration(model *AzurePRModel) (string, *WebError, error)
}
Loading

0 comments on commit 53a9df4

Please sign in to comment.