Found while assessing the SBOM output against CISA's 2026 Minimum Elements for an SBOM (full assessment in docs/cisa-sbom-minimum-elements-2026.md on branch claude/tool-sbom-requirements-assessment-jkudvt). Most of that assessment is missing fields. This one is a defect in what we already emit, and it is the false clean this codebase exists to prevent, in the one format designed to be read by another machine.
The defect
_dependency_graph (src/icebergsca/report/cyclonedx.py:133) builds edges from recorded parents, then at lines 150–152:
# Every component needs an entry, even an empty one, for the graph to be valid.
for dep in report.dependencies:
children.setdefault(dep.ref.purl, [])
Every component therefore gets an entry, and every entry gets "dependsOn": sorted(dependencies) — which is [] when no edges were recorded.
CycloneDX defines an empty dependsOn as a positive claim. From the vendored schema, tests/schemas/bom-1.6.schema.json:1836:
Components or services that do not have their own dependencies must be declared as empty elements within the graph. Components or services that are not represented in the dependency graph may have unknown dependencies. It is recommended that implementations assume this to be opaque and not an indicator of an object being dependency-free. It is recommended to leverage compositions to indicate unknown dependency graphs.
So dependsOn: [] means dependency-free; absence from the graph means unknown. We emit the former for the latter.
Two things make this a clear-cut bug rather than a judgement call:
- The comment on line 150 asserts the entry is needed "for the graph to be valid". It is not — the schema explicitly defines what absence means, and validation passes without it.
- The function's own docstring already commits to the right behaviour (line 136): "Formats that record no edges simply contribute fewer entries rather than a fabricated flat tree." The loop three lines below contributes exactly those entries.
What is affected
Any component whose parents is empty. The model is already correct and explicit about what that means — core/models.py:280-282:
Only populated for lockfiles that record edges; empty is "we don't know", never "nothing depends on it".
The renderer collapses that distinction. It applies to a component's children rather than its parents, but the same information gap drives both: if we never learned the edge, we cannot say either end of it is empty. A direct dependency is not exempt — it hangs off root correctly and still gets a dependsOn: [] of its own.
Suggested fix
- Emit a
dependencies entry only for components whose outgoing edges are actually known. Drop the setdefault loop, and keep root plus any node with recorded children.
- Declare
compositions — the remedy the specification itself names. tests/schemas/bom-1.6.schema.json:2224 defines it, with an aggregate enumeration of complete / incomplete / incomplete_first_party_only / unknown / not_specified. An incomplete aggregate scoped to the affected refs is a truthful statement about a partially-known graph, which is the thing we currently have no way to say.
compositions is also the natural home for the coverage disclosures in the companion issue, so whoever takes this should read both before choosing the shape.
Done when
Found while assessing the SBOM output against CISA's 2026 Minimum Elements for an SBOM (full assessment in
docs/cisa-sbom-minimum-elements-2026.mdon branchclaude/tool-sbom-requirements-assessment-jkudvt). Most of that assessment is missing fields. This one is a defect in what we already emit, and it is the false clean this codebase exists to prevent, in the one format designed to be read by another machine.The defect
_dependency_graph(src/icebergsca/report/cyclonedx.py:133) builds edges from recorded parents, then at lines 150–152:Every component therefore gets an entry, and every entry gets
"dependsOn": sorted(dependencies)— which is[]when no edges were recorded.CycloneDX defines an empty
dependsOnas a positive claim. From the vendored schema,tests/schemas/bom-1.6.schema.json:1836:So
dependsOn: []means dependency-free; absence from the graph means unknown. We emit the former for the latter.Two things make this a clear-cut bug rather than a judgement call:
What is affected
Any component whose
parentsis empty. The model is already correct and explicit about what that means —core/models.py:280-282:The renderer collapses that distinction. It applies to a component's children rather than its parents, but the same information gap drives both: if we never learned the edge, we cannot say either end of it is empty. A direct dependency is not exempt — it hangs off
rootcorrectly and still gets adependsOn: []of its own.Suggested fix
dependenciesentry only for components whose outgoing edges are actually known. Drop thesetdefaultloop, and keeprootplus any node with recorded children.compositions— the remedy the specification itself names.tests/schemas/bom-1.6.schema.json:2224defines it, with anaggregateenumeration ofcomplete/incomplete/incomplete_first_party_only/unknown/not_specified. Anincompleteaggregate scoped to the affected refs is a truthful statement about a partially-known graph, which is the thing we currently have no way to say.compositionsis also the natural home for the coverage disclosures in the companion issue, so whoever takes this should read both before choosing the shape.Done when
dependsOn: []unless its dependencies are genuinely known to be emptycompositionsentry declares the graph incomplete when any component's edges are unknowndependsOnfor a component with no recorded parents, in the spirit oftest_table_never_reads_as_clean_when_nothing_could_be_checkedtests/test_report_formats.pypicks this up oncesample_report()covers a parentless component