Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS)
Pricing
Pay per usage
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
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:
| Source | Endpoint | Role | Authority |
|---|---|---|---|
| NVD CVE API 2.0 | https://services.nvd.nist.gov/rest/json/cves/2.0 | Base entity (the CVE, CVSS, CWE, CPE, references) | U.S. NIST National Vulnerability Database |
| CISA KEV | https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json | Enrichment: is it actively exploited? | U.S. Cybersecurity and Infrastructure Security Agency |
| FIRST EPSS | https://api.first.org/data/v1/epss | Enrichment: 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.scoreinto your patch SLA logic. - KEV compliance monitoring (e.g. BOD 22-01). Run with
kevOnly: trueto get exactly the CVEs on the CISA catalog, with theirdueDateandrequiredAction. - 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.5orepssPercentileMin: 0.95to surface CVEs the EPSS model considers likely to be exploited in the next 30 days. - Enrichment for an existing scanner. Provide a list of
cveIdsfrom 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:
- KEV membership dominates. A CVE with confirmed in-the-wild exploitation is at least
high, andcriticalwhen it is used in ransomware campaigns or carries a critical CVSS. Active exploitation outranks any predicted-likelihood signal. - 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.
unknownonly 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.
| Field | Type | Default | Description |
|---|---|---|---|
mode | enum incremental | full | incremental | incremental: only CVEs new/changed since the last run. full: backfill over the lookback window (never advances the cursor). |
cursorField | enum lastModified | published | lastModified | Which immutable NVD date the incremental cursor orders by. lastModified also surfaces re-analysed CVEs. |
maxItems | integer | 200 | Cap on dataset items after filtering/enrichment/dedup. |
lookbackDays | integer | 30 | First-run incremental window and full-mode backfill window. |
recheckWindowDays | integer | 2 | Re-check already-committed CVEs whose cursor date is within this many days below the cursor, to surface upstream edits. Never regresses the cursor. |
maxNvdPages | integer | 25 | Hard ceiling on NVD pages per run (cost guard). When hit, the cursor is held below the uncaptured region. |
nvdApiKey | string (secret) | — | Optional NVD API key. Raises the rate limit; sent only as a header, never logged. |
cveIds | string[] | — | Explicit CVE allow-list. NVD is queried per-ID. |
keyword | string | — | Free-text; pushed to NVD keywordSearch and re-checked client-side. |
vendor / product / cpe | string | — | Case-insensitive substring filters. |
cvssMin | number 0–10 (decimals allowed) | — | Minimum CVSS base score, e.g. 7.5. CVEs with no CVSS are excluded. |
kevOnly | boolean | false | Keep only CISA-KEV-listed CVEs. |
epssMin | number 0–1 | — | Minimum EPSS probability. |
epssPercentileMin | number 0–1 | — | Minimum EPSS percentile. |
publishedFrom / publishedTo | ISO date | — | Inclusive published-date range. Pushed server-side into the NVD query when cursorField: published. |
modifiedFrom / modifiedTo | ISO date | — | Inclusive last-modified-date range. Pushed server-side into the NVD query when cursorField: lastModified (the default). |
since | ISO date | — | Override the automatic incremental cursor for one run. |
includeRaw | boolean | true | Include 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: akevOnlyorcvssMin: 9.8run 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 themaxItemscap) and is additionally clamped to NVD'scompletedThroughboundary, so no record is ever skipped. - Bounded recheck overlap.
recheckWindowDaysre-fetches already-committed CVEs just below the cursor and re-compares them, so genuine upstream NVD edits surface aschangedFields. These re-checks never advance (or regress) the cursor; if the enrichment budget or themaxItemscap leaves part of the recheck window unchecked, the run reportsrecheckTruncated: 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 asforwardTruncated, andtruncatedstays as the aggregate of the two. - Change tracking.
firstSeen/lastSeen/changedFieldsare 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, emptychangedFields) 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
cveIdsrun in which every per-ID lookup failed: it is reported as a source failure rather than as a clean "0 records" run. AcveIdsrun 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 achangedFieldsentry 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 asenrichmentBlocked: truein the summary. - Write-before-state ordering. The durable cursor/state is committed last, only after every
dataset record and the
LAST_RUN_SUMMARY/OUTPUTrecords 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 onrecordId, 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/lookbackDaysso 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/pubEndDateorlastModStartDate/lastModEndDate), and which one is used is fixed bycursorField. The matchingpublishedFrom/publishedTo(ormodifiedFrom/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 — andcompletedThroughstops 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); setcursorFieldto 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 viastartIndex/resultsPerPageuntiltotalResultsis drained ormaxNvdPagesis 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 (avulnerabilitiesarray + a numerictotalResultsconsistent 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
maxNvdPagesthe 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. SupplyingnvdApiKeyis 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
lastModifieddoes not change when only its EPSS score does. Incremental mode keyed onlastModifiedtherefore will not re-surface a CVE purely because its EPSS moved. To refresh EPSS/KEV across your whole corpus, runmode: full(or a targetedcveIds/kevOnlyrun) on a cadence. TherecheckWindowDaysoverlap 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 viatruncated). 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 bymaxItems, 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: lastModifiedany upstream edit (including a CVSS re-score that would now match) bumpslastModifiedand re-surfaces the CVE above the cursor. UndercursorField: published— which never changes after publication — use a periodicmode: fullrun 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
failedand no score. It is re-enriched and corrected through one of three bounded paths: (1) therecheckWindowDaysoverlap re-checks CVEs just below the cursor every run; (2) any later NVDlastModifiededit re-surfaces it above the cursor; and (3) a periodicmode: fullrun (recommended cadence: daily/weekly) re-enriches the whole lookback corpus. On correction the real KEV/EPSS data appears as a genuinechangedFieldsevent. - Very dense single windows. Because the cursor only advances past fully-drained sub-windows, a
single ≤120-day window containing more than
maxNvdPages × 2000modified CVEs (e.g. during an NVD mass re-analysis event) cannot be fully drained in one run and will stall progress for that window. RaisemaxNvdPages, supply annvdApiKey, or narrowlookbackDaysso 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.3criteria 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: okmeans 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.
| Scenario | Requests / pacing | Approx. runtime | Approx. CU @ 1024 MB | Cost = 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 key | up to maxNvdPages × 6 s | ~3–15 min | ~0.05–0.25 | ~$0.01–$0.10 |
Large 1-year backfill, key, high maxNvdPages | many 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; reservemode: fullfor 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/maxNvdPagesto 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
limitso no scores are dropped), so enrichment cost scales with the number of matched CVEs, not the whole catalog. includeRaw: falsetrims dataset storage/transfer if you do not need the audit fields.
Development
npm installnpm run typecheck # tsc --noEmitnpm run lint # eslintnpm run build # tsc -> dist/npm test # node:test unit + adversarial + smoke testsnpm 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:
-
NVD — U.S. NIST National Vulnerability Database (https://nvd.nist.gov/developers/vulnerabilities). "This product uses data from the NVD API but is not endorsed or certified by the NVD."
-
CISA KEV — U.S. Cybersecurity and Infrastructure Security Agency (https://www.cisa.gov/known-exploited-vulnerabilities-catalog).
-
EPSS — FIRST.org Exploit Prediction Scoring System (https://www.first.org/epss/). Per FIRST's attribution guidance, cite EPSS as:
Jacobs, J., Romanosky, S., Adjerid, I., & Baker, W. — Exploit Prediction Scoring System (EPSS), FIRST.org. Data via the EPSS API (https://api.first.org/data/v1/epss), © FIRST.org. Used under the FIRST EPSS terms of use; EPSS is a trademark of FIRST.org, Inc. and this Actor is not affiliated with or endorsed by FIRST.
Each record's provenance and canonicalUrl point back to the authoritative sources. Always verify
against the official source before acting on a specific CVE.