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; there is noEnabledswitch (the pruner runs in every host that embeds the eventing chain and reclaims only a declared stream, SPRINT_20260916_001 RFC-14, owner ruling Q-42), and declaring the stream 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.
Registry source run retention
sbom.registry_source_runs holds one row per registry discovery run. A run moves from Queued or Running to Completed, Failed or Cancelled. Every reader returns at most the newest SbomService:RegistrySources:MaxRunHistoryLimit runs of one source, and that option is limited to 500. A terminal run ranked past 500 in its source can never be read again.
The host reclaims those runs. A background sweep runs shortly after startup and then hourly. It deletes terminal runs ranked past the newest 500 of their source, in bounded batches. Queued and Running runs are never deleted, whatever their rank. The last-run summary on sbom.registry_sources is stored separately and is not affected.
Settings bind from SbomService:RegistrySourceRunRetention:
Enabled(defaulttrue).RunsKeptPerSource(default 500, minimum 500). The minimum equals the largest readable history, so a configuration change cannot reveal a run that was already deleted. The database routine refuses a smaller value with SQLSTATE 22023.BatchSize(default 1,000) andMaximumBatchesPerPass(default 10).Interval(default one hour).MaximumRunRows(default 1,000,000). This is a diagnostic size budget. It never authorizes deleting an active run.
Doctor check doctor.sbomservice.retention.registry-run-backlog reports the total, active and past-window run counts. It is unhealthy when the sweep is disabled, when any terminal run is past the window, or when the table exceeds MaximumRunRows. The check counts rows with the same database function the sweep uses, so both use one predicate.
The routines are sbom.registry_source_run_retention_candidates(keep) and sbom.prune_registry_source_runs(keep, batch). They are created by startup migration 011_sbomservice_registry_run_retention.sql. Re-verify with the RegistrySourceRunRetentionTests class in StellaOps.SbomService.Persistence.Tests.
Retention classes
Every declared sbom table states its class in a forward COMMENT ON TABLE. Migration 010_sbomservice_retention_classes.sql covers 23 tables and migration 011_sbomservice_registry_run_retention.sql covers registry_source_runs. Each class was decided from the table’s writers and readers in the durable runtime, not from its name.
| Class | Tables | Why |
|---|---|---|
source-of-truth | ledger_versions, ledger_chains, ledger_audit, sbom_documents, analysis_jobs, lineage_edges, artifact_subjects, artifact_links, registry_sources, entrypoints, orchestrator_sources, orchestrator_control, watermarks | The platform writes these as the record and cannot rebuild them. Only the ledger has a reclamation policy: the version-count and optional age policy above. Audit rows, chain cursors and raw documents outlive pruned versions. |
source-of-truth | projections, catalog, component_lookups, inventory_events, resolver_candidates | No in-platform writer exists in the durable runtime. Rows arrive out-of-band and nothing can regenerate them, so they are retained as supplied. See the open gaps in the architecture dossier. |
derived-rebuildable | dependency_graphs, dependency_nodes, dependency_edges | Extracted from the retained raw document. The dependency-path sidecar backfill rebuilds a missing graph, and the rows follow their ledger version by cascade. |
derived-rebuildable | version_events, asset_events | Derived from stored projections. POST /internal/sbom/events/backfill replays version events, and reading a projection republishes its asset event. |
operational-state | registry_source_runs | A run lifecycle with a terminal-row sweep, a size budget and a Doctor check. See the section above. |
sbom.verdict_links is retired by migration 012_sbomservice_retire_verdict_links.sql. No deployable host ever registered its writer or reader. The migration drops the table only while it is empty; a populated table stops startup with SQLSTATE 55000 and its rows are kept.
Verification: retention classification receipt.
