---
key: "vulnerability-prioritization"
title: "Prioritize vulnerabilities by exploitation, exposure and impact"
url: "https://softwaresecurity.com/frameworks/vulnerability-prioritization/"
api: "https://softwaresecurity.com/api/v1/articles/vulnerability-prioritization.json"
domain: "governance"
topic: "Governance \u0026 frameworks"
type: "Guide"
identifiers: ["CWE-1395","A03:2025","V15"]
tags: ["VEX and vulnerability status","Vulnerability management","AppSec programs"]
stages: ["respond","govern"]
roles: ["appsec-engineer","security-leader"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Prioritize vulnerabilities by exploitation, exposure and impact

Rank vulnerabilities with CISA KEV, EPSS, CVSS 4.0 vectors and SSVC decision points, adjust for presence, reachability and asset value, and turn the result into due dates. Covers NVD's 2026 enrichment changes and CISA BOD 26-04.

Canonical URL: https://softwaresecurity.com/frameworks/vulnerability-prioritization/

## In short

Rank by evidence first. A CISA KEV listing or other reliable evidence of exploitation outranks any score. For everything else, use EPSS as the 30-day likelihood of exploitation activity and the CVSS 4.0 vector or SSVC Technical Impact for consequence. Then check that the code is present and reachable, whether the asset is publicly exposed and how much it matters. Turn those signals into an action and a due date with a fixed decision table.

- Do act first on confirmed exploitation: a KEV listing outranks a low EPSS or CVSS score.
- Do use EPSS as a 30-day probability with its date and model version; don't multiply it by CVSS.
- Do store the full CVSS 4.0 vector and label each number CVSS-B, -BT, -BE or -BTE; a Base score already assumes active attack.
- Do confirm presence, reachability and exposure before setting a due date, and record the scope of that evidence.
- Don't wait for NVD: since April 2026 many CVEs get no NVD score or CPE data, so read CNA, CISA ADP, OSV and GitHub data.
- Don't let missing data lower priority by accident: treat unknown exposure and reachability as yes, and use CISA's fallback (Automatable no, Technical Impact total) when SSVC data is missing.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| CVSS version | 4.0 (specification document v1.2, 2024-06-18) | Label numbers CVSS-B, CVSS-BT, CVSS-BE or CVSS-BTE. | FIRST CVSS v4.0 specification |
| CVSS 4.0 Exploit Maturity (E) left Not Defined | Scored as Attacked (A) | A CVSS-B number already assumes active attack. | FIRST CVSS v4.0 specification |
| CVSS 4.0 CR, IR, AR left Not Defined | Scored as High (H) | — | FIRST CVSS v4.0 specification |
| EPSS model | EPSS v5, model v2026.06.15, published since 2026-06-15 | EPSS v4 (v2025.03.14) ran from 2025-03-17. Store the score date and model version. | FIRST EPSS data page |
| EPSS distribution on 2026-10-09 | Median 0.0066; 90th percentile about 0.039; 4.5% of CVEs at or above 0.1 | Measured from the daily CSV (384,993 scored CVEs). | — |
| KEV JSON feed | `https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json` | Each entry has `dueDate`, `knownRansomwareCampaignUse` and `forensicTriage`. | — |
| KEV catalog on 10 October 2026 | 1,739 entries (catalog version 2026.10.08); 361 marked known ransomware use | — | CISA KEV JSON feed |
| BOD 26-04 deadlines (US federal civilian agencies) | 3 days plus forensic triage, 3 days, 14 days, 60 days, or next scheduled upgrade | Issued 2026-06-10; revoked BOD 22-01 and BOD 19-02. | CISA BOD 26-04 |
| CISA fallback when SSVC data is missing (CVE not in KEV) | Automatable = no; Technical Impact = total | — | BOD 26-04 Implementation Guidance |
| NVD API status for CVEs shown as Not Scheduled | `vulnStatus` `"Deferred"` | No NVD CPE configuration. Without an API key, NVD allows 5 requests per rolling 30 seconds. | — |
| CVSS qualitative ratings (3.1 and 4.0) | None 0.0; Low 0.1-3.9; Medium 4.0-6.9; High 7.0-8.9; Critical 9.0-10.0 | — | FIRST CVSS v4.0 specification |

## Verify it

1. Take the last ten KEV additions and time how long your inventory takes to say whether each is deployed, where, and who owns it.
2. Sample open findings and check that each stores the CVSS vector and its label, the EPSS score with date and model version, the KEV state, and the exposure and reachability evidence.
3. Recompute CVSS 4.0 scores from the stored vectors and flag any stored number that differs.
4. Query a CVE that NVD lists as Deferred and confirm your scanner still finds it through package data (OSV, GitHub advisories or the CVE record's affected versions) rather than NVD CPEs.
5. Run the decision function's tests after every policy change; all 16 BOD 26-04 combinations must still map to the published deadlines.
6. Replay last quarter's findings through the decision table and compare its due dates with the dates the fixes actually shipped.

## What you will learn

- Explain what CVSS, EPSS, KEV and SSVC each measure and what they cannot.
- Apply a decision table that reproduces BOD 26-04 and adds presence, reachability and asset value.
- Collect signals from KEV, EPSS, CVE records, CISA ADP, NVD, OSV and GitHub, including when NVD has not enriched a CVE.

Audience: AppSec engineers, vulnerability management owners and engineering leads who set remediation priorities

Before you start:

- Read a CVSS vector string.
- Know which of your services are publicly exposed and who owns them.
- For the code: Python 3.11 or later; the CVSS example also needs the cvss package.

Time: About 38 minutes: 20 to read and 18 for the exercise

## What does each signal tell you?

Prioritization goes wrong when one number stands in for several questions. CVSS describes how bad a successful exploit would be under stated assumptions. EPSS estimates how likely exploitation activity is in the next 30 days. The CISA Known Exploited Vulnerabilities (KEV) catalog records that exploitation has already happened. None of them knows whether the vulnerable code runs in your service, whether an attacker can reach it, or what the service is worth to you. Those local facts often move the decision more than any public score.

Work through the signals in a fixed order: confirmed exploitation, then exposure and reachability, then likelihood and technical impact, then asset value. Record each value with its source and the date you read it. KEV, EPSS and advisory data all change after you triage.

| Question | Signal | Where to get it | What it cannot tell you |
| --- | --- | --- | --- |
| Is it being exploited? | KEV listing; SSVC Exploitation = active | CISA KEV feed; CISA ADP data in the CVE record | Exploitation CISA has not confirmed |
| How likely is exploitation soon? | EPSS probability and percentile | FIRST EPSS API or daily CSV | Your exposure and controls |
| How bad is a successful exploit? | CVSS 4.0 vector; SSVC Technical Impact | CNA data in the CVE record; CISA ADP; NVD for the CVEs it enriches | Whether anyone is attacking it |
| Can an attacker reach it here? | Presence, reachability, exposure | Deployed inventory, SBOM, call analysis, network paths | No public feed has this |
| What would it cost us? | Asset tier, data class, safety impact | Service catalog and data inventory | No public feed has this |

## How do CVSS, EPSS, KEV and SSVC compare?

Use this table as the quick reference; the sections below explain each signal. Other guides on this site link here rather than repeating it.

| Aspect | CVSS 4.0 | EPSS | CISA KEV | SSVC |
| --- | --- | --- | --- | --- |
| Published by | FIRST (specification); scores by CNAs, CISA ADP, NVD and vendors | FIRST EPSS Special Interest Group | CISA | CERT/CC (method); CISA ADP publishes decision points in CVE records |
| Answers | How bad is a successful exploit, under stated assumptions? | How likely is exploitation activity in the next 30 days? | Has this been exploited in the wild? | What should this stakeholder do about it? |
| Values | Vector plus a 0.0 to 10.0 score; None, Low, Medium, High, Critical | Probability 0 to 1 and a percentile | Listed or not, with dateAdded, dueDate, ransomware use and forensic triage | Decision points (Exploitation, Automatable, Technical Impact and others) and an outcome |
| Changes | When the vector is revised | Daily; scores shift when the model changes | When CISA adds entries, usually several a week | When the CVE record or your context changes |
| Get it from | CVE record (CNA and ADP containers), NVD API, GitHub advisories | `api.first.org/data/v1/epss` or the daily CSV | KEV JSON or CSV feed, or the cisagov/kev-data mirror | CVE record ADP container; NVD `ssvcV203` |
| Common misuse | Sorting by a CVSS-B number alone | Multiplying it by CVSS; reading a low score as safe | Treating absence as proof nobody exploits it | Inventing values instead of using the CISA fallback |

## How do you read a CVSS 4.0 score?

CVSS 4.0, published by FIRST on 1 November 2023, has four metric groups. Base metrics cover exploitability (AV, AC, AT, PR, UI) and impact on the vulnerable system (VC, VI, VA) and on subsequent systems (SC, SI, SA). The Threat group has one metric, Exploit Maturity (E). Environmental metrics set confidentiality, integrity and availability requirements (CR, IR, AR) and override Base values for your deployment. Supplemental metrics such as Automatable, Recovery and Provider Urgency add context but never change the score.

The specification asks you to label every number: CVSS-B for Base only, CVSS-BT with Threat, CVSS-BE with Environmental and CVSS-BTE with both. The label matters because missing metrics default to the worst case. Exploit Maturity left Not Defined is scored as Attacked, and CR, IR and AR default to High. A CVSS-B score from a CNA therefore already assumes active attack against a high-value asset.

The example scores one network-reachable vulnerability with high impact on the vulnerable system. It gives 9.3 as CVSS-B, 8.9 with a public proof of concept (E:P) and 8.1 with no known proof of concept or attacks (E:U). Adding that the asset is reachable only from an adjacent network (MAV:A) and has low security requirements (CR, IR and AR set to L) brings the CVSS-BTE score to 4.5. FIRST's reference calculator code and /tools/cvss-calculator/ give the same four numbers. Keep the vector, recompute when you need the number, and never compare a CVSS 3.1 score with a CVSS 4.0 score.

```python
# pip install cvss==3.6   (Red Hat's CVSS library; implements CVSS 4.0)
from cvss import CVSS4

BASE = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"
cases = {
    "CVSS-B   no threat data": BASE,
    "CVSS-BT  public PoC only": BASE + "/E:P",
    "CVSS-BT  no PoC, no attacks": BASE + "/E:U",
    "CVSS-BTE + internal, low-value": BASE + "/E:U/CR:L/IR:L/AR:L/MAV:A",
}
for label, vector in cases.items():
    c = CVSS4(vector)  # base_score holds the score for every metric in the vector
    print(f"{label:31} {c.base_score:4.1f} {c.severity}")
```

## What changed from CVSS 3.1 to 4.0, and why do scanners disagree?

Both versions map scores to the same words: None is 0.0, Low 0.1 to 3.9, Medium 4.0 to 6.9, High 7.0 to 8.9 and Critical 9.0 to 10.0. The metrics changed. CVSS 4.0 adds Attack Requirements (AT) for conditions such as a race the attacker must win, splits User Interaction into Passive and Active, replaces Scope with separate impacts on the vulnerable system (VC, VI, VA) and on subsequent systems (SC, SI, SA), reduces the old Temporal group to a Threat group with only Exploit Maturity, and adds Supplemental metrics such as Automatable and Safety that never change the score. A 3.1 score and a 4.0 score for the same CVE are not comparable.

One CVE can show several scores because several parties publish them: the CNA in the CVE record, CISA as an Authorized Data Publisher for some CVEs, NVD for the CVEs it still enriches, GitHub for reviewed advisories, and your vendor in its own advisory. They may use different versions and assumptions. Store each vector with its source, version and date, prefer the CNA or vendor vector unless you have a reason not to, and recompute environmental scores yourself.

| Store with each finding | Example |
| --- | --- |
| Vector string | `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N` |
| Label | CVSS-B |
| Source and date | CNA container of the CVE record, read 2026-10-09 |
| Your environmental vector, if any | Base vector plus `/E:U/CR:L/IR:L/AR:L/MAV:A`, scored 4.5 (CVSS-BTE) |

## How should you use EPSS?

EPSS is maintained by FIRST's EPSS Special Interest Group. Every day it publishes, for each CVE, the probability that its data partners will observe exploitation activity in the next 30 days, and the percentile rank of that score. The current model is EPSS v5 (model version v2026.06.15), published since 15 June 2026; EPSS v4 (v2025.03.14) ran from 17 March 2025. A model change shifts scores across the board, so store the score date and the model version, which the first line of the daily CSV records. The API returns the date but not the version.

Scores are heavily skewed. In the 9 October 2026 file of 384,993 CVEs, the median was 0.0066 and the 90th percentile about 0.039. FIRST notes that a threshold near 0.04 selects roughly as many CVEs as a CVSS Critical filter, while 0.5 selects only about 1% of them. Pick a threshold by the workload it creates and the exploited CVEs it catches, and review it after model changes.

Two rules prevent most misuse. First, a KEV listing overrides a low EPSS score. CVE-2023-22894 in Strapi was added to KEV on 8 October 2026 with a 3-day due date; the next day its EPSS score was 0.034. Second, do not multiply EPSS by CVSS. EPSS is a calibrated probability and CVSS is an ordinal severity scale, and FIRST warns that the product has no interpretable meaning. The script below collects KEV and EPSS data for a list of CVEs with the standard library only.

For automation, the EPSS API at `https://api.first.org/data/v1/epss` accepts `cve` (a comma-separated list of up to 2,000 characters), `date` for historic scores back to 14 April 2021, `epss-gt` and `percentile-gt` (and their `-lt` forms) to select by threshold, `days` for recently added CVEs, and `scope=time-series` for the last 30 days of scores. Results come in pages with a default `limit` of 100 and an `offset`. For the full set, download the daily CSV from `https://epss.empiricalsecurity.com/epss_scores-current.csv.gz`, which redirects to the current file; its first line is a comment with the model version and date.

```python
import json
import urllib.request

KEV = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
EPSS = "https://api.first.org/data/v1/epss?cve="


def get_json(url):
    req = urllib.request.Request(url, headers={"User-Agent": "vuln-triage/1.0"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.load(resp)


def threat_signals(cves):
    kev = {v["cveID"]: v for v in get_json(KEV)["vulnerabilities"]}
    epss = {}
    for i in range(0, len(cves), 100):  # the cve= list is capped at 2,000 characters
        for row in get_json(EPSS + ",".join(cves[i:i + 100]))["data"]:
            epss[row["cve"]] = row
    for cve in cves:
        k, e = kev.get(cve), epss.get(cve)
        yield {
            "cve": cve,
            "in_kev": k is not None,
            "kev_due": k["dueDate"] if k else None,
            "forensic_triage": bool(k) and k.get("forensicTriage") == "Yes",
            "ransomware": k["knownRansomwareCampaignUse"] if k else None,
            "epss": float(e["epss"]) if e else None,  # 30-day probability
            "epss_percentile": float(e["percentile"]) if e else None,
            "epss_date": e["date"] if e else None,  # store it: scores change daily
        }


if __name__ == "__main__":
    for row in threat_signals(["CVE-2023-22894", "CVE-2026-34480"]):
        print(row)
```

## What does a KEV listing mean, and what are the 2026 due dates?

CISA adds a vulnerability to KEV when it has a CVE ID, reliable evidence of exploitation in the wild, and a clear remediation action. Attempted exploitation counts. Scanning, security research and a public proof of concept do not count on their own. KEV is not complete, so absence from KEV is not evidence that nobody is exploiting a CVE.

On 10 June 2026 CISA issued Binding Operational Directive 26-04, which revoked BOD 22-01 and BOD 19-02. It gives US federal civilian agencies calendar-day deadlines set by four decision points: whether the asset is publicly exposed, whether the CVE is in KEV, whether exploitation is automatable, and whether the technical impact is total or partial. KEV deadlines applied at once; the full table covers every CVE within 180 days of issuance. CISA now computes the KEV `dueDate` from that table, and the `forensicTriage` field marks entries that also need a check for compromise. Of the 122 entries added between 10 June and 8 October 2026, 99 had a 3-day due date and 23 had 14 days; 78 were flagged for forensic triage.

The directive binds only federal civilian agencies, but CISA recommends that every organization monitor KEV, and its table is a well-documented model to adapt. When CISA has no SSVC data for a CVE outside KEV, it assumes Automatable is no and Technical Impact is total until data arrives.

The catalog is published as JSON and CSV, with a JSON schema and a mirror in the cisagov/kev-data repository on GitHub. Each entry has `cveID`, `vendorProject`, `product`, `vulnerabilityName`, `dateAdded`, `shortDescription`, `requiredAction`, `dueDate`, `knownRansomwareCampaignUse`, `forensicTriage`, `notes` and `cwes`. On 10 October 2026 the catalog (version 2026.10.08) held 1,739 entries, 361 of them marked as known to be used in ransomware campaigns.

If you are the supplier, a KEV listing of your product means customers in many organizations will treat the vulnerability as urgent at once, and US federal agencies have a deadline. Make sure your advisory gives exact fixed versions and a mitigation, publish a CVE record with complete data, and keep your PSIRT ready for questions; see /domains/appsec/vulnerability-response/psirt-operating-model/ and /domains/appsec/vulnerability-response/security-advisories-csaf-vex/. CISA's Product Security Bad Practices also call releasing a product that contains a component with an exploitable KEV vulnerability dangerous.

| BOD 26-04 deadline (calendar days) | Publicly exposed | In KEV | Automatable | Technical impact |
| --- | --- | --- | --- | --- |
| 3 days and forensic triage | Yes | Yes | Yes or no | Total |
| 3 days and forensic triage | No | Yes | Yes | Total |
| 3 days | Yes | Yes | Yes | Partial |
| 3 days | Yes | No | Yes | Total |
| 14 days | Yes | Yes | No | Partial |
| 14 days | Yes | No | Yes | Partial |
| 14 days | Yes | No | No | Total |
| 14 days | No | Yes | Yes | Partial |
| 14 days | No | Yes | No | Total or partial |
| 60 days | Yes | No | No | Partial |
| 60 days | No | No | Yes | Total or partial |
| Next scheduled upgrade | No | No | No | Total or partial |

## Where do SSVC decision points come from?

Stakeholder-Specific Vulnerability Categorization (SSVC), developed at CERT/CC, replaces a single score with a decision tree whose outcome is an action. Its Deployer model combines Exploitation (none, public PoC, active), System Exposure (small, controlled, open), Automatable and Human Impact into Defer, Scheduled, Out-of-Cycle or Immediate. The BOD 26-04 table is also published in the SSVC documentation as a decision model with five outcomes, from fix on system upgrade to 3 days plus forensic investigation.

You rarely have to assess the vulnerability-level points yourself. Through its Vulnrichment program, CISA acts as an Authorized Data Publisher (ADP) and adds Exploitation, Automatable and Technical Impact to CVE records, a KEV entry for listed CVEs, and for some higher-risk CVEs a CVSS vector or CWE when the CNA supplied none. It never overwrites the CNA's own data. The values carry an SSVC version, currently 2.0.3. Since 17 June 2026 NVD returns the same values as `metrics.ssvcV203`, with keys such as `technicalImpact` where the CVE record says "Technical Impact", so normalize keys before you join the two sources.

Exposure, mission impact and safety impact remain your judgment. Write the definitions down, for example that publicly exposed means reachable by unauthenticated users from the internet, so two engineers reach the same answer for the same service.

SSVC has decision models for other roles too. The Supplier model, for whoever must create the patch, combines Exploitation, Utility (from Automatable and Value Density), Technical Impact and Public Safety Impact into the same four outcomes as the Deployer model: Defer, Scheduled, Out-of-Cycle and Immediate. Coordinators have a triage model (Decline, Track, Coordinate) and a publication model (Publish, Do Not Publish). A PSIRT can use the Supplier model to set fix priority for its own products.

```python
import json
import urllib.request


def record_signals(cve_id):
    """Read CNA and CISA ADP data straight from the CVE record (format 5.x)."""
    url = f"https://cveawg.mitre.org/api/cve/{cve_id}"
    with urllib.request.urlopen(url, timeout=30) as resp:
        rec = json.load(resp)
    out = {"cve": cve_id, "cvss": [], "ssvc": None, "kev_added": None}
    containers = [rec["containers"]["cna"], *rec["containers"].get("adp", [])]
    for c in containers:
        source = c["providerMetadata"].get("shortName", "unknown")
        for m in c.get("metrics", []):
            for key in ("cvssV4_0", "cvssV3_1"):
                if key in m:
                    out["cvss"].append((source, m[key]["vectorString"]))
            other = m.get("other", {})
            if other.get("type") == "ssvc":
                # Keys here: "Exploitation", "Automatable", "Technical Impact"
                out["ssvc"] = {k: v for o in other["content"]["options"] for k, v in o.items()}
            elif other.get("type") == "kev":
                out["kev_added"] = other["content"]["dateAdded"]
    return out


if __name__ == "__main__":
    for cve in ("CVE-2026-34480", "CVE-2023-22894"):
        print(record_signals(cve))
```

## What if NVD has not enriched a CVE?

On 15 April 2026 NIST stopped trying to enrich every CVE. NVD now prioritizes CVEs in KEV, with a goal of one business day, CVEs for software used by the federal government, and critical software as defined under Executive Order 14028. Other CVEs are labeled 'Lowest Priority - not scheduled for immediate enrichment', and backlogged CVEs published before 1 March 2026 moved to Not Scheduled. NVD no longer routinely adds its own severity score when the CNA supplied one, and it re-analyzes a modified CVE only when the change materially affects its enrichment.

In the API, the website label Not Scheduled appears as `vulnStatus` `Deferred`. Of the 454 CVEs published on 1 September 2026 that the API returned, 199 had the status Deferred on 10 October 2026. They still carried CVSS vectors from the CNA or CISA, CWE entries, CISA's SSVC values and the CNA's affected versions, but none had an NVD CPE configuration, so a scanner that matches software only by CPE misses them. NVD also corrected about 4,500 CVSS 4.0 scores in April 2026, after a calculation error had inflated the scores of about 19% of records with a CVSS 4.0 score. That is another reason to recompute from vectors.

Match on package identity instead. OSV.dev returns advisories by ecosystem, package and version, and its severity field holds a vector string, not a number. The GitHub Advisory Database API returns reviewed advisories with CVSS 3 and 4 vectors, EPSS values, first patched versions and, for some advisories, vulnerable function names. For operating-system packages, use the distribution's advisories, which account for backported fixes.

```bash
# NVD: enrichment status, CISA SSVC values and KEV date for one CVE.
# Without an API key NVD allows 5 requests per rolling 30 seconds.
curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-22894" |
  jq '.vulnerabilities[0].cve | {vulnStatus,
        ssvc: .metrics.ssvcV203[0].ssvcData.options,
        kevAdded: .cisaExploitAdd,
        hasCpe: (.configurations != null)}'

# OSV: advisories for a package version. severity[].score is a vector string,
# not a number, so compute the score yourself.
curl -s -d '{"package":{"ecosystem":"Maven","name":"org.apache.logging.log4j:log4j-core"},"version":"2.14.1"}' \
  https://api.osv.dev/v1/query |
  jq -r '.vulns[] | [.id, (.aliases // [] | join(",")),
                     (.severity // [])[0].type, (.severity // [])[0].score] | @tsv'
```

## Which data source answers which question?

No single database is complete. Read the CVE record first, match packages through OSV or the GitHub Advisory Database, and use vendor and distribution advisories for what you actually installed. For SBOM-driven triage and VEX statements, see /domains/supply-chain/dependencies/evidence-based-triage/ and /domains/supply-chain/vex/producer-and-consumer-decisions/.

| Source | What it gives you | Use it for |
| --- | --- | --- |
| CVE record (cve.org, CVE Services API) | The CNA's description, affected and fixed versions, CWE and CVSS vectors, in CVE Record Format 5.2 | The authoritative description and the CNA's own scores |
| CISA ADP container (Vulnrichment) | SSVC decision points, KEV status and, for some CVEs, a CVSS vector or CWE | Exploitation, Automatable and Technical Impact without assessing them yourself |
| NVD | CPE configurations and NVD scores for the CVEs it prioritizes; CISA SSVC values in `ssvcV203` | CPE-based matching where it exists; check `vulnStatus` first |
| OSV.dev | Advisories by ecosystem, package and version from many databases | Matching open-source dependencies from lockfiles and SBOMs |
| GitHub Advisory Database | Reviewed advisories with CVSS vectors, EPSS values and patched versions | Dependabot alerts and package matching |
| ENISA EU Vulnerability Database (EUVD) | EUVD IDs linked to CVEs, with lists of exploited and critical vulnerabilities; operational since 13 May 2025 | An EU view, including vulnerabilities coordinated by EU CSIRTs |
| Distribution advisories | Fixed package versions including backports | Operating system packages in images and hosts |
| Vendor advisories, CSAF and VEX | Product-level affected, fixed and not-affected statements | Commercial products; suppressing findings with evidence |

## How do presence, reachability, exposure and asset value change priority?

Presence comes first. Confirm that the vulnerable component and version are in the artifact you deploy, not only in a lockfile or a base image you no longer ship. If they are absent, close the finding as not affected and keep the evidence, for example as a VEX statement.

Reachability asks whether attacker-influenced input can reach the vulnerable code in this deployment. Function-level analysis helps when the advisory names the vulnerable symbols: govulncheck reports only Go vulnerabilities whose vulnerable functions your code calls, and Dependabot's "Most important" alert sort considers whether vulnerable function calls were detected. A negative result covers only the code, configuration and entry points examined. Record that scope, reopen the decision when any of it changes, and treat unknown reachability as reachable.

Exposure and asset value come from your own inventory. Define publicly exposed as BOD 26-04 does: accessible to unauthenticated or untrusted parties over a public network such as the internet, wherever the asset is hosted. Keep a few asset tiers based on data, privileges and safety. A compensating control such as a WAF rule changes exposure only after a test shows it blocks the attack path, including alternate routes to the same backend.

## How do the signals map to an action and a due date?

The table is an example policy for a software organization. Use the first row that matches. Rows 2 to 6 reproduce BOD 26-04 exactly; row 1 and the adjustments add presence, reachability, EPSS and asset tier. The EPSS threshold and the 180-day cap are local choices, not part of the directive. Days are calendar days.

For a vulnerability in a third-party component, start the clock at detection on your asset, or at the KEV listing if that came first. For a finding that needs validation, start it at confirmation and give triage its own target. When a signal changes, re-run the table and keep the earlier of the old due date and the change date plus the new period.

| Row | Signals | Action | Due |
| --- | --- | --- | --- |
| 1 | Vulnerable code not present in the deployed artifact | Close as not affected; keep the evidence | None |
| 2 | Exploited, total impact, and publicly exposed or automatable | Mitigate now, remediate, check the asset for compromise | 3 days |
| 3 | Exploited, publicly exposed, automatable, partial impact; or publicly exposed, automatable, total impact | Out-of-cycle fix | 3 days |
| 4 | Any other exploited finding; or publicly exposed and either automatable or total impact | Prioritized fix | 14 days |
| 5 | Publicly exposed, not automatable, partial impact; or not exposed and automatable | Scheduled fix | 60 days |
| 6 | Not exposed, not automatable, not exploited | Next scheduled upgrade | At most 180 days |
| Up | EPSS at or above 0.1 (about the 95th percentile in October 2026); tier-1 asset | Move up one row for each, never into row 2 | - |
| Down | Present but shown unreachable, with the scope recorded | Move down one row; exploited findings stay at row 4 or above | - |
| Unknown | Exposure or reachability unknown; SSVC data missing | Treat exposure and reachability as yes, Automatable as no, Technical Impact as total | - |

## How do you encode the table so every finding gets the same answer?

Put the policy in code next to its tests, so a rule change shows up in review and in the test results. The function below implements the table. Its unit tests check all 16 BOD 26-04 combinations, the fallbacks for missing data, and each adjustment. Store the returned row, the inputs and the evaluation date with the finding, so a later reviewer can see why it got its due date.

```python
from dataclasses import dataclass

# Rows 2-6 of the decision table. Keys are (publicly_exposed, exploited,
# automatable, total_impact); without adjustments the result equals
# BOD 26-04 Table 1.
ROW = {
    (1, 1, 1, 1): 2, (1, 1, 0, 1): 2, (0, 1, 1, 1): 2,
    (1, 1, 1, 0): 3, (1, 0, 1, 1): 3,
    (1, 1, 0, 0): 4, (1, 0, 1, 0): 4, (1, 0, 0, 1): 4,
    (0, 1, 1, 0): 4, (0, 1, 0, 1): 4, (0, 1, 0, 0): 4,
    (1, 0, 0, 0): 5, (0, 0, 1, 1): 5, (0, 0, 1, 0): 5,
    (0, 0, 0, 1): 6, (0, 0, 0, 0): 6,
}
ACTION = {
    2: ("mitigate now, remediate, check for compromise", 3),
    3: ("out-of-cycle fix", 3),
    4: ("prioritized fix", 14),
    5: ("scheduled fix", 60),
    6: ("next scheduled upgrade", 180),  # local cap if no upgrade is planned
}
EPSS_THRESHOLD = 0.1  # local choice; about the 95th percentile in October 2026


@dataclass
class Finding:
    present: bool | None = None       # None (unchecked) counts as present
    reachable: bool | None = None     # None counts as reachable
    public: bool | None = None        # None counts as publicly exposed
    exploited: bool = False           # KEV listing or other reliable evidence
    automatable: bool | None = None   # None -> "no", CISA's fallback
    total_impact: bool | None = None  # None -> "total", CISA's fallback
    epss: float | None = None
    tier1_asset: bool = False


def decide(f: Finding):
    if f.present is False:
        return 1, "close as not affected; keep the VEX evidence", None
    key = (int(f.public is not False), int(f.exploited),
           int(f.automatable is True), int(f.total_impact is not False))
    row = ROW[key]
    if row > 2:  # escalation never creates the exploitation row
        row -= (f.epss is not None and f.epss >= EPSS_THRESHOLD) + f.tier1_asset
        row = max(row, 3)
    if f.reachable is False:  # scoped evidence moves it down one row
        row = min(row + 1, 4 if f.exploited else 6)
    action, days = ACTION[row]
    return row, action, days
```

## How do you keep the policy reviewable as data?

Keep the table itself in version control next to the code that applies it, so a change of due date or threshold is reviewed like a code change and the history shows who changed what. The YAML below holds the rows, the 16 BOD 26-04 combinations, the adjustments and the fallbacks for missing data. A test that loads it and compares it with `decide.py` passed for all 16 combinations and all six rows. Measure how well teams meet these due dates with the attainment and backlog-age definitions in /frameworks/appsec-metrics-and-slas/.

```yaml
# Remediation policy as data: the decision table rows, their actions and due dates (calendar days).
# Review changes to this file like code; decide.py implements the same rules and its tests pin them.
version: 2026-10-10
clock: earlier of detection on our asset and the KEV dateAdded
rows:
  1: {action: close as not affected and keep the evidence, due_days: null}
  2: {action: mitigate now, remediate and check for compromise, due_days: 3}
  3: {action: out-of-cycle fix, due_days: 3}
  4: {action: prioritized fix, due_days: 14}
  5: {action: scheduled fix, due_days: 60}
  6: {action: next scheduled upgrade, due_days: 180}
# Rows 2 to 6 from (publicly_exposed, exploited, automatable, total_impact); equals BOD 26-04 Table 1.
matrix:
  "1111": 2
  "1101": 2
  "0111": 2
  "1110": 3
  "1011": 3
  "1100": 4
  "1010": 4
  "1001": 4
  "0110": 4
  "0101": 4
  "0100": 4
  "1000": 5
  "0011": 5
  "0010": 5
  "0001": 6
  "0000": 6
adjustments:
  up: {epss_at_or_above: 0.1, tier1_asset: true, never_into_row: 2}
  down: {unreachable_with_recorded_scope: 1, exploited_floor_row: 4}
missing_data:
  exposure: treat as publicly exposed
  reachability: treat as reachable
  automatable: "no"        # CISA's fallback when SSVC data is missing
  technical_impact: total  # CISA's fallback when SSVC data is missing
sla_measurement: see /frameworks/appsec-metrics-and-slas/ for attainment and backlog age
```

## Which mistakes put the wrong work first?

Most prioritization failures come from treating a score as a decision or a missing value as a safe one.

- Sorting by CVSS-B alone. A 9.3 on an internal batch host can wait for the next upgrade; a 6.5 on a KEV-listed admin console cannot.
- Multiplying EPSS by CVSS, or treating an EPSS score of 0.02 as safe. Across thousands of findings, small probabilities still add up to real incidents.
- Mixing CVSS versions or trusting stored numbers. Keep vectors and recompute. Implementations can differ by 0.1 through floating-point rounding, so keep policy rules away from tiny score differences.
- Reading NVD silence as safety. A Deferred CVE without CPE data is still a vulnerability in the packages its CNA named.
- Letting unreachable become permanent. Reachability evidence expires when code, configuration or the advisory changes.
- Counting findings instead of fixes. One base-image rebuild can close hundreds of findings, so group findings by the change that fixes them.

## How do you verify the process, and which tools help?

Test the process against real events: time how long recent KEV additions take to map to deployed assets and owners, and replay last quarter's findings through the decision code to compare its due dates with what happened.

Tools collect signals; the decision stays yours. Dependency scanners such as OSV-Scanner, Dependabot and govulncheck find and enrich component findings. The KEV feed, the EPSS API, the CVE Services API and the NVD API supply public signals, and the SSVC project publishes its decision points and outcomes as JSON. Keep the decision table in version control and review a rule change like a code change.

## Guided practice

A platform team's queue of seven findings is sorted by CVSS score. Apply the decision table on 9 October 2026, set each due date and explain the biggest changes to the order. Services, findings and signal values are fictional.

### Supplied material

```text
Policy: decision table rows 1-6 with the Up, Down and Unknown adjustments; EPSS threshold 0.1; calendar days.
Clock: earlier of KEV listing and detection; every finding was detected on 2026-10-09.
id,service,tier,exposed,in_kev_since,ssvc,epss,cvss_b
F1,payments API,1,yes,2026-10-08,active/yes/total,0.03,7.2
F2,reporting job,3,no,,none/no/total,0.004,9.3
F3,marketing site,3,yes,,none/yes/partial,0.12,6.9
F4,admin console,1,no,2026-10-09,active/no/partial,0.02,6.5
F5,search API,2,yes,,none/yes/total,0.01,
F6,partner API,2,yes,,none/no/partial,0.002,
F7,partner API,2,yes,,,,
ssvc = exploitation/automatable/impact. F1: reachability unknown. F5: call analysis shows release 4.2 never calls the vulnerable function.
F6: NVD status Deferred, CNA CVSS 4.0 vector present; found by OSV from the SBOM, missed by the CPE-based scanner.
F7: published 2026-10-08, no SSVC or EPSS data yet.
Current queue (by CVSS): F2, F1, F3, F4, F6, F5, F7.
```

### 1. Set the clock for F1

Choose F1's row, start date and due date.

Worked answer: Row 2: exploited, total impact, publicly exposed, and unknown reachability counts as reachable. The clock starts at the KEV listing, so the fix and a compromise check are due 2026-10-11, not 2026-10-12.

### 2. Place F2 and F3

Apply the table to the highest CVSS score and to the EPSS outlier.

Worked answer: F2: row 6, the next scheduled upgrade and no later than 2027-04-07, despite CVSS-B 9.3. F3: row 4, moved up to row 3 by EPSS 0.12: an out-of-cycle fix due 2026-10-12.

### 3. Apply the adjustments to F4 and F5

Use the tier-1 and reachability rules.

Worked answer: F4: row 4, moved up to row 3 by the tier-1 asset: due 2026-10-12. F5: row 3, moved down to row 4 by the unreachability evidence: due 2026-10-23, recorded as covering release 4.2 only.

### 4. Handle missing data in F6 and F7

Decide without NVD enrichment for F6 and without SSVC data for F7.

Worked answer: F6: use the CNA vector and CISA's SSVC values: row 5, due 2026-12-08; fix the scanner to match on package identity. F7: missing SSVC data means Automatable no and Technical Impact total: row 4, due 2026-10-23; re-run when CISA's data arrives. The new order: F1; F3 and F4; F5 and F7; F6; F2, so F2 falls from first to last and F4, a 6.5, now comes before F2, a 9.3.

Deliverable: A table of the seven findings with row, action, due date and evidence, plus two sentences on why F2 moved down and F1 moved up.

Scope: The signals are supplied, not live. The EPSS threshold, the tier-1 escalation and the 180-day cap are local choices; BOD 26-04 covers only rows 2 to 6.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [FIRST: CVSS v4.0 Specification Document](https://www.first.org/cvss/v4.0/specification-document). Document version 1.2.
- [FIRST: EPSS data and model version history](https://www.first.org/epss/data)
- [FIRST: Using EPSS (thresholds, KEV and common misuses)](https://www.first.org/epss/using-epss)
- [CISA: Reducing the Significant Risk of Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog/reducing-significant-risk-known-exploited-vulnerabilities)
- [CISA BOD 26-04: Prioritizing Security Updates Based on Risk](https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk). 10 June 2026.
- [CISA: BOD 26-04 Implementation Guidance](https://www.cisa.gov/news-events/directives/bod-26-04-implementation-guidance-prioritizing-security-updates-based-risk)
- [CERT/CC SSVC: Deployer patch deployment priority model](https://certcc.github.io/SSVC/howto/deployer_tree/)
- [CISA Vulnrichment: ADP enrichment of CVE records](https://github.com/cisagov/vulnrichment)
- [NIST: NIST Updates NVD Operations to Address Record CVE Growth (15 April 2026)](https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth)
- [NIST: National Vulnerability Database news updates (2026)](https://www.nist.gov/itl/nvd)
- [FIRST: EPSS API documentation](https://api.first.org/epss/)
- [CERT/CC SSVC: Supplier patch development priority model](https://certcc.github.io/SSVC/howto/supplier_tree/)
- [ENISA: the European Vulnerability Database (EUVD) is operational](https://www.enisa.europa.eu/news/consult-the-european-vulnerability-database-to-enhance-your-digital-security). 13 May 2025.