Note № 74 7 min read
Building an SBOM That Would Have Found Log4Shell
A 2021 post here called a list of installed software an SBOM. It wasn't one, and it wouldn't have found Log4Shell. Here's what an SBOM actually is, and how to generate one properly.
On the 12th of December 2021, two days after the Log4Shell CVE went public, I posted a guide here to creating a Software Bill of Materials. It opens with this: "A Software Bill of Materials (SBOM) is a list of all software installed on a computer."
That is not what an SBOM is. The post went on to tell you to run wmic, dpkg --list, or rpm -qa, or to click through eight steps of System Report, save the output with the hostname in the filename, and call the result an SBOM.
None of those commands would have found Log4Shell.
I did think about quietly deleting it. Correcting it in public seems more useful, because plenty of people are still building the thing I built and calling it an SBOM.
What an SBOM actually is
CISA puts it in a single line: "An SBOM is a nested inventory, a list of ingredients that make up software components."
So an SBOM is a hierarchical record of everything that went into building a piece of software: its direct dependencies, their dependencies, and theirs, all the way down. A software inventory answers a different question, which is what happens to be installed on a given machine. One is scoped to a host and the other to a build artefact. That was my 2021 mistake.
The concept has a specific origin. NTIA published the original Minimum Elements for a Software Bill of Materials on the 12th of July 2021, under Executive Order 14028. It set out seven data fields: supplier name, component name, version, other unique identifiers, dependency relationship, author of the SBOM data, and a timestamp.
Stewardship has since moved to CISA, and the 2026 Minimum Elements arrived on the 29th of July this year. It replaces both the NTIA document and a 2025 draft that went out for public comment, and it asks for a good deal more than seven fields. Now mandatory: cryptographic hashes and the algorithm that produced them, component licences, standard identifiers such as PURL or CPE, the name and version of the tool that generated the SBOM, and a signature from whoever authored it.
Depth is where the two documents part company. NTIA asked for top-level components "with all their transitive dependencies listed", which plenty of people read as top-level now and the rest of it later. The 2026 document says:
An SBOM should include information for all components that make up the target software, including transitive dependencies. There is no minimum depth.
The Cyber Resilience Act sets a lower bar than CISA does. It requires manufacturers to draw up an SBOM in a commonly used, machine-readable format covering at least the product's top-level dependencies. It never has to be published. It sits in the technical file, and each member state's market surveillance authority, the regulator that polices product compliance, can demand it. Reporting obligations start on the 11th of September 2026 and the rest applies from the 11th of December 2027. If you sell software into the EU, you will have to produce one regardless of what CISA says about depth.
Why the old commands would have missed it
log4j-core almost never turns up as an installed package, because that is not how Java applications ship it. Java code travels in archives: a JAR (Java Archive) holds a library, a WAR (Web Application Archive) holds a web application you deploy to a servlet container like Tomcat, and an EAR (Enterprise Application Archive) wraps a set of both. Log4j arrives bundled inside a vendor's WAR or EAR, or shaded into the application's own uber-JAR, which means its classes were copied in at build time and there is no separate library left to find.
wmic, dpkg, rpm, and System Report all report what a package manager recorded at install time. None of them open archives. A server could be running a vulnerable, internet-facing copy of Log4j, and every one of those commands would come back clean.
Which is why, in the weeks after disclosure, half the internet was running find / -name "log4j-core*.jar" and unzipping WAR files by hand to look for a class file three directories down.
My own notes from that period are mostly plugin numbers. Finding Log4j across an estate with Tenable took five of them, and one of the five, Tenable plugin 156001, exists purely to hunt for JAR files on Windows. A scanner plugin with that job description is a workaround for an inventory nobody had.
An installed-software list was never going to answer the question everyone was actually asking. Is this specific component present anywhere in what we are running, however it got there?
Generating a real SBOM
You scan an application, a directory of build output, or a container image, rather than the computer. That holds on every operating system, so one tool works everywhere and only the install line changes.
CISA lists Syft as an example tool, and it is a reasonable default. It is a single binary with no dependencies. It understands OS packages, language ecosystems (npm, pip, Maven, Go modules, and more), and Java archive formats well enough to open them and catalogue what is inside. Syft is good and it is Apache-2.0.
Installing Syft, on any of the three platforms:
# macOS
brew install syft
# Linux
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
# Windows
winget install Anchore.Syft
Then point it at whatever you want to know about:
syft dir:/opt/myapp -o cyclonedx-json=sbom.json
The same command, pointed at an archive:
syft file:legacyapp.war -o cyclonedx-json=sbom.json
Syft unpacks the WAR, reads the MANIFEST.MF and pom.properties metadata files inside it, and recurses into nested archives. If log4j-core is in there, it comes out in the SBOM with a real version number, because Syft is reading the contents rather than asking a package manager what it remembers installing.
Syft can only find it if the build left that metadata in place. A shade plugin configured to relocate classes, or to minimise the JAR, can strip the pom.properties that Syft keys on, and a component you are definitely carrying then fails to appear in the SBOM. It is uncommon, and it is the failure mode to check for on anything you actually depend on.
If you build the software rather than deploy someone else's, Microsoft's sbom-tool is designed to run as a build step and emits SPDX:
winget install Microsoft.SbomTool
sbom-tool generate -b .\drop -bc . -pn MyApp -pv 1.0.0 -ps "My Company" -nsb https://example.com/sbom
Trivy is the other one I would look at, mostly because it does vulnerability matching in the same pass rather than as a second job:
trivy fs --format cyclonedx --output sbom.json ./app
It is brew install trivy on macOS, an APT repository on Debian and Ubuntu, and winget install AquaSecurity.Trivy on Windows. The winget package tracks the current release, even though Trivy's own documentation still sends Windows users to a zip file.
Trivy's release pipeline was compromised twice in early 2026, on the 28th of February and again on the 19th of March. The second attack shipped a credential stealer in v0.69.4, along with poisoned setup-trivy and trivy-action tags. Aqua removed the malicious artefacts and the current release is long past it, but if you are pinning versions or GitHub Actions, pin to a commit SHA and check the cosign signature, which is what the advisory recommends.
Managing SBOMs from more than one machine
The one instinct the original post had right was storing the results somewhere sensible and refreshing them when things change.
A real SBOM belongs to a build, so you end up with one per application per release rather than one per machine. A folder of JSON files stops scaling somewhere around the fourth application (possibly the third).
Generate the SBOM in CI on every build, so it is always current and tied to an exact commit, and push each one into something built to hold them. OWASP Dependency-Track is the obvious open-source choice. It ingests CycloneDX, tracks every component across every application in a portfolio, and re-checks the whole portfolio against NVD, GitHub Advisories, Sonatype OSS Index, and OSV as new advisories land.
Note that it is CycloneDX only. SPDX has been an open feature request since 2021 and the maintainers have been fairly clear that it is not coming, so choose your output format with that in mind. It is the reason every syft command above writes cyclonedx-json.
The original post ended on this: "I may work on a script to ingest the files and store them in a database." I never did write it. Dependency-Track is that idea, done properly.
When the next Log4Shell lands, you want to be querying an inventory you already hold, rather than running find across three hundred hosts at two in the morning. Sorry it took me nearly five years to say so.