diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 887b600f8..a452c845d 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -31,6 +31,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/results/output" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-cli-security/utils/xray/artifact" "golang.org/x/exp/slices" @@ -986,50 +987,71 @@ func processScanResults(params *AuditParams, cmdResults *results.SecurityCommand uploadPath := "" if params.uploadCdxResults { log.Debug("Finished scanning. Uploading scan results to Artifactory") - if params.rtResultRepository == "" { + if params.GetRtResultRepositoryWithProjectKey() == "" { return cmdResults.AddGeneralError(errors.New("results repository was not provided, can't upload scan results to Artifactory"), false) } - rtResultRepository := params.rtResultRepository - if params.resultsContext.ProjectKey != "" { - rtResultRepository = fmt.Sprintf("%s-%s", params.resultsContext.ProjectKey, rtResultRepository) - } if params.Progress() != nil { params.Progress().SetHeadlineMsg("Uploading scan results to platform") } - uploadPath, err = uploadCdxResults(params, cmdResults, rtResultRepository) + uploadPath, err = uploadCdxResults(params, cmdResults) if err != nil { return cmdResults.AddGeneralError(fmt.Errorf("failed to upload scan results to Artifactory: %s", err.Error()), false) } + if uiRoute, err := getScanResultsUiRoute(params, uploadPath); err != nil { + log.Warn(fmt.Sprintf("failed to get scan results UI route: %s", err.Error())) + } else if uiRoute != "" { + cmdResults.SetResultsPlatformUrl(uiRoute) + } } // Violations fetching if cmdResults.HasViolationContext() { if params.Progress() != nil { params.Progress().SetHeadlineMsg("Fetching violations") } - rtResultRepository := params.rtResultRepository - if rtResultRepository != "" && params.resultsContext.ProjectKey != "" { - rtResultRepository = fmt.Sprintf("%s-%s", params.resultsContext.ProjectKey, rtResultRepository) - } - if err = fetchViolations(uploadPath, cmdResults, params, rtResultRepository); err != nil { + if err = fetchViolations(uploadPath, cmdResults, params); err != nil { cmdResults.AddGeneralError(fmt.Errorf("failed to get violations: %s", err.Error()), cmdResults.AllowPartialResults) } } return cmdResults } -func uploadCdxResults(auditParams *AuditParams, cmdResults *results.SecurityCommandResults, rtResultRepository string) (uploadPath string, err error) { +func uploadCdxResults(auditParams *AuditParams, cmdResults *results.SecurityCommandResults) (uploadPath string, err error) { serverDetails, err := auditParams.ServerDetails() if err != nil { err = fmt.Errorf("failed to get server details: %s", err.Error()) return } - if uploadPath, err = output.UploadCommandResults(serverDetails, rtResultRepository, cmdResults); err != nil { + if uploadPath, err = output.UploadCommandResults(serverDetails, auditParams.GetRtResultRepositoryWithProjectKey(), cmdResults); err != nil { err = fmt.Errorf("failed to upload scan results to Artifactory: %s", err.Error()) } return } -func fetchViolations(uploadPath string, cmdResults *results.SecurityCommandResults, auditParams *AuditParams, rtResultRepository string) (err error) { +func getScanResultsUiRoute(auditParams *AuditParams, uploadPath string) (string, error) { + if auditParams.GitContext() == nil { + return "", nil + } + serverDetails, err := auditParams.ServerDetails() + if err != nil { + return "", fmt.Errorf("failed to get server details: %s", err.Error()) + } + xrayManager, err := xrayutils.CreateXrayServiceManager(serverDetails, xrayutils.WithScopedProjectKey(auditParams.resultsContext.ProjectKey)) + if err != nil { + return "", fmt.Errorf("failed to create Xray service manager: %s", err.Error()) + } + if err = artifact.WaitForArtifactScanStatus(xrayManager, auditParams.GetRtResultRepositoryWithProjectKey(), uploadPath, artifact.ScanStarted()); err != nil { + return "", fmt.Errorf("failed to wait for artifact scan status: %s", err.Error()) + } + return xsc.GetScanResultsUiRoute(&xsc.ScanResultsUiRouteParams{ + XrayVersion: auditParams.GetXrayVersion(), + ServerDetails: serverDetails, + ProjectKey: auditParams.resultsContext.ProjectKey, + GitContext: auditParams.GitContext(), + ScanResultArtifactPath: fmt.Sprintf("%s/%s", auditParams.GetRtResultRepositoryWithProjectKey(), uploadPath), + }) +} + +func fetchViolations(uploadPath string, cmdResults *results.SecurityCommandResults, auditParams *AuditParams) (err error) { serverDetails, err := auditParams.ServerDetails() if err != nil { return fmt.Errorf("failed to get server details: %s", err.Error()) @@ -1038,7 +1060,7 @@ func fetchViolations(uploadPath string, cmdResults *results.SecurityCommandResul local.WithAllowedLicenses(auditParams.allowedLicenses), enforcer.WithServerDetails(serverDetails), enforcer.WithProjectKey(auditParams.resultsContext.ProjectKey), - enforcer.WithArtifactParams(rtResultRepository, uploadPath), + enforcer.WithArtifactParams(auditParams.GetRtResultRepositoryWithProjectKey(), uploadPath), enforcer.WithWatches(auditParams.resultsContext.Watches), enforcer.WithResultsOutputDir(auditParams.scanResultsOutputDir), ) diff --git a/commands/audit/auditparams.go b/commands/audit/auditparams.go index 99b7a18aa..5cd8a8c73 100644 --- a/commands/audit/auditparams.go +++ b/commands/audit/auditparams.go @@ -1,6 +1,7 @@ package audit import ( + "fmt" "time" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" @@ -361,3 +362,10 @@ func (params *AuditParams) SetIncludeSbom(include bool) *AuditParams { params.resultsContext.IncludeSbom = include return params } + +func (params *AuditParams) GetRtResultRepositoryWithProjectKey() string { + if params.rtResultRepository != "" && params.resultsContext.ProjectKey != "" { + return fmt.Sprintf("%s-%s", params.resultsContext.ProjectKey, params.rtResultRepository) + } + return params.rtResultRepository +} diff --git a/go.mod b/go.mod index 50cc60101..9801af2af 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/jfrog/jfrog-apps-config v1.0.1 github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e - github.com/jfrog/jfrog-client-go v1.55.1-0.20260728135425-4a7f34efb305 + github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00 github.com/magiconair/properties v1.18.11 github.com/owenrumney/go-sarif/v3 v3.2.3 github.com/package-url/packageurl-go v0.1.3 diff --git a/go.sum b/go.sum index ff78e6031..ea75a7eb3 100644 --- a/go.sum +++ b/go.sum @@ -173,8 +173,8 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 h1:D github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e h1:K0IK3w5a5h6SIi9yoOJ6a7DL+kuFjs5acypOxKyT2OM= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260728135425-4a7f34efb305 h1:D86yACKH94D9sJ0XcBK4ioOVSHDec667y22rXondYN0= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260728135425-4a7f34efb305/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00 h1:QekOqpZ4c34Xb/eX9UCM1JMGipGoW/ZpkbwsWrx/kAQ= +github.com/jfrog/jfrog-client-go v1.55.1-0.20260729072925-e1104f6b9e00/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= diff --git a/utils/results/output/reportartifact.go b/utils/results/output/reportartifact.go index e5a499f16..471c37f24 100644 --- a/utils/results/output/reportartifact.go +++ b/utils/results/output/reportartifact.go @@ -49,12 +49,11 @@ func UploadCommandResults(serverDetails *config.ServerDetails, rtResultRepositor } func GetCommandResultsPlatformUrlMessage(cmdResults *results.SecurityCommandResults, pretty bool) string { - isGitContext := cmdResults.CmdType == utils.SourceCode && cmdResults.GitContext != nil - uploadMsg := upload.GetScanResultsPlatformUrlMessage(isGitContext) + uploadMsg := upload.GetScanResultsPlatformUrlMessage(cmdResults.CmdType == utils.SourceCode && cmdResults.GitContext != nil) if pretty { uploadMsg = coreutils.PrintTitle(uploadMsg) } - if isGitContext { + if cmdResults.ResultsPlatformUrl == "" { return uploadMsg } link := cmdResults.ResultsPlatformUrl diff --git a/utils/xray/artifact/artifactstatus.go b/utils/xray/artifact/artifactstatus.go index b9b084a97..1010ed54d 100644 --- a/utils/xray/artifact/artifactstatus.go +++ b/utils/xray/artifact/artifactstatus.go @@ -33,8 +33,9 @@ func GetArtifactScanStatus(xrayManager *xray.XrayServicesManager, repo, path str } type ScanCompleteParams struct { - Overall bool - Steps []XrayScanStep + Overall bool + WaitForScanStarted bool + Steps []XrayScanStep } type ScanCompleteOption func(params *ScanCompleteParams) @@ -45,6 +46,12 @@ func OverallCompletion() ScanCompleteOption { } } +func ScanStarted() ScanCompleteOption { + return func(params *ScanCompleteParams) { + params.WaitForScanStarted = true + } +} + func Steps(steps ...XrayScanStep) ScanCompleteOption { return func(params *ScanCompleteParams) { params.Steps = steps @@ -63,9 +70,13 @@ func NewScanCompleteParams(options ...ScanCompleteOption) *ScanCompleteParams { func WaitForArtifactScanStatus(xrayManager *xray.XrayServicesManager, repo, path string, options ...ScanCompleteOption) error { params := NewScanCompleteParams(options...) if !params.Overall && len(params.Steps) == 0 { - return fmt.Errorf("no scan completion criteria were provided") + if !params.WaitForScanStarted { + return fmt.Errorf("no scan completion criteria were provided") + } + log.Debug("Waiting for artifact scan to start.") + } else { + log.Debug(fmt.Sprintf("Waiting for artifact scan completion. Overall: %t, Steps: %v", params.Overall, params.Steps)) } - log.Debug(fmt.Sprintf("Waiting for artifact scan completion. Overall: %t, Steps: %v", params.Overall, params.Steps)) pollingExecutor := &httputils.PollingExecutor{ PollingInterval: ArtifactStatusFetchingIntervalNano, Timeout: ArtifactStatusFetchTimeoutNano, @@ -94,7 +105,14 @@ func WaitForArtifactScanStatus(xrayManager *xray.XrayServicesManager, repo, path } return } - log.Debug(fmt.Sprintf("Artifact scan completed the requested steps. [%s]", strings.Join(statusMapToString(getStatusMap(status.Details, params.Steps, false)), ", "))) + switch { + case !params.Overall && len(params.Steps) == 0: + log.Debug(fmt.Sprintf("Artifact scan started. (%s)", status.Overall.Status)) + case params.Overall: + log.Debug(fmt.Sprintf("Artifact scan completed. (%s)", status.Overall.Status)) + default: + log.Debug(fmt.Sprintf("Artifact scan completed the requested steps. [%s]", strings.Join(statusMapToString(getStatusMap(status.Details, params.Steps, false)), ", "))) + } // We don't need to return any response body, as we don't use it. // We just need to stop the polling executor. shouldStop = true diff --git a/utils/xsc/scanresultsuiroute.go b/utils/xsc/scanresultsuiroute.go new file mode 100644 index 000000000..025816936 --- /dev/null +++ b/utils/xsc/scanresultsuiroute.go @@ -0,0 +1,47 @@ +package xsc + +import ( + "fmt" + + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + + xrayutils "github.com/jfrog/jfrog-cli-security/utils/xray" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/log" + xscServices "github.com/jfrog/jfrog-client-go/xsc/services" +) + +type ScanResultsUiRouteParams struct { + XrayVersion string + ServerDetails *config.ServerDetails + ProjectKey string + GitContext *xscServices.XscGitInfoContext + ScanResultArtifactPath string +} + +func GetScanResultsUiRoute(params *ScanResultsUiRouteParams) (string, error) { + if err := clientutils.ValidateMinimumVersion(clientutils.Xray, params.XrayVersion, xscServices.GetUIRouteAPIMinXrayVersion); err != nil { + log.Debug(fmt.Sprintf("Getting scan results UI route is not supported on your current Xray version. %s", err.Error())) + return "", nil + } + if params.GitContext == nil || params.ServerDetails == nil { + log.Verbose("No git context or server details provided, skipping getting scan results UI route") + return "", nil + } + + xscService, err := CreateXscService(params.ServerDetails, xrayutils.WithScopedProjectKey(params.ProjectKey)) + if err != nil { + return "", fmt.Errorf("failed to create XSC service: %w", err) + } + + resp, err := xscService.GetScanResultsUIRoute(params.GitContext) + if err != nil { + return "", fmt.Errorf("failed to get scan results UI route: %w", err) + } + + if params.ScanResultArtifactPath != "" && resp.Path != params.ScanResultArtifactPath { + return "", fmt.Errorf("scan result artifact path '%s' does not match the expected path '%s'", params.ScanResultArtifactPath, resp.Path) + } + + return resp.Url, nil +}