SBOM ledger retention policy
Purpose
Retention keeps ledger history bounded while preserving audit trails for compliance.
Configuration
Settings are bound from SbomService:Ledger (env prefix SBOM_SbomService__Ledger__):
MaxVersionsPerArtifact: max ledger versions retained per artifact (default 50).MaxAgeDays: prune versions older than N days (0 disables age pruning).MinVersionsToKeep: minimum versions always retained per artifact.
Operations
POST /internal/sbom/retention/pruneapplies retention rules and returns a summary.GET /internal/sbom/ledger/audit?artifact=<ref>returns audit entries for create/prune actions.
Guarantees
- Audit entries are append-only and preserved even when versions are pruned.
- Deterministic ordering is used when selecting versions to prune.
- Enumeration, policy evaluation, row locking, and removal use the same exact
(tenant_id, artifact_ref)key. Invalid legacy keys are not enumerated, and tenants sharing one artifact cannot prune one another. A delete race returning zero does not count/log a touched chain. - The never-pruned
sbom.ledger_chainscursor preserves chain identity and its high-water sequence after pruning a middle version or every live version. - A tenant-bearing version is locked before retirement. Its
sbom.version.retiredv1 P6 tombstone, physical ledger removal, and prune audit row commit in one owner-database transaction. A retry against an already-removed version is a no-op and cannot resurrect it.
Version-stream outbox retention
Ledger-version retention above and P6 envelope retention are separate bounds. The host declares sbom.versions as RemotelyConsumed only after SbomService:VersionStream:Enabled=true, because Findings checkpoints in its own physical database:
- the source gate defaults false and omits producer append/fence, owner/pruner registration, and all stream/snapshot/consumer routes; ordinary ledger retention remains available;
- operator knobs bind from
Eventing:OutboxRetention; its defaultEnabled=falsechanges nothing, and activation requires explicitWindow,BatchSize,MaxBatchesPerPass,Interval,RequirePublished=false, andPruneSupersededEpochsvalues; RequirePublished=falsebecause pull catch-up is the delivery authority and no transport publisher writespublished_atfor this stream;- an active remote registration holds the floor at its last validated epoch/sequence report;
- the producer owns the explicitly configured
SbomService:VersionStream:RemoteConsumerLease. A lapsed/absent registration no longer pins the floor, while the configured replay window still prevents early deletion; - reports for a stale epoch or above the committed head are rejected. Duplicate or lower reports in the current epoch cannot move durable progress backwards.
Consumers below retentionHorizonSeq receive requiresBootstrap=true and restart from the bounded live-version snapshot before resuming ordered catch-up. See version stream.
Activation validates the complete setup-owned P7 admission/retention contract before registering the producer or pruner. Missing/invalid values stop startup; no numeric deployment value is supplied by source. Enabled upload/retirement writes pause with typed 503 reasons before the configured row, WAL, or database-capacity threshold. See version stream.
