Skip to content

CVE panel 2.0: CISA KEV, EPSS and exposure context - #333

Open
Eales wants to merge 5 commits into
lissy93:masterfrom
Eales:feat/cve-panel-kev-epss
Open

CVE panel 2.0: CISA KEV, EPSS and exposure context#333
Eales wants to merge 5 commits into
lissy93:masterfrom
Eales:feat/cve-panel-kev-epss

Conversation

@Eales

@Eales Eales commented Aug 9, 2026

Copy link
Copy Markdown

What this does

The vulnerabilities panel currently lists whatever CVEs Shodan associates with the host, each linked to its NVD record. That tells you a vulnerability exists, but not whether anyone is actually exploiting it β€” so a decorative CVSS 9.8 with no real-world activity looks exactly as alarming as something under active attack.

This PR enriches each CVE with two free, key-less feeds and reworks the card around the result:

  • CISA KEV β€” is the vulnerability confirmed to be exploited in the wild, when was it added, what is the federal remediation due date, and is it linked to known ransomware campaigns.
  • FIRST EPSS β€” the daily-updated probability that the CVE will be exploited in the next 30 days, plus its percentile against every other CVE.
  • Exposure context β€” which exposed service (port, product, version) actually reported the CVE, pulled from the per-banner vulns that Shodan already returns and that we were previously discarding.
  • Patch priority β€” a single verdict per CVE, derived in the order CISA itself recommends.

Priority rules

CISA's guidance is to work the KEV catalog first and use EPSS to rank what's left; CVSS says how bad exploitation would be, not how likely it is, so it never outranks evidence of actual exploitation.

Verdict Condition
πŸ”΄ Patch now Listed in CISA KEV (ransomware use called out separately)
🟠 Patch soon EPSS β‰₯ 10%
🟑 Schedule EPSS β‰₯ 1%, or CVSS β‰₯ 9.0 with no exploitation signal
πŸ”΅ Monitor Everything else

The effect is to decouple the verdict from raw severity in both directions: a CVSS 5.3 at 95% EPSS is escalated to Patch soon, while a CVSS 9.8 with a negligible EPSS falls back to Schedule (covered by the computePriority de-escalates a high CVSS with negligible exploit probability test).

Example

Against a fixture of four real CVEs, resolved through the live feeds (KEV catalog 2026.08.07, EPSS scored 2026-08-09):

CVE-2021-44228   cvss=10    kev=YES (ransomware)  epss=100.0% / p100.0  svc=HTTPS:443  => PATCH NOW
CVE-2019-11043   cvss=9.8   kev=YES (ransomware)  epss=99.4%  / p99.9   svc=β€”          => PATCH NOW
CVE-2016-2183    cvss=5.3   kev=no                epss=95.7%  / p99.9   svc=SSH:22     => PATCH SOON
CVE-2021-23017   cvss=9.4   kev=no                epss=53.5%  / p98.9   svc=HTTPS:443  => PATCH SOON

Note the third row: SWEET32 scores a middling 5.3 on CVSS, but sits in the 99.9th EPSS percentile, so it outranks the nginx heap-write below it.

Implementation notes

  • All the enrichment lives in api/_common/cve-intel.js, called from api/shodan.js β€” no new API route, no extra Shodan quota, and no new dependencies.
  • Both feeds are best-effort. buildCveIntel uses Promise.allSettled, and shodan.js falls back to the raw host result if enrichment throws, so a CISA or FIRST outage can never fail the check. The card says which feed was unavailable rather than showing a misleading "Not listed".
  • The KEV catalog (~2 MB) is fetched once and cached in-process for 6 hours; it's published at most a few times a day. Cold call measured at ~470 ms, warm at ~40 ms. A host with no CVEs makes no network calls at all.
  • EPSS queries are batched at 100 CVEs, which is api.first.org's per-query cap.
  • The client keeps working against an older API build that returns plain CVE ids β€” asCveIntel reconstructs the shape and marks the entries unranked.

Tests

This adds the first unit tests to the repo, using the built-in Node test runner β€” no new dependencies, no config file. 31 tests over the parsing, merging, sorting and triage rules, including the malformed-input and feed-unavailable paths.

yarn test   # node --test
β„Ή tests 31
β„Ή pass 31
β„Ή fail 0

I've wired a πŸ§ͺ Unit Tests job into .github/workflows/ci.yml alongside the existing lint/typecheck jobs, and mentioned yarn test in the developer setup section of the README. Happy to drop either if you'd rather keep the CI surface as it is.

Verification

  • yarn test β€” 31/31 pass
  • yarn lint β€” clean
  • yarn typecheck (astro check) β€” 0 errors, 0 warnings, 0 hints
  • yarn build β€” completes
  • prettier --check on every touched file β€” clean
  • Live smoke test against both feeds (KEV catalog 2026.08.07, EPSS 2026-08-09), plus a rendered screenshot of the card against enriched fixture data

Notes / open questions

  • "Detected by" is currently always Shodan, since that's the only host-level CVE source Web Check has. I've modelled it as an array on each entry so a second scanner (Censys, for example) can be folded in later without another shape change, but I didn't want to add a credentialed integration inside this PR.
  • Priority thresholds (10% / 1% / CVSS 9.0) are constants at the top of cve-intel.js β€” easy to tune if you'd prefer a different cut.

Eales added 2 commits August 9, 2026 22:43
Shodan gives us bare CVE ids, which says nothing about whether a
vulnerability is actually being exploited. This adds an enrichment step
against two free, key-less feeds:

- CISA KEV, for vulnerabilities confirmed exploited in the wild
- FIRST EPSS, for the probability of exploitation in the next 30 days

It also maps each CVE back to the exposed service that reported it, and
derives a patch priority from the KEV-first ordering CISA recommends.
Both feeds are best-effort: if either is unreachable the host result is
returned unchanged, just with less context.

Adds unit tests covering the parsing, merging and triage rules, run with
`yarn test` via the built-in Node test runner (no new dependencies).
The panel listed CVEs as flat links to NVD, leaving the reader to work
out which ones matter. Each entry now shows its CVSS score, whether CISA
lists it as actively exploited, its EPSS probability and percentile, the
exposed service it was found on, and a resulting patch priority, sorted
worst-first. A summary line reports how many CVEs are in the KEV catalog
and the highest EPSS score on the host.

Older API instances that return plain CVE ids still render, and the card
says so when either feed was unavailable rather than showing a
misleading "not listed".
@netlify

netlify Bot commented Aug 9, 2026

Copy link
Copy Markdown

βœ… Deploy Preview for web-check ready!

Built without sensitive environment variables

Name Link
πŸ”¨ Latest commit d8d92e3
πŸ” Latest deploy log https://app.netlify.com/projects/web-check/deploys/6a78eb482095ab0008f72fe8
😎 Deploy Preview https://deploy-preview-333--web-check.netlify.app
πŸ“± Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
πŸ€– Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Eales added 3 commits August 9, 2026 22:59
The chip was built as module + version + port, which produced "HTTPS 2
:443" and "SSH 9.8 :22" on a real host: the version Shodan reports
belongs to the product behind the service (Apache 2, OpenSSH 9.8), not
to the protocol. The chip now reads "HTTPS :443", and the full banner
plus transport moves to the hover title.
Changing the shape of parseShodanResults().vulns broke the server-info
analyzer, which gated on Array.isArray(d.vulns). It silently returned
nothing, so a host with CVEs β€” including one in the KEV catalog β€” showed
no CVE entry in the Advisory section at all.

The analyzer now reads the enrichment through asCveIntel and reports each
CVE at the severity its evidence supports: KEV-listed as critical, high
EPSS as an issue, and the long tail as warnings and info, instead of
filing 25 mostly-dormant CVEs as equally critical. Nothing is dropped β€”
every CVE is still listed under one of the four headings.

When there is no evidence to rank against, because the API is an older
build or CISA and FIRST are both unreachable, it falls back to the
original single critical finding rather than quietly downgrading
everything to info.
The feed timeout was 20s, but the sample config suggests running the API
with PUBLIC_API_TIMEOUT_LIMIT=25000. A hanging CISA or FIRST request
could therefore exhaust the budget for the whole Shodan check and take
the host name and server info cards down with it, neither of which has
anything to do with CVE enrichment. The timeout drops to 6s, comfortably
above the ~500ms a cold lookup actually costs.

A failed catalog fetch is also cached now, for a shorter interval than a
successful one, so an outage costs one timeout every five minutes rather
than one on every single request.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant