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 2026 — confirmed 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:
- The vulnerability has an assigned CVE ID.
- There is reliable evidence it has been actively exploited in the wild.
- 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}'
{
"title": "CISA Catalog of Known Exploited Vulnerabilities",
"catalogVersion": "2026.09.14",
"dateReleased": "2026-09-14T19:00:02.426Z",
"count": 1710
}
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")'
{
"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"]
}
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
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
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
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'
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/...
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
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
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)