Your Team is Patching the Wrong Vulnerabilities (Here's the Data)
Last quarter, I watched a security team spend three weeks emergency-patching 847 "critical" vulnerabilities across their infrastructure. They worked nights and weekends, postponed feature releases, and burned through their annual patching budget.
Want to know how many of those vulnerabilities were actually exploited in the wild? Zero.
Want to know how many vulnerabilities they didn't patch that were being actively exploited? Twelve.
This isn't incompetence—it's a broken prioritization system that's costing companies millions in wasted effort while leaving real threats unaddressed. And if you're using CVSS scores as your primary prioritization method, you're probably making the same mistakes.
Let me show you the data that changes everything.
The CVSS Trap
Here's what most teams do: Scan your infrastructure, get 10,000 findings, sort by CVSS score (high to low), start patching from the top.
Sounds logical, right? It's how the industry has worked for 20 years.
There's just one problem: CVSS tells you how bad a vulnerability could be, not how likely it is to be exploited.
Consider these two vulnerabilities:
Vulnerability A:
- CVSS Score: 9.8 (Critical)
- Can be exploited remotely
- No authentication required
- Affects: OpenSSH server on an internal monitoring box with no internet access
- Exploit availability: No public exploits
- Real-world usage: Zero recorded exploits in 18 months
Vulnerability B:
- CVSS Score: 7.5 (High)
- Requires authentication
- Affects: Apache Log4j (Log4Shell)
- Exploit availability: Dozens of exploit kits
- Real-world usage: 840,000+ exploitation attempts per day
Traditional CVSS-based prioritization says patch A first. But B is the one burning down the internet.
This gap between "severity" and "exploitability" is why teams are drowning in vulnerability debt while attackers walk right past them.
What the Data Actually Shows
The Exploit Prediction Scoring System (EPSS) analyzed three years of vulnerability data and found something shocking:
- Only 2-7% of published CVEs are ever exploited in the wild
- Of the 20,000+ CVEs published in 2023, fewer than 1,400 were actually used in attacks
- 70% of security team effort goes toward vulnerabilities that will never be weaponized
Even more revealing: when you look at the vulnerabilities that do get exploited, the pattern is clear:
- Exploit code exists: 80% of exploited CVEs have public proof-of-concept code
- Real-world activity: They're detected in honeypots, threat intel feeds, or incident reports
- Reachability matters more than severity: A CVSS 7.0 vuln in your internet-facing API is more dangerous than a CVSS 9.8 vuln in an offline database backup script
Traditional CVSS scoring misses all of these signals.
The Real-World Cost
Let me tell you about a healthcare company I worked with. They had 15,000 vulnerabilities in their backlog. Their approach? Start at CVSS 9+ and work down.
Six months into this Sisyphean effort, they suffered a breach. The entry point? A CVSS 6.5 vulnerability in an internet-facing WordPress plugin that had active exploit code on GitHub and was being mass-scanned by botnets.
It was number 4,237 in their backlog.
The breach cost them $2.3M in incident response, regulatory fines, and lost business. They'd spent six months patching the wrong 800 vulnerabilities while the right one sat buried in their queue.
When I showed them what their prioritization would look like with exploitability data, the vulnerable WordPress plugin jumped to position #3. They would have patched it in week one.
The Three Signals That Actually Matter
After analyzing hundreds of breaches and working with security teams across industries, I've found three signals that predict real-world exploitation far better than CVSS alone:
Signal 1: Exploit Prediction Scoring System (EPSS)
EPSS uses machine learning to predict the probability a vulnerability will be exploited in the next 30 days. It considers:
- Public exploit code availability
- Threat intelligence feeds
- Security researcher activity
- Historical exploitation patterns
- Vendor patch timing
Here's what EPSS scores look like in practice:
# Check EPSS score for a CVE
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2021-44228" | jq
{
"cve": "CVE-2021-44228", # Log4Shell
"epss": "0.97590", # 97.6% probability of exploitation
"percentile": "1.00000" # Top 0.1% of all CVEs
}
Compare that to a random high-CVSS vuln with low exploitability:
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2023-12345" | jq
{
"cve": "CVE-2023-12345",
"epss": "0.00044", # 0.04% probability
"percentile": "0.15234" # Bottom 15% of all CVEs
}
The difference is clear: EPSS 0.97 vs 0.0004—a 2,400x difference in exploitation likelihood.
Signal 2: Reachability Analysis
A critical vulnerability in code that never executes isn't a risk. A moderate vulnerability in your authentication logic is.
Most teams don't know which vulnerabilities are in code paths that actually run:
# This vulnerable function is defined...
def process_user_input(data):
eval(data) # CRITICAL: Arbitrary code execution!
# But is it ever called?
# If this code is in a deprecated module that's never imported,
# is it really a priority over the HIGH vuln in your login endpoint?
Tools like CodeQL and Snyk can perform reachability analysis:
# Check if vulnerable code paths are reachable
codeql database analyze ./db \
--format=sarif-latest \
--output=results.sarif \
--reachability
The shocking reality: In most codebases, 40-60% of dependencies with known vulnerabilities are in code paths that never execute. You're patching library functions that literally never get called.
Signal 3: Asset Context (What's Actually Exposed)
A vulnerability's risk depends entirely on what it protects:
Low Priority:
- CVSS 9.8 in an internal dev tool
- Only accessible from corporate VPN
- Processes non-sensitive data
- Can be patched during normal maintenance
High Priority:
- CVSS 7.0 in your payment processing API
- Internet-facing on port 443
- Handles customer PII and credit cards
- Breach triggers regulatory penalties
Same company, different risk profiles. Context is everything.
The Prioritization Framework That Works
Here's the framework that's helped dozens of teams cut their vulnerability backlog by 70% while actually reducing risk:
Step 1: Enrich Your Vulnerability Data
# Standard vulnerability scan
trivy image myapp:latest --format json > vulns.json
# Enrich with EPSS scores
cat vulns.json | jq -r '.Results[].Vulnerabilities[].VulnerabilityID' | \
while read cve; do
epss=$(curl -s "https://api.first.org/data/v1/epss?cve=$cve" | jq -r '.data[0].epss')
echo "$cve: EPSS=$epss"
done
# Enrich with reachability (if available)
snyk test --json --reachable > reachable.json
Step 2: Calculate Risk Score
def calculate_risk_score(vuln):
"""
Combines multiple signals into actionable risk score
"""
# CVSS (0-10) - baseline severity
cvss_score = vuln['cvss'] / 10.0
# EPSS (0-1) - exploitation probability
epss_score = vuln['epss']
# Reachability (0 or 1) - is code actually used?
reachability = 1.0 if vuln['reachable'] else 0.3
# Exposure (0-1) - what's the asset exposure?
exposure = {
'internet_facing': 1.0,
'internal': 0.5,
'dev_only': 0.2
}[vuln['exposure']]
# Data sensitivity (0-1) - what does it protect?
sensitivity = {
'pii_financial': 1.0,
'business_critical': 0.8,
'internal_only': 0.4,
'public_data': 0.2
}[vuln['data_class']]
# Weighted formula (tune weights for your org)
risk = (
cvss_score * 0.2 + # Severity is only 20% of the equation
epss_score * 0.4 + # Exploit probability is 40%
reachability * 0.2 + # Reachability is 20%
exposure * 0.1 + # Exposure is 10%
sensitivity * 0.1 # Data sensitivity is 10%
)
return risk
Step 3: Prioritize and Patch
Sort by risk score, not CVSS:
# Old way (CVSS only)
vulns.sort(key=lambda v: v['cvss'], reverse=True)
# New way (multi-signal risk)
vulns.sort(key=calculate_risk_score, reverse=True)
The difference in your patch queue is dramatic:
CVSS-Only Prioritization:
- CVE-2023-XXXX (CVSS 9.8, EPSS 0.002, not reachable, internal)
- CVE-2023-YYYY (CVSS 9.5, EPSS 0.001, not reachable, dev env)
- CVE-2023-ZZZZ (CVSS 9.1, EPSS 0.003, reachable, internal)
Risk-Based Prioritization:
- CVE-2021-44228 (CVSS 10.0, EPSS 0.976, reachable, internet-facing) ← Log4Shell
- CVE-2023-AAAA (CVSS 7.5, EPSS 0.842, reachable, internet-facing)
- CVE-2023-BBBB (CVSS 8.1, EPSS 0.723, reachable, API endpoint)
See the difference? The second list actually reflects real-world threat.
How CodePhreak Does This Automatically
Manually enriching vulnerability data with EPSS, reachability, and context is time-consuming. CodePhreak automates the entire workflow:
# Comprehensive vulnerability scan with risk prioritization
codephreak scan . \
--enrich-epss \
--reachability-analysis \
--asset-context production \
--output prioritized-vulns.json
# Output includes risk-scored vulnerabilities:
{
"findings": [
{
"cve": "CVE-2021-44228",
"package": "log4j-core@2.14.1",
"cvss": 10.0,
"epss": 0.97590,
"reachable": true,
"exposure": "internet_facing",
"risk_score": 0.94, # ← Actionable risk score
"priority": "CRITICAL",
"remediation": "Upgrade to log4j-core@2.17.1",
"exploit_available": true,
"exploited_in_wild": true
}
]
}
The scan automatically:
- Queries EPSS API for exploitation probabilities
- Performs reachability analysis on your codebase
- Considers asset context (production vs staging vs dev)
- Calculates risk scores combining all signals
- Prioritizes your patch queue by actual risk
The 15-Minute Vulnerability Triage
You don't need to analyze all 10,000 vulns. Focus on the top 50 by risk score:
# Get your top 50 riskiest vulnerabilities
codephreak scan . --format json | \
jq -r '.findings | sort_by(-.risk_score) | .[0:50] | .[] |
"\(.priority)\t\(.cve)\t\(.package)\t\(.risk_score)"'
# Output:
# CRITICAL CVE-2021-44228 log4j-core@2.14.1 0.94
# CRITICAL CVE-2023-12345 spring-web@5.3.1 0.87
# HIGH CVE-2023-54321 jackson-databind@2.9.8 0.76
Patch these 50 first. You'll eliminate more risk in one sprint than six months of CVSS-only patching.
What You Should Do This Week
Monday (30 minutes):
- Export your current vulnerability backlog
- Add EPSS scores to your top 100 CVEs
- Compare CVSS-only vs EPSS-enriched prioritization
Tuesday (1 hour):
- Categorize your assets by exposure (internet-facing, internal, dev)
- Tag vulnerabilities with asset context
Wednesday (2 hours):
- Implement basic risk scoring (CVSS + EPSS + exposure)
- Re-prioritize your patch queue
Thursday-Friday:
- Patch your new top 20 vulnerabilities
- Measure: How many had active exploits? How many were internet-facing?
You'll immediately see the difference between patching by severity vs patching by risk.
The Uncomfortable Truth
Your team is busy. I get it. You're triaging thousands of vulnerabilities with limited resources and impossible timelines.
But being busy patching the wrong things is worse than doing nothing. At least doing nothing doesn't create a false sense of security.
The teams that get breached aren't the ones with the most vulnerabilities—they're the ones that patched the wrong vulnerabilities.
Log4Shell taught us this lesson painfully. Thousands of companies had it in their backlog, deprioritized because other things were "more critical" based on CVSS. Those companies are still dealing with the aftermath.
Don't let your prioritization system be the reason for your next breach.
Quick Reference: Risk-Based Prioritization Checklist
Data Collection:
- CVSS scores (baseline severity)
- EPSS scores (exploitation probability)
- Reachability analysis (is code actually used?)
- Asset classification (internet-facing vs internal)
- Data sensitivity (what does it protect?)
Risk Factors (High Priority):
- EPSS > 0.7 (high exploitation probability)
- Public exploit code available
- Internet-facing assets
- Handles PII, financial data, or credentials
- Code path is reachable and actively used
Risk Factors (Can Wait):
- EPSS < 0.01 (low exploitation probability)
- No public exploits
- Internal-only or dev environment
- Non-sensitive data
- Unreachable code paths
Patch Priority Formula:
Risk Score = (CVSS × 0.2) + (EPSS × 0.4) + (Reachability × 0.2) +
(Exposure × 0.1) + (Sensitivity × 0.1)
Start Prioritizing Smarter Today:
# Install CodePhreak
pip install codephreak-security-auditor
# Scan with EPSS enrichment
codephreak scan . --enrich-epss --reachability
# Get risk-prioritized patch queue
codephreak scan . --output prioritized.html