CycloneDX SBOM format
A real, downloadable CycloneDX 1.6 SBOM example generated from a container image, with a field-by-field walkthrough: components, PURLs, the dependency graph, and what a scanner reads.
Why CycloneDX
CycloneDX is an OWASP-maintained SBOM standard (also published as ECMA-424) designed for security use cases. StackRadar generates and ingests CycloneDX 1.6 JSON. Uploads are validated against the official 1.6 JSON schema before ingestion, so malformed documents are rejected with a clear error rather than producing silently incomplete inventories.
A real CycloneDX SBOM example
The file below is a genuine CycloneDX 1.6 SBOM, generated with Syft 1.42.1 from the public docker.io/library/nginx:1.27-alpine image for linux/amd64 on 28 August 2026 — the same tool and the same output format the StackRadar scanner uses for every image it finds running in a cluster. Nothing has been edited or trimmed.
Download the example (404 KB JSON)
| What is in it | Count | Notes |
|---|---|---|
library components | 68 | Every Alpine apk package in the image, with a pkg:apk/… PURL and a CPE |
operating-system component | 1 | Alpine 3.21.3, the distro the PURLs are matched against |
file components | 978 | Syft's file evidence (binaries and their digests). Not packages; a vulnerability matcher ignores them |
dependencies edges | 62 | Which package depends on which, from the apk database |
Trimmed to the fields that matter, one of its 68 package entries looks like this — this is the record the vulnerability matcher actually reads:
{
"bom-ref": "pkg:apk/alpine/nginx@1.27.5-r1?arch=x86_64&distro=alpine-3.21.3&package-id=3c9251e68aafb845",
"type": "library",
"publisher": "NGINX Packaging <nginx-packaging@f5.com>",
"name": "nginx",
"version": "1.27.5-r1",
"description": "High performance web server",
"licenses": [{ "license": { "name": "2-clause" } }, { "license": { "name": "BSD-like" } }],
"cpe": "cpe:2.3:a:nginx:nginx:1.27.5-r1:*:*:*:*:*:*:*",
"purl": "pkg:apk/alpine/nginx@1.27.5-r1?arch=x86_64&distro=alpine-3.21.3",
"externalReferences": [{ "url": "https://nginx.org/", "type": "distribution" }],
"properties": [
{ "name": "syft:package:foundBy", "value": "apk-db-cataloger" },
{ "name": "syft:package:type", "value": "apk" },
{ "name": "syft:location:0:layerID", "value": "sha256:0d853d50b128aa460b47e7121849463a14b18d4fd976caf5014744aae24d28aa" },
{ "name": "syft:location:0:path", "value": "/lib/apk/db/installed" },
{ "name": "syft:metadata:installedSize", "value": "2710908" }
]
}Three things to notice. The purl carries the ecosystem (apk), the distro (alpine-3.21.3) and the exact package version, which is enough to evaluate every Alpine advisory's affected-range against it. The cpe is the fallback for advisories published only against NVD-style identifiers. And the properties record where the package was found (the layer digest and the path of the apk database), which is provenance you want when a finding is disputed.
A minimal example
Stripped to the skeleton, this is the shape of what the scanner uploads for a container image:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"component": {
"type": "container",
"name": "registry.example.com/shop/checkout",
"version": "sha256:9f2c…"
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:npm/express@4.19.2",
"name": "express",
"version": "4.19.2",
"purl": "pkg:npm/express@4.19.2",
"licenses": [{ "license": { "id": "MIT" } }]
},
{
"type": "library",
"bom-ref": "pkg:deb/debian/openssl@3.0.11-1~deb12u2",
"name": "openssl",
"version": "3.0.11-1~deb12u2",
"purl": "pkg:deb/debian/openssl@3.0.11-1~deb12u2?arch=amd64"
}
],
"dependencies": [
{
"ref": "pkg:npm/express@4.19.2",
"dependsOn": []
}
]
}The fields that matter
| Field | Role |
|---|---|
bomFormat / specVersion | Format detection and schema validation ("CycloneDX", "1.6") |
metadata.component | The subject of the SBOM — for StackRadar, the container image itself |
components[] | The inventory: every package found in the image (may be nested) |
components[].purl | Package URL — the primary key for vulnerability matching |
components[].cpe | CPE identifier — stored for provenance where present; not used for matching yet |
components[].bom-ref | Document-internal ID that the dependency graph points at |
dependencies[] | Edges (ref → dependsOn) used to tell direct dependencies from transitive ones |
What StackRadar extracts
On ingestion, components are collected from every location the spec allows them — components[], nested sub-components, metadata.component, and build tools declared in metadata.tools.components[] — and stored with:
- Identity: name, group, version, type, PURL, CPE.
- Provenance: publisher, author, description.
- Integrity: cryptographic hashes, external references.
- Compliance: license IDs and expressions (useful well beyond CVE tracking).
- Graph position: whether the component is a direct dependency or pulled in transitively, derived from
dependencies[].dependsOn. - Scope:
required,optional, orexcluded.
Deduplication
Each image (identified by its sha256 digest) has exactly one SBOM per cluster. Before pulling or scanning anything, the scanner asks GET /v1/sboms/check?imageDigest=… whether this cluster already has an SBOM for that digest and skips the whole scan if so — one image running in five namespaces is still one pull, one scan, one upload. A repeat upload for the same digest is answered with 409 Conflict unless you pass ?replace=true, which supersedes the existing SBOM (useful to recover from a bad scan).
Uploading SBOMs yourself
The upload endpoint is documented and curl-able, and it doesn't care who generated the document — anything producing valid CycloneDX 1.6 JSON works (Syft, Trivy, cdxgen, your CI pipeline). The only required parameter is the image digest the SBOM describes:
curl -X POST "https://api.stackradar.io/v1/sboms/upload/cyclonedx?imageDigest=sha256:<digest>" \
-H "X-API-Key: $STACKRADAR_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @bom.cdx.jsonOptional query parameters add context. imageRef, tag, registry, and repository describe the image — registry and repository are derived from imageRef when omitted. namespace, workloadName, workloadKind, containerName, and the JSON-object params namespaceLabels and workloadLabels attribute it to a workload, with one rule: namespace, workloadName, and containerName must be supplied together or omitted together — a half-specified workload is rejected with 400 rather than stored under a placeholder name. replace=true supersedes an existing SBOM for the digest.
What about SPDX?
SPDX documents are detected on upload but not currently ingested — CycloneDX 1.6 is the supported format. If you have an SPDX-producing toolchain, most SBOM generators (including Syft and Trivy) can emit CycloneDX from the same scan.
Next steps
- Vulnerability matchingHow StackRadar matches SBOM components against 900K+ OSV.dev advisories: PURL matching, version-range evaluation from npm to Debian and RPM, and confidence.
- Architecture & data flowHow the StackRadar scanner works: what runs in your cluster, exactly what data leaves it, how SBOMs are matched to vulnerabilities, and where data is stored.