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  }