Filter-correctness fix (no output-schema change; schemaVersion stays 1.0.0). 142 tests pass; verified with lint, typecheck, build, full test suite, and the fixture smoke:local run.
- Bare-date
publishedTo / modifiedTo upper bounds are inclusive through the end of the boundary day again. These filters are documented as inclusive calendar-day upper bounds, but input normalization canonicalized a bare YYYY-MM-DD to midnight UTC, so a record timestamped later that same day was silently dropped. A bare upper bound now anchors to end-of-day (T23:59:59.999Z); an explicit ISO timestamp upper bound stays exact. Added filter-level and run/input-normalization regression tests covering a record later on the boundary day (retained) and one just after it (dropped).
Release-blocker hardening from an adversarial review (no output-schema change; schemaVersion stays 1.0.0). 94 tests pass; verified with fixture and live NVD + CISA KEV + FIRST EPSS smoke runs.
- Enrichment-filtered backlogs never stall at the 3,000-record enrichment budget. The cursor now advances on a scan/progress watermark (every candidate in the fully-enriched prefix is definitively classified) rather than only on emitted matches, so a
kevOnly/epssMin backlog whose first 3,000 records contain no matches still makes forward progress and later runs reach matches beyond the budget. The cursor is still held below any record that was not fully processed (page-budget/mid-window NVD truncation, or a filter-required enrichment outage).
- EPSS completeness is proven, not assumed. The effective batch size is hard-capped at 100 regardless of any injected value, an explicit
limit is sent, and each response is validated against the live FIRST contract (data array present, total ≤ returned rows, server limit ≥ batch size, offset == 0, no unrequested CVEs). A truncated/malformed batch fails the whole enrichment closed; a requested-but-absent CVE with total == returned is treated as genuinely unscored.
- Malformed "successful" source payloads can no longer become false-negative intelligence. NVD (
{} / missing vulnerabilities / inconsistent totalResults), KEV ({} / empty catalog / count mismatch), and EPSS (missing data) responses are rejected instead of being read as a drained empty window / "nothing exploited" / "no scores". A legitimately empty NVD window (totalResults: 0) is still accepted.
- Optional KEV/EPSS outages preserve the last known-good enrichment. Change-tracking carries prior KEV/EPSS/risk snapshot values forward during an optional outage, so an outage (and its recovery) no longer fabricates
changedFields or overwrites good state with failure placeholders; only genuine post-recovery changes surface. Documented the bounded recheck/full-run path for CVEs first seen during an outage.
- NVD pagination advances by the rows actually returned, not the requested page size, so a server-side page cap or a short non-final page can never leave a gap; an empty page below
totalResults stops safely (no infinite loop) and holds the cursor.
- P2/P3: recursive NVD child-node CPE extraction with vendor/product derived from the same capped CPE list (evidence consistency); invalid numeric inputs are rejected rather than silently clamped; README corrected on risk weighting (CVSS 60 / EPSS-probability 25 / KEV 12–15; EPSS percentile drives the bucket, not the score), byte-identical/idempotency claims (run-stamped fields + append-only dataset),
maxConcurrency = 1 operational contract, and concrete pricing/cost estimates + explicit FIRST EPSS attribution text.
Correctness and robustness fixes (no output-schema change; schemaVersion stays 1.0.0).
- EPSS batch truncation fixed. The FIRST EPSS API caps responses at 100 rows unless an explicit
limit is sent; the previous batch size of 120 silently dropped scores past the first 100 per batch, which could make genuinely scored CVEs look unscored and corrupt an epssMin / epssPercentileMin filter. Batches are now ≤100 IDs and send limit=<batch length>, so every requested score is returned (verified live enriching 3000 IDs across 30 batches).
- Page-budget cursor skip fixed. NVD does not return date-window pages sorted by the cursor field, so advancing the keyset cursor after a page-budget (or mid-window-failure) truncation could strand a lower-dated CVE on an un-fetched page below the cursor and skip it permanently.
fetchNvd now reports completedThrough (the end of the last fully-drained window) and the cursor is clamped to it; a cveIds lookup partial failure now holds the cursor entirely. Truncated records are still emitted and simply suppressed as unchanged once the cursor catches up.
cvssMin now accepts decimals (e.g. 7.5, 9.8) instead of integers only, matching how CVSS base scores are expressed.
- Added adversarial regression tests: same-timestamp keyset drain, page-budget cursor clamp, unchanged-suppression + upstream edit via recheck, summary/OUTPUT write-failure ordering, run-level config isolation,
cveIds partial-failure hold, epssPercentileMin block, partial NVD page failure, and EPSS batch/limit behavior. 72 tests pass.
Initial public release.
- Joins official NVD CVE API 2.0, CISA KEV catalog, and FIRST EPSS into one deduplicated, risk-prioritized CVE record (
recordId == cveId).
- Deterministic incremental cursor per (source × query configuration): advances only after successful dataset writes, never past failed/truncated/filtered regions.
recheckWindowDays re-surfaces upstream edits to already-committed CVEs without regressing the cursor.
- Documented order-independent dedup with explicit total-order tie-break.
risk.priority = critical / high / medium / low / unknown synthesised from CVSS + EPSS + KEV (KEV dominates; ransomware bumps to critical; documented heuristic, not an official standard).
- Per-source and per-sub-dataset failure isolation: NVD hard-fail, KEV outage, EPSS outage all hold the cursor and surface as
failed / truncated in the run summary.
- HTTP layer: retry/backoff with both delta-seconds and HTTP-date
Retry-After, clamped to a sane ceiling; rate-limit spacing for NVD (~6 s unkeyed, ~0.6 s keyed).
- 60 unit + adversarial + smoke tests pass; offline
smoke:local exercises the full pipeline against fixtures.
- Multi-stage
npm ci Docker build for byte-identical dependency trees.
- Attribution and source terms linked in the README; data is © the respective authorities.