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 Assistent
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
0
Monthly active users
2 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. |
modifiedFrom / modifiedTo | ISO date | — | Inclusive last-modified-date range. |
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. The cursor is 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. 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 and the cursor advances to the newest emitted record. The remainder is picked up on the next run — nothing is skipped, nothing is emitted twice. - 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. - Change tracking.
firstSeen/lastSeen/changedFieldsare maintained per CVE across runs.
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.
- 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 and source attribution.
Pagination, NVD rate limits & backoff
- 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: because every candidate in that 3,000-record prefix is definitively classified when its enrichment succeeds, the cursor advances past the fully processed non-matches (a scan watermark), so a later run always reaches matches that lie beyond the budget — the backlog never stalls. The cursor is only ever held below records that were not fully processed (page-budget/mid-window NVD truncation, or a filter-required enrichment outage). - 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 (1 CU = 1 GB of RAM × 1 hour) 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.
What a run costs (worked estimates). Default memory is 1 GB, so CU ≈ run-minutes ÷ 60. Using Apify's platform reference of roughly $0.4 / CU (check your current plan for the exact rate):
| Scenario | Requests / pacing | Approx. runtime | Approx. CU | Approx. cost |
|---|---|---|---|---|
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.002–$0.007 |
Routine incremental, lookbackDays: 2, no key (~6 s/req) | same | ~1–2 min | ~0.02–0.03 | ~$0.008–$0.013 |
KEV-compliance watch (kevOnly, key) | ~5–15 NVD + KEV + EPSS batches | ~1–3 min | ~0.02–0.05 | ~$0.008–$0.02 |
| Full 30-day backfill, no key | up to maxNvdPages × 6 s | ~3–15 min | ~0.05–0.25 | ~$0.02–$0.10 |
Large 1-year backfill, key, high maxNvdPages | many pages | ~15–60 min | ~0.25–1.0 | ~$0.10–$0.40 |
These are estimates, not a quote — actual cost depends on your Apify plan's CU rate, the run memory
you configure, 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). - 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, config-isolated
cursors, source outages (NVD hard-fail, KEV/EPSS failures), enrichment-blocked cursor holds, partial
enrichment, 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.