You patched. Are you actually fixed? Verifying a CVE fix in production
Short answer: "the pull request merged" tells you nothing about production. Verify from the running service: ask what each live instance has installed, and gate on that. Look for three things that make a fix look done when it isn't: a version that fixes one advisory but not the next, an instance still running the old build, and a service nobody redeployed. Below is a tested run that shows the first two.
This follows the critical-CVE playbook, whose fifth question is “how will you prove it landed?” Here is how to do that with what Dynv reports. We used a local server and a small app with lodash installed, started under dynv run --vulns (monitoring checks the packages actually installed, docs). Everything ran on 2026-11-01.
Four places the old version hides
- A stale container or image. The new image was built; one replica still runs the old one.
- A second service that shares the dependency and wasn’t in the pull request.
- Lockfile drift. The lockfile says fixed; the installed tree or the built artifact says otherwise (the
dynv fixwalkthrough showsnode_moduleslagging a fixed lockfile). - A vendored copy, or a package inside a framework’s own dist, that your lockfile never mentions.
Step 1: ask the running service
$ dynv vulns --process shop --min high
shop — 1 instance(s): 3 high
high lodash 4.17.15 4.17.21 GHSA-35jh-r3h4-6jhm direct
high lodash 4.17.15 4.17.19 GHSA-p6mc-m468-83gw direct
high lodash 4.17.15 4.18.0 GHSA-r5fr-rjxr-66jc direct
This is what the service reports about itself, decrypted on your machine with your master token. dynv vulns takes --process, --min SEVERITY, --json and --fail-on SEVERITY.
Step 2: roll out a “fix”, and look again
We started a second instance with [email protected], the version most advisories list as the fix, while the first kept running:
$ dynv vulns --process shop --min high
shop — 2 instance(s): 4 high
high lodash 4.17.15 4.17.21 GHSA-35jh-r3h4-6jhm direct
high lodash 4.17.15 4.17.19 GHSA-p6mc-m468-83gw direct
high lodash 4.17.15 4.18.0 GHSA-r5fr-rjxr-66jc direct
high lodash 4.17.21 4.18.0 GHSA-r5fr-rjxr-66jc direct
Two things are visible. The old instance still reports its three findings. And the new instance is not clean either: [email protected] still matches GHSA-r5fr-rjxr-66jc, an advisory fixed only in 4.18.0. Upgrading to the version that fixed the advisory you were looking at is not the same as being fixed. This is the half-fix, and it only shows up when you look at what is installed.
The --json output attributes each finding to an instance and gives each instance its own counts and scan time, which is how you find the laggard:
instances:
mInbBUYk4_aObQZS scanned_at 1793540288 high 1, medium 2 <- the new build
hsr8LzYzqmisHDdX scanned_at 1793540265 high 3, medium 3 <- the old build
(Each finding also lists which instances it affects.) Run the check with --json in a rollout script, and wait until the old instance’s id has stopped reporting or has been replaced.
Step 3: a stopped instance doesn’t vanish immediately
We stopped the old instance and looked 20 seconds later. The report still listed 2 instances with the old findings. We did not find a documented expiry for a stopped instance’s last report, so after a rollout, confirm in your orchestrator that old instances are actually gone, and don’t treat a lingering entry as a failed deploy without checking. (The docs do state a cap of 32 live instances per service, with the oldest evicted past that.)
Step 4: make it a gate
$ dynv vulns --process shop --min high --fail-on high
...
$ echo $?
1
--fail-on exits 1 while any open finding at or above the severity remains. Use it as the last step of a deploy job or a scheduled check: it fails until every reporting instance is clean. You can mute noise with dynv vulns ignore ID --reason "..." --expires 30d for findings you have decided to accept (a writer token is required; the reason is sealed and never reaches the server), and the expiry means the decision gets re-made.
Step 5: keep the evidence
For a customer questionnaire or an audit, a short record per incident is enough: the advisory ids, the date you learned of it, the affected services, the dynv vulns output before and after, and the deploy that fixed it. The sealed reports mean the server holds counts, not package names, so keep the CLI output yourself (why).
Limits
- Only running services report. A stopped service isn’t re-checked; for images you haven’t deployed use
dynv scanin CI (guide). - A clean report is a statement about known advisories at that time, not a guarantee of safety. New advisories arrive; monitoring re-checks and alerts (how).
- It reads what is installed. Code vendored into your source, or bundled into a single file without package metadata, can hide from it.
- KEV and EPSS are not shown by the tool yet (the fields are
nullin the JSON); check exploitation separately for the advisories that matter.
Facts last verified . Spotted an error? Tell us.