SPDX, the licence-first bill of materials
Where SPDX came from, how its document model is shaped, and why licence teams reach for it before security teams do.
Every source file in the Linux kernel carries a line like // SPDX-License-Identifier: GPL-2.0-only.
The kernel's licensing rules require that identifier, taken from the official list at
spdx.org/licenses and written in whatever comment syntax the file uses: // in C files, /* ... */ in headers, # in scripts.
The licence-identifier half of SPDX is load-bearing there, and it produces no document at all.
An SBOM, a software bill of materials, is a machine-readable list of every component inside a piece of software, with enough identifying detail that someone else can look each one up.
What an SBOM is for covers why you would want one.
SPDX is one of the formats people write that list in: fixed field names, a fixed vocabulary for how components relate, and a canonical registry of licence identifiers.
The registry came first and is still the half most projects depend on.
The acronym changed and almost nothing on the internet has caught up. SPDX was Software Package Data Exchange.
At version 3.0, released 15 April 2024, it became System Package Data Exchange, because the standard now describes AI models, datasets and build provenance too.
The 2.3 specification still carries the older wording, so you will meet both. SPDX's own timeline dates the start of drafting to February 2010, in a FOSSBazaar workgroup under the Linux Foundation.
The workgroup took the name SPDX, and its 1.0 specification shipped in August 2011.
The licence list is the part that got adopted
The SPDX License List is a registry of short identifiers such as Apache-2.0, MIT and
GPL-3.0-or-later, so that the tool on the other end matches a fixed string instead of parsing
"the Apache licence, version 2" out of prose. It is at version 3.28.0, released 20 February
2026, and new versions land on a roughly monthly cadence.
SPDX records a licence twice. licenseDeclared is what the upstream project says its licence
is. licenseConcluded is what the person or tool producing the document decided it actually is
after looking. That gap is what a licence audit is for. Either may legitimately be
NOASSERTION, SPDX's word for "I looked and I genuinely do not know", as distinct from leaving
the field out, which means "I did not look".
Most auto-generated SPDX files are full of NOASSERTION, and that is honest rather than broken.
What is actually in a document
An SPDX document has three parts you will meet in a generated file. The creation information at the top records the spec version, the document name and namespace, and who created it and when.
The package information names each component with an identifier and a download location, and optionally a version, a supplier, checksums, a declared and a concluded licence, and copyright text.
The relationships are typed links between elements: DESCRIBES, CONTAINS, DEPENDS_ON.
What is actually required
Far less of that is mandatory than the description suggests. At document level SPDX 2.3 requires seven things: SPDX Version, Data License, SPDX Identifier, Document Name, Document Namespace, Creator and Created.
At package level it requires three: Package Name, Package SPDX Identifier and Package Download Location. Version, file name and supplier are optional.
So is the checksum. A package may carry none, one or many of them, drawn from seventeen permitted algorithms, and when a package carries several the specification asks for SHA-1 first.
SHA-256 is in that list of seventeen, but it is not a field of its own.
The field people forget
The field people forget is documentNamespace, and forgetting it breaks the document. It is an
absolute URI: a globally unique identifier written like a web address, on a domain you control
and unique to this one document.
Two SBOMs that both contain SPDXRef-Package-express can then never be confused. Omit it and
the SPDX project's Python library raises rather than validating.
The published JSON Schema is looser: its list of required top-level fields does not mention
documentNamespace at all, so a document can pass a schema check and still fail the library.
Every element carries an SPDXID beginning with SPDXRef-, unique within the document, so
relationships have something to point at. SPDXRef-DOCUMENT is reserved for the document
itself.
A minimal document
A document with nothing decorative in it:
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "payment-service",
"documentNamespace": "https://example.com/spdx/payment-service-3f2a1c9e-7b4d-4a11-9c2e-5d8f0a6b1e33",
"creationInfo": {
"created": "2026-08-20T10:00:00Z",
"creators": [
"Organization: ExampleCorp",
"Tool: syft-1.51.1"
],
"licenseListVersion": "3.28.0"
},
"packages": [
{
"name": "express",
"SPDXID": "SPDXRef-Package-npm-express-4.19.2",
"versionInfo": "4.19.2",
"downloadLocation": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"filesAnalyzed": false,
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/express@4.19.2"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package-npm-express-4.19.2"
}
]
}The namespace, ExampleCorp and payment-service are stand-ins. Express 4.19.2 is a real
published release with a real MIT licence, pinned so you can check the values against the npm
registry.
Three of those fields need explaining. dataLicense licenses the metadata rather than the
software, and in SPDX 2.x it is effectively fixed at CC0-1.0.
filesAnalyzed: false says nobody walked the package's contents. At the default of true, the
document owes a package verification code: a single checksum computed over every file listed for
that package.
externalRefs is where identifiers from other systems go, and the purl entry in this document
is the one a vulnerability scanner will match on.
The ISO number covers less than you think
ISO/IEC 5962:2021 is real, current and 145 pages long. Its title, in the Information technology series, is "SPDX® Specification V2.2.1". It registers one version of SPDX and one only, 2.2.1; neither 2.3 nor 3.0 is in it.
SPDX 3.0 was submitted separately and sits at Draft International Standard stage as ISO/IEC DIS 5962, unpublished as of September 2026.
That matters because 2.2.1 is not the version anything produces. SPDX 2.3 shipped on 3 November 2022, and it is what GitHub's dependency-graph export, the SPDX Python validator and Syft (Anchore's open-source SBOM scanner) all speak.
The current stable specification is 3.0.1, published 17 December 2024, with v3.1-RC1 tagged in January 2026 as a prerelease.
So "SPDX is an ISO standard" and "the file my scanner just produced is an ISO-standard document" are two different sentences, and the second is usually false.
A procurement clause written against the ISO number will not match anything your scanner produces.
Syft takes a directory, an image or an archive and writes an SBOM in the format you name. Its
format strings show the gap: it offers spdx-json@2.2 and spdx-json@2.3 and nothing else, and
a bare spdx-json emits 2.3, so neither version it can write is the registered one.
The commands are in generating an SBOM from a container image with Syft.
OpenChain is a different number
The other ISO number that turns up in SPDX articles is OpenChain, correctly ISO/IEC 5230:2020, the OpenChain Specification in the same Information technology series, published December 2020. It specifies the requirements of a quality open-source licence compliance programme, and says it covers the what and why rather than the how or when. It does not mandate SPDX, and emitting SPDX files does not make an organisation conformant; conformance takes a programme audit or a self-certification.
The compliance baseline moved too
The compliance baseline people cite alongside these has also moved.
CISA, the US Cybersecurity and Infrastructure Security Agency, published the 2026 Minimum Elements for a Software Bill of Materials on 29 July 2026, together with the NSA, the FBI and fifteen international partner agencies. It replaces the 2021 elements from the National Telecommunications and Information Administration that nearly every SBOM article still cites, and adds new elements including Component Hash Algorithm, Component License, SBOM Tool Name and SBOM Generation Context.
If your checklist is from 2021, it is the wrong checklist.
Which version to target
Which version to target has three real answers. Each branch below pairs a version with the tooling that will produce or check it.
The profiles in that middle branch are SPDX 3.0's answer to having outgrown its own name. There are nine of them: Core, Software, Security, Licensing, Dataset, AI, Build, Lite and Extension.
The conformance clause is blunt: the Core Profile is mandatory, all others are optional. Local tooling has not followed.
The SPDX project's Python library, spdx-tools, reads 2.2 and 2.3 only; 0.8.x can write 3.0 but
cannot read it back. For 3.0 the browser validator at tools.spdx.org is the practical option.
Why the model is shaped for lawyers
The fields SPDX makes first-class say what it was built for.
There are two separate licence fields, one for what the project claims and one for what a reviewer concluded, plus a canonical identifier registry and a field of its own for copyright text.
Below the package level sit file-level records, and snippet records covering a byte range inside a single file, which exist because somebody pasted forty lines of GPL code into an Apache-licensed source file and a redistributor has to find it.
None of that is about vulnerabilities. It is about who is allowed to ship what.
The vulnerability half arrives through externalRefs, and specifically through a Package URL,
or purl. A purl is one string that names a package in any packaging system, shaped
pkg:type/namespace/name@version; in the document above, pkg:npm/express@4.19.2.
The scheme is the constant pkg, and only type and name are required. That string is what lets
a scanner match an SPDX entry against a public vulnerability database.
The purl format became a formal standard on 10 December 2025, when the General Assembly of Ecma International, the standards body, approved its first edition as ECMA-427.
Ecma's TC54 committee developed it, and it is now in process at ISO. Skip externalRefs and
your document is a licence artefact and nothing else.
CycloneDX is another format people write this list in. SPDX puts licensing first, and that is what decides which of its fields feel native and which feel bolted on.
What it will not do for you
An SBOM is not a tamper check
SPDX records checksums. It does not sign them, and the field is optional in the first place. An unsigned SPDX file can be edited as freely as the artefact it describes: change the hash, change the version, nobody notices. Integrity comes from signing the document as an attestation, a signed statement about a named artefact, and checking the recorded hashes against the real artefacts. It does not come from the format.
Nor does SPDX eliminate ambiguity. The specification ships an annex of licence-matching
guidelines because matching licence text to an identifier is hard, and NOASSERTION is a
permitted value throughout.
A generated SPDX file is a starting point for a licence review, not the result of one.
The format does not decide how much detail you record either. File-level records are on by
default: Syft's file.metadata.selection takes all, owned-by-package or none, and
defaults to owned-by-package. That setting is not a command-line flag. It lives in the
environment or in Syft's config file, which is why it is easy to miss.
SPDX's licence identifiers are settled, and used by a project that would notice within a day if they broke. Its document format is on its third major version, registered at ISO on 2.2.1 and produced by tooling at 2.3.
The two halves are in very different health, and the mistake is talking about them as one thing.