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.

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.
Applies to
  • CVSS 4.0
  • EPSS v5 (model v2026.06.15)
  • SSVC 2.0.3, from CISA ADP
  • CISA BOD 26-04 (10 June 2026)
  • NVD API 2.0

Teams triaging CVEs in dependencies, images and infrastructure. EPSS and KEV data change daily, so recompute priorities on a schedule.

Learning goals and prerequisitesAbout 38 minutes: 20 to read and 18 for the exercise

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.

Before you start

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

  • 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.
Go to the guided practice

From a finding to a due date

Decision tableService teamFinding with KEV, SSVC, EPSS, vector1Row, action and due date3Fix shipped, or exception request4Trust boundary1Step, described below
  1. Collect the public signalsKEV status, SSVC values, EPSS with its date and model, and the CVSS 4.0 vector, each with its source.
  2. Add local factsCode absent from the deployed artifact closes as row 1; otherwise add exposure, scoped reachability and tier. Unknowns count as yes.
  3. Pick the row and due dateFirst matching row, then the Up and Down adjustments; the clock starts at the KEV listing or detection, whichever is earlier.
  4. Act and re-runFix by the due date or ask the risk owner for an exception; re-run the table when a signal changes.
Exploitation evidence first, local facts next; never a score alone.

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.

What does each signal tell you?
QuestionSignalWhere to get itWhat it cannot tell you
Is it being exploited?KEV listing; SSVC Exploitation = activeCISA KEV feed; CISA ADP data in the CVE recordExploitation CISA has not confirmed
How likely is exploitation soon?EPSS probability and percentileFIRST EPSS API or daily CSVYour exposure and controls
How bad is a successful exploit?CVSS 4.0 vector; SSVC Technical ImpactCNA data in the CVE record; CISA ADP; NVD for the CVEs it enrichesWhether anyone is attacking it
Can an attacker reach it here?Presence, reachability, exposureDeployed inventory, SBOM, call analysis, network pathsNo public feed has this
What would it cost us?Asset tier, data class, safety impactService catalog and data inventoryNo 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.

How do CVSS, EPSS, KEV and SSVC compare?
AspectCVSS 4.0EPSSCISA KEVSSVC
Published byFIRST (specification); scores by CNAs, CISA ADP, NVD and vendorsFIRST EPSS Special Interest GroupCISACERT/CC (method); CISA ADP publishes decision points in CVE records
AnswersHow 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?
ValuesVector plus a 0.0 to 10.0 score; None, Low, Medium, High, CriticalProbability 0 to 1 and a percentileListed or not, with dateAdded, dueDate, ransomware use and forensic triageDecision points (Exploitation, Automatable, Technical Impact and others) and an outcome
ChangesWhen the vector is revisedDaily; scores shift when the model changesWhen CISA adds entries, usually several a weekWhen the CVE record or your context changes
Get it fromCVE record (CNA and ADP containers), NVD API, GitHub advisoriesapi.first.org/data/v1/epss or the daily CSVKEV JSON or CSV feed, or the cisagov/kev-data mirrorCVE record ADP container; NVD ssvcV203
Common misuseSorting by a CVSS-B number aloneMultiplying it by CVSS; reading a low score as safeTreating absence as proof nobody exploits itInventing 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.

cvss_scores.pyPython
# 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.

What changed from CVSS 3.1 to 4.0, and why do scanners disagree?
Store with each findingExample
Vector stringCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
LabelCVSS-B
Source and dateCNA container of the CVE record, read 2026-10-09
Your environmental vector, if anyBase 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.

threat_signals.pyPython
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.

What does a KEV listing mean, and what are the 2026 due dates?
BOD 26-04 deadline (calendar days)Publicly exposedIn KEVAutomatableTechnical impact
3 days and forensic triageYesYesYes or noTotal
3 days and forensic triageNoYesYesTotal
3 daysYesYesYesPartial
3 daysYesNoYesTotal
14 daysYesYesNoPartial
14 daysYesNoYesPartial
14 daysYesNoNoTotal
14 daysNoYesYesPartial
14 daysNoYesNoTotal or partial
60 daysYesNoNoPartial
60 daysNoNoYesTotal or partial
Next scheduled upgradeNoNoNoTotal 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.

record_signals.pyPython
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.

nvd_osv.shShell
# 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/.

Which data source answers which question?
SourceWhat it gives youUse 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.2The 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 CWEExploitation, Automatable and Technical Impact without assessing them yourself
NVDCPE configurations and NVD scores for the CVEs it prioritizes; CISA SSVC values in ssvcV203CPE-based matching where it exists; check vulnStatus first
OSV.devAdvisories by ecosystem, package and version from many databasesMatching open-source dependencies from lockfiles and SBOMs
GitHub Advisory DatabaseReviewed advisories with CVSS vectors, EPSS values and patched versionsDependabot 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 2025An EU view, including vulnerabilities coordinated by EU CSIRTs
Distribution advisoriesFixed package versions including backportsOperating system packages in images and hosts
Vendor advisories, CSAF and VEXProduct-level affected, fixed and not-affected statementsCommercial 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.

How do the signals map to an action and a due date?
RowSignalsActionDue
1Vulnerable code not present in the deployed artifactClose as not affected; keep the evidenceNone
2Exploited, total impact, and publicly exposed or automatableMitigate now, remediate, check the asset for compromise3 days
3Exploited, publicly exposed, automatable, partial impact; or publicly exposed, automatable, total impactOut-of-cycle fix3 days
4Any other exploited finding; or publicly exposed and either automatable or total impactPrioritized fix14 days
5Publicly exposed, not automatable, partial impact; or not exposed and automatableScheduled fix60 days
6Not exposed, not automatable, not exploitedNext scheduled upgradeAt most 180 days
UpEPSS at or above 0.1 (about the 95th percentile in October 2026); tier-1 assetMove up one row for each, never into row 2-
DownPresent but shown unreachable, with the scope recordedMove down one row; exploited findings stay at row 4 or above-
UnknownExposure or reachability unknown; SSVC data missingTreat 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.

decide.pyPython
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/.

prioritization-policy.yamlYAML
# 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 scenario material
Plain 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. 1. Set the clock for F1

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

    Expected result and reasoning

    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. 2. Place F2 and F3

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

    Expected result and reasoning

    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. 3. Apply the adjustments to F4 and F5

    Use the tier-1 and reachability rules.

    Expected result and reasoning

    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. 4. Handle missing data in F6 and F7

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

    Expected result and reasoning

    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.

Your 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.

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.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. FIRST: CVSS v4.0 Specification Document. Document version 1.2.
  2. FIRST: EPSS data and model version history
  3. FIRST: Using EPSS (thresholds, KEV and common misuses)
  4. CISA: Reducing the Significant Risk of Known Exploited Vulnerabilities
  5. CISA BOD 26-04: Prioritizing Security Updates Based on Risk. 10 June 2026.
  6. CISA: BOD 26-04 Implementation Guidance
  7. CERT/CC SSVC: Deployer patch deployment priority model
  8. CISA Vulnrichment: ADP enrichment of CVE records
  9. NIST: NIST Updates NVD Operations to Address Record CVE Growth (15 April 2026)
  10. NIST: National Vulnerability Database news updates (2026)
  11. FIRST: EPSS API documentation
  12. CERT/CC SSVC: Supplier patch development priority model
  13. ENISA: the European Vulnerability Database (EUVD) is operational. 13 May 2025.

Reference information

Download this guide as Markdown