Add platform docs; reposition as host security platform

Add docs/ (SPECIFICATION, ROADMAP, OPERATIONS, COMMAND_REFERENCE,
THREAT_MODEL) describing the current v0.7 agent and the phased path from
local HIDS to a host security platform. Reframe README, package metadata,
and CLI/module descriptions from "intrusion-detection daemon" to "Linux
host security platform", and surface the v0.7 signed-package verify
(pkgdb_pkgverify*) and anti-rootkit (rootcheck_*) knobs in the sample
config. Also wrap the pacman.conf read in pkgdb.siglevel_alert in a
context manager to avoid a leaked file handle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-10 04:03:06 -07:00
parent 1de5e86fed
commit 6ff2087329
11 changed files with 930 additions and 26 deletions

View file

@ -1,14 +1,41 @@
# Enodia Sentinel # Enodia Sentinel
A host intrusion-detection daemon for Linux. It continuously runs a set of A Linux host security platform: intrusion detection, file/package integrity,
detectors over live system state — processes, sockets, file descriptors, the anti-rootkit cross-checks, tamper-evidence, forensic snapshots, push alerts, and
SUID inventory, and sensitive files — and writes a detailed forensic snapshot a read-only operator console.
(text **and** JSON) with incident-response guidance the moment a known attack
signature appears. 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 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 "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."* 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 is becoming more than an IDS. The local detector is the first layer of a
broader host security platform:
| Function | Current capability | Direction |
|---|---|---|
| Detect | Poll detectors + eBPF exec rules | More event sources and correlation |
| Verify | FIM, package DB anchor, signed-package checks | External anchors and signed evidence |
| Investigate | Text/JSON snapshots, dashboard, triage | Incident timelines and evidence export |
| Respond | Human guidance today | Dry-run response plans and audited containment |
| Assure | Heartbeat, watchdog, self-integrity, rootcheck | Fleet health and attestation-ready anchors |
Project docs:
- [Specification](docs/SPECIFICATION.md) — product model, current scope, target
platform shape, data model, and acceptance criteria.
- [Roadmap](docs/ROADMAP.md) — phased work from local sensor to incident,
response, fleet, and assurance layers.
- [Operations guide](docs/OPERATIONS.md) — install checks, health checks, alert
workflow, baseline hygiene, and evidence export.
> **Two implementations, on purpose.** The project began as a bash prototype > **Two implementations, on purpose.** The project began as a bash prototype
> (`src/sentinel.sh`, kept as the regression **oracle**) and was re-architected > (`src/sentinel.sh`, kept as the regression **oracle**) and was re-architected
@ -131,9 +158,8 @@ sudo tail -f /var/log/enodia-sentinel/events.log
sentinel-redteam # safe, self-cleaning attack simulations sentinel-redteam # safe, self-cleaning attack simulations
``` ```
You'll watch the drills trip `reverse_shell`, `ld_preload`, `deleted_exe`, You'll watch the drills trip the core poll signatures in real time, each
`new_listener`, and `new_suid` in real time, each producing a `.log` + `.json` producing a `.log` + `.json` snapshot with response guidance.
snapshot with response guidance.
Output lives in `/var/log/enodia-sentinel/`: Output lives in `/var/log/enodia-sentinel/`:
- `events.log` — one line per alert - `events.log` — one line per alert
@ -170,7 +196,7 @@ drill only sets the env var on a throwaway process.
## Testing ## Testing
```bash ```bash
make test # 25 unit tests, stdlib unittest, no deps make test # stdlib unittest suite, no runtime deps
``` ```
Detectors are pure functions over an injectable `SystemState`, so tests build Detectors are pure functions over an injectable `SystemState`, so tests build
@ -442,16 +468,11 @@ conscious tradeoff documented in the file itself.
## Roadmap ## Roadmap
1. **bpftrace tracepoints (optional)**`capture_execve_bpftrace = true` adds a The short version: Enodia grows from local HIDS into a host security platform:
live `execve` trace to each snapshot. incident grouping, posture checks, response planning, richer eBPF telemetry,
2. ✅ **Event-driven `execve` detection (done)** — a real eBPF probe via bcc, fleet health, external anchors, and eventually attestation-ready assurance.
feeding a Snort-style rule engine, so a short-lived process can't slip
between sweeps.
3. **More event sources**`tcp_connect` (event-driven egress) and
`security_bprm_check` (LSM) probes, plus per-process lineage tracking.
4. **A libbpf + CO-RE agent (Go or Rust)** — the production EDR core: ring-buffer
event streaming, tamper resistance, no runtime compiler.
See [docs/ROADMAP.md](docs/ROADMAP.md) for the release tracks and phased plan.
The polling daemon isn't throwaway — it's the **oracle**: every signature is a 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 test case the event layer must reproduce, and `sentinel-redteam` is the shared
regression suite for both. regression suite for both.

View file

@ -74,10 +74,24 @@ fim_pkg_verify_interval = 21600 # seconds between package verifications
pkgdb_verify = true pkgdb_verify = true
pkgdb_interval = 600 # seconds between DB integrity checks pkgdb_interval = 600 # seconds between DB integrity checks
heartbeat_max_age = 120 # daemon heartbeat older than this = stale/silent heartbeat_max_age = 120 # daemon heartbeat older than this = stale/silent
# Layer 2: compare on-disk files to hashes in cached signed packages. This
# survives a rewritten local package DB, but needs a populated package cache and
# costs untar+hash work, so it samples packages on a slow cadence.
pkgdb_pkgverify = false
pkgdb_pkgverify_interval = 21600 # seconds between signed-package passes
pkgdb_pkgverify_sample = 40 # packages verified per pass (rotates)
# Sentinel's own binaries/config/units/hook are always FIM-watched (self- # Sentinel's own binaries/config/units/hook are always FIM-watched (self-
# integrity). For real teeth, make them immutable: chattr +i <path> # integrity). For real teeth, make them immutable: chattr +i <path>
# and run an external `enodia-sentinel watchdog` against this host's dashboard. # and run an external `enodia-sentinel watchdog` against this host's dashboard.
# --- anti-rootkit cross-view --------------------------------------------
# Ask the same question two ways and diff the answers: kill(0) vs /proc for
# hidden processes, /sys/module vs /proc/modules for hidden LKMs, /proc/net/tcp
# vs ss for hidden listeners, and interface flags for promiscuous sniffing.
rootcheck_enabled = true
rootcheck_interval = 300 # seconds between cross-view sweeps
rootcheck_pid_cap = 65536 # upper PID to brute-force via kill(0)
# --- eBPF event layer ---------------------------------------------------- # --- eBPF event layer ----------------------------------------------------
# Event-driven execve monitor: catches short-lived processes the poll loop # Event-driven execve monitor: catches short-lived processes the poll loop
# misses, matched against the Snort-style rule engine. Requires python-bpfcc # misses, matched against the Snort-style rule engine. Requires python-bpfcc

226
docs/COMMAND_REFERENCE.md Normal file
View file

@ -0,0 +1,226 @@
# Enodia Sentinel Command Reference
This is the operator-facing command contract for the current local agent. Commands
are intentionally plain: they either inspect state, manage baselines, run the
daemon, or expose existing evidence.
Global options:
```bash
enodia-sentinel --version
enodia-sentinel -c /path/to/config.toml <command>
```
If no command is provided, `run` is used.
## Service Commands
### `run`
```bash
enodia-sentinel run
```
Starts the daemon loop. This is the command used by the systemd service. It
builds or loads baselines, starts optional event monitoring, writes heartbeats,
runs detector sweeps, captures snapshots, and sends notifications.
Expected use: systemd, not an interactive shell.
### `check`
```bash
enodia-sentinel check
```
Runs the enabled detector set once and prints alerts. This forces the SUID scan
and arms baseline-gated detectors immediately, so it is useful for health checks
and debugging.
Exit code:
- `0`: no alerts.
- `0`: alerts may still print today; this command is currently human-oriented,
not a strict CI gate.
### `baseline`
```bash
enodia-sentinel baseline
```
Rebuilds listener and SUID baselines under `log_dir`. Use after installing on a
known-good host or after intentionally adding persistent services or privileged
helpers.
Do not run this to silence unexplained findings.
## Detection and Integrity Commands
### `list-detectors`
```bash
enodia-sentinel list-detectors
```
Prints the poll detectors and whether each is enabled by config.
### `rootcheck`
```bash
enodia-sentinel rootcheck
```
Runs anti-rootkit cross-view checks once:
- PIDs alive via `kill(pid, 0)` but missing from `/proc`.
- Live modules in `/sys/module` but missing from `/proc/modules`.
- Listening TCP ports in `/proc/net/tcp*` but missing from `ss`.
- Network interfaces in promiscuous mode.
Exit code:
- `0`: no rootcheck findings.
- `1`: one or more rootcheck alerts.
### `fim-baseline`
```bash
enodia-sentinel fim-baseline
```
Builds the SHA-256 file integrity baseline for configured critical paths plus
Sentinel's own footprint.
Use on a known-good host. The package hook also calls `fim-update` after normal
package transactions.
### `fim-update`
```bash
enodia-sentinel fim-update
```
Refreshes the FIM baseline. This exists primarily for the package manager hook.
Manual use should be treated as an acknowledgement that the current file state
is legitimate.
### `fim-check`
```bash
enodia-sentinel fim-check
enodia-sentinel fim-check --packages
```
Diffs current file state against the FIM baseline. With `--packages`, also runs
package-owned file verification through the package manager (`pacman -Qkk` on
Arch).
Exit code:
- `0`: no baseline changes.
- `1`: one or more added, removed, or modified files.
### `pkgdb-check`
```bash
enodia-sentinel pkgdb-check
```
Checks whether the local package database fingerprint changed outside a logged
package transaction. This catches an attacker rewriting stored package checksums
to hide a modified binary.
Exit code:
- `0`: package DB matches the anchor or was legitimately re-anchored.
- `1`: out-of-band DB tampering suspected.
### `pkgdb-verify`
```bash
enodia-sentinel pkgdb-verify
enodia-sentinel pkgdb-verify --sample 200
```
Compares on-disk package-owned files to SHA-256 hashes from `.MTREE` manifests
inside cached signed packages. This is independent of the mutable local package
database.
The command also flags insecure global `SigLevel` settings and warns if the
pacman keyring is missing.
Exit code:
- `0`: sampled files match and signature policy is not downgraded.
- `1`: a signed-package mismatch or insecure signature setting was found.
## Evidence and Operator Commands
### `web`
```bash
enodia-sentinel web
```
Starts the read-only dashboard. By default it binds to the Tailscale interface
when available and uses bearer-token authentication for non-loopback binds.
Expected use: `enodia-sentinel-web.service`.
### `triage`
```bash
enodia-sentinel triage
```
Reads captured alert JSON snapshots and classifies distinct findings as likely
false positive or review-needed. It suggests config snippets for known benign
noise but never edits config automatically.
### `watchdog`
```bash
enodia-sentinel watchdog --url http://100.x.y.z:8787 --token <token> --max-age 120
```
Polls a remote dashboard and alerts if the sensor is down, unreachable, or has a
stale heartbeat. Run this from a separate host. A watchdog on the same machine
does not prove the protected host is alive.
Exit code:
- `0`: remote sensor is reachable and fresh.
- `1`: remote sensor is stale, down, or unreachable.
## Development Shortcuts
The Makefile wraps common local tasks:
```bash
make test
make check
make baseline
make web
make drill
```
`make install` installs a plain package directory plus a launcher wrapper. It
does not require pip or a virtualenv.
## Future Command Contracts
The roadmap reserves these command shapes:
```bash
enodia-sentinel status --json
enodia-sentinel incident list
enodia-sentinel incident show <incident-id>
enodia-sentinel incident export <incident-id>
enodia-sentinel posture check
enodia-sentinel respond plan <incident-id>
enodia-sentinel respond apply <plan-id>
```
These should be introduced with stable JSON schemas and tests before they are
documented as supported.

143
docs/OPERATIONS.md Normal file
View file

@ -0,0 +1,143 @@
# Enodia Sentinel Operations Guide
This guide turns the current toolset into repeatable operator workflows. It is
deliberately practical: what to run, what to expect, and what to check next.
## First Install Checklist
1. Install and enable the service.
```bash
sudo make install
sudo make enable
```
2. Build the listener and SUID baselines.
```bash
sudo enodia-sentinel baseline
```
3. Build the FIM baseline.
```bash
sudo enodia-sentinel fim-baseline
```
4. Run a one-shot check.
```bash
sudo enodia-sentinel check
sudo enodia-sentinel rootcheck
sudo enodia-sentinel pkgdb-check
```
5. Start the dashboard if desired.
```bash
sudo systemctl enable --now enodia-sentinel-web
```
6. Configure one off-box notification or watchdog path. A local-only alerting
path is not enough if the whole host goes silent.
## Routine Health Checks
Run these after upgrades, config edits, or suspicious behavior:
```bash
enodia-sentinel check
enodia-sentinel fim-check
enodia-sentinel pkgdb-check
enodia-sentinel pkgdb-verify --sample 100
enodia-sentinel rootcheck
enodia-sentinel triage
```
Expected healthy output:
- `check`: no alerts, or only expected tuned findings.
- `fim-check`: no changes against baseline after legitimate updates have been
acknowledged.
- `pkgdb-check`: package DB consistent with anchor.
- `pkgdb-verify`: sampled files match signed cache packages.
- `rootcheck`: no hidden processes, modules, ports, or sniffers.
## Alert Workflow
When Sentinel fires:
1. Open the newest JSON and text snapshots in `/var/log/enodia-sentinel`.
2. Identify the signature, `sid`, affected PIDs, executable paths, remote peers,
and parent process.
3. Run `enodia-sentinel triage` to separate known benign listener noise from
findings that need review.
4. If the alert involves a binary, run:
```bash
enodia-sentinel fim-check --packages
enodia-sentinel pkgdb-verify --sample 200
```
5. If the alert involves hiding or tampering, run:
```bash
enodia-sentinel rootcheck
enodia-sentinel pkgdb-check
```
6. Preserve evidence before changing state:
```bash
cp -a /var/log/enodia-sentinel /tmp/enodia-sentinel-evidence
```
7. Contain manually for now. Future response commands should produce a dry-run
plan first.
## Common Findings
| Finding | First checks |
|---|---|
| `reverse_shell` | Inspect parent process, fd table, remote peer, shell argv, and user. |
| `egress` | Confirm interpreter, destination IP, command line, and parent process. |
| `new_listener` | Identify owning process and whether the binary is package-owned. |
| `new_suid` | Check path, owner, package provenance, and whether it lives in a writable directory. |
| `persistence` | Diff the changed file and identify the modifying package or process if possible. |
| `fim_modified` | Verify package ownership, package checksum, and whether the change followed an upgrade. |
| `pkgdb_tamper` | Treat as high-confidence tampering until a legitimate package transaction explains it. |
| `pkg_signature_mismatch` | Treat as a potentially trojaned package-owned file. |
| `hidden_*` rootcheck alerts | Compare live tools, preserve evidence, and consider offline analysis. |
## Baseline Hygiene
Baselines are useful only if they represent a known-good state.
- Build baselines after installing Sentinel on a clean host.
- Rebuild listener/SUID baselines after intentionally adding long-running
services or privileged binaries.
- Use `fim-update` only after confirming changes are legitimate. Package hooks
already run it for normal package transactions.
- Keep a copy of important anchors off-box when possible.
## Suggested Hardening
- Keep the dashboard bound to Tailscale or loopback, not a public interface.
- Set `web_token` explicitly for fully read-only service operation.
- Enable at least one push backend.
- Run `watchdog` from another machine.
- Consider `chattr +i` for Sentinel binaries, systemd units, config, baselines,
and the pacman hook after setup.
- Keep the package cache populated if using signed-package verification.
- Do not disable package signature verification.
## Evidence Export
Until the incident command set exists, an evidence bundle is simply:
```bash
tar -C /var/log -czf /tmp/enodia-sentinel-evidence.tgz enodia-sentinel
```
That bundle contains events, snapshots, baselines, heartbeat state, package DB
anchor, and dashboard-readable JSON.

174
docs/ROADMAP.md Normal file
View file

@ -0,0 +1,174 @@
# Enodia Sentinel Roadmap
This roadmap moves Enodia Sentinel from a host IDS into a host security platform:
detect, verify, investigate, respond, and assure. Dates are intentionally not
promised here; the sequence matters more than calendar precision.
## Release Tracks
| Track | Goal |
|---|---|
| Sensor | Better host telemetry, event capture, and detection coverage. |
| Integrity | Stronger proof that files, packages, baselines, and Sentinel itself were not silently modified. |
| Incident | Group alerts, build timelines, and export evidence. |
| Response | Add safe, auditable containment and recovery workflows. |
| Fleet | Let multiple hosts report to a small controller without making the local agent dependent on it. |
| Assurance | Move trust anchors off-box or below user space. |
## v0.8: Documentation, Posture, and Incident Shape
Purpose: make the current agent easier to operate and create the data model for
work that is bigger than single alerts.
- Add `incident_id` grouping around related alerts while preserving existing
alert JSON.
- Add an incident timeline generator:
alert time, process ancestry, sockets, persistence writes, FIM diffs, package
transactions, and rootcheck findings.
- Add `enodia-sentinel incident list/show/export`.
- Add host posture checks:
SSH password login, root SSH login, permissive sudoers entries, unexpected
enabled services, world-writable PATH components, disabled package signatures,
and unsafe systemd unit settings.
- Add a machine-readable `status --json` command for automation.
- Update the red-team harness so every current detection has a named drill and
expected `sid`.
- Document incident response runbooks for reverse shells, persistence, trojaned
binaries, hidden listeners, and sensor tampering.
Exit criteria:
- One alert can become one incident artifact.
- Operators can answer "what changed around this alert?" without reading every
raw snapshot manually.
- Posture checks report reviewable findings but do not block daemon startup.
## v0.9: Response and Recovery
Purpose: move from "tell me" to "help me act" without unsafe automation.
- Add response plan generation:
`enodia-sentinel respond plan <incident-id>`.
- Add dry-run first actions:
kill process, stop/disable service, block remote IP, quarantine file, restore
package-owned file by reinstalling its package, and freeze evidence.
- Require explicit `--apply` for changes; default to read-only plans.
- Write response audit logs under the normal log directory.
- Add baseline reconciliation:
accept legitimate FIM/package/listener/SUID changes with a recorded reason.
- Add richer false-positive suppression suggestions that can emit TOML snippets
but never modify config automatically.
- Add recovery checks:
"verify package", "re-run rootcheck", "confirm no persistence diff", and
"confirm heartbeat/dashboard visible from watchdog".
Exit criteria:
- An operator can go from alert to reviewed response plan with one command.
- Any state-changing action is explicit, logged, reversible where possible, and
testable in a safe fixture.
## v1.0: Stable Local Platform
Purpose: define a stable single-host product.
- Commit to stable JSON schemas for alerts, incidents, status, and response
audits.
- Add compatibility tests for schema evolution.
- Add documentation versioning and manpage-style command reference.
- Harden packaging for Arch first, then document Debian/RPM install paths.
- Add signed release artifacts and checksums.
- Improve dashboard from alert browser to local console:
incidents, posture, integrity state, response plans, and watchdog status.
- Keep the dashboard read-only unless a separate authenticated write path is
designed and reviewed.
Exit criteria:
- The local agent can be installed, operated, upgraded, and integrated without
reading source code.
- JSON and command contracts are stable enough for downstream automation.
## v1.1: Event Coverage and Correlation
Purpose: reduce blind spots and connect related signals.
- Add eBPF event sources:
`tcp_connect`, `bind`, `accept`, module load, privilege transitions, and
writes to watched persistence paths.
- Build process lineage across polling and event sources.
- Correlate multi-stage behavior:
web service spawns shell, shell downloads payload, payload adds persistence,
listener appears, and FIM changes.
- Add configurable correlation windows and severity escalation rules.
- Keep polling as the oracle and fallback.
Exit criteria:
- Short-lived network and persistence actions are visible.
- Related alerts collapse into one incident with a readable timeline.
## v1.2: Fleet Mode
Purpose: support several personal or small-business Linux hosts without turning
the agent into a heavy enterprise platform.
- Add an optional collector service.
- Agents push signed JSON events and heartbeats over HTTPS or a tailnet.
- Collector stores host status, alerts, incidents, and package/integrity state.
- Add host enrollment with per-host tokens.
- Add fleet-wide views:
stale sensors, repeated signatures, hosts missing package signatures, and
integrity drift.
- Preserve local autonomy: detection and evidence capture must continue if the
collector is down.
Exit criteria:
- A small fleet can answer "which host is compromised or silent?" from one
console.
- The agent still works as a standalone local tool.
## v1.3+: Assurance and Hardening
Purpose: make tampering harder to hide from an attacker with high privileges.
- External baseline anchor:
mirror FIM and package DB fingerprints off-box with append-only semantics.
- Hash-chain `events.log` and snapshots.
- Optional snapshot signing with a host key.
- Investigate Linux IMA/EVM and TPM-backed attestation for supported machines.
- Replace bcc with a libbpf + CO-RE agent if the event layer becomes core to
the product.
- Explore minimal kernel-side enforcement hooks for response actions, while
keeping the default product inspect-only.
Exit criteria:
- A local root attacker has to tamper with both the host and an external or
hardware-backed trust anchor to hide compromise.
## Backlog
Useful ideas that need design before implementation:
- Policy-as-code for host posture.
- YARA-compatible scanning for selected paths.
- Sigma-like host event rules.
- SBOM and package provenance reporting.
- Offline evidence bundle for outside analysis.
- Dashboard import of exported incident bundles.
- Support for non-pacman package managers in signed-package verification.
- Dedicated Debian/RPM packaging.
- Installer preflight checks and health diagnostics.
- Rule documentation generated from source defaults.
## Roadmap Rules
- Do not add dependencies to the base daemon without a specific security or
operator value that cannot be achieved with stdlib.
- Prefer read-only detection and dry-run response until the command contract is
tested and documented.
- Every new detector or response action needs a safe drill or fixture.
- Do not build stealth or self-hiding behavior.
- Keep the local agent useful without a dashboard, collector, or network.

206
docs/SPECIFICATION.md Normal file
View file

@ -0,0 +1,206 @@
# Enodia Sentinel Specification
This document defines Enodia Sentinel as a host security platform, not only a
signature IDS. The current implementation is a local Linux sensor with evidence
capture, integrity monitoring, anti-rootkit checks, a dashboard, and alert
delivery. The intended product is a defensive control plane for a host or small
fleet: detect compromise, prove what changed, preserve useful evidence, guide
response, and make sensor tampering visible.
## Product Definition
Enodia Sentinel is a zero-runtime-dependency Linux host security agent. It runs
as a local daemon, observes high-signal host behavior, records forensic context
when something suspicious happens, and exposes the result through local files,
JSON, a read-only dashboard, push notifications, and command-line tools.
The long-term product should combine five functions:
| Function | Purpose |
|---|---|
| Detection | Find active compromise behavior: reverse shells, fileless execution, new listeners, suspicious egress, persistence edits, and privilege-escalation artifacts. |
| Integrity | Prove whether important files, packages, baselines, and Sentinel's own footprint changed. |
| Evidence | Capture enough context for incident response without forcing the operator to reproduce a live state after the attacker is gone. |
| Response | Provide safe, explicit operator workflows for triage, containment, and recovery. |
| Assurance | Make the agent itself measurable: heartbeat, dead-man's switch, tamper-evidence, and future remote attestation. |
## Design Principles
1. **High-signal first.** Prefer detections that are cheap to observe and costly
for attackers to avoid, even if that means a smaller rule set.
2. **Evidence over noise.** Every alert should carry enough context to answer
"what happened, where, and what should I inspect next?"
3. **Zero dependency core.** The base daemon must run with the Python standard
library only. Optional layers such as bcc/eBPF may degrade cleanly.
4. **One local trust boundary is not enough.** On-box checks are useful but
root can tamper with them. Important anchors should move to signed package
metadata, immutable filesystem controls, external watchers, or future
remote attestation.
5. **Operator control.** Sentinel should recommend response actions before it
takes them. Destructive containment must be opt-in and auditable.
6. **Bash prototype remains the oracle.** The original shell implementation and
red-team harness preserve behavioral compatibility for core signatures.
## Current Scope: v0.7
### Poll Detectors
The daemon builds one `SystemState` per sweep and runs pure detector functions
against cached process and socket data:
| Detector | Signal |
|---|---|
| `reverse_shell` | Interpreter with a network socket on fd 0/1/2. |
| `ld_preload` | Non-empty `/etc/ld.so.preload` or writable-path `LD_PRELOAD`. |
| `deleted_exe` | Process executing from a deleted file or `memfd:` image. |
| `new_listener` | Listening socket absent from the startup baseline. |
| `new_suid` | New SUID/SGID binary, critical in writable locations. |
| `persistence` | Changes to cron, systemd units, SSH keys, shell rc files, and similar persistence locations. |
| `egress` | Interpreter with an established connection to a public IP. |
### Event Detection
An optional bcc eBPF `execve` monitor feeds a declarative rule engine. This
closes the polling gap for short-lived commands and supports custom TOML rules
without code changes. Failure is fail-safe: if the probe cannot load, polling
continues.
### Integrity and Tamper-Evidence
Sentinel includes several integrity layers:
| Layer | Current behavior |
|---|---|
| FIM baseline | SHA-256 baseline for critical files and Sentinel's own footprint. |
| Package verification | Optional `pacman -Qkk` verification for package-owned files. |
| Package DB anchor | Detects out-of-band edits to `/var/lib/pacman/local`. |
| Signed-package anchor | Compares on-disk files to `.MTREE` hashes from cached signed packages. |
| Heartbeat | Writes daemon liveness for local dashboard and external watchdog use. |
| Rootcheck | Cross-view checks for hidden processes, hidden modules, hidden ports, and promiscuous interfaces. |
### Evidence Capture
When a fresh alert survives cooldown, Sentinel writes:
- A human-readable snapshot.
- A JSON snapshot suitable for automation and SIEM import.
- Relevant process tree, sockets, environment hints, file descriptors, and
response guidance.
- Event log entries and dashboard-visible status.
### Operator Interfaces
| Interface | Role |
|---|---|
| CLI | Run daemon, one-shot checks, baseline management, FIM checks, package DB checks, rootcheck, triage, watchdog. |
| Logs | Durable local evidence under `/var/log/enodia-sentinel`. |
| Dashboard | Read-only web view for status, alert browsing, and snapshot inspection. |
| Push | ntfy, Pushover, and generic webhook notifications. |
| Red-team harness | Safe drills for testing signatures and demos. |
## Target Scope: Host Security Platform
The next product shape should make Enodia useful before, during, and after an
incident.
### Prevent
- Configuration posture checks for risky SSH, sudo, systemd, cron, kernel,
package manager, firewall, and service settings.
- Baseline drift reports for newly exposed services, new SUID files, new
autostarts, new users/groups, and sensitive permission changes.
- A policy mode that can fail CI or package build checks for unsafe host images.
### Detect
- More event sources: `tcp_connect`, `bind`, `accept`, file-write events for
persistence paths, module loading, privilege transitions, and LSM hooks.
- Correlation across signals: "web service spawned shell" + "new listener" +
"persistence write" should become one incident, not three unrelated alerts.
- Optional YARA-style and Sigma-like local rules if they can stay dependency
light and operator-readable.
### Investigate
- Incident timeline files derived from event log, snapshots, FIM diffs,
package transactions, process lineage, and network observations.
- Stable incident IDs that group related alerts over time.
- `enodia-sentinel incident show <id>` and `incident export <id>` workflows.
### Respond
- Safe containment actions behind explicit flags:
stop a process, disable a systemd unit, remove a persistence entry, block an
IP with nftables, quarantine a file, or freeze evidence.
- Dry-run by default, with JSON plans that can be reviewed before execution.
- Response playbooks that are code-reviewed data files, not shell snippets
pasted into documentation.
### Recover
- Package restore guidance: identify owning package, verify cache signature,
reinstall package, and re-run integrity checks.
- Baseline reconciliation workflow after confirmed legitimate changes.
- Post-incident report generation from captured evidence.
### Assure
- External anchor storage for baselines and package DB fingerprints.
- Signed snapshots or hash-chained event logs.
- Remote watchdog improvements and eventually fleet controller support.
- Optional TPM/IMA integration for machines that can support measured boot.
## Data Model
The current `Alert` object is the public detection unit:
| Field | Meaning |
|---|---|
| `sid` | Stable signature ID for tuning, documentation, and downstream rules. |
| `severity` | `MEDIUM`, `HIGH`, or `CRITICAL`. |
| `signature` | Stable machine-readable detection name. |
| `classtype` | Snort/Suricata-style category. |
| `key` | Cooldown and deduplication identity. |
| `detail` | Human-readable explanation. |
| `pids` | Process IDs for snapshot deep dives. |
Future incident grouping should add an `incident_id` around one or more alerts,
without breaking the alert JSON schema.
## Configuration Model
Configuration is a TOML file loaded at startup. Unknown keys are ignored so
newer config templates remain tolerable on older agents, but documentation
should clearly mark version-specific fields. Existing defaults should prefer
detection correctness over suppression. Convenience suppression knobs should
remain explicit and reviewable.
## Security Model
Sentinel runs as root because it must inspect root-owned files, process state,
file descriptors, and sockets. The systemd unit should continue to keep the
root process narrow: read-mostly, constrained writable paths, no new privileges,
restricted namespaces, and only the capabilities needed for the enabled layers.
Local root can eventually defeat any purely local sensor. Sentinel's answer is
layering: signed package metadata, immutable files, off-box watchdogs, external
anchors, and future kernel/attestation features.
## Non-Goals
- Replacing a full enterprise SIEM.
- Silent self-hiding or rootkit-like persistence.
- Automatic destructive remediation without explicit operator approval.
- Shipping a broad dependency tree in the default agent.
- Claiming complete protection against a custom kernel implant.
## Acceptance Criteria For New Platform Features
A feature is ready when it has:
- A stable `sid`/signature or documented command contract.
- Unit tests for pure logic and a safe integration path where practical.
- JSON output or a durable artifact suitable for automation.
- Configuration documented in the sample TOML and README or relevant docs.
- Failure behavior that does not take down the daemon.
- Clear operator guidance for false positives, limitations, and response.

119
docs/THREAT_MODEL.md Normal file
View file

@ -0,0 +1,119 @@
# Enodia Sentinel Threat Model
This document describes what Enodia Sentinel is meant to catch, what it can only
make evident, and what it does not claim to solve.
## Assets
Sentinel protects and records evidence about:
- Running processes and process ancestry.
- Network sockets and remote peers.
- File descriptors and executable paths.
- SUID/SGID inventory and privilege-escalation artifacts.
- Persistence locations: cron, systemd units, SSH keys, shell startup files, and
sensitive account/sudo configuration.
- Package-owned binaries and local package database integrity.
- Sentinel's own binaries, config, systemd units, hook, baselines, logs, and
heartbeat.
## Adversary Model
Sentinel is designed to help against:
| Adversary | Examples | Expected Sentinel value |
|---|---|---|
| Opportunistic remote shell | Webshell, exposed service RCE, stolen SSH key | Detect reverse shell, suspicious egress, new listeners, persistence writes. |
| Local privilege escalation | New SUID helper, dropped setuid shell, writable path abuse | Detect new SUID/SGID and critical writable-directory placement. |
| Fileless or short-lived execution | Deleted executable, memfd payload, fast `curl|sh` | Detect deleted executables and eBPF exec rules where available. |
| Package/file tampering | Trojaned binary, rewritten package DB checksums | Detect FIM drift, package DB tamper, and signed-package mismatches. |
| Common rootkit hiding | LD_PRELOAD tricks, `/proc` hiding, module-list hiding, hidden listener | Detect LD_PRELOAD and cross-view inconsistencies. |
| Sensor tampering | Stop daemon, edit config, remove hook, modify baseline | Detect self-integrity changes and stale heartbeat via external watchdog. |
## Trust Boundaries
### Local Root Is Not Fully Trusted
The daemon runs as root so it can inspect the host. If an attacker gains root,
they share that privilege. Sentinel therefore does not claim that local files,
local baselines, or local process views are unforgeable.
The design goal is layered tamper-evidence:
- Package signatures are harder for a local attacker to forge than the local
package DB.
- Immutable bits force visible state changes before local files can be edited.
- External watchdogs notice silence.
- Future external anchors and signed logs should make retroactive rewriting
harder.
### Optional eBPF Expands Privilege
The default service is hardened and read-mostly. Enabling the bcc eBPF monitor
requires additional capabilities and memory permissions. That tradeoff is
explicit: stronger event visibility for a wider runtime permission set.
## Detection Assumptions
Sentinel assumes:
- Attackers need a process, socket, file, package, persistence location, or
privilege transition somewhere on the host.
- High-confidence host behaviors are more useful than broad noisy rules.
- Many attacks are visible from more than one view: `/proc`, `/sys`, `ss`,
package metadata, logs, and filesystem hashes.
- Snapshots captured immediately are more useful than asking an operator to
reconstruct state after the process exits.
## Important Limitations
- A custom kernel implant that hooks every view consistently can evade
user-space cross-view checks.
- A local root attacker can delete or rewrite local logs unless external anchors
or immutable controls are in place.
- Signed-package verification depends on a populated package cache and valid
package signature policy.
- `pacman -Qkk` trusts the local package database; it is useful but not an
independent root of trust.
- The dashboard is read-only by design; it is not a remote response console.
- The current product does not perform automatic containment.
## Security Controls Already Present
| Control | Purpose |
|---|---|
| Hardened systemd unit | Limits daemon write surface and runtime privilege where possible. |
| Cooldown deduplication | Prevents alert storms from one persistent condition. |
| Snapshot retention | Preserves evidence while bounding local disk growth. |
| FIM self-watch | Detects changes to Sentinel's own files and service definitions. |
| Package DB anchor | Flags package checksum DB edits outside package transactions. |
| Signed-package verification | Checks files against package manifests independent of the local DB. |
| Rootcheck | Finds common hiding artifacts by comparing independent views. |
| Heartbeat + watchdog | Makes a silent sensor observable from another machine. |
| Read-only dashboard | Exposes evidence without adding a remote write path. |
## Response Safety
Sentinel should remain conservative:
- Detection may be automatic.
- Evidence capture may be automatic.
- Notification may be automatic.
- Triage suggestions may be automatic.
- Destructive response should require an explicit operator action.
Future response features should produce dry-run plans first, log all changes,
and avoid shell-string execution where structured APIs exist.
## Abuse Considerations
Some security tools can become dual-use. Enodia should avoid:
- Stealth, hiding, or rootkit-like self-defense.
- Persistence mechanisms that are hard for the owner to inspect.
- Automated deletion of suspicious files without review.
- Covert exfiltration of host data.
- Cloud dependence for local detection.
The correct defensive posture is visibility, evidence, and explicit operator
control.

View file

@ -1,9 +1,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
"""Enodia Sentinel — a host intrusion-detection daemon. """Enodia Sentinel — a Linux host security platform.
A poll-based HIDS that runs a set of detectors over live system state and Detection, file/package integrity, anti-rootkit checks, tamper-evidence,
captures a forensic snapshot with incident-response guidance whenever a known forensic snapshots, and operator workflows. The Python re-architecture of the
attack signature appears. The Python re-architecture of the bash v0 prototype. bash v0 prototype.
""" """
__version__ = "0.7.0" __version__ = "0.7.0"

View file

@ -51,7 +51,7 @@ def _cmd_check(cfg: Config) -> int:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="enodia-sentinel", prog="enodia-sentinel",
description="Host intrusion-detection daemon.", description="Linux host security platform.",
) )
parser.add_argument("--version", action="version", parser.add_argument("--version", action="version",
version=f"enodia-sentinel {__version__}") version=f"enodia-sentinel {__version__}")

View file

@ -169,7 +169,8 @@ def siglevel_insecure(value: str) -> bool:
def siglevel_alert(conf_path: str = PACMAN_CONF) -> Alert | None: def siglevel_alert(conf_path: str = PACMAN_CONF) -> Alert | None:
try: try:
text = open(conf_path, encoding="utf-8", errors="replace").read() with open(conf_path, encoding="utf-8", errors="replace") as fh:
text = fh.read()
except OSError: except OSError:
return None return None
value = parse_global_siglevel(text) value = parse_global_siglevel(text)

View file

@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "enodia-sentinel" name = "enodia-sentinel"
version = "0.7.0" version = "0.7.0"
description = "Host intrusion-detection daemon — signature-based detection of reverse shells, LD_PRELOAD rootkits, fileless malware, and persistence tampering" description = "Linux host security platform — detection, integrity, anti-rootkit checks, tamper-evidence, and forensic snapshots"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
license = { text = "GPL-3.0-or-later" } license = { text = "GPL-3.0-or-later" }