# -----------------------
# Config / options
# -----------------------
limit = 2000
# -----------------------
# helpers
# -----------------------
def to_iso_date_str(v):
"""Return YYYY-MM-DD string or None for falsy/invalid inputs."""
if not v:
return None
try:
return str(v)[:10]
except Exception:
return None
# -----------------------
# 1) Fetch the Exploits index (base DF)
# (collect ONLY the columns we keep)
# -----------------------
with vulncheck_sdk.ApiClient(configuration) as api_client:
indices_client = vulncheck_sdk.IndicesApi(api_client)
rows_exploits = []
def push_exploit_entry(entry):
tl = getattr(entry, "timeline", None)
rows_exploits.append({
"CVE": entry.id,
"# of Exploits": getattr(getattr(entry, "counts", None), "exploits", None),
"NVD Published Date": to_iso_date_str(getattr(tl, "nvd_published", None)) if tl else None,
"VulnCheck KEV Added Date": to_iso_date_str(getattr(tl, "vulncheck_kev_date_added", None)) if tl else None,
"Reported Exploited by Ransomware": getattr(entry, "reported_exploited_by_ransomware", None),
"First Reported Ransomware": to_iso_date_str(getattr(tl, "first_reported_ransomware", None)) if tl else None,
"Most Recent Reported Ransomware": to_iso_date_str(getattr(tl, "most_recent_reported_ransomware", None)) if tl else None,
})
api_response = indices_client.index_exploits_get(start_cursor="true", limit=limit)
for entry in api_response.data:
push_exploit_entry(entry)
while api_response.meta.next_cursor is not None:
api_response = indices_client.index_exploits_get(
cursor=api_response.meta.next_cursor, limit=limit
)
for entry in api_response.data:
push_exploit_entry(entry)
df_base = pd.DataFrame(rows_exploits)
# -----------------------
# 2) Fetch the CISA KEV index
# (collect ONLY the columns we keep)
# -----------------------
with vulncheck_sdk.ApiClient(configuration) as api_client:
indices_client = vulncheck_sdk.IndicesApi(api_client)
rows_cisa = []
def push_cisa_row(entry):
try:
first_cve = entry.cve[0]
except Exception:
first_cve = None
rows_cisa.append({
"CVE": first_cve,
"CISA Vendor/Project": getattr(entry, "vendor_project", None),
"CISA Product": getattr(entry, "product", None),
"CISA Known Ransomware Use": getattr(entry, "known_ransomware_campaign_use", None),
"CISA KEV Date Added (CISA)": to_iso_date_str(getattr(entry, "date_added", None)),
})
api_response = indices_client.index_cisa_kev_get(start_cursor="true", limit=limit)
for entry in api_response.data:
push_cisa_row(entry)
while api_response.meta.next_cursor is not None:
api_response = indices_client.index_cisa_kev_get(
cursor=api_response.meta.next_cursor, limit=limit
)
for entry in api_response.data:
push_cisa_row(entry)
df_cisa = pd.DataFrame(rows_cisa)
# Deduplicate by earliest CISA date
df_cisa["CISA KEV Date Added (CISA)"] = pd.to_datetime(
df_cisa["CISA KEV Date Added (CISA)"], errors="coerce"
)
df_cisa = df_cisa.dropna(subset=["CVE"])
df_cisa = df_cisa.sort_values(["CVE", "CISA KEV Date Added (CISA)"]).drop_duplicates("CVE", keep="first")
df_cisa["CISA KEV Date Added (CISA)"] = df_cisa["CISA KEV Date Added (CISA)"].dt.date.astype("object").apply(
lambda x: x.isoformat() if pd.notnull(x) else None
)
# -----------------------
# 3) Merge (no cleanup needed since we never collected unwanted cols)
# -----------------------
df_final = pd.merge(df_base, df_cisa, on="CVE", how="left", validate="m:1")
VulnCheck Ransomware Attribution Compared with CISA KEV¶
Source
# ---- CONFIG ----
DF_NAME = "df_final" # change if your DataFrame is named differently
# ---- PREP ----
df_plot = globals()[DF_NAME].copy()
# Ensure date is datetime and derive Year (int)
df_plot["VulnCheck KEV Added Date"] = pd.to_datetime(
df_plot["VulnCheck KEV Added Date"], errors="coerce"
)
df_plot["Year"] = df_plot["VulnCheck KEV Added Date"].dt.year
# Keep only rows where VulnCheck reports ransomware exploitation = True
def is_trueish(v):
if isinstance(v, bool):
return v
if v is None:
return False
return str(v).strip().lower() in {"true", "yes", "y", "1"}
df_plot = df_plot[df_plot["Reported Exploited by Ransomware"].apply(is_trueish)]
# Map CISA Known Ransomware Use -> {Known, Unknown, Unspecified}
def map_cisa_use(v):
if v is None or (isinstance(v, float) and np.isnan(v)):
return "Unspecified"
s = str(v).strip().lower()
if s in {"known", "yes", "true", "used", "y", "1"}:
return "Known"
if s in {"unknown", "no", "false", "n", "0"}:
return "Unknown"
return "Unknown"
# Ensure column exists; if not, create as all Unspecified
if "CISA Known Ransomware Use" not in df_plot.columns:
df_plot["CISA Known Ransomware Use"] = np.nan
df_plot["Stack Category"] = df_plot["CISA Known Ransomware Use"].apply(map_cisa_use)
# Drop rows with no year
df_plot = df_plot.dropna(subset=["Year"])
df_plot["Year"] = df_plot["Year"].astype(int)
# Aggregate counts per Year × Category (consistent order)
cat_order = ["Known", "Unknown", "Unspecified"]
counts = (
df_plot.groupby(["Year", "Stack Category"])
.size()
.unstack(fill_value=0)
.reindex(columns=cat_order, fill_value=0)
.sort_index()
)
# ---- PLOT ----
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(12, 6))
# Custom colors for Known, Unknown, Unspecified
colors = ["#0078ae", "#c41230", "#5e9732"]
# Plot with pandas, but onto our axes object
counts.plot(kind="bar", stacked=True, ax=ax, color=colors)
# Title / axis labels (bold)
ax.set_title("VulnCheck Ransomware Attribution Compared with CISA KEV", fontweight="bold")
ax.set_xlabel("Year", fontweight="bold")
ax.set_ylabel("Number of CVEs", fontweight="bold")
# X ticks: set locations to exact bar positions and labels to 4-digit years (horizontal, bold)
positions = np.arange(len(counts.index))
ax.set_xticks(positions, labels=[str(int(y)) for y in counts.index], rotation=0, fontweight="bold")
# Y ticks: integer locator + formatter; make tick labels bold without replacing the locator
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
ax.yaxis.set_major_formatter(FuncFormatter(lambda v, pos: f"{int(v):d}"))
for tick in ax.yaxis.get_major_ticks():
tick.label1.set_fontweight("bold")
# Legend inside top-left (keep your long labels)
ax.legend(
title="Category",
labels=[
"VulnCheck & CISA Attributed", # Known
"VulnCheck Attributed but Unknown to CISA", # Unknown
"VulnCheck Attributed but not on CISA KEV", # Unspecified
],
loc="upper left",
bbox_to_anchor=(0.02, 0.98),
frameon=True,
)
plt.tight_layout()
plt.show()