Add status command (roadmap v0.8: status --json)

Add `enodia-sentinel status [--json]` — a local health and alert
summary: daemon running state, heartbeat freshness, per-severity alert
counts, last alert time, and eBPF state. Reuses web.daemon_status (the
same data behind /api/status), so the CLI and dashboard never diverge.

Exit code reflects health (0 = running and fresh, 1 = down or stale), so
it doubles as a cron/monitoring probe. Three CLI tests drive it against a
temp log_dir; no daemon or root needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-10 05:17:29 -07:00
parent 9ebc355936
commit f2d896a2d7
4 changed files with 99 additions and 2 deletions

View file

@ -75,6 +75,8 @@ def main(argv: list[str] | None = None) -> int:
help="packages to verify (0 = config default; rotates)")
sub.add_parser("rootcheck",
help="anti-rootkit cross-view: hidden procs/modules/ports")
st = sub.add_parser("status", help="print daemon health/alert summary")
st.add_argument("--json", action="store_true", help="emit status as JSON")
po = sub.add_parser("posture",
help="audit host config hygiene (SSH, sudo, PATH, perms)")
po.add_argument("action", nargs="?", default="check", choices=["check"],
@ -123,6 +125,8 @@ def main(argv: list[str] | None = None) -> int:
return _cmd_pkgdb_verify(cfg, args.sample)
if args.cmd == "rootcheck":
return _cmd_rootcheck(cfg)
if args.cmd == "status":
return _cmd_status(cfg, args.json)
if args.cmd == "posture":
return _cmd_posture(cfg, args.json)
if args.cmd == "watchdog":
@ -178,6 +182,31 @@ def _cmd_rootcheck(cfg: Config) -> int:
return 1
def _cmd_status(cfg: Config, as_json: bool) -> int:
from .web import daemon_status
s = daemon_status(cfg)
if as_json:
import json
print(json.dumps(s, indent=2))
else:
age = s["heartbeat_age"]
if age is None:
hb = "none (daemon never wrote one)"
elif s["heartbeat_stale"]:
hb = f"STALE — {int(age)}s ago (> {cfg.heartbeat_max_age}s)"
else:
hb = f"{int(age)}s ago"
counts = ", ".join(f"{k}: {v}" for k, v in sorted(s["counts"].items())) or "none"
print(f"enodia-sentinel {s['version']} on {s['host']}")
print(f" daemon: {'running' if s['running'] else 'NOT running'}")
print(f" heartbeat: {hb}")
print(f" alerts: {s['total_alerts']} total ({counts})")
print(f" last alert: {s['last_alert'] or 'none'}")
print(f" eBPF: {s['ebpf']}")
# Health gate for monitoring: unhealthy if the daemon is down or silent.
return 0 if (s["running"] and not s["heartbeat_stale"]) else 1
def _cmd_posture(cfg: Config, as_json: bool) -> int:
from . import posture
findings = sorted(posture.run(cfg), key=lambda x: -x.severity)