For the past few months, I’ve been working on a polyglot monorepo in a heavily regulated industry. Before long, it became obvious that dependency management would become a huge burden.
This is a feature, not a bug. Software is the only engineering field where artifacts are reusable at zero marginal cost.
But third-party code is also, by definition, code from strangers. It’s not hard to understand why the idea of a software bill of materials (SBOM) is inextricable from matters of sovereignty and national security.
The Gap
Every time we use npm i or cargo add, we’re exposing ourselves to
three concerns:
Reproducibility: Can we build the same binary from the same source, every time?
Security: Which packages are compromised? Which versions are affected?
Compliance: What licenses are we inheriting? Did we account for all third-party code?
These questions are as old as software, but what’s new is the scale. A decade ago, you might pull in a handful of libraries. Today, a fresh React project starts with hundreds of direct and transitive dependencies. Manual tracking is impossible. Every dependency has to be managed, along with every other one it brings along with it.
Solutions exist. The issue is that they’re designed for enterprise needs, completely out of reach of smaller teams.
Proposal
Design goals
From my point of view, an ideal solution would have to meet two constraints:
It has to be proactive. The earlier you catch a problem, the less work has accumulated on top of it. Reactive tools let you drift.
It has to be lightweight. A small team can’t dedicate infrastructure to dependency management. Security has to meet developers where they are.
Lock files + scanners are often combined as a lightweight alternative to heavier solutions. But they’re both reactive. Lock files record what I installed, not what I should be allowed to install. Scanners warn me after the code is already in the tree. Together, they describe what happened, not what should be allowed to happen.
Nix is proactive, but heavy. It often requires rebuilding your entire workflow around it.
Artifactory is proactive, but heavy. It demands dedicated infrastructure, storage, and authentication I don’t have.
The result is stitching together several tools that all work off the same source of truth, the dependency graph, but don’t share it. Each one reinvents its own view and semantics.
| Lightweight | Heavy | |
|---|---|---|
| Proactive | ★ Sweet Spot | Artifactory, Nix |
| Reactive | Lock files + Scanners |
sbom is an attempt at filling the top-left quadrant, and the name
is deliberate.
Most people think of a SBOM as an artifact you export at the end. I wanted to treat it as an actively managed asset: something you maintain as you work, not something you produce when you’re done.
Policy as Code
Nothing embodies the idea of meeting developers where they are more than keeping SBOM policy in the same place as the code it governs.
The core model separates:
- desired state (what developers want, stored in native lockfiles) from
- approved state (what security has blessed, stored in
.sbom/).
.sbom/
├── config
├── apk/
│ └── ...
├── conan/
│ └── ...
├── docker/
│ └── ...
├── gomod/
│ └── ...
└── npm/
├── allow # approved packages
├── allow.lock # full graph with hashes
└── ban # blocked packages
The allow file is human-readable, listing direct dependencies.
The allow.lock file is machine-generated, containing the complete
dependency graph with cryptographic hashes.
# .sbom/config
[npm]
mirrors = ["https://registry.npmjs.org"]
auditors = ["osv"]
[npm.direct]
auto_add = false
auto_update = false
integrity = true
[npm.transitive]
auto_add = true
auto_update = true
integrity = true
When a developer adds a dependency, sbom sync detects drift between
developer intent and approved state. In lax mode, it auto-approves. In
strict mode, it flags the addition for review.
Most security tools try to be a gatekeeper outside the repo, in a SaaS dashboard or a proxy server. I wanted the policy to live inside the repo, right next to the lockfiles.
Critically, sbom never alters your lock files. Dependency resolution stays
with the package managers: npm, go mod, cargo, whatever you use.
sbom observes and enforces; it doesn’t replace.
This distinction is crucial. It forces developer intent and security policy to agree before merge, instead of discovering conflicts later.
Progressive Enforcement
Policy-as-code only works if teams can adopt it gradually. Enforcement
shouldn’t be binary. You don’t want the same level of scrutiny when
you’re prototyping a feature as when you’re shipping a stable release.
sbom supports progressive policy modes:
- Lax: auto-approve, just record
- Moderate: approve new packages, auto-approve patches
- Strict: approve every version change
- Frozen: no changes allowed on maintenance branches
This allows you to start loose and tighten the screw as your project matures, rather than being forced into a “secure or nothing” choice on day one.
Preventing Circumvention
The most subtle problem with policy-as-code is that the policy itself becomes mutable. If I want to use a banned library, I could theoretically just remove the ban in my PR.
sbom solves this with a git-aware check:
sbom check --policy-from=origin/main
Read the laws from main, but check the crimes on my branch.
A developer cannot unban a package in the same PR that introduces it. When
security adds a ban to main:
sbom ban npm:event-stream@3.3.6 --reason "compromised, see CVE-2018-16492"
All branches fail immediately, without requiring rebases. Security decisions
live on main, not on feature branches.
Physical Enforcement
Linting is useful, but for high-security environments, on-path enforcement is required. In other words, a firewall for dependencies, focused on policy, not network perimeter.
sbom includes a proxy server that sits between your package managers
and the internet:
sbom proxy start --mode=proxy --policy-from=origin/main
eval $(sbom proxy env)
npm ci # requests go through sbom
sbom proxy stop
For each package request, the proxy:
- Checks if banned
- Checks if allowed
- Queries OSV for known vulnerabilities
- Downloads the artifact
- Verifies the hash against the allow.lock file
- Verifies the signature if available
- Completes the install
If any check fails, the install is blocked. Vulnerability checks are optional, with configurable severity thresholds.
This check is opt-in, because not every CVE is relevant to every environment, and builds should remain predictable. A critical vulnerability in an image parsing library doesn’t matter if you never process untrusted images.
For air-gapped environments, you can vendor the entire approved dependency graph:
sbom airgap -o ./vendor
# later, on an isolated machine...
sbom proxy start --mode=cache --cache=./vendor
Putting It Together
Here’s how it looks in practice:
A pre-commit hook runs
sbom check --policy-from=origin/main. Developers get see whether their changes are gonna trigger a security review.A CODEOWNERS rule requires security review for any changes to
.sbom/. Adding a new package means the PR will need sign-off.CI runs with the proxy.
sbom proxy start --policy-from=origin/main --mode=proxyfulfills dependencies from a 100% approved SBOM. If something slips through, the build fails.
The whole process is PR-based. Developers propose, security reviews, and
everything is auditable: sbom blame npm:pkg@1.0.0 tells you who approved or
banned a specific package, when and why.
The Virtual Private Registry
When you combine git-based policy, time-travel enforcement, and the local proxy, you get something powerful: a private, multi-ecosystem package repository, with no central infrastructure to deploy or maintain.
Your git repository is the database. The proxy runs on localhost. Policy is versioned with the code. You get the control of an Artifactory instance with the simplicity of a linter. No license fees, no server maintenance, no separate authentication system.
Closing the Loop
There’s one question left: how do you know the binary you shipped is actually what you think it is?
You can lock down policy. You can enforce it in CI. But if something gets injected during the build, none of that helps. A compromised plugin, a tampered cache, or a malicious post-build script can alter the outcome. This touches on the SolarWinds problem: the source was clean, but the build process itself introduced risk.
sbom attest helps close the loop. It computes a cryptographic hash of the
policy-verified dependency graph and embeds it directly into your compiled
binary:
go build -ldflags "-X 'main.SBOM=$(sbom attest)'" pkg/cmd/program
Now your artifact carries proof of its lineage. Combined with a signature, anyone can verify that the binary they’re running was built from an approved set of dependencies.
For compliance, sbom can also export to standard formats like SPDX 2.3.
But attestation gives you strong evidence, not just a static report.
The Broader Picture
$ sbom --help
Usage: sbom <command> [options]
Commands:
init Initialize .sbom/ in current repository
sync Sync lockfiles to approved state
check Validate dependencies against policy
allow Approve a package or version
ban Block a package or version
diff Show differences between desired and approved state
prune Remove unused packages referenced in the approved state
blame Show approval history for a package
proxy Manage proxy server for package managers
airgap Vendor approved dependencies for offline use
attest Generate attestation hash for binary embedding
export Export SBOM to SPDX, CycloneDX, or other formats
Options:
--policy-from Read policy from a specific git ref
--mode Operating mode: lax, moderate, strict, frozen
--ecosystem Target ecosystem: npm, gomod, cargo, pip, etc.
Run 'sbom <command> --help' for details on a specific command.
Regulatory requirements are tightening because they have to. The code from strangers problem is getting worse, not better. But if we make security tools painful, developers will bypass them. Human nature will always favor the path of least resistance.
My goal with sbom wasn’t just to write a linter. It was to prove that we
can have strict supply chain control without giving up the npm install
workflow.
An SBOM is the source of truth for everything that runs in your software. It should be managed like code, enforced like policy, and verified like a signature.
Security that feels like punishment will be bypassed. Security that feels like part of the workflow will be adopted.