diff --git a/commands.go b/commands.go index b5041ea95..169014a4e 100644 --- a/commands.go +++ b/commands.go @@ -21,7 +21,7 @@ import ( type FrogbotCommand interface { // Run the command - Run(config utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) error + Run(config utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker, sarifPath string) error } func GetCommands() []*clitool.Command { @@ -100,7 +100,7 @@ func Exec(command FrogbotCommand, commandName string) (err error) { // Invoke the command interface log.Info(fmt.Sprintf("Running Frogbot %q command", commandName)) - err = command.Run(frogbotDetails.Repositories, frogbotDetails.GitClient, frogbotRepoConnection) + err = command.Run(frogbotDetails.Repositories, frogbotDetails.GitClient, frogbotRepoConnection, frogbotDetails.SarifPath) // Wait for usage reporting to finish. waitForUsageResponse() diff --git a/docs/templates/jfrog-pipelines/pipelines-dotnet.yml b/docs/templates/jfrog-pipelines/pipelines-dotnet.yml index 43f84c715..4b43e0e4d 100644 --- a/docs/templates/jfrog-pipelines/pipelines-dotnet.yml +++ b/docs/templates/jfrog-pipelines/pipelines-dotnet.yml @@ -198,6 +198,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-go.yml b/docs/templates/jfrog-pipelines/pipelines-go.yml index 24d4f2823..ed4f6f5b0 100644 --- a/docs/templates/jfrog-pipelines/pipelines-go.yml +++ b/docs/templates/jfrog-pipelines/pipelines-go.yml @@ -199,6 +199,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-gradle.yml b/docs/templates/jfrog-pipelines/pipelines-gradle.yml index afec547c5..a87a08f73 100644 --- a/docs/templates/jfrog-pipelines/pipelines-gradle.yml +++ b/docs/templates/jfrog-pipelines/pipelines-gradle.yml @@ -203,6 +203,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-maven.yml b/docs/templates/jfrog-pipelines/pipelines-maven.yml index 6dc5250cb..827874e15 100644 --- a/docs/templates/jfrog-pipelines/pipelines-maven.yml +++ b/docs/templates/jfrog-pipelines/pipelines-maven.yml @@ -191,6 +191,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-npm.yml b/docs/templates/jfrog-pipelines/pipelines-npm.yml index 98086d254..1071c93ab 100644 --- a/docs/templates/jfrog-pipelines/pipelines-npm.yml +++ b/docs/templates/jfrog-pipelines/pipelines-npm.yml @@ -202,6 +202,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-pip.yml b/docs/templates/jfrog-pipelines/pipelines-pip.yml index de4c47628..646a2c6a1 100644 --- a/docs/templates/jfrog-pipelines/pipelines-pip.yml +++ b/docs/templates/jfrog-pipelines/pipelines-pip.yml @@ -202,6 +202,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-pipenv.yml b/docs/templates/jfrog-pipelines/pipelines-pipenv.yml index 5f463f7b7..e5e7c5644 100644 --- a/docs/templates/jfrog-pipelines/pipelines-pipenv.yml +++ b/docs/templates/jfrog-pipelines/pipelines-pipenv.yml @@ -195,6 +195,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-poetry.yml b/docs/templates/jfrog-pipelines/pipelines-poetry.yml index fa78264b7..07774c5ec 100644 --- a/docs/templates/jfrog-pipelines/pipelines-poetry.yml +++ b/docs/templates/jfrog-pipelines/pipelines-poetry.yml @@ -195,6 +195,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path generated by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/docs/templates/jfrog-pipelines/pipelines-yarn2.yml b/docs/templates/jfrog-pipelines/pipelines-yarn2.yml index bd8652aeb..0bb11005f 100644 --- a/docs/templates/jfrog-pipelines/pipelines-yarn2.yml +++ b/docs/templates/jfrog-pipelines/pipelines-yarn2.yml @@ -198,6 +198,10 @@ pipelines: # Add a title to pull request comments generated by Frogbot. # JF_PR_COMMENT_TITLE: "" + # [Optional] + # Add a SARIF output path by Frogbot. + # JF_SARIF_OUTPUT_PATH: "" + execution: onExecute: - cd $res_frogbotGitRepo_resourcePath diff --git a/scanpullrequest/scanallpullrequests.go b/scanpullrequest/scanallpullrequests.go index 4a3fbe3d2..c976f3a92 100644 --- a/scanpullrequest/scanallpullrequests.go +++ b/scanpullrequest/scanallpullrequests.go @@ -17,12 +17,12 @@ var errPullRequestScan = "pull request #%d scan in the '%s' repository returned type ScanAllPullRequestsCmd struct { } -func (cmd ScanAllPullRequestsCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) error { +func (cmd ScanAllPullRequestsCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker, sarifPath string) error { for _, config := range configAggregator { log.Info("Scanning all open pull requests for repository:", config.RepoName) log.Info("-----------------------------------------------------------") config.OutputWriter.SetHasInternetConnection(frogbotRepoConnection.IsConnected()) - err := scanAllPullRequests(config, client) + err := scanAllPullRequests(config, client, sarifPath) if err != nil { return err } @@ -35,7 +35,7 @@ func (cmd ScanAllPullRequestsCmd) Run(configAggregator utils.RepoAggregator, cli // b. Find the ones that should be scanned (new PRs or PRs with a 're-scan' comment) // c. Audit the dependencies of the source and the target branches. // d. Compare the vulnerabilities found in source and target branches, and show only the new vulnerabilities added by the pull request. -func scanAllPullRequests(repo utils.Repository, client vcsclient.VcsClient) (err error) { +func scanAllPullRequests(repo utils.Repository, client vcsclient.VcsClient, sarifPath string) (err error) { openPullRequests, err := client.ListOpenPullRequests(context.Background(), repo.RepoOwner, repo.RepoName) if err != nil { return err @@ -50,7 +50,7 @@ func scanAllPullRequests(repo utils.Repository, client vcsclient.VcsClient) (err continue } repo.PullRequestDetails = pr - if e = scanPullRequest(&repo, client); e != nil { + if e = scanPullRequest(&repo, client, sarifPath); e != nil { // If error, write it in errList and continue to the next PR. err = errors.Join(err, fmt.Errorf(errPullRequestScan, int(pr.ID), repo.RepoName, e.Error())) } diff --git a/scanpullrequest/scanallpullrequests_test.go b/scanpullrequest/scanallpullrequests_test.go index cd42f8aa3..81e6dbc95 100644 --- a/scanpullrequest/scanallpullrequests_test.go +++ b/scanpullrequest/scanallpullrequests_test.go @@ -3,6 +3,11 @@ package scanpullrequest import ( "context" "fmt" + "os" + "path/filepath" + "testing" + "time" + "github.com/golang/mock/gomock" biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/frogbot/v2/testdata" @@ -11,9 +16,6 @@ import ( "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" "github.com/stretchr/testify/assert" - "path/filepath" - "testing" - "time" ) var ( @@ -104,6 +106,13 @@ func TestScanAllPullRequestsMultiRepo(t *testing.T) { _, restoreJfrogHomeFunc := utils.CreateTempJfrogHomeWithCallback(t) defer restoreJfrogHomeFunc() + // Create a temporary file for SARIF output + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() + failOnSecurityIssues := false firstRepoParams := utils.Params{ Scan: utils.Scan{ @@ -143,7 +152,7 @@ func TestScanAllPullRequestsMultiRepo(t *testing.T) { var frogbotMessages []string client := getMockClient(t, &frogbotMessages, mockParams...) scanAllPullRequestsCmd := &ScanAllPullRequestsCmd{} - err := scanAllPullRequestsCmd.Run(configAggregator, client, utils.MockHasConnection()) + err = scanAllPullRequestsCmd.Run(configAggregator, client, utils.MockHasConnection(), sarifPath) if assert.NoError(t, err) { assert.Len(t, frogbotMessages, 4) expectedMessage := outputwriter.GetOutputFromFile(t, filepath.Join(allPrIntegrationPath, "test_proj_with_vulnerability_standard.md")) @@ -154,6 +163,8 @@ func TestScanAllPullRequestsMultiRepo(t *testing.T) { assert.Equal(t, expectedMessage, frogbotMessages[2]) expectedMessage = outputwriter.GetPRSummaryContentNoIssues(t, outputwriter.TestSummaryCommentDir, true, false) assert.Equal(t, expectedMessage, frogbotMessages[3]) + _, err = os.Stat(sarifPath) + assert.NoError(t, err, "SARIF file should exist at the specified path") } } @@ -163,6 +174,14 @@ func TestScanAllPullRequests(t *testing.T) { defer restoreEnv() falseVal := false gitParams.Git.GitProvider = vcsutils.BitbucketServer + + // Create a temporary file for SARIF output + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() + params := utils.Params{ Scan: utils.Scan{ FailOnSecurityIssues: &falseVal, @@ -185,13 +204,15 @@ func TestScanAllPullRequests(t *testing.T) { var frogbotMessages []string client := getMockClient(t, &frogbotMessages, MockParams{repoParams.RepoName, repoParams.RepoOwner, "test-proj-with-vulnerability", "test-proj"}) scanAllPullRequestsCmd := &ScanAllPullRequestsCmd{} - err := scanAllPullRequestsCmd.Run(paramsAggregator, client, utils.MockHasConnection()) + err = scanAllPullRequestsCmd.Run(paramsAggregator, client, utils.MockHasConnection(), sarifPath) assert.NoError(t, err) assert.Len(t, frogbotMessages, 2) expectedMessage := outputwriter.GetOutputFromFile(t, filepath.Join(allPrIntegrationPath, "test_proj_with_vulnerability_simplified.md")) assert.Equal(t, expectedMessage, frogbotMessages[0]) expectedMessage = outputwriter.GetPRSummaryContentNoIssues(t, outputwriter.TestSummaryCommentDir, true, true) assert.Equal(t, expectedMessage, frogbotMessages[1]) + _, err = os.Stat(sarifPath) + assert.NoError(t, err, "SARIF file should exist at the specified path") } func getMockClient(t *testing.T, frogbotMessages *[]string, mockParams ...MockParams) *testdata.MockVcsClient { diff --git a/scanpullrequest/scanpullrequest.go b/scanpullrequest/scanpullrequest.go index ecc1634cb..62be0e6cb 100644 --- a/scanpullrequest/scanpullrequest.go +++ b/scanpullrequest/scanpullrequest.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io/ioutil" "os" "github.com/jfrog/frogbot/v2/utils" @@ -28,7 +29,7 @@ type ScanPullRequestCmd struct{} // Run ScanPullRequest method only works for a single repository scan. // Therefore, the first repository config represents the repository on which Frogbot runs, and it is the only one that matters. -func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) (err error) { +func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker, sarifPath string) (err error) { if err = utils.ValidateSingleRepoConfiguration(&configAggregator); err != nil { return } @@ -42,7 +43,7 @@ func (cmd *ScanPullRequestCmd) Run(configAggregator utils.RepoAggregator, client if repoConfig.PullRequestDetails, err = client.GetPullRequestByID(context.Background(), repoConfig.RepoOwner, repoConfig.RepoName, int(repoConfig.PullRequestDetails.ID)); err != nil { return } - return scanPullRequest(repoConfig, client) + return scanPullRequest(repoConfig, client, sarifPath) } // Verify that the 'frogbot' GitHub environment was properly configured on the repository @@ -81,7 +82,7 @@ func verifyGitHubFrogbotEnvironment(client vcsclient.VcsClient, repoConfig *util // a. Audit the dependencies of the source and the target branches. // b. Compare the vulnerabilities found in source and target branches, and show only the new vulnerabilities added by the pull request. // Otherwise, only the source branch is scanned and all found vulnerabilities are being displayed. -func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err error) { +func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient, sarifPath string) (err error) { pullRequestDetails := repo.PullRequestDetails log.Info(fmt.Sprintf("Scanning Pull Request #%d (from source branch: <%s/%s/%s> to target branch: <%s/%s/%s>)", pullRequestDetails.ID, @@ -95,7 +96,7 @@ func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err er }() // Audit PR code - issues, err := auditPullRequest(repo, client, analyticsService) + issues, err := auditPullRequest(repo, client, analyticsService, sarifPath) if err != nil { return } @@ -128,7 +129,7 @@ func toFailTaskStatus(repo *utils.Repository, issues *utils.IssuesCollection) bo } // Downloads Pull Requests branches code and audits them -func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, analyticsService *xsc.AnalyticsMetricsService) (issuesCollection *utils.IssuesCollection, err error) { +func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, analyticsService *xsc.AnalyticsMetricsService, sarifPath string) (issuesCollection *utils.IssuesCollection, err error) { scanDetails := utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Git). SetXrayGraphScanParams(repoConfig.Watches, repoConfig.JFrogProjectKey, len(repoConfig.AllowedLicenses) > 0). SetFixableOnly(repoConfig.FixableOnly). @@ -148,7 +149,7 @@ func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, for i := range repoConfig.Projects { scanDetails.SetProject(&repoConfig.Projects[i]) var projectIssues *utils.IssuesCollection - if projectIssues, err = auditPullRequestInProject(repoConfig, scanDetails); err != nil { + if projectIssues, err = auditPullRequestInProject(repoConfig, scanDetails, sarifPath); err != nil { return } issuesCollection.Append(projectIssues) @@ -159,7 +160,29 @@ func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient, return } -func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils.ScanDetails) (auditIssues *utils.IssuesCollection, err error) { +// Generate and write SARIF report to sarifPath +func generateAndWriteSarifReport(sourceResults *securityutils.Results, repoConfig *utils.Repository, sarifPath string) error { + // Generate SARIF report + log.Info("Generating SARIF report...") + sarifReportStr, err := utils.GenerateFrogbotSarifReport(sourceResults, sourceResults.IsMultipleProject(), repoConfig.AllowedLicenses) + if err != nil { + log.Error("Error generating SARIF report: ", err) + return err + } + + // Write the SARIF report to a file + log.Info("Writing SARIF report to file: ", sarifPath) + err = ioutil.WriteFile(sarifPath, []byte(sarifReportStr), 0644) + if err != nil { + log.Error("Error writing SARIF report to file: ", err) + return err + } + + log.Info("SARIF report successfully written to file: ", sarifPath) + return nil +} + +func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils.ScanDetails, sarifPath string) (auditIssues *utils.IssuesCollection, err error) { // Download source branch sourcePullRequestInfo := scanDetails.PullRequestDetails.Source sourceBranchWd, cleanupSource, err := utils.DownloadRepoToTempDir(scanDetails.Client(), sourcePullRequestInfo.Owner, sourcePullRequestInfo.Repository, sourcePullRequestInfo.Name) @@ -179,6 +202,13 @@ func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils. return } + // If sarifPath is provided, generate and write SARIF report + if sarifPath != "" { + if err = generateAndWriteSarifReport(sourceResults, repoConfig, sarifPath); err != nil { + return + } + } + // Set JAS output flags sourceScanResults := sourceResults.ExtendedScanResults repoConfig.OutputWriter.SetJasOutputFlags(sourceScanResults.EntitledForJas, len(sourceScanResults.ApplicabilityScanResults) > 0) diff --git a/scanpullrequest/scanpullrequest_test.go b/scanpullrequest/scanpullrequest_test.go index af4e3895c..3c67cc243 100644 --- a/scanpullrequest/scanpullrequest_test.go +++ b/scanpullrequest/scanpullrequest_test.go @@ -604,6 +604,13 @@ func testScanPullRequest(t *testing.T, configPath, projectName string, failOnSec testDir, cleanUp := utils.CopyTestdataProjectsToTemp(t, "scanpullrequest") defer cleanUp() + // Create a temporary file for SARIF output + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() + // Renames test git folder to .git currentDir := filepath.Join(testDir, projectName) restoreDir, err := utils.Chdir(currentDir) @@ -615,12 +622,14 @@ func testScanPullRequest(t *testing.T, configPath, projectName string, failOnSec // Run "frogbot scan pull request" var scanPullRequest ScanPullRequestCmd - err = scanPullRequest.Run(configAggregator, client, utils.MockHasConnection()) + err = scanPullRequest.Run(configAggregator, client, utils.MockHasConnection(), sarifPath) if failOnSecurityIssues { assert.EqualErrorf(t, err, SecurityIssueFoundErr, "Error should be: %v, got: %v", SecurityIssueFoundErr, err) } else { assert.NoError(t, err) } + _, err = os.Stat(sarifPath) + assert.NoError(t, err, "SARIF file should exist at the specified path") // Check env sanitize err = utils.SanitizeEnv() diff --git a/scanrepository/scanmultiplerepositories.go b/scanrepository/scanmultiplerepositories.go index 901f06ca6..46ef09a78 100644 --- a/scanrepository/scanmultiplerepositories.go +++ b/scanrepository/scanmultiplerepositories.go @@ -2,6 +2,7 @@ package scanrepository import ( "errors" + "github.com/jfrog/frogbot/v2/utils" "github.com/jfrog/froggit-go/vcsclient" ) @@ -13,11 +14,11 @@ type ScanMultipleRepositories struct { dryRunRepoPath string } -func (saf *ScanMultipleRepositories) Run(repoAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) (err error) { +func (saf *ScanMultipleRepositories) Run(repoAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker, sarifPath string) (err error) { scanRepositoryCmd := &ScanRepositoryCmd{dryRun: saf.dryRun, dryRunRepoPath: saf.dryRunRepoPath, baseWd: saf.dryRunRepoPath} for repoNum := range repoAggregator { repoAggregator[repoNum].OutputWriter.SetHasInternetConnection(frogbotRepoConnection.IsConnected()) - if e := scanRepositoryCmd.scanAndFixRepository(&repoAggregator[repoNum], client); e != nil { + if e := scanRepositoryCmd.scanAndFixRepository(&repoAggregator[repoNum], client, sarifPath); e != nil { err = errors.Join(err, e) } } diff --git a/scanrepository/scanmultiplerepositories_test.go b/scanrepository/scanmultiplerepositories_test.go index bc8e2ff6f..e6fdc4283 100644 --- a/scanrepository/scanmultiplerepositories_test.go +++ b/scanrepository/scanmultiplerepositories_test.go @@ -4,6 +4,13 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/protocol/packp" "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" @@ -11,12 +18,6 @@ import ( "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" ) var testScanMultipleRepositoriesConfigPath = filepath.Join("..", "testdata", "config", "frogbot-config-scan-multiple-repositories.yml") @@ -38,6 +39,13 @@ func TestScanAndFixRepos(t *testing.T) { client, err := vcsclient.NewClientBuilder(vcsutils.GitHub).ApiEndpoint(server.URL).Token("123456").Build() assert.NoError(t, err) + // Create a temporary file for SARIF output + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() + gitTestParams := utils.Git{ GitProvider: vcsutils.GitHub, RepoOwner: "jfrog", @@ -61,7 +69,7 @@ func TestScanAndFixRepos(t *testing.T) { assert.NoError(t, err) var cmd = ScanMultipleRepositories{dryRun: true, dryRunRepoPath: testDir} - assert.NoError(t, cmd.Run(configAggregator, client, utils.MockHasConnection())) + assert.NoError(t, cmd.Run(configAggregator, client, utils.MockHasConnection(), sarifPath)) } func createScanRepoGitHubHandler(t *testing.T, port *string, response interface{}, projectNames ...string) http.HandlerFunc { diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index 861774ddc..cf0ae24d9 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -49,30 +49,30 @@ type ScanRepositoryCmd struct { analyticsService *xsc.AnalyticsMetricsService } -func (cfp *ScanRepositoryCmd) Run(repoAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker) (err error) { +func (cfp *ScanRepositoryCmd) Run(repoAggregator utils.RepoAggregator, client vcsclient.VcsClient, frogbotRepoConnection *utils.UrlAccessChecker, sarifPath string) (err error) { if err = utils.ValidateSingleRepoConfiguration(&repoAggregator); err != nil { return err } repository := repoAggregator[0] repository.OutputWriter.SetHasInternetConnection(frogbotRepoConnection.IsConnected()) - return cfp.scanAndFixRepository(&repository, client) + return cfp.scanAndFixRepository(&repository, client, sarifPath) } -func (cfp *ScanRepositoryCmd) scanAndFixRepository(repository *utils.Repository, client vcsclient.VcsClient) (err error) { +func (cfp *ScanRepositoryCmd) scanAndFixRepository(repository *utils.Repository, client vcsclient.VcsClient, sarifPath string) (err error) { if err = cfp.setCommandPrerequisites(repository, client); err != nil { return } for _, branch := range repository.Branches { cfp.scanDetails.SetBaseBranch(branch) cfp.scanDetails.SetXscGitInfoContext(branch, repository.Project, client) - if err = cfp.scanAndFixBranch(repository); err != nil { + if err = cfp.scanAndFixBranch(repository, sarifPath); err != nil { return } } return } -func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (err error) { +func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository, sarifPath string) (err error) { cfp.analyticsService = utils.AddAnalyticsGeneralEvent(cfp.scanDetails.XscGitInfoContext, cfp.scanDetails.ServerDetails, analyticsScanRepositoryScanType) defer func() { cfp.analyticsService.UpdateAndSendXscAnalyticsGeneralEventFinalize(err) @@ -104,7 +104,7 @@ func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (er for i := range repository.Projects { cfp.scanDetails.Project = &repository.Projects[i] cfp.projectTech = []techutils.Technology{} - if err = cfp.scanAndFixProject(repository); err != nil { + if err = cfp.scanAndFixProject(repository, sarifPath); err != nil { return } } @@ -142,11 +142,12 @@ func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Reposito return } -func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) error { +func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository, sarifPath string) error { var fixNeeded bool // A map that contains the full project paths as a keys // The value is a map of vulnerable package names -> the scanDetails of the vulnerable packages. // That means we have a map of all the vulnerabilities that were found in a specific folder, along with their full scanDetails. + log.Info("sarifPath is %s", sarifPath) vulnerabilitiesByPathMap := make(map[string]map[string]*utils.VulnerabilityDetails) projectFullPathWorkingDirs := utils.GetFullPathWorkingDirs(cfp.scanDetails.Project.WorkingDirs, cfp.baseWd) for _, fullPathWd := range projectFullPathWorkingDirs { @@ -164,6 +165,11 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) er if err = utils.UploadSarifResultsToGithubSecurityTab(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client()); err != nil { log.Warn(err) } + } else if sarifPath != "" { + // Uploads SARIF result to gitlab Dashborad + if err = utils.UploadSarifResults(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client(), sarifPath); err != nil { + log.Warn(err) + } } if repository.DetectionOnly { continue diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index 01359d24e..10eef48ee 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -177,13 +177,20 @@ func TestScanRepositoryCmd_Run(t *testing.T) { // Manual set of "JF_GIT_BASE_BRANCH" gitTestParams.Branches = []string{"master"} } + // Create a temporary file for SARIF output in this test case + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() utils.CreateDotGitWithCommit(t, testDir, port, test.testName) configAggregator, err := utils.BuildRepoAggregator(client, configData, &gitTestParams, &serverParams, utils.ScanRepository) assert.NoError(t, err) // Run var cmd = ScanRepositoryCmd{dryRun: true, dryRunRepoPath: testDir} - err = cmd.Run(configAggregator, client, utils.MockHasConnection()) + err = cmd.Run(configAggregator, client, utils.MockHasConnection(), sarifPath) defer func() { assert.NoError(t, os.Chdir(baseDir)) }() @@ -292,6 +299,13 @@ pr body APIEndpoint: server.URL, }, RepoName: test.testName, } + // Create a temporary file for SARIF output in this test case + tmpFile, err := os.CreateTemp("", "sarifOutputPath-*.sarif") + assert.NoError(t, err, "Temporary file for SARIF path should be created successfully") + defer os.Remove(tmpFile.Name()) // Clean up the file at the end of the test + + // Use the temporary file's path as sarifPath + sarifPath := tmpFile.Name() utils.CreateDotGitWithCommit(t, testDir, port, test.testName) client, err := vcsclient.NewClientBuilder(vcsutils.GitHub).ApiEndpoint(server.URL).Token("123456").Build() @@ -303,7 +317,7 @@ pr body assert.NoError(t, err) // Run var cmd = ScanRepositoryCmd{dryRun: true, dryRunRepoPath: testDir} - err = cmd.Run(configAggregator, client, utils.MockHasConnection()) + err = cmd.Run(configAggregator, client, utils.MockHasConnection(), sarifPath) defer func() { assert.NoError(t, os.Chdir(baseDir)) }() diff --git a/utils/consts.go b/utils/consts.go index 4e1e005f9..f78d91aa6 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -102,6 +102,9 @@ const ( // Frogbot Git author details showed in commits frogbotAuthorName = "JFrog-Frogbot" frogbotAuthorEmail = "eco-system+frogbot@jfrog.com" + + // SARIF file related environment variables + SarifOutputPathEnv = "JF_SARIF_OUTPUT_PATH" ) type UnsupportedErrorType string diff --git a/utils/params.go b/utils/params.go index 8e5295731..6e0e95b6a 100644 --- a/utils/params.go +++ b/utils/params.go @@ -43,6 +43,7 @@ type FrogbotDetails struct { ServerDetails *coreconfig.ServerDetails GitClient vcsclient.VcsClient ReleasesRepo string + SarifPath string } type RepoAggregator []Repository @@ -390,11 +391,10 @@ func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err if err != nil { return } - + sarifPath := getTrimmedEnv(SarifOutputPathEnv) defer func() { err = errors.Join(err, SanitizeEnv()) }() - // Build a version control client for REST API requests client, err := vcsclient. NewClientBuilder(gitParamsFromEnv.GitProvider). @@ -413,6 +413,8 @@ func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err return } + frogbotDetails = &FrogbotDetails{Repositories: configAggregator, GitClient: client, ServerDetails: jfrogServer, ReleasesRepo: os.Getenv(jfrogReleasesRepoEnv), SarifPath: sarifPath} +======= // We apply the configProfile to all received repositories. This loop must be deleted when we will no longer accept multiple repositories in a single scan for i := range configAggregator { configAggregator[i].Scan.ConfigProfile = configProfile diff --git a/utils/utils.go b/utils/utils.go index d26e8e04b..bf6980bfd 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "encoding/hex" + "encoding/json" "errors" "fmt" "net/http" @@ -233,6 +234,82 @@ func UploadSarifResultsToGithubSecurityTab(scanResults *xrayutils.Results, repo return nil } +func UploadSarifResults(scanResults *xrayutils.Results, repo *Repository, branch string, client vcsclient.VcsClient, sarifPath string) error { + + // Generate SARIF report + sarifReport, err := xrayutils.GenereateSarifReportFromResults(scanResults, scanResults.IsMultipleProject(), false, repo.AllowedLicenses) + if err != nil { + return fmt.Errorf("failed to generate SARIF report: %w", err) + } + + // Serialize the SARIF report to JSON + reportBytes, err := json.MarshalIndent(sarifReport, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal SARIF report to JSON: %w", err) + } + + //// Open or create the SARIF output file + file, err := os.Create(sarifPath) + if err != nil { + return fmt.Errorf("failed to create SARIF file at %s: %w", sarifPath, err) + } + defer file.Close() // Ensure the file is closed even if an error occurs + + // Write the SARIF report to the file + _, err = file.Write(reportBytes) + + if err != nil { + return fmt.Errorf("failed to write SARIF file: %w", err) + } + + log.Info("SARIF report has been written to %s", sarifPath) + + return nil +} + +func prepareRunsForGithubReport(runs []*sarif.Run) []*sarif.Run { + for _, run := range runs { + for _, rule := range run.Tool.Driver.Rules { + // Github security tab can display markdown content on Help attribute and not description + if rule.Help == nil && rule.FullDescription != nil { + rule.Help = rule.FullDescription + } + } + // Github security tab can't accept results without locations, remove them + results := []*sarif.Result{} + for _, result := range run.Results { + if len(result.Locations) == 0 { + continue + } + results = append(results, result) + } + run.Results = results + } + convertToRelativePath(runs) + // If we upload to Github security tab multiple runs, it will only display the last run as active issues. + // Combine all runs into one run with multiple invocations, so the Github security tab will display all the results as not resolved. + combined := sarif.NewRunWithInformationURI(sarifToolName, outputwriter.FrogbotRepoUrl) + sarifutils.AggregateMultipleRunsIntoSingle(runs, combined) + return []*sarif.Run{combined} +} + +func convertToRelativePath(runs []*sarif.Run) { + for _, run := range runs { + for _, result := range run.Results { + for _, location := range result.Locations { + sarifutils.SetLocationFileName(location, sarifutils.GetRelativeLocationFileName(location, run.Invocations)) + } + for _, flows := range result.CodeFlows { + for _, flow := range flows.ThreadFlows { + for _, location := range flow.Locations { + sarifutils.SetLocationFileName(location.Location, sarifutils.GetRelativeLocationFileName(location.Location, run.Invocations)) + } + } + } + } + } +} + func GenerateFrogbotSarifReport(extendedResults *xrayutils.Results, isMultipleRoots bool, allowedLicenses []string) (string, error) { sarifReport, err := xrayutils.GenerateSarifReportFromResults(extendedResults, isMultipleRoots, false, allowedLicenses, xrayutils.GetAllSupportedScans()) if err != nil {