mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-03 08:46:57 +00:00
appview: report a never-scanned image as unscanned, not as a failure
digest_content.go branched on Error == "never-scanned" to pick the "not scanned yet" copy, but nothing anywhere produced that string: both FetchVulnDetails and FetchSbomDetails returned the human sentence "No scan record found" for a missing record. So vulnReason and sbomReason could never be "not-scanned", the friendly branches in vulns-section.html and sbom-section.html were dead code, and every unscanned image fell through to fetch-failed. Free-tier accounts have scan_on_push off, so this was every image they push, told "Scan data couldn't be loaded... try refreshing in a minute" about something that had never been scanned and never would be by refreshing. The digest page showed the raw internal string instead. Replace the prose sentinel with a NotScanned bool the 404 path actually sets, and give other non-200 statuses a distinct message so a 500 from the hold stops being indistinguishable from an absent record. The detail templates branch on it before Error, so nothing leaks the internal value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UAqi2hS2dhZoatqcWoYZQk
This commit is contained in:
co-authored by
Claude Opus 5
parent
27ce122db0
commit
1253ca15ec
@@ -160,7 +160,7 @@ func (h *DigestContentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
vulnReason := "ok"
|
||||
if !holdReachable {
|
||||
vulnReason = "hold-unreachable"
|
||||
} else if vulnData == nil || vulnData.Error == "never-scanned" {
|
||||
} else if vulnData == nil || vulnData.NotScanned {
|
||||
vulnReason = "not-scanned"
|
||||
} else if vulnData.Status == atproto.ScanStatusSkipped {
|
||||
vulnReason = "not-applicable"
|
||||
@@ -171,7 +171,7 @@ func (h *DigestContentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
sbomReason := "ok"
|
||||
if !holdReachable {
|
||||
sbomReason = "hold-unreachable"
|
||||
} else if sbomData == nil || sbomData.Error == "never-scanned" {
|
||||
} else if sbomData == nil || sbomData.NotScanned {
|
||||
sbomReason = "not-scanned"
|
||||
} else if sbomData.Status == atproto.ScanStatusSkipped {
|
||||
sbomReason = "not-applicable"
|
||||
|
||||
@@ -43,9 +43,12 @@ type spdxExternalRef struct {
|
||||
|
||||
// sbomDetailsData is the template data for the sbom-details partial.
|
||||
type sbomDetailsData struct {
|
||||
Packages []sbomPackage
|
||||
Total int
|
||||
Error string
|
||||
Packages []sbomPackage
|
||||
Total int
|
||||
Error string
|
||||
// NotScanned means the hold answered but holds no scan record for this
|
||||
// manifest. See the note on vulnDetailsData.NotScanned.
|
||||
NotScanned bool
|
||||
Status string // scan record's status field (ok | failed | skipped); empty for legacy records
|
||||
Reason string // scan record's reason field (only meaningful when Status != ok)
|
||||
ScannedAt string
|
||||
@@ -125,7 +128,10 @@ func FetchSbomDetails(ctx context.Context, holdEndpoint, digest string) sbomDeta
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return sbomDetailsData{Error: "No scan record found"}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return sbomDetailsData{NotScanned: true}
|
||||
}
|
||||
return sbomDetailsData{Error: fmt.Sprintf("Scan lookup failed (HTTP %d)", resp.StatusCode)}
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
|
||||
@@ -54,9 +54,13 @@ type grypePackage struct {
|
||||
|
||||
// vulnDetailsData is the template data for the vuln-details partial.
|
||||
type vulnDetailsData struct {
|
||||
Matches []vulnMatch
|
||||
Summary vulnSummary
|
||||
Error string // non-empty if something went wrong
|
||||
Matches []vulnMatch
|
||||
Summary vulnSummary
|
||||
Error string // non-empty if something went wrong
|
||||
// NotScanned means the hold answered but holds no scan record for this
|
||||
// manifest. Distinct from Error: nothing failed, the image was simply
|
||||
// never scanned, and the UI must not present it as a failure.
|
||||
NotScanned bool
|
||||
Status string // scan record's status field (ok | failed | skipped); empty for legacy records
|
||||
Reason string // scan record's reason field (only meaningful when Status != ok)
|
||||
ScannedAt string
|
||||
@@ -137,7 +141,11 @@ func (h *VulnDetailsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
h.renderDetails(w, vulnDetailsData{Error: "No scan record found"})
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
h.renderDetails(w, vulnDetailsData{NotScanned: true})
|
||||
return
|
||||
}
|
||||
h.renderDetails(w, vulnDetailsData{Error: fmt.Sprintf("Scan lookup failed (HTTP %d)", resp.StatusCode)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,7 +304,10 @@ func FetchVulnDetails(ctx context.Context, holdEndpoint, digest string) vulnDeta
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return vulnDetailsData{Error: "No scan record found"}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return vulnDetailsData{NotScanned: true}
|
||||
}
|
||||
return vulnDetailsData{Error: fmt.Sprintf("Scan lookup failed (HTTP %d)", resp.StatusCode)}
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
|
||||
@@ -289,9 +289,13 @@ func TestVulnDetails_NotFound(t *testing.T) {
|
||||
|
||||
body := rr.Body.String()
|
||||
|
||||
// Should show an error message (modal still needs content)
|
||||
if !strings.Contains(body, "No scan record found") {
|
||||
t.Error("Expected body to contain error message for 404")
|
||||
// A 404 from the hold means the manifest was never scanned, which is not a
|
||||
// failure: the panel must say so rather than reporting an error.
|
||||
if !strings.Contains(body, "No vulnerability scan available yet") {
|
||||
t.Errorf("Expected the not-scanned copy for a 404, got: %s", body)
|
||||
}
|
||||
if strings.Contains(body, "Scan lookup failed") {
|
||||
t.Error("A 404 must not be reported as a lookup failure")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{{ define "sbom-details" }}
|
||||
{{ if .Error }}
|
||||
{{ if .NotScanned }}
|
||||
<p class="text-base-content/70">No SBOM available yet. The scanner generates an SBOM alongside each scan.</p>
|
||||
{{ else if .Error }}
|
||||
<div class="alert alert-error" role="alert">
|
||||
{{ icon "alert-circle" "size-5 shrink-0" }}
|
||||
<span>{{ .Error }}</span>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{{ define "vuln-details" }}
|
||||
{{ if .Error }}
|
||||
{{ if .NotScanned }}
|
||||
<div class="py-8 text-sm text-base-content/70 max-w-prose">
|
||||
<p class="font-medium text-base-content">No vulnerability scan available yet</p>
|
||||
<p class="mt-1">Scans run automatically shortly after a push. Check back in a few minutes, or push a new tag to trigger a scan.</p>
|
||||
</div>
|
||||
{{ else if .Error }}
|
||||
{{ if gt .Summary.Total 0 }}
|
||||
<!-- Summary available but no detailed report -->
|
||||
<div class="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user