skeletonkey: --explain MODULE — single-page operator briefing

One command that answers 'should we worry about this CVE here,
what would patch it, and what would the SOC see if someone tried
it'. Renders, for the specified module:

  - Header: name + CVE + summary
  - WEAKNESS: CWE id and MITRE ATT&CK technique (from CVE metadata)
  - THREAT INTEL: CISA KEV status (with date_added if listed) and
    the upstream-curated kernel_range
  - HOST FINGERPRINT: kernel + arch + distro from ctx->host plus
    every relevant capability gate (userns / apparmor / selinux /
    lockdown)
  - DETECT() TRACE (live): runs the module's detect() with verbose
    stderr enabled so the operator sees the gates fire in real
    time — 'kernel X is patched', 'userns blocked by AppArmor',
    'no readable setuid binary', etc.
  - VERDICT: the result_t with a one-line operator interpretation
    that varies by outcome (OK / VULNERABLE / PRECOND_FAIL /
    TEST_ERROR each get their own framing)
  - OPSEC FOOTPRINT: word-wrapped .opsec_notes paragraph (from
    last commit) showing what an exploit would leave behind on
    this host
  - DETECTION COVERAGE: which of auditd/sigma/yara/falco have
    embedded rules for this module, with pointers to the
    --module-info / --detect-rules commands that dump the bodies

Targeted at every audience the project is meant to serve:
  - Red team: opsec footprint + 'would this even reach' verdict
    in one screen
  - Blue team: paste-ready triage ticket with CVE / CWE / ATT&CK /
    KEV header and detection-coverage matrix
  - Researchers: the live trace shows the reasoning chain
    (predates check, kernel_range_is_patched lookup, userns gate)
    that drove the verdict — auditable without reading source
  - SOC analysts / students: a single self-contained briefing per
    CVE, no cross-referencing needed

Implementation:
  - New mode MODE_EXPLAIN, new flag --explain MODULE
  - cmd_explain() composes the page from the existing module
    struct, cve_metadata_lookup() (federal-source triage data),
    ctx->host (cached fingerprint), and a live detect() call
  - print_wrapped() helper word-wraps the long .opsec_notes
    paragraph at 76 cols / 2-space indent
  - Help text + README quickstart + DETECTION_PLAYBOOK single-host
    recipe all updated to mention --explain

Smoke tests:
  - macOS: --explain nf_tables shows full briefing; trace says
    'Linux-only module — not applicable here'; verdict
    PRECOND_FAIL with the generic-precondition interpretation
  - Linux (docker gcc:latest): --explain nf_tables on a 6.12 host
    fires '[+] nf_tables: kernel 6.12.76-linuxkit is patched';
    verdict OK with the 'this host is patched' interpretation
  - Both: --explain nope (unknown module) returns 1 with a clear
    'no module ... Try --list' error
  - Both: 87 tests still pass (33 kernel_range + 54 detect on Linux,
    33 + 0 stubbed on macOS)

Closes the metadata + opsec + explain trio. The three together
answer the 'best tool for red team, blue team, researchers, and
more' framing.
This commit is contained in:
2026-05-23 10:49:46 -04:00
parent 39ce4dff09
commit ee3e7dd9a7
3 changed files with 181 additions and 0 deletions
+5
View File
@@ -86,6 +86,11 @@ curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/inst
# What's this box vulnerable to? (no sudo)
skeletonkey --scan
# One-page operator briefing for a single CVE: CWE / MITRE ATT&CK /
# CISA KEV status, live detect() trace, OPSEC footprint, detection
# coverage. Useful for triage tickets and SOC analyst handoffs.
skeletonkey --explain nf_tables
# Pick the safest LPE and run it
skeletonkey --auto --i-know
+11
View File
@@ -41,12 +41,23 @@ make it part of your daily ops" guide.
# Daily/weekly hygiene check
sudo skeletonkey --scan
# Investigate a specific finding (one-page operator briefing)
sudo skeletonkey --explain nf_tables # whichever module came back VULNERABLE
# Shows: CVE / CWE / MITRE ATT&CK / CISA KEV status, live detect() trace,
# OPSEC footprint (what an exploit would leave behind), detection-rule
# coverage, mitigation. Paste into the triage ticket.
# If anything's VULNERABLE, deploy detections + apply mitigation
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
sudo augenrules --load
sudo skeletonkey --mitigate copy_fail # or whichever module fired
```
The `--explain` output is also useful as a learning artifact: each
module's `--explain` block is a self-contained CVE briefing with the
reasoning chain the detect() function walked, so analysts can verify
SKELETONKEY's verdict against their own understanding of the bug.
### Small fleet (~10-100 hosts, SSH-reachable)
Use `tools/skeletonkey-fleet-scan.sh`:
+165
View File
@@ -57,6 +57,10 @@ static void usage(const char *prog)
" (combine with --format=auditd|sigma|yara|falco)\n"
" --module-info <name> full metadata + rule bodies for one module\n"
" (combine with --json for machine-readable output)\n"
" --explain <name> one-page operator briefing: CVE / CWE / ATT&CK /\n"
" KEV, host fingerprint, live detect() trace + verdict,\n"
" OPSEC footprint, detection coverage, mitigation.\n"
" Useful for triage tickets and SOC analyst handoffs.\n"
" --auto scan host, rank vulnerable modules by safety, run the\n"
" safest exploit. Requires --i-know. The 'one command\n"
" that gets you root' mode — picks structural exploits\n"
@@ -110,6 +114,7 @@ enum mode {
MODE_DUMP_OFFSETS,
MODE_HELP,
MODE_VERSION,
MODE_EXPLAIN,
};
enum detect_format {
@@ -644,6 +649,163 @@ static int cmd_module_info(const char *name, const struct skeletonkey_ctx *ctx)
return 0;
}
/* Word-wrap a long paragraph at `width` columns, indenting every line by
* `indent` spaces. Writes to stdout. Used by --explain to render the
* .opsec_notes paragraph (typically 400-700 chars). */
static void print_wrapped(const char *text, int indent, int width)
{
int col = indent;
for (int i = 0; i < indent; i++) fputc(' ', stdout);
const char *p = text;
while (*p) {
const char *word_start = p;
while (*p && *p != ' ') p++;
size_t word_len = (size_t)(p - word_start);
if (col + (int)word_len > width && col > indent) {
fputc('\n', stdout);
for (int i = 0; i < indent; i++) fputc(' ', stdout);
col = indent;
}
fwrite(word_start, 1, word_len, stdout);
col += (int)word_len;
while (*p == ' ') {
if (col + 1 > width) {
fputc('\n', stdout);
for (int i = 0; i < indent; i++) fputc(' ', stdout);
col = indent;
p++;
break;
}
fputc(' ', stdout);
col++;
p++;
}
}
fputc('\n', stdout);
}
/* --explain MODULE — single-page operator briefing. Combines metadata
* (CVE / CWE / ATT&CK / KEV), host fingerprint (kernel / arch / userns
* gates), live detect() trace (the gates the module just walked, what
* the verdict was and why), OPSEC footprint (telemetry the exploit
* leaves), detection coverage (which formats have rules), and mitigation
* guidance. The intended audience is anyone who wants ONE page that
* answers "should we worry about this CVE here, what would patch it,
* and what would the SOC see if someone tried it".
*
* detect() writes its reasoning to stderr (the normal verbose path);
* --explain's structured framing goes to stdout. Redirect 2>&1 to merge. */
static int cmd_explain(const char *name, const struct skeletonkey_ctx *ctx)
{
const struct skeletonkey_module *m = skeletonkey_module_find(name);
if (!m) {
fprintf(stderr, "[-] no module '%s'. Try --list.\n", name);
return 1;
}
const struct cve_metadata *md = cve_metadata_lookup(m->cve);
/* ── header ──────────────────────────────────────────────── */
fprintf(stdout, "\n");
fprintf(stdout, "════════════════════════════════════════════════════\n");
fprintf(stdout, " %s %s\n", m->name, m->cve);
fprintf(stdout, "════════════════════════════════════════════════════\n");
fprintf(stdout, " %s\n", m->summary);
/* ── weakness ────────────────────────────────────────────── */
fprintf(stdout, "\nWEAKNESS\n");
if (md && md->cwe)
fprintf(stdout, " %s\n", md->cwe);
else
fprintf(stdout, " (no NVD CWE mapping yet)\n");
if (md && md->attack_technique)
fprintf(stdout, " MITRE ATT&CK: %s%s%s\n",
md->attack_technique,
md->attack_subtechnique ? " / " : "",
md->attack_subtechnique ? md->attack_subtechnique : "");
/* ── threat-intel context ────────────────────────────────── */
fprintf(stdout, "\nTHREAT INTEL\n");
if (md && md->in_kev)
fprintf(stdout, " ✓ In CISA Known Exploited Vulnerabilities catalog "
"(added %s)\n", md->kev_date_added);
else
fprintf(stdout, " - Not in CISA KEV (no in-the-wild exploitation "
"observed by CISA)\n");
fprintf(stdout, " Affected: %s\n", m->kernel_range);
/* ── host fingerprint summary ────────────────────────────── */
if (ctx->host) {
fprintf(stdout, "\nHOST FINGERPRINT\n");
if (ctx->host->kernel.release && ctx->host->kernel.release[0])
fprintf(stdout, " kernel: %s (%s)\n",
ctx->host->kernel.release, ctx->host->arch);
if (ctx->host->distro_pretty[0])
fprintf(stdout, " distro: %s\n", ctx->host->distro_pretty);
fprintf(stdout, " unpriv userns: %s\n",
ctx->host->unprivileged_userns_allowed ? "ALLOWED" : "blocked");
if (ctx->host->apparmor_restrict_userns)
fprintf(stdout, " apparmor: restricts unprivileged userns\n");
if (ctx->host->selinux_enforcing)
fprintf(stdout, " selinux: enforcing\n");
if (ctx->host->kernel_lockdown_active)
fprintf(stdout, " lockdown: active\n");
}
/* ── live detect trace ───────────────────────────────────── */
fprintf(stdout, "\nDETECT() TRACE (live; reads ctx->host, fires gates)\n");
fflush(stdout);
skeletonkey_result_t r = SKELETONKEY_TEST_ERROR;
if (m->detect) {
struct skeletonkey_ctx dctx = *ctx;
dctx.json = false; /* keep verbose stderr reasoning on */
r = m->detect(&dctx);
fflush(stderr);
} else {
fprintf(stdout, " (this module has no detect() — no probe to run)\n");
}
fprintf(stdout, "\nVERDICT: %s\n", result_str(r));
/* one-line interpretation for the operator */
switch (r) {
case SKELETONKEY_OK:
fprintf(stdout, " -> this host is patched / not applicable / immune.\n");
break;
case SKELETONKEY_VULNERABLE:
fprintf(stdout, " -> bug is reachable. The OPSEC section below shows what a "
"successful exploit() would leave.\n");
break;
case SKELETONKEY_PRECOND_FAIL:
fprintf(stdout, " -> a precondition check rejected this host: wrong "
"OS / arch, kernel out of range, a host-side gate "
"(userns / apparmor / selinux), or a missing carrier "
"file. See trace above for which check fired.\n");
break;
case SKELETONKEY_TEST_ERROR:
fprintf(stdout, " -> probe machinery failed; verdict unknown.\n");
break;
default: break;
}
/* ── OPSEC footprint ─────────────────────────────────────── */
if (m->opsec_notes) {
fprintf(stdout, "\nOPSEC FOOTPRINT (what exploit() leaves on this host)\n");
print_wrapped(m->opsec_notes, 2, 76);
}
/* ── detection coverage matrix ───────────────────────────── */
fprintf(stdout, "\nDETECTION COVERAGE (rules embedded in this binary)\n");
fprintf(stdout, " %s auditd %s sigma %s yara %s falco\n",
m->detect_auditd ? "" : "·",
m->detect_sigma ? "" : "·",
m->detect_yara ? "" : "·",
m->detect_falco ? "" : "·");
fprintf(stdout, " (see skeletonkey --module-info %s for rule bodies,\n"
" or skeletonkey --detect-rules --format=auditd for the full corpus)\n",
m->name);
return (int)r;
}
static int cmd_scan(const struct skeletonkey_ctx *ctx)
{
int worst = 0;
@@ -1130,6 +1292,7 @@ int main(int argc, char **argv)
{"no-color", no_argument, 0, 5 },
{"full-chain", no_argument, 0, 7 },
{"dry-run", no_argument, 0, 10 },
{"explain", required_argument, 0, 11 },
{"version", no_argument, 0, 'V'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
@@ -1155,6 +1318,7 @@ int main(int argc, char **argv)
case 8 : mode = MODE_DUMP_OFFSETS; break;
case 9 : mode = MODE_AUTO; ctx.authorized = i_know ? true : ctx.authorized; break;
case 10 : ctx.dry_run = true; break;
case 11 : mode = MODE_EXPLAIN; target = optarg; break;
case 6 :
if (strcmp(optarg, "auditd") == 0) dr_fmt = FMT_AUDITD;
else if (strcmp(optarg, "sigma") == 0) dr_fmt = FMT_SIGMA;
@@ -1179,6 +1343,7 @@ int main(int argc, char **argv)
if (mode == MODE_SCAN) return cmd_scan(&ctx);
if (mode == MODE_LIST) return cmd_list(&ctx);
if (mode == MODE_MODULE_INFO) return cmd_module_info(target, &ctx);
if (mode == MODE_EXPLAIN) return cmd_explain(target, &ctx);
if (mode == MODE_DETECT_RULES) return cmd_detect_rules(dr_fmt);
if (mode == MODE_AUDIT) return cmd_audit(&ctx);
if (mode == MODE_AUTO) return cmd_auto(&ctx);