Add signed-package verification and anti-rootkit cross-view (v0.7)

Closes the tamper-evidence loop with two trust anchors the attacker can't
forge from userland:

- pkgdb Layer 2: verify on-disk files against the .MTREE in the *signed*
  cache package, surviving a rewritten local checksum DB. Rotating-sample
  cadence keeps it affordable; flags pkg_signature_mismatch (sid 100027)
  and SigLevel downgrades (sid 100026). Fixes parse_mtree, which required
  type=file on every line and so matched nothing on real pacman MTREEs
  (which use a /set type=file default with bare file entries).
- rootcheck: anti-rootkit cross-view — hidden processes, modules, ports,
  and promiscuous interfaces, each caught by diffing two views of the
  same state (sids 100022-100025).

Wired both through config, the daemon (off-loop slow cadence), and CLI
(pkgdb-verify, rootcheck). 14 new tests (95 total). Docs + version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-01 05:42:57 -07:00
parent 34bc09041a
commit 1de5e86fed
10 changed files with 766 additions and 5 deletions

View file

@ -69,6 +69,12 @@ def main(argv: list[str] | None = None) -> int:
fc.add_argument("--packages", action="store_true",
help="also verify package-owned files via pacman -Qkk")
sub.add_parser("pkgdb-check", help="check the package DB for out-of-band tampering")
pv = sub.add_parser("pkgdb-verify",
help="verify on-disk files against the signed cache packages")
pv.add_argument("--sample", type=int, default=0,
help="packages to verify (0 = config default; rotates)")
sub.add_parser("rootcheck",
help="anti-rootkit cross-view: hidden procs/modules/ports")
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")
@ -108,6 +114,10 @@ def main(argv: list[str] | None = None) -> int:
return 1
print("Package DB: consistent with the anchor (no out-of-band changes).")
return 0
if args.cmd == "pkgdb-verify":
return _cmd_pkgdb_verify(cfg, args.sample)
if args.cmd == "rootcheck":
return _cmd_rootcheck(cfg)
if args.cmd == "watchdog":
return _cmd_watchdog(cfg, args.url, args.token, args.max_age)
return _cmd_run(cfg)
@ -132,6 +142,35 @@ def _cmd_watchdog(cfg: Config, url: str, token: str, max_age: int) -> int:
return 0 if ok else 1
def _cmd_pkgdb_verify(cfg: Config, sample: int) -> int:
from . import pkgdb
if sample > 0:
cfg.pkgdb_pkgverify_sample = sample
sl = pkgdb.siglevel_alert()
if sl:
print(f"[CRITICAL] {sl.detail}")
if not pkgdb.keyring_present():
print("[WARN] pacman keyring not found — signature trust may be unestablished.")
alerts = [a for a in pkgdb.verify_alerts(cfg) if a.signature != "pacman_siglevel_disabled"]
for a in alerts:
print(f"[CRITICAL] {a.detail}")
if not sl and not alerts:
print(f"Package verify: sampled files match the signed cache packages "
f"(sample={cfg.pkgdb_pkgverify_sample}).")
return 1 if (sl or alerts) else 0
def _cmd_rootcheck(cfg: Config) -> int:
from . import rootcheck
alerts = list(rootcheck.run(cfg))
if not alerts:
print("Rootcheck: no hidden processes, modules, ports, or sniffers found.")
return 0
for a in sorted(alerts, key=lambda x: -x.severity):
print(f"[{a.severity}] {a.signature:<22} {a.detail}")
return 1
def _cmd_fim_check(cfg: Config, packages: bool) -> int:
from . import fim
sentinel = Sentinel(cfg)