ADR 0015: CycloneDX 1.7 as the SBoM Standard¶
Status¶
Accepted
Date: 2026-05-27
Source design lives in plans/sbom-ingest.md.
Context¶
Imbi attributes third-party software usage to a project's Release
via the Release -[:USES_COMPONENT_RELEASE]-> ComponentRelease edge
(see ReleaseComponentEdge in
imbi-common/src/imbi.common/models.py). To populate that edge we
ingest Software Bills of Materials (SBoMs) produced by build CI and
push the resulting Component / ComponentRelease /
ComponentIdentifier nodes through a single
PUT /organizations/{org}/projects/{project_id}/releases/{release_id}/sbom
endpoint.
Two questions had to be settled before the ingest pipeline could be written:
- Which SBoM spec version do we accept on the wire? CycloneDX 1.5, 1.6, and 1.7 are all in the wild. Each release strictly expanded the schema; 1.7 added fields Imbi reads (notably per-component properties used to carry dependency-group attribution in the cdxgen output). Accepting earlier versions would silently drop those fields on the floor.
- Which producer do we recommend in build CI? Imbi's normalizer
only sees what the producer emitted. We need a single tool that
covers our polyglot service population (uv-based Python, npm /
yarn / pnpm JS / TS, Maven / Gradle Java, Go) and that emits the
per-component metadata required to populate
scopeandgroupsonReleaseComponentEdge.
We evaluated cyclonedx-py (the cyclonedx-bom PyPI package) for
the Python side and found it does not handle PEP 735
[dependency-groups] at all — dev-vs-runtime attribution is simply
lost for uv and pdm projects. We also considered an ecosystem-
per-tool fleet (cyclonedx-py for Python, cyclonedx-npm for JS, the
Maven plugin for Java, etc.) but rejected it because each tool
encodes group / scope metadata in a different shape, which would
push ecosystem-specific knowledge into the Imbi normalizer. cdxgen
(https://github.com/CycloneDX/cdxgen) is the only common tool we
found that emits CycloneDX 1.7 with consistent, useful
per-component metadata across the languages we care about.
Decision¶
1. CycloneDX 1.7 is the only spec version Imbi accepts¶
The ingest endpoint enforces specVersion == "1.7" and rejects
anything else with 415 Unsupported Media Type. Implementation:
imbi-api/src/imbi.api/sbom.pydeclaresSUPPORTED_SPEC_VERSION: typing.Final = '1.7'.- A version mismatch raises
UnsupportedSpecVersionErrorfromparse(), which the endpoint maps to HTTP 415.
We deliberately do not coerce 1.5 / 1.6 payloads up to 1.7. The later schema introduced fields we depend on; silent up-conversion would produce CycloneDX documents that look 1.7-shaped but are missing data the normalizer expects.
2. cdxgen is the recommended producer¶
cdxgen (https://github.com/CycloneDX/cdxgen) is the SBoM producer
Imbi documents in service-onboarding guidance for build CI. It
covers the languages currently in the fleet from a single
invocation, and — critically — exposes per-component metadata that
Imbi reads to populate ReleaseComponentEdge:
component.scope(required/optional/excluded) is defined by CycloneDX itself and is universal across ecosystems. Imbi stores it verbatim asReleaseComponentEdge.scope, mappingNoneto "producer did not declare a scope."component.properties[].name == "cdx:pyproject:group"is cdxgen's encoding for both PEP 735[dependency-groups]and Poetry[tool.poetry.group.X]. Imbi flattens these intoReleaseComponentEdge.groups, sorted and de-duplicated at ingest time so equality comparisons across releases are stable.
Python is currently the only ecosystem that emits named dependency
groups; the Imbi parser maintains a curated allow-list of property
names rather than ingesting every cdxgen property by default. As
cdxgen adds equivalent group support for other ecosystems (e.g. npm
devDependencies groups, Maven scopes-as-groups), the allow-list
grows — the consuming graph contract does not change.
3. cyclonedx-python-lib is the parser, not the producer¶
Imbi parses incoming SBoMs with the cyclonedx-python-lib
PyPI package, pinned >=11.7,<12 in imbi-api/pyproject.toml.
This is an entirely separate concern from the producer choice:
- The library gives us a typed, validated CycloneDX 1.7 object
graph to project into
NormalizedComponentrecords. - It runs server-side, on whatever payload the producer emitted. We could swap producers tomorrow and the parser would not move.
The two choices are recorded together in this ADR only because
they are easy to conflate in conversation; conflating them in
code (e.g., gating ingestion on User-Agent: cdxgen/*) would be
a mistake.
Consequences¶
Positive¶
- The producer side and consumer side share one schema version, so every field cdxgen emits maps onto a field Imbi understands. No silent data loss on dev-group attribution for Python.
- The Imbi normalizer stays ecosystem-agnostic. It reads
component.scopeand one curated set of property names; it does not branch onpurltype to decide which fields exist. - A single recommended producer simplifies onboarding docs and CI
templates. Service teams get one
cdxgen … -o cyclonedx.jsoninvocation, not a per-language matrix. - The parser is library-pinned and free to evolve independently of build CI; producer upgrades don't force coordinated API releases.
Negative¶
- Build CI is on the hook for keeping cdxgen current. cdxgen is
active upstream but not run by us; an upstream regression in
group emission would degrade attribution quietly until someone
noticed
groupsarrays going empty. - Rejecting 1.5 / 1.6 means any team running an older cdxgen (or a
different tool that only emits 1.6) gets a hard
415instead of a partial ingest. This is intentional — partial ingest is the failure mode this ADR exists to avoid — but it does shift the fix-it work onto the producer side. - We are tied to one producer's property naming convention
(
cdx:pyproject:group) for dependency-group attribution. If cdxgen renames or restructures the property, the Imbi normalizer's allow-list must move with it. That coupling is the price of ecosystem-agnostic ingest.
Risks Accepted¶
- Multi-version compatibility: not a goal. We will not run a 1.5 / 1.6 compatibility shim. Producers that cannot emit 1.7 are not supported producers.
- Producer monoculture: we are recommending a single tool. A team is free to emit CycloneDX 1.7 from a different producer (the parser does not care), but they lose dependency-group attribution unless that producer happens to emit the same property names. This is acceptable because the alternative — matrixing the normalizer over every producer's metadata convention — pushes ecosystem branching into core.
- bom-ref persistence: not done. CycloneDX
bom-refis a per-SBoM internal cross-reference, not a stable component identity, and the parser excludes it from_IDENTIFIER_KINDSinimbi-api/src/imbi.api/sbom.py. Anyone tempted to "just persist the bom-ref" should re-read the comment there before doing so.
References¶
plans/sbom-ingest.md— Full ingest plan, including the cdxgen evaluation and the curated property allow-list.imbi-api/src/imbi.api/sbom.py—SUPPORTED_SPEC_VERSION,UnsupportedSpecVersionError, and the parse / upsert pipeline.imbi-common/src/imbi.common/models.py—Component,ComponentRelease,ComponentIdentifier, andReleaseComponentEdge(the graph contract this ADR populates).- CycloneDX 1.7 specification
- cdxgen — recommended SBoM producer.
- cyclonedx-python-lib — server-side parser.