Free Public CVE API

JSON + NDJSON access to the same vulnerability intelligence behind /pulse: NVD CVSS, EchelonGraph synthesised score, CISA KEV flag, FIRST EPSS percentile, GHSA links, and vendor advisory cross-walks (Microsoft, Red Hat, Cisco, AWS, GCP, GitLab).

No API key required. Two limits apply to the endpoints on this page, and the tighter one is the one you meet. Our WAF counts every request you make as one caller (defined below) to the API in a 60-second window that starts with your first request — all paths together, across every serving instance — and refuses a request once that count passes the ceiling for the path it is on:

  • 12,000 requests per minute on /api/v1/public/cves and every path under it (200 per second sustained)
  • 6,000 requests per minute on /api/v1/public/vendor-advisories, /api/v1/public/cwes, /api/v1/public/ecosystems, /api/v1/public/vendors, /api/v1/public/shadow-ai-radar and /api/v1/public/kev/recent (100 per second sustained)
  • 600 requests per minute on any other /api/v1/public path (10 per second sustained)

Because the count is shared, a heavy run on /api/v1/public/cves also uses up the lower ceilings: after 6,000 requests in one window, /api/v1/public/vendor-advisories refuses you too.

On top of that ceiling, 500 requests per second per caller is a burst cap, counted once across every serving instance (an instance counts on its own only while our shared counter is unreachable), so it bounds a burst rather than your sustained rate. For both limits, one caller is one client address as our platform records it on arrival — an IPv4 address, or for IPv6 the /64 network the address is in — and not a value you send in a header, so other callers' traffic does not spend your allowance. Clients behind one shared IPv4 address or one IPv6 /64 (an office NAT, a proxy relaying for others, a hosting provider that puts several customers in one /64) share that one allowance, and a host rotating its IPv6 privacy addresses stays one caller. One exception: a request that reaches the API from inside Google Cloud without a public address (for example, from a VM with no external IP, using Private Google Access) arrives with no client address our platform records, so every such caller is counted as the same caller and all of them share one per-second allowance. Cloud NAT does not change this; to get an allowance of your own, give the workload an external IP address.

What you see: an admitted response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Window: 1s and X-RateLimit-Reset for the per-second limit only, and X-RateLimit-Remaining there is your count across every serving instance. The WAF's per-minute count is not shown until it refuses you. Over either limit you get a 429 with Retry-After in seconds. A WAF refusal carries X-RateLimit-Window: 60s, X-RateLimit-Limit set to that path's per-minute ceiling, and a JSON body whose code is RATE_LIMIT_EXCEEDED; a refusal by the per-second limit carries X-RateLimit-Window: 1s. Need higher throughput? Get in touch.

Quickstart

Every endpoint is GET-only and returns JSON. Try this Log4Shell lookup:

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228 | jq .

Endpoints

GET/api/v1/public/cves

The main feed — paginated JSON list, newest first. Params: limit (default 50, max 200), offset, search, severity, min_cvss, kev_only, year, sort (published | epss | severity | cvss_v3_score), skip_count. reverse=true reads the default listing (sort=published or no sort, with no status other than active, and no severity, min_cvss, since, until, year, kev_only or search filter) from its far end — offset and limit count back from the last row, and the page comes back in the usual order with "reverse": true in the response — so a page near the end costs about what a page near the start does; with any other sort or filter, or with the before_published / before_cve_id cursor, it is refused with a 400. Every list response carries reverse_available, true exactly when the same request with reverse=true would be served, so a pager can ask the server instead of copying that rule; take the page count from a counted response (not one sent with skip_count). kev_only=true restricts to CVEs CISA lists as known-exploited; year filters on UTC publication year. There is no vendor filter on this endpoint — vendor lives in CPE match data, so use /api/v1/public/cves/match?vendor=&product= for that; passing vendor here returns 400 rather than silently ignoring it. severity filters on NVD's CVSS v3 label by default; pass severity_basis=eg to filter on the EchelonGraph severity instead — that is the basis the /pulse counts use, and it is the only way to reach the ~72,000 CVEs NVD never rated under v3. min_cvss matches the highest CVSS we hold (v3.1, v4.0 or v2.0). Response: cves for this page, plus total and two booleans about it, total_is_lower_bound and total_counted. total is how many CVEs match the filter across every page, not how many this page holds. Both booleans are always present, in both polarities — branch on their value, never on whether the field exists. total_is_lower_bound=true means total is a proven floor, not a count: at least that many match, and the exact figure could not be computed inside the query's execution budget. That happens on a free-text search matching a very large share of the corpus; a narrower search gets an exact total. total_counted=false means no count is in hand — because you sent skip_count=1 (for bulk paginators that never show a total), because you used the keyset cursor path (before_published + before_cve_id), or because the server could not count the matches inside the query's execution budget, not even to a floor — and total is 0 and means nothing. With skip_count or the cursor, page with the next_published / next_cve_id the response carries instead; when the budget was the cause the response is an ordinary offset page with its rows and no cursor, and a narrower search gets a total. search_relaxed=true means the rows are for a widened search, not the one you sent: the search term was a phrase — a quoted string, or a hyphenated compound such as cross-site scripting, which the parser reads as adjacent words — and its exact match could not finish inside the query's execution budget, so it was relaxed to a conjunction of the same words in any order; the rows, and total when total_counted is true, are that conjunction's. Always present, in both polarities; false means the search ran as sent. Rate limits: 12,000 requests per minute on /api/v1/public/cves and every path under it (200 per second sustained), counted by our WAF for each caller across all paths; on top of that, 500 requests/second per caller is a burst cap, counted once across every serving instance. For both limits one caller is one IPv4 address, or for IPv6 one /64 network, as our platform records it, never a header you send. Successful responses show only the per-second limit's X-RateLimit-* headers; the paragraph above this list says how both limits are counted and what a 429 from each carries.

curl "https://app.echelongraph.io/api/v1/public/cves?severity=HIGH&severity_basis=eg&min_cvss=7&limit=20"
GET/api/v1/public/cves/summary

Overall counts and the live NVD poller's health snapshot, updated continuously. Severity buckets (critical/high/medium/low/unscored) use the EchelonGraph severity — the band we publish — and always sum to total. NVD's own CVSS v3 classification is returned alongside as nvd_critical/nvd_high/nvd_medium/nvd_low/nvd_none, so you can compare the two bases.

curl https://app.echelongraph.io/api/v1/public/cves/summary
GET/api/v1/public/cves/{id}

Single CVE record — the merged view: NVD CVSS, EchelonGraph synthesised score, KEV/EPSS signals, GHSA references, vendor advisory cross-links. For the ~72,000 pre-2016 CVEs that NVD only ever rated under CVSS v2, cvss_v3_score is 0 and the real rating is in cvss_v2_score / cvss_v2_severity / cvss_v2_vector — NVD maps a v2 score to three bands only (LOW 0.0-3.9 / MEDIUM 4.0-6.9 / HIGH 7.0-10.0, no CRITICAL; CVSS v2 itself defines none) and is NOT comparable to a v3 score. exploit_poc_available is true when EchelonGraph holds at least one reference to public exploit or proof-of-concept code for the CVE: a Metasploit module, an Exploit-DB entry or a GitHub proof-of-concept repository. Nuclei templates do not count, nor does a GitHub repository that names or describes itself as a checker or scanner (exploit kind github_checker): each is a detection check that tests whether a system looks affected, and is not necessarily exploit code. false means no such reference is on record, not that none exists, and neither value is a statement about exploitation in the wild: that is kev_listed, and eg_kev_tier 1. A REJECTED CVE ID no longer describes a vulnerability, so its exploit_poc_available is false and its eg_kev_tier null, whatever references it holds. For a CVE on CISA's Known Exploited Vulnerabilities catalog, three fields carry CISA's own text, verbatim: kev_required_action (CISA's requiredAction, directed at US federal civilian agencies under Binding Operational Directive 26-04, which replaced BOD 22-01 on 10 June 2026, and not EchelonGraph's advice), kev_short_description (CISA's shortDescription) and kev_notes_urls (the https URLs in CISA's notes, in CISA's order, at most 10). Each is absent where CISA gives none, and a CVE that is not KEV-listed carries none of them; only this endpoint serves them, not the list or the KEV feed. {id} may also be another identifier of the CVE: an ENISA EUVD id (EUVD-2026-90503), a GitHub advisory id (GHSA-…), an OSV id that aliases a CVE (PYSEC-, GO-, RUSTSEC-, GSD-, MAL-) or a GCVE-0 id (GCVE-0-2024-3094, which is CVE-2024-3094 by the GCVE specification). The answer is the CVE's own record, 200, with resolved_from { id, namespace, source, attribution } naming the identifier asked for and what mapped it, and a Content-Location header naming the CVE's own URL; an identifier EchelonGraph does not hold is a 404 (ALIAS_NOT_FOUND), one that names more than one CVE a 300 (ALIAS_AMBIGUOUS, with the candidates), and a 503 (ALIAS_INDEX_UNAVAILABLE) means the alias index could not be read. aliases lists the CVE's other identifiers, each { id, namespace, source, first_seen_at, attribution }; source is enisa_euvd (ENISA's European Vulnerability Database), github or osv. eu_kev is present when ENISA's EU Known Exploited Vulnerabilities catalogue lists the CVE: { listed, date_added (ENISA's date), reported_by (the reporting source as ENISA records it), euvd_id, vendor_project, product, first_seen_at, source, attribution }; only this endpoint serves it. ENISA's data — EUVD identifiers and the eu_kev object — is reproduced per record with acknowledgement (attribution, which begins "Source: ENISA EUVD") and is not relicensed; it is served only while EchelonGraph displays EUVD data, and no endpoint lists the eu_kev object or EUVD identifiers in bulk. List rows and MCP search_cves carry, per CVE, score_factors.eu_kev { listed, source, attribution } and Rule 1b's rule and rationale, each attributed; the bulk export carries no factors, so its echelongraph_score for a CVE only ENISA's EU KEV lists reflects that listing (Rule 1b's 9.0 floor) without naming it.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228
GET/api/v1/public/cves/match

Whether a product or a package at a version is affected by known CVEs: the matcher behind /am-i-affected. Send product (an NVD CPE product token) and version, or ecosystem (npm, PyPI, Maven and other OSV ecosystem names), package and version, in the X-EG-Product, X-EG-Version, X-EG-Ecosystem and X-EG-Package request headers (percent-encoded UTF-8), so the lookup is in no request log that records URLs; the same names as query parameters still work. Read assessed before count: assessed false means the lookup did not evaluate the component, not_assessed_reason says why, and a count of 0 then is not "not affected". Each registry match carries interval, the advisory interval that holds the version (introduced, and fixed, exclusive, or last_affected, inclusive), and fixed_in, the lowest version above the one asked about that no affected interval of the same record holds: the holding interval's fixed bound, moved past any overlapping interval that still holds it, which fixed_in_detail then names. It is always above the version asked about, and null where no such version is recorded (the walk reaches an interval that ends at last_affected, or has no end, with none beyond it) or no interval holds the version, and fixed_in_reason then says which. A null fixed_in is not a finding that no fix exists. A CPE match carries no fixed_in: the end of a CPE version range is not a fix, so it is given in its own field, cpe_version_end_excluding (the first version the matched NVD range no longer holds) or cpe_version_end_including (the last one it holds).

curl -H 'X-EG-Ecosystem: Maven' -H 'X-EG-Package: org.apache.logging.log4j:log4j-core' -H 'X-EG-Version: 2.14.1' https://app.echelongraph.io/api/v1/public/cves/match
POST/api/v1/public/cves/match/batch

The registry match for up to 200 components in one call: a JSON body of {"components": [{"purl": …} or {"ecosystem", "package", "version"}]} and nothing else (a CycloneDX or SPDX document posted whole is refused with a 400). Each row of results keeps the single match's answer, assessed and not_assessed_reason among them, each match with interval, fixed_in and fixed_in_reason as above. Each row also names the package its advisories were looked up by, matched_package, and how, matched_via: purl_name (the purl's own name), coordinates (the component's package), or upstream, for a deb, apk or alpine purl whose upstream qualifier (upstream=name or upstream=name@version) names the source package, which is how Debian and Alpine file advisories; an upstream row also carries purl_name, the purl's own (binary) name, and matched_version_from (upstream when the qualifier's version was used, purl when the purl's was). A deb, apk or rpm purl without a distro qualifier naming its release is not assessed (distro_release_unknown). The route is charged per component: 1,200 components a minute per caller.

curl -X POST -H 'content-type: application/json' -d '{"components":[{"purl":"pkg:apk/alpine/libcrypto3@3.3.6-r0?distro=alpine-3.20&upstream=openssl"}]}' https://app.echelongraph.io/api/v1/public/cves/match/batch
GET/api/v1/public/cves/{id}/related

Three lists of related CVEs, up to 10 each. same_product: CVEs sharing an affected package, most specific shared package first. same_vendor: CVEs sharing a vendor advisory, most shared advisories first. same_cwe: CVEs sharing a CWE, nearest in publish date first. EchelonGraph score only breaks ties. basis says why each list is what it is: ok, no_data (this CVE has no rows for that relation), no_siblings (it has rows, but no other CVE shares them), no_specific_key (same_product only: every package it shares is carried by too many CVEs to count as evidence), capped (same_vendor and same_cwe: siblings exist, but only through keys beyond the first 50 this lookup reads), or unknown (the reason was not established in time; the list itself is still correct). Powers the sidebar on every /pulse/{id} page.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/related
GET/api/v1/public/cves/{id}/references

Per-reference enrichment for one CVE — vendor advisory cross-walks, patch URLs, exploit POC indicators where surfaced.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/references
GET/api/v1/public/cves/{id}/enrichment

One CVE's enrichment sections in one answer, among them vendor advisories, patches, fixed versions, affected packages, CWEs, public exploit references and the enrichment timeline. failed_sections names any section that could not be read, served empty. Each affected_packages row carries fixed_branches, every affected range of the package with its fix ({introduced, fixed, last_affected, source, advisory_id}: fixed null where no fix is on record, last_affected null where the range ends at a fix, advisory_id the OSV record that published the range, in that record's order; fixed_branches null where the package's ranges were never loaded, [] where no version range is on record, which is not a finding that no fix exists). fixed_version on that row is one range's fix, kept for compatibility: it is not the fix for every affected version (CVE-2021-44228's log4j-core: 2.12.2, while 2.14.1 is in the range fixed in 2.15.0), so do not read it as yours. The fix for your version is the lowest version above it that no range in fixed_branches holds, in the package ecosystem's own version order: start from the fixed bound of a range your version is in, and move past any range that still holds that bound. Where the walk reaches a range that ends at last_affected, or has no end, with none beyond it, no fix is on record for your version. /api/v1/public/cves/match does this walk for you and returns the answer as fixed_in.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/enrichment
GET/api/v1/public/cves/trends

Weekly + monthly volume + week-over-week delta + severity distribution. Backs the dashboard cards on /pulse.

curl https://app.echelongraph.io/api/v1/public/cves/trends
GET/api/v1/public/cves/export.ndjson

Streaming NDJSON bulk export — one CVE per line. Filters: year, severity, kev_only, min_cvss. min_cvss matches the HIGHEST CVSS we hold for a CVE (v3.1, v4.0 or v2.0), so a v2-only 10.0 is included where a v3-only filter would have missed it. Hard cap 50,000 rows per call (iterate via the year param for the full dataset).

curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2024&severity=CRITICAL&kev_only=true" | jq -c .
GET/api/v1/public/vendor-advisories

Vendor-disclosed security advisories (Microsoft MSRC, Red Hat RHSA, GitHub GHSA, Cisco PSIRT, AWS, GCP, GitLab). Many appear here before NVD assigns a CVE-ID.

curl https://app.echelongraph.io/api/v1/public/vendor-advisories?has_cve=true&limit=50
GET/api/v1/public/vendor-advisories/{vendor}/{advisory_id}

Single vendor-advisory detail — title, description, CVSS, affected products, remediation, references, linked CVE IDs.

curl https://app.echelongraph.io/api/v1/public/vendor-advisories/github/GHSA-99gv-2m7h-3hh9

Bulk NDJSON Export

For researchers + security teams who want offline analysis. The export streams one CVE per line as JSON (NDJSON / JSON Lines), so you can pipe it through jq -c . or process incrementally without buffering the whole dataset.

# All KEV-listed CVEs published in 2024
curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2024&kev_only=true" \
  | jq -c '{cve_id, severity, echelongraph_score, kev_added: .kev_added_date}'

# Full 2023 CRITICAL dataset
curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2023&severity=CRITICAL" \
  > cves-2023-critical.ndjson

Hard cap: 50,000 rows per call. Iterate via the year param for the full dataset. The endpoint streams from a DB cursor, so memory pressure stays flat regardless of result size.

Scoring fields

Every CVE we have assessed carries two independent EchelonGraph numbers: EG Score answers “how severe?” and EG Risk answers “how urgently should I act?”. Since algorithm v4.0 every CVE that is not withdrawn also carries the three questions the EG Score mixes, published apart: eg_severity, eg_likelihood and eg_priority (methodology). When score_assessed is false, neither number is on the wire — no placeholder 0 stands in for an assessment we have not made. Each score also ships its own machine-readable breakdown in score_factors(including a plain-English score_factors.eg_risk.about / .summary), so the response is self-documenting.

FieldMeaning
echelongraph_score0–10 severity, escalated by confirmed exploitation (CVSS-comparable): NVD/GHSA CVSS, floored at 9.0 by a CISA KEV listing (algorithm v4.1+: or by an ENISA EU KEV listing of a CVE CISA does not list, rule_triggered eu_kev_exploitation; Source: ENISA EUVD) and lifted toward 9.0 by EPSS, with reject suppression. A KEV-floored 9.0 is exploitation, not impact: eg_severity gives the impact alone. READ score_assessed FIRST — when that is false this key is ABSENT: no rating exists, and no placeholder 0 is published in its place.
eg_severityAlgorithm v4.0+: impact severity alone — the CVSS base score (CVSS-B) from the best-provenance CVSS EchelonGraph holds, which no KEV listing, EPSS or EG-KEV tier raises: { assessed, score, severity, metric, cvss_version, vector, source, provider, summary }. With no CVSS of any version, assessed is false, score and severity are null (never 0) and unassessed_reason is kev_listed_no_cvss (exploited; severity not yet published), eu_kev_listed_no_cvss (v4.1+: the same for a CVE only ENISA's EU KEV lists; Source: ENISA EUVD) or no_cvss. Top-level on GET /cves/{id}; a list row carries it as score_factors.eg_severity. Absent from a row the v4.0 re-score has not reached and from a withdrawn record.
eg_likelihoodAlgorithm v4.0+: FIRST's EPSS probability of exploitation activity within 30 days, as it stood when the CVE was scored — { probability, horizon_days: 30, source: first_epss, epss_percentile, epss_score_date, epss_model_version } — FIRST's model, not EchelonGraph's. probability is null with null_reason no_epss_on_record when no EPSS is on record: unknown, not low, never 0. Confirmed exploitation is beside it, never folded into it: exploited, and exploited_sources (cisa_kev; eu_kev since v4.1, ENISA's EU KEV listing, Source: ENISA EUVD; eg_kev_tier1). epss_score can be newer until the CVE is re-scored. Top-level on GET /cves/{id}; score_factors.eg_likelihood on a list row.
eg_priorityAlgorithm v4.0+: the decision — ssvc { decision (at medium mission impact), mission, by_mission }, null with null_reason not_assessed on a CVE nobody has assessed; bod_26_04 { if_publicly_exposed, if_not_publicly_exposed }; inputs { kev, exploitation, automatable, technical_impact }, each { value, source }. The same decision and timeline as score_factors.ssvc and score_factors.bod_26_04. Top-level on GET /cves/{id}; score_factors.eg_priority on a list row.
score_assessedALWAYS PRESENT. false = we could NOT assess this CVE; echelongraph_score, echelongraph_severity and echelongraph_risk are then ABSENT — not 0, not NONE — and their absence is NOT a claim that the CVE is harmless. Never threshold, sort or alert on any of them without checking this. Any EchelonGraph number that IS present came from a real assessment, including a genuine 0.
score_unassessed_reasonWhy there is no assessment. "no_signal" = UNDECIDED, no source has published severity data yet (revisited automatically when one does). "rejected" = DECIDED, the record was withdrawn by its CNA/NVD. Absent when assessed.
vendor_severityCRITICAL / HIGH / MEDIUM / LOW stated by the VENDOR in its own advisory prose, for CNAs that publish no CVSS vector (e.g. Chromium). This is the vendor's taxonomy, NOT a CVSS band, and the two do not line up: a Chromium "Low" carries a median NVD CVSS of 5.4 (MEDIUM). Never compare it to a CVSS band or across vendors.
vendor_severity_sourceWhich vendor taxonomy vendor_severity came from, e.g. "chromium". Bands are only comparable within one source.
vendor_severity_evidenceThe exact advisory substring the band was parsed from, e.g. "(Chromium security severity: Low)" — so any published number is traceable to the text it came from.
echelongraph_severityCRITICAL / HIGH / MEDIUM / LOW — the band derived from echelongraph_score. ABSENT when score_assessed is false; the placeholder "NONE" it used to carry there is no longer published, because a word that reads as a rating is the same false claim as a 0. Measured 2026-09-20 over 2,383 assessed rows: the band is only ever one of the four above.
echelongraph_risk0–100 PRIORITY, with weights EchelonGraph set by hand: fuses impact severity — eg_severity, the CVSS base, since algorithm v4.0 (the EG score before it, which counted a KEV listing twice); score_factors.eg_risk.severity_source says which of eg_severity, vendor_severity or unknown_kev_most_urgent applied (45%) + exploitation — KEV/eg_kev/EPSS, score_factors.eg_risk.exploitation_source says which (40%) + automatability — the SSVC Automatable input, from CISA, a CVSS v4.0 vector or CISA's BOD 26-04 default (15%; score_factors.eg_risk.automatable_source says which). Separates equal-severity CVEs so the most dangerous surface first. Higher = act sooner. ABSENT when score_assessed is false: an un-assessed CVE has nothing to fuse, so no priority is published in its place.
echelongraph_ssvc_decisionThe SSVC decision (Act / Attend / Track* / Track) EchelonGraph computes at Mission & Well-being = MEDIUM by applying CISA's published decision tree (SSVC Guide, Nov 2022, Table 9) to the SSVC decision points. CISA publishes the tree and, through Vulnrichment, some of the inputs; it does not publish a decision for a CVE, so this is EchelonGraph's application of CISA's table. Since algorithm v3.4 an input CISA has not published comes only from what CISA or CERT/CC defines: exploitation from a CISA KEV listing (since v4.1 also an ENISA EU KEV listing of a CVE CISA does not list; Source: ENISA EUVD) or exploit code on record (never EPSS); automatable and technical impact from a CVSS v4.0 vector, else CISA's own BOD 26-04 default ("no" and "total") for a CVE not in KEV. SSVC is stakeholder-specific, so this is one column of the answer, not the answer for every reader: score_factors.ssvc.by_mission gives the decision at low, medium and high mission impact, and score_factors.ssvc names where each input came from. An input stays "unknown" only on a KEV-listed CVE CISA has not yet assessed; every decision it allows is listed, and this field is the most urgent of them.
score_confidenceHIGH / MEDIUM / LOW / NONE — how much corroborating enrichment backs the score. A CONFIDENCE, not a severity: the words overlap with the severity bands, so render it labelled ("medium confidence"), never where a severity goes.
score_factorsFull evidence blob: rule_triggered, sources_contributing, cvss/epss/kev/eg_kev inputs (v3.6+: epss.score_date and epss.model_version, the last daily EPSS file that covered the CVE and its model; v4.0+: kev.floor_applied — whether the 9.0 KEV floor set the score — and kev.floor_basis, confirmed_exploitation_not_impact), (v3.6+, nvd_cvss only) headline_cvss { source, provider } — whose CVSS the score is, eg_risk { risk, severity_norm, severity_source (v4.0+: eg_severity, vendor_severity or unknown_kev_most_urgent; v4.1+: unknown_eu_kev_most_urgent), exploitation, exploitation_source (v3.5+: kev, eg_kev_tier1, eg_kev_tier2 or eg_kev_tier3 — EchelonGraph's fixed values, not probabilities — epss or none; v4.1+: eu_kev, ENISA's EU KEV listing of a CVE CISA does not list), automatable, automatable_source (v3.4+), weights, about, summary }, (algorithm v3.2+) ssvc { tree, exploitation, automatable, technical_impact — each { value, source } — by_mission { low, medium, high }, decision, decision_mission }, (v3.4+) bod_26_04, (v4.0+) eg_severity, eg_likelihood and eg_priority, the same objects GET /cves/{id} serves at the top level, and (v4.1+, when ENISA's EU KEV catalogue lists the CVE) eu_kev { listed, source, attribution } — on Rule 1b also floor_applied and floor_basis — which says that ENISA lists it and carries ENISA's acknowledgement, and no value ENISA recorded about the listing (its date and reporting source are on GET /cves/{id}'s eu_kev only). rule_triggered eu_kev_exploitation (v4.1+) is Rule 1b: ENISA's EU KEV lists the CVE and CISA's KEV catalog does not; kev_exploitation stays CISA's, including when both list it.
score_factors.bod_26_04Algorithm v3.4+: CISA BOD 26-04's remediation timeline (Table 1), on every CVE that is not withdrawn, scored or not. Whether a system is publicly exposed is the reader's to answer, so both are given: if_publicly_exposed and if_not_publicly_exposed, each a list of CERT/CC's outcome keys — 3DF (3 days & forensic triage), 3D, 14D, 60D, FSU (Fix on system upgrade) — least to most urgent, named in timelines. One key unless an input is unknown (a KEV-listed CVE CISA has not yet assessed), when every timeline it allows is listed. Inputs: kev, automatable and technical_impact, each { value, source }; source cisa_bod_26_04_default marks CISA's own default for a CVE not in KEV that nobody has assessed (automatable no, technical impact total: 14 days if exposed, Fix on system upgrade if not).

Want to be notified when critical CVEs hit?

Subscribe to real-time or digest alerts covering NVD + vendor-disclosed advisories.

Subscribe via /pulse →

Notes

  • All endpoints are CORS-enabled for browser use.
  • Responses are versioned under /api/v1/. Breaking changes will land under /api/v2/ with a deprecation window.
  • Where two sources disagree on CVSS (CNA vs NVD analyst), the freshest modified timestamp wins. See Why is the EG score different from NVD?
  • Powered by direct feeds from MITRE cvelistV5 (fast), NVD API (deep), GHSA, CISA KEV, FIRST EPSS, and per-vendor advisory pollers.