Documentation menu

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 itCountNotes
library components68Every Alpine apk package in the image, with a pkg:apk/… PURL and a CPE
operating-system component1Alpine 3.21.3, the distro the PURLs are matched against
file components978Syft's file evidence (binaries and their digests). Not packages; a vulnerability matcher ignores them
dependencies edges62Which 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:

components[] — the nginx package, abridged
{
  "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:

bom.cdx.json
{
  "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

FieldRole
bomFormat / specVersionFormat detection and schema validation ("CycloneDX", "1.6")
metadata.componentThe subject of the SBOM — for StackRadar, the container image itself
components[]The inventory: every package found in the image (may be nested)
components[].purlPackage URL — the primary key for vulnerability matching
components[].cpeCPE identifier — stored for provenance where present; not used for matching yet
components[].bom-refDocument-internal ID that the dependency graph points at
dependencies[]Edges (refdependsOn) 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, or excluded.

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:

bash
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.json

Optional 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.

Uploads require a cluster-scoped API key — see API keys & cluster scoping.

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