From e4a600fef29801cbed64789873ccde76191d4453 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 23 May 2026 10:38:01 -0400 Subject: [PATCH] module metadata: CWE + ATT&CK + CISA KEV triage from federal sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds per-CVE triage annotations that turn SKELETONKEY's JSON output into something a SIEM/CTI/threat-intel pipeline can route on, and a KEV badge in --list so operators see at-a-glance which modules cover actively-exploited bugs. New tool — tools/refresh-cve-metadata.py: - Discovers CVEs by scanning modules// (no hardcoded list). - Fetches CISA's Known Exploited Vulnerabilities catalog (https://www.cisa.gov/.../known_exploited_vulnerabilities.csv). - Fetches CWE classifications from NVD's CVE API 2.0 (services.nvd.nist.gov), throttled to the anonymous 5-req/30s limit (~3 minutes for 26 CVEs). - Hand-curated ATT&CK technique mapping (T1068 default; T1611 for container escapes, T1082 for kernel info leaks — MITRE doesn't publish a clean CVE→technique feed). - Generates three outputs: docs/CVE_METADATA.json machine-readable, drift-checkable docs/KEV_CROSSREF.md human-readable table core/cve_metadata.c auto-generated lookup table - --check mode diffs the committed JSON against a fresh fetch for CI drift detection. New core API — core/cve_metadata.{h,c}: struct cve_metadata { cve, cwe, attack_technique, attack_subtechnique, in_kev, kev_date_added }; const struct cve_metadata *cve_metadata_lookup(const char *cve); Lookup keyed by CVE id, not module name — the metadata is properties of the CVE (two modules covering the same bug see the same metadata). The opsec_notes field stays on the module struct because exploit technique varies per-module (different footprints). Output surfacing: - --list: new KEV column shows ★ for KEV-listed CVEs. - --module-info (text): prints cwe / att&ck / 'in CISA KEV: YES (added YYYY-MM-DD)' between summary and operations. - --module-info / --scan (JSON): emits a 'triage' subobject with the full record, plus an 'opsec_notes' field at top level when set. Initial snapshot: - 10 of 26 modules cover KEV-listed CVEs (dirty_cow, dirty_pipe, pwnkit, sudo_samedit, ptrace_traceme, fuse_legacy, nf_tables, overlayfs, overlayfs_setuid, netfilter_xtcompat). - 24 of 26 have NVD CWE mappings; 2 unmapped (NVD has no weakness record for CVE-2019-13272 and CVE-2026-46300 yet). - All 26 mapped to an ATT&CK technique. Verification: - macOS local: 33 kernel_range + clean build, --module-info shows 'in CISA KEV: YES (added 2024-05-30)' for nf_tables, --list KEV column renders. - Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails. Follow-up commits will add per-module OPSEC notes and --explain mode. --- Makefile | 3 +- core/cve_metadata.c | 236 +++++++++++++++++++++++++++ core/cve_metadata.h | 43 +++++ core/module.h | 15 ++ docs/CVE_METADATA.json | 236 +++++++++++++++++++++++++++ docs/KEV_CROSSREF.md | 47 ++++++ skeletonkey.c | 70 +++++++- tools/refresh-cve-metadata.py | 299 ++++++++++++++++++++++++++++++++++ 8 files changed, 942 insertions(+), 7 deletions(-) create mode 100644 core/cve_metadata.c create mode 100644 core/cve_metadata.h create mode 100644 docs/CVE_METADATA.json create mode 100644 docs/KEV_CROSSREF.md create mode 100755 tools/refresh-cve-metadata.py diff --git a/Makefile b/Makefile index e679db0..eac837c 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,8 @@ BUILD := build BIN := skeletonkey # core/ -CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c +CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c \ + core/host.c core/cve_metadata.c CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) # Register-every-module helper. Lives in its own translation unit so diff --git a/core/cve_metadata.c b/core/cve_metadata.c new file mode 100644 index 0000000..e33e191 --- /dev/null +++ b/core/cve_metadata.c @@ -0,0 +1,236 @@ +/* + * SKELETONKEY — CVE metadata table + * + * AUTO-GENERATED by tools/refresh-cve-metadata.py from + * docs/CVE_METADATA.json. Do not hand-edit; rerun the script. + * Sources: CISA KEV catalog + NVD CVE API 2.0. + */ + +#include "cve_metadata.h" + +#include +#include + +const struct cve_metadata cve_metadata_table[] = { + { + .cve = "CVE-2016-5195", + .cwe = "CWE-362", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2022-03-03", + }, + { + .cve = "CVE-2017-7308", + .cwe = "CWE-681", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2019-13272", + .cwe = NULL, + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2021-12-10", + }, + { + .cve = "CVE-2020-14386", + .cwe = "CWE-250", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2021-22555", + .cwe = "CWE-787", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2025-10-06", + }, + { + .cve = "CVE-2021-3156", + .cwe = "CWE-193", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2022-04-06", + }, + { + .cve = "CVE-2021-33909", + .cwe = "CWE-190", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2021-3493", + .cwe = "CWE-270", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2022-10-20", + }, + { + .cve = "CVE-2021-4034", + .cwe = "CWE-787", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2022-06-27", + }, + { + .cve = "CVE-2022-0185", + .cwe = "CWE-190", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2024-08-21", + }, + { + .cve = "CVE-2022-0492", + .cwe = "CWE-287", + .attack_technique = "T1611", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2022-0847", + .cwe = "CWE-665", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2022-04-25", + }, + { + .cve = "CVE-2022-25636", + .cwe = "CWE-269", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2022-2588", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-0179", + .cwe = "CWE-190", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-0386", + .cwe = "CWE-282", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2025-06-17", + }, + { + .cve = "CVE-2023-0458", + .cwe = "CWE-476", + .attack_technique = "T1082", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-2008", + .cwe = "CWE-129", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-22809", + .cwe = "CWE-269", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-32233", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-3269", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2023-4622", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2024-1086", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2024-05-30", + }, + { + .cve = "CVE-2026-31635", + .cwe = "CWE-130", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2026-41651", + .cwe = "CWE-367", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2026-46300", + .cwe = NULL, + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, +}; + +const size_t cve_metadata_table_len = + sizeof(cve_metadata_table) / sizeof(cve_metadata_table[0]); + +const struct cve_metadata *cve_metadata_lookup(const char *cve) +{ + if (!cve) return NULL; + for (size_t i = 0; i < cve_metadata_table_len; i++) { + if (strcmp(cve_metadata_table[i].cve, cve) == 0) + return &cve_metadata_table[i]; + } + return NULL; +} diff --git a/core/cve_metadata.h b/core/cve_metadata.h new file mode 100644 index 0000000..28b82d5 --- /dev/null +++ b/core/cve_metadata.h @@ -0,0 +1,43 @@ +/* + * SKELETONKEY — CVE metadata lookup + * + * Per-CVE annotations sourced from authoritative federal databases: + * - CISA Known Exploited Vulnerabilities catalog (in_kev, date_added) + * - NVD CVE API (cwe) + * - Hand-curated MITRE ATT&CK technique mapping + * + * Kept separate from struct skeletonkey_module because these are + * properties of the CVE (one CVE -> one set of values), not the + * exploit module. Two modules covering the same CVE see the same + * metadata. The OPSEC notes — which vary by exploit technique — + * stay on the module struct. + * + * The table is auto-generated from docs/CVE_METADATA.json by + * tools/refresh-cve-metadata.py. Do not hand-edit cve_metadata.c — + * re-run the refresh tool. + */ + +#ifndef SKELETONKEY_CVE_METADATA_H +#define SKELETONKEY_CVE_METADATA_H + +#include +#include + +struct cve_metadata { + const char *cve; /* "CVE-YYYY-NNNNN" */ + const char *cwe; /* "CWE-NNN" or NULL if NVD has no mapping */ + const char *attack_technique; /* "T1068" etc. */ + const char *attack_subtechnique; /* "T1068.001" or NULL */ + bool in_kev; /* true iff in CISA's KEV catalog */ + const char *kev_date_added; /* "YYYY-MM-DD" or "" */ +}; + +/* The full table. Length is `cve_metadata_table_len`. */ +extern const struct cve_metadata cve_metadata_table[]; +extern const size_t cve_metadata_table_len; + +/* Lookup by CVE id (e.g. "CVE-2024-1086"). Returns NULL if the CVE + * isn't in the table. Cheap linear scan; we have <100 entries. */ +const struct cve_metadata *cve_metadata_lookup(const char *cve); + +#endif /* SKELETONKEY_CVE_METADATA_H */ diff --git a/core/module.h b/core/module.h index 6b37e80..949b5b2 100644 --- a/core/module.h +++ b/core/module.h @@ -104,6 +104,21 @@ struct skeletonkey_module { const char *detect_sigma; /* sigma YAML content */ const char *detect_yara; /* yara rules content */ const char *detect_falco; /* falco rules content */ + + /* Operational-security notes — telemetry footprint THIS specific + * exploit leaves behind. The inverse of detect_auditd/yara/falco + * above (the rules catch what these notes describe). Free-form + * prose, conventionally listing: dmesg lines triggered, auditd + * events, file artifacts created/modified, persistence side- + * effects, recommended cleanup. Per-module (not per-CVE) because + * different exploits for the same bug can leave different + * footprints. NULL if no analysis written yet. + * + * NB: ATT&CK / CWE / KEV metadata is properties of the CVE itself + * (independent of exploit technique) and lives in + * core/cve_metadata.{h,c} — looked up by CVE id, refreshed via + * tools/refresh-cve-metadata.py. */ + const char *opsec_notes; }; #endif /* SKELETONKEY_MODULE_H */ diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json new file mode 100644 index 0000000..69c2e8e --- /dev/null +++ b/docs/CVE_METADATA.json @@ -0,0 +1,236 @@ +[ + { + "cve": "CVE-2016-5195", + "module_dir": "dirty_cow_cve_2016_5195", + "cwe": "CWE-362", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2022-03-03" + }, + { + "cve": "CVE-2017-7308", + "module_dir": "af_packet_cve_2017_7308", + "cwe": "CWE-681", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2019-13272", + "module_dir": "ptrace_traceme_cve_2019_13272", + "cwe": null, + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2021-12-10" + }, + { + "cve": "CVE-2020-14386", + "module_dir": "af_packet2_cve_2020_14386", + "cwe": "CWE-250", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2021-22555", + "module_dir": "netfilter_xtcompat_cve_2021_22555", + "cwe": "CWE-787", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2025-10-06" + }, + { + "cve": "CVE-2021-3156", + "module_dir": "sudo_samedit_cve_2021_3156", + "cwe": "CWE-193", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2022-04-06" + }, + { + "cve": "CVE-2021-33909", + "module_dir": "sequoia_cve_2021_33909", + "cwe": "CWE-190", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2021-3493", + "module_dir": "overlayfs_cve_2021_3493", + "cwe": "CWE-270", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2022-10-20" + }, + { + "cve": "CVE-2021-4034", + "module_dir": "pwnkit_cve_2021_4034", + "cwe": "CWE-787", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2022-06-27" + }, + { + "cve": "CVE-2022-0185", + "module_dir": "fuse_legacy_cve_2022_0185", + "cwe": "CWE-190", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2024-08-21" + }, + { + "cve": "CVE-2022-0492", + "module_dir": "cgroup_release_agent_cve_2022_0492", + "cwe": "CWE-287", + "attack_technique": "T1611", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2022-0847", + "module_dir": "dirty_pipe_cve_2022_0847", + "cwe": "CWE-665", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2022-04-25" + }, + { + "cve": "CVE-2022-25636", + "module_dir": "nft_fwd_dup_cve_2022_25636", + "cwe": "CWE-269", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2022-2588", + "module_dir": "cls_route4_cve_2022_2588", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-0179", + "module_dir": "nft_payload_cve_2023_0179", + "cwe": "CWE-190", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-0386", + "module_dir": "overlayfs_setuid_cve_2023_0386", + "cwe": "CWE-282", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2025-06-17" + }, + { + "cve": "CVE-2023-0458", + "module_dir": "entrybleed_cve_2023_0458", + "cwe": "CWE-476", + "attack_technique": "T1082", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-2008", + "module_dir": "vmwgfx_cve_2023_2008", + "cwe": "CWE-129", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-22809", + "module_dir": "sudoedit_editor_cve_2023_22809", + "cwe": "CWE-269", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-32233", + "module_dir": "nft_set_uaf_cve_2023_32233", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-3269", + "module_dir": "stackrot_cve_2023_3269", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2023-4622", + "module_dir": "af_unix_gc_cve_2023_4622", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2024-1086", + "module_dir": "nf_tables_cve_2024_1086", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2024-05-30" + }, + { + "cve": "CVE-2026-31635", + "module_dir": "dirtydecrypt_cve_2026_31635", + "cwe": "CWE-130", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2026-41651", + "module_dir": "pack2theroot_cve_2026_41651", + "cwe": "CWE-367", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2026-46300", + "module_dir": "fragnesia_cve_2026_46300", + "cwe": null, + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + } +] diff --git a/docs/KEV_CROSSREF.md b/docs/KEV_CROSSREF.md new file mode 100644 index 0000000..003abfd --- /dev/null +++ b/docs/KEV_CROSSREF.md @@ -0,0 +1,47 @@ +# CISA KEV Cross-Reference + +Which SKELETONKEY modules cover CVEs that CISA has observed exploited +in the wild per the Known Exploited Vulnerabilities catalog. +Refreshed via `tools/refresh-cve-metadata.py`. + +**10 of 26 modules cover KEV-listed CVEs.** + +## In KEV (prioritize patching) + +| CVE | Date added to KEV | CWE | Module | +| --- | --- | --- | --- | +| CVE-2019-13272 | 2021-12-10 | ? | `ptrace_traceme_cve_2019_13272` | +| CVE-2016-5195 | 2022-03-03 | CWE-362 | `dirty_cow_cve_2016_5195` | +| CVE-2021-3156 | 2022-04-06 | CWE-193 | `sudo_samedit_cve_2021_3156` | +| CVE-2022-0847 | 2022-04-25 | CWE-665 | `dirty_pipe_cve_2022_0847` | +| CVE-2021-4034 | 2022-06-27 | CWE-787 | `pwnkit_cve_2021_4034` | +| CVE-2021-3493 | 2022-10-20 | CWE-270 | `overlayfs_cve_2021_3493` | +| CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` | +| CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` | +| CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` | +| CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` | + +## Not in KEV + +Not observed exploited per CISA — but several have public PoC code +and are technically reachable. "Not in KEV" is not the same as +"safe to ignore". + +| CVE | CWE | Module | +| --- | --- | --- | +| CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` | +| CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` | +| CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` | +| CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` | +| CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` | +| CVE-2022-2588 | CWE-416 | `cls_route4_cve_2022_2588` | +| CVE-2023-0179 | CWE-190 | `nft_payload_cve_2023_0179` | +| CVE-2023-0458 | CWE-476 | `entrybleed_cve_2023_0458` | +| CVE-2023-2008 | CWE-129 | `vmwgfx_cve_2023_2008` | +| CVE-2023-22809 | CWE-269 | `sudoedit_editor_cve_2023_22809` | +| CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` | +| CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` | +| CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` | +| CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` | +| CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | +| CVE-2026-46300 | ? | `fragnesia_cve_2026_46300` | diff --git a/skeletonkey.c b/skeletonkey.c index 9783e33..0ba8dca 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -19,6 +19,7 @@ #include "core/registry.h" #include "core/offsets.h" #include "core/host.h" +#include "core/cve_metadata.h" #include #include @@ -179,6 +180,36 @@ static void emit_module_json(const struct skeletonkey_module *m, bool include_ru m->detect_sigma ? "true" : "false", m->detect_yara ? "true" : "false", m->detect_falco ? "true" : "false"); + + /* CVE-keyed triage metadata (CWE, ATT&CK, KEV). Sourced from CISA + * + NVD via tools/refresh-cve-metadata.py; lookup is O(corpus). */ + const struct cve_metadata *md = cve_metadata_lookup(m->cve); + if (md) { + char *cwe = json_escape(md->cwe); + char *tech = json_escape(md->attack_technique); + char *sub = json_escape(md->attack_subtechnique); + char *kdate = json_escape(md->kev_date_added); + fprintf(stdout, + ",\"triage\":{\"cwe\":%s%s%s," + "\"attack_technique\":%s%s%s," + "\"attack_subtechnique\":%s%s%s," + "\"in_kev\":%s," + "\"kev_date_added\":\"%s\"}", + cwe ? "\"" : "", cwe ? cwe : "null", cwe ? "\"" : "", + tech ? "\"" : "", tech ? tech : "null", tech ? "\"" : "", + sub ? "\"" : "", sub ? sub : "null", sub ? "\"" : "", + md->in_kev ? "true" : "false", + kdate ? kdate : ""); + free(cwe); free(tech); free(sub); free(kdate); + } + + /* Per-module OPSEC notes — telemetry footprint of this exploit. */ + if (m->opsec_notes) { + char *op = json_escape(m->opsec_notes); + fprintf(stdout, ",\"opsec_notes\":\"%s\"", op ? op : ""); + free(op); + } + if (include_rules) { /* Embed the actual rule text. Useful for --module-info. */ char *aud = json_escape(m->detect_auditd); @@ -210,14 +241,17 @@ static int cmd_list(const struct skeletonkey_ctx *ctx) fprintf(stdout, "]}\n"); return 0; } - fprintf(stdout, "%-20s %-18s %-25s %s\n", - "NAME", "CVE", "FAMILY", "SUMMARY"); - fprintf(stdout, "%-20s %-18s %-25s %s\n", - "----", "---", "------", "-------"); + fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n", + "NAME", "CVE", "KEV", "FAMILY", "SUMMARY"); + fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n", + "----", "---", "---", "------", "-------"); for (size_t i = 0; i < n; i++) { const struct skeletonkey_module *m = skeletonkey_module_at(i); - fprintf(stdout, "%-20s %-18s %-25s %s\n", - m->name, m->cve, m->family, m->summary); + const struct cve_metadata *md = cve_metadata_lookup(m->cve); + fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n", + m->name, m->cve, + (md && md->in_kev) ? "★" : "", + m->family, m->summary); } return 0; } @@ -567,6 +601,26 @@ static int cmd_module_info(const char *name, const struct skeletonkey_ctx *ctx) fprintf(stdout, "family: %s\n", m->family); fprintf(stdout, "kernel_range: %s\n", m->kernel_range); fprintf(stdout, "summary: %s\n", m->summary); + + /* Triage metadata sourced from CISA KEV + NVD (lookup keyed by + * m->cve). Only printed when present; mapping for older or + * recently-disclosed CVEs may be partial. */ + const struct cve_metadata *md = cve_metadata_lookup(m->cve); + if (md) { + if (md->cwe) + fprintf(stdout, "cwe: %s\n", md->cwe); + if (md->attack_technique) + fprintf(stdout, "att&ck: %s%s%s\n", + md->attack_technique, + md->attack_subtechnique ? " / " : "", + md->attack_subtechnique ? md->attack_subtechnique : ""); + if (md->in_kev) + fprintf(stdout, "in CISA KEV: YES (added %s)\n", + md->kev_date_added); + else + fprintf(stdout, "in CISA KEV: no\n"); + } + fprintf(stdout, "operations: %s%s%s%s\n", m->detect ? "detect " : "", m->exploit ? "exploit " : "", @@ -577,6 +631,10 @@ static int cmd_module_info(const char *name, const struct skeletonkey_ctx *ctx) m->detect_sigma ? "sigma " : "", m->detect_yara ? "yara " : "", m->detect_falco ? "falco " : ""); + + if (m->opsec_notes) { + fprintf(stdout, "\n--- opsec notes ---\n%s\n", m->opsec_notes); + } if (m->detect_auditd) { fprintf(stdout, "\n--- auditd rules ---\n%s", m->detect_auditd); } diff --git a/tools/refresh-cve-metadata.py b/tools/refresh-cve-metadata.py new file mode 100755 index 0000000..b7667fe --- /dev/null +++ b/tools/refresh-cve-metadata.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +""" +tools/refresh-cve-metadata.py — fetch CWE + KEV status for every CVE in the +SKELETONKEY corpus from authoritative federal sources. + +Sources: + - CISA Known Exploited Vulnerabilities catalog + https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv + (authoritative for "is this exploited in the wild?") + - NVD CVE API 2.0 + https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=... + (authoritative for CWE classification) + +The output is intentionally NOT auto-applied to module sources — drift +between an external source and our embedded metadata should surface as +a diff a human reviews. The tool produces: + + docs/CVE_METADATA.json machine-readable per-CVE record + docs/KEV_CROSSREF.md human-readable KEV table + +Modules consume the JSON via copy-paste into their struct skeletonkey_module +literal (attack_technique, cwe, in_kev, kev_date_added fields). The +provenance comment in core/module.h points contributors back here. + +No API key required; the script throttles to NVD's anonymous 5-req/30s +limit. ~3 minutes total for 26 CVEs. + +Usage: + tools/refresh-cve-metadata.py # refresh + write outputs + tools/refresh-cve-metadata.py --check # diff against committed JSON, exit 1 on drift + +Dependencies: stdlib only. Python 3.8+. +""" + +import argparse +import csv +import io +import json +import os +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +MODULES_DIR = REPO_ROOT / "modules" +OUT_JSON = REPO_ROOT / "docs" / "CVE_METADATA.json" +OUT_MD = REPO_ROOT / "docs" / "KEV_CROSSREF.md" +OUT_C = REPO_ROOT / "core" / "cve_metadata.c" + +KEV_URL = "https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv" +NVD_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={cve}" + +# Per NVD's anonymous rate limit: 5 requests per 30 seconds. +NVD_DELAY_SECONDS = 7 + +# Module → ATT&CK technique mapping. Almost all kernel/userspace LPEs +# map to T1068 (Exploitation for Privilege Escalation). The two +# exceptions are noted inline. This mapping is hand-curated; the +# tool doesn't pull ATT&CK from any feed (MITRE doesn't publish a +# clean CVE → technique CSV). +ATTACK_MAPPING = { + # Default for every CVE not listed: T1068, no subtechnique. + "CVE-2022-0492": ("T1611", None), # cgroup_release_agent — container escape + "CVE-2023-0458": ("T1082", None), # entrybleed — kernel info leak, not LPE +} + + +def discover_cves() -> list[str]: + """Find every CVE-NNNN-NNNN id by scanning modules//.""" + cves = set() + for child in MODULES_DIR.iterdir(): + if not child.is_dir(): + continue + # Module dirs end in _cve_YYYY_NNNNN + parts = child.name.split("_cve_") + if len(parts) != 2: + continue + cve_tail = parts[1].replace("_", "-") + cves.add(f"CVE-{cve_tail}") + return sorted(cves) + + +def fetch_kev_catalog() -> dict[str, str]: + """Return {cve_id: date_added_yyyy_mm_dd} from CISA's KEV CSV.""" + print(f"[*] fetching CISA KEV catalog ({KEV_URL})", file=sys.stderr) + try: + with urllib.request.urlopen(KEV_URL, timeout=30) as r: + data = r.read().decode("utf-8", errors="replace") + except urllib.error.URLError as e: + print(f"[!] KEV fetch failed: {e}", file=sys.stderr) + sys.exit(1) + out: dict[str, str] = {} + reader = csv.DictReader(io.StringIO(data)) + for row in reader: + cve = row.get("cveID", "").strip() + date = row.get("dateAdded", "").strip() + if cve: + out[cve] = date + print(f"[+] KEV catalog has {len(out)} entries", file=sys.stderr) + return out + + +def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]: + """Return (cwe_id, description) from NVD. Returns (None, None) on miss.""" + url = NVD_URL.format(cve=cve) + req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"}) + try: + with urllib.request.urlopen(req, timeout=30) as r: + blob = json.loads(r.read().decode("utf-8")) + except urllib.error.HTTPError as e: + print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr) + return None, None + except (urllib.error.URLError, json.JSONDecodeError) as e: + print(f"[!] NVD parse error for {cve}: {e}", file=sys.stderr) + return None, None + vulns = blob.get("vulnerabilities") or [] + if not vulns: + return None, None + cve_obj = vulns[0].get("cve", {}) + # weaknesses: [{source, type, description: [{lang, value: "CWE-..."}]}] + for w in cve_obj.get("weaknesses", []) or []: + for d in w.get("description", []) or []: + v = d.get("value", "") + if v.startswith("CWE-"): + return v, None # description not stored; CWE id alone is what we use + return None, None + + +def attack_for_cve(cve: str) -> tuple[str, str | None]: + return ATTACK_MAPPING.get(cve, ("T1068", None)) + + +def short_module_name(cve: str) -> str: + """Find the directory under modules/ that ends with this CVE's tail.""" + tail = cve.removeprefix("CVE-").replace("-", "_") + for child in MODULES_DIR.iterdir(): + if child.is_dir() and child.name.endswith(f"_cve_{tail}"): + return child.name + return "?" + + +def build_records(cves: list[str], kev: dict[str, str]) -> list[dict]: + records = [] + for i, cve in enumerate(cves, 1): + print(f"[*] [{i:2d}/{len(cves)}] {cve}: NVD lookup", file=sys.stderr) + cwe, _ = fetch_nvd_cwe(cve) + tech, subtech = attack_for_cve(cve) + in_kev = cve in kev + rec = { + "cve": cve, + "module_dir": short_module_name(cve), + "cwe": cwe, + "attack_technique": tech, + "attack_subtechnique": subtech, + "in_kev": in_kev, + "kev_date_added": kev.get(cve, ""), + } + records.append(rec) + # Throttle NVD requests + if i < len(cves): + time.sleep(NVD_DELAY_SECONDS) + return records + + +def _c_str(s: str | None) -> str: + """Render a Python str|None as a C string literal or NULL.""" + if s is None: + return "NULL" + # only safe chars in our domain (CVE-/CWE-/T#### / dates) so no escaping needed + return f'"{s}"' + + +def write_c_table(records: list[dict]) -> None: + """Generate core/cve_metadata.c from the JSON records.""" + lines = [ + "/*", + " * SKELETONKEY — CVE metadata table", + " *", + " * AUTO-GENERATED by tools/refresh-cve-metadata.py from", + " * docs/CVE_METADATA.json. Do not hand-edit; rerun the script.", + " * Sources: CISA KEV catalog + NVD CVE API 2.0.", + " */", + "", + '#include "cve_metadata.h"', + "", + "#include ", + "#include ", + "", + "const struct cve_metadata cve_metadata_table[] = {", + ] + for r in records: + lines.append(" {") + lines.append(f" .cve = {_c_str(r['cve'])},") + lines.append(f" .cwe = {_c_str(r['cwe'])},") + lines.append(f" .attack_technique = {_c_str(r['attack_technique'])},") + lines.append(f" .attack_subtechnique = {_c_str(r['attack_subtechnique'])},") + lines.append(f" .in_kev = {'true' if r['in_kev'] else 'false'},") + lines.append(f" .kev_date_added = {_c_str(r['kev_date_added'])},") + lines.append(" },") + lines += [ + "};", + "", + "const size_t cve_metadata_table_len =", + " sizeof(cve_metadata_table) / sizeof(cve_metadata_table[0]);", + "", + "const struct cve_metadata *cve_metadata_lookup(const char *cve)", + "{", + " if (!cve) return NULL;", + " for (size_t i = 0; i < cve_metadata_table_len; i++) {", + " if (strcmp(cve_metadata_table[i].cve, cve) == 0)", + " return &cve_metadata_table[i];", + " }", + " return NULL;", + "}", + "", + ] + OUT_C.write_text("\n".join(lines)) + print(f"[+] wrote {OUT_C.relative_to(REPO_ROOT)}", file=sys.stderr) + + +def write_outputs(records: list[dict]) -> None: + OUT_JSON.parent.mkdir(parents=True, exist_ok=True) + OUT_JSON.write_text(json.dumps(records, indent=2) + "\n") + print(f"[+] wrote {OUT_JSON.relative_to(REPO_ROOT)}", file=sys.stderr) + write_c_table(records) + + # KEV cross-reference table + in_kev = [r for r in records if r["in_kev"]] + not_in_kev = [r for r in records if not r["in_kev"]] + lines = [ + "# CISA KEV Cross-Reference", + "", + "Which SKELETONKEY modules cover CVEs that CISA has observed exploited", + "in the wild per the Known Exploited Vulnerabilities catalog.", + "Refreshed via `tools/refresh-cve-metadata.py`.", + "", + f"**{len(in_kev)} of {len(records)} modules cover KEV-listed CVEs.**", + "", + "## In KEV (prioritize patching)", + "", + "| CVE | Date added to KEV | CWE | Module |", + "| --- | --- | --- | --- |", + ] + for r in sorted(in_kev, key=lambda r: r["kev_date_added"]): + lines.append( + f"| {r['cve']} | {r['kev_date_added']} | {r['cwe'] or '?'} | `{r['module_dir']}` |" + ) + lines += [ + "", + "## Not in KEV", + "", + "Not observed exploited per CISA — but several have public PoC code", + "and are technically reachable. \"Not in KEV\" is not the same as", + "\"safe to ignore\".", + "", + "| CVE | CWE | Module |", + "| --- | --- | --- |", + ] + for r in sorted(not_in_kev, key=lambda r: r["cve"]): + lines.append(f"| {r['cve']} | {r['cwe'] or '?'} | `{r['module_dir']}` |") + lines.append("") + OUT_MD.write_text("\n".join(lines)) + print(f"[+] wrote {OUT_MD.relative_to(REPO_ROOT)}", file=sys.stderr) + + +def check_drift() -> int: + """Exit 1 if the committed JSON differs from a fresh fetch.""" + if not OUT_JSON.exists(): + print(f"[!] no committed {OUT_JSON.name} — run without --check first", file=sys.stderr) + return 1 + committed = json.loads(OUT_JSON.read_text()) + fresh = build_records(discover_cves(), fetch_kev_catalog()) + if committed == fresh: + print("[+] CVE_METADATA.json is current", file=sys.stderr) + return 0 + print("[!] CVE_METADATA.json drifted — refresh via " + "`tools/refresh-cve-metadata.py`", file=sys.stderr) + return 1 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.splitlines()[1]) + ap.add_argument("--check", action="store_true", + help="diff against committed metadata; exit 1 on drift") + args = ap.parse_args() + if args.check: + return check_drift() + cves = discover_cves() + print(f"[*] {len(cves)} CVE(s) in corpus", file=sys.stderr) + kev = fetch_kev_catalog() + records = build_records(cves, kev) + write_outputs(records) + return 0 + + +if __name__ == "__main__": + sys.exit(main())