A vulnerability you already fixed comes back because the fix lived in one place and the vulnerable version could still reach your build through another: a regenerated lockfile, a second copy of the package deeper in the tree, a revert, a vendored copy, an old base image, or a service that never got the change. To stop it, find the exact path it returned through, close that path with a version rule your package manager enforces and a gate that fails the build on the known-bad range, and scan every push so the next regression is caught in minutes rather than at the next audit.
The rest of this post works through each part with the package managers' own documentation and the NIST and OWASP guidance behind it, so you can run it with the tools you already have. The last third shows how Safeguard does the same job and where it stops.
Key takeaways
- Most regressions are not new bugs. They are the old vulnerable version finding a second route into the build. Name the route before you fix anything.
- The durable close has three parts: a version rule the package manager enforces (overrides, resolutions, constraints, exclusions), a gate that fails the build when the known-bad range appears, and a scan on every push.
- A pin with no recorded reason is the next regression waiting to happen.
- NIST SP 800-218 RV.3 asks for root cause analysis, a search for similar vulnerabilities, and a process change. A regression is evidence the process change did not happen.
- Record a reintroduced vulnerability as a regression linked to the original finding, and count regressions separately.
- Safeguard pieces covered, with maturity labels: continuous scanning and SBOM generation (GA), SCA and deep transitive dependency scanning (GA), policies, gates and guardrails (GA), the CLI and CI scan step (GA), the MCP server (GA), and autonomous remediation with fix pull requests (GA, ships switched off with every category set to Manual). Gold Open Source, which gives free CVE intelligence with affected and fixed versions, is GA; fork and patch is generally available. SAST, mentioned once, is generally available.
Why does a vulnerability we fixed keep coming back?
A fix for a vulnerable open source dependency is almost never a change to the vulnerable code. It is a change to which version your build resolves, and that resolution comes from several places: the manifest, the lockfile, the package manager's rules, the base image, vendored copies, and every other repository that ships the same library. Changing one closes one route. The others stay open.
1. The lockfile was regenerated
The npm documentation describes package-lock.json as a record of the exact tree that was generated, so later installs produce identical trees regardless of intermediate dependency updates. That only holds while the lockfile is kept. Delete it to clear a merge conflict, or run a fresh install after changing something unrelated, and every range is resolved again.
If your fix was a lockfile edit and nothing in the manifest requires the patched version, a regeneration can land on any version the ranges allow, including the vulnerable one. The manifest never forbade it. Only the lockfile did.
This is why the install command matters. The npm docs say npm ci requires an existing lockfile, exits with an error instead of updating it when it disagrees with package.json, and never writes to either. A CI job that runs npm install instead can rewrite the tree on every build.
2. A second copy of the package lives elsewhere in the tree
Package managers allow more than one version of a package when different parents need different ranges. You upgrade the copy your application depends on directly, the scanner goes green for that path, and a nested copy under another dependency still sits at the old version.
Maven resolves this differently, with a similar effect. Its documentation describes "nearest definition" mediation: when several versions of an artifact appear, Maven uses the one closest to your project in the tree, and at equal depth the first declaration wins. Add a new direct dependency that brings an older copy at a shallower depth, and the version your build uses changes without anyone touching the dependency you fixed.
3. A revert, a stale branch or a cherry pick
The fix merged to main. Then a release branch cut before the fix ships a hotfix, or a long running feature branch resolves a lockfile conflict by taking its own side, or someone reverts an unrelated commit that included the lockfile change. Version control did what it was asked, and the old version came back with it.
4. Copied or vendored code
Code copied into a vendor or third_party folder, a pasted minified script, or a single file lifted from another project carries no package metadata. Your fix to the managed dependency does not touch it, and a scanner that reads manifests and lockfiles may not see it.
5. The base image or a build stage
Images carry their own packages. You fix the application dependency, but the base image ships the same vulnerable library, or a builder stage copies an old artifact into the final image. Rebuilding on a fresh base fixes it once. Pinning a base by a tag that later moves, or leaving an old image running, brings it back.
6. A service that never got the fix
In a company with dozens of services, the same library sits in many repositories. One team fixed theirs. Three others still carry the vulnerable version, and a new service is created next quarter from an old template. On a dashboard it looks like the vulnerability came back. It never left.
7. The override was removed or misplaced
Overrides get removed in cleanups ("the parent has probably upgraded by now"). They also have scope rules. The npm docs state that overrides are only considered in the root package.json; overrides in installed dependencies, including workspaces, are ignored. An override added to a workspace package does nothing.
8. A range that drifts back
A direct dependency publishes a release that loosens a range or reverts its own upgrade, and your next resolution follows. The lockfile protects you until it is regenerated, which is route 1 again.
The routes at a glance
| Route back in | What you usually see | How to confirm it | The durable close |
|---|---|---|---|
| Lockfile regenerated | Large lockfile diff in an unrelated pull request | Diff the lockfile between the fixed commit and now | Safe version enforced in the manifest, frozen install in CI |
| Second copy in the tree | Finding reappears on a different path | List every path to the package | Root level override so every copy resolves safely |
| Revert or stale branch | Regression on a release or hotfix branch | Find the commit that moved the lockfile back | Scan and gate release branches too |
| Copied or vendored code | Finding, or silence, in a folder with no manifest | Search for the library's files and version strings | Replace with a managed dependency or track by hand |
| Base image or build stage | Finding in the image, not the source | Compare the source scan with the image scan | Rebuild on a fixed base pinned by digest |
| Service never fixed | Same CVE across several projects | Search the inventory for component and version | Fix by component across all projects |
| Override removed or misplaced | Regression after a cleanup or workspace move | Check where the override lives and that it applied | Root level override with a recorded reason |
| Upstream range drift | Regression right after a parent upgrade | Compare the parent's declared ranges | Manifest rule plus a gate on the bad range |
How do you trace which path brought it back?
A fix aimed at the wrong route is how a vulnerability comes back a third time. Find the route first.
- Confirm it is the same vulnerability. Check the advisory identifier, package, ecosystem and installed version. A different advisory on the same package is a new problem.
- List every path to the package. In npm,
npm explain(aliaswhy) prints the chain of dependencies that causes a package to be installed, andnpm lswith a package spec limits the tree to paths that reach it. In Maven, the dependency plugin's tree goal shows the resolved tree. In Go,go mod whyexplains why a module is needed. Every path is a route to close. - Diff the lockfile across time. Compare the lockfile at the commit that shipped the fix with the one now.
git log -pon the lockfile, narrowed to the package name, usually shows the commit where the version moved back. If several touched it,git bisectwith a script that checks the resolved version finds the first bad one. - Read the commit, not just the diff. A regeneration in an unrelated pull request, a conflict resolution, a revert and a nesting change each need a different fix.
- Check the image separately. A clean source scan with a dirty image scan points at the base image or a build stage.
- Check the rest of the estate. If other repositories carry the same component and version, the "regression" is an incomplete rollout and needs a campaign, not a patch.
- Write it down. One paragraph: which route, which commit, why existing controls missed it. That is the root cause record RV.3 asks for.
How do you pin or override a transitive version safely?
Every major ecosystem can force a transitive dependency to a version you choose, and each has scope rules that, ignored, give you a pin that does not apply. The mechanics below come from each package manager's own documentation.
npm: overrides
The npm package.json docs describe overrides as a way to replace a package anywhere in your tree with another version, scoped as narrowly or broadly as you like. The short form forces every copy to one version:
{
"overrides": {
"foo": "1.0.0"
}
}
Keys can be nested so an override applies only under a given parent, and a key can carry a version or range so it applies only under a specific parent version. Three rules from the docs matter here: overrides are only read from the root package.json; you cannot override a direct dependency unless the specs match exactly (npm throws EOVERRIDE), and the docs show a $ reference to keep them in step; and you should commit the resulting lockfile and install with npm ci.
Yarn Classic: resolutions
Yarn 1 supports selective version resolutions through a resolutions field in package.json. Its docs name the security case directly: a sub-dependency received an important security update and you do not want to wait for your direct dependency to raise its minimum. Newer Yarn releases document resolutions separately, so check the version you run.
pnpm: overrides
pnpm has an overrides setting among its dependency resolution settings. Its settings reference notes that some per project settings only apply where each project has its own lockfile, because a workspace on the default shared lockfile has one resolution for every project. Confirm the resolved version in the lockfile after install.
pip: constraints files
The pip user guide describes constraints files as requirements files that control which version of a package is installed, not whether it is installed. Its own example is the regression case: you want one fixed version used wherever your dependencies pull that package in, so you write a single constraints file for the organisation and use it everywhere:
python -m pip install -c constraints.txt
The weak point is "everywhere". A bare install, or a Dockerfile that installs without -c, is not protected. Put the flag in every install command in CI and in your images.
Maven: dependencyManagement
Maven's docs describe dependency management as the way to state which version of an artifact is used when it appears transitively, instead of whichever copy nearest definition would pick. They also note you can always guarantee a version by declaring it explicitly in your own POM. For multi module builds, put it in the parent POM or an imported bill of materials so every module inherits it.
Gradle: constraints and rich versions
Gradle's docs say dependency constraints are transitive and by default mean "at least this version". For tighter control they document rich versions: strictly pins to an exact version or range, prefer states a preference, and reject excludes specific versions from consideration. reject is the closest thing here to "never resolve the known-bad versions", and Gradle lets you attach a because reason to a constraint, which is where the root cause note belongs.
Go modules: require the fix, or exclude
The Go modules reference describes an exclude directive that prevents a module version from being loaded. Since Go 1.16, an excluded version required anywhere is ignored, and go get or go mod tidy may add a requirement on a higher version. exclude only applies in the main module's go.mod. In most cases the simpler fix is to require the patched version directly, since minimal version selection will not choose anything lower than a version some module requires.
Rules that make any pin safe
- Put it where the tool applies it. Root
package.jsonfor npm, the main module for Go, the parent POM or a BOM for Maven, every install command for pip. - Prefer a safe floor or a rejected range to an exact pin. An exact pin blocks the next security fix too.
- Attach the reason. Advisory identifier, date, owner, and the condition for removal ("remove when parent X requires at least version Y").
- Check it took effect. List every path again after install. An override that silently did not apply is worse than none, because everyone believes the problem is closed.
How do you write a policy that blocks the vulnerable range from re-entering?
A package manager rule protects the repository it lives in. It does not protect the next repository, the service built from an old template, or the image on an old base. For that you need a check on every build that fails when the known-bad range appears, whatever route it took. There are two forms, and most teams need both.
Keyed on the advisory. Fail the build when any component matches a vulnerability at or above a chosen severity. Vulnerability data already records which versions are affected, so a reintroduced version in the affected range fails the build automatically. It also catches the next vulnerability you have not met yet, which is why thresholds need care.
Keyed on the package and version. Fail when a named package appears below a named version, whether or not an advisory currently matches. This is a regression test for a dependency, useful when advisory data is incomplete or when you chose a minimum version for more than one reason.
Either way, the policy needs four properties:
- It runs where the regression enters: pull requests into main and pushes to release branches.
- It blocks rather than warns for the known-bad range. A warning is what let it back in.
- It has a maintained exception path with a justification, an approver and an expiry. An exception with no expiry turns a fixed vulnerability into a permanent one.
- Its exceptions are scoped to a version. A risk acceptance that covers "all versions" of a component also covers the regression.
What does NIST SSDF RV.3 expect after a fix?
NIST SP 800-218, the Secure Software Development Framework version 1.1 (February 2022), groups vulnerability response under "Respond to Vulnerabilities (RV)". Practice RV.3, "Analyze Vulnerabilities to Identify Their Root Causes", exists to reduce how often vulnerabilities occur in future. Its four tasks map cleanly onto a regression:
| SSDF task | What the framework says | What it means for a reintroduced dependency |
|---|---|---|
| RV.3.1 | Analyze identified vulnerabilities to determine their root causes; record the root cause. | Record the route the version took back in, not just "upgraded package X". |
| RV.3.2 | Analyze root causes over time to identify patterns; examples include adding toolchain mechanisms to detect future instances automatically. | Three regressions from lockfile regeneration in a quarter point at the install step; the fix is a frozen install plus a gate. |
| RV.3.3 | Review the software for similar vulnerabilities to eradicate a class, and fix them proactively rather than waiting for external reports. | Search every repository and image for the same component and version, and for others that came in by the same route. |
| RV.3.4 | Review the SDLC process and update it to prevent, or reduce the likelihood of, the root cause recurring in updates or new software. | Change the service template, add the gate to the shared pipeline, require a recorded reason for overrides. |
Read the other way round, a regression shows the RV.3 loop was not closed. The fix shipped, which is RV.2 (Assess, Prioritize, and Remediate Vulnerabilities, where RV.2.2 covers planning and implementing risk responses), but the root cause work and the process change did not follow.
The OWASP Software Component Verification Standard sits underneath this. SCVS 1.1 asks that all direct and transitive components and their versions are known at completion of a build. SCVS 5.4 asks for an automated process to identify publicly disclosed vulnerabilities in third party and open source components. SCVS 5.11, an automated process for identifying component quantity, is the one that catches the second copy. You cannot detect a regression in a component you do not inventory, or catch it on the next push without automation.
CISA's Secure by Design page makes the organisational point: every technology provider should take ownership, at the executive level, of making its products secure by design. Fixing the same vulnerability twice is the opposite. Removing the route it came back through is the design fix.
How should finding status work when something reopens?
When a vulnerability returns, either the old finding moves from resolved back to open, or a new finding is created. Both can work. What fails is a new finding with no link to the old one: the regression becomes invisible, remediation time looks fine, and nobody asks why the fix did not hold.
A workable model:
- Identity. Project, component, version range and advisory identify a finding. If the same combination returns after being resolved, it is the same finding.
- Status. Reopen it, or create a child finding marked as a regression. Keep the original first seen date and add a reintroduced date.
- History. Keep the full timeline: first seen, resolved by which change, returned through which change.
- Mitigations. A reopened finding should not inherit an old risk acceptance automatically. The context that justified it may no longer hold.
- Reporting. Count regressions separately from new findings.
Tools differ here, and some do not model a reopened state at all. Check yours, and if it cannot link the two, add the link in the ticket or mitigation record. The live post on container scan finding states: open, fixed, suppressed, reopened goes deeper on state models, and does a vulnerability exception survive a dependency upgrade? covers the exception side.
How Safeguard does this job, step by step
Everything above can be done with package manager features, git and a scanner. Safeguard's part is to keep the inventory current on every push, show where a component sits in each project's tree, evaluate policies and gates on each new scan, fail a CI job when a gate fails, record mitigations with scope and expiry, and, when you turn it on, open fix pull requests. Here is the flow as Safeguard's docs describe it.
1. Scan every push and pull request (CLI and CI scan step: GA)
Safeguard's CI/CD integration docs describe one pipeline step that runs safeguard ci scan. Each push becomes a new version of its Safeguard project, named after the branch and commit, and the step can fail the build on dependency vulnerabilities at or above a severity you choose. The docs list GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Jenkins, Azure Pipelines and any CI that runs a shell step, and say there is no plugin, Action or Pipe to install.
Setup per the docs: create a secret key under Settings › Developer with the scans:trigger and scans:read scopes, store it as SAFEGUARD_API_KEY, and add the step. The GitHub Actions workflow from the docs:
name: Safeguard
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Safeguard scan
env:
SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
SAFEGUARD_INSTALL_DIR: ${{ runner.temp }}/safeguard-bin
run: |
set -eu
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
For regressions, widen the branch lists; the docs say wildcards such as release/** work, which covers route 3. If the repository is not connected or sits behind a firewall, the docs describe a local mode that reads lockfiles and manifests on the runner and sends only results.
2. Let continuous scanning catch changes between pushes (GA)
The continuous scanning docs list three rescan triggers: vulnerability feed updates, Safeguard advisory publications, and asset changes such as a new commit or image tag. For source repositories they describe a scan on every push plus an hourly pull for missed webhooks. Every asset has a Timeline of scans showing what triggered each one and the diff of findings from the previous scan: added, resolved, changed severity.
That Timeline is your regression detector. A resolved finding that shows up again as "added" names the scan, the scan names the version, and the version names the commit.
3. Trace the route in the Dependencies tab
Open the project from Projects. The Explore SBOM docs describe a Dependencies tab with a tree view (parent and child relationships, depth, versions, vulnerability indicators) and a sortable, filterable table. SCA and deep transitive dependency scanning (both GA) fill that tree; Safeguard's docs market the depth as up to 100 levels. Use it to answer the trace questions: one copy or two, which parent pulls the vulnerable one in, and whether that parent changed in the version where the finding returned. Then use the platform's view of which projects contain a given component and version to answer RV.3.3 across the estate.
4. Review the finding and set mitigation scope carefully
The Findings docs describe a finding card with a status (Open, Resolved or Mitigated), the gate that caught it, a First Seen date, and a History timeline. The documented workflow runs Open, In Progress, Resolved, with Mitigated and Suppressed as side paths. The docs we read do not describe an automatic reopened state, so link a regression to its earlier finding yourself.
The Mitigations tab is where the scope trap is avoided or set. The mitigation form has a required Scope field: this version only, or all versions. For a risk acceptance on something you intend to fix, choose this version only. The docs also describe mitigation statuses (Active, Expired, Revoked, Superseded), expiry options from 30 days to a custom date, reminders 14 and 7 days before expiry, and an audit trail of each change.
How mitigations meet gates, per the docs: active mitigations exclude vulnerabilities from gate failures, expired ones do not, and policy based mitigations apply automatically. So a mitigation policy that auto accepts a package or severity band will also auto accept that package's regression.
5. Create a policy for the known-bad range (policies, gates and guardrails: GA)
The Policies and Gates docs describe the builder: Settings › Policies, Create Policy, define conditions with the visual builder or YAML, set severity and actions, save and enable. The documented conditions are vulnerability severity, CVSS score, EPSS score, license, component age, maintenance status and SLSA level, and there is a "No Critical Vulnerabilities" template.
For a regression, a severity or CVSS condition is the advisory keyed rule: any component version in an affected range at or above your threshold fails. The documented list has no "package X below version Y" condition, so keep that form at the package manager level (overrides, constraints, Gradle reject, Go exclude) and let the Safeguard policy be the estate wide backstop. The policy outcomes docs say each policy runs its starred search when evaluated, so whether a search can target one package and version in your tenant is worth testing before you rely on it. Component age and maintenance conditions help with old, unmaintained parents that keep pulling old children.
Safeguard's technical evaluation material also lists internal policies including Duplicate Detection and Dependency Age, which speak to routes 2 and 8. We did not find a docs walkthrough for them, so confirm what they report in your tenant first.
6. Link the policy to a gate with the Block action
Each policy has one Action. The policy outcomes docs define three: Block fails the gate; Warn lets it pass with a warning naming the policy; Notify lets it pass and sends a notification, only when a policy starts failing or finds new matches. For the known-bad range, use Block.
The docs give the gate's outcome a fixed order: no linked policies is Not evaluated; any failed Block policy is Failed; then Error, Not evaluated, Warning and Passed. An Error never counts as a pass, and a gate never evaluated shows Not evaluated, never Passed, which closes the hole of a gate that passes because it never ran.
Check one switch first: the docs say policy outcomes are turned on per workspace by Safeguard and are off until then. If the Gates and Policies tables show no Outcome column, ask your Safeguard contact.
7. Fail the CI job on the gate
With outcomes on, add the gate flag with a gate name, id or all:
safeguard ci scan --wait --fail-on-gate all
Per the docs, this waits for the scan and for the gate's policies to be evaluated on the new data, exits 0 on Passed or Warning, and exits non-zero on Failed, Error or Not evaluated, or when evaluation does not finish before the timeout. It combines with the severity flag. The docs note SAST findings are not part of the severity flag yet (SAST is generally available), which does not affect dependency regressions.
8. Use exceptions with an expiry when you must ship
The docs describe an exception flow: Request Exception, choose the policy and finding, give a business justification, set an expiration date and submit. Approvers are notified, decide, and the decision is recorded in an audit trail. Keep the expiry short.
9. Open the fix pull request (autonomous remediation: GA, ships switched off)
The Vulnerabilities tab has an AI Remediate button. Autonomous remediation, driven by Safeguard's Griffin models, can open a pull request that moves the dependency to a safe version, on one repository or in bulk. It ships switched off with every category set to Manual, so nothing runs until an administrator turns it on, and every pull request still goes through your review. Check the change against your trace: if the version came back through a second copy, a direct upgrade alone will not hold, and the change you want is a root level override or constraint with its reason recorded.
10. Close the RV.3 loop
Write the root cause record where developers can find it; the SSDF's own example is a searchable wiki. Make the process change: add the release branch pattern, move the override to the root, add the constraints file to the Dockerfile, update the template. Then watch the asset Timeline over the next few pushes to confirm the finding stays resolved.
Requirement, Safeguard, and what you still own
| Need | What Safeguard does | What you still own |
|---|---|---|
| Inventory every component per build (SCVS 1.1) | SBOM generation and SCA on each push and on schedule (GA) | Connecting or CI scanning every repository and image |
| Detect disclosed vulnerabilities automatically (SCVS 5.4) | Matches components to vulnerability feeds and rescans on feed changes (GA) | Thresholds that block the bad range without blocking everything |
| Find the route back in | Dependencies tree and table, scan Timeline with finding diffs (GA) | Reading lockfile history and naming the commit |
| Block the range from re-entering | Policies and gates with Block, Warn or Notify, and a CI gate flag (GA; outcomes switched on per workspace) | Package manager pins and the package keyed rule |
| Keep exceptions honest | Exceptions with expiry; mitigations with scope, expiry and audit trail (GA) | Choosing version scoped acceptances and short expiries |
| Fix across the estate | Fix pull requests, single or bulk (GA, ships switched off, every category Manual) | Turning it on, reviewing and testing each pull request |
| Root cause and process change (RV.3) | Timelines, gate history and mitigation audit trail as evidence | The analysis, the record and the SDLC change |
How do you measure regressions over time?
If you do not count regressions, you will not notice that fixes are not holding. Five measures cover most of it:
- Regression count: findings resolved and later returned, per month.
- Regression rate: regressions divided by findings resolved in the same period.
- Time to re-detect: from the reintroducing commit to the scan that flagged it. With a scan on every push, this should be close to your pipeline duration. If it is days, something is not scanned on push.
- Regressions by route: tag each with its route from the list above. This is the pattern RV.3.2 asks for.
- Projects still carrying a "fixed" vulnerability: for each vulnerability declared fixed, the count of projects and images still on the affected version. Anything above zero means the fix was incomplete.
A worked example with illustrative numbers, not data from any team: a platform team resolves 120 dependency findings in a quarter and 9 come back, a 7.5 percent regression rate. Tagging shows 5 from lockfile regeneration, 2 from a second copy and 2 from a release branch cut before the fix. The process change is a frozen install in CI, root level overrides and release branches in the scan trigger. Next quarter's rate tells you whether it worked.
In Safeguard, the asset Timeline shows added and resolved findings per scan and the dashboard tracks remediation time trends, so the raw data is there. The regression tag and route analysis are yours to add.
What Safeguard does not do
- It only sees regressions in what it scans. An unconnected repository, an unscanned image or a branch outside the CI trigger list is invisible. Routes 3 and 6 live in those gaps.
- Code copied without package metadata may not be identified. Vendored source and pasted files may not be matched to a package and version, so route 4 regressions can go unreported. Track them by hand.
- The documented gate conditions have no package and version range rule. They are severity, CVSS, EPSS, license, component age, maintenance and SLSA level. KEV status and reachability are filters elsewhere in the product, not documented gate conditions. Keep package keyed regression tests in the package manager.
- Policy outcomes are off by default. Until Safeguard turns them on for your workspace, the gate flag cannot fail a build on them.
- Fix pull requests are not automatic and need review. Autonomous remediation ships switched off with every category set to Manual. Whether a fix uses an override, a constraint or a direct upgrade is something to check in the pull request.
- It does not do the root cause analysis or process change. It gives you history and evidence. The RV.3 work is your team's.
- The docs we read describe no automatic reopened state. Link a reintroduced finding to its earlier record yourself.
- A blocking policy still needs a staffed exception path. Without one, the first blocked release gets the policy switched to Warn.
Common mistakes
- Fixing the lockfile and nothing else. The next regeneration undoes it. Put the safe floor in the manifest.
- Putting the override where the tool ignores it: a workspace
package.json, a Goexcludeoutside the main module, pip constraints only some installs use. - Pinning to an exact version forever. It blocks the next security fix. Prefer a floor or a rejected range, and record when to remove it.
- Scanning main only. Release and hotfix branches cut before the fix are a common route back.
- Closing the ticket when one service is fixed. Search the estate first.
- Risk acceptances scoped to all versions, or mitigation policies that auto accept a whole package. Both hide the regression.
- Using Warn for the known-bad range. Warn passes the gate.
- No root cause record. Without it there is no pattern, so RV.3.2 and RV.3.4 never happen.
- Treating the reintroduced finding as new. It loses its history and never shows up in any metric.
FAQ
Why does npm install bring back a vulnerable version I already upgraded?
Usually the fix lived only in the lockfile, and the manifest ranges still allow the vulnerable version, so a regeneration resolves back to it. Add an override in the root package.json, commit the new lockfile, and use npm ci in CI, which the npm docs say fails rather than rewriting a lockfile that disagrees with package.json.
How do I find out which dependency pulls in a vulnerable package?
Use the package manager's "why" tool: npm explain in npm, the dependency plugin's tree goal in Maven, go mod why in Go. In Safeguard, the project's Dependencies tab shows the same tree with vulnerability indicators.
How do I force a transitive dependency to a safe version?
npm uses overrides, Yarn Classic uses resolutions, pnpm has an overrides setting, pip uses a constraints file passed with -c, Maven uses dependencyManagement, Gradle uses constraints with rich versions such as strictly and reject, and Go uses a direct requirement on the fixed version or an exclude in the main module.
Should a reintroduced vulnerability be a new finding or a reopened one?
Either works if it is linked to the original. Keep the first seen date, record when and how it came back, do not inherit old risk acceptances automatically, and count it as a regression.
What does NIST SSDF say about vulnerabilities that come back?
SP 800-218 practice RV.3 asks you to find root causes (RV.3.1), find patterns over time (RV.3.2), review for similar vulnerabilities (RV.3.3), and update the SDLC so the root cause does not recur (RV.3.4). A regression signals RV.3.4 was skipped.
Can a CI gate stop a vulnerable version coming back?
Yes, if it runs on every branch that can ship, blocks rather than warns, and has an exception path. In Safeguard, safeguard ci scan fails on vulnerabilities at or above a severity, and the gate flag fails on a policy gate once policy outcomes are on for your workspace.
Why does my container image still have the vulnerability after I fixed the code?
The image carries packages from its base and build stages. Rebuild on a fixed base, pin the base by digest, and scan the image as well as the source.
Does Safeguard automatically fix regressions?
Not by default. Autonomous remediation is GA but ships switched off with every category set to Manual. When enabled, it opens fix pull requests that your team reviews and merges.
Further reading and next steps
Related posts: replace a denied open source dependency without a rewrite and AI security regression gate design patterns.
To try the flow above, download Safeguard from https://safeguard.sh/download and read the setup guides at https://docs.safeguard.sh/docs .
Stopping a regression is mostly about closing the route the old version used. When the harder problem is that the safe version is slow to land or does not exist yet, the options below are how Safeguard helps you get past that.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. You install the CLI, run safeguard to open the interactive terminal, and ask it to "fix the lodash CVE in this repo." It reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan .. For a regression, the same session is where you would put the safe version in the manifest and rerun the scan to confirm the finding is gone.
Click through it at your own pace, or open it full screen.
The second demo connects Claude to the Safeguard MCP server with OAuth and shows you choosing which tools Claude can use. Once connected, you can ask which projects still carry a version you thought you had fixed.
Click through it at your own pace, or open it full screen.
Quick check
You upgraded a vulnerable transitive package by editing the lockfile, and it came back after an unrelated pull request. What is the likely route, and the durable fix?
The lockfile was regenerated and the manifest ranges still allowed the vulnerable version. Put the safe floor in the manifest with an override, resolution or constraint, commit the new lockfile, use a frozen install in CI, and add a blocking gate so the next regeneration fails the build.
Your team accepted the risk of a vulnerable component with the scope set to all versions, then upgraded. The vulnerable version returns. Why does the gate stay green?
An active mitigation excludes the vulnerability from gate failures, and an all versions scope also covers the reintroduced version. Scope acceptances to the exact version, give them an expiry, and revoke them once the fix ships.
Sources
- NIST SP 800-218 SSDF v1.1 (PDF)
- NIST CSRC, SP 800-218
- OWASP SCVS project
- OWASP SCVS V1 Inventory
- OWASP SCVS V5 Component Analysis
- CISA Secure by Design
- npm package.json
- npm package-lock.json
- npm ci
- npm ls
- npm explain
- Yarn Classic resolutions
- pnpm settings
- pip user guide
- Maven dependency mechanism
- Gradle dependency constraints
- Go modules reference
- Safeguard Continuous Scanning
- Safeguard Explore SBOM
- Safeguard Policies and Gates
- Safeguard Policy outcomes
- Safeguard Mitigations
- Safeguard Findings
- Safeguard CI/CD Integration
- Safeguard GitHub Actions
Last reviewed: 24 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.