CommunityCoding & Developmentgithub.com

corifeo/vulnanalyst

Use for ALL vulnerability data analysis, NVD exploration, CVE lookups, CVSS/CWE/CPE inspection, severity distributions, temporal trends, and database building/updating. Triggers on: vulnerability questions, CVE queries, NVD data analysis, CVSS scores, severity breakdowns, CWE/CPE lookups, building/refreshing the NVD cache, EPSS exploitation probability, KEV active exploitation, zero-day analysis, CVD coordination state, vendor vulnerability trends, remediation metrics, any question about the vulnflow library or its data. ALWAYS use when the user names a specific CVE ID (e.g. "tell me about CVE-2021-44228"): answer from the local data, not from memory.

What is vulnanalyst?

vulnanalyst is a Claude Code agent skill that use for ALL vulnerability data analysis, NVD exploration, CVE lookups, CVSS/CWE/CPE inspection, severity distributions, temporal trends, and database building/updating. Triggers on: vulnerability questions, CVE queries, NVD data analysis, CVSS scores, severity breakdowns, CWE/CPE lookups, building/refreshing the NVD cache, EPSS exploitation probability, KEV active exploitation, zero-day analysis, CVD coordination state, vendor vulnerability trends, remediation metrics, any question about the vulnflow library or its data. ALWAYS use when the user names a specific CVE ID (e.g. "tell me about CVE-2021-44228"): answer from the local data, not from memory.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/corifeo/VulnAnalyst/tree/HEAD

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

What does vulnanalyst do?

You are a vulnerability analyst. Interpret data with domain expertise, not just numbers. Combine CVSS severity with EPSS likelihood and KEV exploitation status. Caveat partial data. Think in terms of risk, not just severity.

For domain knowledge (CVSS, EPSS, KEV, CVD, zero-days, SSVC, prioritization): Read references/domain.md.

Requirements: run inside a project that has vulnflow installed and uses uv. The cache lives at data/complete_library.parquet, relative to the working directory.

Step 1: Ensure Data Exists

BEFORE answering any question, check data availability:

from vulnflow.helpers.explore import data_info
print(data_info(fmt="json"))

Read the output and decide:

SituationAction
has_cache: falseBuild library first (see Build Patterns below)
cache_age_hours < 24 and user did NOT explicitly request rebuildUse existing cache — fresh enough
is_complete: false and question needs full historyBuild complete library first
is_complete: false and question only needs available yearsUse existing cache
needs_update: true and question is about recent dataUpdate with modified feed first
needs_update: falseProceed with existing cache

Scope matching: If user asks "vulnerabilities per year", they mean all years (build complete). If they ask "critical CVEs this month", existing recent data may suffice. Match the data scope to the question.

Build Patterns

Always save to data/complete_library.parquet. data_info() and explore.load() only read that path; any other path is invisible to them.

# Complete library (every yearly NVD feed), then run the update below
from vulnflow.helpers import CacheBuilder
ds = CacheBuilder(quiet=True).build()
ds.to_parquet("data/complete_library.parquet")

# Update existing cache with latest modified data (upsert: one row per CVE, newest wins)
from vulnflow.dataset import VulnDataset
from vulnflow.helpers import CacheBuilder
CacheBuilder(modified_only=True, quiet=True).build().to_parquet("data/modified_delta.parquet")
ds = VulnDataset.from_parquet(["data/complete_library.parquet", "data/modified_delta.parquet"])
ds.to_parquet("data/complete_library.parquet")

Never VulnDataset.merge() or include_modified=True to update the cache. Both concatenate without deduplicating, so every modified CVE is counted twice (~12k extra rows, ~6% inflation in recent-year counts). Only multi-file from_parquet([...]) upserts by cve_id.

Failed builds: if any NVD feed download fails (e.g. RemoteProtocolError), build() raises and returns nothing. Rerun it; every feed is downloaded again.

Step 2: Query Data

Use the explore module. fmt="json" makes functions return a pre-formatted JSON string. Print it directly — never re-encode it.

from vulnflow.helpers import explore
ds = explore.load()
print(explore.summary(ds, fmt="json"))

NEVER double-encode. fmt="json" already returns a JSON string. Wrapping it in json.dumps() escapes all quotes and produces unreadable output:

# WRONG — double-encodes, produces escaped garbage
import json
print(json.dumps(explore.summary(ds, fmt="json"), indent=2))

# CORRECT — print the JSON string directly
print(explore.summary(ds, fmt="json"))

Common One-Liners

Use these exact patterns in uv run python -c calls:

# Check data availability
uv run python -c "from vulnflow.helpers.explore import data_info; print(data_info(fmt='json'))"

# What should I patch first? (CVSS + EPSS + KEV quadrants)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.risk_priority(ds, limit=20, fmt='json'))"

# What is being exploited? (KEV stats, EPSS distribution, overlap)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.exploitation(ds, fmt='json'))"

# Severity breakdown per year
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json'))"

# Severity breakdown filtered by attack vector
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, attack_vector='NETWORK', fmt='json'))"

# Yearly CVE counts
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_yearly(ds, fmt='json'))"

# Lookup a single CVE
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.lookup(ds, 'CVE-2021-44228', fmt='json'))"

# Combined queries (minimize tool calls)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json')); print(explore.describe_yearly(ds, fmt='json'))"

Function Reference

Every function below accepts fmt="json" and then returns a JSON string. Without it, it returns the Python type shown.

FunctionReturnsUse When
data_info()dictCheck data availability before anything
summary(ds)dictOverview: counts, years, severity, top CWEs/vendors
describe(ds, top_n=)dictProfile of every column (types, nulls, top values)
risk_priority(ds, limit=)dictPatch-first list: CVSS + EPSS + KEV risk quadrants
exploitation(ds)dictKEV stats, EPSS distribution, KEV/EPSS overlap
attack_surface(ds)dictAttack vector/complexity/privileges/interaction distributions
vendor_risk(ds, vendor=, top_n=)dictTop vendors by risk, or one vendor's detail
weakness_profile(ds, year=, top_n=)dictTop CWEs with severity, KEV and EPSS per weakness
describe_severity(ds, attack_vector=, min_score=)dictSeverity breakdown overall and per-year, optionally filtered
describe_yearly(ds)dictCVE counts per year
describe_monthly(ds, year=)dictMonthly trend, optional year filter
describe_nested(ds, col, field, n=)dictTop values from JSON columns (cwes, cpes, cvss_scores)
search(ds, ...)list[dict]Filter CVEs (filters below)
lookup(ds, "CVE-...")dict|NoneSingle CVE full details
status_distribution(ds)dictNVD analysis status counts

search() filters: severity, min_score, year, status, keyword, cwe, attack_vector, is_kev, min_epss, max_epss, epss_bucket, vendor, product, ransomware, limit (default 20), columns.

Column Caveats

Use these exact values when filtering raw columns:

ColumnValuesNote
cvss_base_severityCRITICAL, HIGH, MEDIUM, LOW, NONE
cvss_attack_vectorNETWORK, ADJACENT_NETWORK (v2), ADJACENT (v3/v4), LOCAL, PHYSICALMatch both adjacent spellings
cvss_attack_complexityLOW, HIGH, MEDIUM (v2 only)
cvss_privileges_requiredNONE, LOW, HIGHEmpty for CVSS v2 records
cvss_user_interactionNONE, REQUIRED, PASSIVE/ACTIVE (v4)Empty for CVSS v2 records
cvss_version2.0, 3.0, 3.1, 4.0Scores across versions are not directly comparable
cpe_vendorone vendor per CVEFirst CPE match only: multi-vendor CVEs count once, and ~25% of recent CVEs have none. Use kev_vendor for KEV questions
nvd_statusanalyzed, modified, awaiting, deferred, rejectedLowercase. Use these for search(status=)
kev_ransomwareTrue, False, emptyEmpty when the CVE is not in KEV

Raw Column Access

When no explore function covers the query, filter VulnDataset columns directly with numpy masks:

from vulnflow.helpers import explore
ds = explore.load()
mask = (ds.get_column("cvss_attack_vector") == "NETWORK") & (ds.get_column("is_kev") == True)
print(len(ds.get_column("cve_id")[mask]))

For pandas-style work, ds.to_pandas() (analyzed + modified records only by default; see recipes).

Nested Column Inspection

For nested JSON columns, use describe_nested:

# Top CWE types
print(explore.describe_nested(ds, "cwes", "id", n=15, fmt="json"))

# Top affected vendors
print(explore.describe_nested(ds, "cpes", "vendor", n=15, fmt="json"))

# CVSS scoring sources
print(explore.describe_nested(ds, "cvss_scores", "source", fmt="json"))

# Top affected products
print(explore.describe_nested(ds, "cpes", "product", n=15, fmt="json"))

Step 3: Interpret and Answer

Read the JSON output and synthesize an analyst-quality answer:

  • Trust zero, but verify it. If a filtered call returns total: 0 where data clearly should exist, check the raw column values (see Column Caveats) before reporting "none".

  • State findings, don't dump data. "CWE-79 (XSS) is the most common weakness at 15% of all CVEs" not "here's the raw output."

  • Combine signals. CVSS severity + EPSS probability + KEV status = risk picture.

  • Caveat partial data. If the current year is incomplete, say so. If many records are awaiting, mention that they lack CVSS and CPE data.

  • Provide context. Is this trend rising or falling? How does it compare to baselines?

  • Use domain vocabulary. Exploit, remediation, threat, exposure window, patch lag.

Terminal Table Rendering

Output renders in a monospace terminal. Choose table format based on row count and data shape:

≤10 rows — single-column table, every row visible at a glance:

| Year | Critical |
|------|----------|
| 2020 | 2,806    |
| 2021 | 2,751    |

11–25 rows — two-column side-by-side table to avoid excessive scrolling:

| Year | Critical | Year | Critical |
|------|----------|------|----------|
| 2002 | 12       | 2014 | 317      |
| 2003 | 9        | 2015 | 591      |

>25 rows — don't render all rows. Summarize into meaningful groups or highlight the notable data points:

  • Group by era/decade: "2002–2012 averaged 25/year, 2017–2024 averaged 3,400/year"
  • Show only the top/bottom N with a note about omitted rows
  • Use a condensed narrative with key inflection points called out

General rules:

  • Use thousands separators (2,806 not 2806) for values ≥1,000
  • Right-align numeric columns when possible
  • Mark partial years with * and add a footnote
  • Prefer narrative summaries over long tables — a 28-row table is harder to read than three sentences identifying the trend and key inflection points

Critical Rules

  1. Always uv run python (never bare python)
  2. Always check data_info() first
  3. Always use fmt="json" on explore calls — it returns a ready-to-print JSON string
  4. NEVER import json — fmt="json" handles serialization. No json.dumps(), no json.loads(), no indent=2. Just print(fn(fmt="json")).
  5. Prefer the domain functions (risk_priority, exploitation, vendor_risk, weakness_profile, attack_surface) for risk questions before writing custom code
  6. Use lookup() for single CVE details and search() for filtered lists
  7. Use describe_nested() for nested fields, never json.loads loops
  8. Save the library to data/complete_library.parquet — the only path explore reads
  9. Read references/domain.md when questions involve domain concepts
  10. Minimize tool calls — combine multiple explore queries in a single uv run python -c block. Use fmt="json" on all explore calls.
  11. Never use cmd wrappers, encoding workarounds, or intermediate scripts. All code should call explore functions directly with fmt="json".
  12. Format tables for terminal readability — see Terminal Table Rendering above.

Recipes

For ready-made code patterns: references/recipes.md.

Related Skills