Building a CISA 2026 conformant SBOM by hand: everything you do without monsys
The 2026 Minimum Elements turned an SBOM into a signed, licensed, machine-verifiable artifact. Here is the full manual pipeline for one Linux host: inventory three layers, map every required field, canonicalize and sign with Ed25519, then derive VEX. It is a lot.
Our last post covered what the CISA 2026 Minimum Elements require. This is the honest sequel: if you do not have monsys, here is what it actually takes to produce a conformant SBOM for a single machine, by hand. Do it once and you understand why "just generate an SBOM" is not a checkbox.
We will build one for a Linux host. Windows is similar in shape, different in tooling.
Step 1: inventory three layers, not one
A machine is not just its application dependencies. A conformant host SBOM covers everything that can carry a CVE:
- Application dependencies (lockfiles)
- OS packages (the distribution's package database)
- The running kernel
Application dependencies
Parse each ecosystem's lockfile: package-lock.json, requirements.txt, composer.lock, go.sum, and so on. Tools like Syft help here, but you still own merging the results into one document and reconciling versions.
OS packages
Query the package manager, one syntax per distro family.
Debian / Ubuntu:
dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n'
RPM (RHEL / Fedora / Rocky / Alma):
rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\t%{LICENSE}\n'
Alpine:
apk info -v
Now turn every line into a Package URL. openssl 3.0.13 on Ubuntu 22.04 becomes:
pkg:deb/ubuntu/openssl@3.0.13?arch=amd64&distro=ubuntu-22.04
Get the qualifiers wrong and downstream CVE matching silently misses.
The running kernel
uname -r # 6.8.0-100-generic
uname -m # x86_64
Which becomes pkg:deb/ubuntu/linux-kernel@6.8.0-100-generic?arch=x86_64&distro=ubuntu. Then tag it as an operating-system component (CycloneDX type, SPDX primaryPackagePurpose), or a scanner treats your kernel like an npm library.
Step 2: fill every required 2026 field
The standard is not "a list of names". Per component you now owe:
- Component Producer. Map each distribution to a real producer: Ubuntu to Canonical Ltd., Debian to the Debian Project, Alpine to Alpine Linux, and so on. Where you genuinely cannot tell, you must write
NOASSERTION, not guess. - Component Identifiers. At least one purl or CPE, as above.
- Component License. An SPDX license identifier. On Debian that means parsing
/usr/share/doc/<pkg>/copyright, which is free text, and mapping it to an SPDX ID. On RPM you get%{LICENSE}, which is closer but still not always a valid SPDX expression. - Component Hash + Algorithm. A hash of the artifact, with an IANA-registered algorithm name. Here is the trap: a metadata inventory does not have the original artifact bytes. You either download and hash every artifact, or you record the hash as genuinely unknown. Fabricating one is worse than omitting it, and the standard's "Explicitly Identifying Unknown Information" element requires you to say so honestly.
Then the document-level metadata: Author, Author Signature (Step 3), Data Format Name and Version, Timestamp (RFC 9557 / RFC 3339), Tool Name and Version, Generation Context (before / during / after build), and an SBOM Version using semantic versioning.
Step 3: sign it so anyone can verify
This is the part most homemade SBOMs skip, and it is now mandatory.
- Generate a key:
openssl genpkey -algorithm ed25519 -out sbom-key.pem
- Canonicalize the document with RFC 8785 (JSON Canonicalization Scheme), with the signature holder removed. There is no
opensslflag for this. JCS sorts object keys by UTF-16 code unit, uses minimal escaping, and has a specific number format. You write or import a JCS library, and if you sign the wrong bytes, every downstream verifier rejects the signature. - Sign the canonical bytes with the Ed25519 key.
- Embed the signature. CycloneDX has a native JSF
signatureblock; SPDX has no signature field, so you carry it in a document-level annotation. - Publish the public key somewhere fetchable, so a third party can re-canonicalize the file, fetch your key, and verify it offline.
Miss any of these five and you have a signature nobody else can check, which is the same as no signature.
Step 4: derive VEX, because a list is not the goal
The 2026 standard adds a section on correlating SBOMs with security advisories, naming VEX and CSAF explicitly. So the inventory is only half the deliverable. For every component you now:
- Match it against a vulnerability source. OSV.dev takes batched
(ecosystem, name, version)triplets; NVD matches by CPE. - Decide a status per CVE:
affected,not_affected,fixed, orunder_investigation. - Justify it. OpenVEX requires an
action_statementforaffectedand a justification orimpact_statementfornot_affected. A backported kernel fix reads as "fixed" even though the version string still looks vulnerable, so you need distro backport awareness or you will over-report. - Emit OpenVEX or CycloneDX-VEX.
Get this wrong in the safe direction and you flood your auditor with false positives. Get it wrong in the convenient direction and you silently hide a real exposure.
Step 5: now do it forever, on every host
The steps above produce one SBOM for one machine at one moment. The standard also asks for Frequency (regenerate on change) and Distribution and Delivery (offer it over an API or URL). So you wrap the whole pipeline in a scheduler, store the outputs, expose an endpoint, rotate and protect the signing key, and repeat for every host in the fleet. Each new distribution is a new producer map and a new license-parsing quirk.
The honest tally
For one Linux host, conformant and signed, by hand:
- 3 inventory sources, merged and de-duplicated
- purl construction with correct distro qualifiers
- a distribution-to-producer mapping
- a license-text-to-SPDX-ID mapping
- an honest unknown-hash policy
- 9 document metadata fields
- an Ed25519 key, a JCS canonicalizer, and an embed-and-publish path
- a VEX derivation with per-CVE justification and backport awareness
- a scheduler, storage, and a delivery endpoint
That is a small project, not a command.
Or: the monsys version
monsys does all of the above from data the agent already reports. Open a host, go to the CVEs tab, and the audit export bar gives you SBOM (SPDX 2.3 or CycloneDX 1.5) and VEX (OpenVEX or CycloneDX-VEX), each signed with our Ed25519 key and verifiable offline against our published public key. Or pull them straight from the API:
GET /api/v1/agents/{id}/sbom?format=spdx-json
GET /api/v1/agents/{id}/sbom?format=cyclonedx-json
GET /api/v1/agents/{id}/vex?format=openvex
Same data, same signature, bundled into the monthly Audit Pack. Everything is generated inside an EU-hosted platform, with nothing sent to third parties.
Background: CISA rewrote the SBOM rulebook. Technical detail: docs.monsys.ai/en/security/sbom-vex. First five servers free: monsys.ai/en/signup.