Add tamper-evidence: package-DB integrity, self-integrity, dead-man's switch

Answers two hard questions: "what guards the hashes FIM trusts?" and "how do we
make the sensor itself hard to silently disable?" — within the honest limit that
a root attacker who shares your privileges can't be fully stopped on-box, only
made loud.

- pkgdb.py: guards the package DB that `pacman -Qkk` trusts. Anchors a
  fingerprint of /var/lib/pacman/local (refreshed ONLY by the pacman hook) and
  cross-checks pacman.log; a DB change with no logged transaction is flagged
  pkgdb_tamper (CRITICAL, sid 100021) — catches an attacker rewriting a stored
  checksum to mask a modified binary. Verified end-to-end against a simulated
  hash-overwrite.
- selfprotect.py: Sentinel's own binaries/config/units/hook are always in the
  FIM watch set (self-integrity); a heartbeat is written each loop; an external
  `watchdog` command polls a remote dashboard and pushes if the sensor goes
  silent or unreachable — silence becomes the alarm.
- daemon: heartbeat + slow-cadence pkgdb check; DB anchor re-anchored in
  build_fim_baseline so the pacman hook refreshes it after each transaction
- web: /api/status now reports heartbeat_age/stale; dashboard shows it
- cli: pkgdb-check, watchdog (--url/--token/--max-age, bypasses min-severity)
- config: pkgdb_verify/_interval, heartbeat_max_age
- README: threat model (trust-anchor problem) + hardening layers (immutability,
  signed-package + external anchor); reframes "anti-rootkit" as tamper-evidence
- tests: +8 (DB fingerprint, anchor/transaction logic, pacman.log parse,
  heartbeat + watchdog verdicts). 80/80 pass

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-31 22:27:50 -07:00
parent 5d577b624f
commit 34bc09041a
13 changed files with 465 additions and 9 deletions

View file

@ -67,6 +67,10 @@ class Config:
fim_scan_interval: int = 300 # seconds between hash scans
fim_pkg_verify: bool = False # run `pacman -Qkk` (slow; opt-in)
fim_pkg_verify_interval: int = 21600 # seconds between package verifications
# tamper-evidence
pkgdb_verify: bool = True # detect out-of-band package-DB edits
pkgdb_interval: int = 600 # seconds between DB integrity checks
heartbeat_max_age: int = 120 # heartbeat older than this = stale
# eBPF on-ramp
capture_execve_bpftrace: bool = False
@ -119,11 +123,12 @@ class Config:
return self.log_dir / "fim-baseline.json"
def fim_path_list(self) -> tuple[str, ...]:
"""Configured FIM paths, defaulting to the built-in critical set."""
if self.fim_paths:
return self.fim_paths
"""FIM paths: the configured/critical set, always plus Sentinel's own
footprint so tampering with the watchdog trips the watchdog."""
from .fim import DEFAULT_FIM_PATHS
return DEFAULT_FIM_PATHS
from .selfprotect import SELF_PATHS
base = self.fim_paths or DEFAULT_FIM_PATHS
return tuple(base) + SELF_PATHS
# ---- loading ---------------------------------------------------------
@classmethod