move the vuln report to tags instead of manifests

This commit is contained in:
Evan Jarrett
2026-02-16 22:32:18 -06:00
parent cd4986c0c8
commit dba201998e
5 changed files with 73 additions and 38 deletions
+3
View File
@@ -127,11 +127,14 @@ type PlatformInfo struct {
Architecture string
Variant string
OSVersion string
Digest string // child platform manifest digest (for manifest lists)
HoldEndpoint string // hold endpoint for this platform manifest
}
// TagWithPlatforms extends Tag with platform information
type TagWithPlatforms struct {
Tag
HoldEndpoint string // hold endpoint from the tag's own manifest
Platforms []PlatformInfo
IsMultiArch bool
HasAttestations bool // true if manifest list contains attestation references
+14 -3
View File
@@ -636,14 +636,18 @@ func GetTagsWithPlatforms(db DBTX, did, repository string) ([]TagWithPlatforms,
t.created_at,
m.media_type,
m.artifact_type,
m.hold_endpoint,
COALESCE(mr.platform_os, '') as platform_os,
COALESCE(mr.platform_architecture, '') as platform_architecture,
COALESCE(mr.platform_variant, '') as platform_variant,
COALESCE(mr.platform_os_version, '') as platform_os_version,
COALESCE(mr.is_attestation, 0) as is_attestation
COALESCE(mr.is_attestation, 0) as is_attestation,
COALESCE(mr.digest, '') as child_digest,
COALESCE(child_m.hold_endpoint, m.hold_endpoint, '') as child_hold_endpoint
FROM tags t
JOIN manifests m ON t.digest = m.digest AND t.did = m.did AND t.repository = m.repository
LEFT JOIN manifest_references mr ON m.id = mr.manifest_id
LEFT JOIN manifests child_m ON mr.digest = child_m.digest AND child_m.did = t.did AND child_m.repository = t.repository
WHERE t.did = ? AND t.repository = ?
ORDER BY t.created_at DESC, mr.reference_index
`, did, repository)
@@ -659,11 +663,15 @@ func GetTagsWithPlatforms(db DBTX, did, repository string) ([]TagWithPlatforms,
for rows.Next() {
var t Tag
var mediaType, artifactType, platformOS, platformArch, platformVariant, platformOSVersion string
var mediaType, artifactType, holdEndpoint string
var platformOS, platformArch, platformVariant, platformOSVersion string
var isAttestation bool
var childDigest, childHoldEndpoint string
if err := rows.Scan(&t.ID, &t.DID, &t.Repository, &t.Tag, &t.Digest, &t.CreatedAt,
&mediaType, &artifactType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil {
&mediaType, &artifactType, &holdEndpoint,
&platformOS, &platformArch, &platformVariant, &platformOSVersion,
&isAttestation, &childDigest, &childHoldEndpoint); err != nil {
return nil, err
}
@@ -672,6 +680,7 @@ func GetTagsWithPlatforms(db DBTX, did, repository string) ([]TagWithPlatforms,
if _, exists := tagMap[tagKey]; !exists {
tagMap[tagKey] = &TagWithPlatforms{
Tag: t,
HoldEndpoint: holdEndpoint,
Platforms: []PlatformInfo{},
ArtifactType: artifactType,
}
@@ -692,6 +701,8 @@ func GetTagsWithPlatforms(db DBTX, did, repository string) ([]TagWithPlatforms,
Architecture: platformArch,
Variant: platformVariant,
OSVersion: platformOSVersion,
Digest: childDigest,
HoldEndpoint: childHoldEndpoint,
})
}
}
+24 -14
View File
@@ -230,22 +230,32 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
artifactType = manifests[0].ArtifactType
}
// Collect digests for batch scan-result request
var scanDigests []string
var scanHoldEndpoint string
for _, m := range manifests {
if !m.IsManifestList && m.HoldEndpoint != "" {
if scanHoldEndpoint == "" {
scanHoldEndpoint = m.HoldEndpoint
// Collect digests for batch scan-result requests, grouped by hold endpoint
holdDigests := make(map[string][]string) // holdEndpoint → []hexDigest
seen := make(map[string]bool) // dedup digests
for _, t := range tagsWithPlatforms {
if len(t.Platforms) > 0 {
// Multi-arch: collect each platform's child digest
for _, p := range t.Platforms {
if p.Digest != "" && p.HoldEndpoint != "" && !seen[p.Digest] {
seen[p.Digest] = true
hex := strings.TrimPrefix(p.Digest, "sha256:")
holdDigests[p.HoldEndpoint] = append(holdDigests[p.HoldEndpoint], hex)
}
}
if m.HoldEndpoint == scanHoldEndpoint {
scanDigests = append(scanDigests, strings.TrimPrefix(m.Digest, "sha256:"))
} else if t.HoldEndpoint != "" {
// Single-arch: use tag's own digest
if !seen[t.Digest] {
seen[t.Digest] = true
hex := strings.TrimPrefix(t.Digest, "sha256:")
holdDigests[t.HoldEndpoint] = append(holdDigests[t.HoldEndpoint], hex)
}
}
}
var scanBatchParams string
if len(scanDigests) > 0 {
scanBatchParams = "holdEndpoint=" + url.QueryEscape(scanHoldEndpoint) + "&digests=" + strings.Join(scanDigests, ",")
var scanBatchParams []template.HTML
for hold, digests := range holdDigests {
scanBatchParams = append(scanBatchParams, template.HTML(
"holdEndpoint="+url.QueryEscape(hold)+"&digests="+strings.Join(digests, ",")))
}
// Build page meta
@@ -285,7 +295,7 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
IsOwner bool // Whether current user owns this repository
ReadmeHTML template.HTML
ArtifactType string // Dominant artifact type: container-image, helm-chart, unknown
ScanBatchParams template.HTML // Pre-encoded query string for batch scan-result endpoint
ScanBatchParams []template.HTML // Pre-encoded query strings for batch scan-result endpoint (one per hold)
}{
PageData: NewPageData(r, &h.BaseUIHandler),
Meta: meta,
@@ -299,7 +309,7 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
IsOwner: isOwner,
ReadmeHTML: readmeHTML,
ArtifactType: artifactType,
ScanBatchParams: template.HTML(scanBatchParams),
ScanBatchParams: scanBatchParams,
}
if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil {
+28 -20
View File
@@ -150,20 +150,30 @@
{{ end }}
</div>
</div>
<div class="text-sm">
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy tag digest to clipboard">{{ icon "copy" "size-3" }}</button>
</div>
{{ if .Platforms }}
<div class="flex flex-wrap gap-1">
{{ range .Platforms }}
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
<div class="text-sm space-y-2">
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy tag digest to clipboard">{{ icon "copy" "size-3" }}</button>
</div>
{{ if .Platforms }}
<div class="space-y-1">
{{ range .Platforms }}
<div class="flex flex-wrap items-center gap-2">
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
{{ if .Digest }}
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Digest }}">{{ .Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Digest }}')" aria-label="Copy platform digest to clipboard">{{ icon "copy" "size-3" }}</button>
{{ if .HoldEndpoint }}
<span id="scan-badge-{{ trimPrefix "sha256:" .Digest }}"></span>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
{{ else if .HoldEndpoint }}
{{/* Single-arch: scan badge for the tag's own digest */}}
<div><span id="scan-badge-{{ trimPrefix "sha256:" .Tag.Digest }}"></span></div>
{{ end }}
</div>
{{ if eq .ArtifactType "helm-chart" }}
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }}
@@ -173,6 +183,14 @@
</div>
{{ end }}
</div>
{{ if $.ScanBatchParams }}
{{ range $.ScanBatchParams }}
<div hx-get="/api/scan-results?{{ . }}"
hx-trigger="load delay:500ms"
hx-swap="none"
style="display:none"></div>
{{ end }}
{{ end }}
{{ else }}
<p class="text-base-content/60">No tags available</p>
{{ end }}
@@ -225,10 +243,6 @@
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')" aria-label="Copy manifest digest to clipboard">{{ icon "copy" "size-3" }}</button>
</div>
{{/* Vulnerability scan badge — own row below digest */}}
{{ if and (not .IsManifestList) .Manifest.HoldEndpoint }}
<div><span id="scan-badge-{{ trimPrefix "sha256:" .Manifest.Digest }}"></span></div>
{{ end }}
</div>
<div class="flex items-center gap-2">
<time class="text-sm text-base-content/60" datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">
@@ -267,12 +281,6 @@
</div>
{{ end }}
</div>
{{ if $.ScanBatchParams }}
<div hx-get="/api/scan-results?{{ $.ScanBatchParams }}"
hx-trigger="load delay:500ms"
hx-swap="none"
style="display:none"></div>
{{ end }}
{{ else }}
<p class="text-base-content/60">No manifests available</p>
{{ end }}
+4 -1
View File
@@ -170,7 +170,10 @@ func LoadOrCreateDID(ctx context.Context, cfg DIDConfig) (string, error) {
rotationKey, _ := parseOptionalMultibaseKey(cfg.RotationKey)
if err := EnsurePLCCurrent(ctx, did, rotationKey, signingKey, cfg.PublicURL, cfg.PLCDirectoryURL); err != nil {
return "", fmt.Errorf("failed to ensure PLC identity is current: %w", err)
slog.Warn("Failed to verify PLC identity is current (will retry on next restart)",
"did", did,
"error", err,
)
}
return did, nil