Both zero-dependency (stdlib http.server + urllib), consistent with the project's no-dependency-tree stance for a security daemon. Web dashboard (enodia_sentinel/web.py + static/dashboard.html): - read-only JSON API over the log dir: /api/status, /api/alerts, /api/alerts/<id>, /api/events - bearer-token auth (constant-time; header or ?token=), required on non- loopback binds, auto-generated + persisted (0600) when unset - binds the host's Tailscale IP by default (auto-detected), reachable from the tailnet but not the LAN/internet - self-contained dark SPA: severity cards, live alert list, full snapshot viewer; 10s auto-refresh - path-traversal-safe alert lookup; `enodia-sentinel web` subcommand; daemon now writes a pidfile so the dashboard can show live status - hardened enodia-sentinel-web.service (read-only, no caps) Phone push (enodia_sentinel/notify/): - pluggable backends — ntfy, Pushover, generic webhook — each separating a pure build() (unit-tested, no network) from send() - a backend turns on when its config keys are set; pushes gated by notify_min_severity; severity → per-service priority/tags - fired from snapshot.capture on worker threads, errors swallowed - desktop notify-send retained Tests: +16 (9 web incl. a real-server 401/200 auth test, 7 notify request-build cases). 55/55 pass. Live end-to-end verified: daemon → alert → API → page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.5 KiB
TOML
96 lines
4.5 KiB
TOML
# Enodia Sentinel configuration (TOML).
|
|
# Read at startup from /etc/enodia-sentinel.toml (or $ENODIA_CONFIG).
|
|
# Restart after editing: sudo systemctl restart enodia-sentinel.service
|
|
#
|
|
# Every key is optional; omitted keys keep their built-in default.
|
|
|
|
# --- timing --------------------------------------------------------------
|
|
sample_interval = 4.0 # seconds between detector sweeps
|
|
cooldown = 60 # min seconds before re-alerting the same signature
|
|
baseline_grace = 10 # seconds after start before new-listener/suid arm
|
|
suid_refresh = 3600 # seconds between SUID baseline refreshes
|
|
suid_scan_interval = 60 # seconds between (backgrounded) SUID filesystem scans
|
|
|
|
# --- detectors (omit one to disable it) ----------------------------------
|
|
detectors = [
|
|
"reverse_shell", "ld_preload", "deleted_exe",
|
|
"new_listener", "new_suid", "persistence", "egress",
|
|
]
|
|
|
|
# --- tuning --------------------------------------------------------------
|
|
# Process names treated as "interpreters" for reverse-shell / egress checks.
|
|
interpreters = [
|
|
"bash", "sh", "dash", "zsh", "ksh", "ash", "nc", "ncat", "netcat",
|
|
"socat", "telnet", "python", "python2", "python3", "perl", "ruby",
|
|
"php", "lua", "awk",
|
|
]
|
|
|
|
# Trusted public CIDRs that should NOT trip the egress detector
|
|
# (your VPS, VPN exit, monitoring endpoints, …).
|
|
egress_allow_cidrs = []
|
|
|
|
# Listener ports that never alert even if they appear after baseline.
|
|
listener_allow_ports = []
|
|
|
|
# Dirs where any SUID binary is treated as CRITICAL (attacker-writable).
|
|
suid_hot_dirs = ["/tmp", "/dev/shm", "/var/tmp", "/home", "/run/user"]
|
|
|
|
# Writable dirs always scanned for SUID even if on a separate filesystem
|
|
# (a tmpfs /tmp would otherwise be skipped by the on-device walk).
|
|
suid_scan_extra_dirs = ["/tmp", "/dev/shm", "/var/tmp", "/run/user"]
|
|
|
|
# Persistence files/dirs watched for modification.
|
|
watch_persistence = [
|
|
"/etc/cron.d", "/etc/crontab", "/etc/cron.daily", "/etc/cron.hourly",
|
|
"/etc/cron.weekly", "/var/spool/cron", "/etc/systemd/system",
|
|
"/etc/ld.so.preload", "/etc/passwd", "/etc/sudoers", "/etc/sudoers.d",
|
|
"/root/.ssh/authorized_keys", "/root/.bashrc", "/root/.profile",
|
|
]
|
|
|
|
# --- eBPF event layer ----------------------------------------------------
|
|
# Event-driven execve monitor: catches short-lived processes the poll loop
|
|
# misses, matched against the Snort-style rule engine. Requires python-bpfcc
|
|
# and root; degrades gracefully to polling otherwise.
|
|
ebpf_exec_monitor = true
|
|
# Optional path to a TOML file of extra [[exec_rules]] (sid/msg/severity/
|
|
# classtype + path_prefixes/exec_comm/parent_comm/argv_regex).
|
|
exec_rules_file = ""
|
|
|
|
# Add a 3s bpftrace execve trace to each snapshot (requires the bpftrace pkg).
|
|
capture_execve_bpftrace = false
|
|
|
|
# --- retention -----------------------------------------------------------
|
|
max_snapshots = 300 # auto-delete oldest beyond this count (0 = no cap)
|
|
max_snapshot_age_days = 60 # auto-delete older than this (0 = no age cap)
|
|
|
|
# --- notifications: desktop ---------------------------------------------
|
|
notify_users = [] # e.g. ["luna"] — desktop notify-send on alert
|
|
notify_urgency = "critical" # low / normal / critical
|
|
|
|
# --- notifications: phone push ------------------------------------------
|
|
# A backend turns on when its required keys are set. Only alerts at or above
|
|
# notify_min_severity are pushed.
|
|
notify_min_severity = "HIGH" # MEDIUM / HIGH / CRITICAL
|
|
|
|
# ntfy (open-source, self-hostable). Install the ntfy app and subscribe to the
|
|
# topic; keep the topic name secret — it is the only access control on ntfy.sh.
|
|
notify_ntfy_url = "" # e.g. "https://ntfy.sh" or your own server
|
|
notify_ntfy_topic = "" # e.g. "enodia-<random>"
|
|
notify_ntfy_token = "" # optional Bearer token (protected topics)
|
|
|
|
# Pushover
|
|
notify_pushover_token = "" # application token
|
|
notify_pushover_user = "" # user/group key
|
|
|
|
# Generic webhook — receives the alert JSON via POST (Discord/Slack/your own).
|
|
notify_webhook_url = ""
|
|
|
|
# Optional dashboard base URL embedded in pushes (e.g. your Tailscale URL).
|
|
dashboard_url = ""
|
|
|
|
# --- web dashboard (read-only) ------------------------------------------
|
|
web_bind = "" # "" = auto-detect Tailscale IP; or an explicit address
|
|
web_port = 8787
|
|
web_token = "" # bearer token; auto-generated + saved if empty on a
|
|
# non-loopback bind. Set explicitly to keep the unit
|
|
# fully read-only.
|