# Enodia Sentinel A host intrusion-detection daemon for Linux. It continuously runs a set of detectors over live system state — processes, sockets, file descriptors, the SUID inventory, and sensitive files — and writes a detailed forensic snapshot with incident-response guidance the moment a known attack signature appears. Think of it as the security counterpart to a performance watchdog: instead of "I/O pressure spiked, here's the kernel state," it's *"a shell just wired itself to a socket — here's the process tree, the peer, and what to do about it."* > **Design lineage.** Sentinel reuses the proven control loop of a performance > anomaly-capture daemon (sample → threshold/trigger → rich snapshot → classify > → notify/retain) and swaps the inputs and signatures from *performance* to > *security*. The forensic-snapshot-on-trigger pattern is the same; the > detectors and the response guidance are new. ## Why these detectors Every detector keys on a behavior that is **cheap to observe** and **expensive for an attacker to avoid** — the high-signal, low-false-positive heuristics that real EDRs are built on: | Signature | What it catches | Why it's hard to evade | |---|---|---| | `reverse_shell` | A shell/interpreter with a **socket on fd 0/1/2** | Interactive shells get a pty, not a socket — a socket on stdio is almost always `nc -e` / `bash -i >& /dev/tcp/...` | | `ld_preload_global` | A non-empty `/etc/ld.so.preload` | Injecting into *every* process is the whole point of a userland rootkit; the file has to exist | | `ld_preload_proc` | `LD_PRELOAD` pointing into `/tmp`, `/dev/shm`, … | Hooking libc to hide files/creds needs the library somewhere writable | | `deleted_exe` | A process running from a **deleted / `memfd:`** binary | Fileless malware deletes its dropper but the kernel still names the inode `(deleted)` | | `new_listener` | A listening port absent from the startup baseline | Bind shells and backdoors have to listen somewhere | | `new_suid` | A new SUID/SGID binary (critical in writable dirs) | A SUID `/tmp` binary is a textbook privesc persistence trick | | `persistence` | Changes to cron, systemd units, `authorized_keys`, rc files | Persistence has to write *somewhere* that survives reboot | | `suspicious_egress` | An interpreter holding an outbound conn to a public IP | C2 beacons and exfil need to phone home | ## The control loop ``` ┌────────────────────────────────────────────┐ │ every SAMPLE_INTERVAL seconds │ │ │ │ run_detectors() ──► alert lines │ │ │ SEV signature detail │ │ ▼ │ │ cooldown dedup (per signature+key) │ │ │ │ │ ▼ (fresh alerts only) │ │ capture_snapshot() │ │ • flagged-pid deep dive (/proc) │ │ • full process tree + sockets │ │ • ld.so.preload, recent file mods │ │ • logins, failed auth, kernel modules │ │ • per-signature response guidance │ │ │ │ │ ▼ │ │ events.log + alert-*.log + desktop notify │ └────────────────────────────────────────────┘ ``` Output lives in `/var/log/enodia-sentinel/`: - `events.log` — one line per alert (timestamp, severity, signatures, file) - `alert-YYYYMMDD-HHMMSS.log` — the full forensic snapshot ## Quick start ```bash sudo make install sudo make enable # start + enable the systemd service # prove it works — in one terminal: sudo tail -f /var/log/enodia-sentinel/events.log # in another: sentinel-redteam # safe, self-cleaning attack simulations ``` You'll watch the drills trip `reverse_shell`, `ld_preload_proc`, `deleted_exe`, `new_listener`, and `new_suid` in real time, each producing a snapshot with response guidance. ### Without installing (try it in place) ```bash sudo ./src/sentinel.sh --baseline # establish listener/SUID baselines sudo ./src/sentinel.sh --check # run every detector once, print findings ``` ## The red-team harness `sentinel-redteam` is the demo and the regression test in one. It simulates each threat with **safe, clearly-labeled stand-ins** (everything tagged `enodia-drill`, auto-cleaned on exit): - **reverse_shell** — a `bash` whose stdio is a TCP socket to a *local* listener (no traffic leaves the box) - **ld_preload** — a process with `LD_PRELOAD=/tmp/...` (an empty file; never actually loaded, and it does **not** touch the system-wide `/etc/ld.so.preload`) - **deleted_exe** — a copy of `/bin/sleep` run from `/tmp`, then deleted - **new_listener** — an ephemeral `nc -l` on loopback - **new_suid** — a `chmod 4755` copy of `/bin/true` in `/tmp` - **persistence** — writes to a sandbox `authorized_keys` (only fires if you opt the sandbox into `WATCH_PERSISTENCE`; it refuses to touch your real dotfiles) ```bash sentinel-redteam --list # list drills sentinel-redteam reverse_shell new_suid # run specific ones HOLD=30 sentinel-redteam # keep artifacts alive 30s ``` ## Configuration Edit `/etc/enodia-sentinel.conf`, then `sudo systemctl restart enodia-sentinel.service`. Key options: | Variable | Default | Purpose | |---|---|---| | `SAMPLE_INTERVAL` | 4 | seconds between detector sweeps | | `COOLDOWN` | 60 | min seconds before re-alerting the same signature | | `DET_*` | 1 | per-detector on/off switches | | `INTERPRETERS` | bash sh … | process names treated as shells for rsh/egress | | `EGRESS_ALLOW_CIDRS` | "" | trusted public ranges that won't trip egress | | `LISTENER_ALLOW_PORTS` | "" | ports that never trip `new_listener` | | `SUID_HOT_DIRS` | /tmp /dev/shm … | dirs where SUID = CRITICAL | | `WATCH_PERSISTENCE` | cron/systemd/… | files watched for tampering | | `CAPTURE_EXECVE_BPFTRACE` | 0 | add a bpftrace execve trace to snapshots | | `MAX_SNAPSHOTS` / `MAX_SNAPSHOT_AGE_DAYS` | 300 / 60 | retention | | `NOTIFY_USERS` | "" | desktop notify-send targets | ## Tuning out false positives - A long-running daemon legitimately running a deleted exe after a package upgrade won't alert — `deleted_exe` only fires for `/tmp`, `/dev/shm`, `/run`, `/var/tmp`, or `memfd:` paths. - Your own admin shells over SSH won't trip `reverse_shell` — they're on a pty. - Browsers and chat apps that interpreters open to public IPs *can* trip `suspicious_egress` if you add them to `INTERPRETERS`; the default list is deliberately scripting-focused. Use `EGRESS_ALLOW_CIDRS` for known-good ranges. ## Security model Sentinel runs as root because it must read every process's `/proc`, the full socket table, and root-owned files like `authorized_keys`. The systemd unit constrains that power hard: `ProtectSystem=strict` with the log dir as the only writable path, `ProtectHome=read-only`, `NoNewPrivileges`, `MemoryDenyWriteExecute`, and a minimal capability set (`CAP_SYS_PTRACE`, `CAP_DAC_READ_SEARCH`). It only ever **reads** the system and **writes** to its own log directory. ## Roadmap — from polling to eBPF This v0 is **poll-based**: it sweeps `/proc` and `ss` every few seconds. That's robust, dependency-light, and catches anything that lingers — but it can miss sub-second processes, and a sweep is observable. The planned evolution: 1. **bpftrace tracepoints (now, optional)** — `CAPTURE_EXECVE_BPFTRACE=1` adds a live `execve` trace to each snapshot. The gentle on-ramp to kernel tracing. 2. **Event-driven execve/connect detection** — replace the polling gap with `bpftrace` probes on `sys_enter_execve`, `security_bprm_check`, and `tcp_connect`, streamed to the daemon so short-lived reverse shells can't slip between sweeps. 3. **A libbpf + CO-RE agent (Go or Rust userland)** — the real EDR core: LSM hooks, ring-buffer event streaming, per-process lineage tracking, and tamper-resistance. This is the production-grade rewrite the polling daemon prototypes the detection logic for. The polling daemon isn't throwaway — it's the **oracle**: every signature here is a test case the eBPF agent must reproduce, and `sentinel-redteam` is the shared regression suite for both. ## Project status v0.1 — working poll-based detection across 7 signatures, forensic snapshots, red-team harness, systemd packaging. Built and tested on Arch Linux. ## License MIT