Securing the Software Supply Chain: Dependencies, SBOMs, and Provenance

Most of the code running in your application is code you did not write. A typical modern project pulls in hundreds of open-source dependencies, and each of those has dependencies of its own. That is a spectacular productivity gain — and a supply chain you are trusting with your users’ data. When attackers realized it was easier to compromise a popular library than to breach a well-defended company directly, software supply chain security stopped being optional. Here is what it means to take it seriously.

The Risk Is Real and Specific

Supply chain attacks are not theoretical. A dependency you trust gets a malicious update; a maintainer’s account is compromised; a package name is typo-squatted so a single mistyped install pulls hostile code; a build server is tampered with so the artifact you ship is not the code you reviewed. In each case the malicious code arrives with your blessing, inside something you chose to trust — which is exactly what makes it dangerous.

Know What You Ship: The SBOM

You cannot secure what you cannot see. A Software Bill of Materials (SBOM) is a complete inventory of every component in your software — direct and transitive dependencies, with versions. It sounds bureaucratic until the next widespread vulnerability drops and the only question that matters is “are we affected?” Teams with an SBOM answer in minutes. Teams without one spend days grepping through code and guessing. An SBOM turns a panic into a query.

The Practices That Reduce the Risk

  • Scan dependencies continuously. Automated pipeline checks that flag known-vulnerable packages on every build, not once a quarter.
  • Pin and lock versions. Use lockfiles so builds are reproducible and a dependency cannot silently change underneath you.
  • Verify integrity. Check hashes and signatures so you know a package is what it claims to be, from who it claims to be.
  • Minimize the surface. Every dependency is trust extended. Prefer fewer, well-maintained libraries over pulling in a package to save five lines of code.
  • Control your build. A secured, reproducible build pipeline means the artifact you ship is provably the code you reviewed.

Provenance: Trust, but Verify the Chain

The frontier of supply chain security is provenance — cryptographically verifiable records of where an artifact came from and how it was built. The direction is clear: moving from “we assume this is fine” to “we can prove this artifact was built from this source by this pipeline.” You do not need the most advanced tooling to start, but you do need to be moving toward verifiable trust rather than assumed trust.

Start With Visibility

If you do nothing else, get to the point where you know exactly what is in your software and get automatically alerted when one of those components becomes dangerous. Visibility is the foundation everything else builds on — and it is achievable this quarter, not someday.

BulletproofSoft hardens the software supply chain as part of secure development and DevSecOps — SBOMs, dependency scanning, build integrity, and the move toward verifiable provenance. Tell us about your stack and we will help you see and secure what you actually ship.

What an SBOM Actually Contains, and Where It Is Generated

An SBOM is a file in a specific format, not a concept. The two formats in real use are CycloneDX and SPDX, both of which have JSON serializations that every mainstream scanner reads. Component identity is the part that decides whether the file is useful: a display name and a version string are not enough to match against a vulnerability feed, which is why both formats carry Package URL (purl) identifiers such as pkg:npm/lodash@4.17.21 and, where one exists, a CPE string for NVD matching. Components that arrive with neither identifier are not flagged as unknown; they are silently skipped.

Where you generate the SBOM changes what it says. A manifest scan reads package.json or pom.xml and reports what was declared, including ranges that have not been resolved yet. Build-time generation records the versions actually installed, including transitive and platform-specific ones. A scan of the built container image sees something different again: OS packages from apk, deb or rpm, plus anything copied in during the build. Most teams need at least two, one for the application dependency tree and one for the image that ships it, because neither view contains the other.

The predictable gaps are the components that no manifest ever mentions: source vendored into the repository, a binary pulled by curl in a Dockerfile, statically linked C libraries, and classes shaded into an uber-jar where the original coordinates are gone. The diagnostic is to generate from the built artifact and compare the component count against what a filesystem-level scan of the same image finds. A large gap between the two is not a tooling error; it is the part of your software that your inventory does not cover.

What a Lockfile Actually Guarantees

"Pin your versions" describes four different behaviors depending on the language. A lockfile can pin resolution (which version you get), pin content (which bytes you get), both, or neither, and that difference decides whether a maintainer who republishes a compromised build under an existing version number reaches your pipeline.

  • npm: package-lock.json records the resolved version, the tarball URL, and an integrity hash in sha512 subresource-integrity form. npm ci installs strictly from the lockfile and fails when package.json disagrees with it, while npm install will quietly rewrite it. Content is pinned, so a republished tarball fails the hash check rather than installing.
  • pip: requirements.txt with == pins the version, not the contents. Contents are pinned only when hashes are present and pip runs with --require-hashes, which then requires every dependency, transitive ones included, to be listed with a hash. pip-compile generates that file; PEP 751 defines pylock.toml as a standard lock format, with tool support still uneven.
  • Go: there is no separate lockfile, and none is needed for resolution. Minimal version selection picks the highest minimum required across the module graph, so the same go.mod produces the same versions without a lock step, and go.sum carries the expected hashes, verified against the public checksum database unless GOPRIVATE excludes the module path. Since Go 1.16 the default -mod=readonly makes a build fail rather than silently amend go.mod.
  • Maven: no lockfile at all by default. Version ranges and SNAPSHOT dependencies resolve fresh on each build, and transitive version conflicts are settled by nearest-wins depth rather than by highest version, so two builds a week apart can legitimately differ. Pinning transitive versions means declaring them in dependencyManagement, and mvn dependency:tree -Dverbose is what shows which mediation decision was actually made.

The Build Pipeline Is Part of the Dependency Graph

Treating a dependency as something you run only after you deploy understates it, because in most ecosystems installing is executing. npm lifecycle scripts (preinstall, install, postinstall) run with the privileges of whoever ran the install, on a developer laptop or a CI runner, before anyone has read a line of the package. Python sdists execute the project's build backend at install time, while wheels are unpacked without running project code, which is why pip install --only-binary=:all: is a real control that sits alongside npm's --ignore-scripts. Both flags have a cost: some packages ship no wheel for your platform and a few genuinely need a postinstall step, so each needs a documented exception path rather than a blanket exemption.

CI configuration is dependency data too, and it is usually not scanned. A workflow step written as uses: owner/action@v3 resolves a mutable tag, and whoever controls that repository can move the tag to different code without your file changing. Token scope compounds it: a workflow token with write access to the repository and to the package registry is a publishing credential that is present in the environment of every step in that job, including third-party ones.

The third gap is name resolution across registries. When a build can reach both an internal index and the public one and picks the highest version it finds, an attacker who registers your internal package name publicly at version 99.0.0 wins the resolution. pip's --extra-index-url has exactly this behavior, since it adds an index to search rather than replacing the default, while --index-url does not. The fixes for all three are structural rather than detective:

  • Run installs with --ignore-scripts for npm and --only-binary=:all: for pip as the default, and keep an explicit, reviewed list of the packages that are exempt.
  • Pin every CI action to a full 40-character commit SHA instead of a tag, and let an update bot raise the SHA change as a reviewable pull request.
  • Set the workflow token to read-only by default and grant write scopes per job, so the steps that consume third-party code are not the steps holding publish rights.
  • Move publishing into a separate job or workflow that runs no third-party steps and holds the only credential with write access to the registry.
  • Scope internal packages, for example @company/name, and map that scope to the private registry so the public registry is never consulted for it; where scoping is not possible, register the names publicly as placeholders.
  • Prefer a single index that proxies upstream over two indexes searched in parallel, which turns resolution order into a server-side policy instead of a client-side flag anyone can forget.

After the Alert: Triage That Is Not a Fire Drill

Continuous scanning produces findings, not decisions, and the volume surprises teams the first week. The matching mechanism explains most of the noise: advisory databases such as OSV, GHSA and NVD record affected version ranges, and the scanner compares your resolved version against a range. That produces false positives when the range metadata is coarser than the actual fix, when a distribution has backported a patch without changing the version number, which is normal for Debian and RHEL packages, and when the vulnerable component is present but the vulnerable code path is never called.

Severity is not risk, and treating a CVSS score as a priority order is what makes the queue unusable. A CVSS base score describes the flaw in the abstract, with no knowledge of your deployment. EPSS estimates the probability that a given vulnerability is exploited in the wild in the near term, and the CISA Known Exploited Vulnerabilities catalog lists what is confirmed to be under active exploitation. A 9.8 in an XML parser your code never invokes ranks below a 7.5 on the path that handles login.

When there is no fixed version upstream, the options are to upgrade past the dependency, replace it, patch and pin a fork, or record why it does not apply. The last one is worth doing formally: VEX documents, in OpenVEX or CSAF VEX form, attach a machine-readable status of not_affected, affected, fixed or under_investigation to a specific product and vulnerability, with a justification code required for not_affected. Stored next to the SBOM, that analysis travels to downstream consumers and to whoever repeats the triage in six months, which is usually you.

What Provenance Proves, and What It Does Not

An attestation is a signed statement about a specific artifact: a subject, meaning a name plus a cryptographic digest, and a predicate describing how that artifact came to exist. SLSA provenance uses the in-toto attestation format, and the predicate names the builder identity, the source repository and commit, and the build parameters. Verification is three steps: recompute the digest of the artifact in front of you, check the signature, then check the predicate against a policy such as "built by this workflow, from this repository, on this branch." The digest check is what makes the statement about that artifact rather than about a name.

The signing side changed shape once long-lived keys in CI proved to be the weak point. In keyless signing, the build requests an OIDC identity token from the CI provider, Fulcio issues a certificate valid for minutes that binds the signature to that workflow identity, and the signature is recorded in the Rekor transparency log so it stays verifiable after the certificate expires. npm publish --provenance and PyPI's attestations both work this way, which means consumers of those registries can already verify build origin without the publisher managing a key.

What provenance does not do is vouch for the source. A malicious commit merged into your main branch and built by your pipeline produces provenance that verifies perfectly, because the statement is a binding between an artifact and a build, not a review of what was built. The other limit is enforcement: an attestation that nothing checks is just a file in a registry. Verification has to happen at a point where failing is meaningful, in the deploy step, at an admission controller, or at install time, or the chain ends where it was supposed to start.

Frequently Asked Questions

Is an SBOM the same thing as a dependency scan?

No. An SBOM is an inventory: the components and versions present in a specific build, written in a format, at a point in time. A scan is the act of comparing an inventory against advisory data, and scanners typically build that inventory internally and discard it. Keeping the SBOM per release is what lets you answer a question about something you shipped eighteen months ago, including a build whose CI environment no longer exists. The SBOM is the durable record; a scan result is a snapshot that goes stale the moment a new advisory publishes.

What has to be in place before SBOM work is worth doing?

A repository and a way to build the software is enough to begin, and the first SBOM is usually a step added to the build you already run. The caveat is about quality rather than readiness: an SBOM generated on a developer machine describes that machine, not the release, so generation should be tied to the build that produces the shipped artifact and stored with that artifact and its digest. Where builds are not yet fully repeatable, the SBOM still tells you what is in the current build; it just cannot be bound to a specific release until that link exists. Making the link is worth doing, and it is not a reason to wait before getting visibility.

Where does this run, and what does it connect to?

Generation runs in CI as a build step, and the output is stored alongside the artifact: as a release asset, in the container registry next to the image, or in a component repository such as Dependency-Track. Scanners consume CycloneDX or SPDX JSON directly, so the format choice mostly determines which tools ingest the file without a conversion step. Alerts should route into whatever already carries work, an issue tracker or a chat channel with an owner, because a dashboard nobody opens is not an alert. Nothing needs an agent on production hosts; the data comes from the build and from the registry.

Do we have to give SBOMs to our customers?

Not as a general legal requirement. Customer delivery is a contractual matter, and it shows up most often in enterprise procurement questionnaires and vendor security reviews. The EU Cyber Resilience Act requires manufacturers to draw up an SBOM in a commonly used machine-readable format covering at least the top-level dependencies, and to make it available to market surveillance authorities on a reasoned request; it does not require handing the SBOM to customers. In US federal procurement, SBOM delivery reaches suppliers through contract terms flowing from Executive Order 14028 rather than through a rule that applies to everyone. The practical position is to produce one per release and decide separately who receives it.

We already have vulnerability alerts turned on in our repository host. Is that enough?

It covers one slice: known vulnerabilities in the declared manifest dependencies of code hosted there. It does not see what is in your container base image, what was vendored or copied into the tree, what the build actually resolves when the manifest declares a range, or the install-time execution and publishing credentials inside your CI. Those are separate sources of the same category of risk, and each needs its own inventory. If the concern is flaws in code your team wrote, such as injection, broken authorization, or secrets in the repository, that is application security review territory rather than supply chain work.

What do we have to provide or decide?

Access is read access to the repositories and build configuration, plus read access to the registry where artifacts land. The decisions are yours and cannot be delegated to tooling: which artifacts count as releases, what severity and exploitability threshold fails a build rather than filing a ticket, who can grant an exception and for how long, and whether SBOMs are published, shared under NDA, or kept internal. The threshold is the decision that determines whether the pipeline gets used or worked around, since a gate that fails every build on medium findings is usually disabled within a month.

If we pin everything, do we stop getting updates?

Pinning changes who decides when an update happens, not whether it happens. The fix for most vulnerable dependencies is an upgrade, so pinning without an update process trades a silent-change risk for a staleness risk, and a stale tree is harder to patch under pressure because the required upgrade crosses several major versions at once. What makes pinning safe is automated update pull requests plus a test suite that actually runs on them. Grouping non-major updates into a weekly batch and handling majors individually keeps the review load survivable.