Chapter 59 — Vulnerability management at scale
In one sentence
At real scale you have far more findings than you can fix, so vulnerability management is the discipline of prioritising by actual risk — exploitability in the wild, reachability in your code, exposure, and asset criticality — rather than drowning in raw counts or a single, incomplete severity score.
Where we are
G1 Ch 47-48 covered dependency scanning and CVSS. Part 3's supply-chain gates and Part 8's detection backlog both produce findings. This chapter is what happens when those streams combine into thousands of open items and a small team has to decide, every day, what to fix first.
How it works
Why CVSS alone isn't enough:
CVSS (Common Vulnerability Scoring System, G1 Ch 48) measures SEVERITY
IF EXPLOITED -- it says nothing about the PROBABILITY that a given CVE
is actually being exploited anywhere, by anyone, right now. A CVSS 9.8
vulnerability with no known exploit and no realistic attack path in
your environment is, in practice, often lower priority than a CVSS 7.0
vulnerability under active, widespread exploitation.
EPSS — the probability layer CVSS is missing:
The Exploit Prediction Scoring System (FIRST.org, the same body that
maintains CVSS) publishes a DAILY-UPDATED score (0 to 1) for every CVE:
the estimated PROBABILITY it will be exploited in the wild in the next
30 days, trained on real-world signals (exploit-code availability,
scanning/attack telemetry, discussion activity). It's a genuinely
different axis from CVSS, not a replacement for it -- CVSS says "how
bad if it happens", EPSS says "how likely is it to happen".
CISA KEV — the strongest signal of all, because it's not probabilistic:
The Known Exploited Vulnerabilities catalog lists CVEs with CONFIRMED,
OBSERVED exploitation in the wild -- not predicted, not theoretical.
US federal civilian agencies are REQUIRED (Binding Operational
Directive 22-01) to remediate KEV-listed vulnerabilities by a
published deadline, but the catalog itself is free and useful for any
organisation: a CVE appearing in KEV should, almost without exception,
jump to the front of your queue regardless of its CVSS score.
Reachability analysis — cutting the noise that CVSS/EPSS can't:
A CVE in a dependency you've pulled in TRANSITIVELY (G1 Ch 47) may sit
in a function your code NEVER ACTUALLY CALLS. Modern SCA tooling
(reachability-aware scanners -- several major SCA products now offer
this, alongside open tooling like call-graph-based analysis) can
determine whether the vulnerable CODE PATH is reachable from your
application's own entry points. An UNREACHABLE critical CVE is a much
lower operational priority than a REACHABLE medium one -- this single
filter routinely eliminates a large fraction of a typical SCA
finding backlog with no actual risk reduction lost.
The prioritisation formula, combining all four signals plus asset context:
priority ~ CVSS (severity)
x EPSS (likelihood of exploitation)
x reachability (is the vulnerable path actually exercised?)
x exposure (internet-facing vs internal-only, Part 2's
network architecture makes this answerable
precisely)
x asset criticality (Part 7 Ch 41's classification tier --
a finding on a crown-jewel system outranks the
same finding on a low-value internal tool)
A CVE that is KEV-listed short-circuits this formula entirely --
treat KEV membership as an automatic top-priority regardless of the
other factors' product, because "confirmed exploited in the wild" is
qualitatively different information from every predictive/contextual
signal above it.
SLAs set by risk tier, not by CVSS alone:
Example tiering (adapt the specific windows to your risk tolerance):
KEV-listed, OR (internet-facing AND reachable AND high EPSS)
-> 24-48 hours
high CVSS, reachable, internet-facing, moderate EPSS
-> 7 days
high CVSS but unreachable OR internal-only OR low EPSS
-> next regular patch cycle (e.g. 30-90 days) or batched with the
next planned dependency update
low CVSS, unreachable, internal, low EPSS
-> track, don't actively chase; revisit if any input signal
changes (a KEV addition, a reachability change from new code)
SLAs tied to a formula like this, rather than "CVSS >= 7 means 7
days" applied uniformly, are both MORE responsive to real risk (KEV
items move faster than a blanket policy would allow) and LESS wasteful
(unreachable/internal/unlikely findings don't consume the same urgent
capacity).
The risk-acceptance process, for when SLA genuinely can't be met:
Not every finding can or should be fixed by its SLA -- a legacy
dependency with no available patch, a fix requiring a larger
architectural change than the timeline allows. The answer is the SAME
documented exception process as Part 1 Ch 4's threat-modeling
"accept" decision: written, with an OWNER, an EXPIRY/review date, and
VISIBLE to someone accountable (not a private note in a ticket nobody
reviews) -- an unfixed finding with a documented, expiring acceptance
is a managed risk; an unfixed finding silently ignored is unmanaged
risk wearing a disguise.
The "10,000 findings" problem, practically:
[ ] AUTO-CLOSE (or de-prioritise into a low-touch backlog) findings
that are simultaneously unreachable, low-EPSS, not KEV-listed, and
internal-only -- reviewed periodically as a batch, not individually.
[ ] BATCH BY ROOT CAUSE: one dependency version bump can resolve
dozens or hundreds of individual CVE tickets at once -- track and
communicate progress at the "upgrade X to version Y" level, not
the individual-CVE-ticket level, wherever that's the actual fix.
[ ] DASHBOARD BY TEAM/SERVICE/ASSET-CRITICALITY, not a single
org-wide raw count -- a rising total count is not itself
informative (it rises simply because you scan more surface over
time); trend SLA-compliance percentage and KEV/high-EPSS-item
age instead.
[ ] Tools built for this specifically: DefectDojo and OWASP
Dependency-Track (both open source) ingest findings from many
scanners, support reachability and policy-based triage, and are
designed around exactly this "manage thousands of findings by risk,
not by raw list" workflow.
Worked example
Cutting a real backlog from 4,000 findings to a defensible top-20.
STARTING POINT: an SCA scan across SecureShop's services returns 4,000
open dependency-vulnerability findings after a year of steady
ingestion (Part 3 Ch 11's supply-chain gate has been running, but
nobody had triaged the ACCUMULATED backlog by anything more than raw
CVSS).
APPLY THE FORMULA:
- Cross-reference against CISA KEV: 6 findings are KEV-listed. These
jump to the top regardless of anything else -- IMMEDIATE, 24-48h
SLA, all 6.
- Pull EPSS scores for the remainder: roughly 3,700 of the 4,000
have an EPSS score under 0.01 (essentially negligible predicted
exploitation probability) -- these move to the low-touch,
periodically-batch-reviewed tier regardless of their CVSS.
- Of the remaining ~300 with meaningful EPSS and/or high CVSS, run
REACHABILITY analysis: roughly 220 are in code paths the
application never actually calls (common with large transitive
dependency trees, Part 3 Ch 11) -- deprioritised to the batch tier
as well, though still tracked (a future code change could make an
unreachable path reachable).
- The remaining ~80 are cross-referenced against Part 2's network
architecture for EXPOSURE (internet-facing vs internal) and Part 7
Ch 41's ASSET CLASSIFICATION for criticality -- roughly 14 are
both internet-facing/reachable AND on a RESTRICTED-data-handling
service.
RESULT: 6 KEV items (immediate) + 14 high-exposure/high-criticality
items (7-day SLA) = a DEFENSIBLE, ACTIONABLE top-20 the team can
realistically close this sprint, out of an originally undifferentiated
4,000. The remaining ~3,980 aren't ignored -- they're correctly
classified as low-touch, batch-reviewed quarterly, and several dozen
of them turn out to be resolvable in ONE pass by a routine dependency-
version bump identified during the batch review, closing hundreds of
tickets at once without ever having been individually "prioritised."
Practice (60 min)
- Pull your own (or a lab project's) current SCA/dependency-scan findings. Cross-reference the CVE IDs against the current CISA KEV catalog (a free, downloadable JSON feed) — how many, if any, are listed?
- Pull EPSS scores for the same CVE list (FIRST.org publishes a free daily data feed/API). Sort by EPSS descending and compare the ranking to a CVSS-descending sort — do they diverge significantly?
- For your top 10 by the combined formula, manually check (or use a reachability-aware tool) whether the vulnerable function/module is actually called anywhere in your codebase.
- Design your own SLA tiering table (like the example above) appropriate to your risk tolerance, and apply it to produce a "top N, fix this sprint" list from your full backlog.
- Write one formal risk-acceptance document for a finding you (realistically) won't fix by any reasonable SLA — owner, reason, expiry/review date.
Build it in Go (20 min) — reachability with govulncheck
"Reachability" is the prioritisation signal this chapter recommends after
KEV and EPSS. Go's official scanner, govulncheck, computes it from the call
graph. To see the difference, here are two tiny programs that depend on the
same vulnerable version of golang.org/x/text (v0.3.7, affected by
GO-2022-1059, a denial of service in language.ParseAcceptLanguage):
// calls/main.go: uses the vulnerable function
tags, _, _ := language.ParseAcceptLanguage("en-US,fr;q=0.8")
// imports/main.go: same package, vulnerable function never called
fmt.Println(language.English)
$ go run golang.org/x/vuln/cmd/govulncheck@latest ./calls
Vulnerability #1: GO-2022-1059
Denial of service via crafted Accept-Language header in golang.org/x/text/language
Found in: golang.org/x/text@v0.3.7
Fixed in: golang.org/x/text@v0.3.8
Example traces found:
#1: calls/main.go:10:44: calls.main calls language.ParseAcceptLanguage
Your code is affected by 1 vulnerability from 1 module.
This scan also found 2 vulnerabilities in packages you import and 27
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
exit status 3
$ go run golang.org/x/vuln/cmd/govulncheck@latest ./imports
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 3 vulnerabilities in packages you import and 27
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
What to notice:
- Same dependency, different verdicts. A manifest-based scanner flags both programs, with about 30 findings each. Reachability reports one actionable finding with the exact call site, and none for the second program.
- The non-zero exit code (3) only for reachable findings makes it a clean CI gate that doesn't drown teams in noise.
- Reachability is evidence, not proof. Reflection, plugins, and code paths the analysis can't see can still reach a vulnerable function. Keep upgrading unreachable vulnerable dependencies on a normal schedule. Just don't page anyone for them.
Exercise: run govulncheck ./... on your own Go services, then run
-show verbose, and compare the reachable count with what your current SCA tool
reports. That ratio is the case for adopting reachability.
Common mistakes
- Sorting purely by CVSS. Ignores real-world exploitation likelihood (EPSS) and confirmed active exploitation (KEV), the two strongest available prioritisation signals.
- Treating every dependency finding as equally urgent regardless of reachability. Wastes scarce remediation capacity on code paths that are never executed.
- No risk-acceptance process, so unfixed findings just silently age. An undocumented, un-owned, never-reviewed gap is unmanaged risk pretending to be managed.
- A single org-wide raw finding count as the headline metric. Naturally rises as scanning coverage improves; says nothing about actual risk trend. Track SLA compliance and KEV/high-EPSS item age instead.
- Individual-CVE-ticket tracking when a single dependency bump would resolve dozens at once. Batch by root cause; report progress at that level.
- No periodic review of the "low-touch" tier. A finding correctly deprioritised today (unreachable, low EPSS) can become urgent tomorrow (a KEV addition, a code change making it reachable) — review the batch, don't just close it forever.
Check yourself
- What does CVSS measure, and what does EPSS add that CVSS doesn't?
- Why does CISA KEV membership override the rest of the prioritisation formula?
- What is reachability analysis and why does it dramatically cut a typical SCA backlog?
- Sketch an SLA tiering table using CVSS, EPSS, reachability, exposure, and asset criticality.
- What must a valid risk-acceptance record contain?
- Why is a raw, org-wide vulnerability count a poor headline metric, and what should you track instead?
Further reading
- Standard: FIRST.org — the CVSS specification and the EPSS documentation/ data feeds (both free, and EPSS updates daily — worth automating the pull).
- Catalog: CISA Known Exploited Vulnerabilities (KEV) catalog and Binding
Operational Directive 22-01 (
cisa.gov/known-exploited-vulnerabilities- catalog). - Tools: OWASP Dependency-Track and DefectDojo — both open source, both built specifically around ingesting multi-source findings and triaging by risk rather than raw list.
- Article: "Prioritizing vulnerability remediation" write-ups combining CVSS+EPSS+KEV (several vulnerability-management vendors and FIRST.org itself publish worked prioritisation-formula examples worth comparing against yours).