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

@ -43,6 +43,13 @@ class Sentinel:
self._fim_pkg_thread: threading.Thread | None = None
self._last_fim_pkg = 0.0
self._last_pkgdb = 0.0
# Layer-2 package verification (rotating sample each pass).
self._pkgverify_thread: threading.Thread | None = None
self._last_pkgverify = 0.0
self._pkgverify_offset = 0
# Anti-rootkit cross-view sweep (off the loop thread; brute-forces PIDs).
self._rootcheck_thread: threading.Thread | None = None
self._last_rootcheck = 0.0
self._stop = threading.Event()
# -- baselines ---------------------------------------------------------
@ -158,6 +165,44 @@ class Sentinel:
for alert in pacman_verify_alerts():
self._on_exec_alert(alert)
# -- package signature verification (Layer 2, off the loop thread) ------
def _maybe_pkgdb_verify(self, now: float) -> None:
if not self.cfg.pkgdb_pkgverify:
return
if self._pkgverify_thread and self._pkgverify_thread.is_alive():
return
if (now - self._last_pkgverify) < self.cfg.pkgdb_pkgverify_interval:
return
self._last_pkgverify = now
self._pkgverify_thread = threading.Thread(
target=self._pkgdb_verify, daemon=True)
self._pkgverify_thread.start()
def _pkgdb_verify(self) -> None:
from . import pkgdb
for alert in pkgdb.verify_alerts(self.cfg, offset=self._pkgverify_offset):
self._on_exec_alert(alert)
# Advance the rotating window so the next pass covers a different slice.
self._pkgverify_offset += self.cfg.pkgdb_pkgverify_sample
# -- anti-rootkit cross-view (off the loop thread) ---------------------
def _maybe_rootcheck(self, now: float) -> None:
if not self.cfg.rootcheck_enabled:
return
if self._rootcheck_thread and self._rootcheck_thread.is_alive():
return
if (now - self._last_rootcheck) < self.cfg.rootcheck_interval:
return
self._last_rootcheck = now
self._rootcheck_thread = threading.Thread(
target=self._rootcheck, daemon=True)
self._rootcheck_thread.start()
def _rootcheck(self) -> None:
from . import rootcheck
for alert in rootcheck.run(self.cfg):
self._on_exec_alert(alert)
# -- one sweep ---------------------------------------------------------
def sweep(self, *, force_suid: bool = False) -> list[Alert]:
now = time.time()
@ -221,6 +266,8 @@ class Sentinel:
self._maybe_scan_fim(now)
self._maybe_pkg_verify(now)
self._maybe_check_pkgdb(now)
self._maybe_pkgdb_verify(now)
self._maybe_rootcheck(now)
alerts = self.sweep()
fresh = self.fresh_alerts(alerts, now)
if fresh: