Running a CVE Campaign From "Are We Affected?" to Closure

Treat a named CVE as programme work with a scope, a deadline and a visible finish line.
A CVE response playbook: scope affected versions, set a deadline, assign owners, ship validated fixes, and count an instance done only when it's gone from prod.
September 14, 2026

A new CVE lands on a Tuesday morning. By lunch someone senior has asked the question every AppSec team dreads: "Are we affected?" By Friday the question has changed to "Are we done?", and the honest answer is usually a spreadsheet with a column of maybes.

CVE response is a different job from routine vulnerability management. Routine work is a queue ranked by risk. A named CVE response is a campaign: a declared scope, a deadline, an owner, and a finish line everyone can see. The teams that handle these well treat them as programme work with a start and an end, not as another row in the backlog.

This post lays out how to run a CVE campaign from the first question to verified closure.

When a CVE deserves a campaign

Most CVEs do not need special handling. Your normal SLOs and ranking cover them. A campaign earns its overhead when one of these is true:

  • The vulnerability is in the CISA Known Exploited Vulnerabilities catalog, or there is credible evidence of exploitation.
  • Leadership, a customer or a regulator has asked about it by name.
  • The affected package is everywhere: a logging library, an HTTP client, a serialization framework.
  • A contractual or regulatory deadline applies.
  • Several related CVEs in the same component need to be closed together.

If none apply, let the normal process handle it. A campaign for every advisory is just a second backlog.

Step 1: Answer "are we affected?" precisely

"Affected" means more than "the package name appears in a lockfile". A precise answer separates four things.

Which versions

Match the advisory's affected ranges against resolved versions, not declared ranges. ^2.4.0 in a manifest tells you nothing. The lockfile or the resolved graph tells you what is installed.

Direct or transitive

A direct dependency is a one-line bump. A transitive one may need a parent upgrade or an override. Know which you have before you promise a date.

Where it runs

A vulnerable version in a service deployed to production is more urgent than the same version in an archived prototype. Deployment context changes the order you work in. It does not change whether the instance counts.

Aliases

The same flaw can carry a CVE ID and a GHSA ID. Search by both, or you will under-count.

Step 2: Declare the scope and the deadline

Write the campaign down before work starts. At minimum:

  • The CVEs, by ID. Group related ones if they are fixed by the same upgrade.
  • The repositories in scope. Often "all of them". Sometimes a subset, such as customer-facing services first.
  • A target end date, stated publicly inside the company.
  • An owner for the campaign as a whole, separate from the owners of each fix.

Keep the scope fixed once the campaign starts. If new repositories need the same fix, track them visibly as out of scope rather than quietly widening the campaign. A moving denominator makes progress numbers meaningless.

Step 3: Assign every instance to an owner

A CVE campaign stalls on ownership far more often than on the fix itself. For each affected instance you need a team and a person who will merge the change. If your ownership data comes from CODEOWNERS, a service catalog or an identity provider, make sure the campaign reads from the same source your ticket routing uses.

Instances with no owner should be visible as unassigned, not hidden. An unassigned bucket that shrinks is a sign the campaign is working.

Step 4: Fix at scale, not ticket by ticket

Opening fifty tickets and waiting is the slow path. The fast path is generating the fix for each instance and handing the owner a pull request to review.

  • Pick the upgrade once. Choose the fixed version that closes the declared CVEs without introducing new ones, and use it everywhere you can.
  • Handle transitive cases deliberately. Prefer the smallest direct upgrade that pulls in the fixed version. Fall back to an override where no such upgrade exists.
  • Validate before review. A pull request that already passes the repository's build and tests is merged in hours. One that fails CI sits for weeks.
  • Batch by owner. Group pull requests so one team reviews its set together.

Step 5: Decide what "done" means

This is where most campaigns declare victory too early. A merged pull request is not a remediated instance. The vulnerable version is gone when it is gone from every environment where the code is deployed.

Track three states, not two:

  1. Active. No fix yet.
  2. Fixed. The dependency is upgraded in the code, but at least one deployed environment still runs the old version.
  3. Remediated. The vulnerable version is gone from every deployed environment.

Some instances will not be fixed at all, for good reasons: a decommissioned service, a test fixture, a component that never ships. Exclude them explicitly, with a written reason, and keep them visible. An excluded instance with a reason is a decision. A missing instance is a gap.

Step 6: Report progress people can trust

Campaign reporting has two audiences. Leadership wants one number and a date. Engineering managers want to know which of their teams is behind.

  • Overall progress: remediated instances out of affected instances, over time, with the starting point shown.
  • Progress by team: remediated and open per team, so the conversation goes to the right manager.
  • Out of scope: the same CVE in repositories the campaign does not cover, so nobody mistakes "campaign complete" for "company clear".

Report from the system of record, not a copied spreadsheet. The spreadsheet is always a day behind.

Step 7: Close, and keep it closed

A campaign is complete when every affected instance is remediated or excluded. Two things keep it that way.

  • A guardrail that fails any pull request reintroducing a vulnerable version of the package.
  • A record of what was fixed, excluded and why, available when the auditor or the customer asks next quarter.

Where CVE campaigns usually go wrong

The same few mistakes show up in most post-incident reviews of a CVE response.

  • Counting manifests, not deployments. The campaign reports 100% because every repository merged the bump, while two services still run the old image in production.
  • Losing the transitive cases. Direct dependencies get fixed on day one. The transitive ones, which need a parent upgrade or an override, sit untouched until someone asks why the number stopped moving.
  • Widening scope mid-flight. New repositories get added quietly, the denominator grows, and progress appears to go backwards.
  • Deleting instead of excluding. An instance removed from the list with no reason looks exactly like one that was never found.
  • Stopping at the deadline. The campaign closes on its target date with open instances folded back into the backlog, and the next person to search for the CVE finds it still there.

Each of these is avoidable with the steps above. Most are avoidable with one rule: report from the same system that tracks deployments.

A CVE response checklist

  1. Decide whether this CVE needs a campaign or the normal queue.
  2. Match affected ranges against resolved versions, including aliases.
  3. Declare CVEs, repository scope, a target end date and a campaign owner.
  4. Assign every instance to a team and a person. Make unassigned visible.
  5. Generate validated pull requests instead of tickets where you can.
  6. Count an instance as remediated only when it is gone from every deployed environment.
  7. Exclude with a reason. Never delete silently.
  8. Report overall and per-team progress from the system of record.
  9. Add a guardrail so the vulnerable version does not come back.

How Heeler runs a campaign

Heeler Campaigns track one declared set of CVEs to closure across every repository that carries them.

  • An administrator picks one or more CVEs, optionally limits them to chosen repositories, and sets an optional target end date. Heeler finds every affected instance.
  • Each instance names the repository, module and package with its installed version, classifies the dependency (direct or transitive, first or third party, unpinned, compromised), and carries the owning team and tech lead.
  • Instances sit on three tabs: Active, Remediated, and Out of Scope for matches in repositories the campaign does not cover.
  • An instance counts as remediated when the vulnerable version is gone from all deployed environments. An upgraded instance still running somewhere stays on Active.
  • Instances can be excluded with a reason and re-included later.
  • A Progress Over Time card and a Team Progress card show overall and per-team progress.
  • Where SCA Auto-fix is available, selected instances can be remediated as pull requests from the campaign, validated in your CI.

The details are in the Campaigns documentation. For how risk shapes the order you work in, see Autotriage.

FAQ

What is a CVE response campaign?

A CVE response campaign is a time-boxed effort to find and fix every instance of a named set of CVEs, with a declared scope, a deadline, an owner and visible progress to closure.

When is a CVE instance actually remediated?

When the vulnerable version is gone from every environment where the code is deployed. A merged upgrade that has not been deployed everywhere is fixed in code but not remediated.

How do we handle instances we will not fix?

Exclude them explicitly with a written reason and keep them visible in the campaign. That turns an unfixed instance into a recorded decision an auditor can review.

Should a campaign cover every repository?

Often, yes. When it does not, track matches in out-of-scope repositories separately so a completed campaign is not mistaken for a clean estate.

Run your next CVE response in one place

Heeler finds every instance of a declared CVE, assigns it to the team that owns it, and tracks it until it is gone from production. Get a demo.

What’s new on Heeler
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Related resources

See All Resources