diff --git a/utils/formats/sarifutils/sarifutils.go b/utils/formats/sarifutils/sarifutils.go index daa8d0489..c5095bc51 100644 --- a/utils/formats/sarifutils/sarifutils.go +++ b/utils/formats/sarifutils/sarifutils.go @@ -25,6 +25,7 @@ const ( TokenValidationStatusSarifPropertyKey = "tokenValidation" TokenValidationMetadataSarifPropertyKey = "metadata" CAUndeterminedReasonSarifPropertyKey = "undetermined_reason" + ScanIdSarifPropertyKey = "scanId" ) // Specific JFrog Sarif Utils diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 1edd30bbe..073ed9eb1 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -80,6 +80,8 @@ type currentTargetRuns struct { iacCurrentRun *sarif.Run sastCurrentRun *sarif.Run maliciousCurrentRun *sarif.Run + // Scan IDs from SCA scan responses + scaScanIds []string } // Parse parameters for the SCA result @@ -150,6 +152,14 @@ func (sc *CmdResultsSarifConverter) flush() { } // Flush Sca if needed if sc.currentTargetConvertedRuns.scaCurrentRun != nil { + // Add scan IDs to run properties if available + if len(sc.currentTargetConvertedRuns.scaScanIds) > 0 { + if sc.currentTargetConvertedRuns.scaCurrentRun.Properties == nil { + sc.currentTargetConvertedRuns.scaCurrentRun.Properties = sarif.NewPropertyBag() + } + // Add scan IDs as a comma-separated string + sc.currentTargetConvertedRuns.scaCurrentRun.Properties.Add(sarifutils.ScanIdSarifPropertyKey, strings.Join(sc.currentTargetConvertedRuns.scaScanIds, ",")) + } sc.current.Runs = append(sc.current.Runs, patchSarifRuns(sc.getVulnerabilitiesConvertParams(utils.ScaScan), sc.currentTargetConvertedRuns.scaCurrentRun)...) } // Flush secrets if needed @@ -312,10 +322,21 @@ func (sc *CmdResultsSarifConverter) parseScaVulnerabilities(target results.ScanT if sc.currentTargetConvertedRuns.scaCurrentRun == nil { sc.currentTargetConvertedRuns.scaCurrentRun = sc.createScaRun(target, len(sc.currentErrors)) } + // Collect scan ID from the response + if scanResponse.ScanId != "" { + sc.currentTargetConvertedRuns.scaScanIds = utils.UniqueUnion(sc.currentTargetConvertedRuns.scaScanIds, scanResponse.ScanId) + } sarifResults, sarifRules, err := PrepareSarifScaVulnerabilities(sc.currentCmdType, target, descriptors, scanResponse.Vulnerabilities, sc.entitledForJas, results.CollectRuns(applicableScan...)...) if err != nil || len(sarifRules) == 0 || len(sarifResults) == 0 { return } + // Add hostedViewerURI to results if we have a base URL and scan ID + if sc.baseJfrogUrl != "" && scanResponse.ScanId != "" { + hostedViewerURI := fmt.Sprintf("%sui/onDemandScanning/%s", sc.baseJfrogUrl, scanResponse.ScanId) + for _, result := range sarifResults { + result.WithHostedViewerURI(hostedViewerURI) + } + } sc.addResultsToCurrentRun(ScaRun, maps.Values(sarifRules), sarifResults...) return } @@ -348,6 +369,14 @@ func (sc *CmdResultsSarifConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, appli if err != nil || len(sarifRules) == 0 || len(sarifResults) == 0 { return } + // Add hostedViewerURI to results if we have a base URL and scan IDs + if sc.baseJfrogUrl != "" && len(sc.currentTargetConvertedRuns.scaScanIds) > 0 { + // Use the first scan ID for the hosted viewer URI + hostedViewerURI := fmt.Sprintf("%sui/onDemandScanning/%s", sc.baseJfrogUrl, sc.currentTargetConvertedRuns.scaScanIds[0]) + for _, result := range sarifResults { + result.WithHostedViewerURI(hostedViewerURI) + } + } sc.addResultsToCurrentRun(ScaRun, maps.Values(sarifRules), sarifResults...) return } diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 90c9eb0b3..9a6606629 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -271,6 +271,8 @@ func (rw *ResultsWriter) printTables() (err error) { if len(rw.tableNotes) > 0 { printMessages(rw.tableNotes) } + // Print scan IDs with links to on-demand scanning + rw.printScanIdsIfAvailable() return } @@ -454,3 +456,38 @@ func WriteJsonResults(results *results.SecurityCommandResults) (resultsPath stri func WriteSarifResultsAsString(report *sarif.Report, escape bool) (sarifStr string, err error) { return utils.GetAsJsonString(report, escape, true) } + +func (rw *ResultsWriter) printScanIdsIfAvailable() { + if rw.platformUrl == "" { + return + } + // Collect all scan IDs from all targets + scanIds := []string{} + for _, target := range rw.commandResults.Targets { + if target.ScaResults != nil && len(target.ScaResults.ScanIds) > 0 { + scanIds = append(scanIds, target.ScaResults.ScanIds...) + } + } + if len(scanIds) == 0 { + return + } + // Remove duplicates by using UniqueUnion with an empty base + scanIds = utils.UniqueUnion([]string{}, scanIds...) + // Print scan IDs with links + log.Output() + icon := "🔗" + if !isPrettyOutputSupported() { + icon = "*" + } + if len(scanIds) == 1 { + onDemandUrl := fmt.Sprintf("%sui/onDemandScanning/%s", rw.platformUrl, scanIds[0]) + log.Output(fmt.Sprintf("%s Scan ID: %s", icon, scanIds[0])) + log.Output(fmt.Sprintf(" View scan results: %s", onDemandUrl)) + } else { + log.Output(fmt.Sprintf("%s Scan IDs:", icon)) + for _, scanId := range scanIds { + onDemandUrl := fmt.Sprintf("%sui/onDemandScanning/%s", rw.platformUrl, scanId) + log.Output(fmt.Sprintf(" - %s: %s", scanId, onDemandUrl)) + } + } +} diff --git a/utils/results/results.go b/utils/results/results.go index 6a73c9ae4..e76f80bca 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -216,6 +216,8 @@ type ScaScanResults struct { // Metadata about the scan Descriptors []string `json:"descriptors,omitempty"` IsMultipleRootProject *bool `json:"is_multiple_root_project,omitempty"` + // ScanIds from the scan responses, can be used to generate links to on-demand scanning + ScanIds []string `json:"scan_ids,omitempty"` // Sca scan results DeprecatedXrayResults []services.ScanResponse `json:"xray_scan,omitempty"` // Sbom (potentially, with enriched components and CVE Vulnerabilities) of the target @@ -890,6 +892,12 @@ func (sr *TargetResults) ScaScanResults(statusCode int, responses ...services.Sc sr.ScaResults = &ScaScanResults{} } sr.ScaResults.DeprecatedXrayResults = append(sr.ScaResults.DeprecatedXrayResults, responses...) + // Collect scan IDs from responses + for _, response := range responses { + if response.ScanId != "" { + sr.ScaResults.ScanIds = utils.UniqueUnion(sr.ScaResults.ScanIds, response.ScanId) + } + } sr.ResultsStatus.UpdateStatus(CmdStepSca, &statusCode) return sr.ScaResults }