mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
move the vuln report to tags instead of manifests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user