enodia-sentinal/docs/behavior/03-event-detection.md

7.5 KiB
Raw Permalink Blame History

3. Event Detection

Scope: the transport-independent contract for exec and syscall events and the rules that turn them into alerts. Python remains the oracle. The Go port accepts replay fixtures and reproduces the alert envelopes. Its opt-in native exec and syscall sources now feed the same contract through cilium/ebpf; typed-host kernel transports remain a separate boundary.

Typed host-event rules are part of the same replay and catalog contract.

3.1 Exec events

ExecEvent fields are pid, ppid, uid, parent_comm, filename, and argv. argv excludes filename.

  • exec_comm is the POSIX basename of filename. A trailing slash therefore produces an empty basename; ports must not clean the path first.
  • argv_str is filename and every captured argv item joined with one space, then stripped at both ends.
  • Rules AND every condition they specify. Values within one condition are ORed.
  • A rule with no positive condition is invalid. parent_exclude is a veto, not a positive condition.
  • argv_regex is case-insensitive and searches the whole argv_str.

Alert contract:

Field Value
signature exec_rule.<classtype>
key exec:<sid>:<pid>
pids the event PID, even when zero
detail sid=<sid> <msg> — pid=… ppid=… parent=… exec=… argv=[…]

The argv text in detail is truncated to 120 characters. Built-ins:

SID Severity Classtype Match
100001 CRITICAL fileless-execution filename begins /tmp/, /dev/shm/, or /var/tmp/
100002 CRITICAL c2-reverse-shell reverse-shell regex in argv_str
100003 CRITICAL web-rce web/DB parent launches a configured interpreter basename
100004 HIGH ingress-tool-transfer curl/wget/fetch piped to a shell

Configured rules are appended after built-ins from [[exec_rules]] tables in exec_rules_file. Missing files mean built-ins only. Unknown severity strings fall back to HIGH, matching Python.

3.2 Syscall events

SyscallEvent fields are pid, ppid, uid, comm, syscall, six unsigned arguments (arg0arg5), and optional text. Missing arguments are zero.

Alert contract:

Field Value
signature syscall_rule.<classtype>
key syscall:<sid>:<pid>:<syscall>:<arg0-hex>:<arg2-hex>
pids the event PID
detail rule metadata plus arg0arg3 formatted as prefixed lowercase hex

Optional text is appended as text=[…] and truncated to 80 characters.

SID Severity Classtype Match
100060 CRITICAL memory-obfuscation mprotect, arg2 contains both WRITE (0x2) and EXEC (0x4)
100061 CRITICAL memory-obfuscation mmap, arg2 contains WRITE and EXEC
100062 MEDIUM fileless-execution any memfd_create
100063 HIGH anti-analysis ptrace request TRACEME (0), ATTACH (16), or SEIZE (0x4206)
100064 MEDIUM anti-analysis any seccomp, or prctl arg0 PR_SET_SECCOMP (22)
100065 HIGH credential-access process_vm_readv or process_vm_writev
100066 MEDIUM memory-obfuscation mlock, mlock2, or mlockall

3.3 Current Go acceptance boundary

scripts/check-go-parity.py replays shared JSON into both implementations and compares decoded enodia.event.v1 alert envelopes exactly. Acceptance requires:

  • all four built-in exec rules plus one configured rule;
  • all seven syscall rules and all ten host rules plus negative predicate cases;
  • identical SID, severity, signature, classtype, key, detail, PID list, host, and timestamp;
  • no BPF/root/kernel dependency for replay mode.

Replay flags are --exec-events, --syscall-events, and --host-events. They emit alert JSONL only and exit. They remain deterministic verification surfaces independent of the live source.

--ebpf-exec loads a compiled tracepoint program with cilium/ebpf, captures PID, parent PID, UID, caller comm, filename, and the first two arguments, then passes each record through the same exec rule engine, shared cooldown gate, and enodia.event.v1 builder. Parent lookup uses CO-RE relocation against kernel BTF. Startup and reader failures update ebpf/ebpf_exec status and leave the poll loop running. The flag is opt-in and requires Go 1.25+ to build.

--ebpf-syscall attaches one raw-syscall entry tracepoint. An architecture- specific map allows only mprotect, mmap, memfd_create, ptrace, prctl, seccomp, process_vm_readv, process_vm_writev, mlock, mlock2, and mlockall through the BPF program, plus setuid and setgid, avoiding a user-space event for every host syscall. It captures the six arguments and the memfd name before routing syscall events through SIDs 100060100066. setuid/setgid become typed host events for SIDs 100071/100072. The portable fchmodat/fchownat calls, plus legacy chmod/chown/lchown on amd64, become chmod/chown host events for SID 100070. Relative paths are resolved against /proc/<pid>/cwd before matching persistence prefixes. The capset effective bitmap is decoded to canonical capability names for SID 100077. finit_module resolves its file descriptor through /proc/<pid>/fd and supplies path/module fields to SID 100078. Syscall numbers come from build-target golang.org/x/sys/unix constants rather than a hand-maintained architecture table. Static builds are exercised for amd64, arm64, riscv64, and big-endian s390x.

3.4 Typed host events

HostEvent covers tcp_connect, listen, bind, accept, file_write, chmod, chown, setuid, setgid, capset, and module_load. Compatibility aliases accepted by ruleops are stable: type, remote_*, bind_*, listen_*, uid_target, gid_target, caps, cap_effective, singular capability, and module name. Numeric strings accept decimal/hex/octal; absent target UID/GID values are -1, not zero.

Host alerts use signature host_rule.<classtype> and a key containing every typed identity field. Capabilities are normalized uppercase for matching and rendering. Built-in SIDs are 100067100073 and 100076100078.

3.5 Mixed pipeline and catalog

--event-stream accepts mixed JSONL (or - for stdin), infers exec/syscall events when no explicit type exists, and routes typed events through one transport-independent rule set. --rules-list and --rules-show expose the same sorted metadata and conditions as Python ruleops.

The native exec/syscall readers and all replay modes feed this boundary. The syscall reader also supplies typed-host transports for root UID/GID transition requests, persistence-path permission/ownership changes, and capability-set requests. finit_module supplies module-load events with resolved paths. Interpreter write, writev, pwrite64, pwritev, and pwritev2 calls are filtered by the same comm list as SID 100069, correlated across raw syscall enter/exit in a bounded BPF map, emitted only for positive byte counts, and resolved through /proc/<pid>/fd. Successful IPv4/IPv6 connect and bind calls use the same enter/exit correlation and in-kernel interpreter filter for SIDs 100067/100073. Connect events are verified against /proc/net/tcp{,6} by socket inode so UDP cannot be mislabeled as tcp_connect. Successful listen, accept, and accept4 calls resolve their socket FD to local/peer TCP endpoints through the same inode tables for SIDs 100068/100076. Every current typed-host event family now has a native transport.