Adds .opsec_notes to every module's struct skeletonkey_module
(31 entries across 26 module files). One paragraph per exploit
describing the runtime footprint a defender/SOC would see:
- file artifacts created/modified (exact paths from source)
- syscall observables (the unshare / socket / setsockopt /
splice / msgsnd patterns the embedded detection rules look for)
- dmesg signatures (silent on success vs KASAN oops on miss)
- network activity (loopback-only vs none)
- persistence side-effects (/etc/passwd modification, dropped
setuid binaries, backdoors)
- cleanup behaviour (callback present? what it restores?)
Each note is grounded in the module's source code + its existing
auditd/sigma/yara/falco detection rules — the OPSEC notes are
literally the inverse of those rules (the rules describe what to
look for; the notes describe what the exploit triggers).
Three intelligence agents researched the modules in parallel,
reading source + MODULE.md, then their proposals were embedded
verbatim via tools/inject_opsec.py (one-shot script, not retained).
Where surfaced:
- --module-info <name>: '--- opsec notes ---' section between
detect-rules summary and the embedded auditd/sigma rule bodies.
- --module-info / --scan --json: 'opsec_notes' top-level string.
Audience uses:
- Red team: see what footprint each exploit leaves so they pick
chains that match the host's telemetry posture.
- Blue team: the notes mirror the existing detection rules from the
attacker side — easy diff to find gaps in their SIEM coverage.
- Researchers: per-exploit footprint catalog for technique analysis.
copy_fail_family gets one shared note across all 5 register entries
(copy_fail, copy_fail_gcm, dirty_frag_esp, dirty_frag_esp6,
dirty_frag_rxrpc) since they share exploit infrastructure.
Verification:
- macOS local: clean build, --module-info nf_tables shows full
opsec section + CWE + ATT&CK + KEV row from previous commit.
- Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
Next: --explain mode (uses these notes + the triage metadata to
render a single 'why is this verdict, what would patch fix it, and
what would the SOC see' page per module).
The kpti_enabled bool in struct skeletonkey_host flattens three
distinct sysfs states into one bit:
/sys/devices/system/cpu/vulnerabilities/meltdown content:
- 'Not affected' → CPU is Meltdown-immune; KPTI off; EntryBleed
doesn't apply (verdict: OK)
- 'Mitigation: PTI' → KPTI on (verdict: VULNERABLE)
- 'Vulnerable' → KPTI off but CPU not hardened (rare;
verdict: VULNERABLE conservatively)
- file unreadable → unknown (verdict: VULNERABLE conservatively)
kpti_enabled=true only captures 'Mitigation: PTI'; kpti_enabled=false
collapses 'Not affected', 'Vulnerable', and 'unreadable' into one
indistinguishable case. That meant entrybleed_detect() had to
re-open the sysfs file to recover the raw string.
Fix by also stashing the raw first line in
ctx->host->meltdown_mitigation[64]. kpti_enabled stays for callers
that only need the simple bool; new code that needs the nuance reads
the string. populate happens once at startup, like every other host
field.
entrybleed migration:
- reads ctx->host->meltdown_mitigation instead of opening sysfs
- removes the file-local read_first_line() helper (now dead code)
- same three-way verdict logic, but driven by a const char *
instead of a fresh fopen() each detect()
Test coverage:
- 3 new test rows on x86_64 fingerprints:
empty mitigation → VULNERABLE (conservative)
'Not affected' → OK
'Mitigation: PTI' → VULNERABLE
- 1 stub-path test row on non-x86_64 fingerprints (PRECOND_FAIL)
- registry coverage report: 30/31 modules now have direct tests
(up from 29/31; copy_fail is the only remaining untested module)
Verification:
- macOS: 33 kernel_range + 1 entrybleed-stub = 34 passes, 0 fails
- Linux (docker gcc:latest): 33 kernel_range + 54 detect = 87
passes, 0 fails. Up from 83 last commit.