A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

move the vuln report to tags instead of manifests

+73 -38
+3
pkg/appview/db/models.go
··· 127 127 Architecture string 128 128 Variant string 129 129 OSVersion string 130 + Digest string // child platform manifest digest (for manifest lists) 131 + HoldEndpoint string // hold endpoint for this platform manifest 130 132 } 131 133 132 134 // TagWithPlatforms extends Tag with platform information 133 135 type TagWithPlatforms struct { 134 136 Tag 137 + HoldEndpoint string // hold endpoint from the tag's own manifest 135 138 Platforms []PlatformInfo 136 139 IsMultiArch bool 137 140 HasAttestations bool // true if manifest list contains attestation references
+14 -3
pkg/appview/db/queries.go
··· 636 636 t.created_at, 637 637 m.media_type, 638 638 m.artifact_type, 639 + m.hold_endpoint, 639 640 COALESCE(mr.platform_os, '') as platform_os, 640 641 COALESCE(mr.platform_architecture, '') as platform_architecture, 641 642 COALESCE(mr.platform_variant, '') as platform_variant, 642 643 COALESCE(mr.platform_os_version, '') as platform_os_version, 643 - COALESCE(mr.is_attestation, 0) as is_attestation 644 + COALESCE(mr.is_attestation, 0) as is_attestation, 645 + COALESCE(mr.digest, '') as child_digest, 646 + COALESCE(child_m.hold_endpoint, m.hold_endpoint, '') as child_hold_endpoint 644 647 FROM tags t 645 648 JOIN manifests m ON t.digest = m.digest AND t.did = m.did AND t.repository = m.repository 646 649 LEFT JOIN manifest_references mr ON m.id = mr.manifest_id 650 + LEFT JOIN manifests child_m ON mr.digest = child_m.digest AND child_m.did = t.did AND child_m.repository = t.repository 647 651 WHERE t.did = ? AND t.repository = ? 648 652 ORDER BY t.created_at DESC, mr.reference_index 649 653 `, did, repository) ··· 659 663 660 664 for rows.Next() { 661 665 var t Tag 662 - var mediaType, artifactType, platformOS, platformArch, platformVariant, platformOSVersion string 666 + var mediaType, artifactType, holdEndpoint string 667 + var platformOS, platformArch, platformVariant, platformOSVersion string 663 668 var isAttestation bool 669 + var childDigest, childHoldEndpoint string 664 670 665 671 if err := rows.Scan(&t.ID, &t.DID, &t.Repository, &t.Tag, &t.Digest, &t.CreatedAt, 666 - &mediaType, &artifactType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil { 672 + &mediaType, &artifactType, &holdEndpoint, 673 + &platformOS, &platformArch, &platformVariant, &platformOSVersion, 674 + &isAttestation, &childDigest, &childHoldEndpoint); err != nil { 667 675 return nil, err 668 676 } 669 677 ··· 672 680 if _, exists := tagMap[tagKey]; !exists { 673 681 tagMap[tagKey] = &TagWithPlatforms{ 674 682 Tag: t, 683 + HoldEndpoint: holdEndpoint, 675 684 Platforms: []PlatformInfo{}, 676 685 ArtifactType: artifactType, 677 686 } ··· 692 701 Architecture: platformArch, 693 702 Variant: platformVariant, 694 703 OSVersion: platformOSVersion, 704 + Digest: childDigest, 705 + HoldEndpoint: childHoldEndpoint, 695 706 }) 696 707 } 697 708 }
+24 -14
pkg/appview/handlers/repository.go
··· 230 230 artifactType = manifests[0].ArtifactType 231 231 } 232 232 233 - // Collect digests for batch scan-result request 234 - var scanDigests []string 235 - var scanHoldEndpoint string 236 - for _, m := range manifests { 237 - if !m.IsManifestList && m.HoldEndpoint != "" { 238 - if scanHoldEndpoint == "" { 239 - scanHoldEndpoint = m.HoldEndpoint 233 + // Collect digests for batch scan-result requests, grouped by hold endpoint 234 + holdDigests := make(map[string][]string) // holdEndpoint → []hexDigest 235 + seen := make(map[string]bool) // dedup digests 236 + for _, t := range tagsWithPlatforms { 237 + if len(t.Platforms) > 0 { 238 + // Multi-arch: collect each platform's child digest 239 + for _, p := range t.Platforms { 240 + if p.Digest != "" && p.HoldEndpoint != "" && !seen[p.Digest] { 241 + seen[p.Digest] = true 242 + hex := strings.TrimPrefix(p.Digest, "sha256:") 243 + holdDigests[p.HoldEndpoint] = append(holdDigests[p.HoldEndpoint], hex) 244 + } 240 245 } 241 - if m.HoldEndpoint == scanHoldEndpoint { 242 - scanDigests = append(scanDigests, strings.TrimPrefix(m.Digest, "sha256:")) 246 + } else if t.HoldEndpoint != "" { 247 + // Single-arch: use tag's own digest 248 + if !seen[t.Digest] { 249 + seen[t.Digest] = true 250 + hex := strings.TrimPrefix(t.Digest, "sha256:") 251 + holdDigests[t.HoldEndpoint] = append(holdDigests[t.HoldEndpoint], hex) 243 252 } 244 253 } 245 254 } 246 - var scanBatchParams string 247 - if len(scanDigests) > 0 { 248 - scanBatchParams = "holdEndpoint=" + url.QueryEscape(scanHoldEndpoint) + "&digests=" + strings.Join(scanDigests, ",") 255 + var scanBatchParams []template.HTML 256 + for hold, digests := range holdDigests { 257 + scanBatchParams = append(scanBatchParams, template.HTML( 258 + "holdEndpoint="+url.QueryEscape(hold)+"&digests="+strings.Join(digests, ","))) 249 259 } 250 260 251 261 // Build page meta ··· 285 295 IsOwner bool // Whether current user owns this repository 286 296 ReadmeHTML template.HTML 287 297 ArtifactType string // Dominant artifact type: container-image, helm-chart, unknown 288 - ScanBatchParams template.HTML // Pre-encoded query string for batch scan-result endpoint 298 + ScanBatchParams []template.HTML // Pre-encoded query strings for batch scan-result endpoint (one per hold) 289 299 }{ 290 300 PageData: NewPageData(r, &h.BaseUIHandler), 291 301 Meta: meta, ··· 299 309 IsOwner: isOwner, 300 310 ReadmeHTML: readmeHTML, 301 311 ArtifactType: artifactType, 302 - ScanBatchParams: template.HTML(scanBatchParams), 312 + ScanBatchParams: scanBatchParams, 303 313 } 304 314 305 315 if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil {
+28 -20
pkg/appview/templates/pages/repository.html
··· 150 150 {{ end }} 151 151 </div> 152 152 </div> 153 - <div class="text-sm"> 154 - <div class="flex flex-wrap justify-between items-center gap-2"> 155 - <div class="flex items-center gap-2"> 156 - <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code> 157 - <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy tag digest to clipboard">{{ icon "copy" "size-3" }}</button> 158 - </div> 159 - {{ if .Platforms }} 160 - <div class="flex flex-wrap gap-1"> 161 - {{ range .Platforms }} 162 - <span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 153 + <div class="text-sm space-y-2"> 154 + <div class="flex items-center gap-2"> 155 + <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code> 156 + <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy tag digest to clipboard">{{ icon "copy" "size-3" }}</button> 157 + </div> 158 + {{ if .Platforms }} 159 + <div class="space-y-1"> 160 + {{ range .Platforms }} 161 + <div class="flex flex-wrap items-center gap-2"> 162 + <span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 163 + {{ if .Digest }} 164 + <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Digest }}">{{ .Digest }}</code> 165 + <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Digest }}')" aria-label="Copy platform digest to clipboard">{{ icon "copy" "size-3" }}</button> 166 + {{ if .HoldEndpoint }} 167 + <span id="scan-badge-{{ trimPrefix "sha256:" .Digest }}"></span> 168 + {{ end }} 163 169 {{ end }} 164 170 </div> 165 171 {{ end }} 166 172 </div> 173 + {{ else if .HoldEndpoint }} 174 + {{/* Single-arch: scan badge for the tag's own digest */}} 175 + <div><span id="scan-badge-{{ trimPrefix "sha256:" .Tag.Digest }}"></span></div> 176 + {{ end }} 167 177 </div> 168 178 {{ if eq .ArtifactType "helm-chart" }} 169 179 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }} ··· 173 183 </div> 174 184 {{ end }} 175 185 </div> 186 + {{ if $.ScanBatchParams }} 187 + {{ range $.ScanBatchParams }} 188 + <div hx-get="/api/scan-results?{{ . }}" 189 + hx-trigger="load delay:500ms" 190 + hx-swap="none" 191 + style="display:none"></div> 192 + {{ end }} 193 + {{ end }} 176 194 {{ else }} 177 195 <p class="text-base-content/60">No tags available</p> 178 196 {{ end }} ··· 225 243 <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code> 226 244 <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')" aria-label="Copy manifest digest to clipboard">{{ icon "copy" "size-3" }}</button> 227 245 </div> 228 - {{/* Vulnerability scan badge — own row below digest */}} 229 - {{ if and (not .IsManifestList) .Manifest.HoldEndpoint }} 230 - <div><span id="scan-badge-{{ trimPrefix "sha256:" .Manifest.Digest }}"></span></div> 231 - {{ end }} 232 246 </div> 233 247 <div class="flex items-center gap-2"> 234 248 <time class="text-sm text-base-content/60" datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> ··· 267 281 </div> 268 282 {{ end }} 269 283 </div> 270 - {{ if $.ScanBatchParams }} 271 - <div hx-get="/api/scan-results?{{ $.ScanBatchParams }}" 272 - hx-trigger="load delay:500ms" 273 - hx-swap="none" 274 - style="display:none"></div> 275 - {{ end }} 276 284 {{ else }} 277 285 <p class="text-base-content/60">No manifests available</p> 278 286 {{ end }}
+4 -1
pkg/hold/pds/did.go
··· 170 170 rotationKey, _ := parseOptionalMultibaseKey(cfg.RotationKey) 171 171 172 172 if err := EnsurePLCCurrent(ctx, did, rotationKey, signingKey, cfg.PublicURL, cfg.PLCDirectoryURL); err != nil { 173 - return "", fmt.Errorf("failed to ensure PLC identity is current: %w", err) 173 + slog.Warn("Failed to verify PLC identity is current (will retry on next restart)", 174 + "did", did, 175 + "error", err, 176 + ) 174 177 } 175 178 176 179 return did, nil