SBOM adoption has accelerated since the US Executive Order 14028 on cybersecurity in 2021 established software bills of materials as an expected artifact for federal software vendors. Even outside the US federal procurement context, SBOMs have become a standard ask in vendor security reviews, enterprise procurement conversations, and supply chain compliance frameworks.
The challenge isn't generating a single SBOM for a single repository. Tools like Syft, CycloneDX CLI, and the various language-specific generators (pip-audit, cyclonedx-npm, cdxgen) can produce CycloneDX or SPDX formatted documents in minutes. The challenge is generating meaningful, accurate, and queryable SBOMs across an engineering estate of dozens or hundreds of repositories, with shared internal libraries, cross-service dependencies, and multiple ecosystems in play simultaneously.
What a Per-Repository SBOM Actually Captures
A per-repository SBOM generated from a single repository's manifest and lockfile captures the dependency graph for that repository in isolation. For a self-contained service with no shared internal libraries, that's reasonably complete. For most real engineering organizations, it's a partial picture.
The gap shows up with internal packages. If your organization has an internal authentication library published to a private registry, and 30 services depend on it, a per-repository SBOM for each service will list that library as a component. But the SBOM doesn't capture the internal library's own dependency graph, including its transitives and their CVE exposure. To get that, you need an SBOM for the internal library itself, and then a way to compose that SBOM into each downstream consumer's document.
SPDX 2.3 and CycloneDX 1.4 and higher both have mechanisms for linking SBOMs: SPDX's external document references and CycloneDX's component nesting and dependency graph sections. These features are well-specified in the standards. They're also frequently not used by teams that generate SBOMs per-repository without a coordinating layer, because the composition step requires knowing about the internal dependency graph that links the repositories together.
The Shared Library Fan-Out Problem
Shared internal libraries create a specific compounding problem. If @internal/http-client depends on a version of got that has a CVE, every service using @internal/http-client is transitively exposed. A per-repository SBOM for each service will list @internal/http-client as a component. It will not necessarily list got at the correct version, because that requires resolving the internal library's own lockfile.
Consider a scenario at a growing SaaS company with around 45 services: their security team is asked by a prospective enterprise customer to provide SBOM evidence as part of vendor security assessment. They generate SBOMs for all 45 services using a standard CI pipeline step. The resulting documents look complete and pass automated validation. But three weeks later, a CVE is published against a serialization library that's a transitive of their internal data utilities package. None of the 45 individual SBOMs listed the vulnerable component because the SBOM generation step didn't recurse into private registry packages. The customer's SBOM validation tool found no issue because the component simply wasn't there.
This isn't a failure of the SBOM format. SPDX and CycloneDX are capable of representing this relationship. It's a failure of the generation process to follow the dependency chain through private registries.
Generation Strategies for Multi-Repo Estates
There are a few practical approaches, with different coverage-versus-effort trade-offs.
Shallow per-repo generation with manual composition: Generate per-repository SBOMs in CI, then maintain a composition layer that stitches them together using SPDX external document references or CycloneDX dependency graph links. Accurate if maintained. Requires operational discipline to keep the composition layer current as services and their dependencies change. Works well for estates where the internal library graph is stable.
Deep resolution at the repo level: Configure the SBOM generator to resolve internal packages from the private registry rather than treating them as opaque components. This produces a fully-resolved SBOM per service that includes all transitives at every depth. The trade-off is that generation takes longer and requires registry access from CI, which may be more complex in certain security configurations. CycloneDX's cdxgen supports deep resolution for some ecosystems with the right configuration flags.
Centralized graph resolution with SBOM export: Maintain a centralized dependency graph across all repositories and generate SBOMs from that graph on demand. This is the approach Repohelm takes. The dependency graph is resolved continuously as repositories change, internal libraries are tracked by their published versions, and SBOMs can be generated at any point as a snapshot of the current graph state. This requires standing up the centralized graph infrastructure but gives you on-demand SBOM generation with full transitive coverage and an always-current picture of the estate.
Format Considerations: CycloneDX vs. SPDX
Both formats are mature and widely supported, but they have different strengths for different use cases.
CycloneDX is better suited for operational security use cases. Its dependency graph representation is more expressive, it has first-class support for vulnerability annotations (the VEX component format in CycloneDX 1.4+), and the tooling ecosystem for CycloneDX is generally more active for supply chain security applications. If you're generating SBOMs primarily to support security tooling and CVE analysis workflows, CycloneDX is the stronger choice.
SPDX has deeper traction in license compliance contexts and open source distribution scenarios. If your primary SBOM use case is license review and open source disclosure (SPDX was specifically designed for this by the Linux Foundation), SPDX is well-supported and widely understood by legal teams and compliance frameworks. Many organizations generate both formats from the same dependency graph to serve different audiences.
What SBOM Quality Actually Requires
The NTIA minimum elements for SBOM (supplier name, component name, version, unique identifier, dependency relationship, author of SBOM data, timestamp) set a floor that any reasonable generator satisfies. Meeting the minimum elements does not make an SBOM useful for security analysis.
For an SBOM to support meaningful CVE matching, the package identifier needs to be resolvable against NVD's CPE dictionary or OSV's ecosystem identifiers. This requires the package name and version to be in a format the matching tool recognizes, and it requires the full transitive graph to be present, not just the direct layer.
We're not saying per-repository SBOMs are useless. For simple services with few transitives and no shared internal libraries, they're sufficient and straightforward to maintain. The complexity scales with your estate's internal coupling and the accuracy requirement of your downstream consumers. Security teams doing real CVE analysis from SBOM data, or enterprise customers whose procurement process includes SBOM review, will find gaps in shallow SBOMs that accurate deep resolution would have prevented.