By the fastCRW team · Catalogue figures checked 2026-11-05 against GET /v1/data · Verify independently before relying on any figure.
A CVE API for an AI agent is the set of lookups a triage agent needs before it can rank a vulnerability: the CVE record and its severity, an exploit prediction score, whether the issue is known to be exploited, and any package advisories that point at it. NVD, EPSS, CISA KEV, the CVE record and OSV are all on Pergamon, so one fastCRW key covers the set. Every response keeps its own attribution. The calls in this post were run on 2026-11-05.
This is a tutorial for building a triage step. It does not decide what to patch first for you.
Sources and what each one answers
| Source | Capability | What it tells the agent |
|---|---|---|
| NVD | nvd/cve, option cveId | The CVE entry, including CVSS metrics and the CISA KEV fields when the CVE is listed. |
| EPSS | epss/score, option cve | An exploit prediction score and percentile for the CVE, with a date. |
| CISA KEV | cisakev/catalog | The full catalogue of CVEs with known exploitation. Large, so use it sparingly. |
| CVE record | cve/record, option id | Record metadata such as the publish and update dates and the assigning organisation. |
| OSV | osv/vulnerability, option id | Package-level advisories, with their aliases, such as the CVE id they map to. |
NVD and OSV sit in the Security category next to cisakev, cve and epss, so a find call filtered to Security covers all five sources.
NVD and its rolling window
NVD publishes its own limits. In its start-here guide it states: "The public rate limit (without an API key) is 5 requests in a rolling 30 second window; the rate limit with an API key is 50 requests in a rolling 30 second window" (nvd.nist.gov). The key goes in an apiKey request header. The nvdlib documentation puts the practical effect plainly: "a request will take 6 seconds with no API key" (nvdlibdocs).
Do not assume that Pergamon raises those limits. Plan the agent around NVD's published window: one NVD call per CVE, cached for the run, and a pause between batches. Here is the NVD call for Log4Shell, CVE-2021-44228:
curl "https://api.fastcrw.com/v1/data/nvd/cve?cveId=CVE-2021-44228" \
-H "Authorization: Bearer $CRW_API_KEY"
Trimmed response:
{
"success": true,
"provider": "nvd",
"capability": "cve",
"credits_used": 1,
"data": {
"...": "...",
"vulnerabilities": [
{
"cve": {
"id": "CVE-2021-44228",
"metrics": {
"cvssMetricV31": [
{ "cvssData": { "baseSeverity": "CRITICAL" } }
]
},
"cisaExploitAdd": "2021-12-10",
"cisaActionDue": "2021-12-24",
"cisaVulnerabilityName": "Apache Log4j2 Remote Code Execution Vulnerability"
}
}
]
}
}
The severity sits at vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData.baseSeverity. For this CVE it reads CRITICAL.
EPSS score and history
EPSS gives a score for how likely the CVE is to be exploited, plus the percentile of that score across all CVEs. The epss/score capability returns the current value for one CVE:
curl "https://api.fastcrw.com/v1/data/epss/score?cve=CVE-2021-44228" \
-H "Authorization: Bearer $CRW_API_KEY"
The entry you need is data.data[0]. It carries cve, epss, percentile and date. On 2026-11-05 the score for CVE-2021-44228 was 0.999990000 with a percentile of 1.000000000. EPSS returns these values as strings, so convert them with float() before you compare them.
For a trend, epss/history returns the daily values for the same CVE in data.data[0]["time-series"], newest first, each with epss, percentile and date. An agent can use it to tell a rising score from a flat one.
Read the EPSS score as an estimate of exploitation activity, not as proof that a system is under attack. Its value lies in ordering a long list of CVEs, and it works best next to the KEV fields, which record known exploitation rather than predicted exploitation.
Is it in KEV?
The CISA Known Exploited Vulnerabilities list is the question most triage agents care about. For a CVE that is listed, you do not need the whole list. The NVD record carries the KEV fields itself. For CVE-2021-44228 they read:
cisaExploitAdd: 2021-12-10cisaActionDue: 2021-12-24cisaVulnerabilityName: Apache Log4j2 Remote Code Execution Vulnerability
The full catalogue is available as cisakev/catalog. Over REST it returned 200 with a body of about 1.4 MB. Over MCP the same call is refused with [result_too_large] The result is 1398 KiB, over the 256 KiB limit. For a server source that refused call is refunded, but the agent still gets no data. Check KEV through the NVD record in the agent, and download the catalogue only in a batch job. The KEV fields appear only for a CVE that is listed, so a missing field means the NVD record does not list the CVE. It is not a statement that the CVE is safe.
OSV for package-level advisories
NVD describes the CVE. OSV describes the package advisory that fixes or explains it, and it is the better source when you triage a dependency. Look it up by its advisory id:
curl "https://api.fastcrw.com/v1/data/osv/vulnerability?id=GHSA-jfh8-c2jp-5v3q" \
-H "Authorization: Bearer $CRW_API_KEY"
Trimmed response:
{
"id": "GHSA-jfh8-c2jp-5v3q",
"aliases": ["CVE-2021-44228"],
"summary": "Remote code injection in Log4j"
}
If OSV has no advisory for a CVE, record that absence in the triage output. Do not treat it as a clean bill of health, because it only means this source has no package advisory for that identifier.
The aliases list is how you connect the advisory to a CVE. The same call with id=CVE-2021-44228 also returned the advisory on 2026-11-05, with the GHSA id in its aliases. The OSV data is licensed CC BY 4.0 and published by the OSV project, and that is the licence line to show.
A triage function with the Python SDK
The function below makes the four lookups for one CVE and returns the fields a triage step needs. It keeps each source's attribution, so the output can show where every field came from.
from crw import CrwClient
client = CrwClient() # reads CRW_API_KEY
def triage_cve(cve_id: str) -> dict:
nvd = client.get_data("nvd", "cve", {"cveId": cve_id})
epss = client.get_data("epss", "score", {"cve": cve_id})
record = client.get_data("cve", "record", {"id": cve_id})
osv = client.get_data("osv", "vulnerability", {"id": cve_id})
cve = nvd["data"]["vulnerabilities"][0]["cve"]
return {
"severity": cve["metrics"]["cvssMetricV31"][0]["cvssData"]["baseSeverity"],
"kev_added": cve.get("cisaExploitAdd"),
"epss": epss["data"]["data"][0]["epss"],
"published": record["data"]["cveMetadata"]["datePublished"],
"osv_aliases": osv["data"]["aliases"],
"attribution": [
nvd["provenance"]["attributionText"],
epss["provenance"]["attributionText"],
osv["provenance"]["attributionText"],
],
}
print(triage_cve("CVE-2021-44228"))
The run on 2026-11-05 printed:
{'severity': 'CRITICAL', 'kev_added': '2021-12-10', 'epss': '0.999990000', 'published': '2021-12-10T00:00:00.000Z', 'osv_aliases': ['GHSA-jfh8-c2jp-5v3q'], 'attribution': ['Source: NIST National Vulnerability Database', 'Source: EPSS, FIRST.org', 'Source: OSV.dev']}
Each lookup costs 1 credit, so a triage of one CVE costs 4 credits. A failed upstream call is not billed.
Stay inside NVD's window in a batch
NVD allows 5 requests in a rolling 30 second window without a key. Spread the calls evenly and the spacing is one call every 6 seconds, because 30 divided by 5 is 6. The loop below makes one NVD lookup per CVE and pauses after each one. It reads the severity only, to keep the example short.
import time
from crw import CrwClient
client = CrwClient() # reads CRW_API_KEY
NVD_SPACING_SECONDS = 6 # 5 NVD requests per rolling 30 seconds: one call every 6 seconds
def nvd_severity(cve_id: str) -> str:
nvd = client.get_data("nvd", "cve", {"cveId": cve_id})
cve = nvd["data"]["vulnerabilities"][0]["cve"]
return cve["metrics"]["cvssMetricV31"][0]["cvssData"]["baseSeverity"]
def severities(cve_ids: list[str]) -> dict[str, str]:
result = {}
for cve_id in cve_ids:
result[cve_id] = nvd_severity(cve_id)
time.sleep(NVD_SPACING_SECONDS)
return result
print(severities(["CVE-2021-44228", "CVE-2014-0160"]))
The run on 2026-11-05 printed {'CVE-2021-44228': 'CRITICAL', 'CVE-2014-0160': 'HIGH'}. The pause is the simplest way to stay inside the window. If your agent runs several sources in parallel, count the NVD calls separately, because the window belongs to NVD and not to the other sources.
What to keep in a triage record
A triage record should hold the facts and the place each fact came from. For each CVE, store the severity, the EPSS score and percentile with its date, the KEV dates when they exist, the OSV aliases, and the attributionText of each source. Store the time of the lookup as well, because the scores and the catalogue change over time.
Keep the decision apart from the record: the ranking is a policy your team owns, and the record should only show its inputs.
Attribution you must show
NVD asks products that use its data to say so. Its start-here guide words the notice as "This product uses data from the NVD API but is not endorsed or certified by the NVD." (nvd.nist.gov), and its terms of use carry a similar line (terms of use). Show that notice wherever your agent displays NVD fields. Keep the attributionText from each result next to the field it supports, as the function above does.
Honest limits
- The triage function returns facts from four sources. It does not score exploitability or decide priority.
- NVD's rate window applies to every NVD call your agent makes.
- The KEV catalogue is too large for MCP. Use the NVD KEV fields for single lookups.
- A CVE with no package advisory has no OSV record to triage.
Sources
- NIST NVD: Start here
- NIST NVD: Terms of use
- nvdlib documentation, CVE v2
- FIRST: EPSS
- fastCRW data sources documentation
Related: Public data API for AI agents · Security sources in the Pergamon catalogue · NVD on Pergamon · OSV on Pergamon
