Time-to-Exploit Survival Analysis

What Survival Analysis of the CISA KEV Catalog Reveals About the SLA-to-Exploit Mismatch

Author

Patrick Lefler

Published

September 10, 2026

Abstract
Fourteen-day and thirty-day patch deadlines are based on the idea that vulnerability severity predicts how attackers behave. In reality, it does not. This project uses survival analysis, a method from actuarial science and clinical trials for studying time-to-event data, to examine 196,000 vulnerabilities disclosed since November 2021. The analysis draws on the National Vulnerability Database, Cyentia’s EPSS scores, and CISA’s Known Exploited Vulnerabilities (KEV) catalog to identify when each flaw was first weaponized. Out of the entire group, only 815 vulnerabilities (0.4%) were ever confirmed as exploited, leaving the other 99.6% unexploited as of the observation cutoff. Among those exploited, 215 vulnerabilities (26.4%) were already active on or before their public disclosure date. This day-zero gap cannot be closed by any patching SLA, no matter how aggressive. For vulnerabilities weaponized after disclosure, the median time to KEV inclusion is 31 days. This is shorter than the 30-day remediation window most organizations use for high-severity flaws. By the 14-day critical-patch deadline, about 70% of network-exploitable weaponized flaws are already active in the wild. The findings suggest moving away from uniform, severity-based patch cycles and instead using exposure-based SLAs: 72 hours to 7 days for internet-facing, network-exploitable systems, and 60 to 90 days for all others. Since CISA KEV only logs confirmed, federal-relevant exploitation, the 0.4% weaponization rate is a minimum. Any unreported or undetected exploitation would only make the case stronger for focusing patch urgency on the network-exposed minority.

Introduction

Every company today faces a growing cybersecurity challenge. Engineering teams spend millions of valuable developer hours constantly patching long lists of “Critical” and “High” software flaws. In the past, company policies and regulations have set strict, calendar-based Service Level Agreements (SLAs), usually requiring IT to fix critical vulnerabilities within 14 days and high-severity issues within 30 days. This compliance-focused approach may look diligent, but it treats all vulnerabilities as equally risky and assumes that 14- or 30-day deadlines match how quickly real attackers move.

The main problem with this old model is that it mixes up how bad a flaw could be with whether anyone is actually using it to attack. Vulnerability scanners use the Common Vulnerability Scoring System (CVSS) to rate how much damage a flaw could cause, but this does not show if attackers are really exploiting it. In fact, less than one percent of all known vulnerabilities are ever used by threat actors. When companies use severity alone to decide on emergency fixes, they waste valuable engineering time on patches for issues that are not a real threat, which leads to staff burnout and slows down important business projects.

To address this problem of how to best use resources, this project uses Time-to-Exploit Survival Analysis. This method borrows from the math used in medical trials and life insurance to estimate how long a vulnerability exists before it is attacked. By combining real data from the Cybersecurity and Infrastructure Security Agency’s Known Exploited Vulnerabilities (CISA KEV) catalog, National Vulnerability Database, and machine-learning predictions (EPSS), the model shows how quickly attackers take advantage of flaws in different types of software and situations.

For executive leaders and the Board, the main goals are to use resources wisely and to meet regulatory requirements. Instead of relying on arbitrary deadlines during audits or after incidents, this analysis gives leaders real data to guide decisions. It shows where patching timelines can be safely extended to regular release cycles, highlights the small group of internet-facing risks that need urgent action within 72 hours, and addresses the important “Day-Zero Gap” when quick patching is not enough to protect company assets.

Display code
# Setup & Environment

  library(broom)
  library(gt)
  library(lubridate)
  library(reactable)
  library(scales)
  library(sessioninfo)
  library(survival)
  library(survminer)
  library(tidyverse)

theme_set(
  theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      plot.subtitle = element_text(color = "gray30", margin = margin(b = 10)),
      panel.grid.minor = element_blank(),
      legend.position = "bottom"
    )
)

The Raw Data

This analysis is grounded in an integrated threat-intelligence dataset from Kaggle, a globally recognized platform for open data science, machine learning research, and analytical benchmarking. Rather than relying on isolated security feeds or subjective vendor reports, this curated repository brings together the cybersecurity ecosystem’s three most authoritative data pipelines into a unified schema: the National Vulnerability Database (NVD), the Cybersecurity and Infrastructure Security Agency’s Known Exploited Vulnerabilities (CISA KEV) catalog, and Cyentia Institute’s Exploit Prediction Scoring System (EPSS).

The main dataset used for analysis is the enriched file (cve_cisa_epss_enriched_dataset.csv), which contains data on hundreds of thousands of past software vulnerabilities. This file connects Common Vulnerabilities and Exposures (CVE) identifiers with their related Common Vulnerability Scoring System (CVSS) details, such as base scores and attack paths like remote network or local access. It also links these technical details with real-world threat indicators, including weaponization tracking and EPSS probability scores. EPSS is a machine-learning metric that estimates the chance a vulnerability will be exploited within 30 days.

Alongside the main metrics, a second file (cve_corpus.csv) supports natural language processing and audit trails. This file collects narrative descriptions, vendor release notes, security advisory references, and Common Weakness Enumeration (CWE) categories for each vulnerability. While the main analysis uses the numbers and hazard data from the enriched dataset, the corpus explains how and why certain flaws are weaponized.

For executive committees, the main benefit of this combined data is its strong governance. In many organizations, cybersecurity reporting is fragmented because patching, vulnerability scanning, and threat intelligence are handled separately. By bringing together flaw disclosures, real-world exploitation records, and predictive machine-learning indicators in one place, this dataset offers a clear, reliable way to check whether company remediation efforts match actual threat activity.

Data Ingestion

The raw Kaggle dataset (raw_df) brings together NVD vulnerability scores and Cyentia EPSS probabilities, but it has a key limitation for long-term analysis. The CISA KEV status is only shown as a simple true or false flag, without any event dates. For survival analysis, we need two time points: when a vulnerability becomes a risk and when it is actually exploited. Since we cannot calculate the time-to-exploit without knowing when a flaw was weaponized, we needed to combine the static Kaggle data with an external time-series source.

To solve this, the pipeline pulls data from CISA’s main feed (kev_feed) to get the exact date (dateAdded) when each vulnerability was added to the federal weaponization list. These dates are collected in kev_dates. By joining this live feed with raw_df using the CVE identifier, we can match each vulnerability’s exploitability details and EPSS score with the actual date it was weaponized. For about 99.6% of vulnerabilities that were never weaponized, the pipeline sets an observation end date (snapshot_date) based on the latest disclosure date in the dataset. This marks the point where we stop tracking and defines the right-censoring limit.

Display code
# Ingest CISA KEV directly to obtain dateAdded timestamps
cisa_url <- "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
kev_feed <- jsonlite::fromJSON(cisa_url)

kev_dates <- as_tibble(kev_feed$vulnerabilities) %>%
  transmute(
    cve_id = cveID,
    kev_date = as.Date(dateAdded)
  ) %>%
  distinct(cve_id, .keep_all = TRUE)

# Ingest CVE CISA EPSS Enriched dataset

raw_df <- read_csv("data/cve_cisa_epss_enriched_dataset.csv", show_col_types = FALSE)

Turning Raw Data into a Survival Cohort

Before modeling started, the unified dataset needed careful cleaning to remove long-term distortions. An initial review showed that many old vulnerabilities from the early 2000s appeared with recent publication dates because of updates at NVD. Also, some snapshot boundaries were not set correctly, which led to negative observation periods for new flaws. The engineering process fixes these issues by only including disclosures from November 1, 2021, onward (matching CISA Binding Operational Directive 22-01) and making sure the CVE identifier is from 2021 or later. This approach helps the survival model focus on current attacker methods and widely available exploit tools, not outdated software cycles.

The last step turns the raw tables into the survival_cohort, which sets up the data for both parametric and semi-parametric modeling. Each entry gets a binary indicator: 1 for confirmed KEV weaponization, and 0 for cases where the event did not happen. The follow-up time is also calculated. If a vulnerability is exploited before it is officially listed in the NVD, the pipeline marks it as is_zero_day for further review and sets its duration to 0.5 days. This setup keeps day-zero failures as immediate drops in the survival curve and avoids errors in regression analysis, resulting in a reliable dataset for SLA calibration.

Display code
# Define observation snapshot dynamically from data boundaries
max_nvd_date <- max(as.Date(raw_df$published_date), na.rm = TRUE)
max_kev_date <- max(kev_dates$kev_date, na.rm = TRUE)
cohort_snapshot_date <- max(max_nvd_date, max_kev_date)

# Build survival cohort using confirmed raw_df column names
survival_cohort <- raw_df %>%
  transmute(
    cve_id = cve_id,
    cve_year = as.integer(str_extract(cve_id, "(?<=CVE-)\\d{4}")),
    pub_date = as.Date(published_date),
    cisa_kev = as.logical(cisa_kev),
    epss = as.numeric(epss_score),
    attack_vector = factor(attack_vector, levels = c("NETWORK", "ADJACENT_NETWORK", "LOCAL", "PHYSICAL")),
    attack_complexity = factor(attack_complexity, levels = c("LOW", "HIGH")),
    privileges_required = factor(privileges_required, levels = c("NONE", "LOW", "HIGH")),
    user_interaction = factor(user_interaction, levels = c("NONE", "REQUIRED")),
    base_score = as.numeric(base_score)
  ) %>%
  # Exclude legacy CVE backfills and focus on modern post-BOD 22-01 cohort
  filter(
    pub_date >= as.Date("2021-11-01"),
    cve_year >= 2021
  ) %>%
  left_join(kev_dates, by = "cve_id") %>%
  mutate(
    event = if_else(!is.na(kev_date) | cisa_kev == TRUE, 1L, 0L),

    # Calculate follow-up duration
    delta_days = if_else(
      event == 1L,
      as.numeric(coalesce(kev_date, cohort_snapshot_date) - pub_date),
      as.numeric(cohort_snapshot_date - pub_date)
    ),

    # Flag pre-disclosure/zero-day exploits
    is_zero_day = if_else(event == 1L & delta_days <= 0, 1L, 0L),

    # Left-truncate/offset instantaneous failures to 0.5 days; keep censored t > 0
    duration_days = case_when(
      event == 1L & delta_days <= 0 ~ 0.5,
      delta_days <= 0 ~ 0.5, # Boundary guard for same-day releases
      TRUE ~ delta_days
    )
  ) %>%
  # Drop records with missing core CVSS vector data
  filter(!is.na(attack_vector), !is.na(attack_complexity))

# Sanity check: verify no negative durations remain
stopifnot(all(survival_cohort$duration_days > 0))

# Preview table is built from the final cohort above, so it always matches
# what every downstream table and chart in this document actually uses.
reactTable <- reactable(head(survival_cohort, n = 50),
  highlight = TRUE,
  striped = TRUE,
  defaultColDef = colDef(
    headerStyle = list(
        textAlign = "center",
        lineHeight = "12px",
        textTransform = "uppercase",
        color = "black",
        fontWeight = "400",
        borderBottom = "2px solid grey50",
        paddingBottom = "3px",
        verticalAlign = "bottom")
    ),
  columns = list(
    cve_id = colDef(width = 100),
    cve_year = colDef(width = 70),
    pub_date = colDef(width = 80),
    cisa_kev = colDef(width = 65),
    epss = colDef(width = 70),
    attack_vector = colDef(width = 130),
    attack_complexity = colDef(width = 135),
    privileges_required = colDef(width = 160),
    user_interaction = colDef(width = 150),
    base_score = colDef(width = 85),
    kev_date = colDef(width = 70),
    event = colDef(width = 60),
    delta_days = colDef(width = 80),
    is_zero_day = colDef(width = 85),
    duration_days = colDef(width = 105)
  ),
  class = ".react_table",
  theme = reactableTheme(
    borderColor = "#dfe2e5",
    stripedColor = "#f6f8fa",
    style = list(fontSize = "11px", fontFamily = "Roboto, Helvetica, Arial, sans-serif"),
    searchInputStyle = list(width = "100%")
  )
)
NoteWhy Survival Analysis?

Survival analysis is a branch of statistics first used in life insurance and medical studies to look at how long it takes for something to happen. Instead of just asking if a patient survives or if an insurance policyholder reaches a certain age, it measures how much time passes before a specific event. In this project, survival analysis is used to study software vulnerabilities. Here, the ‘birth’ is when a vulnerability is publicly disclosed, and the final ‘event’ is when real-world attackers weaponize it, as listed in CISA’s Known Exploited Vulnerabilities catalog.

This method is especially important for managing vulnerabilities because of a problem called right-censoring. Out of 196,000 vulnerabilities in the current data, more than 99.6% have never been exploited. Regular averages and regression methods do not work well here. If unexploited flaws were treated as ‘safe,’ we ignore that they could still be attacked in the future. If left out, our results focus too much on the small number of quick failures. Survival analysis solves this by including both groups. It uses the exact timing of known attacks and also gives credit to unexploited flaws for every day they remain untouched.

For executives and board members, survival analysis offers a real measure of time and speed instead of relying on guesswork or severity scores. Like actuaries who use real data to set insurance reserves, this approach helps risk teams understand how quickly attackers move. This provides a solid, data-driven way to set Service Level Agreements. And it helps leaders see which risks need urgent action and which ones can wait for regular maintenance.

Survival Cohort Data

The Survival Cohort data contains approximately 200,000 rows. The first fifty rows are displayed below to provide context.

cve_id: The official, standardized Common Vulnerabilities and Exposures tracking identifier assigned to the vulnerability.

cve_year: The four-digit calendar year extracted from the vulnerability identifier, used to filter out legacy CVE backfills.

pub_date: The date the vulnerability entry was officially cataloged and published in the National Vulnerability Database (NVD).

cisa_kev: A boolean flag indicating whether the vulnerability was marked as actively exploited within the source dataset.

epss: The continuous Exploit Prediction Scoring System probability estimating the likelihood of exploitation in the wild within 30 days.

attack_vector: The CVSS categorical metric defining the network context required for an adversary to reach the vulnerability (e.g., Network, Adjacent, Local, Physical).

attack_complexity: The CVSS categorical metric measuring the difficulty of conditions outside the attacker’s control required to execute an exploit.

privileges_required: The CVSS categorical metric defining the level of authentication an attacker must possess prior to exploiting the flaw.

user_interaction: The CVSS categorical metric capturing whether a human user must take an action for the vulnerability to be successfully triggered.

base_score: The overall numerical CVSS v3 severity score ranging from 0.0 to 10.0 based on technical exploitability and impact metrics.

kev_date: The calendar date when CISA formally added the vulnerability to its Known Exploited Vulnerabilities catalog.

event: The binary survival analysis status indicator, where 1 signifies confirmed real-world weaponization and 0 denotes right-censoring.

delta_days: The raw elapsed duration in days from NVD publication to either active weaponization or the right-censoring cutoff (cohort_snapshot_date, the later of the maximum NVD publication date and the maximum CISA KEV addition date in the data).

is_zero_day: A binary classification flag identifying whether confirmed exploitation occurred on or prior to formal NVD public disclosure.

duration_days: The finalized non-negative survival time variable, where instantaneous or pre-disclosure failures are bounded to 0.5 days for valid statistical modeling.

Cohort Follow-up Duration Summary Table

Why this table matters
This summary table establishes the empirical observation runway used to evaluate software vulnerability lifecycles since late 2021. In risk and actuarial modeling, the validity of any survival calculation depends on having sufficient follow-up time to observe whether an adverse event—in this case, active adversary weaponization—actually occurs. With an average observation span of 682.3 days and a median of 603.0 days (nearly 20 months), the dataset captures an extensive operational timeframe rather than a fleeting snapshot. This assures executive leadership and the Board that our conclusions are drawn from mature, real-world data reflecting modern threat-actor behavior.

Display code
# Extract five-number summary plus mean into a tidy tibble
duration_summary_calc <- survival_cohort %>%
  summarise(
    Min = min(duration_days, na.rm = TRUE),
    Q1  = quantile(duration_days, 0.25, na.rm = TRUE),
    Median = median(duration_days, na.rm = TRUE),
    Mean = mean(duration_days, na.rm = TRUE),
    Q3  = quantile(duration_days, 0.75, na.rm = TRUE),
    Max = max(duration_days, na.rm = TRUE)
  ) %>%
  pivot_longer(
    cols = everything(),
    names_to = "Metric",
    values_to = "Days"
  ) %>%
  mutate(
    Description = case_when(
      Metric == "Min"    ~ "Lower bound / bounded instantaneous failure (t = 0.5)",
      Metric == "Q1"     ~ "25th percentile of follow-up observation",
      Metric == "Median" ~ "Midpoint follow-up duration",
      Metric == "Mean"   ~ "Average follow-up duration across cohort",
      Metric == "Q3"     ~ "75th percentile of follow-up observation",
      Metric == "Max"    ~ "Maximum duration observed post-BOD 22-01"
    )
  )


# Render professional gt table
duration_gt <- duration_summary_calc %>%
  gt() %>%
  tab_header(
    title = md("**Cohort Follow-up Duration Summary**"),
    subtitle = "Distribution of observation and weaponization windows (`duration_days`)"
  ) %>%
  cols_label(
    Metric = md("**Statistic**"),
    Days = md("**Duration (Days)**"),
    Description = md("**Analytical Context**")
  ) %>%
  fmt_number(
    columns = Days,
    decimals = 1
  ) %>%
  cols_align(
    align = "left",
    columns = c(Metric, Description)
  ) %>%
  cols_align(
    align = "right",
    columns = Days
  ) %>%
  tab_options(
    heading.align = "left",
    table.align = "left",
    table.border.bottom.style = "solid",
    table.border.bottom.width = px(2),
    table.border.bottom.color = "#2c3e50",
    column_labels.border.bottom.style = "solid",
    column_labels.border.bottom.width = px(1.5),
    column_labels.border.bottom.color = "#2c3e50",
    table.font.size = px(12),
    data_row.padding = px(6)
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_body(
      columns = Metric
    )
  )
Cohort Follow-up Duration Summary
Distribution of observation and weaponization windows (`duration_days`)
Statistic Duration (Days) Analytical Context
Min 0.5 Lower bound / bounded instantaneous failure (t = 0.5)
Q1 219.0 25th percentile of follow-up observation
Median 610.0 Midpoint follow-up duration
Mean 689.3 Average follow-up duration across cohort
Q3 1,073.0 75th percentile of follow-up observation
Max 1,782.0 Maximum duration observed post-BOD 22-01

Understanding the Cohort Follow-up Duration Summary table results
The data highlights a major gap in operations: relying only on reactive patching SLAs is not enough. In the latest group studied, 215 out of 815 weaponized vulnerabilities—over 26%—were actively exploited on or before their public NVD disclosure date. This means that for more than one in four real-world threats, organizations start fixing the problem only after attackers have already acted. Standard 14- or 30-day patching rules offer limited protection against these day-zero threats. This clearly shows why the Board should invest in layered security measures like micro-segmentation, zero trust access controls, and attack surface reduction, instead of depending only on patching deadlines.

The timeline is just as tough for vulnerabilities that are weaponized after they are disclosed. For these, the median time to CISA KEV inclusion is only 31 days. This fast pace makes standard industry compliance rules, like “30-day Critical / 60-day High” patching policies, ineffective. By the time an engineering team finishes a typical 30-day patching cycle, attackers have already exploited half of these flaws. Relying on standard maintenance cycles means organizations are always a step behind attackers.

However, most of the risk comes from a small group of threats. While some attacks happen within half a day of disclosure, most vulnerabilities are never targeted by attackers. Half of all vulnerabilities go more than 600 days without being weaponized, and 75% last almost three years (1,066 days). Leaders should move away from broad, calendar-based patching and instead use a two-part approach: respond quickly—within 72 hours—to the small number of weaponized, network-exposed threats, and handle the rest during regular update cycles.


Cumulative Exploitation Rate by Attack Vector Plot

Why this plot matters
The Cumulative Exploitation Rate by Attack Vector plot shows the chance that a publicly disclosed software vulnerability will be used by attackers within its first 90 days. It covers almost 200,000 vulnerabilities recorded since late 2021 and tracks how likely each is to be added to the CISA Known Exploited Vulnerabilities (KEV) list. The data is grouped by how attackers can reach the flaw: over the internet (NETWORK), on the same local network (ADJACENT_NETWORK), through a logged-in session (LOCAL), or by direct hardware access (PHYSICAL). The dashed lines mark standard corporate deadlines for fixing issues at 14, 30, and 60 days.

Display code
# ------------------------------------------------------------------------------
# 1. Panel A: Cumulative Exploitation Probability (Cohort-Wide)
# ------------------------------------------------------------------------------

# Extract tidy survival curve data
km_tidy <- survfit(Surv(duration_days, event) ~ attack_vector, data = survival_cohort) %>%
  broom::tidy() %>%
  mutate(
    attack_vector = str_remove(strata, "attack_vector="),
    cum_hazard_pct = (1 - estimate) * 100 # Invert to % exploited
  ) %>%
  filter(time <= 90)

# Identify endpoint values for direct labeling
panel_a_labels <- km_tidy %>%
  group_by(attack_vector) %>%
  filter(time <= 90) %>%
  slice_tail(n = 1)

p1 <- ggplot(km_tidy, aes(x = time, y = cum_hazard_pct, color = attack_vector)) +
  geom_step(linewidth = 1.1) +
  # Reference mandate lines
  geom_vline(xintercept = c(14, 30, 60), linetype = "dashed", color = "gray55", linewidth = 0.5) +
  annotate("text", x = 14, y = max(km_tidy$cum_hazard_pct) * 0.95, label = "14d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  annotate("text", x = 30, y = max(km_tidy$cum_hazard_pct) * 0.95, label = "30d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  annotate("text", x = 60, y = max(km_tidy$cum_hazard_pct) * 0.95, label = "60d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  scale_x_continuous(breaks = seq(0, 90, by = 15), limits = c(0, 95)) +
  scale_y_continuous(labels = function(x) paste0(x, "%")) +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Cumulative Exploitation Rate by Attack Vector",
    subtitle = "Probability of CISA KEV weaponization across all disclosed vulnerabilities (N ≈ 196k)",
    x = "Days Post-NVD Disclosure",
    y = "Cumulative Exploited (% of All CVEs)",
    color = "Attack Vector"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    legend.position = "bottom",
    plot.title = element_text(face = "bold", size = 13),
    panel.grid.minor = element_blank()
  )

p1

How to read the plot
Start with the timeline on the horizontal axis, which runs from Day 0 (disclosure) to 90 days. The vertical axis shows the percentage of weaponized vulnerabilities. Each colored stepped line represents a different entry point: NETWORK (green) for remote internet exposures, LOCAL (blue) for logged-in host sessions, PHYSICAL (purple) for direct hardware interactions, and ADJACENT_NETWORK (red) for same-segment network flaws. The three vertical dashed lines mark standard compliance deadlines at 14, 30, and 60 days, showing how much risk has built up by each point.

The plot shows two main patterns. At Day 0, LOCAL vulnerabilities jump to about 0.19% and PHYSICAL flaws rise to around 0.13%. These early spikes reflect pre-disclosure and zero-day exploits, when there was no time to patch. After that, LOCAL flaws increase quickly, reaching about 0.26% by the 14-day deadline and leveling off near 0.31% at 90 days. NETWORK vulnerabilities, on the other hand, grow steadily. They pass 0.22% at Day 14, overtake LOCAL flaws around Day 42 at about 0.29%, and end up as the highest risk at roughly 0.34% by Day 90. ADJACENT_NETWORK exposures stay low, below 0.07% through Day 30, then rise to about 0.13% by Day 65.

Key insights
The main point for leaders is that the chance of a vulnerability being exploited is very low across all software. Even after 90 days, no attack method goes above a 0.35% exploitation rate. This means over 99.6% of disclosed vulnerabilities are never confirmed as used by attackers. NETWORK vulnerabilities build up threat activity slowly and pass other types around day 40, while LOCAL access vulnerabilities spike early and then level off. For executives, this shows that treating every new flaw as an urgent patch wastes valuable engineering time on issues that attackers usually ignore.


Adversary Exploitation Velocity Plot

Why this plot matters
While the Cumulative Exploitation Rate by Attack Vector plot covers all 196,000 vulnerabilities, the Cumulative Exploitation Rate by Attack Vector plot focuses on the 815 that attackers have actually used. It aims to answer a key question for the board: “Once attackers pick a vulnerability, how quickly do they act?” By showing the percentage of weaponized flaws active in the wild over the first 90 days, it checks if the usual 14-, 30-, and 60-day Service Level Agreements (SLAs) match how fast attackers move.

Display code
# Analyze failure timing strictly among weaponized CVEs
weaponized_subset <- survival_cohort %>%
  filter(event == 1L) %>%
  mutate(
    # Truncate negative zero-days to 0 for cumulative timing
    observed_lag = pmax(delta_days, 0)
  )

p2 <- ggplot(weaponized_subset, aes(x = observed_lag, color = attack_vector)) +
  stat_ecdf(geom = "step", linewidth = 1.1) +
  geom_vline(xintercept = c(14, 30, 60), linetype = "dashed", color = "gray55", linewidth = 0.5) +
  annotate("text", x = 14, y = 0.25, label = "14d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  annotate("text", x = 30, y = 0.25, label = "30d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  annotate("text", x = 60, y = 0.25, label = "60d SLA", angle = 90, vjust = -0.5, size = 3.2, color = "gray30") +
  scale_x_continuous(breaks = seq(0, 90, by = 15), limits = c(0, 90)) +
  scale_y_continuous(labels = percent_format(accuracy = 5), breaks = seq(0, 1, by = 0.2)) +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Adversary Exploitation Velocity",
    subtitle = "Cumulative % of weaponized flaws already active in the wild (n = 815)",
    x = "Days Post-NVD Disclosure",
    y = "% of Weaponized CVEs Active",
    color = "Attack Vector"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    legend.position = "bottom",
    plot.title = element_text(face = "bold", size = 13),
    panel.grid.minor = element_blank()
  )

# ------------------------------------------------------------------------------
# 3. Combine Panels into Executive Briefing Graphic
# ------------------------------------------------------------------------------

p2

How to read the plot
Follow the timeline on the bottom from Day 0 (disclosure) to 90 days after. Compare this with the percentage of active targeted vulnerabilities shown on the side. Each colored stepped line shows a different way attackers can get in: red for remote internet (NETWORK), green for authenticated host access (LOCAL), blue for local network segments (ADJACENT_NETWORK), and purple for physical hardware (PHYSICAL). The three dashed vertical lines show where standard patching policies fall at 14, 30, and 60 days, so you can see what share of weaponized flaws attackers have used before each patch cycle ends.

The step changes show where regular patching schedules cannot keep up with how quickly attackers use new vulnerabilities. At Day 0, PHYSICAL flaws jump right to 50% and hit 100% in just three days. LOCAL flaws start with about 60% already active at disclosure, rising to 86% by 14 days and 91% by Day 30. For remote NETWORK vulnerabilities, which are the most common, attackers move quickly: about 28% are active at Day 0, over 50% by Day 5, 70% by Day 14, and 81% by Day 30. ADJACENT_NETWORK flaws stay steady at around 57% until Day 47, then jump to 71% and reach 100% by Day 69.

Key insights
The results show that traditional calendar-based patch deadlines do not work. When attackers target a vulnerability, they act fast: 25% to 60% of weaponized flaws are already active on Day 0, either before or on the day the flaw is made public. By the 14-day SLA, almost 70% of network-weaponized flaws and over 85% of local-weaponized flaws are already in use. By 30 days, about 80% of network flaws have been exploited. This means that 30- or 60-day patch cycles are too slow to stop attacks. Leaders should move from reactive patching to proactive controls like zero trust and network segmentation, and only use emergency patching for early-warning threat indicators.


Insights & Conclusion

The analysis of recent vulnerability data shows a major gap in traditional patch management. Standard, calendar-based patch deadlines, like 14 days for critical and 30 days for high-severity issues, do not match how quickly real attackers move. More than 99% of published vulnerabilities never appear in the CISA Known Exploited Vulnerabilities catalog. When organizations apply the same deadlines to all high-severity vulnerabilities, they waste valuable engineering time fixing issues that are unlikely to be exploited. The data makes it clear: a vulnerability’s severity does not always match how quickly it might be exploited. Treating them as the same leads to patch fatigue for engineering teams and leaves the organization open to real threats.

The analysis also highlights a bigger risk: the Day-Zero Gap. More than 26% of weaponized vulnerabilities were already being exploited before or at the same time as their official NVD disclosure. For these threats, reacting after disclosure offers no protection. This shows that simply meeting patching deadlines does not guarantee strong security. It gives leaders a clear reason to balance quick patching with proactive steps like improving system design, reducing attack surfaces, and strengthening detection.

When vulnerabilities are exploited after disclosure, attackers move quickly. For targeted flaws, the median time to appear in the KEV catalog is about 31 days. Vulnerabilities that are easy to access over the network or require little effort to exploit are attacked much faster than those needing local or authenticated access. As a result, standard 30- or 60-day patch cycles are often too slow for the small group of vulnerabilities that attackers actually use. By the end of a typical 30-day window, half of these vulnerabilities have already been exploited.

To use resources wisely and reduce risk, leaders should move away from compliance-only patching and adopt a flexible, risk-based approach. In this model, patch deadlines are set based on real-world data. Internet-facing systems with high risk should be patched quickly, within 72 hours to 7 days. Lower-risk internal systems can have longer patch cycles, such as 60 or 90 days. This data-driven method protects the organization from the most serious threats and provides a clear, measurable standard for regulators, auditors, and executives.


Limitations & Next Steps

The analysis uses real data and a solid method, but it is not complete yet. There are still some open questions that need answers before these findings can guide final policy. Each question has a clear next step.

The biggest gap is in how the four attack vectors change over time, not just in their risk levels. In the Cumulative Exploitation Rate plot, the NETWORK line starts lower than the LOCAL line and stays that way for about six weeks, then crosses over and ends up as the highest-risk vector by Day 90. This crossover is important because it shows that NETWORK and LOCAL flaws have different risk patterns: one increases slowly and keeps rising, while the other spikes early and then levels off. Using a single timing model for all vectors cannot show both patterns accurately. The next step is to create a separate timing model for each attack vector, so the SLA recommendations match how each vector actually behaves instead of averaging them together.

Another gap is with the 215 “day-zero” exploits, which were already active before or at the time of public disclosure. Right now, each is recorded as happening at a fixed half-day after disclosure, but that is not accurate. We only know they happened sometime before disclosure, not the exact time. Treating these unknown dates as if they were known puts over a quarter of all confirmed attacks into one artificial data point. The next step is to record these events as happening sometime in an unknown period before disclosure, which is the correct statistical approach and will give a clearer picture of the day-zero problem.

A third, smaller issue is the sample size. ADJACENT_NETWORK, which is the smallest of the four attack-vector groups, shows an unusual pattern in the Adversary Exploitation Velocity plot: it stays flat for 47 days, then suddenly jumps. This could be a real effect, or it might just be what happens with a small number of data points. The next step is to add confidence bands to these plots so readers can see how much of the pattern is real and how much is just noise. If the sample is too small, it may also make sense to combine this category with a related one.

Fourth, EPSS is included in the underlying dataset but isn’t currently used as a moving input to thFourth, EPSS is in the dataset but is not yet used as a changing input to the timing model. Right now, it is only a reference column. EPSS scores update daily as new exploitation data comes in, so a vulnerability’s score today can be very different from its score a year ago. The next step is to use EPSS in the model as a value that updates over time, not just as a fixed number. This way, the model will show that a flaw’s risk profile can change too.A’s KEV catalog only records confirmed, federal-relevant exploitation, so this number is a floor, not a complete count. Some exploitation in the wild is never publicly confirmed or never rises to a level CISA tracks. Nothing in this analysis changes that limitation, and any refinement of these numbers should still be read as a conservative estimate of the true rate, not the rate itself.

None of these gaps change the main finding: attackers move faster than standard patch deadlines expect, and treating all vulnerabilities the same wastes engineering time. However, closing these gaps, especially the attack-vector timing issue, would turn this from a strong general case into a set of SLA thresholds that an organization could defend with real statistical confidence.


Session Information

#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.2 (2025-10-31)
#>  os       macOS Tahoe 26.6.2
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       America/New_York
#>  date     2026-09-18
#>  pandoc   3.8.3 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
#>  quarto   1.8.26 @ /usr/local/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version date (UTC) lib source
#>  abind          1.4-8   2024-09-12 [1] CRAN (R 4.5.0)
#>  archive        1.1.12  2025-03-20 [1] CRAN (R 4.5.0)
#>  backports      1.5.0   2024-05-23 [1] CRAN (R 4.5.0)
#>  base64enc      0.1-3   2015-07-28 [1] CRAN (R 4.5.0)
#>  bit            4.6.0   2025-03-06 [1] CRAN (R 4.5.0)
#>  bit64          4.8.2   2026-05-19 [1] CRAN (R 4.5.2)
#>  broom        * 1.0.10  2025-09-13 [1] CRAN (R 4.5.0)
#>  car            3.1-3   2024-09-27 [1] CRAN (R 4.5.0)
#>  carData        3.0-5   2022-01-06 [1] CRAN (R 4.5.0)
#>  cli            3.6.6   2026-04-09 [1] CRAN (R 4.5.2)
#>  commonmark     2.0.0   2025-07-07 [1] CRAN (R 4.5.0)
#>  crayon         1.5.3   2024-06-20 [1] CRAN (R 4.5.0)
#>  crosstalk      1.2.2   2025-08-26 [1] CRAN (R 4.5.0)
#>  data.table     1.17.8  2025-07-10 [1] CRAN (R 4.5.0)
#>  digest         0.6.39  2025-11-19 [1] CRAN (R 4.5.2)
#>  dplyr        * 1.2.1   2026-04-03 [1] CRAN (R 4.5.2)
#>  evaluate       1.0.5   2025-08-27 [1] CRAN (R 4.5.0)
#>  farver         2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
#>  forcats      * 1.0.1   2025-09-25 [1] CRAN (R 4.5.0)
#>  Formula        1.2-5   2023-02-24 [1] CRAN (R 4.5.0)
#>  fs             1.6.6   2025-04-12 [1] CRAN (R 4.5.0)
#>  generics       0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
#>  ggplot2      * 4.0.3   2026-04-22 [1] CRAN (R 4.5.2)
#>  ggpubr       * 0.6.2   2025-10-17 [1] CRAN (R 4.5.0)
#>  ggsignif       0.6.4   2022-10-13 [1] CRAN (R 4.5.0)
#>  glue           1.8.1   2026-04-17 [1] CRAN (R 4.5.2)
#>  gridExtra      2.3     2017-09-09 [1] CRAN (R 4.5.0)
#>  gt           * 1.3.0   2026-01-22 [1] CRAN (R 4.5.2)
#>  gtable         0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
#>  hms            1.1.4   2025-10-17 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1 2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4   2023-12-06 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
#>  km.ci          0.5-6   2022-04-06 [1] CRAN (R 4.5.0)
#>  KMsurv         0.1-6   2025-05-20 [1] CRAN (R 4.5.0)
#>  knitr          1.50    2025-03-16 [1] CRAN (R 4.5.0)
#>  labeling       0.4.3   2023-08-29 [1] CRAN (R 4.5.0)
#>  lattice        0.22-7  2025-04-02 [1] CRAN (R 4.5.2)
#>  lifecycle      1.0.5   2026-01-08 [1] CRAN (R 4.5.2)
#>  litedown       0.8     2025-11-02 [1] CRAN (R 4.5.0)
#>  lubridate    * 1.9.4   2024-12-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.5   2026-04-04 [1] CRAN (R 4.5.2)
#>  markdown       2.0     2025-03-23 [1] CRAN (R 4.5.0)
#>  Matrix         1.7-4   2025-08-28 [1] CRAN (R 4.5.2)
#>  pillar         1.11.1  2025-09-17 [1] CRAN (R 4.5.0)
#>  pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
#>  purrr        * 1.2.2   2026-04-10 [1] CRAN (R 4.5.2)
#>  R6             2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
#>  RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
#>  reactable    * 0.4.5   2025-12-01 [1] CRAN (R 4.5.2)
#>  reactR         0.6.1   2024-09-14 [1] CRAN (R 4.5.0)
#>  readr        * 2.1.5   2024-01-10 [1] CRAN (R 4.5.0)
#>  rlang          1.3.0   2026-07-05 [1] CRAN (R 4.5.2)
#>  rmarkdown      2.30    2025-09-28 [1] CRAN (R 4.5.0)
#>  rstatix        0.7.3   2025-10-18 [1] CRAN (R 4.5.0)
#>  rstudioapi     0.17.1  2024-10-22 [1] CRAN (R 4.5.0)
#>  S7             0.2.2   2026-04-22 [1] CRAN (R 4.5.2)
#>  sass           0.4.10  2025-04-11 [1] CRAN (R 4.5.0)
#>  scales       * 1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
#>  sessioninfo  * 1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
#>  stringi        1.8.7   2025-03-27 [1] CRAN (R 4.5.0)
#>  stringr      * 1.6.0   2025-11-04 [1] CRAN (R 4.5.0)
#>  survival     * 3.8-6   2026-01-16 [1] CRAN (R 4.5.2)
#>  survminer    * 0.5.1   2025-09-02 [1] CRAN (R 4.5.0)
#>  survMisc       0.5.6   2022-04-07 [1] CRAN (R 4.5.0)
#>  tibble       * 3.3.1   2026-01-11 [1] CRAN (R 4.5.2)
#>  tidyr        * 1.3.1   2024-01-24 [1] CRAN (R 4.5.0)
#>  tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
#>  tidyverse    * 2.0.0   2023-02-22 [1] CRAN (R 4.5.0)
#>  timechange     0.3.0   2024-01-18 [1] CRAN (R 4.5.0)
#>  tzdb           0.5.0   2025-03-15 [1] CRAN (R 4.5.0)
#>  vctrs          0.7.3   2026-04-11 [1] CRAN (R 4.5.2)
#>  vroom          1.6.6   2025-09-19 [1] CRAN (R 4.5.0)
#>  withr          3.0.3   2026-06-19 [1] CRAN (R 4.5.2)
#>  xfun           0.54    2025-10-30 [1] CRAN (R 4.5.0)
#>  xml2           1.4.1   2025-10-27 [1] CRAN (R 4.5.0)
#>  xtable         1.8-4   2019-04-21 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10  2024-07-26 [1] CRAN (R 4.5.0)
#>  zoo            1.8-14  2025-04-10 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────

Rendered with Quarto and R. Core packages: broom, gt, lubridate, reactable, scales, sessioninfo, survival, survminer, tidyverse