Skip to main content

SPDX SBOM Output

CVE Lite CLI can write an SPDX 2.3 Software Bill of Materials in JSON. SPDX is the format named in US Executive Order 14028 and the one most federal and regulated buyers ask for by name, alongside CycloneDX.

Both SBOM formats come from the same scan, so a single command gives you the inventory and the vulnerability picture together.

Generating an SPDX SBOM

cve-lite . --sbom spdx

This writes a timestamped file (cve-lite-scan-<timestamp>.spdx.json) to the current directory. Terminal output renders as normal.

--sbom accepts cyclonedx, spdx, and spdx2.3. Plain spdx means SPDX 2.3 today and will keep meaning 2.3, so a pinned CI command will not change format under you.

cve-lite . --sbom cyclonedx # CycloneDX 1.6
cve-lite . --cdx # the same thing, original flag, still supported

What the document contains

Every package from the scanned lockfile appears in packages, not just the vulnerable ones, so the document is a useful compliance artifact even on a clean scan.

SPDX fieldValue
packages[].name / versionInfoPackage name and resolved version
packages[].downloadLocationResolved tarball URL, or NOASSERTION
packages[].licenseDeclaredDeclared license, or NOASSERTION
packages[].licenseConcludedAlways NOASSERTION
packages[].externalRefs (PACKAGE-MANAGER)pkg:npm/<name>@<version>
packages[].externalRefs (SECURITY)One advisory reference per advisory, linking to OSV
packages[].annotationsSeverity, advisory count, and the fix command
packages[].supplierAlways NOASSERTION
packages[].filesAnalyzedAlways false
relationshipsDESCRIBES for the root project, DEPENDENCY_OF for each dependency edge

filesAnalyzed is false because the scan reads lockfile metadata and never opens package contents. Omitting the field would default it to true and claim otherwise.

supplier is NOASSERTION because a lockfile does not record who published a package. Guessing a supplier from the package name would be a fabrication, and SPDX provides NOASSERTION precisely for this case.

Dependency relationships and NTIA minimum elements

The NTIA minimum elements for an SBOM, which Executive Order 14028 points to, name seven required data fields: supplier name, component name, version, other unique identifiers, dependency relationship, author, and timestamp. The document carries all seven.

Dependency relationships are derived from the resolved dependency paths, so a transitive package is linked to the parent that actually pulled it in rather than to the project root.

How complete the graph is depends on your package manager.

For npm and pnpm projects, edges come directly from the resolved lockfile graph, so the graph is complete. Measured on @lit-internal/monorepo (npm, 2060 packages) and on two large pnpm monorepos, every package has a parent edge except the root project itself, which correctly has none.

For Yarn and Bun, edges are derived from recorded dependency paths instead, and the scanner keeps at most five paths per package as a deliberate bound on large trees. A package reachable by more than five routes will therefore have some edges missing, and one whose five recorded routes all run through packages excluded from the scan can end up without a parent edge. The package list is always complete; only the edges between packages can be partial.

If you need an exhaustive dependency graph on a Yarn or Bun project, that is a known limitation rather than a bug, tracked in #1109 and #1108.

Why licenses are sometimes NOASSERTION

npm records a declared license for every package in package-lock.json, so npm projects get real license data at no cost. pnpm, Yarn and Bun lockfiles do not carry licenses, so those projects show NOASSERTION, which is SPDX's explicit marker for "not determined" rather than a claim that no license exists.

licenseConcluded is always NOASSERTION. Concluding a license is a legal judgement about what a package actually is, not something a scanner can observe.

How vulnerabilities are represented

SPDX 2.3 has no vulnerabilities section and no way to express CVSS or a severity rating. Findings are therefore attached per package as SECURITY external references of type advisory, which is the mechanism the SPDX specification defines for this.

Severity and the runnable fix command go into a package annotation instead. They cannot go into an external reference, because referenceLocator must be a URI and a shell command is not one.

This means the SPDX overlay carries less than the CycloneDX one. If you need machine-readable severity ratings in the SBOM itself, use --sbom cyclonedx, which has a native vulnerability model.

Inventory-only mode

cve-lite . --sbom spdx --sbom-inventory-only

This omits the security references and annotations, leaving a pure inventory. Use it when the document feeds SBOM diffing, archival, or attestation, where you want the content to change only when your dependencies change, not when a new advisory is published.

The package list, licenses and relationships are then fully determined by the lockfile. Note that creationInfo.created and documentNamespace still differ between runs: SPDX requires both, and the namespace must be unique per document. SBOM comparison tools ignore those two fields by convention.

Combining with other outputs

--sbom can be combined with --json and --sarif. All files are written in one scan:

cve-lite . --sbom spdx --sarif --json

--sbom cannot be combined with --report or with --fix.

GitHub Actions integration

The OWASP/cve-lite-cli action does not expose an SPDX input yet; its cdx input covers CycloneDX only. Run the CLI directly for SPDX output:

- name: Generate SPDX SBOM
run: npx --yes cve-lite-cli@1 . --sbom spdx

- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: spdx-sbom
path: cve-lite-scan-*.spdx.json