Skip to content

Commit 25d5521

Browse files
authored
Refactor Scan Pull Request to accept PR ID as input (#398)
1 parent 16d358e commit 25d5521

File tree

199 files changed

+114
-536
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

199 files changed

+114
-536
lines changed

commands/scanpullrequest.go

+38-54
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"github.com/go-git/go-git/v5"
87
"github.com/jfrog/gofrog/datastructures"
98
"os"
109
"os/exec"
@@ -27,58 +26,43 @@ const (
2726
noGitHubEnvReviewersErr = "frogbot did not scan this PR, because the existing GitHub Environment named 'frogbot' doesn't have reviewers selected. Please refer to the Frogbot documentation for instructions on how to create the Environment"
2827
)
2928

30-
type ScanPullRequestCmd struct{}
29+
type ScanPullRequestCmd struct {
30+
// Optional provided pull request details, used in scan-pull-requests command.
31+
pullRequestDetails vcsclient.PullRequestInfo
32+
}
3133

3234
// Run ScanPullRequest method only works for a single repository scan.
3335
// Therefore, the first repository config represents the repository on which Frogbot runs, and it is the only one that matters.
34-
func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient) error {
35-
if err := utils.ValidateSingleRepoConfiguration(&configAggregator); err != nil {
36-
return err
36+
func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient) (err error) {
37+
if err = utils.ValidateSingleRepoConfiguration(&configAggregator); err != nil {
38+
return
3739
}
3840
repoConfig := &(configAggregator)[0]
3941
if repoConfig.GitProvider == vcsutils.GitHub {
40-
if err := verifyGitHubFrogbotEnvironment(client, repoConfig); err != nil {
41-
return err
42+
if err = verifyGitHubFrogbotEnvironment(client, repoConfig); err != nil {
43+
return
4244
}
4345
}
44-
if err := cmd.verifyDifferentBranches(repoConfig); err != nil {
45-
return err
46-
}
47-
return scanPullRequest(repoConfig, client)
48-
}
4946

50-
// Verifies current branch and target branch are not the same.
51-
// The Current branch is the branch the action is triggered on.
52-
// The Target branch is the branch to open pull request to.
53-
func (cmd *ScanPullRequestCmd) verifyDifferentBranches(repoConfig *utils.Repository) error {
54-
repo, err := git.PlainOpen(".")
55-
if err != nil {
56-
return err
57-
}
58-
ref, err := repo.Head()
59-
if err != nil {
60-
return err
61-
}
62-
currentBranch := ref.Name().Short()
63-
defaultBranch := repoConfig.Branches[0]
64-
if currentBranch == defaultBranch {
65-
return fmt.Errorf(utils.ErrScanPullRequestSameBranches, currentBranch)
47+
// PullRequestDetails can be defined already when using the scan-all-pull-requests command.
48+
if cmd.pullRequestDetails.ID == utils.UndefinedPrID {
49+
if cmd.pullRequestDetails, err = client.GetPullRequestByID(context.Background(), repoConfig.RepoOwner, repoConfig.RepoName, repoConfig.PullRequestID); err != nil {
50+
return
51+
}
6652
}
67-
return nil
53+
54+
return scanPullRequest(repoConfig, client, cmd.pullRequestDetails)
6855
}
6956

7057
// By default, includeAllVulnerabilities is set to false and the scan goes as follows:
7158
// a. Audit the dependencies of the source and the target branches.
7259
// b. Compare the vulnerabilities found in source and target branches, and show only the new vulnerabilities added by the pull request.
7360
// Otherwise, only the source branch is scanned and all found vulnerabilities are being displayed.
74-
func scanPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) error {
75-
// Validate scan params
76-
if len(repoConfig.Branches) == 0 {
77-
return &utils.ErrMissingEnv{VariableName: utils.GitBaseBranchEnv}
78-
}
79-
61+
func scanPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, pullRequestDetails vcsclient.PullRequestInfo) error {
62+
log.Info("Scanning Pull Request ID:", pullRequestDetails.ID, "Source:", pullRequestDetails.Source.Name, "Target:", pullRequestDetails.Target.Name)
63+
log.Info("-----------------------------------------------------------")
8064
// Audit PR code
81-
vulnerabilitiesRows, iacRows, err := auditPullRequest(repoConfig, client)
65+
vulnerabilitiesRows, iacRows, err := auditPullRequest(repoConfig, client, pullRequestDetails)
8266
if err != nil {
8367
return err
8468
}
@@ -98,17 +82,23 @@ func scanPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) e
9882
return err
9983
}
10084

101-
func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) ([]formats.VulnerabilityOrViolationRow, []formats.IacSecretsRow, error) {
85+
// Downloads Pull Requests branches code and audits them
86+
func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, pullRequestDetails vcsclient.PullRequestInfo) ([]formats.VulnerabilityOrViolationRow, []formats.IacSecretsRow, error) {
10287
var vulnerabilitiesRows []formats.VulnerabilityOrViolationRow
10388
var iacRows []formats.IacSecretsRow
104-
targetBranch := repoConfig.Branches[0]
89+
targetBranch := pullRequestDetails.Target.Name
90+
sourceBranch := pullRequestDetails.Source.Name
10591
for i := range repoConfig.Projects {
92+
// Source scan details
10693
scanDetails := utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Git).
10794
SetProject(&repoConfig.Projects[i]).
10895
SetXrayGraphScanParams(repoConfig.Watches, repoConfig.JFrogProjectKey).
10996
SetMinSeverity(repoConfig.MinSeverity).
110-
SetFixableOnly(repoConfig.FixableOnly)
111-
sourceResults, err := auditSource(scanDetails)
97+
SetFixableOnly(repoConfig.FixableOnly).
98+
SetBranch(sourceBranch).
99+
SetRepoOwner(pullRequestDetails.Source.Owner)
100+
101+
sourceResults, err := downloadAndAuditBranch(scanDetails)
112102
if err != nil {
113103
return nil, nil, err
114104
}
@@ -123,9 +113,12 @@ func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient)
123113
iacRows = append(iacRows, xrayutils.PrepareIacs(sourceResults.ExtendedScanResults.IacScanResults)...)
124114
continue
125115
}
126-
// Audit target code
127-
scanDetails.SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues).SetBranch(targetBranch)
128-
targetResults, err := auditTarget(scanDetails)
116+
// Target scan details
117+
scanDetails.SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues).
118+
SetBranch(targetBranch).
119+
SetRepoOwner(pullRequestDetails.Target.Owner)
120+
121+
targetResults, err := downloadAndAuditBranch(scanDetails)
129122
if err != nil {
130123
return nil, nil, err
131124
}
@@ -235,15 +228,6 @@ func getScanVulnerabilitiesRows(auditResults *audit.Results) ([]formats.Vulnerab
235228
return []formats.VulnerabilityOrViolationRow{}, nil
236229
}
237230

238-
func auditSource(scanSetup *utils.ScanDetails) (auditResults *audit.Results, err error) {
239-
wd, err := os.Getwd()
240-
if err != nil {
241-
return
242-
}
243-
fullPathWds := getFullPathWorkingDirs(scanSetup.WorkingDirs, wd)
244-
return runInstallAndAudit(scanSetup, fullPathWds...)
245-
}
246-
247231
func getFullPathWorkingDirs(workingDirs []string, baseWd string) []string {
248232
var fullPathWds []string
249233
if len(workingDirs) != 0 {
@@ -260,9 +244,9 @@ func getFullPathWorkingDirs(workingDirs []string, baseWd string) []string {
260244
return fullPathWds
261245
}
262246

263-
func auditTarget(scanSetup *utils.ScanDetails) (auditResults *audit.Results, err error) {
247+
func downloadAndAuditBranch(scanSetup *utils.ScanDetails) (auditResults *audit.Results, err error) {
264248
// First download the target repo to temp dir
265-
log.Info("Auditing the", scanSetup.Git.RepoName, "repository on the", scanSetup.Branch(), "branch")
249+
log.Info("Auditing repository:", scanSetup.Git.RepoName, "branch:", scanSetup.Branch())
266250
wd, cleanup, err := utils.DownloadRepoToTempDir(scanSetup.Client(), scanSetup.Branch(), scanSetup.Git)
267251
if err != nil {
268252
return

commands/scanpullrequest_test.go

+27-38
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const (
3434
testCleanProjConfigPath = "testdata/config/frogbot-config-clean-test-proj.yml"
3535
testProjConfigPath = "testdata/config/frogbot-config-test-proj.yml"
3636
testProjConfigPathNoFail = "testdata/config/frogbot-config-test-proj-no-fail.yml"
37-
testSameBranchProjConfigPath = "testdata/config/frogbot-config-test-same-branch-fail.yml"
37+
testSourceBranchName = "pr"
38+
testTargetBranchName = "master"
3839
)
3940

4041
func TestCreateVulnerabilitiesRows(t *testing.T) {
@@ -504,32 +505,6 @@ func TestScanPullRequest(t *testing.T) {
504505
testScanPullRequest(t, testProjConfigPath, "test-proj", true)
505506
}
506507

507-
func TestScanPullRequestSameBranchFail(t *testing.T) {
508-
params, restoreEnv := verifyEnv(t)
509-
defer restoreEnv()
510-
511-
// Create mock GitLab server
512-
projectName := "test-same-branch-fail"
513-
514-
server := httptest.NewServer(createGitLabHandler(t, projectName))
515-
defer server.Close()
516-
517-
configAggregator, client := prepareConfigAndClient(t, testSameBranchProjConfigPath, server, params)
518-
_, cleanUp := utils.PrepareTestEnvironment(t, projectName, "scanpullrequest")
519-
defer cleanUp()
520-
521-
// Run "frogbot scan pull request"
522-
var scanPullRequest ScanPullRequestCmd
523-
err := scanPullRequest.Run(configAggregator, client)
524-
exceptedError := fmt.Errorf(utils.ErrScanPullRequestSameBranches, "main")
525-
assert.Equal(t, exceptedError, err)
526-
527-
// Check env sanitize
528-
err = utils.SanitizeEnv()
529-
assert.NoError(t, err)
530-
utils.AssertSanitizedEnv(t)
531-
}
532-
533508
func TestScanPullRequestNoFail(t *testing.T) {
534509
testScanPullRequest(t, testProjConfigPathNoFail, "test-proj", false)
535510
}
@@ -663,30 +638,43 @@ func TestScanPullRequestError(t *testing.T) {
663638
// Create HTTP handler to mock GitLab server
664639
func createGitLabHandler(t *testing.T, projectName string) http.HandlerFunc {
665640
return func(w http.ResponseWriter, r *http.Request) {
641+
switch r.RequestURI {
666642
// Return 200 on ping
667-
if r.RequestURI == "/api/v4/" {
643+
case "/api/v4/":
668644
w.WriteHeader(http.StatusOK)
669645
return
670-
}
671-
672-
// Return test-proj.tar.gz when using DownloadRepository
673-
if r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/repository/archive.tar.gz?sha=master", "%2F"+projectName) {
646+
// Mimic get pull request by ID
647+
case fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1", "%2F"+projectName):
674648
w.WriteHeader(http.StatusOK)
675-
repoFile, err := os.ReadFile(filepath.Join("..", projectName+".tar.gz"))
649+
expectedResponse, err := os.ReadFile(filepath.Join("..", "expectedPullRequestDetailsResponse.json"))
650+
assert.NoError(t, err)
651+
_, err = w.Write(expectedResponse)
652+
assert.NoError(t, err)
653+
return
654+
// Mimic download specific branch to scan
655+
case fmt.Sprintf("/api/v4/projects/jfrog%s/repository/archive.tar.gz?sha=%s", "%2F"+projectName, testSourceBranchName):
656+
w.WriteHeader(http.StatusOK)
657+
repoFile, err := os.ReadFile(filepath.Join("..", projectName, "sourceBranch.gz"))
676658
assert.NoError(t, err)
677659
_, err = w.Write(repoFile)
678660
assert.NoError(t, err)
679-
}
661+
return
662+
// Download repository mock
663+
case fmt.Sprintf("/api/v4/projects/jfrog%s/repository/archive.tar.gz?sha=%s", "%2F"+projectName, testTargetBranchName):
664+
w.WriteHeader(http.StatusOK)
665+
repoFile, err := os.ReadFile(filepath.Join("..", projectName, "targetBranch.gz"))
666+
assert.NoError(t, err)
667+
_, err = w.Write(repoFile)
668+
assert.NoError(t, err)
669+
return
680670
// clean-test-proj should not include any vulnerabilities so assertion is not needed.
681-
if r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1/notes", "%2Fclean-test-proj") {
671+
case fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1/notes", "%2Fclean-test-proj"):
682672
w.WriteHeader(http.StatusOK)
683673
_, err := w.Write([]byte("{}"))
684674
assert.NoError(t, err)
685675
return
686-
}
687-
688676
// Return 200 when using the REST that creates the comment
689-
if r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1/notes", "%2F"+projectName) {
677+
case fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1/notes", "%2F"+projectName):
690678
buf := new(bytes.Buffer)
691679
_, err := buf.ReadFrom(r.Body)
692680
assert.NoError(t, err)
@@ -707,6 +695,7 @@ func createGitLabHandler(t *testing.T, projectName string) http.HandlerFunc {
707695
w.WriteHeader(http.StatusOK)
708696
_, err = w.Write([]byte("{}"))
709697
assert.NoError(t, err)
698+
return
710699
}
711700
}
712701
}

commands/scanpullrequests.go

+9-69
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ type ScanAllPullRequestsCmd struct {
1919

2020
func (cmd ScanAllPullRequestsCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient) error {
2121
for _, config := range configAggregator {
22+
log.Info("Scanning all open pull requests for repository:", config.RepoName)
23+
log.Info("-----------------------------------------------------------")
2224
err := scanAllPullRequests(config, client)
2325
if err != nil {
2426
return err
2527
}
2628
}
27-
2829
return nil
2930
}
3031

@@ -43,14 +44,14 @@ func scanAllPullRequests(repo utils.Repository, client vcsclient.VcsClient) (err
4344
if e != nil {
4445
err = errors.Join(err, fmt.Errorf(errPullRequestScan, int(pr.ID), repo.RepoName, e.Error()))
4546
}
46-
if shouldScan {
47-
e = downloadAndScanPullRequest(pr, repo, client)
48-
// If error, write it in errList and continue to the next PR.
49-
if e != nil {
50-
err = errors.Join(err, fmt.Errorf(errPullRequestScan, int(pr.ID), repo.RepoName, e.Error()))
51-
}
52-
} else {
47+
if !shouldScan {
5348
log.Info("Pull Request", pr.ID, "has already been scanned before. If you wish to scan it again, please comment \"rescan\".")
49+
return
50+
}
51+
spr := &ScanPullRequestCmd{pullRequestDetails: pr}
52+
if e = spr.Run(utils.RepoAggregator{repo}, client); e != nil {
53+
// If error, write it in errList and continue to the next PR.
54+
err = errors.Join(err, fmt.Errorf(errPullRequestScan, int(pr.ID), repo.RepoName, e.Error()))
5455
}
5556
}
5657
return
@@ -83,64 +84,3 @@ func shouldScanPullRequest(repo utils.Repository, client vcsclient.VcsClient, pr
8384
func isFrogbotRescanComment(comment string) bool {
8485
return strings.Contains(strings.ToLower(strings.TrimSpace(comment)), utils.RescanRequestComment)
8586
}
86-
87-
func downloadAndScanPullRequest(pr vcsclient.PullRequestInfo, repo utils.Repository, client vcsclient.VcsClient) (err error) {
88-
// Download the pull request source ("from") branch
89-
params := utils.Params{
90-
Git: utils.Git{
91-
ClientInfo: utils.ClientInfo{
92-
GitProvider: repo.GitProvider,
93-
VcsInfo: vcsclient.VcsInfo{APIEndpoint: repo.APIEndpoint, Token: repo.Token},
94-
RepoOwner: repo.RepoOwner,
95-
RepoName: pr.Source.Repository,
96-
Branches: []string{pr.Source.Name}},
97-
}}
98-
frogbotParams := &utils.Repository{
99-
Server: repo.Server,
100-
Params: params,
101-
}
102-
wd, cleanup, err := utils.DownloadRepoToTempDir(client, pr.Source.Name, &frogbotParams.Git)
103-
if err != nil {
104-
return err
105-
}
106-
// Cleanup
107-
defer func() {
108-
err = errors.Join(err, cleanup())
109-
}()
110-
restoreDir, err := utils.Chdir(wd)
111-
if err != nil {
112-
return err
113-
}
114-
defer func() {
115-
err = errors.Join(err, restoreDir())
116-
}()
117-
// The target branch (to) will be downloaded as part of the Frogbot scanPullRequest execution
118-
params = utils.Params{
119-
Scan: utils.Scan{
120-
FailOnSecurityIssues: repo.FailOnSecurityIssues,
121-
IncludeAllVulnerabilities: repo.IncludeAllVulnerabilities,
122-
Projects: repo.Projects,
123-
},
124-
Git: utils.Git{
125-
ClientInfo: utils.ClientInfo{
126-
GitProvider: repo.GitProvider,
127-
VcsInfo: vcsclient.VcsInfo{APIEndpoint: repo.APIEndpoint, Token: repo.Token},
128-
RepoOwner: repo.RepoOwner,
129-
Branches: []string{pr.Target.Name},
130-
RepoName: pr.Target.Repository,
131-
},
132-
PullRequestID: int(pr.ID),
133-
},
134-
JFrogPlatform: utils.JFrogPlatform{
135-
Watches: repo.Watches,
136-
JFrogProjectKey: repo.JFrogProjectKey,
137-
},
138-
}
139-
140-
frogbotParams = &utils.Repository{
141-
OutputWriter: utils.GetCompatibleOutputWriter(repo.GitProvider),
142-
Server: repo.Server,
143-
Params: params,
144-
}
145-
return scanPullRequest(frogbotParams, client)
146-
}

0 commit comments

Comments
 (0)