DEV Community

Nicholas Toledo
Nicholas Toledo

Posted on

Severity is not exploitation, and only one of them starts a 24-hour clock

Run a dependency scanner on a real project and you get four hundred CVEs. Somewhere in there might be one that matters right now. The scanner cannot tell you which, so it sorts by CVSS and hopes.

CVSS was never designed to answer "is anyone attacking this". It is a severity model — how bad would it be if exploited, given attack vector, complexity, privileges required, impact. It is a statement about the shape of a vulnerability, not about the world.

Exploitation is a statement about the world. Those are different questions, and there is now a regulation that keys on the second one and ignores the first.

The regulation that cares

The EU Cyber Resilience Act, Regulation (EU) 2024/2847. Most of it applies from December 2027, but the reporting obligations in Article 14 have been enforceable since 11 September 2026confirmed by the Commission — and they cover products already on the market.

Article 14(1) and 14(2)(a):

A manufacturer shall notify any actively exploited vulnerability contained in the product with digital elements that it becomes aware of [...] an early warning notification of an actively exploited vulnerability, without undue delay and in any event within 24 hours of the manufacturer becoming aware of it

Then 72 hours for a fuller notification, and a final report no later than 14 days after a corrective or mitigating measure is available.

The trigger is not "critical". Not "CVSS 9.8". Not "there's a PoC on GitHub". Article 3(42) defines it precisely:

'actively exploited vulnerability' means a vulnerability for which there is reliable evidence that a malicious actor has exploited it in a system without permission of the system owner

Two properties of that definition are worth pausing on. It requires evidence, not plausibility. And the exploitation has to have actually happened. A vulnerability can be trivially exploitable, remotely, unauthenticated, with a public exploit — and if nobody has used it, it is not an actively exploited vulnerability under Article 3(42).

So: severity is a property of the vulnerability. Exploitation is a fact about the world. Only the second one starts the clock.

(I am an engineer reading a primary source, not a lawyer. Whether Article 14 applies to you, and when, is a legal question. Nothing here is legal advice.)

The closest public proxy: CISA KEV

Which raises the obvious engineering problem: how would you know?

The best public answer is the CISA Known Exploited Vulnerabilities catalogue. CISA maintains it under Binding Operational Directive 22-01, and the inclusion criteria are three things:

  1. The vulnerability has an assigned CVE ID.
  2. There is reliable evidence it has been actively exploited in the wild.
  3. There is a clear remediation action, such as a vendor-provided update.

Criterion 2 is the interesting one, and CISA's own wording is close to the CRA's: "reliable evidence that execution of malicious code was performed by an actor on a system without permission of the system owner". CISA's version also includes attempted exploitation.

It is a JSON file at a stable public URL. No key, no account, no rate limit worth worrying about.

curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  | jq '{title, catalogVersion, dateReleased, count}'
Enter fullscreen mode Exit fullscreen mode
{
  "title": "CISA Catalog of Known Exploited Vulnerabilities",
  "catalogVersion": "2026.09.14",
  "dateReleased": "2026-09-14T19:00:02.426Z",
  "count": 1710
}
Enter fullscreen mode Exit fullscreen mode

1710 entries. For scale, the CVE programme published 48,185 records in 2025 alone. KEV is roughly four digits in total, across its entire history. That gap is the whole point: it is not a smaller feed of the same thing, it is a different question being asked.

One entry:

curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  | jq '.vulnerabilities[] | select(.cveID=="CVE-2021-44228")'
Enter fullscreen mode Exit fullscreen mode
{
  "cveID": "CVE-2021-44228",
  "vendorProject": "Apache",
  "product": "Log4j2",
  "vulnerabilityName": "Apache Log4j2 Remote Code Execution Vulnerability",
  "dateAdded": "2021-12-10",
  "shortDescription": "Apache Log4j2 contains a vulnerability where JNDI features do not protect against attacker-controlled JNDI-related endpoints, allowing for remote code execution.",
  "requiredAction": "For all affected software assets for which updates exist, the only acceptable remediation actions are: 1) Apply updates; OR 2) remove affected assets from agency networks. ...",
  "dueDate": "2021-12-24",
  "knownRansomwareCampaignUse": "Known",
  "forensicTriage": "No",
  "notes": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228",
  "cwes": ["CWE-20", "CWE-400", "CWE-502"]
}
Enter fullscreen mode Exit fullscreen mode

Useful fields: dateAdded (when CISA saw evidence), knownRansomwareCampaignUse (Known or Unknown), requiredAction, cwes. 360 of the current 1710 entries are flagged as known ransomware campaign use:

curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  | jq '[.vulnerabilities[] | select(.knownRansomwareCampaignUse=="Known")] | length'
# 360
Enter fullscreen mode Exit fullscreen mode

Flatten it to a set for intersection:

curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  | jq -r '.vulnerabilities[].cveID' | sort > kev-ids.txt
Enter fullscreen mode Exit fullscreen mode

Now you have a sorted file of every CVE with public evidence of exploitation. comm -12 it against your own CVE list and you are done. That is the entire idea.

Be honest about what KEV is not

This part matters more than the tooling.

  • It is a US federal catalogue. It exists to drive remediation in US civilian executive branch agencies under BOD 22-01. It is not an EU instrument and has no legal standing under the CRA.
  • Using it as a proxy for "actively exploited" is a judgement call, not a legal definition. The CRA's definition and CISA's criteria are close in substance. Close is not the same as identical, and no regulator has said KEV is the test.
  • Absence is not evidence of absence. Plenty of exploitation never makes it into KEV. A clean result means "nothing you ship appears on this particular list today", not "nobody is attacking you".
  • It is vendor/product-centric, not package-centric. Entries name "Apache Log4j2", not org.apache.logging.log4j:log4j-core. Mapping one to the other is the actual engineering work, and it is where a naive implementation quietly fails.
  • Article 14 asks about exploitation in your product. KEV tells you a vulnerability is exploited somewhere in the world. If the vulnerable code path is not reachable in your build, you may be outside the trigger. That is a human decision and nobody can automate it for you.

KEV is the best available public signal. It is a signal, not an oracle.

A worked example: log4j-core 2.14.1

Take the canonical case. A gradle.lockfile containing:

org.apache.logging.log4j:log4j-core:2.14.1=runtimeClasspath
org.apache.logging.log4j:log4j-api:2.14.1=runtimeClasspath
com.google.guava:guava:31.1-jre=runtimeClasspath
Enter fullscreen mode Exit fullscreen mode

Ask OSV.dev what is known about that exact coordinate and version:

curl -s -X POST https://api.osv.dev/v1/query \
  -H 'Content-Type: application/json' \
  -d '{"package":{"name":"org.apache.logging.log4j:log4j-core","ecosystem":"Maven"},
       "version":"2.14.1"}' \
  | jq -r '.vulns[].id'
Enter fullscreen mode Exit fullscreen mode

Seven advisories come back. Resolved to CVE aliases and checked against KEV:

CVE                GHSA sev  KEV?        CVSS vector
CVE-2021-44228     CRITICAL  KEV         CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:H
CVE-2021-44832     MODERATE  not on KEV  CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CVE-2021-45046     CRITICAL  KEV         CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H/E:H
CVE-2021-45105     HIGH      not on KEV  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H
CVE-2025-68161     MODERATE  not on KEV  CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/...
CVE-2026-34477     MODERATE  not on KEV  CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/...
CVE-2026-34480     MODERATE  not on KEV  CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/...
Enter fullscreen mode Exit fullscreen mode

Seven CVEs on one dependency. Two on KEV.

Look at CVE-2021-45105 — rated HIGH, AV:N/AC:L/PR:N/UI:N, network, low complexity, no privileges, no interaction. It sorts near the top of any severity-ranked list. It is a denial of service via recursive lookups, and it is not on KEV.

Meanwhile CVE-2021-45046 has AC:H — high attack complexity, which reduces its CVSS base score. It is on KEV. Attack complexity is a statement about difficulty. Difficulty did not stop anyone.

That inversion is the argument in one table. Severity ranking would have you look at 45105 before 45046. Exploitation ranking puts 45105 in the backlog, where it belongs, and 45046 in front of a human today.

Note the two KEV vectors end in /E:H — the temporal Exploit Code Maturity metric set to High. CVSS can express this, via temporal metrics. Almost nobody publishes them, and scanners almost universally sort on base score. The capability exists and is unused, which is why you go to a separate catalogue.

Automating the intersection

The pipeline is: manifests → resolved components → OSV.dev → CVE aliases → intersect KEV. Four steps, all public APIs, no account.

I wrote it up as cra-watch so I would stop rebuilding it per project. Single Python file, standard library only, MIT.

curl -fsSL https://raw.githubusercontent.com/ntoledo319/cra-watch/main/cra_watch.py -o cra-watch
chmod +x cra-watch
./cra-watch scan
Enter fullscreen mode Exit fullscreen mode

On the lockfile above:

cra-watch v1.0.0
EU Cyber Resilience Act, Article 14 - actively-exploited screen

  Scope    /tmp/demojava
           gradle.lockfile (3)
  Unique   3 components

  Advisories  3 component(s) carry at least one known advisory
  KEV feed    1709 entries, catalogue 2026.09.11

  --------------------------------------------------------------------

   2 KEV MATCH(ES) - MANUAL DECISION REQUIRED NOW

  CVE-2021-45046  (GHSA-7rjr-3q55-vv33)
    component      org.apache.logging.log4j:log4j-core 2.14.1  [Maven]
    known as       Apache Log4j2 Deserialization of Untrusted Data Vulnerability
    KEV listed     2023-05-01   ransomware use: Known

  CVE-2021-44228  (GHSA-jfh8-c2jp-5v3q)
    component      org.apache.logging.log4j:log4j-core 2.14.1  [Maven]
    known as       Apache Log4j2 Remote Code Execution Vulnerability
    KEV listed     2021-12-10   ransomware use: Known
Enter fullscreen mode Exit fullscreen mode

It reads 14 lockfile formats plus CycloneDX and SPDX SBOMs, and exits 0 for no KEV matches, 1 for at least one, 2 when it found no manifests at all. The "manual decision required" wording is deliberate — the tool's output is the start of a human judgement, not a verdict.

(That run says 1709 entries, catalogue 2026.09.11, while the curl earlier in this post returned 1710 and 2026.09.14. The tool caches the catalogue on disk for six hours so repeat runs stay fast and do not hammer a free public API. Printing the catalogue version is how you notice you are reading a cached answer; --refresh forces a re-download.)

Run it on a schedule, not just on push. Your dependency graph did not change last night; the KEV catalogue did. A repo that was clean yesterday can be a reporting question this morning without a single commit. That asymmetry is the strongest argument for a cron job over a pre-commit hook.

What to take away, even if you never touch either tool

Your scanner's ranking encodes an assumption: that severity predicts urgency. Sometimes it does. For the specific question "is someone attacking this right now", it does not, and there is a free public dataset that answers the real question directly.

Pull known_exploited_vulnerabilities.json, jq -r '.vulnerabilities[].cveID', and intersect it with whatever CVE list you already produce. Twenty minutes. The output is short enough that a human will actually read it — which is the only property that has ever made a security list useful.


Tool: cra-watch. Engineering tooling, not legal advice, and not a compliance determination.

Top comments (0)