7.7 KiB
EPSS Integration Guide
Overview
EPSS (Exploit Prediction Scoring System) is a FIRST.org initiative that provides probability scores for vulnerability exploitation within 30 days. StellaOps integrates EPSS as a risk signal alongside CVSS and KEV (Known Exploited Vulnerabilities) to provide more accurate vulnerability prioritization.
How EPSS Works
EPSS uses machine learning to predict the probability that a CVE will be exploited in the wild within the next 30 days. The model considers:
- Vulnerability characteristics (CVSS metrics, CWE, etc.)
- Social signals (Twitter mentions, GitHub issues, etc.)
- Exploit database entries
- Historical exploitation patterns
EPSS outputs two values:
- Score (0.0-1.0): Probability of exploitation in next 30 days
- Percentile (0-100): Ranking relative to all other CVEs
How EPSS Affects Risk Scoring in StellaOps
Combined Risk Formula
StellaOps combines CVSS, KEV, and EPSS signals into a unified risk score:
risk_score = clamp01(
(cvss / 10) + # Base severity (0-1)
kevBonus + # +0.20 if in CISA KEV
epssBonus # +0.02 to +0.10 based on percentile
)
EPSS Bonus Thresholds
| EPSS Percentile | Bonus | Rationale |
|---|---|---|
| >= 99th | +10% | Top 1% most likely to be exploited; urgent priority |
| >= 90th | +5% | Top 10%; high exploitation probability |
| >= 50th | +2% | Above median; moderate additional risk |
| < 50th | 0% | Below median; no bonus applied |
Example Calculations
| CVE | CVSS | KEV | EPSS Percentile | Risk Score |
|---|---|---|---|---|
| CVE-2024-1234 | 9.8 | Yes | 99.5th | 1.00 (clamped) |
| CVE-2024-5678 | 7.5 | No | 95th | 0.80 |
| CVE-2024-9012 | 6.0 | No | 60th | 0.62 |
| CVE-2024-3456 | 8.0 | No | 30th | 0.80 |
Implementation Reference
IEpssSource Interface
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/IEpssSources.cs
public interface IEpssSource
{
/// <summary>
/// Returns EPSS data for the given CVE identifier, or null if unknown.
/// </summary>
Task<EpssData?> GetEpssAsync(string cveId, CancellationToken cancellationToken);
}
public sealed record EpssData(double Score, double Percentile, DateTimeOffset? ModelVersion = null);
Risk Providers
EpssProvider - Uses EPSS score directly as risk (0.0-1.0):
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/EpssProvider.cs
public const string ProviderName = "epss";
CvssKevEpssProvider - Combined provider using all three signals:
// Location: src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.Core/Providers/EpssProvider.cs
public const string ProviderName = "cvss-kev-epss";
Policy Configuration
Enabling EPSS Integration
# etc/risk-engine.yaml
risk:
providers:
- name: cvss-kev-epss
enabled: true
priority: 1
epss:
enabled: true
source: database # or "api" for live FIRST API
cache_ttl: 24h
# Percentile-based bonus thresholds
thresholds:
- percentile: 99
bonus: 0.10
- percentile: 90
bonus: 0.05
- percentile: 50
bonus: 0.02
Custom Threshold Configuration
Organizations can customize EPSS bonus thresholds based on their risk tolerance:
# More aggressive (higher bonuses for high-risk vulns)
epss:
thresholds:
- percentile: 99
bonus: 0.15
- percentile: 95
bonus: 0.10
- percentile: 75
bonus: 0.05
# More conservative (smaller bonuses)
epss:
thresholds:
- percentile: 99
bonus: 0.05
- percentile: 95
bonus: 0.02
EPSS in Lattice Decisions
EPSS influences VEX lattice state transitions for vulnerability triage:
| Current State | EPSS >= 90th Percentile | Recommended Action |
|---|---|---|
| SR (Static Reachable) | Yes | Escalate to CR (Confirmed Reachable) priority |
| SU (Static Unreachable) | Yes | Flag for review - high exploit probability despite unreachable |
| DV (Denied by Vendor VEX) | Yes | Review denial validity - exploit activity contradicts vendor |
| U (Unknown) | Yes | Prioritize for reachability analysis |
VEX Policy Example
# etc/vex-policy.yaml
lattice:
transitions:
- from: SR
to: CR
condition:
epss_percentile: ">= 90"
action: auto_escalate
- from: SU
to: REVIEW
condition:
epss_percentile: ">= 95"
action: flag_for_review
reason: "High EPSS despite static unreachability"
Offline EPSS Data
EPSS data is included in offline risk bundles for air-gapped environments.
Bundle Structure
risk-bundle-2025-12-14/
├── manifest.json
├── kev/
│ └── kev-catalog.json
├── epss/
│ ├── epss-scores.csv.zst # Compressed EPSS data
│ └── epss-metadata.json # Model date, row count, checksum
└── signatures/
└── bundle.dsse.json
EPSS Metadata
{
"model_date": "2025-12-14",
"row_count": 248732,
"sha256": "abc123...",
"source": "first.org",
"created_at": "2025-12-14T00:00:00Z"
}
Importing Offline EPSS Data
# Import risk bundle (includes EPSS)
stellaops offline import --kit risk-bundle-2025-12-14.tar.zst
# Verify EPSS data imported
stellaops epss status
# Output:
# EPSS Data Status:
# Model Date: 2025-12-14
# CVE Count: 248,732
# Last Import: 2025-12-14T10:30:00Z
Accuracy Considerations
| Metric | Value | Notes |
|---|---|---|
| EPSS Coverage | ~95% of NVD CVEs | Some very new CVEs (<24h) not yet scored |
| Model Refresh | Daily | Scores can change day-to-day |
| Prediction Window | 30 days | Probability of exploit in next 30 days |
| Historical Accuracy | ~85% AUC | Based on FIRST published evaluations |
Limitations
- New CVEs: Very recent CVEs may not have EPSS scores yet
- Model Lag: EPSS model updates daily; real-world exploit activity may be faster
- Zero-Days: Pre-disclosure vulnerabilities cannot be scored
- Context Blind: EPSS doesn't consider your specific environment
Best Practices
- Combine Signals: Always use EPSS alongside CVSS and KEV, not in isolation
- Review High EPSS: Manually review vulnerabilities with EPSS >= 95th percentile
- Track Changes: Monitor EPSS score changes over time for trending threats
- Update Regularly: Keep EPSS data fresh (daily in online mode, weekly for offline)
- Verify High-Risk: For critical decisions, verify EPSS data against FIRST API
API Usage
Query EPSS Score
# Get EPSS score for a specific CVE
stellaops epss get CVE-2024-12345
# Batch query
stellaops epss batch --file cves.txt --output epss-scores.json
Programmatic Access
// Using IEpssSource
var epssData = await epssSource.GetEpssAsync("CVE-2024-12345", cancellationToken);
if (epssData is not null)
{
Console.WriteLine($"Score: {epssData.Score:P2}");
Console.WriteLine($"Percentile: {epssData.Percentile:F1}th");
}
Troubleshooting
EPSS Data Not Available
# Check EPSS source status
stellaops epss status
# Force refresh from FIRST API
stellaops epss refresh --force
# Check for specific CVE
stellaops epss get CVE-2024-12345 --verbose
Stale EPSS Data
If EPSS data is older than 7 days:
# Check staleness
stellaops epss check-staleness
# Import fresh bundle
stellaops offline import --kit latest-bundle.tar.zst