Poll-based host intrusion-detection daemon modeled on freeze-watcher's sample→trigger→snapshot→classify loop, with security signatures instead of performance ones: - reverse_shell (interpreter with a network socket on stdio) - ld_preload (/etc/ld.so.preload or LD_PRELOAD in writable dirs) - deleted_exe (process running from a deleted/memfd binary) - new_listener (listening port absent from startup baseline) - new_suid (new SUID/SGID binary; critical in writable dirs) - persistence (cron/systemd/authorized_keys/rc-file changes) - suspicious_egress (interpreter holding an outbound public connection) Each capture writes a forensic snapshot with per-signature incident- response guidance. Includes a safe, self-cleaning red-team harness (sentinel-redteam), hardened systemd unit, config, Makefile, and Arch packaging. Tested on Arch: detectors fire on drills, no false positives on a clean sweep, sweep cost optimized from ~15s to ~1s via batched syscalls and a gated filesystem-wide SUID scan. This bash implementation is retained as the regression oracle for the forthcoming Python rewrite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| config | ||
| packaging | ||
| src | ||
| systemd | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
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
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)
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
bashwhose 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/sleeprun from/tmp, then deleted - new_listener — an ephemeral
nc -lon loopback - new_suid — a
chmod 4755copy of/bin/truein/tmp - persistence — writes to a sandbox
authorized_keys(only fires if you opt the sandbox intoWATCH_PERSISTENCE; it refuses to touch your real dotfiles)
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_exeonly fires for/tmp,/dev/shm,/run,/var/tmp, ormemfd: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_egressif you add them toINTERPRETERS; the default list is deliberately scripting-focused. UseEGRESS_ALLOW_CIDRSfor 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:
- bpftrace tracepoints (now, optional) —
CAPTURE_EXECVE_BPFTRACE=1adds a liveexecvetrace to each snapshot. The gentle on-ramp to kernel tracing. - Event-driven execve/connect detection — replace the polling gap with
bpftraceprobes onsys_enter_execve,security_bprm_check, andtcp_connect, streamed to the daemon so short-lived reverse shells can't slip between sweeps. - 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