Convert CISA KEV, ENISA CNW EUVD, KEVIntel, and Shadowserver Foundation honeypot feeds into GCVE-BCP-07 Known Exploited Vulnerability (KEV) assertions.
The goal is to turn list-based KEV feeds into attributable, structured exploitation assertions suitable for ingestion into GCVE-compatible systems, vulnerability databases, or analytical pipelines — such as a Vulnerability-Lookup instance.
For each source, the tool downloads the feed and converts every entry into a GCVE-BCP-07 assertion, preserving the source's semantics while adding:
- Explicit attribution (
evidence.source) - Separated timestamps (
first_seen_at,asserted_at,last_seen_at,recorded_at) - A conservative
evidence.confidenceper source (to avoid overstating claims)
Assertions are written as NDJSON (default, one per line) or a single JSON array, and can be pushed to a Vulnerability-Lookup instance. Previously converted assertions can also be rendered as Atom/RSS feeds.
The tool preserves published KEV information and does not invent or infer extra
metadata (geography, sector, etc.). When no CVE is available, provider identifiers
(e.g. EUVD) are kept as references. recorded_at is the local ingestion time added
by the collector.
| Command | Source | Feed | Confidence |
|---|---|---|---|
gcve-from-cisa |
CISA KEV Catalog | JSON | 0.8 |
gcve-from-enisa |
ENISA CNW EUVD KEV | JSON | 0.75 |
gcve-from-kevintel |
KEVIntel | Paginated JSON API | 0.7 |
gcve-from-shadowserver |
Shadowserver Foundation | HMAC-signed honeypot/exploited-vulnerabilities |
0.7 |
Each command is also runnable as a module (python -m gcve_eu_kev.cisa --help, etc.).
git clone https://github.com/gcve-eu/gcve-eu-kev
cd gcve-eu-kev
poetry installPython >= 3.12 is required. The only runtime dependency is requests.
Write assertions as NDJSON (default) or a JSON array:
gcve-from-cisa -o - # NDJSON to stdout
gcve-from-enisa -o kev_assertions.ndjson # NDJSON to a file
gcve-from-cisa --json-array -o kev.json # single JSON arrayConvert existing assertions into an Atom or RSS feed:
gcve-from-cisa -o kev.ndjson
gcve-kev-to-feed --input kev.ndjson --format atom -o kev.atom.xml
gcve-kev-to-feed --input kev.ndjson --format rss -o kev.rss.xmlPush assertions to a Vulnerability-Lookup instance (requires API key and origin UUID):
gcve-from-cisa --push
gcve-from-cisa --push --since-date 2026-02-02
gcve-from-enisa --push --since-date '10 days'--since-date accepts ISO dates, relative expressions ('1 day', '2 weeks'), and
yesterday.
Fetches every page of the paginated kevintel.com API. Filter on the
not_yet_in_cisa_kev key with --not-yet-in-cisa-kev {all,true,false}:
gcve-from-kevintel -o kevintel.ndjson # all entries
gcve-from-kevintel --not-yet-in-cisa-kev true -o kevintel_new.ndjson # not yet in CISA KEV
gcve-from-kevintel --push --not-yet-in-cisa-kev true --since-date '1 week'Queries the HMAC-signed Shadowserver Reports API. Provide credentials via the
environment (SHADOWSERVER_API_KEY / SHADOWSERVER_API_SECRET / optional
SHADOWSERVER_API_URI), via conf.py, or via the official ~/.shadowserver.api
INI file (an [api] section with key / secret / uri).
Each record carries a first_seen field (when Shadowserver first observed
exploitation), so timestamps are read straight from the feed — no inference or
persistent state. first_seen becomes first_seen_at and asserted_at (a
telemetry source asserts exploitation as of its first observation); the queried day
becomes last_seen_at and status.status_updated_at.
# A single day (defaults to yesterday, since today is usually incomplete)
gcve-from-shadowserver --date 2026-06-24
# Backfill a range (one API call per day), optionally capped with --to-date
gcve-from-shadowserver --since-date '7 days' --json-array -o shadowserver.json
gcve-from-shadowserver --since-date 2026-01-01 --to-date 2026-02-01 --json-array -o jan.json
# Filter on iot / cisa_kev / geo, then push
gcve-from-shadowserver --iot yes --cisa-kev no --geo DE,FR --pushThe output file is the full per-day time series (one assertion per vulnerability per
day). On --push, those collapse to one merged assertion per vulnerability (the
latest day's record), because the KEV API keys a record on (origin_uuid, vulnId).
Unlike the other converters, Shadowserver --push upserts: it POSTs to create
each entry and, on HTTP 409 (an entry already exists for that (origin_uuid, vulnId)), falls back to PUT /kev/origin/{origin_uuid}/{vuln_id} to update it. So a
re-run refreshes last_seen_at on the existing record instead of being rejected as a
duplicate.
By default the Vulnerability-Lookup API URL and key are read from
gcve_eu_kev/conf.py (gitignored; falls back to public defaults if missing). To push
to a different instance without editing that file, set environment variables:
VULNERABILITY_LOOKUP_API_URL=https://other-instance.example.com/api/kev \
VULNERABILITY_LOOKUP_API_KEY=mykey \
gcve-from-cisa --push- GCVE BCP-07: https://gcve.eu/bcp/gcve-bcp-07/
- CISA KEV: https://www.cisa.gov/known-exploited-vulnerabilities-catalog
- ENISA CNW KEV JSON: https://raw.githubusercontent.com/enisaeu/CNW/refs/heads/main/advisories/eukev/eukev.json
- KEVIntel: https://kevintel.com
- Shadowserver Foundation: https://www.shadowserver.org
Please open issues or PRs for improvements, corner cases, or fixes.
gcve-eu-kev is licensed under the GNU General Public License version 3.
Copyright (c) 2026 Computer Incident Response Center Luxembourg (CIRCL)
Copyright (c) 2026 Alexandre Dulaunoy - https://github.com/adulau
Copyright (C) 2026 Cédric Bonhomme - https://github.com/cedricbonhomme