Patching a vulnerable dependency yourself works for one package. It stops working somewhere around the tenth, because every patch you own is a fork you now maintain, rebuild and explain to an auditor for as long as you ship it. Sourcing fixed builds moves that work to one place: someone backports the fix once, proves it, signs it, and every team on that version pulls the same result.
This post is for AppSec and platform engineers who own dependency risk. By the end you'll know what a backport involves, why transitive dependencies and lockfiles make self-patching harder than it looks, what evidence a fixed build should carry, and how to check that evidence yourself. The last third shows how Safeguard Gold does it, with the configuration we publish.
Upgrading is the right answer, when you can do it
Start with the obvious. If the fixed version is a patch or minor release away and your tests pass on it, upgrade. Nothing in this post is a reason not to.
The trouble is the cases where you can't. The fix for a critical CVE might only exist in the next major version, which changes an API you call in forty places. A release branch might be frozen for a customer. Or the vulnerable package isn't yours at all: it sits three levels down, pinned by a library whose maintainer hasn't cut a release in a year.
Those findings don't get fixed. They get a risk acceptance and come back next quarter.
What "patch it yourself" actually involves
A backport takes the upstream commit that fixed a vulnerability and applies it to the older version you run. It's narrower than an upgrade, which is the point: you change the lines that closed the hole and nothing else.
Done properly, it looks like this:
- Find the commit or commits that fixed the issue upstream. Advisories often link several, and some belong to other release lines.
- Apply them to the source of the exact version you run. Resolve conflicts by hand where the code has drifted.
- Run the project's own test suite, plus the upstream proof-of-concept test if one exists.
- Build the package, give it a version that won't collide with upstream, and publish it to an internal registry.
- Point every affected service at it.
- Repeat the whole thing when the next CVE lands on that version, and keep your fork in step with any upstream fixes you still want.
Steps 1 to 3 take skill. Steps 4 to 6 take time, every time. And the work is identical for every company running that version, which is why doing it in hundreds of places at once is such a waste.
Transitive dependencies are where self-patching breaks
Most of the packages in a lockfile weren't chosen by anyone on your team. A typical service declares a few dozen direct dependencies and resolves hundreds of transitive ones. When the vulnerable package is transitive, you don't control the version range that pulls it in. The parent package does.
Package managers give you an escape hatch: force a version for a transitive dependency without touching the parent. In npm that's overrides. Yarn calls it resolutions, pnpm has its own overrides, and pip uses a constraints file. Here's what that looks like when the forced version is a Gold build:
// package.json: force one Gold build for a transitive dependency (npm 8.3 and later)
{
"overrides": {
"<package>": "1.2.3-sg.1",
"<parent>": { "<package>": "1.2.3-sg.1" }
}
}
// Roll back: remove the entry, then reinstall so package-lock.json is rewritten
# constraints.txt: force a Gold build for a transitive dependency without adding it as a direct one
<package>==1.2.3+sg.1
pip install -r requirements.txt -c constraints.txt
# Or for every pip run: export PIP_CONSTRAINT=constraints.txt
# Roll back: remove the line (or the -c flag) and reinstall
An override only helps if a fixed build exists to point at. If you patch it yourself, you also own the build that override points to. And the parent package's tests never ran against your patched version, so you're the one who finds out whether it still works.
A fixed build can also have vulnerable dependencies of its own. Safeguard builds the whole dependency tree as Gold where it needs to, recursively, so a fixed package doesn't quietly pull a vulnerable one back in underneath it.
Lockfiles keep installing what they pinned
Lockfiles are the reason a patched build doesn't magically reach your services. That's a feature. package-lock.json, poetry.lock, uv.lock and hash-pinned requirements.txt files record the exact file and its integrity hash. A clean install reproduces that file, byte for byte, until someone re-resolves.
So adopting any fixed build, yours or a vendor's, is a deliberate re-lock:
# Pinned stays pinned: npm ci installs exactly the resolved URL and integrity in package-lock.json,
# even after Safeguard publishes a rebuild (1.2.3-sg.2 after 1.2.3-sg.1).
# A fresh resolve picks up the rebuild: version 1.2.3 now answers with 1.2.3-sg.2
rm package-lock.json
npm install --prefer-online
# Or install the newest Gold build by its own version (1.2.3-sg.N), pinned exactly
npm install <package>@gold-latest --save-exact
# pnpm and Yarn work the same way: delete pnpm-lock.yaml or yarn.lock and install again
For Python, the same idea:
# requirements.txt: pin one Gold build (PEP 440 local version)
<package>==1.2.3+sg.1
# Or keep the upstream pin: ==1.2.3 already selects the newest entitled Gold build 1.2.3+sg.N
<package>==1.2.3
# uv / poetry: re-lock after switching the index so the lockfile records the Gold file hashes
uv lock --upgrade-package <package>
poetry lock
Two things follow. A rebuild never changes what an existing lockfile installs, so nothing shifts under a release without a commit that says so. And you need a way to know when to re-lock, which is what the next post in this series covers.
Versions that can't be mistaken for upstream
A patched build needs a version that says what it is and doesn't get picked up by accident. Gold builds use a suffix on the upstream version: 1.2.3-sg.1 for npm and most ecosystems, and 1.2.3+sg.1 on PyPI.
The two forms behave differently, on purpose:
- In npm's semver,
-sg.1is a prerelease tag, so a range like^1.2.3never selects it by itself. You install a Gold build by pinning it, overriding it or following thegold-latestdist-tag. With the registry's same-coordinate option, a request for upstream1.2.3is also answered with the newest Gold build of1.2.3, in a file whose version reads1.2.3and whose metadata names the Gold build. - On PyPI,
+sg.1is a PEP 440 local version. A pin of==1.2.3matches1.2.3+sg.1, so with the Gold index configured, an existing upstream pin resolves to the newest Gold build on a fresh resolve.
The number after sg. goes up when Safeguard rebuilds the same upstream version, for example to fix a second CVE.
The proof a fixed build should carry
If someone else builds the code you run, "trust us" isn't enough. Here is the evidence worth asking for, from anyone, and what each piece tells you.
A software bill of materials (SBOM). A machine-readable list of the components inside the artifact, with their versions and licences. Gold builds ship a CycloneDX SBOM.
Build provenance. A signed statement of how the artifact was built: from which source, with which build steps and inputs. The SLSA framework defines levels for this. Gold provenance is at SLSA Build Level 2 today. In the SLSA v1.0 spec, that means the build runs on a hosted build platform that generates the provenance and signs it.
A VEX document. VEX (Vulnerability Exploitability eXchange) states, per vulnerability, whether the artifact is affected. For a fixed build, it's where the vendor says in writing which CVEs this build fixes. A scanner that reads VEX can stop flagging what's been fixed.
A reproducible build. If you build the same source twice and get different bytes, a signature only proves who signed it, not what's inside. Every Gold build is built twice and published only when both builds are byte-identical.
A signature. Gold builds are signed with a Safeguard key held in a cloud key management service. Signing proves the file you downloaded is the file we built and scanned.
Scan results. Every Gold build has no known vulnerabilities at its last scan and no known malware (checked with ClamAV). Note the wording: "no known vulnerabilities at last scan". New CVEs get published against old code all the time, so nobody can honestly promise a build stays clean forever. What you can get is the date of the last clean scan and a clear rule for what happens when a build stops being clean.
Licence and notice files from the upstream project are preserved in every build. A patch doesn't change the terms you received the code under.
How to check it yourself
Every piece of evidence above is only worth something if you can verify it without asking us. Gold uses cosign with Safeguard's published public key. For packages, you download the signature, SBOM, provenance and VEX for a build and check each one against the file you installed. For container images, the signature and provenance travel with the image.
KEY=https://registry.safeguard.sh/_gold/keys/cosign.pub
# Gold builds are signed with a Safeguard key held in a hardware-backed vault, not with a public
# transparency log, so cosign is told not to look one up.
# Images: signature and SLSA provenance
cosign verify --key "$KEY" --insecure-ignore-tlog registry.safeguard.sh/library/python@sha256:<digest>
cosign verify-attestation --key "$KEY" --insecure-ignore-tlog --type slsaprovenance registry.safeguard.sh/library/python@sha256:<digest>
# Packages: download the file's signature, SBOM, provenance and VEX for a Gold build
BASE=https://registry.safeguard.sh/_gold/attest/<ecosystem>/<name>/<gold_version>
curl -fsS -u "token:$SAFEGUARD_TOKEN" -o <file>.sig "$BASE/<file>.sig"
curl -fsS -u "token:$SAFEGUARD_TOKEN" -o sbom.cdx.json "$BASE/sbom"
curl -fsS -u "token:$SAFEGUARD_TOKEN" -o provenance.intoto.jsonl "$BASE/provenance"
curl -fsS -u "token:$SAFEGUARD_TOKEN" -o vex.openvex.json "$BASE/vex"
curl -fsS -u "token:$SAFEGUARD_TOKEN" -o sbom.cdx.json.sig "$BASE/sbom.cdx.json.sig"
# <file> is the package file itself, for example from `npm pack <package>@1.2.3-sg.1`
cosign verify-blob --key "$KEY" --insecure-ignore-tlog --signature <file>.sig <file>
cosign verify-blob --key "$KEY" --insecure-ignore-tlog --signature sbom.cdx.json.sig sbom.cdx.json
cosign verify-blob-attestation --key "$KEY" --insecure-ignore-tlog --type slsaprovenance1 --signature provenance.intoto.jsonl <file>
One honest note on the --insecure-ignore-tlog flag. Many signing setups also write each signature to a public transparency log, such as Sigstore's Rekor, so anyone can see that a signature existed at a given time. Gold doesn't use a public transparency log. Signatures are made with a key we control and you verify them against the public key we publish at registry.safeguard.sh, so the flag tells cosign not to look a log entry up.
The trade-off is real: a transparency log gives you an independent public record, and with key-based signing you're relying on us to protect the key. We hold it in a cloud key management service, and the private key never leaves it.
You can also confirm what your package manager actually installed:
# npm ls shows the Gold version (1.2.3-sg.1, or 1.2.3 with the same-coordinate option)
npm ls <package>
# A Gold build names itself in its package.json
node -p "require('<package>/package.json').safeguard"
# The lockfile records the file from registry.safeguard.sh and its integrity hash
grep -A4 '"node_modules/<package>"' package-lock.json
# A Gold build reads 1.2.3+sg.1
pip show <package> | grep -i '^version'
What sourcing fixed builds changes
With Safeguard Gold, customers pull Safeguard-fixed open-source packages and container images from a private registry, registry.safeguard.sh, instead of running the six steps above themselves.
The method is the same one a careful engineer would use. We pin the exact upstream release, backport the upstream fix, run the project's own tests, and store only our patch on top of the upstream source. The first Gold builds published are minimist@1.2.5-sg.1 and pyyaml==5.3+sg.1. The first Gold post walks through how those fixes were made, including a third test run where the pipeline refused to call a package clean because the evidence wasn't there.
Setup is configuration, not a migration. You create a pull token in your workspace, then point your package manager at the registry:
# The pull token from Set up Gold registry (shown once). In CI, store it as the secret SAFEGUARD_TOKEN instead.
export SAFEGUARD_TOKEN='<your pull token>'
registry=https://registry.safeguard.sh/npm/
//registry.safeguard.sh/npm/:_authToken=${SAFEGUARD_TOKEN}
export PIP_INDEX_URL="https://token:${SAFEGUARD_TOKEN}@registry.safeguard.sh/pypi/simple/"
pip install -r requirements.txt
Anything that isn't in Gold passes through to the public source via Safeguard's cache, so you keep one source configuration rather than two. A live end-to-end test passed with real npm, pnpm and Yarn clients and with pip, uv and Poetry.
The registry also supports Maven and Gradle, NuGet, Composer, RubyGems, Go, Cargo, Helm, Docker and OCI images, and OS packages (apt, dnf and apk) for configuration and pass-through. Gold builds so far exist for npm and PyPI. Support for the other ecosystems is built, and builds for them are rolling out.
When patching it yourself is still the right call
Sourcing isn't always the answer. If the package is internal, or so niche that no vendor will carry it, you'll patch it yourself. If your team already maintains a well-run internal fork with its own tests and release process, that fork may be the cheapest place for the fix. And if you simply can upgrade, upgrade.
What changes the economics is repetition. A backport done once and verified by everyone who pulls it costs less than the same backport done in a thousand places, each with its own fork to maintain. That's the case for sourcing fixed builds. The SBOM, provenance, VEX and signature are what make it safe to accept.
Next in this series: Gold, Gold Live, Gold Zero-Day: coverage over time explained, on what happens after a fixed build ships and a new CVE lands on it. To see how vulnerable components get found in the first place, see the SCA docs.
Gold is in early access. Pricing is available on request. To try it, book a call or email sales@safeguard.sh.