Vulnify: Giving Your Agents a CVE Brain

Table of contents
When building agentic components for pentesting, CVEs are inevitably going to come up. I never found anything that matched what I needed; I did not want "search the web and hope," but actual answers bound to a technology, and the ability to answer questions like “Does this version of this package have this CVE?”, “Is there a public exploit?”, “and “Where is it?” By packaging all that up, agents can get a head start on the scope of the product they’re looking at.
Prior to this work, there are a few ways it can be fudged. The first is by letting the agent search the web with Tavily. It sort of works, but it is slow and non-deterministic, and the agent ends up reading vendor marketing and blogspam to find a CVSS score that lives in a database somewhere. Or, even worse, the 1,000 free credits a month don’t scale - I don’t fancy Yet-Another-Subscription.
The second is by bolting on a scanner like Nuclei. Nuclei is great at templated detection against a live host. But we may not always want to point heavy tools at a product right away. Following the approach of recon and light poking first, Nuclei doesn’t fit.
The third is by hitting NVD and vendor APIs live. This is the closest-to-right solution, but painful to do deterministically and a waste of tokens to let agents figure it out. NVD also throttles you to about five (5) requests every 30 seconds without a key. Furthermore, every source has its own schema, and nothing joins together well.
None of these solve the problem, they work around it and are probably fine for smaller projects, but they do not satisfy my stability and reproducibility requirements, let alone save tokens.
This is where Vulnify comes in. It is an MCP-capable server set on top of one (1) normalized SQLite database that stitches the authoritative sources together and hands the agent a clean, joined answer.
One (1) caveat up front, Vulnify is point-in-time. It needs to pull data every so often to stay relevant; I am working on something CICD-able for this in the future.
How It Got Built
Before I discuss the internals, I want to make a foreword on writing bigger projects with agentic workflows. I find that LLMs are great at extending a codebase that already has a strong opinion baked in. If you let an agent loose on a new project with no guardrails or guidelines, you will see all sorts of weird and wonderful coding styles.
My personal approach, and the approach used to build Vulnify, is to manually build v0.1. I know, writing code in 2026—wow. But, by doing so, I get to lay the groundwork for coding paradigms: how docstrings are laid out, how functions are named, data models, and generally writing the code how I like to. Then, I pass an agent (Claude, in my case) over top of it to learn it, write docs, and update CLAUDE.md and memory; whatever it needs to ensure it matches my style. Then, over time, I introduce subagents to reinforce that pre-commit. If this first pre-pass is done in a strong model like Opus 4.8, then all subsequent work can be done with something like Sonnet, or even local models in some cases.
From there, it is spec-driven, one (1) feature at a time: plan, grill, and build. Plan is a written spec with scope and acceptance criteria. Grill is where I stress-test that plan against the real codebase before a line is written and the load-bearing constraints and awkward edge cases turn up on paper, where they are cheap to fix. Build is implementing in slices to contain scope and context.
Doing all this keeps the codebase coherent because a human set the taste, and the tooling enforces it. It does not get re-argued by every agent that shows up.
What's Actually In It?
Vulnify is one (1) normalized SQLite database with 20 tables, foreign keys on, and everything joined on the CVE ID to create a relational graph.
The mental model is a spine plus enrichment. The spine is CVEProject/cvelistV5 because it is the authoritative CNA-submitted record set. It defines which CVEs exist and carries the core fields:
- Summary
- Technical details
- Affected products
- CWEs
- References
- CVSS
Everything else is enrichment layered onto CVEs that already exist. The following table lists the enrichment sources and why each one earns its place.
Source | Description |
|---|---|
This gives the analysis layer NVD-scored CVSS, CPE applicability, and vuln status. CNAs do not always score a CVE, and NVD gives you a consistent CVSS and machine-readable CPE. It is also the slow part of a build, thanks to that rate limit. | |
CISA KEV is the ground truth. CVSS and EPSS are predictions; KEV is confirmed exploitation in the wild, with regulatory due dates and a ransomware flag. It is the highest signal input you have for prioritization. | |
This is the probability that a CVE gets exploited in the next 30 days. CVSS tells you how bad it would be if it happened. EPSS tells you how likely it is to happen. These are two (2) different questions, both worth asking. | |
OSV maps a CVE to affected packages across ecosystems like PyPI, npm, and Go. It is the bridge from a CVE to a concrete package and the way in for dependency and SBOM questions. |
This is the exploit corpora, which is where Vulnify gets useful for offense:
Source | Description |
|---|---|
A CVE-tagged template means there is a real, runnable detection. The CVE ID comes from the template's structured classification field, so Vulnify records the link as exact. | |
This uses the files_exploits.csv from Exploit-DB. From here, the CVE IDs are scraped out of a free-text column, so those links are tagged parsed, which is a lower-trust grade. | |
This is Metasploit module metadata. If it has an MSF module, then it means this is a weaponized, operator-ready exploit, which is about the highest operational signal there is. The CVE IDs come from the module's References, so those are exact as well. |
Those three (3) feed a table of concrete exploit artefacts (source, stable ID, URL, and confidence) and drive the summary signals on each CVE. A few of the data-model decisions are worth pulling out, because they are the "taste" I mentioned earlier.
The most important thing is that the exploit signals are tri-state; public_poc and Metasploit are not booleans. They are NULL, false, or true: NULL if no source has looked yet, false if a source looked and found nothing, or true if it found something. This matters because a confident "no public exploit exists" in a report, when there is one, is a liability. "Not assessed" and "assessed, found nothing" are different answers, and Vulnify never quietly turns one into the other. When the data does degrade, say, mid-rebuild, it degrades toward NULL, which is the honest direction.
“Exact” means the CVE ID came from a structured field, while “parsed” means it was extracted from free text. It tells an operator how far to trust the link. Vulnify only emits those two (2), because a link guessed from a title is worse than no link at all, so those get dropped.
Version ranges are stored as first-class structured data rather than free text, because "does version X of this thing hit this CVE?" is a real question, and string comparison is a trap. "1.10" sorts before "1.9" lexically, which is the wrong answer. Keeping the ranges structured is what will let that comparison be done properly in Python, three (3) state, instead of as lexicographic SQL. (The comparator and the version-matching tools that sit on top of it are the next thing on the roadmap; they have not shipped yet.)
There is also an FTS5 full-text index over the titles, summaries, and technical detail, so an agent can search for "container escape" as easily as it can filter by vendor.
Every source is its own idempotent, watermarked phase, so the whole pipeline is built to be re-run and pick up where it left off, which brings us back to the caveat: it is a snapshot, designed to be rebuilt.
The Sources, Raw
If you want a feel for why the stitching is worth doing once, here is each source on its own. There are eight (8) feeds, and three (3) of them are not even queryable APIs.
The spine, cvelistV5, is a bulk zip. There is no per-CVE endpoint, so you take the lot and parse a quarter of a million JSON files:

NVD 2.0 has a proper API and the rate-limit tax to go with it:

Without a key, you are throttled to roughly five (5) requests every 30 seconds, which is why a cold build takes hours.
EPSS is clean and keyless and comes back as { epss: "0.99999", percentile: "1.0" }:

OSV maps a CVE to affected packages, and only open source packages get a mapping, which becomes relevant shortly:

CISA KEV is one (1) big JSON catalog. At the time of writing, that is catalog version 2026.07.01 with 1,631 entries:

Nuclei is a git repo, so you take the tarball and use the commit SHA as a version stamp:

Exploit-DB is a CSV:

And Metasploit is one (1) large module-metadata JSON, keyed by module path:

There are eight (8) feeds and eight (8) different shapes where three (3) need a parser rather than an HTTP call. There is also a rate limit on the big one, and nothing shared between them but the CVE ID. Vulnify does that stitching once, offline, so the agent asks a single question instead of orchestrating eight (8).
One CVE
The cleanest way to show what "enriched" means is through one (1) fully populated record. CVE-2025-53770 is a good pick. It is the July 2025 SharePoint RCE that got named "ToolShell" and mass-exploited, and it has a value in every component, with exactly one (1) exploit artefact from each of the three (3) corpora.
The cvelistV5 record forms the backbone of the entry, providing the authoritative CNA-supplied metadata.
Field | Value |
|---|---|
Title | Microsoft SharePoint Remote Code Execution Vulnerability |
CNA | Microsoft |
Summary | Deserialization of untrusted data, including Microsoft's note that exploitation has been observed in the wild |
CWE | CWE-502 |
Affected Products | SharePoint 2016, SharePoint 2019, and SharePoint Subscription Edition, including version ranges |
References | 13 references spanning the Microsoft advisory, CISA alerts, eye.security research, and industry reporting |
NVD enriches the record with standardized scoring and platform metadata.
Field | Value |
|---|---|
CVSS Base Score | 9.8 |
CNA Vector | Includes temporal metrics (E:F/RL:W/RC:C) |
NVD Vector | Complete base scoring vector |
CPE Matches | Three (3) CPE match strings |
Analysis Status | Analysed |
CISA's Known Exploited Vulnerabilities catalogue provides operational prioritization.
Field | Value |
|---|---|
Listed | Yes |
Date Added | July 20, 2025 |
Due Date | July 21, 2025 |
Observation | One (1) day remediation deadline, indicating exceptional urgency |
EPSS contributes an estimate of exploitation likelihood.
Field | Value |
|---|---|
EPSS Score | 0.885 |
Percentile | 99.5th percentile |
Interpretation | Among the highest predicted exploitation probabilities |
And from the exploit corpora, all three (3) signals are true (public PoC, Metasploit, and in the wild) and backed by the artefact triad:
Source | Artefact | Confidence |
|---|---|---|
Metasploit | exploit/windows/http/sharepoint_toolpane_rce | Exact |
Nuclei | CVE-2025-53770 template | Exact |
Exploit-DB | EDB-52405 | Parsed |
The one (1) thing that is empty is the OSV package mapping, and that is correct. SharePoint is obviously proprietary, so OSV has nothing to say about it. That is the tri-state mindset applied to coverage: "assessed, nothing applies" is not the same as "did not look." For an open source CVE, the mapping is populated. CVE-2004-2462, for example, comes back with Debian:10:cplay.
One (1) simple query yields a plethora of data for your agents.
The Explorer
Before the agent side, it is worth showing that the database is just explorable. There is a Streamlit dashboard bundled in the repo, around 70 views across 14 tabs, reading the exact same database the agents query.




Talking to It: The MCP Server
The MCP server is a FastMCP server over stdio.
There are seven (7) tools, and the easiest way to think about them is by the questions they answer:
Function | Description |
|---|---|
get_cve(cve_id) | This is the "tell me everything about this CVE" function. The fully hydrated record is: CVSS, KEV, EPSS, exploit signals, references, CWEs, affected products, CPEs, and the artefact evidence list. |
search_cves(...) | This is the "find CVEs matching these criteria" by vendor, product, CWE, KEV status, minimum CVSS, minimum EPSS, or year function. |
search_cves_text(query) | This is the "search the text" function, using FTS5 with the usual AND, OR, NOT phrases and prefixes. |
exploits_for(cve_id) | This is the "is there a real exploit, and where?" function, returning the artefacts with source, confidence, and URL. |
references_for(cve_id, tag) | This translates to "give me the patch or advisory links." |
list_kev(since, limit) | This translates to "what has CISA added recently?" |
database_overview() | This essentially summarizes the stats of the db. |
Actually Using It
The rest is easier to show than to describe. Everything below came off the live server.
Starting with orientation, database_overview() reports 347,248 CVEs, 21,142 vendors, 70,266 products, 1,593 KEV-listed CVEs, and EPSS on all of them. The CVE data runs to April 30, 2026, and the enrichment phases last ran on June 10, 2026. The tool reports its own staleness, which is the point.
Ask it about Log4Shell with get_cve("CVE-2021-44228"), and you get the whole triage picture in one (1) call:
Signal | Value |
|---|---|
KEV | Listed, added December 10, 2021, due December 24, 2021 |
EPSS | 0.944 (99.96th percentile) |
Exploit | Public PoC, Metasploit, and in-the-wild, all true |
Artefacts | 52 |
Pull the evidence with exploits_for("CVE-2021-44228"), and those 52 artefacts break down by source, each carrying its confidence:
Source | Count | Confidence |
|---|---|---|
Nuclei | 44 | Exact |
Metasploit | 5 | Exact |
Exploit-DB | 3 | Parsed |
The Metasploit module paths are the "there is a weaponized path" signal, and the exact and parsed tags tell the agent how far to trust each link.
For triage across the whole corpus, search_cves(kev_only=True, min_epss=0.9) gives you everything CISA has confirmed exploited that EPSS also puts above 90 percent:
CVE | CVSS | EPSS | What It Is |
|---|---|---|---|
CVE-2025-47812 | 10.0 | 0.929 | Wing FTP Server RCE (root/SYSTEM) |
CVE-2025-64446 | 9.8 | 0.931 | FortiWeb path traversal to admin command exec |
CVE-2026-24061 | 9.8 | 0.911 | GNU Inetutils telnetd auth bypass |
CVE-2026-41940 | 9.8 | 0.908 | cPanel and WHM auth bypass |
And the full-text index handles the questions you cannot express as filters. search_cves_text("container escape") comes back with youki, Versa Concerto, iSula, and LXC escapes, ranked by relevance. It also surfaces one (1) loose match on "escape sequences," which is stemmed full-text search being honest about what it is.
Where it gets interesting is chaining tools together. Take a question an operator would actually ask: What is the most exploit-likely Fortinet bug CISA has flagged, and can I pop it with Metasploit? Two (2) calls.
First, search_cves(vendor="fortinet", kev_only=True) returns 25 KEV-listed Fortinet CVEs. Ranked by EPSS, the top of the list is CVE-2018-13379, the FortiOS SSL-VPN path traversal and credential-leak bug, at 0.945.
Then exploits_for("CVE-2018-13379") answers the second half:
Source | Artefact | Confidence |
|---|---|---|
Metasploit | auxiliary/gather/fortios_vpnssl_traversal_creds_leak | Exact |
Nuclei | CVE-2018-13379 | Exact |
Exploit-DB | EDB-47287, EDB-47288 | Parsed |
Yes. There is a Metasploit module, plus a Nuclei template and two (2) Exploit-DB entries. Two (2) calls, and the agent has a prioritized target and a runnable module path, without a single web search or a guessed CVE.
You Don't Have to Use MCP
One (1) last thing, because it is easy to assume MCP is the whole story; it is not. MCP gets it up and running super easy. But it’s not the only way to use it—after all, it’s just a database.
In a larger system I run, I skip the MCP transport entirely and import the query layer directly, the same store and query functions that the MCP tools wrap, and call them as plain Python. There are a few reasons to do that.
The first is visibility. The more I work with agentic systems, the more important this has become to me. Owning the call site means I can wrap every lookup in my own logging and see what the agent asked, what came back, how long it took, and which CVEs it touched. The stdio transport is a black box by comparison. If you want an audit trail of what your agents are doing with the data, you write the wrapper yourself. However, this can probably be resolved directly in Vulnify by allowing it to be a library.
The second is that it is just an in-process function call, with none of the subprocess or stdio overhead. The third is control: caching, my own ranking, policy, and wiring the results into the rest of the pipeline.
So the way to think about Vulnify is as a database and a query library first, and as an MCP server second. MCP is the easy way in. Direct embedding is what you reach for when you want control and visibility over how the data is used. It is one (1) example of usage, not the whole of it.
Wrapping Up
The problem was that agents doing security work have no real CVE knowledge to draw on, only workarounds. Vulnify is my attempt to give agents access to proper and enriched data regarding CVEs and to cut out a lot of the token wastage while doing so. I spent a lot of time talking about how Vulnify can be used for agents, but it’s also great deterministically. Say you have C2 automation and can extract OS information; well, now you can use that to query the installed applications or OS version too. It’s only Python, after all.