mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 00:34:16 +00:00
211 lines
6.2 KiB
Go
211 lines
6.2 KiB
Go
package pds
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
"github.com/ipfs/go-cid"
|
|
)
|
|
|
|
// unscannableLayerMediaSubstrings lists layer media-type fragments that
|
|
// identify artifact types the scanner intentionally skips. Kept in sync with
|
|
// scanner/internal/scan/worker.go's unscannableConfigTypes — that map keys on
|
|
// config media types; here we look at *layer* media types because the
|
|
// backfill walks the hold's layer index (manifest AT-URIs join layers to
|
|
// scan records).
|
|
var unscannableLayerMediaSubstrings = []string{
|
|
"helm.chart.content",
|
|
"in-toto",
|
|
"dsse.envelope",
|
|
}
|
|
|
|
// ScanBackfillResult summarizes what BackfillScanStatus did.
|
|
type ScanBackfillResult struct {
|
|
Scanned int // total scan records examined
|
|
AlreadyTagged int // records with non-empty status (or non-failure shape) — left alone
|
|
MarkedSkipped int // records rewritten as status=skipped
|
|
MarkedFailed int // records rewritten as status=failed
|
|
Rewritten int // total rewrites (MarkedSkipped + MarkedFailed) that succeeded
|
|
}
|
|
|
|
// BackfillScanStatus walks every io.atcr.hold.scan record on this hold and
|
|
// assigns a status ("skipped" or "failed") to legacy records that pre-date the
|
|
// status field. Idempotent — records that already have a non-empty status are
|
|
// left alone.
|
|
//
|
|
// A legacy record is one with empty status, no SBOM blob, and zero
|
|
// vulnerability counts. Layer media types are inspected to choose the
|
|
// rewrite: helm/in-toto/DSSE → skipped, otherwise → failed. ScannedAt is
|
|
// preserved on rewrite so the rescan timer doesn't reset.
|
|
//
|
|
// Safe to call on a running hold — uses the existing repomgr APIs the same
|
|
// way scan-job handling does.
|
|
//
|
|
// progress, if non-nil, is called periodically with a snapshot of the
|
|
// running totals so callers can surface progress to the UI. The callback
|
|
// must not retain the pointer past its return.
|
|
func (p *HoldPDS) BackfillScanStatus(ctx context.Context, log func(format string, args ...any), progress func(*ScanBackfillResult)) (*ScanBackfillResult, error) {
|
|
if log == nil {
|
|
log = func(string, ...any) {}
|
|
}
|
|
if progress == nil {
|
|
progress = func(*ScanBackfillResult) {}
|
|
}
|
|
ri := p.RecordsIndex()
|
|
if ri == nil {
|
|
return nil, fmt.Errorf("records index not available")
|
|
}
|
|
|
|
const batchSize = 200
|
|
res := &ScanBackfillResult{}
|
|
var cursor string
|
|
|
|
for {
|
|
records, nextCursor, err := ri.ListRecords(atproto.ScanCollection, batchSize, cursor, true)
|
|
if err != nil {
|
|
return res, fmt.Errorf("list scan records: %w", err)
|
|
}
|
|
|
|
for _, rec := range records {
|
|
if err := ctx.Err(); err != nil {
|
|
return res, err
|
|
}
|
|
res.Scanned++
|
|
manifestDigest := "sha256:" + rec.Rkey
|
|
|
|
_, scanRecord, err := p.GetScanRecord(ctx, manifestDigest)
|
|
if err != nil {
|
|
log("skip rkey=%s: get failed: %v", rec.Rkey, err)
|
|
continue
|
|
}
|
|
|
|
// Already classified — nothing to do.
|
|
if scanRecord.Status != "" {
|
|
res.AlreadyTagged++
|
|
continue
|
|
}
|
|
// Real data present (legacy successful scan) — treat as ok, don't rewrite.
|
|
if scanRecord.SbomBlob != nil || scanRecord.Total != 0 {
|
|
res.AlreadyTagged++
|
|
continue
|
|
}
|
|
|
|
// Determine artifact type from layer media types.
|
|
layers, err := p.ListLayerRecordsForManifest(ctx, scanRecord.Manifest)
|
|
if err != nil {
|
|
log("skip rkey=%s: list layers failed: %v", rec.Rkey, err)
|
|
continue
|
|
}
|
|
|
|
skipped := false
|
|
for _, l := range layers {
|
|
for _, frag := range unscannableLayerMediaSubstrings {
|
|
if strings.Contains(l.MediaType, frag) {
|
|
skipped = true
|
|
break
|
|
}
|
|
}
|
|
if skipped {
|
|
break
|
|
}
|
|
}
|
|
|
|
var rewrite *atproto.ScanRecord
|
|
if skipped {
|
|
rewrite = atproto.NewSkippedScanRecord(
|
|
manifestDigest,
|
|
scanRecord.Repository,
|
|
scanRecord.UserDID,
|
|
"backfilled: unscannable artifact type",
|
|
scanRecord.ScannerVersion,
|
|
)
|
|
res.MarkedSkipped++
|
|
} else {
|
|
rewrite = atproto.NewFailedScanRecord(
|
|
manifestDigest,
|
|
scanRecord.Repository,
|
|
scanRecord.UserDID,
|
|
"backfilled: legacy record (no SBOM and zero counts)",
|
|
scanRecord.ScannerVersion,
|
|
)
|
|
res.MarkedFailed++
|
|
}
|
|
// Preserve original ScannedAt so the rescan timer doesn't reset
|
|
// and we don't lose audit signal.
|
|
if scanRecord.ScannedAt != "" {
|
|
rewrite.ScannedAt = scanRecord.ScannedAt
|
|
}
|
|
|
|
if _, _, err := p.CreateScanRecord(ctx, rewrite); err != nil {
|
|
log("rewrite rkey=%s failed: %v", rec.Rkey, err)
|
|
continue
|
|
}
|
|
res.Rewritten++
|
|
}
|
|
|
|
// Report progress at every batch boundary — gives the UI smooth-ish
|
|
// updates without locking the loop on every record.
|
|
progress(res)
|
|
|
|
if nextCursor == "" || len(records) == 0 {
|
|
break
|
|
}
|
|
cursor = nextCursor
|
|
}
|
|
|
|
progress(res)
|
|
return res, nil
|
|
}
|
|
|
|
// CreateScanRecord creates or updates a scan result record in the hold's PDS
|
|
// Uses a deterministic rkey based on the manifest digest, so re-scans upsert
|
|
func (p *HoldPDS) CreateScanRecord(ctx context.Context, record *atproto.ScanRecord) (string, cid.Cid, error) {
|
|
if record.Type != atproto.ScanCollection {
|
|
return "", cid.Undef, fmt.Errorf("invalid record type: %s", record.Type)
|
|
}
|
|
|
|
if record.Manifest == "" {
|
|
return "", cid.Undef, fmt.Errorf("manifest AT-URI is required")
|
|
}
|
|
|
|
// Extract the digest from the manifest AT-URI to use as rkey
|
|
manifestDigest, err := atproto.ParseManifestURI(record.Manifest)
|
|
if err != nil {
|
|
return "", cid.Undef, fmt.Errorf("invalid manifest AT-URI: %w", err)
|
|
}
|
|
rkey := atproto.ScanRecordKey(manifestDigest)
|
|
|
|
// Upsert: re-scans update the existing record
|
|
rpath, recordCID, _, err := p.repomgr.UpsertRecord(
|
|
ctx,
|
|
p.uid,
|
|
atproto.ScanCollection,
|
|
rkey,
|
|
record,
|
|
)
|
|
if err != nil {
|
|
return "", cid.Undef, fmt.Errorf("failed to upsert scan record: %w", err)
|
|
}
|
|
|
|
return rpath, recordCID, nil
|
|
}
|
|
|
|
// GetScanRecord retrieves a scan result record by manifest digest
|
|
func (p *HoldPDS) GetScanRecord(ctx context.Context, manifestDigest string) (cid.Cid, *atproto.ScanRecord, error) {
|
|
rkey := atproto.ScanRecordKey(manifestDigest)
|
|
|
|
recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, atproto.ScanCollection, rkey, cid.Undef)
|
|
if err != nil {
|
|
return cid.Undef, nil, fmt.Errorf("failed to get scan record: %w", err)
|
|
}
|
|
|
|
scanRecord, ok := val.(*atproto.ScanRecord)
|
|
if !ok {
|
|
return cid.Undef, nil, fmt.Errorf("unexpected type for scan record: %T", val)
|
|
}
|
|
|
|
return recordCID, scanRecord, nil
|
|
}
|