Add host posture checks (roadmap v0.8: posture check)

Add `enodia-sentinel posture check` — a config-hygiene audit that finds
the conditions making a host easy to attack, complementing the live
detectors. Checks: SSH root/password/empty-password login (Include-aware,
first-wins sshd resolution), passwordless and group/world-writable
sudoers, world-writable or non-root PATH directories, loose permissions
on sensitive files (/etc/shadow, /etc/passwd, ...), and downgraded
package-signature policy (reusing pkgdb.siglevel_alert).

Findings reuse the Alert type, so they serialize through the existing
JSON/triage pipeline (`posture check --json`). Each check is a pure
evaluator over injected content/stat facts plus a thin system reader, so
the 18 new tests need no root or live /etc. Posture is command-driven and
never runs in the daemon loop, per the v0.8 exit criterion.

SIDs 100040-100047 (classtype host-posture). Sample config and docs
(COMMAND_REFERENCE, OPERATIONS, ROADMAP, SPECIFICATION) updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-10 05:15:06 -07:00
parent 6ff2087329
commit 9ebc355936
9 changed files with 520 additions and 6 deletions

View file

@ -75,6 +75,11 @@ 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")
po = sub.add_parser("posture",
help="audit host config hygiene (SSH, sudo, PATH, perms)")
po.add_argument("action", nargs="?", default="check", choices=["check"],
help="posture action (default: check)")
po.add_argument("--json", action="store_true", help="emit findings as JSON")
wd = sub.add_parser("watchdog",
help="poll a remote dashboard and push if Sentinel is silent")
wd.add_argument("--url", required=True, help="dashboard base URL")
@ -118,6 +123,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 == "posture":
return _cmd_posture(cfg, args.json)
if args.cmd == "watchdog":
return _cmd_watchdog(cfg, args.url, args.token, args.max_age)
return _cmd_run(cfg)
@ -171,6 +178,22 @@ def _cmd_rootcheck(cfg: Config) -> int:
return 1
def _cmd_posture(cfg: Config, as_json: bool) -> int:
from . import posture
findings = sorted(posture.run(cfg), key=lambda x: -x.severity)
if as_json:
import json
print(json.dumps([f.to_dict() for f in findings], indent=2))
return 1 if findings else 0
if not findings:
print("Posture: no findings — SSH, sudo, PATH, file perms, and package "
"signature policy look sound.")
return 0
for a in findings:
print(f"[{a.severity}] {a.signature:<24} {a.detail}")
return 1
def _cmd_fim_check(cfg: Config, packages: bool) -> int:
from . import fim
sentinel = Sentinel(cfg)