147 lines
7.5 KiB
Markdown
147 lines
7.5 KiB
Markdown
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||
# 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 (`arg0`…`arg5`), 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 `arg0`…`arg3` 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 `100060`–`100066`.
|
||
`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 `100067`–`100073` and `100076`–`100078`.
|
||
|
||
## 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.
|