Use when a team needs SBOMs for compliance (US EO 14028, EU CRA, FDA), SBOM-based vulnerability scanning or drift checks between releases. Generates CycloneDX or SPDX SBOMs with Syft from images or folders, scans them with Grype, diffs SBOMs in CI and explains how to choose a format.
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Two specification families dominate the SBOM landscape. CycloneDX is the OWASP-curated, security-focused format and this file's primary subject; SPDX is the Linux Foundation's license-focused standard, covered in spdx.md.
Per cyclonedx.org/specification/overview, CycloneDX's distinguishing features vs SPDX:
vulnerabilities[] block
with VEX-style status assertionsservices[] block describes service
endpoints + data flowsThis reference defines the schemas and tooling landscape; the main skill generates SBOMs in either format from real codebases.
| Signal | Pick |
|---|---|
| Security-focused consumer (vuln tracking, supply-chain attestation) | CycloneDX |
| US Federal procurement (NIST SP 800-218 + EO 14028 guidance) | SPDX (spdx.md) |
| License-compliance focus (richest license-expression vocabulary) | SPDX |
| Linux Foundation member organization workflow | SPDX |
| EU consumers / broad modern tooling | CycloneDX (most common) |
| Consumer dictates the format | Whatever the consumer requires |
When no consumer constraint exists, CycloneDX is the default here: its
embedded VEX (vulnerabilities[].analysis) feeds the triage workflow
directly. Generating both (Syft emits either) satisfies mixed audiences.
Note SPDX license IDs are the cross-format standard - even CycloneDX
license blocks use them.
Work the numbered Steps in order: generate the BOM (Steps 1-2), add a
VEX-style analysis record per triaged finding (Steps 3, 5), validate
against the schema (Step 4), then sign + attest in CI (Step 6). Pin the
specVersion your consumer agreed; bump version on each re-issue.
For an SPDX consumer, follow spdx.md
instead.
A minimal CycloneDX 1.6 BOM (JSON):
{
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2026-05-06T12:00:00Z",
"tools": [{"vendor": "anchore", "name": "syft", "version": "1.16.0"}],
"component": {
"type": "application",
"name": "my-app",
"version": "1.0.0",
"purl": "pkg:generic/my-app@1.0.0"
}
},
"components": [
{
"type": "library",
"bom-ref": "pkg:npm/lodash@4.17.20",
"name": "lodash",
"version": "4.17.20",
"purl": "pkg:npm/lodash@4.17.20",
"licenses": [{"license": {"id": "MIT"}}]
}
],
"dependencies": [
{
"ref": "pkg:generic/my-app@1.0.0",
"dependsOn": ["pkg:npm/lodash@4.17.20"]
}
]
}Per cdx-spec:
| Field | Required? | Use |
|---|---|---|
bomFormat | yes | Must be "CycloneDX" |
specVersion | yes | "1.6" (current) / "1.5" / "1.4" |
serialNumber | recommended | URN UUID identifying the BOM |
version | recommended | BOM revision (incremented per re-issue) |
metadata | recommended | Generation context (timestamp, tools, top-level component) |
components[] | required for non-empty BOMs | Inventory of dependencies |
dependencies[] | recommended | Dependency-graph edges via bom-ref |
services[] | optional | Hosted services (SaaS BOM) |
vulnerabilities[] | optional | Per-finding records |
formulation[] | optional | Build metadata |
Component types and per-language native generators are cataloged in
component-types-and-tooling.md;
the purl (Package URL) field is the canonical component identifier.
CycloneDX has first-class vuln support (unlike SPDX which delegates to companion files):
"vulnerabilities": [
{
"id": "CVE-2024-1234",
"source": {"name": "NVD", "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234"},
"ratings": [
{"source": {"name": "NVD"}, "severity": "critical", "method": "CVSSv3", "score": 9.8}
],
"cwes": [798],
"description": "Hardcoded credential in lodash sortBy function",
"affects": [{"ref": "pkg:npm/lodash@4.17.20"}],
"analysis": {
"state": "not_affected",
"justification": "code_not_present",
"detail": "Vulnerable function not exported in current build"
}
}
]The analysis.state field uses VEX-equivalent values:
resolved, resolved_with_pedigree, exploitable,
in_triage, false_positive, not_affected.
Validate a CycloneDX SBOM against the schema:
# Using cyclonedx-cli
cyclonedx validate --input-file sbom.json --input-version v1_6
# Or via npm
npx @cyclonedx/cyclonedx-bom validate sbom.jsonValidation catches structural issues (missing required fields,
invalid PURLs, unknown component types) before publishing. For SPDX
validation (pyspdxtools), see spdx.md.
The Step 3 vulnerabilities[] record IS embedded VEX (Vulnerability
Exploitability Exchange, CycloneDX 1.4+). To assert a triaged CVE does
not affect the shipped product, extend that same record - deltas only:
analysis.justification: "vulnerable_code_not_in_execute_path" with a
concrete analysis.detail (e.g. "vulnerable parser only invoked under
--debug; production builds disable it").analysis.response: ["will_not_fix"] - the planned action.This lets downstream consumers filter the false-positive finding instead of re-doing reachability analysis.
jobs:
cyclonedx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# Per-language native (recommended for richer SBOM)
- run: npx @cyclonedx/cyclonedx-npm --output-file=sbom.cyclonedx.json
# OR via Syft (broader source coverage)
- uses: anchore/sbom-action@v0
with:
format: cyclonedx-json
output-file: sbom.cyclonedx.json
# Validate
- run: cyclonedx validate --input-file sbom.cyclonedx.json --input-version v1_6
# Sign + attest
- run: cosign attest --predicate sbom.cyclonedx.json --type cyclonedx my-image:1.0A Node.js service must ship a CycloneDX SBOM to a security-focused customer. The team:
npx @cyclonedx/cyclonedx-npm --output-file=sbom.cyclonedx.json.
Output is bomFormat: "CycloneDX", specVersion: "1.6", with a
components[] entry per dependency (each carrying a purl) and a
dependencies[] edge list.CVE-2024-1234 on pkg:npm/lodash@4.17.20. Triage
shows the vulnerable function is not in the execute path, so the
team adds a vulnerabilities[] record with
analysis.state: "not_affected" and
justification: "vulnerable_code_not_in_execute_path" (Step 3).cyclonedx validate --input-file sbom.cyclonedx.json --input-version v1_6 - passes.cosign attest --predicate sbom.cyclonedx.json --type cyclonedx my-image:1.0.Result: a schema-valid, attested CycloneDX 1.6 BOM whose embedded VEX lets the customer drop the lodash finding without re-doing reachability analysis.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Skip serialNumber field | Can't deduplicate across re-generations | Generate URN UUID per BOM |
Use metadata.tools[] v1.4 schema in 1.6+ | Schema evolution; tools shape changed | Use metadata.tools.components[] (newer schema) |
Skip dependencies[] block | Loss of dep-graph info; downstream tools degrade | Always include (Step 1) |
| Hand-author CycloneDX | Schema is large; errors easy to introduce | Use generators (references) |
| Skip schema validation | Invalid SBOMs pass into prod; downstream consumers fail | Validate in CI (Step 4) |
| Pick format by tooling habit, not consumer | Consumer rejects the delivery | Format-choice table (Choosing a format) |
not_affected claims are worse than no claim.trivy-image, vex-author - sister skills