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:

  1. Reproducibility: Can we build the same binary from the same source, every time?

  2. Security: Which packages are compromised? Which versions are affected?

  3. 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:

  1. 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.

  2. 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.

LightweightHeavy
Proactive★ Sweet SpotArtifactory, Nix
ReactiveLock 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:

  1. Checks if banned
  2. Checks if allowed
  3. Queries OSV for known vulnerabilities
  4. Downloads the artifact
    • Verifies the hash against the allow.lock file
    • Verifies the signature if available
  5. 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:

  1. A pre-commit hook runs sbom check --policy-from=origin/main. Developers get see whether their changes are gonna trigger a security review.

  2. A CODEOWNERS rule requires security review for any changes to .sbom/. Adding a new package means the PR will need sign-off.

  3. CI runs with the proxy. sbom proxy start --policy-from=origin/main --mode=proxy fulfills 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.