# SPDX-License-Identifier: GPL-3.0-or-later """Tests for package-DB tamper detection and the dead-man's-switch watchdog.""" import tempfile import time import unittest from pathlib import Path from enodia_sentinel import pkgdb, selfprotect from enodia_sentinel.alert import Severity from enodia_sentinel.config import Config def cfg_with_dir(tmp: Path) -> Config: c = Config() c.log_dir = tmp return c class TestPkgdbFingerprint(unittest.TestCase): def test_fingerprint_changes_with_content(self): with tempfile.TemporaryDirectory() as d: db = Path(d) (db / "pkgA").mkdir() (db / "pkgA" / "desc").write_text("sha256 = AAAA") f1 = pkgdb.db_fingerprint(str(db)) self.assertIsNotNone(f1) (db / "pkgA" / "desc").write_text("sha256 = BBBB") # attacker rewrites hash self.assertNotEqual(f1, pkgdb.db_fingerprint(str(db))) class TestPkgdbCheck(unittest.TestCase): def setUp(self): self.d = tempfile.TemporaryDirectory() self.cfg = cfg_with_dir(Path(self.d.name)) def tearDown(self): self.d.cleanup() def test_first_run_establishes_anchor(self): v = pkgdb.check(self.cfg, _fingerprint=lambda: "FP1", _last_tx=lambda: None) self.assertIsNone(v) self.assertEqual(pkgdb.load_anchor(self.cfg)["fingerprint"], "FP1") def test_unchanged_is_silent(self): pkgdb.check(self.cfg, _fingerprint=lambda: "FP1", _last_tx=lambda: None) v = pkgdb.check(self.cfg, _fingerprint=lambda: "FP1", _last_tx=lambda: None) self.assertIsNone(v) def test_change_with_no_transaction_is_tamper(self): pkgdb.check(self.cfg, _fingerprint=lambda: "FP1", _last_tx=lambda: None) v = pkgdb.check(self.cfg, _fingerprint=lambda: "FP2", _last_tx=lambda: None) self.assertIsNotNone(v) self.assertEqual(v.severity, Severity.CRITICAL) self.assertEqual(v.sid, pkgdb.SID_PKGDB) def test_change_with_recent_transaction_is_legit(self): pkgdb.check(self.cfg, _fingerprint=lambda: "FP1", _last_tx=lambda: None) future = time.time() + 10 # a transaction occurred after the anchor v = pkgdb.check(self.cfg, _fingerprint=lambda: "FP2", _last_tx=lambda: future) self.assertIsNone(v) # re-anchored, no alert self.assertEqual(pkgdb.load_anchor(self.cfg)["fingerprint"], "FP2") class TestPacmanLogParse(unittest.TestCase): def test_last_transaction_time(self): with tempfile.NamedTemporaryFile("w", suffix=".log", delete=False) as f: f.write("[2026-05-30T10:00:00-0700] [ALPM] transaction completed\n") f.write("[2026-05-31T12:34:56-0700] [ALPM] transaction completed\n") f.write("[2026-05-31T12:35:00-0700] [ALPM] upgraded foo (1 -> 2)\n") path = f.name ts = pkgdb.last_transaction_time(path) self.assertIsNotNone(ts) Path(path).unlink() class TestHeartbeatWatchdog(unittest.TestCase): def test_heartbeat_roundtrip(self): with tempfile.TemporaryDirectory() as d: cfg = cfg_with_dir(Path(d)) selfprotect.write_heartbeat(cfg) self.assertLess(selfprotect.heartbeat_age(cfg), 5) def test_watchdog_verdicts(self): ok, _ = selfprotect.watchdog_verdict({"running": True, "heartbeat_age": 5}, 120) self.assertTrue(ok) bad, msg = selfprotect.watchdog_verdict(None, 120) self.assertFalse(bad) self.assertIn("UNREACHABLE", msg) bad2, _ = selfprotect.watchdog_verdict({"running": False}, 120) self.assertFalse(bad2) bad3, msg3 = selfprotect.watchdog_verdict( {"running": True, "heartbeat_age": 9999}, 120) self.assertFalse(bad3) self.assertIn("STALE", msg3) if __name__ == "__main__": unittest.main()