In December 2021, a flaw was found in a Java logging library called Log4j. Not a famous library. The boring thing that writes messages to a log file.
Then every company on earth asked the same question at once: are we using it?
Most couldn't answer. Not out of carelessness, but because nobody keeps a list of what is actually inside their software. Their app pulled in a library, which pulled in another, which quietly pulled in Log4j four levels down. Teams spent entire weekends grepping through servers trying to work out whether they were exposed.
The companies that answered in ten minutes had one thing the others didn't. A list.
That list is called an SBOM, and it's one of four things I learned to attach to my own artifacts this week.
First, what a build actually does
Source code is text. UserService.java is a file full of words like public class and return. A processor cannot run that. It speaks something much lower level.
Building is what closes that gap. It's the cooking, and source code is the recipe.
Nobody serves a recipe card to a customer. They serve the dish. Nobody deploys source code to a production server either; they deploy the built artifact.
I built two of them. A Java service with Maven, which produced a 4.8K JAR file. A Node service with npm, which produced a 1.7K .tgz. Different languages, same idea: source in, shippable thing out.
Every build tool is the same tool wearing different clothes
Java uses Maven. Node uses npm. Python uses pip. Three tools, three sets of commands, and it looks like three separate things to learn.
It isn't. They all answer the same five questions:
-
A config file. What is this project and what does it need? (
pom.xml,package.json,requirements.txt) -
A lock file. Which exact versions did we actually install? (
package-lock.json, pinned versions) - A registry. Where do the libraries come from? (Maven Central, npmjs.com, PyPI)
-
A cache. Where do downloaded libraries live locally? (
~/.m2,node_modules/) -
An artifact. What comes out at the end? (
.jar,.tgz,.whl)
Config, lock, registry, cache, artifact. Once you see those five boxes, learning the third tool takes minutes, because it's just the first tool with different filenames.
One genuine difference worth knowing. Java compiles everything upfront, translating the whole book before anyone reads it. Node and Python translate live, line by line, as the program runs. That's why my first Maven build took 64 seconds and npm install took 4.
Four questions that turn a build output into an artifact
Here's the part most tutorials skip. A file sitting in a target folder isn't ready to ship. A real artifact answers four questions.
What's in it?
An SBOM, a Software Bill of Materials, is a complete itemised list of every component inside your artifact. It's the ingredients label on food packaging.
I generated mine with a tool called Syft. My Node service listed two entries: my own code, and one library called dotenv.
Small, but the version was interesting. My config file asked for ^16.4.5, meaning "16.4.5 or anything compatible." What actually got installed was 16.6.1. Fuzzy request, specific reality. That gap is precisely why lock files exist.
Real applications run this and get three hundred entries. Most of which nobody on the team chose.
Is it safe?
Knowing what's inside is step one. Step two is checking whether any of it is known to be broken. Security researchers maintain public databases of flaws found in software libraries, and a tool called Grype cross-checks your ingredients list against them.
It's a food safety inspection. You have the ingredients; now someone checks them against the recall list.
Both my artifacts came back clean, which was true and slightly boring. So I deliberately installed an old version of a popular library called lodash to see what a real finding looks like.
Six findings from one library. Three rated High. Read one row as a sentence: you have lodash 4.17.15, it has a high severity flaw, version 4.17.21 fixes it.
Two columns earn their place. EPSS estimates how likely the flaw is to be exploited in the wild, and the top result sat at 22.4%, the 97th percentile. That's not theoretical; attackers are actively using it. RISK blends severity with that likelihood so you know what to fix on Monday morning.
And every single one had a fix already available. Most vulnerabilities are not unsolvable mysteries. They're someone forgetting to update a library for two years.
Is it the one I built?
In 2016, attackers broke into the servers of Linux Mint and swapped the download file for their own version with a backdoor in it. Same filename, same page, same site. Anyone downloading that day got malware.
Underneath the download link sat a line of gibberish characters. Anyone who ran one command against their file and compared it would have known instantly.
That line is a checksum: a fingerprint calculated from a file's exact contents. It's the tamper-evident seal on a medicine bottle.
I made one for my JAR, verified it passed, then appended a single character to the file and checked again.
One character. FAILED, immediately. There is no such thing as small tampering; any change at all breaks the seal, which is exactly what makes it worth publishing.
Who made it?
A checksum proves the file didn't change. It doesn't prove who made it. An attacker who controls the download page can replace both the file and the checksum, and everything matches perfectly. It's just their seal now.
Signing closes that gap. A signed release carries cryptographic proof it came from a specific person, and it cannot be forged without their private key.
I signed my release tag with the same SSH key I set up months ago for signed commits, and got back: Good "git" signature for vivian-signing-2026. On GitHub it shows a green Verified badge.
A checksum is a seal anyone can apply. A signature is your handwriting, notarised.
Then I stopped relying on my memory
Producing all of that took eight separate commands in the right order with the right flags. Which is fine on a Tuesday morning and useless at 5pm on a Friday when you need to ship a fix and the scan feels optional.
So I wrapped the whole path in one script. Build, SBOM, scan, checksum, in that order, stopping dead if anything fails.
![The full build and verify run]

One command, four steps, three files at the end: the artifact, its ingredients list, and its seal. You can't skip a step because there are no steps to skip.
That's also the bridge to what comes next. A CI pipeline is essentially this script running on a server, triggered by a push. Write it now, hand it to a robot later.
Three things that caught me
A command finishing without error is not proof it did the right thing. My Node tests ran green while testing nothing at all, because I'd pointed the test runner at the wrong path. Five tests written, one thing ran. I only caught it because the count said 1 and I expected 5.
Node's package doesn't contain its dependencies. Scanning the .tgz found zero packages, which looked broken. It wasn't. npm deliberately excludes node_modules, so the box holds the recipe and the ingredients get fetched on arrival. Java bakes them in; Node doesn't.
Build output does not belong in git. My first commit attempt swept in compiled classes, test reports, and the JAR itself. All of it reproducible from source with one command. Commit the recipe, not the meal.
The question I can answer now
If someone hands me a JAR with no SBOM, what should I assume about it?
That nobody knows what's inside it. Including the person who built it.
It might be perfectly fine. But there's no way to answer the Log4j question about it, no way to know whether it's carrying something with a public exploit, and no way to prove it's the file they think it is.
That's not ready to ship. It's just bytes.




Top comments (0)