tangled
alpha
login
or
join now
evan.jarrett.net
/
at-container-registry
66
fork
atom
A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
66
fork
atom
overview
issues
1
pulls
pipelines
move the vuln report to tags instead of manifests
evan.jarrett.net
1 month ago
dba20199
cd4986c0
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
2/2
lint.yaml
success
3m 30s
tests.yml
success
3m 22s
+73
-38
5 changed files
expand all
collapse all
unified
split
pkg
appview
db
models.go
queries.go
handlers
repository.go
templates
pages
repository.html
hold
pds
did.go
+3
pkg/appview/db/models.go
reviewed
···
127
127
Architecture string
128
128
Variant string
129
129
OSVersion string
130
130
+
Digest string // child platform manifest digest (for manifest lists)
131
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
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
reviewed
···
636
636
t.created_at,
637
637
m.media_type,
638
638
m.artifact_type,
639
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
643
-
COALESCE(mr.is_attestation, 0) as is_attestation
644
644
+
COALESCE(mr.is_attestation, 0) as is_attestation,
645
645
+
COALESCE(mr.digest, '') as child_digest,
646
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
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
662
-
var mediaType, artifactType, platformOS, platformArch, platformVariant, platformOSVersion string
666
666
+
var mediaType, artifactType, holdEndpoint string
667
667
+
var platformOS, platformArch, platformVariant, platformOSVersion string
663
668
var isAttestation bool
669
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
666
-
&mediaType, &artifactType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil {
672
672
+
&mediaType, &artifactType, &holdEndpoint,
673
673
+
&platformOS, &platformArch, &platformVariant, &platformOSVersion,
674
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
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
704
+
Digest: childDigest,
705
705
+
HoldEndpoint: childHoldEndpoint,
695
706
})
696
707
}
697
708
}
+24
-14
pkg/appview/handlers/repository.go
reviewed
···
230
230
artifactType = manifests[0].ArtifactType
231
231
}
232
232
233
233
-
// Collect digests for batch scan-result request
234
234
-
var scanDigests []string
235
235
-
var scanHoldEndpoint string
236
236
-
for _, m := range manifests {
237
237
-
if !m.IsManifestList && m.HoldEndpoint != "" {
238
238
-
if scanHoldEndpoint == "" {
239
239
-
scanHoldEndpoint = m.HoldEndpoint
233
233
+
// Collect digests for batch scan-result requests, grouped by hold endpoint
234
234
+
holdDigests := make(map[string][]string) // holdEndpoint → []hexDigest
235
235
+
seen := make(map[string]bool) // dedup digests
236
236
+
for _, t := range tagsWithPlatforms {
237
237
+
if len(t.Platforms) > 0 {
238
238
+
// Multi-arch: collect each platform's child digest
239
239
+
for _, p := range t.Platforms {
240
240
+
if p.Digest != "" && p.HoldEndpoint != "" && !seen[p.Digest] {
241
241
+
seen[p.Digest] = true
242
242
+
hex := strings.TrimPrefix(p.Digest, "sha256:")
243
243
+
holdDigests[p.HoldEndpoint] = append(holdDigests[p.HoldEndpoint], hex)
244
244
+
}
240
245
}
241
241
-
if m.HoldEndpoint == scanHoldEndpoint {
242
242
-
scanDigests = append(scanDigests, strings.TrimPrefix(m.Digest, "sha256:"))
246
246
+
} else if t.HoldEndpoint != "" {
247
247
+
// Single-arch: use tag's own digest
248
248
+
if !seen[t.Digest] {
249
249
+
seen[t.Digest] = true
250
250
+
hex := strings.TrimPrefix(t.Digest, "sha256:")
251
251
+
holdDigests[t.HoldEndpoint] = append(holdDigests[t.HoldEndpoint], hex)
243
252
}
244
253
}
245
254
}
246
246
-
var scanBatchParams string
247
247
-
if len(scanDigests) > 0 {
248
248
-
scanBatchParams = "holdEndpoint=" + url.QueryEscape(scanHoldEndpoint) + "&digests=" + strings.Join(scanDigests, ",")
255
255
+
var scanBatchParams []template.HTML
256
256
+
for hold, digests := range holdDigests {
257
257
+
scanBatchParams = append(scanBatchParams, template.HTML(
258
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
288
-
ScanBatchParams template.HTML // Pre-encoded query string for batch scan-result endpoint
298
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
302
-
ScanBatchParams: template.HTML(scanBatchParams),
312
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
reviewed
···
150
150
{{ end }}
151
151
</div>
152
152
</div>
153
153
-
<div class="text-sm">
154
154
-
<div class="flex flex-wrap justify-between items-center gap-2">
155
155
-
<div class="flex items-center gap-2">
156
156
-
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
157
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
158
-
</div>
159
159
-
{{ if .Platforms }}
160
160
-
<div class="flex flex-wrap gap-1">
161
161
-
{{ range .Platforms }}
162
162
-
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
153
153
+
<div class="text-sm space-y-2">
154
154
+
<div class="flex items-center gap-2">
155
155
+
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
156
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
157
+
</div>
158
158
+
{{ if .Platforms }}
159
159
+
<div class="space-y-1">
160
160
+
{{ range .Platforms }}
161
161
+
<div class="flex flex-wrap items-center gap-2">
162
162
+
<span class="badge badge-sm badge-soft badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span>
163
163
+
{{ if .Digest }}
164
164
+
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Digest }}">{{ .Digest }}</code>
165
165
+
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Digest }}')" aria-label="Copy platform digest to clipboard">{{ icon "copy" "size-3" }}</button>
166
166
+
{{ if .HoldEndpoint }}
167
167
+
<span id="scan-badge-{{ trimPrefix "sha256:" .Digest }}"></span>
168
168
+
{{ end }}
163
169
{{ end }}
164
170
</div>
165
171
{{ end }}
166
172
</div>
173
173
+
{{ else if .HoldEndpoint }}
174
174
+
{{/* Single-arch: scan badge for the tag's own digest */}}
175
175
+
<div><span id="scan-badge-{{ trimPrefix "sha256:" .Tag.Digest }}"></span></div>
176
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
186
+
{{ if $.ScanBatchParams }}
187
187
+
{{ range $.ScanBatchParams }}
188
188
+
<div hx-get="/api/scan-results?{{ . }}"
189
189
+
hx-trigger="load delay:500ms"
190
190
+
hx-swap="none"
191
191
+
style="display:none"></div>
192
192
+
{{ end }}
193
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
228
-
{{/* Vulnerability scan badge — own row below digest */}}
229
229
-
{{ if and (not .IsManifestList) .Manifest.HoldEndpoint }}
230
230
-
<div><span id="scan-badge-{{ trimPrefix "sha256:" .Manifest.Digest }}"></span></div>
231
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
270
-
{{ if $.ScanBatchParams }}
271
271
-
<div hx-get="/api/scan-results?{{ $.ScanBatchParams }}"
272
272
-
hx-trigger="load delay:500ms"
273
273
-
hx-swap="none"
274
274
-
style="display:none"></div>
275
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
reviewed
···
170
170
rotationKey, _ := parseOptionalMultibaseKey(cfg.RotationKey)
171
171
172
172
if err := EnsurePLCCurrent(ctx, did, rotationKey, signingKey, cfg.PublicURL, cfg.PLCDirectoryURL); err != nil {
173
173
-
return "", fmt.Errorf("failed to ensure PLC identity is current: %w", err)
173
173
+
slog.Warn("Failed to verify PLC identity is current (will retry on next restart)",
174
174
+
"did", did,
175
175
+
"error", err,
176
176
+
)
174
177
}
175
178
176
179
return did, nil