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
89
tests/test_rootcheck.py
Normal file
89
tests/test_rootcheck.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Anti-rootkit cross-view tests — the pure diff cores plus a run() integration
|
||||
test that injects each system view, so it needs no live /proc, /sys, or ss."""
|
||||
import unittest
|
||||
|
||||
from enodia_sentinel import rootcheck
|
||||
from enodia_sentinel.alert import Severity
|
||||
from enodia_sentinel.config import Config
|
||||
from enodia_sentinel.system import Socket, SystemState
|
||||
|
||||
|
||||
class TestDiffCores(unittest.TestCase):
|
||||
def test_hidden_pids_are_alive_minus_visible(self):
|
||||
# 1337 is alive (kernel knows it) but absent from the /proc listing.
|
||||
self.assertEqual(
|
||||
rootcheck.find_hidden_pids({1, 2, 3}, {1, 2, 3, 1337}), {1337})
|
||||
|
||||
def test_no_hidden_pids_when_views_agree(self):
|
||||
self.assertEqual(rootcheck.find_hidden_pids({1, 2}, {1, 2}), set())
|
||||
|
||||
def test_hidden_modules_live_in_sys_but_not_proc(self):
|
||||
self.assertEqual(
|
||||
rootcheck.find_hidden_modules({"ext4", "nf_tables"},
|
||||
{"ext4", "nf_tables", "evil_rk"}),
|
||||
{"evil_rk"})
|
||||
|
||||
def test_hidden_ports_in_procnet_not_in_ss(self):
|
||||
self.assertEqual(
|
||||
rootcheck.find_hidden_ports({22, 80, 31337}, {22, 80}), {31337})
|
||||
|
||||
|
||||
class TestSsPortsFromState(unittest.TestCase):
|
||||
def test_extracts_listening_ports_from_injected_state(self):
|
||||
socks = [
|
||||
Socket("LISTEN", "0.0.0.0:22", "0.0.0.0:0", 10, "sshd", 1),
|
||||
Socket("LISTEN", "127.0.0.1:631", "0.0.0.0:0", 11, "cupsd", 2),
|
||||
]
|
||||
state = SystemState(sockets=socks)
|
||||
self.assertEqual(rootcheck.ss_listen_ports(state), {22, 631})
|
||||
|
||||
|
||||
class TestRunIntegration(unittest.TestCase):
|
||||
"""Drive run() with every system view monkeypatched to a known state."""
|
||||
|
||||
def setUp(self):
|
||||
self.cfg = Config()
|
||||
self._saved = {}
|
||||
for name in ("proc_pids", "alive_pids", "proc_modules",
|
||||
"sys_live_modules", "procnet_listen_ports",
|
||||
"promiscuous_interfaces"):
|
||||
self._saved[name] = getattr(rootcheck, name)
|
||||
|
||||
def tearDown(self):
|
||||
for name, fn in self._saved.items():
|
||||
setattr(rootcheck, name, fn)
|
||||
|
||||
def _patch(self, **views):
|
||||
rootcheck.proc_pids = lambda: views.get("proc_pids", set())
|
||||
rootcheck.alive_pids = lambda cap: views.get("alive_pids", set())
|
||||
rootcheck.proc_modules = lambda: views.get("proc_modules", set())
|
||||
rootcheck.sys_live_modules = lambda: views.get("sys_live_modules", set())
|
||||
rootcheck.procnet_listen_ports = lambda: views.get("procnet_ports", set())
|
||||
rootcheck.promiscuous_interfaces = lambda: views.get("promisc", [])
|
||||
|
||||
def test_clean_system_yields_nothing(self):
|
||||
self._patch(proc_pids={1, 2}, alive_pids={1, 2},
|
||||
proc_modules={"ext4"}, sys_live_modules={"ext4"})
|
||||
state = SystemState(sockets=[])
|
||||
self.assertEqual(list(rootcheck.run(self.cfg, state)), [])
|
||||
|
||||
def test_hidden_module_and_port_and_promisc(self):
|
||||
self._patch(
|
||||
proc_pids={1}, alive_pids={1},
|
||||
proc_modules={"ext4"}, sys_live_modules={"ext4", "diamorphine"},
|
||||
procnet_ports={22, 31337}, promisc=["eth0"])
|
||||
state = SystemState(sockets=[
|
||||
Socket("LISTEN", "0.0.0.0:22", "0.0.0.0:0", 10, "sshd", 1)])
|
||||
alerts = {a.signature: a for a in rootcheck.run(self.cfg, state)}
|
||||
self.assertIn("rootkit_hidden_module", alerts)
|
||||
self.assertEqual(alerts["rootkit_hidden_module"].severity, Severity.CRITICAL)
|
||||
self.assertIn("diamorphine", alerts["rootkit_hidden_module"].detail)
|
||||
self.assertIn("rootkit_hidden_port", alerts)
|
||||
self.assertIn("31337", alerts["rootkit_hidden_port"].detail)
|
||||
self.assertIn("promiscuous_interface", alerts)
|
||||
self.assertEqual(alerts["promiscuous_interface"].sid, rootcheck.SID_PROMISC)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue