Skip to content

Commit d1c5866

Browse files
authored
Scan PR - Add option to scan source and most common ancestor target c… (#763)
1 parent 6964eab commit d1c5866

File tree

4 files changed

+161
-19
lines changed

4 files changed

+161
-19
lines changed

scanpullrequest/scanpullrequest.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,7 @@ func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDeta
207207
// Download target branch (if needed)
208208
cleanupTarget := func() error { return nil }
209209
if !repoConfig.IncludeAllVulnerabilities {
210-
targetBranchInfo := repoConfig.PullRequestDetails.Target
211-
if targetBranchWd, cleanupTarget, err = utils.DownloadRepoToTempDir(scanDetails.Client(), targetBranchInfo.Owner, targetBranchInfo.Repository, targetBranchInfo.Name); err != nil {
210+
if targetBranchWd, cleanupTarget, err = prepareTargetForScan(repoConfig.PullRequestDetails, scanDetails); err != nil {
212211
return
213212
}
214213
}
@@ -230,6 +229,64 @@ func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDeta
230229
return
231230
}
232231

232+
func prepareTargetForScan(pullRequestDetails vcsclient.PullRequestInfo, scanDetails *utils.ScanDetails) (targetBranchWd string, cleanupTarget func() error, err error) {
233+
target := pullRequestDetails.Target
234+
// Download target branch
235+
if targetBranchWd, cleanupTarget, err = utils.DownloadRepoToTempDir(scanDetails.Client(), target.Owner, target.Repository, target.Name); err != nil {
236+
return
237+
}
238+
if !scanDetails.Git.UseMostCommonAncestorAsTarget {
239+
return
240+
}
241+
log.Debug("Using most common ancestor commit as target branch commit")
242+
// Get common parent commit between source and target and use it (checkout) to the target branch commit
243+
if e := tryCheckoutToMostCommonAncestor(scanDetails, pullRequestDetails.Source.Name, target.Name, targetBranchWd); e != nil {
244+
log.Warn(fmt.Sprintf("Failed to get best common ancestor commit between source branch: %s and target branch: %s, defaulting to target branch commit. Error: %s", pullRequestDetails.Source.Name, target.Name, e.Error()))
245+
}
246+
return
247+
}
248+
249+
func tryCheckoutToMostCommonAncestor(scanDetails *utils.ScanDetails, baseBranch, headBranch, targetBranchWd string) (err error) {
250+
repositoryInfo, err := scanDetails.Client().GetRepositoryInfo(context.Background(), scanDetails.RepoOwner, scanDetails.RepoName)
251+
if err != nil {
252+
return
253+
}
254+
scanDetails.Git.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP
255+
bestAncestorHash, err := getMostCommonAncestorCommitHash(scanDetails, baseBranch, headBranch)
256+
if err != nil {
257+
return
258+
}
259+
return checkoutToCommitAtTempWorkingDir(scanDetails, bestAncestorHash, targetBranchWd)
260+
}
261+
262+
func getMostCommonAncestorCommitHash(scanDetails *utils.ScanDetails, baseBranch, headBranch string) (hash string, err error) {
263+
gitManager, err := utils.NewGitManager().SetAuth(scanDetails.Username, scanDetails.Token).SetRemoteGitUrl(scanDetails.Git.RepositoryCloneUrl)
264+
if err != nil {
265+
return
266+
}
267+
return gitManager.GetMostCommonAncestorHash(baseBranch, headBranch)
268+
}
269+
270+
func checkoutToCommitAtTempWorkingDir(scanDetails *utils.ScanDetails, commitHash, wd string) (err error) {
271+
// Change working directory to the temp target branch directory
272+
cwd, err := os.Getwd()
273+
if err != nil {
274+
return
275+
}
276+
if err = os.Chdir(wd); err != nil {
277+
return
278+
}
279+
defer func() {
280+
err = errors.Join(err, os.Chdir(cwd))
281+
}()
282+
// Load .git info in directory and Checkout to the commit hash
283+
gitManager, err := utils.NewGitManager().SetAuth(scanDetails.Username, scanDetails.Token).SetRemoteGitUrl(scanDetails.Git.RepositoryCloneUrl)
284+
if err != nil {
285+
return
286+
}
287+
return gitManager.CheckoutToHash(commitHash, wd)
288+
}
289+
233290
func getAllIssues(cmdResults *results.SecurityCommandResults, allowedLicenses []string, hasViolationContext bool) (*utils.IssuesCollection, error) {
234291
log.Info("Frogbot is configured to show all vulnerabilities")
235292
simpleJsonResults, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{

utils/consts.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ const (
4242
GitUseLocalRepositoryEnv = "JF_USE_LOCAL_REPOSITORY"
4343

4444
// Git naming template environment variables
45-
BranchNameTemplateEnv = "JF_BRANCH_NAME_TEMPLATE"
46-
CommitMessageTemplateEnv = "JF_COMMIT_MESSAGE_TEMPLATE"
47-
PullRequestTitleTemplateEnv = "JF_PULL_REQUEST_TITLE_TEMPLATE"
48-
PullRequestCommentTitleEnv = "JF_PR_COMMENT_TITLE"
45+
BranchNameTemplateEnv = "JF_BRANCH_NAME_TEMPLATE"
46+
CommitMessageTemplateEnv = "JF_COMMIT_MESSAGE_TEMPLATE"
47+
PullRequestTitleTemplateEnv = "JF_PULL_REQUEST_TITLE_TEMPLATE"
48+
PullRequestCommentTitleEnv = "JF_PR_COMMENT_TITLE"
49+
UseMostCommonAncestorAsTargetEnv = "JF_USE_MOST_COMMON_ANCESTOR_AS_TARGET"
4950

5051
// Repository environment variables - Ignored if the frogbot-config.yml file is used
5152
InstallCommandEnv = "JF_INSTALL_DEPS_CMD"

utils/git.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
// "os/exec"
78
"regexp"
89
"strings"
910
"time"
@@ -160,6 +161,63 @@ func (gm *GitManager) Checkout(branchName string) error {
160161
return nil
161162
}
162163

164+
func (gm *GitManager) CheckoutToHash(hash, targetBranchWd string) error {
165+
if err := gm.Fetch(); err != nil {
166+
return err
167+
}
168+
log.Debug("Running git checkout to hash:", hash)
169+
if err := gm.createBranchAndCheckoutToHash(hash, false); err != nil {
170+
return fmt.Errorf("'git checkout %s' failed with error: %s", hash, err.Error())
171+
}
172+
return nil
173+
}
174+
175+
func (gm *GitManager) Fetch() error {
176+
log.Debug("Running git fetch...")
177+
err := gm.localGitRepository.Fetch(&git.FetchOptions{
178+
RemoteName: gm.remoteName,
179+
RemoteURL: gm.remoteGitUrl,
180+
Auth: gm.auth,
181+
})
182+
if err != nil && err != git.NoErrAlreadyUpToDate {
183+
return fmt.Errorf("git fetch failed with error: %s", err.Error())
184+
}
185+
return nil
186+
}
187+
188+
func (gm *GitManager) GetMostCommonAncestorHash(baseBranch, targetBranch string) (string, error) {
189+
// Get the commit of the base branch
190+
baseCommitHash, err := gm.localGitRepository.ResolveRevision(plumbing.Revision(baseBranch))
191+
if err != nil {
192+
return "", err
193+
}
194+
baseCommit, err := gm.localGitRepository.CommitObject(*baseCommitHash)
195+
if err != nil {
196+
return "", err
197+
}
198+
// Get the HEAD commit of the target branch
199+
headCommitHash, err := gm.localGitRepository.ResolveRevision(plumbing.Revision(targetBranch))
200+
if err != nil {
201+
return "", err
202+
}
203+
headCommit, err := gm.localGitRepository.CommitObject(*headCommitHash)
204+
if err != nil {
205+
return "", err
206+
}
207+
// Get the most common ancestor
208+
log.Debug(fmt.Sprintf("Finding common ancestor between %s and %s...", baseBranch, targetBranch))
209+
ancestorCommit, err := baseCommit.MergeBase(headCommit)
210+
if err != nil {
211+
return "", err
212+
}
213+
if len(ancestorCommit) == 0 {
214+
return "", fmt.Errorf("no common ancestor found for %s and %s", baseBranch, targetBranch)
215+
} else if len(ancestorCommit) > 1 {
216+
return "", fmt.Errorf("more than one common ancestor found for %s and %s", baseBranch, targetBranch)
217+
}
218+
return ancestorCommit[0].Hash.String(), nil
219+
}
220+
163221
func (gm *GitManager) Clone(destinationPath, branchName string) error {
164222
if gm.dryRun {
165223
// "Clone" the repository from the testdata folder
@@ -207,6 +265,26 @@ func (gm *GitManager) CreateBranchAndCheckout(branchName string, keepLocalChange
207265
return err
208266
}
209267

268+
func (gm *GitManager) createBranchAndCheckoutToHash(hash string, keepLocalChanges bool) error {
269+
var checkoutConfig *git.CheckoutOptions
270+
if keepLocalChanges {
271+
checkoutConfig = &git.CheckoutOptions{
272+
Hash: plumbing.NewHash(hash),
273+
Keep: true,
274+
}
275+
} else {
276+
checkoutConfig = &git.CheckoutOptions{
277+
Hash: plumbing.NewHash(hash),
278+
Force: true,
279+
}
280+
}
281+
worktree, err := gm.localGitRepository.Worktree()
282+
if err != nil {
283+
return err
284+
}
285+
return worktree.Checkout(checkoutConfig)
286+
}
287+
210288
func (gm *GitManager) createBranchAndCheckout(branchName string, create bool, keepLocalChanges bool) error {
211289
var checkoutConfig *git.CheckoutOptions
212290
if keepLocalChanges {

utils/params.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -297,19 +297,20 @@ func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) {
297297
type Git struct {
298298
GitProvider vcsutils.VcsProvider
299299
vcsclient.VcsInfo
300-
RepoOwner string
301-
RepoName string `yaml:"repoName,omitempty"`
302-
Branches []string `yaml:"branches,omitempty"`
303-
BranchNameTemplate string `yaml:"branchNameTemplate,omitempty"`
304-
CommitMessageTemplate string `yaml:"commitMessageTemplate,omitempty"`
305-
PullRequestTitleTemplate string `yaml:"pullRequestTitleTemplate,omitempty"`
306-
PullRequestCommentTitle string `yaml:"pullRequestCommentTitle,omitempty"`
307-
AvoidExtraMessages bool `yaml:"avoidExtraMessages,omitempty"`
308-
EmailAuthor string `yaml:"emailAuthor,omitempty"`
309-
AggregateFixes bool `yaml:"aggregateFixes,omitempty"`
310-
PullRequestDetails vcsclient.PullRequestInfo
311-
RepositoryCloneUrl string
312-
UseLocalRepository bool
300+
UseMostCommonAncestorAsTarget bool `yaml:"useMostCommonAncestorAsTarget,omitempty"`
301+
RepoOwner string
302+
RepoName string `yaml:"repoName,omitempty"`
303+
Branches []string `yaml:"branches,omitempty"`
304+
BranchNameTemplate string `yaml:"branchNameTemplate,omitempty"`
305+
CommitMessageTemplate string `yaml:"commitMessageTemplate,omitempty"`
306+
PullRequestTitleTemplate string `yaml:"pullRequestTitleTemplate,omitempty"`
307+
PullRequestCommentTitle string `yaml:"pullRequestCommentTitle,omitempty"`
308+
AvoidExtraMessages bool `yaml:"avoidExtraMessages,omitempty"`
309+
EmailAuthor string `yaml:"emailAuthor,omitempty"`
310+
AggregateFixes bool `yaml:"aggregateFixes,omitempty"`
311+
PullRequestDetails vcsclient.PullRequestInfo
312+
RepositoryCloneUrl string
313+
UseLocalRepository bool
313314
}
314315

315316
func (g *Git) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) (err error) {
@@ -349,6 +350,11 @@ func (g *Git) extractScanPullRequestEnvParams(gitParamsFromEnv *Git) (err error)
349350
if g.PullRequestCommentTitle == "" {
350351
g.PullRequestCommentTitle = getTrimmedEnv(PullRequestCommentTitleEnv)
351352
}
353+
if !g.UseMostCommonAncestorAsTarget {
354+
if g.UseMostCommonAncestorAsTarget, err = getBoolEnv(UseMostCommonAncestorAsTargetEnv, true); err != nil {
355+
return
356+
}
357+
}
352358
g.AvoidExtraMessages, err = getBoolEnv(AvoidExtraMessages, false)
353359
return
354360
}

0 commit comments

Comments
 (0)