feat(go): advance validation sidecar toward production
This commit is contained in:
parent
6a06eba255
commit
f85c2e831a
92 changed files with 8881 additions and 91 deletions
|
|
@ -331,7 +331,7 @@ Store paths derive from `log_dir`: `listener-baseline.json`,
|
|||
|
||||
---
|
||||
|
||||
## 1.8 Port parity notes (Go 2.0)
|
||||
## 1.8 Port parity notes (Go port)
|
||||
|
||||
What the Go agent must reproduce to be behavior-equivalent for this subsystem:
|
||||
|
||||
|
|
@ -349,7 +349,8 @@ What the Go agent must reproduce to be behavior-equivalent for this subsystem:
|
|||
the other wrote.
|
||||
- **Reconcile-before-cooldown** ordering and 60s per-key cooldown (§1.6).
|
||||
|
||||
Current Go status (as of this writing): poll detectors are ported and pass the
|
||||
parity fixture, but baselines are still **injected from fixtures** — the store
|
||||
package, live SUID walk, and the daemon-equivalent lifecycle above are the
|
||||
work this document specifies.
|
||||
Current Go status: poll detectors pass the parity fixture. The baseline store,
|
||||
live SUID walk, startup grace, persistence scan, and first-seen retention are
|
||||
implemented behind the sidecar's explicit `--state-dir` lifecycle. Fixture mode
|
||||
continues to inject state and never touches a live store. Production service
|
||||
cutover remains outside this acceptance slice.
|
||||
|
|
|
|||
276
docs/behavior/02-poll-detectors.md
Normal file
276
docs/behavior/02-poll-detectors.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||
# 2. Poll Detectors
|
||||
|
||||
Scope: the per-detector behavioral contract for the twelve pure detectors in
|
||||
`detectors/`. Each is a `detect(state, cfg) -> Iterable[Alert]` over the
|
||||
`SystemState` built in §1. This section documents, per detector, the inputs it
|
||||
reads, the match algorithm, the exact alert it emits, and its allow-list /
|
||||
fail-open rules. The registry order and the baseline-arming gate are in §1.3 /
|
||||
§1.5; the alert fields and dedup keying are in §1.1.
|
||||
|
||||
Every detector is fail-open by inheritance: it reads already-captured
|
||||
`SystemState` fields, and missing `/proc`/`ss` data is an empty value, never an
|
||||
exception.
|
||||
|
||||
## 2.0 Signature summary
|
||||
|
||||
The stable contract. A port emitting a different SID, severity, classtype, or
|
||||
key for the same condition is a parity break.
|
||||
|
||||
| SID | signature | severity | classtype | key |
|
||||
|-----|-----------|----------|-----------|-----|
|
||||
| 100010 | `reverse_shell` | CRITICAL | `c2-reverse-shell` | `rsh:<pid>` |
|
||||
| 100011 | `ld_preload` | CRITICAL | `rootkit-preload` | `ldp:global`, `ldp:<pid>` |
|
||||
| 100012 | `deleted_exe` | CRITICAL | `fileless-execution` | `del:<pid>` |
|
||||
| 100013 | `new_listener` | HIGH | `backdoor-listener` | `lis:<port>/<comm>` |
|
||||
| 100014 | `new_suid` | CRITICAL / HIGH | `privilege-escalation` | `suid:<path>` |
|
||||
| 100015 | `persistence` | HIGH | `persistence` | `persist:<path>` |
|
||||
| 100016 | `egress` | HIGH | `c2-exfil` | `egr:<pid>:<host>` |
|
||||
| 100032 | `input_snooper` | HIGH | `credential-keylogging` | `input:<pid>:<target>` |
|
||||
| 100033 | `credential_access` | CRITICAL | `credential-theft` | `cred:<pid>:<target>` |
|
||||
| 100036 | `stealth_network` | MEDIUM / HIGH | `covert-channel` | `stealthnet:<kind>:<pid>:<local>:<peer>` |
|
||||
| 100039 | `memory_obfuscation` | HIGH / CRITICAL | `memory-obfuscation` | `mem:memory_obfuscation:<pid>:<lo>-<hi>` |
|
||||
| 100048 | `process_hiding_library` | CRITICAL | `process-hiding` | `mem:process_hiding_library:<pid>:<lo>-<hi>` |
|
||||
| 100049 | `process_injection_library` | HIGH | `process-injection` | `mem:process_injection_library:<pid>:<lo>-<hi>` |
|
||||
| 100074 | `first_public_destination` | MEDIUM | `network-rarity` | `firstdest:<comm>:<ip:port>` |
|
||||
| 100075 | `first_listener_port` | MEDIUM | `network-rarity` | `firstlisten:<comm>:<port>` |
|
||||
|
||||
`<lo>-<hi>` are the map bounds formatted as lowercase hex (`{start:x}-{end:x}`).
|
||||
|
||||
Allow-list config keys and their defaults live in `config.py`; this section
|
||||
names each key where used. The Go port mirrors the same defaults (see
|
||||
`go-agent/internal/config`), so the values are asserted by config tests, not
|
||||
repeated here.
|
||||
|
||||
---
|
||||
|
||||
## 2.1 Shared address helpers (`netutil.py`)
|
||||
|
||||
Used by `egress`, `stealth_network`, and `first_seen`.
|
||||
|
||||
- `split_host_port(endpoint)` — splits an `ss` endpoint. If it starts with `[`,
|
||||
partition on `]` (IPv6, e.g. `[::1]:22` → `::1`, `22`); else `rpartition(":")`
|
||||
(e.g. `10.0.0.2%eth0:443` → `10.0.0.2%eth0`, `443`). Returns `(host, port)`
|
||||
strings.
|
||||
- `parse_addr(addr)` — strips `[]`, drops an IPv6 `%zone`, returns an
|
||||
`ipaddress` object or `None`.
|
||||
- `is_public_ip(addr)` — `True` only if `parse_addr(...).is_global`: excludes
|
||||
loopback, RFC1918, link-local, CGNAT (100.64/10), and other non-global
|
||||
ranges. Unparseable → `False`.
|
||||
- `ip_in_cidrs(addr, cidrs)` — `True` if `addr` is inside any CIDR (the operator
|
||||
trust list); version-matched; bad CIDRs skipped.
|
||||
|
||||
## 2.2 Package provenance (`provenance.py`)
|
||||
|
||||
Used by `new_listener` (§2.11) as an optional suppressor.
|
||||
|
||||
`package_owner(path)` returns the owning package string or `None`, using the
|
||||
first available of `pacman -Qo` / `dpkg -S` / `rpm -qf` (5s timeout each, cached
|
||||
via `lru_cache`; `(deleted)` suffix stripped first). `is_package_owned(path) =
|
||||
package_owner(path) is not None`. **Contract:** ownership *ranks/explains*, it
|
||||
never proves safety — it may suppress a `new_listener` alert but is never used
|
||||
to silently drop anything else.
|
||||
|
||||
---
|
||||
|
||||
## 2.3 `reverse_shell` — SID 100010, CRITICAL
|
||||
|
||||
Interpreter process with a **network** socket on its stdio.
|
||||
|
||||
- Input: `state.processes`, `state.net_peer_by_inode`.
|
||||
- Match, per process: `proc.comm in cfg.interpreters`; `proc.stdio_socket_inode()`
|
||||
is not `None` (fd 0/1/2 links to `socket:[…]`); and that inode appears in the
|
||||
network socket table (`net_peer_by_inode`). The last condition is what
|
||||
excludes benign unix sockets / pipes and keeps false positives near zero.
|
||||
- Emit: key `rsh:<pid>`, `pids=(pid,)`, detail
|
||||
`pid=… comm=… stdio=net-socket peer=[local -> peer] cmd=[<cmdline first 90>]`.
|
||||
- Config: `interpreters` allow-*set of shell/scripting comms* (membership means
|
||||
"watch", not "trust").
|
||||
|
||||
## 2.4 `ld_preload` — SID 100011, CRITICAL
|
||||
|
||||
Two independent signatures, same SID:
|
||||
|
||||
1. **Global** — `/etc/ld.so.preload` exists and is non-empty (fail-open read).
|
||||
Key `ldp:global`, no pids, detail includes the file contents (newlines →
|
||||
spaces).
|
||||
2. **Per-process** — a process whose `environ["LD_PRELOAD"]` starts with a
|
||||
suspicious prefix: `("/tmp/", "/dev/shm/", "/var/tmp/", "/run/user/", "./")`.
|
||||
Key `ldp:<pid>`, `pids=(pid,)`.
|
||||
|
||||
Note the global check reads `/etc/ld.so.preload` directly (not the injected
|
||||
`state`), so a port must read it live inside the detector or supply it on state.
|
||||
|
||||
## 2.5 `deleted_exe` — SID 100012, CRITICAL
|
||||
|
||||
Process running from a deleted or memfd-backed binary in an attacker-controlled
|
||||
location.
|
||||
|
||||
- Match: `proc.exe` is fileless — `"memfd:" in exe`, **or** `"(deleted)" in exe`
|
||||
**and** `exe` starts with `("/tmp/", "/dev/shm/", "/var/tmp/", "/run/")`.
|
||||
Normal-path deleted exes (a daemon still running after a package upgrade) are
|
||||
ignored on purpose.
|
||||
- Emit: key `del:<pid>`, `pids=(pid,)`, detail `pid=… comm=… exe=[…]`.
|
||||
|
||||
## 2.6 `input_snooper` — SID 100032, HIGH
|
||||
|
||||
Process holding a keyboard/HID event device open (keylogger tell).
|
||||
|
||||
- Match, per process: skip if `comm in cfg.input_snooper_allow_comms`; then scan
|
||||
`fd_targets` for a target starting with `("/dev/input/event", "/dev/uinput",
|
||||
"/dev/hidraw")`. **First** matching fd emits and then `break` (at most one
|
||||
alert per process).
|
||||
- Emit: key `input:<pid>:<target>`, `pids=(pid,)`, detail
|
||||
`pid=… comm=… fd=… has input device open: <target>`.
|
||||
- Config: `input_snooper_allow_comms` (display servers, compositors, remappers).
|
||||
|
||||
## 2.7 `credential_access` — SID 100033, CRITICAL
|
||||
|
||||
Live process with a sensitive credential file open.
|
||||
|
||||
- Match, per process: skip if `comm in cfg.credential_access_allow_comms`; then
|
||||
per fd target (with a trailing ` (deleted)` stripped), classify via
|
||||
`_sensitive_kind`, first hit emits then `break`. Classification precedence:
|
||||
1. exact file in `{/etc/shadow, /etc/gshadow, /etc/security/opasswd}` →
|
||||
"system credential database";
|
||||
2. matches `cfg.credential_access_extra_paths` (entry ending `/` = prefix;
|
||||
else exact or `entry/` prefix) → "configured credential path";
|
||||
3. path contains `/.ssh/` and basename starts `id_` or ends `.pem`/`.key` →
|
||||
"private SSH key";
|
||||
4. path under `/etc/NetworkManager/system-connections/` → "NetworkManager
|
||||
secret profile";
|
||||
5. basename in `{logins.json, key4.db, "Login Data", Cookies}` **and** path
|
||||
(lowercased) contains a browser marker (`/.mozilla/`, `/firefox/`,
|
||||
`/chromium/`, `/google-chrome/`, `/chrome/`, `/brave`, `/vivaldi/`,
|
||||
`/edge/`) → "browser credential store".
|
||||
|
||||
Non-absolute paths are never sensitive.
|
||||
- Emit: key `cred:<pid>:<target>`, `pids=(pid,)`, detail
|
||||
`pid=… comm=… fd=… has <kind> open: <target>`.
|
||||
- Config: `credential_access_allow_comms`, `credential_access_extra_paths`.
|
||||
|
||||
## 2.8 `stealth_network` — SID 100036, MEDIUM or HIGH
|
||||
|
||||
Sockets in families attackers use to slide around TCP/UDP checks.
|
||||
|
||||
- Watched kinds: `{raw, sctp, dccp, packet, mptcp, tipc, xdp, vsock}` (matched
|
||||
on `sock.kind.lower()`).
|
||||
- Skip if: kind in `cfg.stealth_network_allow_kinds`; or `sock.comm` in
|
||||
`cfg.stealth_network_allow_comms`; or `sock.state` is non-empty and **not** in
|
||||
`{ESTAB, LISTEN, UNCONN, CONNECTED, SYN-SENT, SYN-RECV}`.
|
||||
- Severity: HIGH if kind in `{raw, packet, xdp}`; else HIGH if `state == LISTEN`
|
||||
or the peer is a public IP outside the egress trust list; else MEDIUM.
|
||||
- Emit: key `stealthnet:<kind>:<pid>:<local>:<peer>`, `pids=(pid,)` only when
|
||||
pid is known, detail `<kind> socket state=… local=… peer=… comm=<comm or ?>
|
||||
pid=<pid or ?>`.
|
||||
|
||||
## 2.9 `egress` — SID 100016, HIGH
|
||||
|
||||
Interpreter holding an outbound connection to a public IP.
|
||||
|
||||
- Match, per `established_sockets()`: `s.comm in cfg.interpreters`; peer host is
|
||||
public (`is_public_ip`); peer not in `cfg.egress_allow_cidrs`.
|
||||
- Emit: key `egr:<pid>:<host>`, `pids=(pid,)` when pid known, detail
|
||||
`pid=… comm=… -> <host>:<port> (interpreter to public IP)`.
|
||||
|
||||
## 2.10 `memory_obfuscation` — SIDs 100039 / 100048 / 100049
|
||||
|
||||
Inspects `/proc/<pid>/maps` shapes (never reads process memory). Per process,
|
||||
each map is classified by `_indicator`; **at most one alert per signature per
|
||||
process** (a per-process `seen` set). Precedence within a single map:
|
||||
|
||||
1. If `path` matches `cfg.memory_obfuscation_allow_paths` (prefix) → ignore.
|
||||
2. `path` basename contains a hide hint (`libhide`, `libprocesshide`,
|
||||
`process_hide`, `proc_hide`, `rootkit_hide`) → **`process_hiding_library`**,
|
||||
SID 100048, CRITICAL, classtype `process-hiding`.
|
||||
3. executable **and** library from a writable/runtime path (`/tmp/`, `/dev/shm/`,
|
||||
`/var/tmp/`, `/run/user/` with a `.so`/`.so.`/`.dylib` in the name) →
|
||||
**`process_injection_library`**, SID 100049, HIGH, classtype
|
||||
`process-injection`.
|
||||
4. executable **and** path contains `memfd:` or `(deleted)` →
|
||||
**`memory_obfuscation`**, SID 100039, CRITICAL.
|
||||
5. executable **and** writable (RWX) → `memory_obfuscation`, HIGH.
|
||||
6. executable **and** anonymous (`""`, `[heap]`, `[stack]`, `[anon…`,
|
||||
`[stack:…`) → `memory_obfuscation`, HIGH.
|
||||
|
||||
The `memory_obfuscation` signature (only) is additionally suppressed when
|
||||
`comm in cfg.memory_obfuscation_allow_comms` (JITs: java, node, browsers,
|
||||
dotnet, qemu, wine, wasmtime). The library signatures are **not** suppressed by
|
||||
that allow-list.
|
||||
|
||||
- Emit: key `mem:<signature>:<pid>:<lo>-<hi>`, `pids=(pid,)`, detail
|
||||
`pid=… comm=… <indicator detail> range=<lo>-<hi>`.
|
||||
|
||||
## 2.11 `new_listener` — SID 100013, HIGH
|
||||
|
||||
New listening socket absent from the startup baseline (see §1.5.1). No-op when
|
||||
`state.listener_baseline is None` (unarmed).
|
||||
|
||||
- Match, per `LISTEN` socket: identity `key = port/comm` where
|
||||
`port = local.rpartition(":")[2]` and `comm` defaults `?`. Skip if key in
|
||||
`state.listener_baseline`, or `port in cfg.listener_allow_ports`, or
|
||||
`comm in cfg.listener_allow_comms`.
|
||||
- Optional provenance gate: if `cfg.suppress_package_owned_listeners` and the
|
||||
owning process's `exe` `is_package_owned`, skip.
|
||||
- Emit: key `lis:<port>/<comm>`, `pids=(pid,)` when known, detail
|
||||
`new listening socket <local> by comm=<comm>`.
|
||||
|
||||
## 2.12 `new_suid` — SID 100014, HIGH or CRITICAL
|
||||
|
||||
SUID/SGID binary not in the baseline. No-op when `state.suid_binaries is None`
|
||||
**or** `state.suid_baseline is None` (see §1.5.3 — no alerts until an async scan
|
||||
result exists).
|
||||
|
||||
- Match, per path in `state.suid_binaries` not in `state.suid_baseline`:
|
||||
`hot = any(path.startswith(d.rstrip("/") + "/") for d in cfg.suid_hot_dirs)`.
|
||||
- Severity: CRITICAL if hot (writable dir), else HIGH.
|
||||
- Emit: key `suid:<path>`, no pids, detail
|
||||
`SUID/SGID binary in writable dir: <path>` (hot) or `new SUID/SGID binary:
|
||||
<path>`.
|
||||
|
||||
## 2.13 `persistence` — SID 100015, HIGH
|
||||
|
||||
Watched persistence-relevant file modified since the previous sweep. No-op when
|
||||
`state.persist_since is None` (unarmed).
|
||||
|
||||
- Input: walk `cfg.watch_persistence` (a dir → recurse `os.walk`, a file →
|
||||
itself). For each existing file, `mtime = os.lstat(path).st_mtime`
|
||||
(unreadable → skip).
|
||||
- Match: `mtime > state.persist_since` (the *previous* sweep's timestamp; §1.5.4).
|
||||
- Emit: key `persist:<path>`, no pids, detail `persistence file modified:
|
||||
<path>`.
|
||||
|
||||
## 2.14 `first_seen` — SIDs 100074 / 100075, MEDIUM
|
||||
|
||||
Network-rarity tracking with a self-owned persistent store. The store format,
|
||||
the `initialized` learn-once semantics, and the restart asymmetry are specified
|
||||
in §1.5.5 — not repeated here. Match summary:
|
||||
|
||||
- `first_public_destination` (100074): a `comm` reaches an `ESTAB` peer that is
|
||||
a public IP:port not previously recorded for it. Key
|
||||
`firstdest:<comm>:<ip:port>`, `pids=(pid,)` when known.
|
||||
- `first_listener_port` (100075): a `comm` opens a `LISTEN`/`UNCONN` tcp/udp
|
||||
socket on a numeric non-zero port not previously recorded for it. Key
|
||||
`firstlisten:<comm>:<port>`.
|
||||
|
||||
Both only alert when the store is already `initialized`; the recording of the
|
||||
new value happens regardless (so the alert fires once).
|
||||
|
||||
---
|
||||
|
||||
## 2.15 Port parity notes (Go port)
|
||||
|
||||
- The Go poll detectors for §2.3–§2.14 are **ported and pass the parity
|
||||
fixture** (`scripts/check-go-parity.py`), covering exact SID/severity/key/
|
||||
classtype/detail output. This document is the human-readable form of that
|
||||
fixture's contract.
|
||||
- `first_seen`/`new_listener`/`new_suid`/`persistence` receive injected baseline
|
||||
inputs in parity mode and use the opt-in live lifecycle described in §1 when
|
||||
`--state-dir` is supplied.
|
||||
- Watch two easily-missed rules when reviewing a port: the **first-match-then-
|
||||
break** behavior in `input_snooper`/`credential_access` (one alert per
|
||||
process), and the **per-signature `seen` dedup** in `memory_obfuscation` (one
|
||||
alert per signature per process, precedence-ordered).
|
||||
- `ld_preload`'s global check reads `/etc/ld.so.preload` **inside the detector**
|
||||
(or from an injected state field), not from the shared `SystemState` process
|
||||
view.
|
||||
147
docs/behavior/03-event-detection.md
Normal file
147
docs/behavior/03-event-detection.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<!-- 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.
|
||||
|
|
@ -38,8 +38,8 @@ Written in port-priority order. Status reflects which documents exist.
|
|||
| # | File | Covers | Status |
|
||||
|---|------|--------|--------|
|
||||
| 1 | [`01-sweep-and-state.md`](01-sweep-and-state.md) | `system.py` capture, `daemon.py` sweep loop + baseline lifecycle, `alert.py`, detector registry, `config.py` timing knobs | **written** |
|
||||
| 2 | `02-poll-detectors.md` | the twelve `detectors/*` — per-detector input → match → output contract | planned |
|
||||
| 3 | `03-event-detection.md` | `event.py`, exec/syscall/host rule engines, `ruleops.py`, `sids.py` | planned |
|
||||
| 2 | [`02-poll-detectors.md`](02-poll-detectors.md) | the twelve `detectors/*` — per-detector input → match → output contract | **written** |
|
||||
| 3 | [`03-event-detection.md`](03-event-detection.md) | `event.py`, exec/syscall/host rule engines, `ruleops.py`, `sids.py` | **written** |
|
||||
| 4 | `04-integrity.md` | `fim.py`, `pkgdb.py`, `selfprotect.py`, `assurance.py` | planned |
|
||||
| 5 | `05-rootcheck.md` | `rootcheck.py` anti-rootkit cross-view checks | planned |
|
||||
| 6 | `06-posture.md` | `posture.py` advisory host-hygiene checks | planned |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue