Skip to content

CycloneDX dependsOn: [] claims a component has no dependencies when we never learned its parentage #24

Description

@richardmhope

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

  1. 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.
  2. 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

  • No component receives dependsOn: [] unless its dependencies are genuinely known to be empty
  • A compositions entry declares the graph incomplete when any component's edges are unknown
  • A test asserts the absence of an empty dependsOn for a component with no recorded parents, in the spirit of test_table_never_reads_as_clean_when_nothing_could_be_checked
  • Still validates against the vendored schema — tests/test_report_formats.py picks this up once sample_report() covers a parentless component
  • The line 150 comment corrected, since it currently records a belief the schema contradicts

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions