In a CycloneDX SBOM, the dependencies list holds one entry per component. Each entry has a ref and a dependsOn array of other refs. It records direct dependencies only, so you rebuild the full tree by following the links. A component missing from the list has unknown dependencies, not zero.
How the pieces connect
Every component can carry a bom-ref, an identifier that must be unique within the file. The dependencies block uses those identifiers to draw edges. A small example:
"components": [
{ "bom-ref": "app", "type": "application", "name": "shop", "version": "2.1.0" },
{ "bom-ref": "pkg:npm/express@4.18.2", "type": "library", "name": "express", "version": "4.18.2" },
{ "bom-ref": "pkg:npm/qs@6.11.0", "type": "library", "name": "qs", "version": "6.11.0" }
],
"dependencies": [
{ "ref": "app", "dependsOn": ["pkg:npm/express@4.18.2"] },
{ "ref": "pkg:npm/express@4.18.2", "dependsOn": ["pkg:npm/qs@6.11.0"] },
{ "ref": "pkg:npm/qs@6.11.0", "dependsOn": [] }
]
Read it as: shop depends on express, express depends on qs, and qs depends on nothing. The last line is the important one.
Empty versus absent
The CycloneDX 1.6 reference is explicit. Components that have no dependencies of their own must be declared as empty elements in the graph. Components not represented in the graph may have unknown dependencies. The reference recommends consumers treat that as opaque, not as a sign the component is dependency free, and suggests using compositions to indicate unknown graphs.
That gives you two states that look similar but mean different things:
| Pattern | Meaning |
|---|---|
"dependsOn": [] | The author states this component has no dependencies |
| No entry at all | Dependencies are unknown or were not recorded |
The NTIA minimum elements report makes the same point from the policy side: the data should distinguish a component with no further dependencies from one where dependencies are unknown, and by default the data should be read as incomplete.
A reading routine
- Find the root. It is usually the component under
metadata.component. Locate its entry in the dependencies list. - Walk one level. List its
dependsOnrefs and look each one up incomponents. - Go deeper on what matters. You do not have to walk the whole tree. Pick the components you care about, such as a web framework or a parser, and trace upward to see which direct dependency pulls them in.
- Check for refs that match nothing. If a
dependsOnvalue does not match anybom-ref, the file is internally inconsistent. - Count entries versus components. If there are 400 components and 6 dependency entries, the graph is thin.
Useful jq commands
List components with no dependency entry:
jq -r '[.dependencies[].ref] as $r | .components[] | select([.["bom-ref"]] | inside($r) | not) | .name' bom.json
Show direct dependencies of the root:
jq '.dependencies[] | select(.ref == "app")' bom.json
Swap app for the real root ref from the file.
What you learn from a flat graph
If every component lists the root as its only parent, the generator probably read a lock file or a package list without resolving the tree. That is still useful as an inventory, but it will not tell you why a given library is present, which makes it harder to plan a fix.
Where Safeguard fits
Safeguard provides SBOM and dependency scanning with reachability analysis, which helps separate dependencies that are actually called from ones that are only present. A free scan is at gold.safeguard.sh/scan.
Quick check
Does dependsOn list all transitive dependencies?
No. Each entry lists the direct dependencies of one component. You follow the chain to see the rest.
What is a bom-ref?
It is an identifier for a component, unique within the file, used to refer to that component elsewhere in the same SBOM. Many tools use the PURL as the value.
Sources
Last reviewed: 19 October 2026.