A host intrusion-detection daemon for Linux. Think of it as a watchdog for security and host stability purposes. https://enodiainformatics.com
Find a file
Luna 5e560f4307 Add tray applet implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 05:49:06 -07:00
config Add baseline reconciliation: accept audited drift with a reason 2026-06-27 10:33:55 -07:00
docs Add tray applet implementation plan 2026-06-30 05:49:06 -07:00
enodia_sentinel Surface reconciliation in the dashboard and keep the view read-only 2026-06-27 10:39:15 -07:00
packaging Document packaging and release artifacts 2026-06-17 00:22:43 -07:00
src Add egress drill + sid-verified harness and IR runbooks (roadmap v0.8) 2026-06-10 05:28:53 -07:00
systemd Frame Sentinel as IDS IPS and EDR 2026-06-13 05:59:03 -07:00
tests Make release-artifacts dirty assertion track real git state 2026-06-27 10:47:31 -07:00
.gitignore Re-architect Sentinel as a zero-dependency Python package 2026-05-31 01:50:50 -07:00
CLAUDE.md Add baseline reconciliation: accept audited drift with a reason 2026-06-27 10:33:55 -07:00
LICENSE Relicense under GPL-3.0-or-later 2026-05-31 06:52:34 -07:00
Makefile Document packaging and release artifacts 2026-06-17 00:22:43 -07:00
pyproject.toml Frame Sentinel as IDS IPS and EDR 2026-06-13 05:59:03 -07:00
README.md Add baseline reconciliation: accept audited drift with a reason 2026-06-27 10:33:55 -07:00

Enodia Sentinel

A Linux IDS/IPS/EDR platform: intrusion detection, explicit prevention and containment workflows, endpoint detection and response, file/package integrity, anti-rootkit cross-checks, tamper-evidence, forensic snapshots, push alerts, and a read-only operator console.

The current daemon continuously runs detectors over live system state — processes, sockets, file descriptors, the SUID inventory, sensitive files, the package database, and rootkit-hiding artifacts — then writes a detailed forensic snapshot (text and JSON) 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, what changed on disk, whether the package database was tampered with, and what to do next."

Product direction

Enodia Sentinel is an IDS, IPS, and EDR system. IDS-style signatures identify suspicious host behavior, IPS workflows turn those findings into explicit containment/prevention plans, and EDR features preserve evidence for triage, investigation, and recovery. Automatic inline blocking is intentionally gated behind reviewed response workflows rather than silent remediation.

Function Current capability Direction
Detect Poll detectors + eBPF exec/syscall rules More event sources and correlation
Prevent Posture checks + dry-run containment plans Audited, explicit --apply workflows
Verify FIM, package DB anchor, signed-package checks External anchors and signed evidence
Investigate Text/JSON snapshots, incidents, dashboard, triage Richer timelines and evidence export
Respond Persisted dry-run response plans Audited containment and recovery execution
Assure Heartbeat, watchdog, self-integrity, rootcheck Fleet health and attestation-ready anchors

Project docs:

  • Documentation index — versioned entry point for operator, product, schema, and roadmap documents.
  • Specification — product model, current scope, target platform shape, data model, and acceptance criteria.
  • Roadmap — phased work from local sensor to incident, response, fleet, and assurance layers.
  • Operations guide — install checks, health checks, alert workflow, baseline hygiene, and evidence export.
  • Packaging guide — Arch package layout plus Debian/RPM source-install paths.
  • JSON schemas — stable v1 contracts for alerts, incidents, status, response plans, response audit records, and reconciliation acks.
  • IR runbooks — confirm/preserve/contain/recover playbooks per alert: reverse shells, persistence, trojaned binaries, rootkits, tampering.
  • Rule reference — generated event-rule documentation: SID, signature, classtype, match fields, expected false positives, and drills.

Two implementations, on purpose. The project began as a bash prototype (src/sentinel.sh, kept as the regression oracle) and was re-architected into a zero-dependency Python package with a unit-test suite, structured detectors, and JSON output. The bash version and the Python version share one red-team harness, so every signature is exercised against both.

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 real EDRs are built on:

Signature What it catches Why it's hard to evade
reverse_shell An interpreter with a network socket on fd 0/1/2 Interactive shells get a pty and daemons get unix sockets — a network socket on stdio is nc -e / bash -i >& /dev/tcp/...
ld_preload Non-empty /etc/ld.so.preload, or LD_PRELOAD into a writable dir Injecting into processes needs the library to exist somewhere
deleted_exe A process running from a deleted / memfd: binary Fileless malware deletes its dropper; the kernel still names the inode (deleted)
input_snooper A non-allowlisted process holding /dev/input, /dev/uinput, or HID raw devices open Keyloggers have to read keystroke/event devices somewhere; expected compositors/remappers are tunable
credential_access A non-allowlisted process with shadow files, private SSH keys, browser stores, or secret profiles open Credential harvesters have to open the material they steal; legitimate auth/keyring/browser readers are tunable
stealth_network Raw, SCTP, DCCP, packet, MPTCP, TIPC, XDP, or vsock activity Covert channels often avoid ordinary TCP/UDP paths; expected network managers/sniffers are tunable
memory_obfuscation Executable anonymous/memfd/deleted mappings, RWX pages, or mapped process-hiding libraries Encrypted/packed payloads still need executable memory after decrypting; hide libraries must be mapped to hook tools
new_listener A listening port absent from the startup baseline Bind shells/backdoors have to listen somewhere
new_suid A new SUID/SGID binary (critical in a writable dir) A SUID /tmp binary is a textbook privesc trick
persistence Changes to cron, systemd units, authorized_keys, rc files Persistence has to write somewhere that survives reboot
egress An interpreter with an established connection to a public IP C2 beacons and exfil have to phone home

Every detection carries a stable sid and a classtype (à la Snort/Suricata), so it can be referenced, tuned, and tracked across revisions.

Event-driven detection (eBPF + a Snort-style rule engine)

Polling has a blind spot: a process that runs and exits between two sweeps is invisible to it. The event layer closes that gap. An eBPF probe (loaded with bcc) fires on every execve and hands each event to a declarative rule engine — the host-event analogue of Snort matching packets:

# a rule is data, not code — sid, msg, classtype, severity + conditions
sid        = 100002
msg        = "Reverse-shell command pattern in execve arguments"
severity   = "CRITICAL"
classtype  = "c2-reverse-shell"
argv_regex = "/dev/(tcp|udp)/| -i\\b| -e\\b| pty\\.spawn"

Shipped exec rules cover fileless execution from world-writable dirs (sid 100001), reverse-shell argv patterns (100002), web/DB services spawning a shell — webshell/RCE (100003), and curl|sh-style ingress tool transfer (100004). The syscall stream adds short-lived memory/anti-analysis coverage for RWX mprotect/mmap (100060/100061), memfd_create (100062), sensitive ptrace (100063), seccomp hardening (100064), cross-process memory access (100065), and memory locking (100066). Operators add custom exec rules via exec_rules_file without touching code. They can inspect and test the active event rule set with:

enodia-sentinel rules list
enodia-sentinel rules show 100002
enodia-sentinel rules test event.json
enodia-sentinel rules docs

The layer is fail-safe: if bcc/root/BTF aren't available it logs the reason and the daemon runs poll-only — a broken probe can never take detection down. Lineage: the rule-driven engine + SIDs come from Snort; the host-IDS framing (and the queued FIM / hidden-process checks) from OSSEC.

Architecture

enodia_sentinel/
├── cli.py            run / check / baseline / rules / list-detectors
├── daemon.py         sweep loop · cooldown dedup · backgrounded SUID scan
├── system.py         SystemState — one cached snapshot of /proc + ss per sweep
├── snapshot.py       forensic text+JSON capture · response guidance · retention
├── config.py         dataclass config (TOML + env overrides)
├── netutil.py        public-IP / CIDR logic (stdlib ipaddress)
├── alert.py          Alert / Severity (with Snort-style sid + classtype)
├── web.py            HTTPS read-only management console + JSON API + auth
├── static/           the self-contained dashboard SPA
├── fim.py            file integrity monitoring (hash baseline + pacman verify)
├── pkgdb.py          package-DB integrity + signed-package verification
├── rootcheck.py      anti-rootkit cross-view, module names, kernel/module taint
├── selfprotect.py    self-integrity footprint + dead-man's-switch heartbeat
├── triage.py         false-positive classification
├── provenance.py     package-ownership lookups (pacman/dpkg/rpm)
├── detectors/        poll detectors — one module per signature, each a pure
│                       function: detect(state, cfg) -> Iterable[Alert]
├── notify/           outbound push — ntfy / Pushover / webhook backends
└── events/           event-driven layer (eBPF)
    ├── bcc_source.py   real eBPF execve probe loaded via bcc
    ├── bcc_syscall_source.py  real eBPF syscall telemetry probe
    ├── exec_event.py   the ExecEvent type
    ├── syscall_event.py  the SyscallEvent type
    ├── rules.py        Snort-style ExecRule engine + default rules
    ├── syscall_rules.py  memory/anti-analysis SyscallRule engine
    └── monitor.py      runs the probe on a thread, routes events → rules

Three complementary detection paths feed one Alert → snapshot pipeline:

  • Poll — every few seconds, sweep /proc/ss (catches anything lingering).
  • Event — eBPF fires on every execve, matched against the rule engine (catches processes that exit between sweeps).
  • Syscall event — optional eBPF telemetry catches short-lived memory and anti-analysis actions such as RWX mappings, memfd_create, ptrace, seccomp, cross-process memory access, and memory locking.

The loop is deliberately the same control flow as the bash prototype, but the state lives in real objects:

every sample_interval seconds:
    state  = SystemState()                 # /proc + ss gathered once, cached
    alerts = run_all(detectors, state, cfg)
    fresh  = drop alerts still within cooldown
    if fresh:
        snapshot.capture(fresh)            # on a worker thread

Two design choices keep it fast and unobtrusive:

  • One SystemState per sweep. Detectors read shared, cached /proc/ss data instead of each shelling out — a sweep costs ~200 ms regardless of how many detectors run.
  • The filesystem-wide SUID scan runs off the loop thread on a slow cadence, so the multi-second walk never stalls live detection.

Everything in SystemState is injectable, which is what makes the detectors unit-testable without root or a live system (see tests/).

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 the core poll signatures in real time, each producing a .log + .json snapshot with response guidance.

Output lives in /var/log/enodia-sentinel/:

  • events.log — one line per alert
  • alert-YYYYMMDD-HHMMSS.log — human-readable forensic snapshot
  • alert-YYYYMMDD-HHMMSS.json — same data, structured (SIEM-ready)

Without installing

make test                                   # run the unit suite
python3 -m enodia_sentinel.cli baseline     # establish baselines
python3 -m enodia_sentinel.cli check        # run every detector once, print findings

No pip, no virtualenv, no dependencies — it's stdlib-only and installs as a plain package directory plus a launcher wrapper.

The red-team harness

sentinel-redteam is the demo and the integration test in one. It simulates each threat with safe, clearly-labeled stand-ins (everything tagged enodia-drill, auto-cleaned on exit), using a local Python TCP listener so no traffic ever leaves the host:

sentinel-redteam --list                  # list drills
sentinel-redteam reverse_shell new_suid  # run specific ones
HOLD=30 sentinel-redteam                  # keep artifacts alive 30s

It never touches your real dotfiles or /etc/ld.so.preload; the LD_PRELOAD drill only sets the env var on a throwaway process.

Testing

make test          # stdlib unittest suite, no runtime deps

Detectors are pure functions over an injectable SystemState, so tests build fake processes/sockets and assert on the alerts — no root, no /proc, no ss:

proc  = FakeProc(pid=100, comm="bash", _stdio_inode=999)
sock  = Socket("ESTAB", "127.0.0.1:55", "9.9.9.9:443", 999, "bash", 100)
state = SystemState(processes=[proc], sockets=[sock])
assert list(reverse_shell.detect(state, Config()))[0].signature == "reverse_shell"

Configuration

Edit /etc/enodia-sentinel.toml, then sudo systemctl restart enodia-sentinel.service. Every key is optional. Highlights:

Key Default Purpose
sample_interval 4 seconds between sweeps
cooldown 60 min seconds before re-alerting a signature
detectors all 11 the enabled detector list
interpreters bash sh … process names treated as shells
egress_allow_cidrs [] trusted public ranges (won't trip egress)
input_snooper_allow_comms desktop input stack comm names allowed to hold input devices
credential_access_allow_comms auth/keyring/browsers comm names allowed to read credential stores
credential_access_extra_paths [] extra exact paths or directory prefixes treated as secrets
stealth_network_allow_comms network managers/sniffers comm names allowed to own special protocol sockets
stealth_network_allow_kinds [] socket families to ignore entirely
memory_obfuscation_allow_comms JIT runtimes/browsers comm names allowed to own JIT-like executable anonymous mappings
memory_obfuscation_allow_paths [] mapped path prefixes allowed for suspicious map shapes
suid_hot_dirs /tmp … dirs where a SUID binary is CRITICAL
suid_scan_extra_dirs /tmp … writable mounts always scanned (tmpfs-safe)
capture_execve_bpftrace false add a bpftrace execve trace to snapshots
ebpf_exec_monitor true optional execve event monitor
ebpf_syscall_monitor true optional memory/anti-analysis syscall monitor
notify_users [] desktop notify-send targets
pkgdb_pkgverify false verify on-disk files against signed cache packages
pkgdb_pkgverify_sample 40 packages verified per pass (rotates over time)
rootcheck_enabled true run the anti-rootkit cross-view sweep
rootcheck_interval 300 seconds between cross-view sweeps

Web dashboard

A read-only HTTPS management console, served by the stdlib http.server (no Flask, no JS framework, no CDN — one self-contained page):

enodia-sentinel web                      # serves on the Tailscale IP by default
# or as a service:
sudo systemctl enable --now enodia-sentinel-web
  • Bound to your Tailscale interface by default (auto-detected), so it's reachable from your phone/laptop on the tailnet but not the LAN or internet.
  • TLS is mandatory. If web_tls_cert / web_tls_key are unset, Sentinel auto-generates a self-signed certificate under log_dir. Add a browser exception for now, or point config at a local/private CA certificate.
  • Bearer-token auth (constant-time check); the token is auto-generated and saved on first run and printed in the startup line. Open https://<tailscale-ip>:8787/?token=….
  • Read-only management: incidents, timelines, alert inventory, posture findings, integrity/watchdog state, event rule atlas, event tail, and dry-run response plans. No commands are executed from the browser. JSON API at /api/status, /api/incidents, /api/respond/plan/<id>, /api/posture, /api/integrity, /api/rules, /api/alerts, /api/alerts/<id>, /api/events.
  • Operator settings: the self-contained dashboard includes a Settings menu with persistent local themes: Console, Paper, Contrast, LGBTQ, Trans, Dracula, Solarized Dark, Solarized Light, and Twilight. Static dashboard tests guard the theme registry and core text/status color contrast across every palette.

CLI-generated response plans are saved for handoff/review under <log_dir>/response-plans/, with a JSONL trail in <log_dir>/response-audit.log. Dashboard plan previews stay read-only and do not create artifacts.

Phone push notifications

When an alert at/above notify_min_severity fires, Sentinel pushes to whichever backends you've configured (all via stdlib urllib, no SDKs):

Backend Enable by setting Notes
ntfy notify_ntfy_url + notify_ntfy_topic open-source, self-hostable, free apps
Pushover notify_pushover_token + _user polished, reliable
Webhook notify_webhook_url generic JSON POST (Discord/Slack/your own)

Severity maps to each service's priority (a CRITICAL is an urgent ntfy push / a high-priority Pushover). Sends happen on worker threads and swallow their own errors — a flaky notifier never stalls detection.

notify_min_severity = "HIGH"
notify_ntfy_url   = "https://ntfy.sh"
notify_ntfy_topic = "enodia-7Hq2x"      # keep this secret — it's the access control

Tamper-evidence & self-protection

An attacker's first move against a sensor is to disable or blind it — and on a box where they have root, any purely-local defense is ultimately defeatable (they share your privileges). Sentinel doesn't pretend otherwise. The goal is to make tampering evident by anchoring trust where the attacker has less control, and to make going silent loud.

Package-DB integrity. pacman -Qkk trusts the local package database — so a root attacker can modify /usr/bin/ssh and rewrite its stored checksum, and verification passes. Sentinel guards the DB itself: a legitimate change only happens during a logged transaction, so it anchors a fingerprint of /var/lib/pacman/local (refreshed only by the pacman hook) and cross-checks pacman.log. A DB change with no corresponding transaction is flagged pkgdb_tamper (CRITICAL, sid 100021) — directly catching the "overwrite the hashes" attack.

Self-integrity. Sentinel's own binaries, config, systemd units, and pacman hook are always in the FIM watch set, so tampering with the watchdog trips the watchdog.

Dead-man's switch. The daemon writes a heartbeat every loop; the dashboard exposes its age. Run an external watcher on another host (over Tailscale) and silence becomes the alarm:

# on a SEPARATE machine — alerts you if the sensor or the whole box goes dark
enodia-sentinel watchdog --url https://100.x.x.x:8787 --token <T> --max-age 120 --insecure-tls

Hardening (the layers beyond on-box detection):

  1. Immutabilitychattr +i Sentinel's binaries, config, baselines, and the pacman hook so even root must visibly clear the flag first.
  2. Cryptographic anchor (done) — verify on-disk files against the signed package in the cache rather than the mutable DB; a maintainer's PGP signature is a root of trust the attacker doesn't hold. See Signed-package verification below.
  3. External anchor (next) — mirror the DB/FIM fingerprints off-box so a local attacker can't refresh the anchor to cover their tracks.

On "anti-rootkit": the robust version isn't stealth (fragile, and a genuine dual-use rootkit technique) — it's tamper-evidence. Don't hide; be un-hideable. The durable defenses move the trust anchor below or outside the attacker: signatures, external monitors, and ultimately the kernel (the eBPF roadmap) rather than userland the attacker can rewrite.

Signed-package verification (the independent anchor)

Layer 1 (above) catches an out-of-band DB edit. But a root attacker can do the full job: modify /usr/bin/sshd, rewrite its hash in the local DB, and run fim-update to re-anchor — defeating both pacman -Qkk and the DB-fingerprint check, because every reference they're checked against is one they can rewrite.

The one reference they can't forge is the distro's signing key. Packages in the cache are signed, and each carries a .MTREE manifest of per-file SHA-256 hashes. Layer 2 extracts that manifest from the cached package and compares the on-disk files to it — a reference independent of the local DB:

enodia-sentinel pkgdb-verify              # verify a rotating sample of packages
enodia-sentinel pkgdb-verify --sample 200 # verify more per run

A divergence is pkg_signature_mismatch (CRITICAL, sid 100027) — a trojaned binary that a rewritten checksum DB would have hidden. It also flags a SigLevel downgrade in pacman.conf (pacman_siglevel_disabled, CRITICAL, sid 100026), since disabling signature checking is how an attacker would slip an unsigned package past the anchor in the first place.

Verifying every package each pass is expensive (untar + hash every file), so it runs on its own slow cadence (pkgdb_pkgverify_interval) and checks a rotating sample (pkgdb_pkgverify_sample) per pass — over enough passes the whole installed set is covered, and any single mismatch fires immediately. It's off by default (needs the package cache populated); enable with pkgdb_pkgverify = true.

Anti-rootkit (cross-view detection)

A rootkit hides by lying to one view of the system — but the technique that hides it is also how you catch it: ask the same question two different ways and compare the answers. A discrepancy is the hiding artifact.

Cross-check Hidden thing it surfaces sid
kill(pid, 0) for every PID vs the /proc listing a process the kernel schedules but /proc omits 100022
/proc process listing vs ps -e a process visible to the kernel but hidden from normal process tools 100038
/sys/module (initstate=live) vs /proc/modules a loaded LKM hidden from the module list 100023
/proc/net/tcp vs ss a listening port a hooked ss won't report 100024
/sys/class/net/*/flags an interface in promiscuous mode (a sniffer) 100025
known LKM rootkit module names Diamorphine/Reptile/Adore-style module artifacts 100028
/sys/module/*/taint out-of-tree/proprietary/unsigned loaded modules needing review 100029
/proc/sys/kernel/tainted global kernel taint: forced loads/unloads, unsigned modules, warnings 100030
/proc/net/udp vs ss -u a UDP socket a hooked ss won't report 100031
/proc/net/raw vs ss -w a raw socket protocol a hooked ss won't report 100034
/proc/net/raw ICMP protocols persistent raw ICMP sockets used by knockers/sniffers 100035
/proc/net special families vs ss SCTP/DCCP/packet/TIPC/XDP sockets a hooked ss won't report 100037
enodia-sentinel rootcheck     # one-shot cross-view scan

The daemon runs it on a slow background cadence (rootcheck_interval) and routes any finding into the normal alert/snapshot/push pipeline. If you intentionally run tainted vendor/DKMS modules, add exact names to rootcheck_module_allow. Known rootkit module names are never suppressed by that allowlist.

Honest limits: this runs in user space, so it reliably catches userland (LD_PRELOAD) rootkits and the common /proc-hiding LKMs, but a kernel rootkit that hooks every path consistently can still evade it. It raises the bar and catches the common cases; it is not a guarantee against a bespoke ring-0 implant — pair it with the off-box dead-man's switch. (Lineage: OSSEC's rootcheck / chkrootkit's cross-view idea.)

File integrity monitoring (Tripwire-style)

Detects tampering with binaries and critical configs by content hash, so it catches a malicious swap even when the timestamp is preserved (the weakness of mtime-based checks). Two engines, split by who owns the file:

  • Package verificationpacman -Qkk checks package-owned binaries against the distro's own signed checksums. No baseline to maintain, and it's implicitly current because the package DB updates on every pacman -Syu. A trojaned /usr/bin/ssh surfaces as a checksum mismatch (CRITICAL, sid 100020).
  • Hash baseline — a SHA-256 baseline of the files pacman doesn't track (/usr/local, /etc configs, systemd units, SSH keys). A pacman PostTransaction hook runs fim-update after every upgrade, so legitimate package changes never alert — no manual tripwire --update ritual.
enodia-sentinel fim-baseline          # establish the baseline
enodia-sentinel fim-check             # report changes vs baseline
enodia-sentinel fim-check --packages  # also verify package-owned files

The baseline is only refreshed by fim-update / the pacman hook — a flagged change stays flagged until you acknowledge it, exactly like Tripwire. The daemon runs the scan on a slow background cadence (fim_scan_interval) and routes any change into the normal alert/snapshot/push pipeline (fim_modified sid 100017, fim_added 100018, fim_removed 100019).

False positives & triage

An EDR that cries wolf gets ignored, so Sentinel ships explicit tooling to separate benign noise from real findings — built on provenance: a binary shipped by your package manager (pacman/dpkg/rpm) is overwhelmingly likely to be legitimate (the same idea behind OSSEC's rootcheck and AIDE).

enodia-sentinel triage     # classify captured alerts, suggest allowlist entries
12 distinct detections — 11 likely false-positive, 1 to review.

[FP  ] new_listener   x99  listener binary is package-owned (qbittorrent)
[FP  ] new_listener   x1   loopback-only listener (not externally reachable)
[REVIEW] new_listener x1   unrecognized listener *:1740 (?)
...
To suppress the false positives, add to your config:
  # listener_allow_comms += "qbittorrent"

Triage is deliberately conservative: reverse_shell, egress, and the eBPF exec rules are always flagged review (provenance can't clear a network shell), and any listener it can't attribute to a process is reviewed rather than cleared. Suppression is never automatic — you choose what to allowlist.

Knobs to quiet known-good activity:

Config Effect
listener_allow_comms never alert on listeners owned by these apps
listener_allow_ports never alert on these ports
suppress_package_owned_listeners drop new_listener when the binary is package-owned (best single knob for a desktop/seedbox)
egress_allow_cidrs trusted public ranges for the egress detector
suid_hot_dirs / exec_rules_file tune SUID criticality / add custom exec rules

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: ProtectSystem=strict with the log dir as the only writable path, ProtectHome=read-only, NoNewPrivileges, MemoryDenyWriteExecute, RestrictNamespaces, 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.

Enabling the eBPF monitor

The event layer needs python-bpfcc and privileges the hardened unit deliberately withholds (bcc JIT-compiles its programs, so it needs write+exec memory and CAP_BPF/CAP_PERFMON/CAP_SYS_ADMIN). Under the default unit the monitor simply fails closed and the daemon runs poll-only. To turn it on:

sudo pacman -S python-bpfcc
sudo install -Dm644 systemd/enodia-sentinel-ebpf.conf \
    /etc/systemd/system/enodia-sentinel.service.d/ebpf.conf
sudo systemctl daemon-reload && sudo systemctl restart enodia-sentinel
# confirm:
grep 'eBPF exec monitor' /var/log/enodia-sentinel/events.log
grep 'eBPF syscall monitor' /var/log/enodia-sentinel/events.log

The drop-in relaxes MemoryDenyWriteExecute and widens the capability set — a conscious tradeoff documented in the file itself.

Roadmap

The short version: Enodia grows from a local IDS/IPS/EDR agent into a host security platform: incident grouping, posture checks, response planning, richer eBPF telemetry, fleet health, external anchors, and eventually attestation-ready assurance.

See docs/ROADMAP.md for the release tracks and phased plan. The polling daemon isn't throwaway — it's the oracle: every signature is a test case the event layer must reproduce, and sentinel-redteam is the shared regression suite for both.

Project status

v0.8-dev — expands security monitoring for Gonzalo/Peopleswar-style samples: input_snooper catches direct keyboard/HID event access, credential_access catches live credential harvesting against shadow files, SSH keys, browser stores, and secret profiles, stealth_network watches SCTP/DCCP/raw/packet and other special protocol families, and rootcheck now covers raw ICMP plus hidden SCTP/DCCP/packet-family sockets. Process-hiding coverage now includes /proc vs ps cross-view checks and memory-map scanning for mapped hide libraries, RWX/executable anonymous memory, and executable memfd/deleted mappings.

v0.7 — closes the tamper-evidence loop with the independent anchor: signed-package verification (compares on-disk files to the .MTREE in the signed cache package, surviving a rewritten checksum DB) plus a SigLevel-downgrade check, and an anti-rootkit cross-view layer (hidden processes, modules, TCP/UDP sockets, promiscuous interfaces, known LKM rootkit names, and kernel/module taint — each caught by asking independent views and diffing the answers).

v0.6 — adds tamper-evidence: out-of-band package-DB integrity (catches rewritten checksums), self-integrity of Sentinel's own footprint, and a dead-man's-switch heartbeat with an external watchdog.

v0.5 — adds file integrity monitoring (SHA-256 baseline + pacman -Qkk verification, auto-refreshed by a pacman hook) and false-positive triage via package-ownership provenance.

v0.4 — adds a read-only web dashboard (stdlib server, Tailscale-bound, token-auth; now HTTPS-only) and phone push (ntfy / Pushover / webhook), both zero-dependency.

v0.3 — adds the event-driven eBPF layer: a real bcc execve probe feeding a Snort-style declarative rule engine (4 default rules), stable signature IDs + classtypes on every detection, fail-safe degradation to poll-only, and an opt-in hardening drop-in. Inspired by Snort (rule engine, SIDs) and OSSEC (HIDS framing; FIM + hidden-process checks are next).

v0.2 — Python re-architecture of the bash prototype: 7 detectors, text+JSON forensic snapshots, backgrounded SUID scanning, 25-test unit suite, red-team harness, hardened systemd unit, Arch packaging. Zero runtime dependencies. Built and tested on Arch Linux.

License

GPL-3.0-or-later — see LICENSE.