github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/scanpullrequest/scanpullrequest.go (about) 1 package scanpullrequest 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 9 "github.com/jfrog/frogbot/utils" 10 "github.com/jfrog/froggit-go/vcsclient" 11 "github.com/jfrog/froggit-go/vcsutils" 12 "github.com/jfrog/gofrog/datastructures" 13 "github.com/jfrog/jfrog-cli-core/v2/xray/formats" 14 xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" 15 "github.com/jfrog/jfrog-client-go/utils/log" 16 "github.com/jfrog/jfrog-client-go/xray/services" 17 ) 18 19 const ( 20 SecurityIssueFoundErr = "issues were detected by Frogbot\n You can avoid marking the Frogbot scan as failed by setting failOnSecurityIssues to false in the " + utils.FrogbotConfigFile + " file" 21 noGitHubEnvErr = "frogbot did not scan this PR, because a GitHub Environment named 'frogbot' does not exist. Please refer to the Frogbot documentation for instructions on how to create the Environment" 22 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" 23 ) 24 25 type ScanPullRequestCmd struct{} 26 27 // Run ScanPullRequest method only works for a single repository scan. 28 // Therefore, the first repository config represents the repository on which Frogbot runs, and it is the only one that matters. 29 func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) (err error) { 30 if err = utils.ValidateSingleRepoConfiguration(&configAggregator); err != nil { 31 return 32 } 33 repoConfig := &(configAggregator)[0] 34 if repoConfig.GitProvider == vcsutils.GitHub { 35 if err = verifyGitHubFrogbotEnvironment(client, repoConfig); err != nil { 36 return 37 } 38 } 39 repoConfig.OutputWriter.SetHasInternetConnection(frogbotRepoConnection.IsConnected()) 40 if repoConfig.PullRequestDetails, err = client.GetPullRequestByID(context.Background(), repoConfig.RepoOwner, repoConfig.RepoName, int(repoConfig.PullRequestDetails.ID)); err != nil { 41 return 42 } 43 return scanPullRequest(repoConfig, client) 44 } 45 46 // Verify that the 'frogbot' GitHub environment was properly configured on the repository 47 func verifyGitHubFrogbotEnvironment(client vcsclient.VcsClient, repoConfig *utils.Repository) error { 48 if repoConfig.APIEndpoint != "" && repoConfig.APIEndpoint != "https://api.github.com" { 49 // Don't verify 'frogbot' environment on GitHub on-prem 50 return nil 51 } 52 if _, exist := os.LookupEnv(utils.GitHubActionsEnv); !exist { 53 // Don't verify 'frogbot' environment on non GitHub Actions CI 54 return nil 55 } 56 57 // If the repository is not public, using 'frogbot' environment is not mandatory 58 repoInfo, err := client.GetRepositoryInfo(context.Background(), repoConfig.RepoOwner, repoConfig.RepoName) 59 if err != nil { 60 return err 61 } 62 if repoInfo.RepositoryVisibility != vcsclient.Public { 63 return nil 64 } 65 66 // Get the 'frogbot' environment info and make sure it exists and includes reviewers 67 repoEnvInfo, err := client.GetRepositoryEnvironmentInfo(context.Background(), repoConfig.RepoOwner, repoConfig.RepoName, "frogbot") 68 if err != nil { 69 return errors.New(err.Error() + "\n" + noGitHubEnvErr) 70 } 71 if len(repoEnvInfo.Reviewers) == 0 { 72 return errors.New(noGitHubEnvReviewersErr) 73 } 74 75 return nil 76 } 77 78 // By default, includeAllVulnerabilities is set to false and the scan goes as follows: 79 // a. Audit the dependencies of the source and the target branches. 80 // b. Compare the vulnerabilities found in source and target branches, and show only the new vulnerabilities added by the pull request. 81 // Otherwise, only the source branch is scanned and all found vulnerabilities are being displayed. 82 func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err error) { 83 pullRequestDetails := repo.PullRequestDetails 84 log.Info(fmt.Sprintf("Scanning Pull Request #%d (from source branch: <%s/%s/%s> to target branch: <%s/%s/%s>)", 85 pullRequestDetails.ID, 86 pullRequestDetails.Source.Owner, pullRequestDetails.Source.Repository, pullRequestDetails.Source.Name, 87 pullRequestDetails.Target.Owner, pullRequestDetails.Target.Repository, pullRequestDetails.Target.Name)) 88 log.Info("-----------------------------------------------------------") 89 90 // Audit PR code 91 issues, err := auditPullRequest(repo, client) 92 if err != nil { 93 return 94 } 95 96 // Output results 97 shouldSendExposedSecretsEmail := issues.SecretsExists() && repo.SmtpServer != "" 98 if shouldSendExposedSecretsEmail { 99 secretsEmailDetails := utils.NewSecretsEmailDetails(client, repo, issues.Secrets) 100 if err = utils.AlertSecretsExposed(secretsEmailDetails); err != nil { 101 return 102 } 103 } 104 105 // Handle PR comments for scan output 106 if err = utils.HandlePullRequestCommentsAfterScan(issues, repo, client, int(pullRequestDetails.ID)); err != nil { 107 return 108 } 109 110 // Fail the Frogbot task if a security issue is found and Frogbot isn't configured to avoid the failure. 111 if toFailTaskStatus(repo, issues) { 112 err = errors.New(SecurityIssueFoundErr) 113 } 114 return 115 } 116 117 func toFailTaskStatus(repo *utils.Repository, issues *utils.IssuesCollection) bool { 118 failFlagSet := repo.FailOnSecurityIssues != nil && *repo.FailOnSecurityIssues 119 return failFlagSet && issues.IssuesExists() 120 } 121 122 // Downloads Pull Requests branches code and audits them 123 func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) (issuesCollection *utils.IssuesCollection, err error) { 124 scanDetails := utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Git). 125 SetXrayGraphScanParams(repoConfig.Watches, repoConfig.JFrogProjectKey, len(repoConfig.AllowedLicenses) > 0). 126 SetMinSeverity(repoConfig.MinSeverity). 127 SetFixableOnly(repoConfig.FixableOnly). 128 SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues) 129 130 issuesCollection = &utils.IssuesCollection{} 131 for i := range repoConfig.Projects { 132 scanDetails.SetProject(&repoConfig.Projects[i]) 133 var projectIssues *utils.IssuesCollection 134 if projectIssues, err = auditPullRequestInProject(repoConfig, scanDetails); err != nil { 135 return 136 } 137 issuesCollection.Append(projectIssues) 138 } 139 return 140 } 141 142 func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils.ScanDetails) (auditIssues *utils.IssuesCollection, err error) { 143 // Download source branch 144 sourcePullRequestInfo := scanDetails.PullRequestDetails.Source 145 sourceBranchWd, cleanupSource, err := utils.DownloadRepoToTempDir(scanDetails.Client(), sourcePullRequestInfo.Owner, sourcePullRequestInfo.Repository, sourcePullRequestInfo.Name) 146 if err != nil { 147 return 148 } 149 defer func() { 150 err = errors.Join(err, cleanupSource()) 151 }() 152 153 // Audit source branch 154 var sourceResults *xrayutils.Results 155 workingDirs := utils.GetFullPathWorkingDirs(scanDetails.Project.WorkingDirs, sourceBranchWd) 156 log.Info("Scanning source branch...") 157 sourceResults, err = scanDetails.RunInstallAndAudit(workingDirs...) 158 if err != nil { 159 return 160 } 161 162 // Set JAS output flags 163 sourceScanResults := sourceResults.ExtendedScanResults 164 repoConfig.OutputWriter.SetJasOutputFlags(sourceScanResults.EntitledForJas, len(sourceScanResults.ApplicabilityScanResults) > 0) 165 166 // Get all issues that exist in the source branch 167 if repoConfig.IncludeAllVulnerabilities { 168 if auditIssues, err = getAllIssues(sourceResults, repoConfig.AllowedLicenses); err != nil { 169 return 170 } 171 utils.ConvertSarifPathsToRelative(auditIssues, sourceBranchWd) 172 return 173 } 174 175 var targetBranchWd string 176 if auditIssues, targetBranchWd, err = auditTargetBranch(repoConfig, scanDetails, sourceResults); err != nil { 177 return 178 } 179 utils.ConvertSarifPathsToRelative(auditIssues, sourceBranchWd, targetBranchWd) 180 return 181 } 182 183 func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDetails, sourceScanResults *xrayutils.Results) (newIssues *utils.IssuesCollection, targetBranchWd string, err error) { 184 // Download target branch (if needed) 185 cleanupTarget := func() error { return nil } 186 if !repoConfig.IncludeAllVulnerabilities { 187 targetBranchInfo := repoConfig.PullRequestDetails.Target 188 if targetBranchWd, cleanupTarget, err = utils.DownloadRepoToTempDir(scanDetails.Client(), targetBranchInfo.Owner, targetBranchInfo.Repository, targetBranchInfo.Name); err != nil { 189 return 190 } 191 } 192 defer func() { 193 err = errors.Join(err, cleanupTarget()) 194 }() 195 196 // Set target branch scan details 197 var targetResults *xrayutils.Results 198 workingDirs := utils.GetFullPathWorkingDirs(scanDetails.Project.WorkingDirs, targetBranchWd) 199 log.Info("Scanning target branch...") 200 targetResults, err = scanDetails.RunInstallAndAudit(workingDirs...) 201 if err != nil { 202 return 203 } 204 205 // Get newly added issues 206 newIssues, err = getNewlyAddedIssues(targetResults, sourceScanResults, repoConfig.AllowedLicenses) 207 return 208 } 209 210 func getAllIssues(results *xrayutils.Results, allowedLicenses []string) (*utils.IssuesCollection, error) { 211 log.Info("Frogbot is configured to show all vulnerabilities") 212 scanResults := results.ExtendedScanResults 213 xraySimpleJson, err := xrayutils.ConvertXrayScanToSimpleJson(results, results.IsMultipleProject(), false, true, allowedLicenses) 214 if err != nil { 215 return nil, err 216 } 217 return &utils.IssuesCollection{ 218 Vulnerabilities: append(xraySimpleJson.Vulnerabilities, xraySimpleJson.SecurityViolations...), 219 Iacs: xrayutils.PrepareIacs(scanResults.IacScanResults), 220 Secrets: xrayutils.PrepareSecrets(scanResults.SecretsScanResults), 221 Sast: xrayutils.PrepareSast(scanResults.SastScanResults), 222 Licenses: xraySimpleJson.LicensesViolations, 223 }, nil 224 } 225 226 // Returns all the issues found in the source branch that didn't exist in the target branch. 227 func getNewlyAddedIssues(targetResults, sourceResults *xrayutils.Results, allowedLicenses []string) (*utils.IssuesCollection, error) { 228 var newVulnerabilitiesOrViolations []formats.VulnerabilityOrViolationRow 229 var newLicenses []formats.LicenseRow 230 var err error 231 if len(sourceResults.GetScaScansXrayResults()) > 0 { 232 if newVulnerabilitiesOrViolations, newLicenses, err = createNewVulnerabilitiesRows(targetResults, sourceResults, allowedLicenses); err != nil { 233 return nil, err 234 } 235 } 236 237 var newIacs []formats.SourceCodeRow 238 if len(sourceResults.ExtendedScanResults.IacScanResults) > 0 { 239 targetIacRows := xrayutils.PrepareIacs(targetResults.ExtendedScanResults.IacScanResults) 240 sourceIacRows := xrayutils.PrepareIacs(sourceResults.ExtendedScanResults.IacScanResults) 241 newIacs = createNewSourceCodeRows(targetIacRows, sourceIacRows) 242 } 243 244 var newSecrets []formats.SourceCodeRow 245 if len(sourceResults.ExtendedScanResults.SecretsScanResults) > 0 { 246 targetSecretsRows := xrayutils.PrepareIacs(targetResults.ExtendedScanResults.SecretsScanResults) 247 sourceSecretsRows := xrayutils.PrepareIacs(sourceResults.ExtendedScanResults.SecretsScanResults) 248 newSecrets = createNewSourceCodeRows(targetSecretsRows, sourceSecretsRows) 249 } 250 251 var newSast []formats.SourceCodeRow 252 if len(targetResults.ExtendedScanResults.SastScanResults) > 0 { 253 targetSastRows := xrayutils.PrepareSast(targetResults.ExtendedScanResults.SastScanResults) 254 sourceSastRows := xrayutils.PrepareSast(sourceResults.ExtendedScanResults.SastScanResults) 255 newSast = createNewSourceCodeRows(targetSastRows, sourceSastRows) 256 } 257 258 return &utils.IssuesCollection{ 259 Vulnerabilities: newVulnerabilitiesOrViolations, 260 Iacs: newIacs, 261 Secrets: newSecrets, 262 Sast: newSast, 263 Licenses: newLicenses, 264 }, nil 265 } 266 267 func createNewSourceCodeRows(targetResults, sourceResults []formats.SourceCodeRow) []formats.SourceCodeRow { 268 targetSourceCodeVulnerabilitiesKeys := datastructures.MakeSet[string]() 269 for _, row := range targetResults { 270 targetSourceCodeVulnerabilitiesKeys.Add(row.File + row.Snippet) 271 } 272 var addedSourceCodeVulnerabilities []formats.SourceCodeRow 273 for _, row := range sourceResults { 274 if !targetSourceCodeVulnerabilitiesKeys.Exists(row.File + row.Snippet) { 275 addedSourceCodeVulnerabilities = append(addedSourceCodeVulnerabilities, row) 276 } 277 } 278 return addedSourceCodeVulnerabilities 279 } 280 281 // Create vulnerabilities rows. The rows should contain only the new issues added by this PR 282 func createNewVulnerabilitiesRows(targetResults, sourceResults *xrayutils.Results, allowedLicenses []string) (vulnerabilityOrViolationRows []formats.VulnerabilityOrViolationRow, licenseRows []formats.LicenseRow, err error) { 283 targetScanAggregatedResults := aggregateScanResults(targetResults.GetScaScansXrayResults()) 284 sourceScanAggregatedResults := aggregateScanResults(sourceResults.GetScaScansXrayResults()) 285 286 if len(sourceScanAggregatedResults.Violations) > 0 { 287 return getNewViolations(&targetScanAggregatedResults, &sourceScanAggregatedResults, sourceResults) 288 } 289 if len(sourceScanAggregatedResults.Vulnerabilities) > 0 { 290 if vulnerabilityOrViolationRows, err = getNewSecurityVulnerabilities(&targetScanAggregatedResults, &sourceScanAggregatedResults, sourceResults); err != nil { 291 return 292 } 293 } 294 var newLicenses []formats.LicenseRow 295 if newLicenses, err = getNewLicenseRows(&targetScanAggregatedResults, &sourceScanAggregatedResults); err != nil { 296 return 297 } 298 licenseRows = xrayutils.GetViolatedLicenses(allowedLicenses, newLicenses) 299 return 300 } 301 302 func getNewSecurityVulnerabilities(targetScan, sourceScan *services.ScanResponse, auditResults *xrayutils.Results) (newVulnerabilitiesRows []formats.VulnerabilityOrViolationRow, err error) { 303 targetVulnerabilitiesRows, err := xrayutils.PrepareVulnerabilities(targetScan.Vulnerabilities, auditResults, auditResults.IsMultipleProject(), true) 304 if err != nil { 305 return newVulnerabilitiesRows, err 306 } 307 sourceVulnerabilitiesRows, err := xrayutils.PrepareVulnerabilities(sourceScan.Vulnerabilities, auditResults, auditResults.IsMultipleProject(), true) 308 if err != nil { 309 return newVulnerabilitiesRows, err 310 } 311 newVulnerabilitiesRows = getUniqueVulnerabilityOrViolationRows(targetVulnerabilitiesRows, sourceVulnerabilitiesRows) 312 return 313 } 314 315 func getUniqueVulnerabilityOrViolationRows(targetRows, sourceRows []formats.VulnerabilityOrViolationRow) []formats.VulnerabilityOrViolationRow { 316 existingRows := make(map[string]formats.VulnerabilityOrViolationRow) 317 var newRows []formats.VulnerabilityOrViolationRow 318 for _, row := range targetRows { 319 existingRows[utils.GetVulnerabiltiesUniqueID(row)] = row 320 } 321 for _, row := range sourceRows { 322 if _, exists := existingRows[utils.GetVulnerabiltiesUniqueID(row)]; !exists { 323 newRows = append(newRows, row) 324 } 325 } 326 return newRows 327 } 328 329 func getNewViolations(targetScan, sourceScan *services.ScanResponse, auditResults *xrayutils.Results) (newSecurityViolationsRows []formats.VulnerabilityOrViolationRow, newLicenseViolationsRows []formats.LicenseRow, err error) { 330 targetSecurityViolationsRows, targetLicenseViolationsRows, _, err := xrayutils.PrepareViolations(targetScan.Violations, auditResults, auditResults.IsMultipleProject(), true) 331 if err != nil { 332 return 333 } 334 sourceSecurityViolationsRows, sourceLicenseViolationsRows, _, err := xrayutils.PrepareViolations(sourceScan.Violations, auditResults, auditResults.IsMultipleProject(), true) 335 if err != nil { 336 return 337 } 338 newSecurityViolationsRows = getUniqueVulnerabilityOrViolationRows(targetSecurityViolationsRows, sourceSecurityViolationsRows) 339 if len(sourceLicenseViolationsRows) > 0 { 340 newLicenseViolationsRows = getUniqueLicenseRows(targetLicenseViolationsRows, sourceLicenseViolationsRows) 341 } 342 return 343 } 344 345 func getNewLicenseRows(targetScan, sourceScan *services.ScanResponse) (newLicenses []formats.LicenseRow, err error) { 346 targetLicenses, err := xrayutils.PrepareLicenses(targetScan.Licenses) 347 if err != nil { 348 return 349 } 350 sourceLicenses, err := xrayutils.PrepareLicenses(sourceScan.Licenses) 351 if err != nil { 352 return 353 } 354 newLicenses = getUniqueLicenseRows(targetLicenses, sourceLicenses) 355 return 356 } 357 358 func getUniqueLicenseRows(targetRows, sourceRows []formats.LicenseRow) []formats.LicenseRow { 359 existingLicenses := make(map[string]formats.LicenseRow) 360 var newLicenses []formats.LicenseRow 361 for _, row := range targetRows { 362 existingLicenses[getUniqueLicenseKey(row)] = row 363 } 364 for _, row := range sourceRows { 365 if _, exists := existingLicenses[getUniqueLicenseKey(row)]; !exists { 366 newLicenses = append(newLicenses, row) 367 } 368 } 369 return newLicenses 370 } 371 372 func getUniqueLicenseKey(license formats.LicenseRow) string { 373 return license.LicenseKey + license.ImpactedDependencyName + license.ImpactedDependencyType 374 } 375 376 func aggregateScanResults(scanResults []services.ScanResponse) services.ScanResponse { 377 aggregateResults := services.ScanResponse{ 378 Violations: []services.Violation{}, 379 Vulnerabilities: []services.Vulnerability{}, 380 Licenses: []services.License{}, 381 } 382 for _, scanResult := range scanResults { 383 aggregateResults.Violations = append(aggregateResults.Violations, scanResult.Violations...) 384 aggregateResults.Vulnerabilities = append(aggregateResults.Vulnerabilities, scanResult.Vulnerabilities...) 385 aggregateResults.Licenses = append(aggregateResults.Licenses, scanResult.Licenses...) 386 } 387 return aggregateResults 388 }