Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/docs/xray/downloadstatus/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Notes:
- UNKNOWN means the violation scan has not finished, failed, or is partial, or the indexed checksum does not match the current file. A watch that blocks unscanned artifacts can still block the download while the status is UNKNOWN.
- A blocking violation always reports BLOCKED, even if the scan itself failed or is partial.
- A scan status of "not supported" reports ALLOWED, even though a watch that blocks unscanned artifacts could still block the real download.
- The repository can be local, remote or virtual. A remote repo is resolved to its cache repo, and a virtual repo to the member repo that serves the artifact. The resolved repo is reported next to the one you passed.
- For a multi-arch docker image, each platform manifest present in the repository is checked and listed. If any of them is blocked, the image is reported as BLOCKED, and the violations show which platform they belong to.
- A docker pull reference ([host/]<repo>/<image>:<tag>, or @sha256:<digest>) is resolved to the manifest Artifactory stores for that image.

Common patterns:
Expand Down
128 changes: 79 additions & 49 deletions commands/xray/downloadstatus/downloadstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,35 +233,76 @@ func (cmd *DownloadStatusCommand) Run() (err error) {
}

func (cmd *DownloadStatusCommand) FetchResult() (*Result, error) {
path, sha256, err := cmd.resolveArtifact()
artifactoryManager, err := artifactoryUtils.CreateServiceManager(cmd.serverDetails, -1, 0, false)
if err != nil {
return nil, err
}
located, err := locateArtifact(artifactoryManager, cmd.repo, cmd.pathCandidates)
if err != nil {
return nil, err
}
log.Debug(fmt.Sprintf("Resolved artifact %s/%s sha256 %s", cmd.repo, path, sha256))
repo, path := located.repo, located.path
log.Debug(fmt.Sprintf("Resolved artifact %s/%s sha256 %s", repo, path, located.sha256))

xrayManager, err := corexray.CreateXrayServiceManager(cmd.serverDetails, corexray.WithScopedProjectKey(cmd.project))
if err != nil {
return nil, err
}
result, err := cmd.evaluateArtifact(xrayManager, repo, path, located.sha256)
if err != nil {
return nil, err
}

if isManifestListPath(path) {
platforms, listErr := cmd.evaluatePlatformManifests(artifactoryManager, xrayManager, repo, path)
if listErr != nil {
log.Debug(fmt.Sprintf("Could not expand manifest list %s/%s: %s", repo, path, listErr.Error()))
}
mergePlatformResults(result, platforms)
}

if repo != cmd.repo {
result.RequestedRepo = cmd.repo
}
log.Debug(fmt.Sprintf("Download status for %s/%s is %s (%s)", repo, path, result.DownloadStatus, result.StatusReason))
return result, nil
}

func (cmd *DownloadStatusCommand) evaluatePlatformManifests(source manifestSource, xrayManager *xray.XrayServicesManager, repo, listPath string) ([]platformResult, error) {
manifests, err := listPlatformManifests(source, repo, listPath)
if err != nil {
return nil, err
}
var platforms []platformResult
for _, manifest := range manifests {
platformResultValue, err := cmd.evaluateArtifact(xrayManager, repo, manifest.path, manifest.sha256)
if err != nil {
return platforms, err
}
platforms = append(platforms, platformResult{platform: manifest.platform, result: platformResultValue})
}
return platforms, nil
}

scanStatus, err := xrayManager.GetArtifactStatus(cmd.repo, path)
func (cmd *DownloadStatusCommand) evaluateArtifact(xrayManager *xray.XrayServicesManager, repo, path, sha256 string) (*Result, error) {
scanStatus, err := xrayManager.GetArtifactStatus(repo, path)
if err != nil {
return nil, err
}
if scanStatus != nil {
log.Debug(fmt.Sprintf("Violation scan status for %s/%s is %s", cmd.repo, path, scanStatus.Details.Violations.Status))
log.Debug(fmt.Sprintf("Violation scan status for %s/%s is %s", repo, path, scanStatus.Details.Violations.Status))
}

violationsResponse, err := xrayManager.GetViolations(
xrayUtils.NewViolationsRequest().
IncludeDetails(true).
FilterByArtifacts(xrayUtils.ArtifactResourceFilter{Repository: cmd.repo, Path: path}),
FilterByArtifacts(xrayUtils.ArtifactResourceFilter{Repository: repo, Path: path}),
)
if err != nil {
return nil, err
}

packageId, version, indexedSha256 := cmd.resolvePackageIdentity(xrayManager, path)
packageId, version, indexedSha256 := cmd.resolvePackageIdentity(xrayManager, repo, path)
var violations []services.XrayViolation
if violationsResponse != nil {
violations = violationsResponse.Violations
Expand All @@ -271,17 +312,15 @@ func (cmd *DownloadStatusCommand) FetchResult() (*Result, error) {
log.Debug(fmt.Sprintf("Indexed sha256 %s does not match artifact sha256 %s", indexedSha256, sha256))
violations = nil
}
result := buildResult(cmd.repo, path, sha256, cmd.serverDetails.Url, packageId, version, checksumMismatch, scanStatus, violations)
log.Debug(fmt.Sprintf("Download status for %s/%s is %s (%s)", cmd.repo, path, result.DownloadStatus, result.StatusReason))
return result, nil
return buildResult(repo, path, sha256, cmd.serverDetails.Url, packageId, version, checksumMismatch, scanStatus, violations), nil
}

// resolvePackageIdentity looks up how Xray identifies this artifact as a package (e.g. 'deb://debian:12:libxml2'),
// which the scans-list UI needs in its URL. Falls back to a generic identity if the artifact isn't a recognized
// package type or the lookup fails - the resulting link still works, just without the platform's stricter package
// context.
func (cmd *DownloadStatusCommand) resolvePackageIdentity(xrayManager *xray.XrayServicesManager, path string) (packageId, version, indexedSha256 string) {
candidates := cmd.artifactSummaryPaths(path)
func (cmd *DownloadStatusCommand) resolvePackageIdentity(xrayManager *xray.XrayServicesManager, repo, path string) (packageId, version, indexedSha256 string) {
candidates := cmd.artifactSummaryPaths(repo, path)
log.Debug(fmt.Sprintf("Looking up package identity for %s/%s via %s", cmd.repo, path, strings.Join(candidates, ", ")))
for _, candidate := range candidates {
summary, err := xrayManager.ArtifactSummary(services.ArtifactSummaryParams{Paths: []string{candidate}, CliCommand: "xr_status"})
Expand All @@ -302,8 +341,8 @@ func (cmd *DownloadStatusCommand) resolvePackageIdentity(xrayManager *xray.XrayS
return "generic://" + base, "", ""
}

func (cmd *DownloadStatusCommand) artifactSummaryPaths(path string) []string {
repoPath := cmd.repo + "/" + path
func (cmd *DownloadStatusCommand) artifactSummaryPaths(repo, path string) []string {
repoPath := repo + "/" + path
if cmd.project != "" {
// Project-scoped first: an unscoped or default/ hit can belong to a
// different project and carry a checksum that doesn't match this artifact.
Expand Down Expand Up @@ -357,32 +396,6 @@ func xrayPackageScheme(pkgType string) string {
return lower
}

// resolveArtifact tries each path candidate in turn (a docker tag can resolve to a manifest list or a plain
// manifest) and returns the first one Artifactory actually has, along with its sha256.
func (cmd *DownloadStatusCommand) resolveArtifact() (path, sha256 string, err error) {
artifactoryManager, err := artifactoryUtils.CreateServiceManager(cmd.serverDetails, -1, 0, false)
if err != nil {
return "", "", err
}
var lastErr error
for _, candidate := range cmd.pathCandidates {
fileInfo, ferr := artifactoryManager.FileInfo(cmd.repo + "/" + candidate)
if ferr == nil {
return candidate, fileInfo.Checksums.Sha256, nil
}
if !isArtifactNotFound(ferr) {
log.Debug(fmt.Sprintf("File info for %s/%s failed: %s", cmd.repo, candidate, ferr.Error()))
return "", "", ferr
}
log.Debug(fmt.Sprintf("Artifact %s/%s was not found", cmd.repo, candidate))
lastErr = ferr
}
if lastErr == nil {
return "", "", errorutils.CheckErrorf("could not find artifact under repo '%s' (tried: %s)", cmd.repo, strings.Join(cmd.pathCandidates, ", "))
}
return "", "", errorutils.CheckErrorf("could not find artifact under repo '%s' (tried: %s): %s", cmd.repo, strings.Join(cmd.pathCandidates, ", "), lastErr.Error())
}

func isArtifactNotFound(err error) bool {
var httpErr *errorutils.HttpResponseError
return errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound
Expand All @@ -399,17 +412,20 @@ type violationRow struct {
Detail string `json:"detail"`
ViolationId string `json:"violation_id"`
Link string `json:"link,omitempty"`
Platform string `json:"platform,omitempty"`
issueKey string
}

type Result struct {
Repo string `json:"repo"`
RequestedRepo string `json:"requested_repo,omitempty"`
Path string `json:"path"`
Sha256 string `json:"sha256,omitempty"`
ScansListLink string `json:"scans_list_link,omitempty"`
ScanStatus services.ArtifactDetailedStatus `json:"scan_status"`
DownloadStatus string `json:"download_status"`
StatusReason string `json:"status_reason,omitempty"`
Platforms []PlatformStatus `json:"platforms,omitempty"`
Violations []violationRow `json:"violations"`
}

Expand Down Expand Up @@ -473,15 +489,7 @@ func buildResult(repo, path, sha256, platformUrl, packageId, version string, che
}
}

sort.SliceStable(result.Violations, func(i, j int) bool {
if result.Violations[i].Blocking != result.Violations[j].Blocking {
return result.Violations[i].Blocking
}
if result.Violations[i].SeverityNumValue != result.Violations[j].SeverityNumValue {
return result.Violations[i].SeverityNumValue > result.Violations[j].SeverityNumValue
}
return result.Violations[i].issueKey < result.Violations[j].issueKey
})
sortViolationRows(result.Violations)

switch {
case checksumMismatch:
Expand All @@ -499,6 +507,18 @@ func buildResult(repo, path, sha256, platformUrl, packageId, version string, che
return result
}

func sortViolationRows(rows []violationRow) {
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].Blocking != rows[j].Blocking {
return rows[i].Blocking
}
if rows[i].SeverityNumValue != rows[j].SeverityNumValue {
return rows[i].SeverityNumValue > rows[j].SeverityNumValue
}
return rows[i].issueKey < rows[j].issueKey
})
}

func violationIgnored(policy services.ViolationPolicy, violation services.XrayViolation) bool {
if policy.IsIgnored {
return true
Expand Down Expand Up @@ -698,6 +718,9 @@ func printResult(outputFormat format.OutputFormat, result *Result) error {
}

log.Output(fmt.Sprintf("Artifact: %s/%s", result.Repo, result.Path))
if result.RequestedRepo != "" {
log.Output(fmt.Sprintf("Resolved From: %s", result.RequestedRepo))
}
if result.Sha256 != "" {
log.Output(fmt.Sprintf("Sha256: %s", result.Sha256))
}
Expand All @@ -709,6 +732,9 @@ func printResult(outputFormat format.OutputFormat, result *Result) error {
if result.ScansListLink != "" {
log.Output(fmt.Sprintf("Results: %s", result.ScansListLink))
}
for _, platform := range result.Platforms {
log.Output(fmt.Sprintf("Platform: %s %s", platform.Platform, colorizeDownloadStatus(platform.DownloadStatus)))
}

// The Link column only earns its place when the terminal can actually render it as a clickable OSC 8
// hyperlink (e.g. iTerm2, VS Code, Windows Terminal) - in a terminal that can't (e.g. Terminal.app), a
Expand Down Expand Up @@ -740,10 +766,14 @@ func printResult(outputFormat format.OutputFormat, result *Result) error {
}

func newViolationTableRow(violation violationRow) violationTableRow {
detail := violation.Detail
if violation.Platform != "" {
detail = "[" + violation.Platform + "] " + detail
}
return violationTableRow{
Blocking: colorizeBlocking(violation.Blocking),
Severity: colorizeSeverity(violation.Severity),
Detail: violation.Detail,
Detail: detail,
State: violationState(violation.Ignored),
ViolationId: violation.ViolationId,
Watch: violation.Watch,
Expand Down
4 changes: 2 additions & 2 deletions commands/xray/downloadstatus/downloadstatus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,14 +510,14 @@ func TestBuildResultUsesIssueIdAndFallsBackToViolationId(t *testing.T) {

func TestArtifactSummaryPathsTriesProjectScopeFirst(t *testing.T) {
cmd := NewDownloadStatusCommand().SetRepoAndPathCandidates("libs-release-local", nil)
assert.Equal(t, []string{"libs-release-local/com/acme/foo.jar", "default/libs-release-local/com/acme/foo.jar"}, cmd.artifactSummaryPaths("com/acme/foo.jar"))
assert.Equal(t, []string{"libs-release-local/com/acme/foo.jar", "default/libs-release-local/com/acme/foo.jar"}, cmd.artifactSummaryPaths("libs-release-local", "com/acme/foo.jar"))

cmd.SetProject("team-a")
assert.Equal(t, []string{
"team-a/libs-release-local/com/acme/foo.jar",
"libs-release-local/com/acme/foo.jar",
"default/libs-release-local/com/acme/foo.jar",
}, cmd.artifactSummaryPaths("com/acme/foo.jar"))
}, cmd.artifactSummaryPaths("libs-release-local", "com/acme/foo.jar"))
}

func TestIsArtifactNotFound(t *testing.T) {
Expand Down
Loading
Loading