A CycloneDX SBOM is a JSON document that describes the composition of a software artifact. Reading one well is a skill: the fields are documented, but the useful ones are scattered across five or six top-level keys, and most scanner output buries the information you actually need under noise.
This walkthrough covers the anatomy of a CycloneDX 1.6 JSON document — the top-level structure, the component record, the dependency graph, and the vulnerability block — and shows what each field tells you. The goal is to make you faster at triaging an SBOM handed to you in a PR review or a vendor assessment.
What is in a CycloneDX document at the top level?
A CycloneDX document has seven top-level objects that matter: bomFormat, specVersion, serialNumber, version, metadata, components, and (optionally) services, dependencies, vulnerabilities, and compositions. Every valid document starts with the same four bookkeeping fields; the rest is where the content lives.
Here is a trimmed real-looking example for a Node.js service:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2026-04-13T09:12:40Z",
"tools": {
"components": [{
"type": "application",
"name": "cdxgen",
"version": "10.9.3"
}]
},
"component": {
"type": "application",
"bom-ref": "pkg:npm/acme-checkout-api@2.14.0",
"name": "acme-checkout-api",
"version": "2.14.0",
"purl": "pkg:npm/acme-checkout-api@2.14.0"
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:npm/express@4.21.2",
"name": "express",
"version": "4.21.2",
"purl": "pkg:npm/express@4.21.2",
"hashes": [{ "alg": "SHA-256", "content": "a1b2c3..." }],
"licenses": [{ "license": { "id": "MIT" } }]
},
{
"type": "library",
"bom-ref": "pkg:npm/lodash@4.17.21",
"name": "lodash",
"version": "4.17.21",
"purl": "pkg:npm/lodash@4.17.21"
}
],
"dependencies": [
{
"ref": "pkg:npm/acme-checkout-api@2.14.0",
"dependsOn": [
"pkg:npm/express@4.21.2",
"pkg:npm/lodash@4.17.21"
]
}
]
}
That is the minimum-viable shape. Real SBOMs from production scanners typically run 500–5,000 components and include vulnerabilities, services, and richer metadata sections, but the skeleton is the same.
What do I actually look at first?
Start with metadata.component and metadata.tools, because those two fields tell you what this SBOM describes and how it was generated. Everything else is a function of those answers.
The metadata.component block identifies the subject of the SBOM. If you are reviewing an SBOM from a vendor, this is how you confirm you are looking at the product you think you are — name, version, and PURL should match the artifact you actually received. If the metadata.component is missing or generic (for example, just "name": "application" with no version), the SBOM was likely generated by a scanner run against an unidentified directory, and you should ask for a regenerated one with the metadata populated.
metadata.tools tells you which generator produced the document. This matters because different generators have meaningfully different coverage. Syft is strong on container images. cdxgen is strong on language ecosystems (npm, Maven, Python). Trivy focuses on OS packages. Knowing the generator tells you where you should and should not trust the component list — a Syft-generated SBOM of a Node.js container image, for example, typically captures the base image packages well but may miss application-level devDependencies depending on the configuration.
Also check metadata.timestamp against the artifact build time. An SBOM that was generated six months ago for an artifact you deployed last week is describing a different binary, and the mismatch is a reliable signal that someone is submitting a cached document instead of regenerating on each build.
What is the difference between a component hash and a PURL?
A PURL identifies what the component is supposed to be. A hash identifies what it actually is. The two answer different questions, and you need both to reason about supply chain integrity.
A PURL like pkg:npm/lodash@4.17.21 is a namespaced identifier that resolves through a registry — in this case, npm. It lets a scanner look up lodash 4.17.21 against vulnerability databases and know which CVEs apply. If the attacker swaps the actual node_modules/lodash/lodash.js contents with malicious code but preserves the name and version metadata, the PURL is unchanged and your vulnerability scanner is none the wiser.
A hash — typically SHA-256 in CycloneDX — captures the bytes of the actual component file or archive. If you received a vendor SBOM with hashes and reproduce the build yourself, matching hashes prove you got the same bytes the vendor shipped. Mismatched hashes prove tampering somewhere between their build and your installation.
The practical guidance: require hashes on components in any SBOM you rely on for attestation. A hashless SBOM is a manifest, not an attestation.
How do I trace a vulnerability back to a dependency?
You trace through the dependencies block by following ref values until you hit the root component. Every component has a bom-ref identifier; every dependencies entry is a directed edge in the dependency graph.
Suppose the vulnerabilities block reports CVE-2024-12345 affecting pkg:npm/ansi-regex@3.0.0. You want to know whether that dependency is a direct one or buried six levels deep — because a direct dependency you can update with a single package.json edit, while a deep transitive dependency may require waiting for an intermediate package to ship a fix or using an npm override.
The walk is straightforward. Find "ref": "pkg:npm/ansi-regex@3.0.0" as a dependsOn target in the dependencies array, which gives you its parent. Repeat with the parent until you reach the root (usually the metadata.component). That chain — for example, acme-checkout-api → yargs → string-width → ansi-regex — is your remediation path. The shorter the chain, the faster you can fix it.
A CycloneDX document with a well-populated dependencies array is dramatically more useful than one without. Some generators skip this block when the source project does not expose a resolvable lockfile; if you are consuming such an SBOM, know that every vulnerability in it is harder to triage.
What does the vulnerabilities block actually tell you?
The vulnerabilities block is where CycloneDX 1.4+ documents carry CVE data, VEX statements, or both. Each entry references one or more components via their bom-ref and attaches metadata — source (NVD, GHSA, OSV), ratings, references, and optionally an analysis block with VEX status and justification.
A useful vulnerability entry looks like this:
{
"id": "CVE-2024-12345",
"source": { "name": "NVD" },
"ratings": [{
"source": { "name": "NVD" },
"severity": "high",
"method": "CVSSv3",
"score": 7.5
}],
"affects": [{ "ref": "pkg:npm/ansi-regex@3.0.0" }],
"analysis": {
"state": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
}
The analysis.state is the VEX field, and it is the single most valuable piece of information in the block for triage. A not_affected with a justification is a decision you do not need to re-make. An affected or missing analysis means the entry is still open and belongs on somebody's queue.
Be skeptical of vulnerability blocks that are too clean. An SBOM with zero vulnerabilities reported for a 1,500-component Node.js service almost certainly means the generator did not look, not that there are none.
How does this compare to SPDX?
SPDX and CycloneDX encode the same core information with different structural choices. SPDX is older (originally license-focused) and uses a flatter structure with explicit relationships between elements; CycloneDX was designed security-first and has first-class support for vulnerabilities and VEX inside the SBOM.
If you control the format, CycloneDX is the easier choice in 2026 for security-driven use cases. If you are receiving SBOMs from a mixed fleet of vendors, expect both and use a converter where needed — both formats are feature-complete enough to round-trip the essentials. The vulnerability handling is the one place they differ materially: SPDX 3.0 added security profile support, but CycloneDX's vulnerability and VEX integration is more mature in tooling.
Either way, the reading skills transfer. Components, dependencies, hashes, and provenance are the same concepts wearing different JSON shapes.
How Safeguard.sh Helps
Safeguard.sh reads, writes, and cross-references CycloneDX and SPDX SBOMs as a first-class operation. Generate SBOMs on every build with complete metadata.component, hashes, and dependency graphs, or ingest vendor SBOMs into the TPRM module to run reachability and policy checks against them before you sign the contract. Reachability analysis filters the vulnerabilities block to the subset that is actually invoked by your code, cutting 60–80% of the CVE noise before it reaches a human reviewer. Griffin AI takes the remaining reachable vulnerabilities and autonomously produces patch PRs, updating the SBOM and VEX analysis together on every build. The scanner traces dependencies to 100 levels of depth so no component hides behind transitive indirection, and container self-healing keeps the SBOM current as base images drift.