# Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS) (`atlas_digital_assistant/cyber-exposure-intelligence`) Actor

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.

- **URL**: https://apify.com/atlas\_digital\_assistant/cyber-exposure-intelligence.md
- **Developed by:** [Atlas Assistent](https://apify.com/atlas_digital_assistant) (community)
- **Categories:** Developer tools, Business
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

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

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.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):

```jsonc
{
  "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.

| 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:**

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

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

```json
{ "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.** `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.
- **Change tracking.** `firstSeen` / `lastSeen` / `changedFields` are 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 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 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 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:** 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 `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 (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`; 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

```bash
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, 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.

# Actor input Schema

## `mode` (type: `string`):

incremental: only CVEs new/changed since the last run (or the lookback window on first run). full: a broad backfill over the lookback window (never advances the cursor).

## `cursorField` (type: `string`):

Which immutable NVD date the incremental keyset cursor orders by. lastModified surfaces re-analysed CVEs (recommended); published tracks only newly disclosed CVEs.

## `maxItems` (type: `integer`):

Upper bound on the number of records saved to the dataset (after filtering, enrichment and deduplication).

## `lookbackDays` (type: `integer`):

First-run incremental window and full-mode backfill window. NVD limits a single query to a 120-day span, so longer windows are fetched as multiple sub-windows.

## `recheckWindowDays` (type: `integer`):

Incremental overlap: re-fetch and re-compare already-committed CVEs whose cursor date is within this many days below the cursor, so upstream NVD edits are surfaced as changed records. The cursor is never regressed by these re-checks. Set 0 to disable.

## `maxNvdPages` (type: `integer`):

Hard ceiling on NVD API pages fetched in one run (cost/runtime guard). When hit, the cursor is held below the uncaptured region and the remainder is picked up next run.

## `nvdApiKey` (type: `string`):

Optional NVD API key. Raises the NVD rate limit (~50 vs ~5 requests / 30 s) and is sent only as a request header — never logged. Leave empty to run unauthenticated.

## `cveIds` (type: `array`):

Explicit CVE ID allow-list, e.g. CVE-2021-44228. When set, NVD is queried per-ID and only these CVEs are returned.

## `keyword` (type: `string`):

Free-text keyword. Pushed to NVD server-side (keywordSearch) and re-checked against description / CPE client-side.

## `vendor` (type: `string`):

Case-insensitive substring match against vendors parsed from CPE / KEV.

## `product` (type: `string`):

Case-insensitive substring match against products parsed from CPE / KEV.

## `cpe` (type: `string`):

Case-insensitive substring match against CPE 2.3 match criteria, e.g. cpe:2.3:a:apache:log4j.

## `cvssMin` (type: `string`):

Keep only CVEs whose best-available CVSS base score is at least this value (0-10, decimals allowed, e.g. 7.5 or 9.8). CVEs with no CVSS score are excluded.

## `kevOnly` (type: `boolean`):

Keep only CVEs on the CISA Known Exploited Vulnerabilities catalog. If the KEV fetch fails while this is on, the run emits nothing and holds the cursor rather than misclassifying records.

## `epssMin` (type: `string`):

Keep only CVEs whose EPSS exploit-probability is at least this value (0-1, e.g. 0.5). Requires the EPSS enrichment.

## `epssPercentileMin` (type: `string`):

Keep only CVEs whose EPSS percentile is at least this value (0-1, e.g. 0.95). Requires the EPSS enrichment.

## `publishedFrom` (type: `string`):

ISO date, e.g. 2024-01-01. Keeps only CVEs published on/after this date. Invalid calendar dates are rejected.

## `publishedTo` (type: `string`):

ISO date, e.g. 2024-12-31. Keeps only CVEs published on/before this date.

## `modifiedFrom` (type: `string`):

ISO date. Keeps only CVEs last modified on/after this date.

## `modifiedTo` (type: `string`):

ISO date. Keeps only CVEs last modified on/before this date.

## `since` (type: `string`):

Optional ISO date/timestamp overriding the automatic incremental cursor for this run.

## `includeRaw` (type: `boolean`):

Include a compact `raw` object of selected upstream values for auditing. Disable for leaner output.

## Actor input object example

```json
{
  "mode": "incremental",
  "cursorField": "lastModified",
  "maxItems": 200,
  "lookbackDays": 30,
  "recheckWindowDays": 2,
  "maxNvdPages": 25,
  "kevOnly": false,
  "includeRaw": true
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("atlas_digital_assistant/cyber-exposure-intelligence").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("atlas_digital_assistant/cyber-exposure-intelligence").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call atlas_digital_assistant/cyber-exposure-intelligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=atlas_digital_assistant/cyber-exposure-intelligence",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Cyber Exposure Intelligence (NVD + CISA KEV + FIRST EPSS)",
        "description": "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.",
        "version": "1.0",
        "x-build-id": "UScFk0wxIw0Mun9hv"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/atlas_digital_assistant~cyber-exposure-intelligence/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-atlas_digital_assistant-cyber-exposure-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/atlas_digital_assistant~cyber-exposure-intelligence/runs": {
            "post": {
                "operationId": "runs-sync-atlas_digital_assistant-cyber-exposure-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/atlas_digital_assistant~cyber-exposure-intelligence/run-sync": {
            "post": {
                "operationId": "run-sync-atlas_digital_assistant-cyber-exposure-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "mode": {
                        "title": "Run mode",
                        "enum": [
                            "incremental",
                            "full"
                        ],
                        "type": "string",
                        "description": "incremental: only CVEs new/changed since the last run (or the lookback window on first run). full: a broad backfill over the lookback window (never advances the cursor).",
                        "default": "incremental"
                    },
                    "cursorField": {
                        "title": "Incremental cursor field",
                        "enum": [
                            "lastModified",
                            "published"
                        ],
                        "type": "string",
                        "description": "Which immutable NVD date the incremental keyset cursor orders by. lastModified surfaces re-analysed CVEs (recommended); published tracks only newly disclosed CVEs.",
                        "default": "lastModified"
                    },
                    "maxItems": {
                        "title": "Maximum items",
                        "minimum": 1,
                        "maximum": 50000,
                        "type": "integer",
                        "description": "Upper bound on the number of records saved to the dataset (after filtering, enrichment and deduplication).",
                        "default": 200
                    },
                    "lookbackDays": {
                        "title": "Lookback window (days)",
                        "minimum": 1,
                        "maximum": 3650,
                        "type": "integer",
                        "description": "First-run incremental window and full-mode backfill window. NVD limits a single query to a 120-day span, so longer windows are fetched as multiple sub-windows.",
                        "default": 30
                    },
                    "recheckWindowDays": {
                        "title": "Recheck window (days)",
                        "minimum": 0,
                        "maximum": 365,
                        "type": "integer",
                        "description": "Incremental overlap: re-fetch and re-compare already-committed CVEs whose cursor date is within this many days below the cursor, so upstream NVD edits are surfaced as changed records. The cursor is never regressed by these re-checks. Set 0 to disable.",
                        "default": 2
                    },
                    "maxNvdPages": {
                        "title": "Max NVD pages per run",
                        "minimum": 1,
                        "maximum": 2000,
                        "type": "integer",
                        "description": "Hard ceiling on NVD API pages fetched in one run (cost/runtime guard). When hit, the cursor is held below the uncaptured region and the remainder is picked up next run.",
                        "default": 25
                    },
                    "nvdApiKey": {
                        "title": "NVD API key (optional)",
                        "type": "string",
                        "description": "Optional NVD API key. Raises the NVD rate limit (~50 vs ~5 requests / 30 s) and is sent only as a request header — never logged. Leave empty to run unauthenticated."
                    },
                    "cveIds": {
                        "title": "CVE IDs",
                        "uniqueItems": true,
                        "type": "array",
                        "description": "Explicit CVE ID allow-list, e.g. CVE-2021-44228. When set, NVD is queried per-ID and only these CVEs are returned.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "keyword": {
                        "title": "Keyword",
                        "type": "string",
                        "description": "Free-text keyword. Pushed to NVD server-side (keywordSearch) and re-checked against description / CPE client-side."
                    },
                    "vendor": {
                        "title": "Vendor filter",
                        "type": "string",
                        "description": "Case-insensitive substring match against vendors parsed from CPE / KEV."
                    },
                    "product": {
                        "title": "Product filter",
                        "type": "string",
                        "description": "Case-insensitive substring match against products parsed from CPE / KEV."
                    },
                    "cpe": {
                        "title": "CPE filter",
                        "type": "string",
                        "description": "Case-insensitive substring match against CPE 2.3 match criteria, e.g. cpe:2.3:a:apache:log4j."
                    },
                    "cvssMin": {
                        "title": "Minimum CVSS base score",
                        "pattern": "^(10(\\.0+)?|[0-9](\\.\\d+)?)$",
                        "type": "string",
                        "description": "Keep only CVEs whose best-available CVSS base score is at least this value (0-10, decimals allowed, e.g. 7.5 or 9.8). CVEs with no CVSS score are excluded."
                    },
                    "kevOnly": {
                        "title": "KEV only",
                        "type": "boolean",
                        "description": "Keep only CVEs on the CISA Known Exploited Vulnerabilities catalog. If the KEV fetch fails while this is on, the run emits nothing and holds the cursor rather than misclassifying records.",
                        "default": false
                    },
                    "epssMin": {
                        "title": "Minimum EPSS probability",
                        "pattern": "^(0(\\.\\d+)?|1(\\.0+)?)$",
                        "type": "string",
                        "description": "Keep only CVEs whose EPSS exploit-probability is at least this value (0-1, e.g. 0.5). Requires the EPSS enrichment."
                    },
                    "epssPercentileMin": {
                        "title": "Minimum EPSS percentile",
                        "pattern": "^(0(\\.\\d+)?|1(\\.0+)?)$",
                        "type": "string",
                        "description": "Keep only CVEs whose EPSS percentile is at least this value (0-1, e.g. 0.95). Requires the EPSS enrichment."
                    },
                    "publishedFrom": {
                        "title": "Published from (inclusive)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}([T ].*)?$",
                        "type": "string",
                        "description": "ISO date, e.g. 2024-01-01. Keeps only CVEs published on/after this date. Invalid calendar dates are rejected."
                    },
                    "publishedTo": {
                        "title": "Published to (inclusive)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}([T ].*)?$",
                        "type": "string",
                        "description": "ISO date, e.g. 2024-12-31. Keeps only CVEs published on/before this date."
                    },
                    "modifiedFrom": {
                        "title": "Modified from (inclusive)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}([T ].*)?$",
                        "type": "string",
                        "description": "ISO date. Keeps only CVEs last modified on/after this date."
                    },
                    "modifiedTo": {
                        "title": "Modified to (inclusive)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}([T ].*)?$",
                        "type": "string",
                        "description": "ISO date. Keeps only CVEs last modified on/before this date."
                    },
                    "since": {
                        "title": "Incremental since (override)",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}([T ].*)?$",
                        "type": "string",
                        "description": "Optional ISO date/timestamp overriding the automatic incremental cursor for this run."
                    },
                    "includeRaw": {
                        "title": "Include raw source fields",
                        "type": "boolean",
                        "description": "Include a compact `raw` object of selected upstream values for auditing. Disable for leaner output.",
                        "default": true
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
