diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index 01aa69cae..5b48260b6 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -18,7 +18,7 @@ jobs: exit 1 fi - if ! [[ "$PR_TITLE" =~ \(AST-[0-9]+\)$ ]]; then + if ! [[ "$PR_TITLE" =~ \(AST-[0-9]+\)$ || "$PR_TITLE" =~ \(AST-[0-9]+(, AST-[0-9]+)*\)$ ]]; then echo "::error::PR title must contain a Jira ticket ID at the end in the format '(AST-XXXX)'." exit 1 fi diff --git a/Dockerfile b/Dockerfile index 47c1ef014..bb61ceb48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM cgr.dev/chainguard/bash@sha256:f8e48690d991e6814c81f063833176439e8f0d4bc1c5f0a47f94858dea3e4f44 +FROM cgr.dev/chainguard/bash@sha256:e1d16dec8d976859080d984167109b3557c2b6494f10be08147806b78bdef691 USER nonroot COPY cx /app/bin/cx diff --git a/go.mod b/go.mod index 060fd2b6c..b8b30262b 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,10 @@ require ( github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63 github.com/CheckmarxDev/containers-resolver v1.0.14 github.com/MakeNowJust/heredoc v1.0.0 + github.com/bouk/monkey v1.0.0 github.com/checkmarxDev/gpt-wrapper v0.0.0-20230721160222-85da2fd1cc4c github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 + github.com/gomarkdown/markdown v0.0.0-20241102151059-6bc1ffdc6e8c github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 diff --git a/go.sum b/go.sum index 856f46fb1..767da068c 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bouk/monkey v1.0.0 h1:k6z8fLlPhETfn5l9rlWVE7Q6B23DoaqosTdArvNQRdc= +github.com/bouk/monkey v1.0.0/go.mod h1:PG/63f4XEUlVyW1ttIeOJmJhhe1+t9EC/je3eTjvFhE= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= @@ -429,8 +431,8 @@ github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= -github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20241102151059-6bc1ffdc6e8c h1:CrUrhyZMx1Me0fyvvFtQq6W18ss2WEfgPRfjnwrTtiQ= +github.com/gomarkdown/markdown v0.0.0-20241102151059-6bc1ffdc6e8c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= diff --git a/internal/commands/util/pr.go b/internal/commands/util/pr.go index 6c243e0d1..ab5207d0d 100644 --- a/internal/commands/util/pr.go +++ b/internal/commands/util/pr.go @@ -23,6 +23,10 @@ const ( 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 ) func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper) *cobra.Command { @@ -44,7 +48,7 @@ func NewPRDecorationCommand(prWrapper wrappers.PRWrapper, policyWrapper wrappers return cmd } -func isScanRunningOrQueued(scansWrapper wrappers.ScansWrapper, scanID string) (bool, error) { +func IsScanRunningOrQueued(scansWrapper wrappers.ScansWrapper, scanID string) (bool, error) { var scanResponseModel *wrappers.ScanResponseModel var errorModel *wrappers.ErrorModel var err error @@ -93,6 +97,7 @@ func PRDecorationGithub(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Pol RunE: runPRDecoration(prWrapper, policyWrapper, scansWrapper), } + prDecorationGithub.Flags().String(params.CodeRepositoryFlag, "", params.CodeRepositoryFlagUsage) prDecorationGithub.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from") prDecorationGithub.Flags().String(params.SCMTokenFlag, "", params.GithubTokenUsage) prDecorationGithub.Flags().String(params.NamespaceFlag, "", fmt.Sprintf(params.NamespaceFlagUsage, "Github")) @@ -120,7 +125,7 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Pol Example: heredoc.Doc( ` $ cx utils pr gitlab --scan-id --token --namespace --repo-name - --iid --gitlab-project + --iid --gitlab-project --code-repository-url `, ), Annotations: map[string]string{ @@ -132,6 +137,7 @@ func PRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Pol RunE: runPRDecorationGitlab(prWrapper, policyWrapper, scansWrapper), } + prDecorationGitlab.Flags().String(params.CodeRepositoryFlag, "", params.CodeRepositoryFlagUsage) prDecorationGitlab.Flags().String(params.ScanIDFlag, "", "Scan ID to retrieve results from") prDecorationGitlab.Flags().String(params.SCMTokenFlag, "", params.GitLabTokenUsage) prDecorationGitlab.Flags().String(params.NamespaceFlag, "", fmt.Sprintf(params.NamespaceFlagUsage, "Gitlab")) @@ -160,8 +166,9 @@ func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Policy namespaceFlag, _ := cmd.Flags().GetString(params.NamespaceFlag) repoNameFlag, _ := cmd.Flags().GetString(params.RepoNameFlag) prNumberFlag, _ := cmd.Flags().GetInt(params.PRNumberFlag) + apiURL, _ := cmd.Flags().GetString(params.CodeRepositoryFlag) - scanRunningOrQueued, err := isScanRunningOrQueued(scansWrapper, scanID) + scanRunningOrQueued, err := IsScanRunningOrQueued(scansWrapper, scanID) if err != nil { return err @@ -179,6 +186,8 @@ func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Policy } // Build and post the pr decoration + updatedAPIURL := updateAPIURLForGithubOnPrem(apiURL) + prModel := &wrappers.PRModel{ ScanID: scanID, ScmToken: scmTokenFlag, @@ -186,6 +195,7 @@ func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Policy RepoName: repoNameFlag, PrNumber: prNumberFlag, Policies: policies, + APIURL: updatedAPIURL, } prResponse, errorModel, err := prWrapper.PostPRDecoration(prModel) if err != nil { @@ -202,6 +212,20 @@ func runPRDecoration(prWrapper wrappers.PRWrapper, policyWrapper wrappers.Policy } } +func updateAPIURLForGithubOnPrem(apiURL string) string { + if apiURL != "" { + return apiURL + githubOnPremURLSuffix + } + return githubCloudURL +} + +func updateAPIURLForGitlabOnPrem(apiURL string) string { + if apiURL != "" { + return apiURL + gitlabOnPremURLSuffix + } + return gitlabCloudURL +} + 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) @@ -210,8 +234,9 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers. repoNameFlag, _ := cmd.Flags().GetString(params.RepoNameFlag) iIDFlag, _ := cmd.Flags().GetInt(params.PRIidFlag) gitlabProjectIDFlag, _ := cmd.Flags().GetInt(params.PRGitlabProjectFlag) + apiURL, _ := cmd.Flags().GetString(params.CodeRepositoryFlag) - scanRunningOrQueued, err := isScanRunningOrQueued(scansWrapper, scanID) + scanRunningOrQueued, err := IsScanRunningOrQueued(scansWrapper, scanID) if err != nil { return err @@ -229,6 +254,8 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers. } // Build and post the mr decoration + updatedAPIURL := updateAPIURLForGitlabOnPrem(apiURL) + prModel := &wrappers.GitlabPRModel{ ScanID: scanID, ScmToken: scmTokenFlag, @@ -237,6 +264,7 @@ func runPRDecorationGitlab(prWrapper wrappers.PRWrapper, policyWrapper wrappers. IiD: iIDFlag, GitlabProjectID: gitlabProjectIDFlag, Policies: policies, + APIURL: updatedAPIURL, } prResponse, errorModel, err := prWrapper.PostGitlabPRDecoration(prModel) diff --git a/internal/commands/util/pr_test.go b/internal/commands/util/pr_test.go index f2a86ae29..9e11b8385 100644 --- a/internal/commands/util/pr_test.go +++ b/internal/commands/util/pr_test.go @@ -24,17 +24,17 @@ func TestNewMRDecorationCommandMustExist(t *testing.T) { assert.ErrorContains(t, err, "scan-id") } -func TestIfScanRunning_WhenScanRunning_ShouldReturnTrue(t *testing.T) { +func TestIsScanRunning_WhenScanRunning_ShouldReturnTrue(t *testing.T) { scansMockWrapper := &mock.ScansMockWrapper{Running: true} - scanRunning, _ := isScanRunningOrQueued(scansMockWrapper, "ScanRunning") + scanRunning, _ := IsScanRunningOrQueued(scansMockWrapper, "ScanRunning") asserts.True(t, scanRunning) } -func TestIfScanRunning_WhenScanDone_ShouldReturnFalse(t *testing.T) { +func TestIsScanRunning_WhenScanDone_ShouldReturnFalse(t *testing.T) { scansMockWrapper := &mock.ScansMockWrapper{Running: false} - scanRunning, _ := isScanRunningOrQueued(scansMockWrapper, "ScanNotRunning") + scanRunning, _ := IsScanRunningOrQueued(scansMockWrapper, "ScanNotRunning") asserts.False(t, scanRunning) } @@ -44,3 +44,25 @@ func TestPRDecorationGithub_WhenNoViolatedPolicies_ShouldNotReturnPolicy(t *test prPolicy := policiesToPrPolicies(policyResponse) asserts.True(t, len(prPolicy) == 0) } + +func TestUpdateAPIURLForGithubOnPrem_whenAPIURLIsSet_ShouldUpdateAPIURL(t *testing.T) { + selfHostedURL := "https://github.example.com" + updatedAPIURL := updateAPIURLForGithubOnPrem(selfHostedURL) + asserts.Equal(t, selfHostedURL+githubOnPremURLSuffix, updatedAPIURL) +} + +func TestUpdateAPIURLForGithubOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(t *testing.T) { + cloudAPIURL := updateAPIURLForGithubOnPrem("") + asserts.Equal(t, githubCloudURL, cloudAPIURL) +} + +func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsSet_ShouldUpdateAPIURL(t *testing.T) { + selfHostedURL := "https://gitlab.example.com" + updatedAPIURL := updateAPIURLForGitlabOnPrem(selfHostedURL) + asserts.Equal(t, selfHostedURL+gitlabOnPremURLSuffix, updatedAPIURL) +} + +func TestUpdateAPIURLForGitlabOnPrem_whenAPIURLIsNotSet_ShouldReturnCloudAPIURL(t *testing.T) { + cloudAPIURL := updateAPIURLForGitlabOnPrem("") + asserts.Equal(t, gitlabCloudURL, cloudAPIURL) +} diff --git a/internal/params/flags.go b/internal/params/flags.go index 6bd011ec8..b52903aee 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -33,6 +33,8 @@ const ( BranchFlag = "branch" BranchFlagSh = "b" ScanIDFlag = "scan-id" + CodeRepositoryFlag = "code-repository-url" + CodeRepositoryFlagUsage = "Code repository URL (optional for self-hosted SCMs)" BranchFlagUsage = "Branch to scan" MainBranchFlag = "branch" ScaResolverFlag = "sca-resolver" diff --git a/internal/wrappers/pr.go b/internal/wrappers/pr.go index 1a6fb0cbf..40ccdb159 100644 --- a/internal/wrappers/pr.go +++ b/internal/wrappers/pr.go @@ -11,6 +11,7 @@ type PRModel struct { RepoName string `json:"repoName"` PrNumber int `json:"prNumber"` Policies []PrPolicy `json:"violatedPolicyList"` + APIURL string `json:"apiUrl"` } type GitlabPRModel struct { @@ -21,6 +22,7 @@ type GitlabPRModel struct { IiD int `json:"iid"` GitlabProjectID int `json:"gitlabProjectID"` Policies []PrPolicy `json:"violatedPolicyList"` + APIURL string `json:"apiUrl"` } type PRWrapper interface { diff --git a/test/integration/pr_test.go b/test/integration/pr_test.go index 8a18aae21..932273862 100644 --- a/test/integration/pr_test.go +++ b/test/integration/pr_test.go @@ -3,6 +3,8 @@ package integration import ( + "fmt" + "github.com/checkmarx/ast-cli/internal/wrappers" "os" "strings" "testing" @@ -10,6 +12,7 @@ import ( "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/logger" + "github.com/bouk/monkey" "github.com/checkmarx/ast-cli/internal/params" "gotest.tools/assert" ) @@ -26,8 +29,35 @@ const ( prGitlabIid = "PR_GITLAB_IID" 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." + outputFileName = "test_output.log" + scans = "api/scans" ) +var completedScanId = "" + +func getCompletedScanID(t *testing.T) string { + if completedScanId != "" { + return completedScanId + } + scanWrapper := wrappers.NewHTTPScansWrapper(scans) + scanID, _ := getRootScan(t, params.IacType) + + file := createOutputFile(t, outputFileName) + defer deleteOutputFile(t, file) + defer logger.SetOutput(os.Stdout) + + for isRunning, err := util.IsScanRunningOrQueued(scanWrapper, scanID); isRunning; isRunning, err = util.IsScanRunningOrQueued(scanWrapper, scanID) { + if err != nil { + t.Fatalf("Failed to get scan status: %v", err) + } + logger.PrintIfVerbose("Waiting for scan to finish. scan running: " + fmt.Sprintf("%t", isRunning)) + } + completedScanId = scanID + return scanID +} + func TestPRGithubDecorationSuccessCase(t *testing.T) { scanID, _ := getRootScan(t, params.SastType) args := []string{ @@ -51,6 +81,45 @@ func TestPRGithubDecorationSuccessCase(t *testing.T) { assert.NilError(t, err, "Error should be nil") } +func TestPRGithubDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T) { + args := []string{ + "utils", + "pr", + "github", + flag(params.ScanIDFlag), + getCompletedScanID(t), + flag(params.SCMTokenFlag), + os.Getenv(prGithubToken), + flag(params.NamespaceFlag), + os.Getenv(prGithubNamespace), + flag(params.PRNumberFlag), + os.Getenv(prGithubNumber), + flag(params.RepoNameFlag), + os.Getenv(prGithubRepoName), + flag(params.CodeRepositoryFlag), + "https://github.example.com", + } + + monkey.Patch((*wrappers.PRHTTPWrapper).PostPRDecoration, func(*wrappers.PRHTTPWrapper, *wrappers.PRModel) (string, *wrappers.WebError, error) { + return githubPRCommentCreated, 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, githubPRCommentCreated), true, "Expected output: %s", githubPRCommentCreated) +} + func TestPRGithubDecorationFailure(t *testing.T) { args := []string{ "utils", @@ -73,7 +142,6 @@ func TestPRGithubDecorationFailure(t *testing.T) { func TestPRGitlabDecorationSuccessCase(t *testing.T) { scanID, _ := getRootScan(t, params.SastType) - args := []string{ "utils", "pr", @@ -95,6 +163,48 @@ func TestPRGitlabDecorationSuccessCase(t *testing.T) { assert.NilError(t, err, "Error should be nil") } +func TestPRGitlabDecoration_WhenUseCodeRepositoryFlag_ShouldSuccess(t *testing.T) { + args := []string{ + "utils", + "pr", + "gitlab", + flag(params.ScanIDFlag), + getCompletedScanID(t), + flag(params.SCMTokenFlag), + os.Getenv(prGitlabToken), + flag(params.NamespaceFlag), + os.Getenv(prGitlabNamespace), + flag(params.RepoNameFlag), + os.Getenv(prGitlabRepoName), + flag(params.PRGitlabProjectFlag), + os.Getenv(prGitlabProjectId), + flag(params.PRIidFlag), + os.Getenv(prGitlabIid), + flag(params.CodeRepositoryFlag), + "https://gitlab.example.com", + } + + monkey.Patch((*wrappers.PRHTTPWrapper).PostGitlabPRDecoration, func(*wrappers.PRHTTPWrapper, *wrappers.GitlabPRModel) (string, *wrappers.WebError, error) { + return gitlabPRCommentCreated, nil, nil + }) + defer monkey.Unpatch((*wrappers.PRHTTPWrapper).PostGitlabPRDecoration) + + 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, gitlabPRCommentCreated), true, "Expected output: %s", gitlabPRCommentCreated, " | actual: ", stdoutString) + +} + func TestPRGitlabDecorationFailure(t *testing.T) { args := []string{ @@ -137,7 +247,7 @@ func TestPRGithubDecoration_WhenScanIsRunning_ShouldAvoidPRDecorationCommand(t * "--debug", } - file := createOutputFile(t, "test_output.log") + file := createOutputFile(t, outputFileName) _, _ = executeCommand(t, args...) stdoutString, err := util.ReadFileAsString(file.Name()) if err != nil { @@ -169,7 +279,7 @@ func TestPRGitlabDecoration_WhenScanIsRunning_ShouldAvoidPRDecorationCommand(t * os.Getenv(prGitlabIid), } - file := createOutputFile(t, "test_output.log") + file := createOutputFile(t, outputFileName) _, _ = executeCommand(t, args...) stdoutString, err := util.ReadFileAsString(file.Name()) if err != nil {