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:
parent
34bc09041a
commit
1de5e86fed
10 changed files with 766 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue