Change-detection over the CISA Known Exploited Vulnerabilities (KEV) catalog. Diffs the feed run-to-run to emit only newly-added CVEs the moment CISA publishes them, normalized (dateAdded, dueDate, ransomware flag, CWEs) with vendor/product/since filters. Keyless, CC0. Not endorsed by CISA/DHS.
docs/PRICING.md — PPE pricing proposal (DRAFT, owner-gated): single-event kev-returned model
(charged once per newly-added CVE; _baseline/_noData/_error never charged), rejected
alternatives (per-run / per-MB / split-event / per-CU), a CC0-ceiling / cost-floor price band with
the BUILD_QUEUE.md §8 working target ≈ $0.001/record, and the owner-gated launch checklist.
Documented-only — no live $/event set (no-publish gate).
README polish: status note flipped scaffold → complete (all layers + tests + COMPLIANCE + PRICING
exist); removed the stale "later slice" pointers for COMPLIANCE.md / docs/PRICING.md.
Actor #8 complete — all six slices done, 51 tests green offline, built-never-published.
0.1 — 2026-07-10 — slice 5: COMPLIANCE.md
COMPLIANCE.md mirroring the shipped oig-leie-monitor structure, adapted for KEV: license =
CC0 1.0 Universal (public-domain-equivalent, no attribution) with the one operative condition —
no use of the CISA Logo or DHS Seal / no implied CISA/DHS endorsement (carried verbatim in the
per-record DISCLAIMER); robots.txt PASSES (/sites/default/files/feeds/ not disallowed); keyless
single-file access; cveID stable PK / add-only delta (no removal-reinstatement stream); §5 has
no PII machinery — KEV is a plain public catalog, so the LEIE DOB/address handling is dropped for a
short "public catalog, source-only, no enrichment" note. Evidence pulled from BUILD_QUEUE.md §8
(gate cleared pre-build); no live fetch (OFFLINE gate).
0.1 — 2026-07-10 — slice 4: offline integration test suite
test/integration.test.js (+5 tests) drives the exact pure main.js pipeline (fetch → assert →
normalize → diff → changeType → filters → maxResults) through an injected fake fetchImpl over tiny
in-memory envelope fixtures — the cross-module WIRING the per-layer suites don't reach, NO network.
Covers: fetchKevJson end-to-end 2-run delta (run-2 = exactly the one new cveID); seen-set
round-trip (run-2 buildSeenSet = UNION of prior + newcomer, 3rd no-change run diffs empty); a
drifted envelope aborting at assertEnvelope before normalize; parseKevDate↔filterByAddedSince
(malformed dateAdded → null → dropped by an active lower bound); the full filter chain narrowing a
2-newcomer delta to one survivor.
diff.js — buildSeenSet (compact seen-cveID array for the KV store) + diffSeen ({ added },
accepts a Set-or-array prior; no removal/reinstatement — KEV only adds on a stable PK).
Implemented the fetch/parse/normalize layer against the slice-1 contract (consts unchanged):
isRetryableStatus (5xx/429 retryable; other 4xx/2xx not) + backoffDelayMs (deterministic
1-based exponential, base 500 ms, cap 8000 ms) — identical policy to oig-leie-monitor/src/leie.js.
parseKevDate — validates the feed's already-ISO YYYY-MM-DDdateAdded/dueDate (calendar
range checked) and returns it verbatim; blank/malformed/out-of-range → null (never throws, so one
bad cell can't sink the diff; dueDate is optional on some rows).
assertEnvelope — asserts every ENVELOPE_KEYS key present + vulnerabilities is an array;
throws KEV envelope drift: … on drift (surfaced later as an _error control record).
fetchKevJson({ fetchImpl, sleep, url, … }) — GETs KEV_URL with the courteous UA + optional
proxy dispatcher, retries 5xx/429/transport errors with backoff, JSON.parses the body, returns
the envelope object. A malformed 200 body is a non-retryable parse error (err.nonRetryable).
Injectable fetchImpl/sleep — the ~1.5 MB live feed is NEVER fetched in tests (OFFLINE gate).
normalizeVulnerabilities(vulns, catalogVersion) — folds each raw row into the flat kev shape
(cveID, vendorProject, product, vulnerabilityName, dateAdded, shortDescription,
requiredAction, dueDate, knownRansomwareCampaignUse, notes, cwes[], catalogVersion);
blank → null, cwes defaults to [] (non-array coerced), source fields only (no enrichment),
dedupe by cveID (first wins), rows without a cveID / non-object rows skipped.
test/kev.test.js (node:test, no deps, no network): 20 tests — retry/backoff predicates, date
validation incl. malformed → null, assertEnvelope accept + every drift path, fetchKevJson via
injected fetch (success + courteous UA, 503-then-success retry schedule, non-retryable 404, transport
give-up, malformed-200 parse error), normalizeVulnerabilities end-to-end over a small 3-vuln JSON
fixture (canonical shape, dedupe, skip, empty input, null catalogVersion).
Scaffolded from the shipped oig-leie-monitor reference implementation (structure, PPE model,
full-file client-side diff pattern). Actor #8 of the offline BUILD_QUEUE.md (§8 CISA KEV).
Same family as #7 — a full-file client-side diff, not a query-API paginator — but over JSON,
not CSV, keyed on a stable globally-unique cveID, and with no PII (a plain public
vulnerability catalog), so none of #7's DOB/address snapshot-drop machinery is needed.
Fetch-based Apify Actor (no browser): apify@^3.2.6 only, apify/actor-node:20 base, 256–512
MB (sized modestly — the KEV feed is ~1.5 MB, far smaller than the LEIE file).
.actor/: actor.json (name cisa-kev-monitor, title "CISA KEV Newly-Added Vulnerability
Monitor", intended kev-returned PPE), Dockerfile, and input_schema.json defining the product
input surface — vendorProjects, products, addedSince, knownRansomwareOnly, monitorNewOnly,
maxResults, proxyConfiguration.
src/ stubs establishing the module layout (each function throws "not implemented (slice N)"):
kev.js (fetch/parse/normalize layer — impl slice 2): consts populated now (KEV_URL
= cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json, USER_AGENT,
DISCLAIMER courtesy CISA/DHS no-endorsement string, ENVELOPE_KEYS the
{title, catalogVersion, dateReleased, count, vulnerabilities} envelope, VULN_FIELDS the
per-row source fields); stub fns isRetryableStatus, backoffDelayMs, parseKevDate,
assertEnvelope, fetchKevJson (injectable fetchImpl — the live feed is NEVER fetched in
tests), normalizeVulnerabilities.
diff.js (seen-set diff core — impl slice 3): buildSeenSet (compact seen-cveID array for the
KV store), diffSeen (added = cveIDs this∖prior). No reinstatement/removal logic — KEV only
adds entries.
The delta is the moat: everyone republishes the whole KEV catalog; nobody sells the change feed.
The Actor emits first-seen CVEs — cveIDs absent from the prior seen set — the moment CISA adds
them, normalized (dateAdded, dueDate, knownRansomwareCampaignUse, cwes[]) and
vendor/product-filterable. The seen set is written only after a clean untruncated pass.
License: the KEV catalog is distributed under CC0 1.0 Universal (public-domain-equivalent;
commercial reuse explicit). No key or attribution required. A courtesy DISCLAIMER (no CISA/DHS
endorsement; no CISA Logo / DHS Seal use) is carried on every record. robots.txt
(cisa.gov/robots.txt) does NOT disallow the /sites/default/files/feeds/ feed path. (Full
compliance record in COMPLIANCE.md, slice 5.)
No PII: the KEV catalog is a plain public vulnerability catalog keyed on cveID — no personal
data. The Actor relays only source fields; no enrichment.
README.md documents the intended product surface + output kev shape + the courtesy disclaimer;
CHANGELOG.md (this file).
Verified offline: all src/*.jsnode --check clean; .actor/*.json + package.json valid
JSON (no network).