github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/scanrepository/scanrepository.go (about) 1 package scanrepository 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "path/filepath" 8 "regexp" 9 "strings" 10 11 "github.com/jfrog/frogbot/packagehandlers" 12 "github.com/jfrog/frogbot/utils" 13 "github.com/jfrog/frogbot/utils/outputwriter" 14 "github.com/jfrog/froggit-go/vcsclient" 15 "github.com/jfrog/froggit-go/vcsutils" 16 "github.com/jfrog/gofrog/version" 17 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 18 "github.com/jfrog/jfrog-cli-core/v2/xray/formats" 19 xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" 20 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 21 "github.com/jfrog/jfrog-client-go/utils/log" 22 "golang.org/x/exp/maps" 23 "golang.org/x/exp/slices" 24 ) 25 26 type ScanRepositoryCmd struct { 27 // The interface that Frogbot utilizes to format and style the displayed messages on the Git providers 28 outputwriter.OutputWriter 29 // dryRun is used for testing purposes, mocking part of the git commands that requires networking 30 dryRun bool 31 // When dryRun is enabled, dryRunRepoPath specifies the repository local path to clone 32 dryRunRepoPath string 33 // The scanDetails of the current scan 34 scanDetails *utils.ScanDetails 35 // The base working directory 36 baseWd string 37 // The git client the command performs git operations with 38 gitManager *utils.GitManager 39 // Determines whether to open a pull request for each vulnerability fix or to aggregate all fixes into one pull request 40 aggregateFixes bool 41 // The current project technology 42 projectTech []coreutils.Technology 43 // Stores all package manager handlers for detected issues 44 handlers map[coreutils.Technology]packagehandlers.PackageHandler 45 } 46 47 func (cfp *ScanRepositoryCmd) Run(repoAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) (err error) { 48 if err = utils.ValidateSingleRepoConfiguration(&repoAggregator); err != nil { 49 return err 50 } 51 repository := repoAggregator[0] 52 repository.OutputWriter.SetHasInternetConnection(frogbotRepoConnection.IsConnected()) 53 return cfp.scanAndFixRepository(&repository, client) 54 } 55 56 func (cfp *ScanRepositoryCmd) scanAndFixRepository(repository *utils.Repository, client vcsclient.VcsClient) (err error) { 57 if err = cfp.setCommandPrerequisites(repository, client); err != nil { 58 return 59 } 60 for _, branch := range repository.Branches { 61 cfp.scanDetails.SetBaseBranch(branch) 62 cfp.scanDetails.SetXscGitInfoContext(branch, repository.Project, client) 63 if err = cfp.scanAndFixBranch(repository); err != nil { 64 return 65 } 66 } 67 return 68 } 69 70 func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (err error) { 71 clonedRepoDir, restoreBaseDir, err := cfp.cloneRepositoryAndCheckoutToBranch() 72 if err != nil { 73 return 74 } 75 cfp.baseWd = clonedRepoDir 76 defer func() { 77 // On dry run don't delete the folder as we want to validate results 78 if cfp.dryRun { 79 return 80 } 81 err = errors.Join(err, restoreBaseDir(), fileutils.RemoveTempDir(clonedRepoDir)) 82 }() 83 for i := range repository.Projects { 84 cfp.scanDetails.Project = &repository.Projects[i] 85 cfp.projectTech = []coreutils.Technology{} 86 if err = cfp.scanAndFixProject(repository); err != nil { 87 return 88 } 89 } 90 return 91 } 92 93 func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Repository, client vcsclient.VcsClient) (err error) { 94 // Set the scan details 95 cfp.scanDetails = utils.NewScanDetails(client, &repository.Server, &repository.Git). 96 SetXrayGraphScanParams(repository.Watches, repository.JFrogProjectKey, len(repository.AllowedLicenses) > 0). 97 SetFailOnInstallationErrors(*repository.FailOnSecurityIssues). 98 SetFixableOnly(repository.FixableOnly). 99 SetMinSeverity(repository.MinSeverity) 100 repositoryInfo, err := client.GetRepositoryInfo(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName) 101 if err != nil { 102 return 103 } 104 cfp.scanDetails.Git.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP 105 // Set the flag for aggregating fixes to generate a unified pull request for fixing vulnerabilities 106 cfp.aggregateFixes = repository.Git.AggregateFixes 107 // Set the outputwriter interface for the relevant vcs git provider 108 cfp.OutputWriter = outputwriter.GetCompatibleOutputWriter(repository.GitProvider) 109 // Set the git client to perform git operations 110 cfp.gitManager, err = utils.NewGitManager(). 111 SetAuth(cfp.scanDetails.Username, cfp.scanDetails.Token). 112 SetDryRun(cfp.dryRun, cfp.dryRunRepoPath). 113 SetRemoteGitUrl(cfp.scanDetails.Git.RepositoryCloneUrl) 114 if err != nil { 115 return 116 } 117 _, err = cfp.gitManager.SetGitParams(cfp.scanDetails.Git) 118 return 119 } 120 121 func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) error { 122 var fixNeeded bool 123 // A map that contains the full project paths as a keys 124 // The value is a map of vulnerable package names -> the scanDetails of the vulnerable packages. 125 // That means we have a map of all the vulnerabilities that were found in a specific folder, along with their full scanDetails. 126 vulnerabilitiesByPathMap := make(map[string]map[string]*utils.VulnerabilityDetails) 127 projectFullPathWorkingDirs := utils.GetFullPathWorkingDirs(cfp.scanDetails.Project.WorkingDirs, cfp.baseWd) 128 for _, fullPathWd := range projectFullPathWorkingDirs { 129 scanResults, err := cfp.scan(fullPathWd) 130 if err != nil { 131 return err 132 } 133 134 if repository.GitProvider.String() == vcsutils.GitHub.String() { 135 // Uploads Sarif results to GitHub in order to view the scan in the code scanning UI 136 // Currently available on GitHub only 137 if err = utils.UploadSarifResultsToGithubSecurityTab(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client()); err != nil { 138 log.Warn(err) 139 } 140 } 141 142 // Prepare the vulnerabilities map for each working dir path 143 currPathVulnerabilities, err := cfp.getVulnerabilitiesMap(scanResults, scanResults.IsMultipleProject()) 144 if err != nil { 145 return err 146 } 147 if len(currPathVulnerabilities) > 0 { 148 fixNeeded = true 149 } 150 vulnerabilitiesByPathMap[fullPathWd] = currPathVulnerabilities 151 } 152 if fixNeeded { 153 return cfp.fixVulnerablePackages(vulnerabilitiesByPathMap) 154 } 155 return nil 156 } 157 158 // Audit the dependencies of the current commit. 159 func (cfp *ScanRepositoryCmd) scan(currentWorkingDir string) (*xrayutils.Results, error) { 160 // Audit commit code 161 auditResults, err := cfp.scanDetails.RunInstallAndAudit(currentWorkingDir) 162 if err != nil { 163 return nil, err 164 } 165 log.Info("Xray scan completed") 166 contextualAnalysisResultsExists := len(auditResults.ExtendedScanResults.ApplicabilityScanResults) > 0 167 entitledForJas := auditResults.ExtendedScanResults.EntitledForJas 168 cfp.OutputWriter.SetJasOutputFlags(entitledForJas, contextualAnalysisResultsExists) 169 cfp.projectTech = auditResults.GetScaScannedTechnologies() 170 return auditResults, nil 171 } 172 173 func (cfp *ScanRepositoryCmd) getVulnerabilitiesMap(scanResults *xrayutils.Results, isMultipleRoots bool) (map[string]*utils.VulnerabilityDetails, error) { 174 vulnerabilitiesMap, err := cfp.createVulnerabilitiesMap(scanResults, isMultipleRoots) 175 if err != nil { 176 return nil, err 177 } 178 179 // Nothing to fix, return 180 if len(vulnerabilitiesMap) == 0 { 181 log.Info("Didn't find vulnerable dependencies with existing fix versions for", cfp.scanDetails.RepoName) 182 } 183 return vulnerabilitiesMap, nil 184 } 185 186 func (cfp *ScanRepositoryCmd) fixVulnerablePackages(vulnerabilitiesByWdMap map[string]map[string]*utils.VulnerabilityDetails) (err error) { 187 if cfp.aggregateFixes { 188 return cfp.fixIssuesSinglePR(vulnerabilitiesByWdMap) 189 } 190 return cfp.fixIssuesSeparatePRs(vulnerabilitiesByWdMap) 191 } 192 193 func (cfp *ScanRepositoryCmd) fixIssuesSeparatePRs(vulnerabilitiesMap map[string]map[string]*utils.VulnerabilityDetails) error { 194 var err error 195 for fullPath, vulnerabilities := range vulnerabilitiesMap { 196 if e := cfp.fixProjectVulnerabilities(fullPath, vulnerabilities); e != nil { 197 err = errors.Join(err, fmt.Errorf("the following errors occured while fixing vulnerabilities in %s:\n%s", fullPath, e)) 198 } 199 } 200 return err 201 } 202 203 func (cfp *ScanRepositoryCmd) fixProjectVulnerabilities(fullProjectPath string, vulnerabilities map[string]*utils.VulnerabilityDetails) (err error) { 204 // Update the working directory to the project's current working directory 205 projectWorkingDir := utils.GetRelativeWd(fullProjectPath, cfp.baseWd) 206 207 // 'CD' into the relevant working directory 208 if projectWorkingDir != "" { 209 var restoreDirFunc func() error 210 if restoreDirFunc, err = utils.Chdir(projectWorkingDir); err != nil { 211 return 212 } 213 defer func() { 214 err = errors.Join(err, restoreDirFunc()) 215 }() 216 } 217 218 // Fix every vulnerability in a separate pull request and branch 219 for _, vulnerability := range vulnerabilities { 220 if e := cfp.fixSinglePackageAndCreatePR(vulnerability); e != nil { 221 err = errors.Join(err, cfp.handleUpdatePackageErrors(e)) 222 } 223 224 // After fixing the current vulnerability, checkout to the base branch to start fixing the next vulnerability 225 if e := cfp.gitManager.Checkout(cfp.scanDetails.BaseBranch()); e != nil { 226 err = errors.Join(err, cfp.handleUpdatePackageErrors(e)) 227 return 228 } 229 } 230 231 return 232 } 233 234 func (cfp *ScanRepositoryCmd) fixMultiplePackages(fullProjectPath string, vulnerabilities map[string]*utils.VulnerabilityDetails) (fixedVulnerabilities []*utils.VulnerabilityDetails, err error) { 235 // Update the working directory to the project's current working directory 236 projectWorkingDir := utils.GetRelativeWd(fullProjectPath, cfp.baseWd) 237 238 // 'CD' into the relevant working directory 239 if projectWorkingDir != "" { 240 var restoreDir func() error 241 restoreDir, err = utils.Chdir(projectWorkingDir) 242 if err != nil { 243 return nil, err 244 } 245 defer func() { 246 err = errors.Join(err, restoreDir()) 247 }() 248 } 249 for _, vulnDetails := range vulnerabilities { 250 if e := cfp.updatePackageToFixedVersion(vulnDetails); e != nil { 251 err = errors.Join(err, cfp.handleUpdatePackageErrors(e)) 252 continue 253 } 254 fixedVulnerabilities = append(fixedVulnerabilities, vulnDetails) 255 log.Info(fmt.Sprintf("Updated dependency '%s' to version '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) 256 } 257 return 258 } 259 260 // fixIssuesSinglePR fixes all the vulnerabilities in a single aggregated pull request. 261 // If an existing aggregated fix is present, it checks for different scan results. 262 // If the scan results are the same, no action is taken. 263 // Otherwise, it performs a force push to the same branch and reopens the pull request if it was closed. 264 // Only one aggregated pull request should remain open at all times. 265 func (cfp *ScanRepositoryCmd) fixIssuesSinglePR(vulnerabilitiesMap map[string]map[string]*utils.VulnerabilityDetails) (err error) { 266 aggregatedFixBranchName := cfp.gitManager.GenerateAggregatedFixBranchName(cfp.scanDetails.BaseBranch(), cfp.projectTech) 267 existingPullRequestDetails, err := cfp.getOpenPullRequestBySourceBranch(aggregatedFixBranchName) 268 if err != nil { 269 return 270 } 271 return cfp.aggregateFixAndOpenPullRequest(vulnerabilitiesMap, aggregatedFixBranchName, existingPullRequestDetails) 272 } 273 274 // Handles possible error of update package operation 275 // When the expected custom error occurs, log to debug. 276 // else, return the error 277 func (cfp *ScanRepositoryCmd) handleUpdatePackageErrors(err error) error { 278 var errUnsupportedFix *utils.ErrUnsupportedFix 279 if errors.As(err, &errUnsupportedFix) { 280 log.Debug(strings.TrimSpace(err.Error())) 281 return nil 282 } 283 return err 284 } 285 286 // Creates a branch for the fixed package and open pull request against the target branch. 287 // In case a branch already exists on remote, we skip it. 288 func (cfp *ScanRepositoryCmd) fixSinglePackageAndCreatePR(vulnDetails *utils.VulnerabilityDetails) (err error) { 289 fixVersion := vulnDetails.SuggestedFixedVersion 290 log.Debug("Attempting to fix", fmt.Sprintf("%s:%s", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion), "with", fixVersion) 291 fixBranchName, err := cfp.gitManager.GenerateFixBranchName(cfp.scanDetails.BaseBranch(), vulnDetails.ImpactedDependencyName, fixVersion) 292 if err != nil { 293 return 294 } 295 existsInRemote, err := cfp.gitManager.BranchExistsInRemote(fixBranchName) 296 if err != nil { 297 return 298 } 299 if existsInRemote { 300 log.Info(fmt.Sprintf("A pull request updating the dependency '%s' to version '%s' already exists. Skipping...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) 301 return 302 } 303 304 workTreeIsClean, err := cfp.gitManager.IsClean() 305 if !workTreeIsClean { 306 var removeTempDirCallback func() error 307 err, removeTempDirCallback = cfp.gitManager.CreateBranchAndCheckoutWithCopyingFilesDiff(fixBranchName) 308 defer func() { 309 err = errors.Join(err, removeTempDirCallback()) 310 }() 311 if err != nil { 312 err = fmt.Errorf("failed while creating branch %s: %s", fixBranchName, err.Error()) 313 return 314 } 315 } else { 316 if err = cfp.gitManager.CreateBranchAndCheckout(fixBranchName); err != nil { 317 err = fmt.Errorf("failed while creating branch %s: %s", fixBranchName, err.Error()) 318 return 319 } 320 } 321 322 if err = cfp.updatePackageToFixedVersion(vulnDetails); err != nil { 323 return 324 } 325 if err = cfp.openFixingPullRequest(fixBranchName, vulnDetails); err != nil { 326 return fmt.Errorf("failed while creating a fixing pull request for: %s with version: %s with error: \n%s", 327 vulnDetails.ImpactedDependencyName, fixVersion, err.Error()) 328 } 329 log.Info(fmt.Sprintf("Created Pull Request updating dependency '%s' to version '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) 330 return 331 } 332 333 func (cfp *ScanRepositoryCmd) openFixingPullRequest(fixBranchName string, vulnDetails *utils.VulnerabilityDetails) (err error) { 334 log.Debug("Checking if there are changes to commit") 335 isClean, err := cfp.gitManager.IsClean() 336 if err != nil { 337 return 338 } 339 if isClean { 340 return fmt.Errorf("there were no changes to commit after fixing the package '%s'", vulnDetails.ImpactedDependencyName) 341 } 342 commitMessage := cfp.gitManager.GenerateCommitMessage(vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion) 343 if err = cfp.gitManager.AddAllAndCommit(commitMessage); err != nil { 344 return 345 } 346 if err = cfp.gitManager.Push(false, fixBranchName); err != nil { 347 return 348 } 349 pullRequestTitle, prBody, err := cfp.preparePullRequestDetails(vulnDetails) 350 if err != nil { 351 return 352 } 353 log.Debug("Creating Pull Request form:", fixBranchName, " to:", cfp.scanDetails.BaseBranch()) 354 return cfp.scanDetails.Client().CreatePullRequest(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName, fixBranchName, cfp.scanDetails.BaseBranch(), pullRequestTitle, prBody) 355 } 356 357 // openAggregatedPullRequest handles the opening or updating of a pull request when the aggregate mode is active. 358 // If a pull request is already open, Frogbot will update the branch and the pull request body. 359 func (cfp *ScanRepositoryCmd) openAggregatedPullRequest(fixBranchName string, pullRequestInfo *vcsclient.PullRequestInfo, vulnerabilities []*utils.VulnerabilityDetails) (err error) { 360 commitMessage := cfp.gitManager.GenerateAggregatedCommitMessage(cfp.projectTech) 361 if err = cfp.gitManager.AddAllAndCommit(commitMessage); err != nil { 362 return 363 } 364 if err = cfp.gitManager.Push(true, fixBranchName); err != nil { 365 return 366 } 367 pullRequestTitle, prBody, err := cfp.preparePullRequestDetails(vulnerabilities...) 368 if err != nil { 369 return 370 } 371 if pullRequestInfo == nil { 372 log.Info("Creating Pull Request from:", fixBranchName, "to:", cfp.scanDetails.BaseBranch()) 373 return cfp.scanDetails.Client().CreatePullRequest(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName, fixBranchName, cfp.scanDetails.BaseBranch(), pullRequestTitle, prBody) 374 } 375 log.Info("Updating Pull Request from:", fixBranchName, "to:", cfp.scanDetails.BaseBranch()) 376 return cfp.scanDetails.Client().UpdatePullRequest(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName, pullRequestTitle, prBody, pullRequestInfo.Target.Name, int(pullRequestInfo.ID), vcsutils.Open) 377 } 378 379 func (cfp *ScanRepositoryCmd) preparePullRequestDetails(vulnerabilitiesDetails ...*utils.VulnerabilityDetails) (prTitle string, prBody string, err error) { 380 if cfp.dryRun && cfp.aggregateFixes { 381 // For testings, don't compare pull request body as scan results order may change. 382 return cfp.gitManager.GenerateAggregatedPullRequestTitle(cfp.projectTech), "", nil 383 } 384 vulnerabilitiesRows := utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilitiesDetails) 385 prBody = utils.GenerateFixPullRequestDetails(vulnerabilitiesRows, cfp.OutputWriter) 386 if cfp.aggregateFixes { 387 var scanHash string 388 if scanHash, err = utils.VulnerabilityDetailsToMD5Hash(vulnerabilitiesRows...); err != nil { 389 return 390 } 391 return cfp.gitManager.GenerateAggregatedPullRequestTitle(cfp.projectTech), prBody + outputwriter.MarkdownComment(fmt.Sprintf("Checksum: %s", scanHash)), nil 392 } 393 // In separate pull requests there is only one vulnerability 394 vulnDetails := vulnerabilitiesDetails[0] 395 pullRequestTitle := cfp.gitManager.GeneratePullRequestTitle(vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion) 396 return pullRequestTitle, prBody, nil 397 } 398 399 func (cfp *ScanRepositoryCmd) cloneRepositoryAndCheckoutToBranch() (tempWd string, restoreDir func() error, err error) { 400 if cfp.dryRun { 401 tempWd = filepath.Join(cfp.dryRunRepoPath, cfp.scanDetails.RepoName) 402 } else { 403 // Create temp working directory 404 if tempWd, err = fileutils.CreateTempDir(); err != nil { 405 return 406 } 407 } 408 log.Debug("Created temp working directory:", tempWd) 409 410 // Clone the content of the repo to the new working directory 411 if err = cfp.gitManager.Clone(tempWd, cfp.scanDetails.BaseBranch()); err != nil { 412 return 413 } 414 415 // 'CD' into the temp working directory 416 restoreDir, err = utils.Chdir(tempWd) 417 return 418 } 419 420 // Create a vulnerabilities map - a map with 'impacted package' as a key and all the necessary information of this vulnerability as value. 421 func (cfp *ScanRepositoryCmd) createVulnerabilitiesMap(scanResults *xrayutils.Results, isMultipleRoots bool) (map[string]*utils.VulnerabilityDetails, error) { 422 vulnerabilitiesMap := map[string]*utils.VulnerabilityDetails{} 423 for _, scanResult := range scanResults.GetScaScansXrayResults() { 424 if len(scanResult.Vulnerabilities) > 0 { 425 vulnerabilities, err := xrayutils.PrepareVulnerabilities(scanResult.Vulnerabilities, scanResults, isMultipleRoots, true) 426 if err != nil { 427 return nil, err 428 } 429 for i := range vulnerabilities { 430 if err = cfp.addVulnerabilityToFixVersionsMap(&vulnerabilities[i], vulnerabilitiesMap); err != nil { 431 return nil, err 432 } 433 } 434 } else if len(scanResult.Violations) > 0 { 435 violations, _, _, err := xrayutils.PrepareViolations(scanResult.Violations, scanResults, isMultipleRoots, true) 436 if err != nil { 437 return nil, err 438 } 439 for i := range violations { 440 if err = cfp.addVulnerabilityToFixVersionsMap(&violations[i], vulnerabilitiesMap); err != nil { 441 return nil, err 442 } 443 } 444 } 445 } 446 if len(vulnerabilitiesMap) > 0 { 447 log.Debug("Frogbot will attempt to resolve the following vulnerable dependencies:\n", strings.Join(maps.Keys(vulnerabilitiesMap), ",\n")) 448 } 449 return vulnerabilitiesMap, nil 450 } 451 452 func (cfp *ScanRepositoryCmd) addVulnerabilityToFixVersionsMap(vulnerability *formats.VulnerabilityOrViolationRow, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails) error { 453 if len(vulnerability.FixedVersions) == 0 { 454 return nil 455 } 456 if len(cfp.projectTech) == 0 { 457 cfp.projectTech = []coreutils.Technology{vulnerability.Technology} 458 } 459 vulnFixVersion := getMinimalFixVersion(vulnerability.ImpactedDependencyVersion, vulnerability.FixedVersions) 460 if vulnFixVersion == "" { 461 return nil 462 } 463 if vulnDetails, exists := vulnerabilitiesMap[vulnerability.ImpactedDependencyName]; exists { 464 // More than one vulnerability can exist on the same impacted package. 465 // Among all possible fix versions that fix the above-impacted package, we select the maximum fix version. 466 vulnDetails.UpdateFixVersionIfMax(vulnFixVersion) 467 } else { 468 isDirectDependency, err := utils.IsDirectDependency(vulnerability.ImpactPaths) 469 if err != nil { 470 return err 471 } 472 // First appearance of a version that fixes the current impacted package 473 newVulnDetails := utils.NewVulnerabilityDetails(*vulnerability, vulnFixVersion) 474 newVulnDetails.SetIsDirectDependency(isDirectDependency) 475 vulnerabilitiesMap[vulnerability.ImpactedDependencyName] = newVulnDetails 476 } 477 // Set the fixed version array to the relevant fixed version so that only that specific fixed version will be displayed 478 vulnerability.FixedVersions = []string{vulnerabilitiesMap[vulnerability.ImpactedDependencyName].SuggestedFixedVersion} 479 return nil 480 } 481 482 // Updates impacted package, can return ErrUnsupportedFix. 483 func (cfp *ScanRepositoryCmd) updatePackageToFixedVersion(vulnDetails *utils.VulnerabilityDetails) (err error) { 484 if err = isBuildToolsDependency(vulnDetails); err != nil { 485 return 486 } 487 488 if cfp.handlers == nil { 489 cfp.handlers = make(map[coreutils.Technology]packagehandlers.PackageHandler) 490 } 491 492 handler := cfp.handlers[vulnDetails.Technology] 493 if handler == nil { 494 handler = packagehandlers.GetCompatiblePackageHandler(vulnDetails, cfp.scanDetails) 495 cfp.handlers[vulnDetails.Technology] = handler 496 } else if _, unsupported := handler.(*packagehandlers.UnsupportedPackageHandler); unsupported { 497 return 498 } 499 500 return cfp.handlers[vulnDetails.Technology].UpdateDependency(vulnDetails) 501 } 502 503 // The getRemoteBranchScanHash function extracts the checksum written inside the pull request body and returns it. 504 func (cfp *ScanRepositoryCmd) getRemoteBranchScanHash(prBody string) string { 505 // The pattern matches the string "Checksum: <checksum>", followed by one or more word characters (letters, digits, or underscores). 506 re := regexp.MustCompile(`Checksum: (\w+)`) 507 match := re.FindStringSubmatch(prBody) 508 509 // The first element is the entire matched string, and the second element is the checksum value. 510 // If the length of match is not equal to 2, it means that the pattern was not found or the captured group is missing. 511 if len(match) != 2 { 512 log.Debug("Checksum not found in the aggregated pull request. Frogbot will proceed to update the existing pull request.") 513 return "" 514 } 515 516 return match[1] 517 } 518 519 func (cfp *ScanRepositoryCmd) getOpenPullRequestBySourceBranch(branchName string) (prInfo *vcsclient.PullRequestInfo, err error) { 520 list, err := cfp.scanDetails.Client().ListOpenPullRequestsWithBody(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName) 521 if err != nil { 522 return 523 } 524 for _, pr := range list { 525 if pr.Source.Name == branchName { 526 log.Debug("Found pull request from source branch ", branchName) 527 return &pr, nil 528 } 529 } 530 log.Debug("No pull request found from source branch ", branchName) 531 return 532 } 533 534 func (cfp *ScanRepositoryCmd) aggregateFixAndOpenPullRequest(vulnerabilitiesMap map[string]map[string]*utils.VulnerabilityDetails, aggregatedFixBranchName string, existingPullRequestInfo *vcsclient.PullRequestInfo) (err error) { 535 log.Info("-----------------------------------------------------------------") 536 log.Info("Starting aggregated dependencies fix") 537 538 workTreeIsClean, err := cfp.gitManager.IsClean() 539 if !workTreeIsClean { 540 var removeTempDirCallback func() error 541 err, removeTempDirCallback = cfp.gitManager.CreateBranchAndCheckoutWithCopyingFilesDiff(aggregatedFixBranchName) 542 defer func() { 543 err = errors.Join(err, removeTempDirCallback()) 544 }() 545 if err != nil { 546 err = fmt.Errorf("failed while creating branch %s: %s", aggregatedFixBranchName, err.Error()) 547 return 548 } 549 } else { 550 if err = cfp.gitManager.CreateBranchAndCheckout(aggregatedFixBranchName); err != nil { 551 err = fmt.Errorf("failed while creating branch %s: %s", aggregatedFixBranchName, err.Error()) 552 return 553 } 554 } 555 556 // Fix all packages in the same branch if expected error accrued, log and continue. 557 var fixedVulnerabilities []*utils.VulnerabilityDetails 558 for fullPath, vulnerabilities := range vulnerabilitiesMap { 559 currentFixes, e := cfp.fixMultiplePackages(fullPath, vulnerabilities) 560 if e != nil { 561 err = errors.Join(err, fmt.Errorf("the following errors occured while fixing vulnerabilities in %s:\n%s", fullPath, e)) 562 continue 563 } 564 fixedVulnerabilities = append(fixedVulnerabilities, currentFixes...) 565 } 566 updateRequired, e := cfp.isUpdateRequired(fixedVulnerabilities, existingPullRequestInfo) 567 if e != nil { 568 err = errors.Join(err, e) 569 return 570 } 571 if !updateRequired { 572 log.Info("The existing pull request is in sync with the latest scan, and no further updates are required.") 573 return 574 } 575 if len(fixedVulnerabilities) > 0 { 576 if e = cfp.openAggregatedPullRequest(aggregatedFixBranchName, existingPullRequestInfo, fixedVulnerabilities); e != nil { 577 err = errors.Join(err, fmt.Errorf("failed while creating aggreagted pull request. Error: \n%s", e.Error())) 578 } 579 } 580 log.Info("-----------------------------------------------------------------") 581 err = errors.Join(err, cfp.gitManager.Checkout(cfp.scanDetails.BaseBranch())) 582 return 583 } 584 585 // Performs a comparison of the Xray scan results hashes between an existing pull request's remote source branch 586 // and the current source branch to identify any differences. 587 func (cfp *ScanRepositoryCmd) isUpdateRequired(fixedVulnerabilities []*utils.VulnerabilityDetails, prInfo *vcsclient.PullRequestInfo) (updateRequired bool, err error) { 588 if prInfo == nil { 589 updateRequired = true 590 return 591 } 592 log.Info("Aggregated pull request already exists, verifying if update is needed...") 593 log.Debug("Comparing current scan results to existing", prInfo.Target.Name, "scan results") 594 fixedVulnerabilitiesRows := utils.ExtractVulnerabilitiesDetailsToRows(fixedVulnerabilities) 595 currentScanHash, err := utils.VulnerabilityDetailsToMD5Hash(fixedVulnerabilitiesRows...) 596 if err != nil { 597 return 598 } 599 remoteBranchScanHash := cfp.getRemoteBranchScanHash(prInfo.Body) 600 updateRequired = currentScanHash != remoteBranchScanHash 601 if updateRequired { 602 log.Info("The existing pull request is not in sync with the latest scan, updating pull request...") 603 } 604 return 605 } 606 607 // getMinimalFixVersion find the minimal version that fixes the current impactedPackage; 608 // fixVersions is a sorted array. The function returns the first version in the array, that is larger than impactedPackageVersion. 609 func getMinimalFixVersion(impactedPackageVersion string, fixVersions []string) string { 610 // Trim 'v' prefix in case of Go package 611 currVersionStr := strings.TrimPrefix(impactedPackageVersion, "v") 612 currVersion := version.NewVersion(currVersionStr) 613 for _, fixVersion := range fixVersions { 614 fixVersionCandidate := parseVersionChangeString(fixVersion) 615 if currVersion.Compare(fixVersionCandidate) > 0 { 616 return fixVersionCandidate 617 } 618 } 619 return "" 620 } 621 622 // 1.0 --> 1.0 ≤ x 623 // (,1.0] --> x ≤ 1.0 624 // (,1.0) --> x < 1.0 625 // [1.0] --> x == 1.0 626 // (1.0,) --> 1.0 >= x 627 // (1.0, 2.0) --> 1.0 < x < 2.0 628 // [1.0, 2.0] --> 1.0 ≤ x ≤ 2.0 629 func parseVersionChangeString(fixVersion string) string { 630 latestVersion := strings.Split(fixVersion, ",")[0] 631 if latestVersion[0] == '(' { 632 return "" 633 } 634 latestVersion = strings.Trim(latestVersion, "[") 635 latestVersion = strings.Trim(latestVersion, "]") 636 return latestVersion 637 } 638 639 // Skip build tools dependencies (for example, pip) 640 // that are not defined in the descriptor file and cannot be fixed by a PR. 641 func isBuildToolsDependency(vulnDetails *utils.VulnerabilityDetails) error { 642 //nolint:typecheck // Ignoring typecheck error: The linter fails to deduce the returned type as []string from utils.BuildToolsDependenciesMap, despite its declaration in utils/utils.go as map[coreutils.Technology][]string. 643 if slices.Contains(utils.BuildToolsDependenciesMap[vulnDetails.Technology], vulnDetails.ImpactedDependencyName) { 644 return &utils.ErrUnsupportedFix{ 645 PackageName: vulnDetails.ImpactedDependencyName, 646 FixedVersion: vulnDetails.SuggestedFixedVersion, 647 ErrorType: utils.BuildToolsDependencyFixNotSupported, 648 } 649 } 650 return nil 651 }