--- name: cve-vulnerability-scraper description: Read the NIST National Vulnerability Database as structured records via the Apify Actor arman-bd/cve-vulnerability-scraper. Returns one flat row per CVE with the English description, CVSS v3 base score, severity and vector plus exploitability and impact sub-scores, CWE weakness IDs, every affected CPE configuration with its version bounds, a vendor:product summary, tagged reference URLs and the current NVD analysis status. Use when a task needs a vulnerability feed for a keyword or a specific CPE product, a severity-filtered daily sync into a ticketing or patch pipeline, or bulk lookup of named CVE IDs for research. Not for exploit code, KEV or EPSS scoring, CVSS v2 metrics, vendor advisories that have no CVE, or package-level advisory feeds. --- # CVE Vulnerability Scraper Apify Actor `arman-bd/cve-vulnerability-scraper`. Give it a keyword, a CPE, a published-date floor or a list of CVE IDs, get one dataset record per vulnerability from the NIST National Vulnerability Database. It runs without credentials. A free NVD API key is optional and only raises the pace. ## When to use it - A daily vulnerability sync: everything published since your last run, above a severity floor, handed to a ticketing system. - Watching an exact product you run, pinned by its CPE 2.3 URI rather than by a word that also appears in unrelated descriptions. - Bulk lookup of a named set of CVE IDs with full vectors, CWE classes and references for a write-up or an audit. - Building a searchable local mirror of a slice of NVD: descriptions, scores and affected version ranges. - Version-range matching, where `cpeMatches` gives you the inclusive and exclusive bounds rather than a prose sentence about affected versions. ## When not to use it - Exploit code, proof-of-concept links beyond what NVD tags as `Exploit`, or any exploitation guidance. This reports what NVD publishes and nothing else. - KEV membership, EPSS probabilities, or CVSS v2 metrics. None are returned. - Ecosystem advisory feeds keyed on a package name and version, as opposed to a CPE. A CVE that has no CPE configuration will still come back, with an empty `affectedProducts`. - Fuzzy or boolean search. The keyword filter requires every word to appear, and there is no OR, no phrase quoting and no negation. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/cve-vulnerability-scraper').call({ keywordSearch: 'openssl', minCvssScore: 7, publishedFrom: '2026-01-01', maxResults: 25, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~cve-vulnerability-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"cveIds":["CVE-2024-3094","CVE-2021-44228"]}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/cve-vulnerability-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input Every field is optional, but a query has to be built out of something. | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `keywordSearch` | string | no | `""` | Matched against CVE descriptions. All words must appear, case-insensitively, so `microsoft outlook` returns only CVEs mentioning both. | | `cveIds` | string[] | no | `[]` | Exact identifiers such as `CVE-2024-3094`. When set, this becomes a lookup: one request per ID, and `keywordSearch` and `cpeName` are ignored. | | `cpeName` | string | no | `""` | A complete, well-formed CPE 2.3 URI, for example `cpe:2.3:o:linux:linux_kernel:6.6:*:*:*:*:*:*:*`. Partial URIs are rejected upstream and fail that query unit immediately. | | `minCvssScore` | integer | no | `0` | Keep only CVEs with a CVSS v3 base score at or above this, 0 to 10. Applied locally after fetching. | | `publishedFrom` | string | no | `""` | Only CVEs published on or after this date, `YYYY-MM-DD`. Ranges longer than 120 days are split into consecutive windows automatically. | | `maxResults` | integer | no | `100` | Cap on total CVEs saved across every query unit. `0` means no cap. | | `nvdApiKey` | string | no | `""` | A free NVD API key. Raises the pace from one request every 6.5 seconds to one every 0.8 seconds. Secret field. | **Pacing is the whole cost model, and pacing is per request, not per CVE.** A page holds up to 2,000 CVEs, so a single keyword query usually finishes in one or two requests regardless of how many rows come back. What multiplies requests is the number of query units: one per CVE ID, or one per 120-day window when `publishedFrom` is set, or exactly one otherwise. A `publishedFrom` two years back is six windows, which is roughly 40 seconds of enforced waiting without a key and 5 seconds with one. A list of 200 CVE IDs is 200 units, which is over 20 minutes without a key. Supply `nvdApiKey` whenever the unit count is above about a dozen, and prefer `cpeName` to `keywordSearch` when you know the CPE, because keyword matching is textual and will catch unrelated mentions. ## Output One record per CVE. Never written twice, even when several query units match it. | Field | Type | Notes | |---|---|---| | `cveId` | string | The identifier, upper-cased. Join and dedup key. | | `description` | string \| null | English description, or the first available language if there is no English one. | | `publishedDate` | string \| null | NVD publication timestamp. Naive, no timezone suffix, unlike `scrapedAt`. | | `lastModified` | string \| null | Last NVD revision, same naive format. Watch this to catch re-scores. | | `cvssV3Score` | number \| null | Base score, preferring NIST's own Primary assessment over a vendor's Secondary one, v3.1 before v3.0. | | `cvssV3Severity` | string \| null | `NONE`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`, from the same metric as the score. | | `cvssV3Vector` | string \| null | Full vector string, for example `CVSS:3.1/AV:N/AC:L/…`. | | `cweIds` | string[] | Weakness classes, deduplicated, only entries starting `CWE-`. Empty when NVD recorded a non-CWE placeholder. | | `affectedProducts` | string[] | Deduplicated `vendor:product` labels derived from the CPE matches. Entries marked not vulnerable, and wildcard or `n/a` vendors, are dropped. | | `cpeMatches` | object[] | Every affected configuration, deduplicated on `criteria`, children of nested nodes included. Keys: `criteria`, `vulnerable`, `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, `versionEndExcluding`. | | `references` | object[] | Deduplicated on URL. Keys: `url`, `source`, `tags`. Tags are NVD's own, such as `Patch`, `Exploit`, `Vendor Advisory`. | | `exploitabilityScore` | number \| null | CVSS v3 sub-score from the same chosen metric. | | `impactScore` | number \| null | CVSS v3 sub-score from the same chosen metric. | | `status` | string \| null | NVD analysis state: `Analyzed`, `Modified`, `Awaiting Analysis`, `Rejected` and so on. | | `nvdUrl` | string | Link to the NVD detail page for this CVE. | | `scrapedAt` | string | Run timestamp, ISO 8601 with a `Z`. | A real record, long strings trimmed: ```json { "cveId": "CVE-2025-59464", "description": "A memory leak in Node.js's OpenSSL integration occurs when converting `X.509` certificate fields to UTF-8 without freeing the allocated buffer.", "publishedDate": "2026-01-20T21:16:03.900", "lastModified": "2026-06-17T09:46:12.450", "cvssV3Score": 7.5, "cvssV3Severity": "HIGH", "cvssV3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "cweIds": ["CWE-400"], "affectedProducts": ["nodejs:node.js"], "cpeMatches": [ { "criteria": "cpe:2.3:a:nodejs:node.js:*:*:*:*:-:*:*:*", "vulnerable": true, "versionStartIncluding": "24.0.0", "versionStartExcluding": null, "versionEndIncluding": null, "versionEndExcluding": "24.12.0" } ], "references": [ { "url": "https://nodejs.org/en/blog/vulnerability/december-2025-security-releases", "source": "support@hackerone.com", "tags": ["Release Notes", "Vendor Advisory"] } ], "exploitabilityScore": 3.9, "impactScore": 3.6, "status": "Analyzed", "nvdUrl": "https://nvd.nist.gov/vuln/detail/CVE-2025-59464", "scrapedAt": "2026-08-06T11:32:03.941Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "queryUnits": 2, "queryUnitsFailed": 0, "requestsMade": 2, "cvesSaved": 25, "cvesBelowCvssThreshold": 19, "failures": [], "filters": { "keywordSearch": "openssl", "cveIds": [], "cpeName": null, "minCvssScore": 7, "publishedFrom": "2026-01-01", "maxResults": 25 }, "usedApiKey": false, "finishedAt": "2026-08-06T11:32:10.967Z" } ``` A *query unit* is one independent piece of work: one CVE-ID lookup, one 120-day published-date window, or the single unfiltered query. `queryUnitsFailed` above zero means a slice of your query never ran, and each `failures` entry carries the unit `source` label, the `startIndex` it stopped at, and the `error`. Re-run just those. `cvesSaved` equal to `maxResults` means you were truncated, not that the query is exhausted. `cvesBelowCvssThreshold` explains the gap between how many CVEs matched upstream and how many you kept. ## Behaviour to plan around - **The cap truncates the oldest end, not the newest.** Date windows are generated forwards from `publishedFrom` to now, and the run stops the moment `maxResults` is reached, abandoning the remaining windows. Combining a distant `publishedFrom` with a small `maxResults` gives you the oldest CVEs in the range, which is almost never what a monitoring task wants. Either set `maxResults: 0` or keep the window short. - **`minCvssScore` is applied locally, and it drops unscored CVEs.** Anything with no CVSS v3 metric at all, typically pre-2016 CVEs or ones still awaiting analysis, fails a threshold above zero and is counted in `cvesBelowCvssThreshold`. Use `0` if you want those records. - **`cveIds` overrides the search filters.** When it is non-empty, `keywordSearch` and `cpeName` are ignored entirely because the source does not accept them alongside an ID. `minCvssScore` still applies, locally, so a lookup can legitimately return fewer records than IDs you passed. - **Malformed CVE IDs vanish silently.** Only strings matching `CVE--` are kept, duplicates are removed, and the rest are dropped before any request. Compare the length of `filters.cveIds` in the summary against your input list. - **A rejected query fails fast, not after three retries.** A malformed CPE or an illegal date range is a bug in the request, so that unit errors immediately and lands in `failures`. Transient conditions get three attempts with linear backoff. - **One failing unit never aborts the run.** The Actor only throws when every unit failed and nothing was saved. Treat `queryUnitsFailed > 0` as the failure signal. - **`publishedDate` and `lastModified` have no timezone marker.** They are naive timestamps. Do not feed them to a parser that assumes local time; treat them as UTC. - **`affectedProducts` can be empty while `cpeMatches` is not.** The summary drops entries flagged not vulnerable and any CPE whose vendor field is a wildcard, so a CVE with only running-on configurations shows an empty product list. - **Only CVSS v3 is reported.** A CVE that carries only a v2 metric comes back with all five CVSS fields `null`, and `status` will usually explain why. ## Recipes **Daily severity-filtered sync.** Everything serious published since your last run. Leave `maxResults` at 0 so no window is abandoned. ```json { "publishedFrom": "2026-08-01", "minCvssScore": 7, "maxResults": 0 } ``` Move `publishedFrom` forward each run and diff on `cveId`. Re-check `lastModified` on records you already hold to catch re-scores. **Watch one exact product.** Pin the CPE rather than a keyword, so you do not pick up CVEs that merely mention the name. ```json { "cpeName": "cpe:2.3:o:linux:linux_kernel:6.6:*:*:*:*:*:*:*", "minCvssScore": 8, "maxResults": 200 } ``` Match your installed version against `cpeMatches[].versionStartIncluding` and `versionEndExcluding` to decide whether you are actually in range. **Bulk lookup for a write-up.** One request per ID, so supply a key for anything longer than a dozen. ```json { "cveIds": ["CVE-2024-3094", "CVE-2021-44228", "CVE-2014-0160"], "nvdApiKey": "…" } ``` Take `cvssV3Vector`, `cweIds` and the `Patch`-tagged entries in `references`. **Rule of thumb for the API key.** Count your query units first: CVE IDs, or 120-day windows since `publishedFrom`, or one. Under about a dozen units you do not need a key. Above that, supply `nvdApiKey` or expect the run to spend most of its time waiting.