TL;DR

CVSS measures potential impact, not likelihood of exploitation. Fix-firsting on raw CVSS burns time on low-likelihood high-severity vulns while CRITICALs with public PoCs sit. A multi-factor model — CVSS + EPSS + KEV + asset criticality + exploit maturity — produces a priority score that correlates with real-world risk. Ship one, and your top-10 list shrinks to the eight that actually matter.

The Problem with CVSS-Only Prioritization

CVSS v3.1 base score 10.0 means “unauthenticated remote code execution with no user interaction.” It is a severity score, not a priority score. Two CRITICALs can differ in exploitability by orders of magnitude: one has a public PoC and a Metasploit module, the other requires a rare configuration and has no known attack path.

Organizations that fix-first on CVSS:

  • Waste remediation cycles on high-severity, low-likelihood vulns
  • Miss CRITICALs that are actively exploited in the wild (KEV)
  • Produce long backlogs with no clear top-10
  • Audit-ready but operationally blind

This is not CVSS’s fault. CVSS measures impact, not likelihood. You need additional signals.

The Multi-Factor Model

A practical prioritization model weights five signals:

Signal Weight Source
CVSS base score 25% NVD / vendor advisory
EPSS probability 20% EPSS API (first.org)
CISA KEV membership 20% CISA catalog
Asset criticality × exposure × data sensitivity 20% Asset inventory
Exploit maturity (public PoC, Metasploit, etc.) 15% Exploit-DB, vendor notes, OSS-SEC

Composite score 0-100:

Score Category SLA
≥ 80 P1 Critical 24 hours
≥ 60 P2 High 7 days
≥ 40 P3 Medium 30 days
< 40 P4 Low 90 days

CVSS Component (25%)

Normalize CVSS 0-10 to 0-100 by multiplying by 10. Simple, bounded, deterministic. If an Environmental CVSS is available (local asset context), use it; otherwise fall back to Base.

cvss_score = (env_cvss or temp_cvss or base_cvss) * 10

EPSS Component (20%)

EPSS gives the probability (0-1) that a CVE will be exploited in the wild within 30 days. The EPSS API at api.first.org returns this per-CVE. Convert to 0-100 by multiplying by 100.

import requests
epss = requests.get(f"https://api.first.org/data/v1/epss?cve={cve}").json()["data"][0]["epss"]
epss_score = epss * 100

EPSS correlates well with actual exploitation. A CVSS 7.2 with EPSS 0.95 is a higher-priority fix than CVSS 9.8 with EPSS 0.001.

KEV Component (20%)

CISA’s Known Exploited Vulnerabilities catalog is binary: in or out. In = 100, out = 0. KEV entries have due dates; an overdue KEV is operationally critical.

kev_score = 100 if in_kev else 0

KEV alone often bumps a vuln to P1. CISA mandates accelerated remediation.

Asset Context (20%)

The same CVE on a critical internet-facing asset with confidential data scores higher than on an internal test box. Multiply asset criticality (25-100), exposure (0.7-1.0), and data sensitivity (0.5-1.0).

asset_score = criticality * exposure * data_sensitivity

Exploit Maturity (15%)

Public PoC = 50, functional exploit = 80, no evidence = 20. Public PoCs cap at 60 minimum regardless of other maturity. A Metasploit module or ransomware TTP maps at least at PoC level.

exploit_score = maturity_map.get(maturity, 20)
exploit_score = max(exploit_score, 60) if public_poc else exploit_score

Putting It Together

priority = (
    0.25 * cvss_score +
    0.20 * epss_score +
    0.20 * kev_score +
    0.20 * asset_score +
    0.15 * exploit_score
)

Example: CVE-2021-44228 (Log4Shell)

  • CVSS 10.0 → 100 × 0.25 = 25
  • EPSS 0.975 → 97.5 × 0.20 = 19.5
  • KEV yes → 100 × 0.20 = 20
  • Asset critical external confidential → 100 × 1.0 × 1.0 = 100 × 0.20 = 20
  • Public exploit high maturity → 100 × 0.15 = 15
  • Total: 99.0 → P1 Critical, 24h SLA

Example: a CVSS 8.1 with EPSS 0.02, no KEV, internal asset

  • CVSS 81 × 0.25 = 20.25
  • EPSS 2 × 0.20 = 0.4
  • KEV 0 × 0.20 = 0
  • Asset 50 × 0.7 × 0.7 = 24.5 × 0.20 = 4.9
  • Exploit 20 × 0.15 = 3
  • Total: 28.55 → P4 Low, 90 days

Same CRITICAL by CVSS, bottom of the priority list by composite.

Implementation Notes

  • Cache EPSS bulk queries (one HTTP call per batch, not per-CVE).
  • Cache the KEV catalog locally; refresh daily.
  • CVSS calculation: the formula is deterministic — cache results.
  • Re-prioritize automatically when new signals arrive (PoC published, KEV added, EPSS updated).

Trade-offs and Limitations

No model is perfect. A multi-factor score:

  • Requires an asset inventory and classification — garbage in, garbage out
  • Weights are opinionated; tune to your org’s risk appetite
  • EPSS is probabilistic, not deterministic — a 0.05 EPSS still gets exploited
  • KEV lags: a vuln actively exploited today may not be in KEV yet
  • Cannot replace human judgment on business-critical systems

But it is demonstrably better than CVSS-only. The top-10 list produced by this model correlates with real exploitation and reduces mean time to remediate the things that actually matter.

Further Reading