diff --git a/internal/commands/util/pr.go b/internal/commands/util/pr.go index 6c243e0d1..8042cb0e0 100644 --- a/internal/commands/util/pr.go +++ b/internal/commands/util/pr.go @@ -35,10 +35,11 @@ func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers `, ), } - + prDecorationAzure := PRDecorationAzure(prWrapper, policyWrapper, scansWrapper) prDecorationGithub := PRDecorationGithub(prWrapper, policyWrapper, scansWrapper) prDecorationGitlab := PRDecorationGitlab(prWrapper, policyWrapper, scansWrapper) + cmd.AddCommand(prDecorationAzure) cmd.AddCommand(prDecorationGithub) cmd.AddCommand(prDecorationGitlab) return cmd @@ -73,6 +74,38 @@ func isScanRunningOrQueued(scansWrapper wrappers.ScansWrapper, scanID string) (b return false, nil } +func PRDecorationAzure(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { + prDecorationAzure := &cobra.Command{ + Use: "azure", + Short: "Decorate Azure DevOps PR with vulnerabilities", + Long: "Decorate Azure DevOps PR with vulnerabilities", + Example: heredoc.Doc( + ` + $ cx utils pr azure --scan-id --token --namespace --project + --pr-number --code-repository-url + `, + ), + 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 DevOps")) + prDecorationAzure.Flags().String(params.ProjectName, "", "Azure DevOps project name") + prDecorationAzure.Flags().Int(params.PRNumberFlag, 0, params.PRNumberFlagUsage) + prDecorationAzure.Flags().String(params.ServerURLFlag, "", params.ServerURLFlagUsage) + + _ = viper.BindPFlag(params.SCMTokenFlag, prDecorationAzure.Flags().Lookup(params.SCMTokenFlag)) + + _ = prDecorationAzure.MarkFlagRequired(params.ScanIDFlag) + _ = prDecorationAzure.MarkFlagRequired(params.SCMTokenFlag) + _ = prDecorationAzure.MarkFlagRequired(params.NamespaceFlag) + _ = prDecorationAzure.MarkFlagRequired(params.ProjectName) + _ = prDecorationAzure.MarkFlagRequired(params.PRNumberFlag) + + return prDecorationAzure +} + func PRDecorationGithub(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { prDecorationGithub := &cobra.Command{ Use: "github", @@ -153,6 +186,53 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Pol return prDecorationGitlab } +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) + organizationFlag, _ := cmd.Flags().GetString(params.NamespaceFlag) + projectFlag, _ := cmd.Flags().GetString(params.ProjectName) + prNumberFlag, _ := cmd.Flags().GetInt(params.PRNumberFlag) + serverURLFlag, _ := cmd.Flags().GetString(params.ServerURLFlag) + + 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, failedCreatingGithubPrDecoration) + } + + prModel := &wrappers.AzurePRModel{ + ScanID: scanID, + ScmToken: scmTokenFlag, + Organization: organizationFlag, + ProjectName: projectFlag, + PrNumber: prNumberFlag, + Policies: policies, + ServerUrl: serverURLFlag, + } + + prResponse, errorModel, err := prWrapper.PostAzurePRDecoration(prModel) + if err != nil { + return err + } + if errorModel != nil { + return errors.Errorf(errorCodeFormat, failedCreatingGithubPrDecoration, errorModel.Code, errorModel.Message) + } + + logger.Print(prResponse) + return nil + } +} + 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) diff --git a/internal/params/flags.go b/internal/params/flags.go index 6bd011ec8..d3e028bdf 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -169,6 +169,8 @@ const ( PRIidFlagUsage = "Gitlab IID (internal ID) of the merge request" PRGitlabProjectFlag = "gitlab-project-id" PRGitlabProjectFlagUsage = "Gitlab project ID" + ServerURLFlag = "code-repository-url" + ServerURLFlagUsage = "Code repository URL (optional for self-hosted Azure DevOps)" // Chat (General) ChatAPIKey = "chat-apikey" diff --git a/internal/wrappers/pr-http.go b/internal/wrappers/pr-http.go index 8756d74ac..785a960ea 100644 --- a/internal/wrappers/pr-http.go +++ b/internal/wrappers/pr-http.go @@ -18,15 +18,39 @@ const ( type PRHTTPWrapper struct { githubPath string gitlabPath string + azurePath string } -func NewHTTPPRWrapper(githubPath, gitlabPath string) PRWrapper { +func NewHTTPPRWrapper(githubPath, gitlabPath, azurePath string) PRWrapper { return &PRHTTPWrapper{ githubPath: githubPath, gitlabPath: gitlabPath, + azurePath: azurePath, } } +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 (r *PRHTTPWrapper) PostPRDecoration(model *PRModel) ( string, *WebError, diff --git a/internal/wrappers/pr.go b/internal/wrappers/pr.go index 1a6fb0cbf..f935e48b6 100644 --- a/internal/wrappers/pr.go +++ b/internal/wrappers/pr.go @@ -23,7 +23,18 @@ type GitlabPRModel struct { Policies []PrPolicy `json:"violatedPolicyList"` } +type AzurePRModel struct { + ScanID string `json:"scanId"` + ScmToken string `json:"scmToken"` + Organization string `json:"organization"` + ProjectName string `json:"projectName"` + PrNumber int `json:"prNumber"` + Policies []PrPolicy `json:"violatedPolicyList"` + ServerUrl string `json:"serverUrl"` +} + type PRWrapper interface { PostPRDecoration(model *PRModel) (string, *WebError, error) PostGitlabPRDecoration(model *GitlabPRModel) (string, *WebError, error) + PostAzurePRDecoration(model *AzurePRModel) (string, *WebError, error) }