Add read-only web dashboard and phone push notifications
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>
This commit is contained in:
parent
0eb5077551
commit
c00fff224c
17 changed files with 960 additions and 5 deletions
|
|
@ -8,6 +8,7 @@ expensive filesystem-wide SUID scan is gated to its own slow cadence.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
|
@ -124,6 +125,10 @@ class Sentinel:
|
|||
# -- main loop ---------------------------------------------------------
|
||||
def run(self) -> None:
|
||||
self.cfg.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
(self.cfg.log_dir / "sentinel.pid").write_text(str(os.getpid()))
|
||||
except OSError:
|
||||
pass
|
||||
with open(self.cfg.events_log, "a") as fh:
|
||||
fh.write(f"{time.strftime('%FT%T%z')} enodia-sentinel started\n")
|
||||
self.build_baselines()
|
||||
|
|
@ -150,6 +155,9 @@ class Sentinel:
|
|||
|
||||
def _start_exec_monitor(self) -> None:
|
||||
if not self.cfg.ebpf_exec_monitor:
|
||||
with open(self.cfg.events_log, "a") as fh:
|
||||
fh.write(f"{time.strftime('%FT%T%z')} "
|
||||
"eBPF exec monitor: off (disabled in config)\n")
|
||||
return
|
||||
from .events.monitor import ExecMonitor
|
||||
self._exec_monitor = ExecMonitor(self.cfg, self._on_exec_alert)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue