Skip to article frontmatterSkip to article content

VulnCheck KEV Exploitation Timelines

2025 Time from CVE disclosure to Exploitation

Source
# Ensure date columns are datetime
df['VulnCheck KEV Added Date'] = pd.to_datetime(df['VulnCheck KEV Added Date'], errors='coerce')
df['NVD Published Date'] = pd.to_datetime(df['NVD Published Date'], errors='coerce')

# Filter for rows where the VulnCheck KEV Added Date is in 2025
df_2025 = df[df['VulnCheck KEV Added Date'].dt.year == 2025].copy()

# Calculate the number of days between the two dates
df_2025.loc[:, 'Days Between'] = (df_2025['VulnCheck KEV Added Date'] - df_2025['NVD Published Date']).dt.days

# Drop rows where 'Days Between' is NaN (either date is missing)
df_2025 = df_2025.dropna(subset=['Days Between'])

# Sort by the 'Days Between' to prepare for cumulative counting
df_2025 = df_2025.sort_values(by='Days Between').reset_index(drop=True)

# Create a cumulative count of CVEs
df_2025['Cumulative CVEs'] = df_2025.index + 1

# Calculate the percentage of CVEs with 'Days Between' <= 0
percent_zero_or_less = (df_2025[df_2025['Days Between'] <= 0].shape[0] / df_2025.shape[0]) * 100
days_zero_or_less = 0  # Days for the <= 0 group

# Calculate 50% and 75% cumulative CVE count values
cve_50 = df_2025['Cumulative CVEs'].max() * 0.50
cve_75 = df_2025['Cumulative CVEs'].max() * 0.75

# Find the corresponding day values closest to these cumulative markers
days_50 = df_2025[df_2025['Cumulative CVEs'] >= cve_50].iloc[0]['Days Between']
days_75 = df_2025[df_2025['Cumulative CVEs'] >= cve_75].iloc[0]['Days Between']

# Set up dark mode for the plot
plt.style.use('dark_background')

# Plot the cumulative count with x-axis based on days, but labels as years
plt.figure(figsize=(10, 6))
plt.plot(df_2025['Days Between'], df_2025['Cumulative CVEs'], linestyle='-', color='red')

# Customize x-axis to show labels in years
tick_positions = [i * 365 for i in range(-1, 8)]
tick_labels = [str(i) for i in range(-1, 8)]
plt.xticks(tick_positions, tick_labels)

# Limit x-axis to 8 years, starting at -1 year (approximately -365 to 2555 days)
plt.xlim(-365, 2555)
plt.xlabel('Years Between CVE Publish Date and Known Exploitation', color='white', fontweight='bold')
plt.ylabel('Cumulative # of CVEs', color='white', fontweight='bold')
plt.title('Time from CVE Disclosure to Known Exploitation (2025-YTD)', color='white', fontweight='bold')

# Define annotations for "25%" (<= 0 days), 50%, and 75%
annotations = [
    (days_zero_or_less, df_2025[df_2025['Days Between'] <= 0].shape[0],
     f'{percent_zero_or_less:.1f}%\n{int(days_zero_or_less)} days', 'white'),
    (days_50, cve_50, f'50%\n{int(days_50)} days', 'white'),
    (days_75, cve_75, f'75%\n{int(days_75)} days', 'white')
]

# Add annotations with arrows, offsetting to the left with shorter arrows
for days, cve, label, color in annotations:
    plt.annotate(
        f'{label}',
        xy=(days, cve),
        xytext=(days - 150, cve + 0.1 * df_2025['Cumulative CVEs'].max()),
        textcoords='data',
        arrowprops=dict(arrowstyle="->", color=color, lw=1),
        ha='center', va='bottom',
        fontsize=10, color=color, fontweight='bold'
    )

# Customize grid and tick colors for better visibility on a dark background
plt.grid(True, color='gray', linestyle='--', alpha=0.7)
plt.tick_params(colors='white', labelsize=10, width=1.5)

plt.show()
<Figure size 1000x600 with 1 Axes>

2024 Time from CVE disclosure to Exploitation

Source
# Filter for rows where the VulnCheck KEV Added Date is in 2024
df_2024 = df[df['VulnCheck KEV Added Date'].dt.year == 2024].copy()

# Calculate the number of days between the two dates
df_2024.loc[:, 'Days Between'] = (df_2024['VulnCheck KEV Added Date'] - df_2024['NVD Published Date']).dt.days

# Drop rows where 'Days Between' is NaN (either date is missing)
df_2024 = df_2024.dropna(subset=['Days Between'])

# Sort by the 'Days Between' to prepare for cumulative counting
df_2024 = df_2024.sort_values(by='Days Between').reset_index(drop=True)

# Create a cumulative count of CVEs
df_2024['Cumulative CVEs'] = df_2024.index + 1

# Calculate the percentage of CVEs with 'Days Between' <= 0
percent_zero_or_less = (df_2024[df_2024['Days Between'] <= 0].shape[0] / df_2024.shape[0]) * 100
days_zero_or_less = 0  # Days for the <= 0 group

# Calculate 50% and 75% cumulative CVE count values
cve_50 = df_2024['Cumulative CVEs'].max() * 0.50
cve_75 = df_2024['Cumulative CVEs'].max() * 0.75

# Find the corresponding day values closest to these cumulative markers
days_50 = df_2024[df_2024['Cumulative CVEs'] >= cve_50].iloc[0]['Days Between']
days_75 = df_2024[df_2024['Cumulative CVEs'] >= cve_75].iloc[0]['Days Between']

# Set up dark mode for the plot
plt.style.use('dark_background')

# Plot the cumulative count with x-axis based on days, but labels as years
plt.figure(figsize=(10, 6))
plt.plot(df_2024['Days Between'], df_2024['Cumulative CVEs'], linestyle='-', color='red')

# Customize x-axis to show labels in years
tick_positions = [i * 365 for i in range(-1, 8)]
tick_labels = [str(i) for i in range(-1, 8)]
plt.xticks(tick_positions, tick_labels)

# Limit x-axis to 8 years, starting at -1 year (approximately -365 to 2555 days)
plt.xlim(-365, 2555)
plt.xlabel('Years Between CVE Publish Date and Known Exploitation', color='white', fontweight='bold')
plt.ylabel('Cumulative # of CVEs', color='white', fontweight='bold')
plt.title('Time from CVE Disclosure to Known Exploitation (2024)', color='white', fontweight='bold')

# Define annotations for "25%" (<= 0 days), 50%, and 75%
annotations = [
    (days_zero_or_less, df_2024[df_2024['Days Between'] <= 0].shape[0],
     f'{percent_zero_or_less:.1f}%\n{int(days_zero_or_less)} days', 'white'),
    (days_50, cve_50, f'50%\n{int(days_50)} days', 'white'),
    (days_75, cve_75, f'75%\n{int(days_75)} days', 'white')
]

# Add annotations with arrows, offsetting to the left with shorter arrows
for days, cve, label, color in annotations:
    plt.annotate(
        f'{label}',
        xy=(days, cve),
        xytext=(days - 150, cve + 0.1 * df_2024['Cumulative CVEs'].max()),
        textcoords='data',
        arrowprops=dict(arrowstyle="->", color=color, lw=1),
        ha='center', va='bottom',
        fontsize=10, color=color, fontweight='bold'
    )

# Customize grid and tick colors for better visibility on a dark background
plt.grid(True, color='gray', linestyle='--', alpha=0.7)
plt.tick_params(colors='white', labelsize=10, width=1.5)

plt.show()
<Figure size 1000x600 with 1 Axes>

2023 Time from CVE disclosure to Exploitation

Source
# Ensure date columns are in datetime format
df['NVD Published Date'] = pd.to_datetime(df['NVD Published Date'], errors='coerce')
df['VulnCheck KEV Added Date'] = pd.to_datetime(df['VulnCheck KEV Added Date'], errors='coerce')

# Filter for rows where the VulnCheck KEV Added Date is in 2023
df_2023 = df[df['VulnCheck KEV Added Date'].dt.year == 2023].copy()

# Calculate the number of days between the two dates
df_2023.loc[:, 'Days Between'] = (df_2023['VulnCheck KEV Added Date'] - df_2023['NVD Published Date']).dt.days

# Drop rows where 'Days Between' is NaN (either date is missing)
df_2023 = df_2023.dropna(subset=['Days Between'])

# Sort by the 'Days Between' to prepare for cumulative counting
df_2023 = df_2023.sort_values(by='Days Between').reset_index(drop=True)

# Create a cumulative count of CVEs
df_2023['Cumulative CVEs'] = df_2023.index + 1

# Calculate the percentage of CVEs with 'Days Between' <= 0
percent_zero_or_less = (df_2023[df_2023['Days Between'] <= 0].shape[0] / df_2023.shape[0]) * 100
days_zero_or_less = 0  # Days for the <= 0 group

# Calculate 50% and 75% cumulative CVE count values
cve_50 = df_2023['Cumulative CVEs'].max() * 0.50
cve_75 = df_2023['Cumulative CVEs'].max() * 0.75

# Find the corresponding day values closest to these cumulative markers
days_50 = df_2023[df_2023['Cumulative CVEs'] >= cve_50].iloc[0]['Days Between']
days_75 = df_2023[df_2023['Cumulative CVEs'] >= cve_75].iloc[0]['Days Between']

# Set up dark mode for the plot
plt.style.use('dark_background')

# Plot the cumulative count with x-axis based on days, but labels as years
plt.figure(figsize=(10, 6))
plt.plot(df_2023['Days Between'], df_2023['Cumulative CVEs'], linestyle='-', color='red')

# Customize x-axis to show labels in years
tick_positions = [i * 365 for i in range(-1, 8)]
tick_labels = [str(i) for i in range(-1, 8)]
plt.xticks(tick_positions, tick_labels)

# Limit x-axis to 8 years, starting at -1 year (approximately -365 to 2555 days)
plt.xlim(-365, 2555)
plt.xlabel('Years Between CVE Publish Date and Known Exploitation', color='white', fontweight='bold')
plt.ylabel('Cumulative # of CVEs', color='white', fontweight='bold')
plt.title('Time from CVE Disclosure to Known Exploitation (2023)', color='white', fontweight='bold')

# Define annotations for "25%" (<= 0 days), 50%, and 75%
annotations = [
    (days_zero_or_less, df_2023[df_2023['Days Between'] <= 0].shape[0],
     f'{percent_zero_or_less:.1f}%\n{int(days_zero_or_less)} days', 'white'),
    (days_50, cve_50, f'50%\n{int(days_50)} days', 'white'),
    (days_75, cve_75, f'75%\n{int(days_75)} days', 'white')
]

# Add annotations with arrows, offsetting to the left with shorter arrows
for days, cve, label, color in annotations:
    plt.annotate(
        f'{label}',
        xy=(days, cve),
        xytext=(days - 150, cve + 0.1 * df_2023['Cumulative CVEs'].max()),
        textcoords='data',
        arrowprops=dict(arrowstyle="->", color=color, lw=1),
        ha='center', va='bottom',
        fontsize=10, color=color, fontweight='bold'
    )

# Customize grid and tick colors for better visibility on a dark background
plt.grid(True, color='gray', linestyle='--', alpha=0.7)
plt.tick_params(colors='white', labelsize=10, width=1.5)

plt.show()
<Figure size 1000x600 with 1 Axes>

2023/2024 Time from CVE disclosure to Exploitation

Source
# Filter for rows where the VulnCheck KEV Added Date is in 2023
df_2023 = df[df['VulnCheck KEV Added Date'].dt.year == 2023].copy()
df_2023.loc[:, 'Days Between'] = (df_2023['VulnCheck KEV Added Date'] - df_2023['NVD Published Date']).dt.days
df_2023 = df_2023.dropna(subset=['Days Between']).sort_values(by='Days Between').reset_index(drop=True)
df_2023['Cumulative CVEs'] = df_2023.index + 1

# Filter for rows where the VulnCheck KEV Added Date is in 2024
df_2024 = df[df['VulnCheck KEV Added Date'].dt.year == 2024].copy()
df_2024.loc[:, 'Days Between'] = (df_2024['VulnCheck KEV Added Date'] - df_2024['NVD Published Date']).dt.days
df_2024 = df_2024.dropna(subset=['Days Between']).sort_values(by='Days Between').reset_index(drop=True)
df_2024['Cumulative CVEs'] = df_2024.index + 1

# Set up dark mode for the plot
plt.style.use('dark_background')
plt.figure(figsize=(10, 6))

# Plot the cumulative counts for 2023 and 2024
plt.plot(df_2023['Days Between'], df_2023['Cumulative CVEs'], linestyle='-', color='red', label='2023')
plt.plot(df_2024['Days Between'], df_2024['Cumulative CVEs'], linestyle='-', color='orange', label='2024')

# Customize x-axis to show labels in years
tick_positions = [i * 365 for i in range(-1, 8)]
tick_labels = [str(i) for i in range(-1, 8)]
plt.xticks(tick_positions, tick_labels)

# Limit x-axis to 8 years, starting at -1 year (approximately -365 to 2555 days)
plt.xlim(-365, 2555)
plt.xlabel('Years Between CVE Publish Date and Known Exploitation', color='white', fontweight='bold')
plt.ylabel('Cumulative # of CVEs', color='white', fontweight='bold')
plt.title('Time from CVE Disclosure to Known Exploitation (2023 vs 2024-YTD)', color='white', fontweight='bold')

# Add a legend
plt.legend(loc='upper left', fontsize=10, facecolor='black', edgecolor='white', framealpha=0.8)

# Customize grid and tick colors for better visibility on a dark background
plt.grid(True, color='gray', linestyle='--', alpha=0.7)
plt.tick_params(colors='white', labelsize=10, width=1.5)

# Show the plot
plt.show()
<Figure size 1000x600 with 1 Axes>