Frontend Labs
⋯

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)

  1. 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?
  2. 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?
  3. 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.
  4. 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.
  5. 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

  1. What does CVSS measure, and what does EPSS add that CVSS doesn't?
  2. Why does CISA KEV membership override the rest of the prioritisation formula?
  3. What is reachability analysis and why does it dramatically cut a typical SCA backlog?
  4. Sketch an SLA tiering table using CVSS, EPSS, reachability, exposure, and asset criticality.
  5. What must a valid risk-acceptance record contain?
  6. 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).

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36