SbomService
The canonical SBOM repository for Stella Ops — storage, versioning, lineage, and the artifact association graph.
Status: Implemented Source: src/SbomService/ Owner: Scanner Guild
Purpose
SbomService is the system of record for Software Bills of Materials in the Stella Ops vulnerability scanner. It provides SBOM storage, versioning, and lineage tracking over a canonical repository that supports the CycloneDX and SPDX formats, including temporal (point-in-time and range) ledger queries, version diffing, dependency-path queries, and artifact lineage graph analysis.
The service also owns the artifact association graph for source snapshots, build runs, OCI image digests, SBOM documents, scan jobs, detector reports, findings, and evidence bundles. See artifact-links-api.md for the API contract and CLI/UI workflow.
Supported SBOM formats (validated at upload by
SbomNormalizationService/SbomUploadService): CycloneDX 1.4, 1.5, 1.6 and SPDX 2.3 and 3.0. Other versions are rejected with a validation error. Detection keys offbomFormat(CycloneDX) andspdxVersion(SPDX).
Components
Services:
StellaOps.SbomService- Main SBOM service host. Exposes the HTTP surface (ASP.NET Core minimal APIs) and business logic, wires persistence/lineage, and registers as the Stella Router microservicesbomservice.
Libraries:
StellaOps.SbomService.Contracts- BCL-only producer contract forsbom.uploaded/sbom.version.retiredv1 payloads and the bounded live-version snapshot. This is the only SbomService source project Findings may reference; no new consumer may copy an existing register-governed transitional edge to a SbomService implementation project.StellaOps.SbomService.Persistence- PostgreSQL persistence library (SbomServiceDataSource, catalog/component-lookup/entrypoint/orchestrator/projection repositories, embedded SQL migrations, startup auto-migration).StellaOps.SbomService.Lineage- Lineage domain library (EF CoreLineageDbContext, lineage-edge / verdict-link / VEX-delta entities and repositories, graph services).
Tests:
StellaOps.SbomService.Contracts.Tests- wire goldens and closed-graph proof.StellaOps.SbomService.Tests- Host/endpoint and service unit tests.StellaOps.SbomService.Persistence.Tests- PostgreSQL persistence integration tests.StellaOps.SbomService.Lineage.Tests- Lineage domain/service tests.
Note: the MVC controllers under
Controllers/(LineageController,LineageStreamController,RegistrySourceController,RegistryWebhookController) remain present in source, butProgram.csdoes not callAddControllers()/MapControllers(). The live HTTP surface is the set of minimal-API endpoints registered inProgram.cs. Registry-source management is live throughRegistrySourceEndpointExtensions.MapRegistrySourceEndpoints(...); lineage-stream controller routes remain controller-only until explicitly mapped.
Authentication & Authorization
SbomService validates Authority bearer tokens and Router-generated signed identity envelopes through the shared resource-server pipeline. Raw x-tenant-id/x-user-id headers cannot authenticate a request. Four named policies are defined in Auth/SbomPolicies.cs; each requires an authenticated principal plus its canonical scope:
Sbom.Read/sbom:read- tenant-scoped versions, ledger, lineage, projection, entrypoint, registry-source, and artifact-link reads.Sbom.Write/sbom:write- mutating endpoints (upload, entrypoints, registry sources, artifact-link status, replay verify).Sbom.Attest/sbom:attest- signed lineage evidence-pack export.Sbom.Internal/sbom:operate- installation-wide maintenance, tenant-scoped internal orchestrator control, and the legacy installation-wide catalog/component/path views until those schemas are tenantized.
The authenticated stellaops:tenant claim is the sole tenant authority. Existing body/query tenant fields are compatibility assertions only: omitted uses the authenticated tenant, equal is accepted, and mismatch is rejected. Authenticated subject identity also overrides caller-supplied actor fields.
API surface (minimal APIs in Program.cs)
All routes require the policy noted; health/OpenAPI are anonymous.
Health & metadata:
GET /healthz,GET /readyz(anonymous),GET /openapi/*(anonymous), build-info endpoint (viaBuildInfo).
SBOM upload & ledger:
POST /sbom/uploadandPOST /api/v1/sbom/upload(alias) - upload/ingest an SBOM (Write).GET /sbom/ledger/history|/sbom/ledger/point|/sbom/ledger/range|/sbom/ledger/diff|/sbom/ledger/lineage(Read).GET /api/v1/sbom/version-stream/events- ordered P6sbom.versionscatch-up with current epoch/head, retention horizon, and explicit bootstrap signal (Operate).GET /api/v1/sbom/version-stream/snapshot- bounded UUID-keyset snapshot at a fixed epoch/head fence; rows lacking exact document identity or carrying invalid closed-wire text, empty version/chain UUIDs, or nonpositive sequences are counted and withheld (Operate).POST|GET|DELETE /api/v1/sbom/version-stream/consumers[/{consumerId}]- register, report/list, or retire cross-database retention-floor participation (Operate). The five version-stream operations are mapped only on a durable host withSbomService:VersionStream:Enabled=true; they are absent (404) by default.
SBOM queries:
GET /console/sboms- installation-wide legacy catalog (Operate).GET /components/lookup- installation-wide legacy component lookup (Operate).GET /sbom/context- tenant timeline plus installation-wide dependency paths (Operate;501if no dependency-path backend).GET /sbom/paths- installation-wide dependency paths (Operate;501if no dependency-path backend).GET /sbom/versions- version timeline for an artifact (Read).GET /sboms/{snapshotId}/projection- normalized projection for a snapshot (Read).GET /entrypoints/POST /entrypoints- service entrypoints (Read/Write).
Artifact links (association graph):
POST /api/v1/artifact-links,POST /api/v1/artifact-links/import(Write).GET /api/v1/artifact-links/resolve,/unlinked,/candidates(Read).POST /api/v1/artifact-links/{linkId:guid}/confirm|/reject|/verify(Write).
Registry sources:
GET /api/v1/registry-sources,POST /api/v1/registry-sources,GET|PUT|DELETE /api/v1/registry-sources/{id}(Read/Write).POST /api/v1/registry-sources/test,POST /api/v1/registry-sources/{id}/test,POST /api/v1/registry-sources/{id}/trigger,POST /api/v1/registry-sources/{id}/pause,POST /api/v1/registry-sources/{id}/resume(Write).GET /api/v1/registry-sources/{id}/runsreturns run history with trigger type, counts, scan job ids, and matched images.GET /api/v1/registry-sources/{id}/discover/repositories,/discover/tags/{repository}, and/discover/imagesreturn discovery previews filtered by enabled mask records.POST /api/v1/registry-sources/{id}/discover-and-scandiscovers matching images and submits Scanner jobs immediately.GET /api/v1/registry-sources/{id}/imagesand/images/sbomquery matched image rows and SBOM status for operator review.- Enabled path/tag mask records require a registry write credential reference (
authRefUri/credentialRef) because matching images are expected to receive SBOM/CDX OCI referrer metadata. Missing write credentials fail connection tests and create/update preflight; stale sources without credentials record failed runs so the Web Schedule Health panel can show that nightly refresh is not working. - Mask records do not create independent schedules. The persisted schedule belongs to the registry source; removing a mask removes it from automatic mask-change/nightly selection, and removing all enabled masks makes the nightly refresh skip that source instead of leaving orphan mask work behind.
Lineage graph & compare:
GET /api/v1/lineage/{artifactDigest},/{artifactDigest}/children,/{artifactDigest}/parents(Read).GET /api/v1/lineage/diff,/api/v1/lineage/hover,/api/v1/lineage/compare(Read).POST /api/v1/lineage/diff/export(Attest;413over 50 MB,501if export backend unavailable).POST /api/v1/lineage/verify(Write),POST /api/v1/lineage/compare-drift(Read) - deterministic replay verification.POST /api/change-traces/build,GET /api/change-traces/{traceId}- change-trace compatibility shim (Read).GET /api/compare/baselines/{scanDigest}- baseline recommendation stub (Read).
Internal/operational (Sbom.Internal / sbom:operate):
GET /internal/sbom/events,/asset-events,/inventory,/resolver-feed,/resolver-feed/export(NDJSON),/ledger/audit,/analysis/jobs.POST /internal/sbom/events/backfill,/inventory/backfill,/resolver-feed/backfill,/retention/prune.GET|POST /internal/orchestrator/sources,/control,/watermarks.
Configuration
Configuration is bound from appsettings.json plus environment variables prefixed SBOM_. Keys live under the SbomService configuration root.
Key settings:
STELLAOPS_POSTGRES_SBOMSERVICE_CONNECTION- the canonical owner-database variable (CoC §8.2 / ADR-039). Read as a raw environment variable, so theSBOM_prefix does not apply, and resolved AHEAD of the key below; a present-but-blank value throws rather than falling back. The database isstellaops_sbomservice; the schema inside it issbom.SbomService:PostgreSQL(PostgresOptions, incl.ConnectionString,SchemaName) - durable store;ConnectionStringis the legacy rollback-compatibility key. It was removed from the running container at the 2026-08-22 W3-07 M4 cutover, while the source resolver temporarily continues to accept it for rollback; source-level retirement is a separate build/deploy follow-up. When no connection is set by either source, the service throws on startup unless an explicit local harness is enabled (SbomService:LocalHarness:Enabled=true, or theDevelopmentLocalHarness/TestingLocalHarnessenvironment); the canonical event, ledger, watermark, lineage, and registry-source stores are durable-only.SbomService:Ledger(SbomLedgerOptions) - retention:MaxVersionsPerArtifact(default 50),MaxAgeDays,MinVersionsToKeep(default 1).SbomService:VersionStream:Enabled- defaultsfalse. Until explicitly enabled it suppressessbom.versionsappends/fences, owner/pruner registration, and all stream/snapshot/consumer routes; durable ledger cursor and exact-document-digest state still converge for later bootstrap. SBOM 008 and shared Eventing schema migrations plus dormant reliability primitives may load at startup; they do not append, expose the owner API, mutate a consumer floor, or start retention.Eventing:OutboxRetention- P6 outbox window/cadence/batch controls. Once the source gate is enabled, the host declaressbom.versionsremotely consumed with a 24-hour registration lease and no publish-bit requirement; pruning remains disabled unless the operator also enables this section. The activation contract (every admission, lease and retention value explicit; source ships no numbers) and the WAL-reservation measuring procedure are inversion-stream.md; the compose key mapping isdevops/compose/docker-compose.sbomservice.ymlwith values in the operator’s git-ignored.env.SbomService:HoverCache(UseDistributed,Ttl) - lineage hover-card cache over the signed Router messaging cache factory (five-minute default TTL).SbomService:CompareCache(UseDistributed) - directional lineage compare cache over the same factory (ten-minute default TTL).SbomService:Concelier- retired 2026-09-16 (SPRINT_20260914_001VRP-7, register X19). The best-effort forward to Concelier/api/v1/learn/sbomis deleted with its options; the target host has been frozen undersrc/__Obsoleted/Concelier/since 2026-09-11. Uploads reach Findings through thesbom.versionsowner stream instead.SbomService:RegistryHttp,SbomService:ScannerHttp(HTTP client timeouts),SbomService:RegistrySources,SbomService:LineageExport(incl.StorageRootPathfor the content-addressed export blob store).SbomService:RegistrySources:NightlyRefreshEnabled,NightlyRefreshHourUtc,NightlyRefreshMinuteUtc, andNightlyRefreshPollSecondscontrol the registry-source nightly SBOM refresh hosted service. Defaults are enabled at 02:00 UTC with a 60-second poll interval.- Registry-source nightly failures are persisted in
/api/v1/registry-sources/{id}/runsand surfaced in the Web source detail Schedule Health panel. Sources with no enabled mask records are skipped by nightly refresh. - Local-harness fixture paths:
SbomService:ComponentLookupPath,SbomService:CatalogPath,SbomService:ProjectionsPath. - Authority/Router integration (
Routersection), CORS, and unified audit emission (events posted to the Timeline service).
Persistence
PostgreSQL schema sbom (SbomServiceDataSource.DefaultSchemaName = "sbom"). Migrations are embedded SQL applied via AddStartupMigrations(...) (forward-only, auto-migrate on startup):
001_v1_sbomservice_baseline.sql- collapsed pre-1.0 baseline covering catalog, lookups, entrypoints, orchestrator state, projections, lineage/verdict links, durable ledger/event/inventory/runtime state, registry-source tables, and artifact associations.002_registry_source_masks_and_run_reports.sql- paired registry mask records plus richer run reports (scan_job_ids,matched_images).003_sbom_documents_sidecar.sql- immutable digest-keyed raw document sidecar.004_ledger_versions_tenant_id.sql/005_ledger_versions_tenant_chain.sql- fail-closed tenant ownership and tenant-local version sequence uniqueness.006_catalog_component_lookup_tenant_id.sql- tenant-scoped derived catalog/lookups.007_dependency_edges.sql- tenant/version-keyed dependency graph rows.008_ledger_version_stream_fence.sql- nullable P6 epoch/sequence publication fence, exact-bytedocument_digest, and never-pruned(tenant_id, artifact_ref)chain-id/next-sequence cursor. Upgrade/reapply raises a lagging high-water mark without changing established chain identity; invalid legacy keys and sidecar-less rows are withheld rather than adopted or fabricated.
The source-only Lineage library retains migration 00001_InitialSchema.sql for audit and focused tests, but no production host or CLI module applies it. W3-07 / CM-2 retired its Platform authority on 2026-08-24; the live service uses only the host-owned lineage tables from the canonical persistence chain.
Observability
- Metrics meter
StellaOps.SbomService(SbomMetrics):sbom_paths_latency_seconds,sbom_paths_queries_total,sbom_timeline_latency_seconds,sbom_timeline_queries_total,sbom_projection_seconds,sbom_projection_size_bytes,sbom_projection_queries_total,sbom_events_backlog,sbom_orchestrator_control_updates,sbom_resolver_feed_published,sbom_ledger_uploads_total,sbom_ledger_diffs_total,sbom_ledger_retention_pruned_total. - Tracing ActivitySource
StellaOps.SbomService(SbomTracing); spans includeentrypoints.list/upsert,console.sboms,components.lookup,sbom.projection,lineage.graph/diff/hover/children/parents/export/compare/verify/compare-drift,events.list,inventory.list, etc. - A Grafana dashboard ships at
Observability/sbomservice-grafana-dashboard.json.
Dependencies
- PostgreSQL (schema:
sbom). - Authority/Router - validated bearer or signed-envelope identity and canonical scopes.
- Scanner - SBOM generation source (HTTP client
Scanner,SbomService:ScannerHttp). - Concelier - best-effort learn-on-upload forwarding to
/api/v1/learn/sbom(config-gated). - Timeline - unified audit emission target.
- Attestor - SBOM attestation integration.
- ExportCenter - SBOM export and distribution.
- Signed Router messaging cache factory (Valkey in the standard Compose profile) - distributed hover/compare caches when
UseDistributedis set. SBOM does not compose an independent Redis client or a Router-disabled fallback.
Related Documentation
- Architecture:
./architecture.md - API reference:
./api-reference.md - Version publication stream:
./version-stream.md - Artifact links API:
./artifact-links-api.md - Ledger & lineage:
./ledger-lineage.md,./lineage-ledger.md - SPDX 3 profile support:
./spdx3-profile-support.md - Scanner:
../scanner/ - Attestor:
../attestor/ - Data Schemas:
../../technical/DATA_SCHEMAS.md
Current Status
Implemented with a PostgreSQL storage backend (schema sbom). Supports SBOM ingestion, versioning (ledger with point-in-time/range/diff queries), artifact lineage graphs, deterministic replay verification, the artifact association graph, and a resolver/inventory feed for downstream policy/scanner consumers. The sbom.versions owner seam is implemented but intentionally default-off pending the P7 activation decision; FND-X18-3 also retains the separate pinned P19 sbomservice|integrations blocker. The host expects durable PostgreSQL-backed state for all canonical runtime stores; fixture-backed and in-memory repositories are injected only by explicit test/local-harness configuration.
