module metadata: CWE + ATT&CK + CISA KEV triage from federal sources

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/<dir>/ (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.
This commit is contained in:
2026-05-23 10:38:01 -04:00
parent 60d22eb4f6
commit e4a600fef2
8 changed files with 942 additions and 7 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ BUILD := build
BIN := skeletonkey BIN := skeletonkey
# core/ # 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)) CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
# Register-every-module helper. Lives in its own translation unit so # Register-every-module helper. Lives in its own translation unit so
+236
View File
@@ -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 <stddef.h>
#include <string.h>
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;
}
+43
View File
@@ -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 <stdbool.h>
#include <stddef.h>
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 */
+15
View File
@@ -104,6 +104,21 @@ struct skeletonkey_module {
const char *detect_sigma; /* sigma YAML content */ const char *detect_sigma; /* sigma YAML content */
const char *detect_yara; /* yara rules content */ const char *detect_yara; /* yara rules content */
const char *detect_falco; /* falco 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 */ #endif /* SKELETONKEY_MODULE_H */
+236
View File
@@ -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": ""
}
]
+47
View File
@@ -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` |
+64 -6
View File
@@ -19,6 +19,7 @@
#include "core/registry.h" #include "core/registry.h"
#include "core/offsets.h" #include "core/offsets.h"
#include "core/host.h" #include "core/host.h"
#include "core/cve_metadata.h"
#include <time.h> #include <time.h>
#include <sys/utsname.h> #include <sys/utsname.h>
@@ -179,6 +180,36 @@ static void emit_module_json(const struct skeletonkey_module *m, bool include_ru
m->detect_sigma ? "true" : "false", m->detect_sigma ? "true" : "false",
m->detect_yara ? "true" : "false", m->detect_yara ? "true" : "false",
m->detect_falco ? "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) { if (include_rules) {
/* Embed the actual rule text. Useful for --module-info. */ /* Embed the actual rule text. Useful for --module-info. */
char *aud = json_escape(m->detect_auditd); char *aud = json_escape(m->detect_auditd);
@@ -210,14 +241,17 @@ static int cmd_list(const struct skeletonkey_ctx *ctx)
fprintf(stdout, "]}\n"); fprintf(stdout, "]}\n");
return 0; return 0;
} }
fprintf(stdout, "%-20s %-18s %-25s %s\n", fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n",
"NAME", "CVE", "FAMILY", "SUMMARY"); "NAME", "CVE", "KEV", "FAMILY", "SUMMARY");
fprintf(stdout, "%-20s %-18s %-25s %s\n", fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n",
"----", "---", "------", "-------"); "----", "---", "---", "------", "-------");
for (size_t i = 0; i < n; i++) { for (size_t i = 0; i < n; i++) {
const struct skeletonkey_module *m = skeletonkey_module_at(i); const struct skeletonkey_module *m = skeletonkey_module_at(i);
fprintf(stdout, "%-20s %-18s %-25s %s\n", const struct cve_metadata *md = cve_metadata_lookup(m->cve);
m->name, m->cve, m->family, m->summary); fprintf(stdout, "%-20s %-18s %-3s %-25s %s\n",
m->name, m->cve,
(md && md->in_kev) ? "" : "",
m->family, m->summary);
} }
return 0; 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, "family: %s\n", m->family);
fprintf(stdout, "kernel_range: %s\n", m->kernel_range); fprintf(stdout, "kernel_range: %s\n", m->kernel_range);
fprintf(stdout, "summary: %s\n", m->summary); 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", fprintf(stdout, "operations: %s%s%s%s\n",
m->detect ? "detect " : "", m->detect ? "detect " : "",
m->exploit ? "exploit " : "", 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_sigma ? "sigma " : "",
m->detect_yara ? "yara " : "", m->detect_yara ? "yara " : "",
m->detect_falco ? "falco " : ""); m->detect_falco ? "falco " : "");
if (m->opsec_notes) {
fprintf(stdout, "\n--- opsec notes ---\n%s\n", m->opsec_notes);
}
if (m->detect_auditd) { if (m->detect_auditd) {
fprintf(stdout, "\n--- auditd rules ---\n%s", m->detect_auditd); fprintf(stdout, "\n--- auditd rules ---\n%s", m->detect_auditd);
} }
+299
View File
@@ -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/<dir>/."""
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 <stddef.h>",
"#include <string.h>",
"",
"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())