SBOM hosting — design
Class:
design· Function:sbom(usesregistryfor the artifact coordinate, the sharednornir-serviceserve loop for the gRPC surface, and modgunn for the SBOM schema). The whole-system view isdesign.md; the parity gap this closes isfeature-complete-gaps.md§6 (security / SBOM — an SBOM was generated in nornir but never hosted) and §11 (ops / misc — "SBOM hosting"), plusartifactory-parity-design.md§F. Sibling to the search verb, whose exact tri-surface shape it mirrors.
What this is
Until now an SBOM (Software Bill of Materials) was only generated — in nornir's security.rs, cargo-only — and never lived anywhere a consumer could pull it back. SBOM hosting makes holger store an SBOM attached to an artifact coordinate (repository, namespace/name/version) and serve it back byte-identical — an SBOM becomes a first-class servable artifact next to the package it describes.
It ships as server/lib/src/sbom.rs — a small on-disk store plus one serialization seam — behind the same three surfaces the search verb landed:
| Surface | Shape | Auth |
gRPC SbomService.AttachSbom |
attach a document to a coordinate | write (writer/admin, RBAC fail-closed, scoped to the repo) |
gRPC SbomService.FetchSbom |
fetch the document back | open read (NOT_FOUND when unattached) |
HTTP GET /-/sbom?repo=&namespace=&name=&version= |
fetch back (read twin of the RPC) | open read (404 when unattached) |
CLI holger-server sbom {attach,fetch} |
attach a file / fetch to a file | in-process (drives the same store) |
The chosen design
One coordinate-keyed content store, model consumed from modgunn.
Storage — a flat, coordinate-keyed file store
SbomStore roots at <data_root>/holger-sboms (a sibling of the promote PROD store + jera job ledger; data_root is the parent of the configured audit dir, so the gRPC endpoint, the HTTP gateway, and the CLI all derive one store from the config). Layout:
holger-sboms/<repo>/<coord>.sbom.json — the hosted SBOM document bytes
<repo> and <coord> are reduced to path-safe single segments by promote's sanitize_repo / coord_key (now pub(crate) and reused, not twinned — the same helpers that guard the promote refs), so an untrusted coordinate straight off the wire can never escape the store root. coord is sanitize(namespace)__sanitize(name)__sanitize(version).
- Attach is last-wins: an SBOM is regenerated over an artifact's life, so a re-attach overwrites (staged to a temp file + renamed, so a concurrent fetch never sees a half-written document). Returns the blake3 content address.
- Fetch is total: a coordinate with no attached SBOM returns
None(not-found), never an I/O error — the not-found path is the load-bearing, RED-when-broken behaviour each surface asserts independently.
Model — consumed from modgunn (Law #5)
The SBOM data model is not re-declared here. modgunn is the single home of the nordisk SBOM schema; sbom.rs re-exports modgunn::sbom::{SbomInput, SbomComponent, SbomVuln, to_cyclonedx, purl} verbatim and adds exactly one thin seam:
pub fn build_cyclonedx(input: &SbomInput) -> Vec<u8> // = serde_json::to_vec(modgunn::sbom::to_cyclonedx(input))
A producer holding a ready CycloneDX document attaches its raw bytes directly; a producer holding the structured model builds through build_cyclonedx here. Either way holger emits modgunn's CycloneDX 1.5 verbatim and can never fork the schema — a unit test asserts build_cyclonedx == modgunn::sbom::to_cyclonedx byte-for-byte (RED-when-broken if the model is ever twinned).
Why store raw bytes, not a re-parsed struct
The servable object is the document. Storing the exact attached bytes makes the round-trip byte-identical by construction (a producer signs/hashes the bytes it will get back), keeps the store schema-agnostic (a future CycloneDX/SPDX version needs no store change), and keeps holger a host, not a re-serializer.
Why attach is gRPC/CLI-only, fetch is also HTTP
Mirrors search + the rest of holger: writes go through the authenticated gRPC surface (and the in-process CLI); the HTTP door is a read-only convenience for HTTP-only consumers, reserved under /-/ so it never collides with a repository name. Same posture as GET /-/search.
Alternatives rejected
- A new backend / repository type per SBOM — heavyweight; an SBOM is metadata attached to an existing coordinate, not an artifact ecosystem of its own.
- Re-parsing into a stored struct — would break byte-identity and couple the store to one SBOM schema version. (Kept in reasoning.)
- A holger-local SBOM struct — violates L5; the model lives in modgunn.
Observability
Every surface emits a gated #[cfg(feature="testmatrix")] functional_status cell (holger-grpc/attach_sbom, holger-grpc/fetch_sbom, holger-http/sbom, holger-cli/sbom) and audits attach as an Upload, fetch as a Download (including the 404). Tests: 6 store unit + 2 gRPC RPC + 2 HTTP door, each with a RED-when-broken not-found / wrong-coordinate guard.