Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS) avatar

Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS)

Pricing

Pay per usage

Go to Apify Store
Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS)

Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS)

Joins the official NVD CVE API 2.0, the CISA Known Exploited Vulnerabilities catalog and FIRST EPSS into one deduplicated, risk-prioritized vulnerability record with incremental change tracking and durable cross-run state.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Atlas

Atlas

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Turn three official, authoritative vulnerability feeds into one deduplicated, risk-prioritized CVE record you can pipe straight into a SIEM, a ticketing queue, a spreadsheet or a dashboard.

This Actor joins:

SourceEndpointRoleAuthority
NVD CVE API 2.0https://services.nvd.nist.gov/rest/json/cves/2.0Base entity (the CVE, CVSS, CWE, CPE, references)U.S. NIST National Vulnerability Database
CISA KEVhttps://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.jsonEnrichment: is it actively exploited?U.S. Cybersecurity and Infrastructure Security Agency
FIRST EPSShttps://api.first.org/data/v1/epssEnrichment: probability of exploitation (score + percentile)FIRST.org Exploit Prediction Scoring System

The output is one row per CVE with a transparent, deterministic risk-priority synthesis (critical / high / medium / low / unknown) that combines CVSS severity, EPSS likelihood and KEV membership — so "patch this first" is answered, not just "here are 1,000 CVEs".


Who it is for (ICP) & use cases

Ideal customer: vulnerability-management, threat-intel, and detection-engineering teams; MSSPs; GRC/compliance functions; and security-tooling vendors that need a clean, joined CVE feed without building and maintaining three separate ingestion pipelines.

Concrete use cases:

  • Risk-based patch prioritization. Rank your exposure by KEV membership + EPSS + CVSS instead of CVSS alone. Feed the risk.priority / risk.score into your patch SLA logic.
  • KEV compliance monitoring (e.g. BOD 22-01). Run with kevOnly: true to get exactly the CVEs on the CISA catalog, with their dueDate and requiredAction.
  • Continuous exposure watch for your stack. Filter by vendor / product / cpe (e.g. Apache, cpe:2.3:a:apache:log4j) and schedule incremental runs; you get only what is new or changed.
  • Emerging-threat triage. Filter epssMin: 0.5 or epssPercentileMin: 0.95 to surface CVEs the EPSS model considers likely to be exploited in the next 30 days.
  • Enrichment for an existing scanner. Provide a list of cveIds from your scanner and get back the joined NVD + KEV + EPSS + risk view for each.

Output schema

Every dataset item carries schemaVersion (currently 1.0.0) so downstream consumers can pin a version. recordId equals the canonical cveId — the stable, globally-unique key you can join on.

Key fields (see .actor/dataset_schema.json for the full contract):

{
"recordId": "CVE-2021-44228", // == cveId, the stable key
"schemaVersion": "1.0.0",
"source": "nvd",
"cveId": "CVE-2021-44228",
"vulnStatus": "Analyzed",
"description": "Apache Log4j2 JNDI ... (Log4Shell).",
"published": "2021-12-10T10:15:09.143Z",
"lastModified": "2024-05-20T12:00:00.000Z",
"cursorField": "lastModified", // which NVD date the incremental cursor keys on
"cursorDate": "2024-05-20T12:00:00.000Z",
"cvss": { "version": "3.1", "baseScore": 10, "baseSeverity": "CRITICAL", "vectorString": "...", "exploitabilityScore": 3.9, "impactScore": 6, "source": "nvd@nist.gov" },
"cweIds": ["CWE-502"],
"cpe": ["cpe:2.3:a:apache:log4j:2.0:*:*:*:*:*:*:*"],
"vendors": ["apache"],
"products": ["log4j", "Log4j2"],
"references": ["https://logging.apache.org/log4j/2.x/security.html"],
"enrichment": {
"kev": { "status": "ok", "listed": true, "dateAdded": "2021-12-10", "dueDate": "2021-12-24", "requiredAction": "...", "knownRansomwareCampaignUse": "Known", "vulnerabilityName": "..." },
"epss": { "status": "ok", "score": 0.94474, "percentile": 0.99988, "date": "2024-06-01" }
},
"risk": { "priority": "critical", "score": 98.6, "factors": ["cvss=10.0/v3.1","epss=0.9447","kev=listed+ransomware"], "rationale": "Priority critical: On the CISA KEV catalog and linked to ransomware campaigns; CVSS base 10.0; EPSS 0.9447 (p100.0)." },
"canonicalUrl": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228",
"provenance": { "sources": ["nvd","kev","epss"], "endpoints": ["..."], "fetchedAt": "2024-06-01T00:00:00.000Z", "nvdLastModified": "2024-05-20T12:00:00.000Z" },
"firstSeen": "2024-06-01T00:00:00.000Z",
"lastSeen": "2024-06-01T00:00:00.000Z",
"changedFields": []
}

Enrichment status is always explicit

enrichment.kev.status and enrichment.epss.status are one of ok / failed / not_enriched. A failed status means the upstream feed could not be fetched this run — it is never silently collapsed into "not listed" / "no score". This lets you distinguish "genuinely not exploited" from "we could not check", and it drives the safe cursor behavior described below.

Risk-priority synthesis (how risk is computed)

A transparent, documented heuristic (not an official standard), inspired by CISA's SSVC framing and the KEV "if it's on the catalog, patch it" mandate:

  1. KEV membership dominates. A CVE with confirmed in-the-wild exploitation is at least high, and critical when it is used in ransomware campaigns or carries a critical CVSS. Active exploitation outranks any predicted-likelihood signal.
  2. Otherwise the priority is a monotone combination of CVSS severity and EPSS likelihood, so a severe flaw that is also likely to be exploited outranks one that is merely severe.
  3. unknown only when none of CVSS, EPSS or KEV provide any signal.

risk.score (0–100) is a fixed-weight blend for ranking within a priority bucket:

  • CVSS contributes up to 60 (baseScore / 10 × 60),
  • EPSS probability contributes up to 25 (score × 25),
  • KEV membership contributes up to 15 — exactly 12 for a KEV-listed CVE and 15 when it is additionally linked to ransomware campaigns.

The EPSS percentile and the CVSS severity label influence the priority bucket (the critical/high/… classification above) but are deliberately not part of the numeric score, to avoid double-counting the EPSS signal. The function is pure and order-independent — identical inputs always yield an identical result — and risk.factors / risk.rationale explain every decision.


Input

All fields are optional. See .actor/input_schema.json for editor metadata. Numeric inputs are strictly validated — a non-numeric or out-of-range value (e.g. maxItems: 0, cvssMin: 15) is rejected with a clear error, not silently clamped, so an API caller that bypasses the input-schema editor cannot get surprising truncated behavior.

FieldTypeDefaultDescription
modeenum incremental | fullincrementalincremental: only CVEs new/changed since the last run. full: backfill over the lookback window (never advances the cursor).
cursorFieldenum lastModified | publishedlastModifiedWhich immutable NVD date the incremental cursor orders by. lastModified also surfaces re-analysed CVEs.
maxItemsinteger200Cap on dataset items after filtering/enrichment/dedup.
lookbackDaysinteger30First-run incremental window and full-mode backfill window.
recheckWindowDaysinteger2Re-check already-committed CVEs whose cursor date is within this many days below the cursor, to surface upstream edits. Never regresses the cursor.
maxNvdPagesinteger25Hard ceiling on NVD pages per run (cost guard). When hit, the cursor is held below the uncaptured region.
nvdApiKeystring (secret)Optional NVD API key. Raises the rate limit; sent only as a header, never logged.
cveIdsstring[]Explicit CVE allow-list. NVD is queried per-ID.
keywordstringFree-text; pushed to NVD keywordSearch and re-checked client-side.
vendor / product / cpestringCase-insensitive substring filters.
cvssMinnumber 0–10 (decimals allowed)Minimum CVSS base score, e.g. 7.5. CVEs with no CVSS are excluded.
kevOnlybooleanfalseKeep only CISA-KEV-listed CVEs.
epssMinnumber 0–1Minimum EPSS probability.
epssPercentileMinnumber 0–1Minimum EPSS percentile.
publishedFrom / publishedToISO dateInclusive published-date range. Pushed server-side into the NVD query when cursorField: published.
modifiedFrom / modifiedToISO dateInclusive last-modified-date range. Pushed server-side into the NVD query when cursorField: lastModified (the default).
sinceISO dateOverride the automatic incremental cursor for one run.
includeRawbooleantrueInclude a compact raw object of upstream values for auditing.

Example — KEV compliance watch:

{ "mode": "incremental", "kevOnly": true, "maxItems": 500 }

Example — likely-exploited, high-severity Apache exposure:

{ "vendor": "apache", "cvssMin": 7, "epssPercentileMin": 0.95, "mode": "incremental" }

Incremental mode, cursors & durability

Incremental runs persist a composite keyset cursor (cursorDate, cveId) in a named key-value store (cyber-exposure-intelligence-state, key CEI_STATE). A named store is required because the default store is per-run on the Apify platform and would lose cross-run state.

  • Config-isolated cursor and change tracking. Both are partitioned by a hash of the full filter set + cursor field. A narrow/filtered run therefore cannot advance the cursor used by a broader run — each distinct configuration drains independently — and a fresh configuration always emits its first run in full instead of having records suppressed as "unchanged" because a different configuration happened to see them earlier. Over-partitioning is safe (a new partition just starts from a conservative lookback).
  • Oldest-first forward drain. When more matching CVEs exist than maxItems, the oldest matching records are emitted first. The remainder is picked up on the next run — nothing is skipped, nothing is emitted twice.
  • The cursor is a scan watermark, not an emitted-match watermark. It advances over every record this run classified definitively — whether the verdict was "emit" or "reject", and whether the rejection came from a base filter (cvssMin, vendor, date range: decided from the NVD record alone) or from an enrichment filter (kevOnly, epssMin). This is what guarantees forward progress for a selective configuration: a kevOnly or cvssMin: 9.8 run whose backlog prefix contains thousands of non-matches still moves past them and reaches the matches behind them, instead of re-scanning the same prefix forever. The watermark stops at the first record that was not definitively handled (left un-enriched by the 3,000-record enrichment budget, or a match dropped by the maxItems cap) and is additionally clamped to NVD's completedThrough boundary, so no record is ever skipped.
  • Bounded recheck overlap. recheckWindowDays re-fetches already-committed CVEs just below the cursor and re-compares them, so genuine upstream NVD edits surface as changedFields. These re-checks never advance (or regress) the cursor; if the enrichment budget or the maxItems cap leaves part of the recheck window unchecked, the run reports recheckTruncated: true. That flag is independent of the forward drain: a run whose forward pass completed is still recorded as a successful drain (lastSuccessAt) — the recheck gap only tells you that an upstream edit below the cursor may not have been noticed yet. A forward gap is reported separately as forwardTruncated, and truncated stays as the aggregate of the two.
  • Change tracking. firstSeen / lastSeen / changedFields are maintained per CVE per configuration across runs. The durable state stores only a compact per-field digest of each tracked field — never the field values themselves — so an entry costs ~0.3 kB regardless of description/reference size.
  • Bounded state (12,000 entries) with rolling pruning. The whole state document is a single key-value record, and the platform serializes it pretty-printed into a request bounded by Apify's 9 MiB payload limit — so the ceiling is a size budget, not a free choice. At the measured worst-case ~330 pretty-printed bytes per entry, the 12,000-entry cap projects to ≈3.8 MiB, under half the limit; a test fills a real document to the cap and fails if that headroom erodes. Pruning is rolling and deliberately lossy: at the cap the least-recently-seen entries are dropped, and a dropped CVE that reappears later is re-emitted once as new (fresh firstSeen, empty changedFields) rather than being suppressed — over-reporting, never a missed change. A single run tracks at most ~6,000 CVEs (3,000 forward + 3,000 recheck), so the cap binds only across many runs or many configurations sharing one state document.

Deterministic output & dedup

CVEs are deduplicated by canonical cveId with an order-independent selector (newest lastModified wins, with documented tie-breaks), and the emitted rows are sorted by risk.score then a stable cveId order. The derived intelligence is deterministic: given the same upstream data, the risk synthesis, ordering, dedup winner and field values are reproducible regardless of fetch order.

Two fields are, by design, run-stamped and therefore not byte-identical across runs: provenance.fetchedAt and lastSeen (and firstSeen/changedFields, which are cross-run change-tracking outputs). Dataset pushes are append-only and not idempotent: the Apify dataset has no upsert/idempotency key, so a run that fails after pushing some rows and is retried will re-append those rows. Deduplicate downstream on recordId (== cveId), keeping the row with the newest provenance.fetchedAt.

Safety: partial failures never corrupt state

  • NVD (base) fails with zero records → the run throws and writes no cursor, so the platform retries and nothing is skipped. This includes a cveIds run in which every per-ID lookup failed: it is reported as a source failure rather than as a clean "0 records" run. A cveIds run where some lookups succeeded is a partial success — those records are emitted and the cursor is held.
  • KEV or EPSS enrichment fails but no filter depends on it → records are still emitted with that enrichment marked failed; the cursor advances normally (the CVE selection is unaffected). The last known-good enrichment snapshot is preserved: an optional outage never overwrites the stored KEV/EPSS/risk values with failure placeholders and never fabricates a changedFields entry for them, so the outage — and the subsequent recovery — do not produce spurious "changed" events. When the feed recovers, only a genuine change vs. the last good value is reported.
  • KEV or EPSS enrichment fails and a filter depends on it (kevOnly, epssMin, epssPercentileMin) → the run emits nothing for that dimension and holds the cursor, rather than misclassifying records. This is reported as enrichmentBlocked: true in the summary.
  • Write-before-state ordering. The durable cursor/state is committed last, only after every dataset record and the LAST_RUN_SUMMARY / OUTPUT records are written. Any failure before that final write leaves the cursor exactly where it was — a replay re-processes the same records and re-advances the same cursor. This guarantees no record is skipped; it does not de-duplicate the dataset, because a partial run's already-pushed rows are re-appended on retry (dedup downstream on recordId, as above).

Concurrency

Just before the final state write, the Actor re-reads and merges the persisted state (furthest cursor + unioned change-tracking entries). This narrows — but, because the Apify KV store has no compare-and-swap, cannot fully eliminate — a lost-update race between two runs of the same configuration that overlap in time.

Operational requirement — run at most one run per configuration at a time (maxConcurrency = 1). The Actor cannot self-enforce this (Apify has no built-in per-configuration mutex), so it is an operational contract:

  • Use a single non-overlapping schedule per configuration, and size maxNvdPages / lookbackDays so a run comfortably finishes before the next fires.
  • If you drive runs via the API/scheduler, set the run concurrency for this Actor to 1 (do not fan out concurrent runs that share the same filter set + cursorField, i.e. the same cursor partition).
  • Distinct configurations are already isolated by the partition key and may run concurrently safely — the constraint is only within one configuration.

Two runs of the same configuration that truly overlap can still lose one run's change-tracking updates (never causing a skipped record — the cursor only moves forward via maxCursor); keeping to one run per configuration avoids it entirely.

Each run also writes a LAST_RUN_SUMMARY (and OUTPUT) KV record with per-source status, counts by priority, KEV count, truncation/blocked flags (truncated, forwardTruncated, recheckTruncated, enrichmentBlocked), scan progress (scanned, cursorAdvancedTo) and source attribution. partialFailures lists every source that degraded — a hard failure and a source that succeeded only in part (e.g. a cveIds run in which some per-ID lookups failed, which reports status: "ok" alongside an error).

State document upgrade (v1 → v2)

The persisted state document moved to version 2 in Actor 1.0.5 (compact per-field digests + entries partitioned by configuration). An existing v1 document is migrated in place on first read: cursors carry over unchanged (a v1 watermark is always at or below where v2 semantics would have put it, so nothing can be skipped) and the stored snapshots are reduced to digests under a reserved legacy partition.

Adoption of those migrated baselines is a one-shot grant held by an explicit set of partitions, captured verbatim from the v1 document's cursors at migration time. Only a configuration that provably ran before the upgrade can inherit a pre-upgrade baseline — preserving its cursor and firstSeen, and avoiding a spurious "everything changed" burst — the grant is consumed the first time that configuration adopts, and once no grant remains the legacy entries are dropped. A configuration created after the migration is never in that set, so it always emits its own first sighting of a CVE as new; having run once (and therefore owning a cursor) does not make it eligible. Overlapping runs merge grants by intersection, so a grant one run already spent cannot be resurrected by another; at worst a pre-upgrade configuration loses an unspent grant and re-emits its baseline once with a fresh firstSeen. A mode: full configuration (which keeps no cursor, and re-emits its whole window every run regardless) has its firstSeen re-stamped once on the first post-upgrade run.


Pagination, NVD rate limits & backoff

  • Date-range push-down. NVD 2.0 accepts exactly one date-range type per query (pubStartDate / pubEndDate or lastModStartDate / lastModEndDate), and which one is used is fixed by cursorField. The matching publishedFrom/publishedTo (or modifiedFrom/modifiedTo) bounds are therefore intersected into the fetched window itself, so a bounded historical range is queried directly instead of being scanned and then discarded client-side — and completedThrough stops at the requested upper bound, so the cursor is never advanced over a range that was deliberately not fetched. An empty effective window (e.g. an upper bound below the incremental cursor) issues no NVD request and is logged as such. The other date pair cannot be expressed in the same query and stays a client-side filter (a warning names it); set cursorField to match the range you want narrowed server-side.
  • NVD 2.0 caps a single query's date range at 120 days and returns up to 2000 results per page. This Actor splits [since, now] into ≤120-day sub-windows and pages each via startIndex / resultsPerPage until totalResults is drained or maxNvdPages is hit. Pagination advances by the number of rows the server actually returned, not the requested page size, so a server-side page-size cap (or a short non-final page) can never leave a gap. Every page is validated (a vulnerabilities array + a numeric totalResults consistent with the rows); a malformed or error body is treated as an uncaptured window (cursor held), never as a drained empty result.
  • Truncation is skip-safe. NVD does not return window pages sorted by the cursor field, so when a run is cut short by maxNvdPages the incremental cursor is advanced only as far as the end of the last fully-drained sub-window; records above that boundary are still emitted and simply suppressed as unchanged once the cursor catches up next run. This guarantees no CVE is skipped.
  • Rate limiting. NVD's public guidance is roughly 5 requests / 30 s without a key and 50 / 30 s with a key. The Actor enforces a minimum spacing between NVD requests (~6 s unkeyed, ~0.7 s keyed) and additionally honours 429 / Retry-After (delta-seconds and HTTP-date forms) with capped exponential backoff + jitter. Supplying nvdApiKey is strongly recommended for large runs.
  • Response bodies are size-capped and reads are timeout-bounded to protect the Actor from oversized or stalled upstream responses.

Limitations & conservative assumptions

  • EPSS refresh vs. the incremental cursor. EPSS scores change daily, but a CVE's NVD lastModified does not change when only its EPSS score does. Incremental mode keyed on lastModified therefore will not re-surface a CVE purely because its EPSS moved. To refresh EPSS/KEV across your whole corpus, run mode: full (or a targeted cveIds / kevOnly run) on a cadence. The recheckWindowDays overlap only covers CVEs near the cursor.
  • Enrichment applies after the per-source cap window. Enrichment-dependent filters (kevOnly, epssMin) are evaluated on up to a bounded number (3,000) of the oldest matching CVEs per run; a very large filtered window is drained across multiple runs (reported via truncated). Forward progress is guaranteed even with a zero-match prefix: every record in the drain that is definitively classified — base-filter rejections (no enrichment needed) as well as enriched candidates that were emitted or rejected — carries the scan watermark forward, so a later run always reaches matches that lie beyond the budget and the backlog never stalls. The cursor is only ever held below records that were not fully processed (a match dropped by maxItems, a candidate left un-enriched by the budget, page-budget/mid-window NVD truncation, or a filter-required enrichment outage).
  • A rejected record is not re-evaluated once the watermark passes it. Scan progress is per configuration, so a CVE that failed this configuration's filters stays behind that configuration's cursor. With the default cursorField: lastModified any upstream edit (including a CVSS re-score that would now match) bumps lastModified and re-surfaces the CVE above the cursor. Under cursorField: published — which never changes after publication — use a periodic mode: full run to re-evaluate previously-rejected CVEs.
  • Recovering enrichment for CVEs committed during an optional outage. If a CVE is first seen while KEV/EPSS is down, it is stored with that enrichment failed and no score. It is re-enriched and corrected through one of three bounded paths: (1) the recheckWindowDays overlap re-checks CVEs just below the cursor every run; (2) any later NVD lastModified edit re-surfaces it above the cursor; and (3) a periodic mode: full run (recommended cadence: daily/weekly) re-enriches the whole lookback corpus. On correction the real KEV/EPSS data appears as a genuine changedFields event.
  • Very dense single windows. Because the cursor only advances past fully-drained sub-windows, a single ≤120-day window containing more than maxNvdPages × 2000 modified CVEs (e.g. during an NVD mass re-analysis event) cannot be fully drained in one run and will stall progress for that window. Raise maxNvdPages, supply an nvdApiKey, or narrow lookbackDays so each window fits the page budget. This is safe (no wrong data, no skips) but can slow a backfill.
  • CVSS selection. The best-available metric is chosen (v3.1 > v3.0 > v2, Primary > Secondary). For v2-only CVEs a severity label is derived from the base score, and CVSS scores originate from NVD (which may include ADP/CNA-provided values).
  • CPE-derived vendor/product are parsed heuristically from cpe:2.3 criteria and merged with KEV vendor/product; they are a convenience index, not an authoritative asset inventory.
  • KEV / EPSS coverage. KEV lists only the (relatively few) CVEs CISA has confirmed as exploited; EPSS only scores published CVEs. A missing entry with status: ok means genuinely absent, not an error.
  • The risk synthesis is a heuristic, not an official CISA/FIRST product; use it as one signal alongside your own context. All inputs are preserved so you can recompute your own scoring.
  • This Actor consumes public, unauthenticated feeds. No credentials are required (the optional NVD key only raises rate limits and is never logged).

Cost & pricing

Charge model. This Actor is billed on Apify platform usage — the standard pay-per-usage model: compute units (CUs) for actor runtime, plus platform data-transfer/storage and, for residential proxy if you enable it, proxy traffic. This Actor uses no paid proxy by default (it calls public feeds directly) and holds only a small named state document plus your dataset, so compute time is the dominant cost, and NVD rate-limiting is what drives run time.

How a CU is computed.

CU = (configured run memory in GB) × (runtime in hours)

So memory is a straight multiplier: the same run costs 4× as many CUs at 4096 MB as at 1024 MB. Halving memory halves the CU bill for a run of the same length.

Memory default. The Actor's remote defaultRunOptions.memoryMbytes is intended to be 1024 MB and is set to that value at deployment time; the numbers below assume that default. This is a default, not a limit — you can override memory per run (Actor run options), per schedule, or via the API/ActorClient (memory / memoryMbytes), and your actual CU consumption scales with whatever you choose. Check the memory shown on the Actor page or on a finished run if you need to confirm what your account is using.

What a run costs (worked estimates). At the intended 1024 MB default, CU ≈ run-minutes ÷ 60. The CU column is the durable number; convert it with your own plan's rate:

cost ≈ CU × (your plan's $ / CU)

The $ / CU rate is plan- and account-specific (published Apify plans have ranged roughly $0.20–$0.40 per CU, and negotiated or bundled rates differ) — so the cost column below is a range across that spread, not a quote for your account. Look up your effective rate in your Apify billing page and multiply the CU column by it.

ScenarioRequests / pacingApprox. runtimeApprox. CU @ 1024 MBCost = CU × your rate (illustrative at $0.20–$0.40/CU)
Routine incremental, lookbackDays: 2, with nvdApiKey (~0.7 s/req)~3–8 NVD + 1 KEV + a few EPSS~20–60 s~0.006–0.017~$0.001–$0.007
Routine incremental, lookbackDays: 2, no key (~6 s/req)same~1–2 min~0.02–0.03~$0.004–$0.013
KEV-compliance watch (kevOnly, key)~5–15 NVD + KEV + EPSS batches~1–3 min~0.02–0.05~$0.004–$0.02
Full 30-day backfill, no keyup to maxNvdPages × 6 s~3–15 min~0.05–0.25~$0.01–$0.10
Large 1-year backfill, key, high maxNvdPagesmany pages~15–60 min~0.25–1.0~$0.05–$0.40

These are estimates, not a quote — actual cost depends on your Apify plan's CU rate, the run memory you configure (multiply the CU column by memory_GB ÷ 1, e.g. ×4 at 4096 MB), how many CVEs match, and NVD's current latency. Cost scales ≈ linearly with the number of NVD pages fetched, which is why an API key (10× the request rate) and a small lookbackDays cut cost the most.

How to keep it cheap:

  • Use an nvdApiKey — the single biggest lever (≈6 s → ≈0.7 s per request).
  • Check the run memory. CU scales linearly with it, and this Actor is single-threaded with maxConcurrency = 1, so it does not need more than the 1024 MB default; raising memory raises cost without shortening a run that is bound by NVD rate-limiting.
  • Prefer incremental mode with a modest lookbackDays; reserve mode: full for occasional corpus refreshes. Truncated work is safely resumed next run, so small, frequent incremental runs are cheaper and fresher than rare huge ones.
  • Size maxItems / maxNvdPages to what you actually consume.
  • KEV is a single (few-MB) download per run; EPSS is queried in batches of ≤100 CVE IDs (with an explicit limit so no scores are dropped), so enrichment cost scales with the number of matched CVEs, not the whole catalog.
  • includeRaw: false trims dataset storage/transfer if you do not need the audit fields.

Development

npm install
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run build # tsc -> dist/
npm test # node:test unit + adversarial + smoke tests
npm run smoke:local # offline, fixture-backed end-to-end run (CEI_SMOKE_FIXTURES=1)

The test suite covers caps/truncation, incremental cursor drain across runs, scan-watermark progress over base-filter rejections (zero-match filters, rare/lagging matches, repeated multi-run progress, page-budget truncation), config-isolated cursors and change tracking, source outages (NVD hard-fail including total cveIds lookup failure, KEV/EPSS failures), enrichment-blocked cursor holds, partial enrichment (including partial-failure reporting), forward-vs-recheck truncation, the measured 9 MiB state-size budget and rolling pruning, v1 migration and its one-shot adoption grants (a post-migration configuration must never inherit a pre-upgrade baseline), write-failure state safety, and concurrency/state merge. Fixtures under test/fixtures/ mirror the real NVD 2.0 / CISA KEV / FIRST EPSS response shapes.

Source attribution

Data is retrieved from and remains the property of its respective authorities:

Each record's provenance and canonicalUrl point back to the authoritative sources. Always verify against the official source before acting on a specific CVE.