Expand TUI guidance and host detection coverage

This commit is contained in:
Luna 2026-07-09 19:16:27 -07:00
parent 3b037646d2
commit 8194d13734
20 changed files with 414 additions and 42 deletions

View file

@ -0,0 +1,12 @@
{
"event": "listen",
"pid": 4242,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"parent_comm": "bash",
"path": "/usr/bin/python3",
"argv": ["-m", "http.server", "4444"],
"local_ip": "0.0.0.0",
"local_port": 4444
}

View file

@ -303,6 +303,16 @@ DRILLS: dict[int, Callable[[], list[Alert]]] = {
]),
_cfg(),
)),
100049: lambda: list(memory_obfuscation.detect(
SystemState(processes=[
FakeProc(pid=355, comm="sshd",
memory_maps=[
MemoryMap(0xb000, 0xc000, "r-xp",
"/tmp/libinject.so"),
]),
]),
_cfg(),
)),
100050: lambda: list(posture.systemd_findings([
posture.UnitFact("foo.service", "/etc/systemd/system/foo.service",
0o100664, True, ("/usr/bin/foo",)),

View file

@ -197,6 +197,30 @@ class TestMemoryObfuscation(unittest.TestCase):
self.assertEqual(alerts[0].signature, "process_hiding_library")
self.assertEqual(alerts[0].severity, Severity.CRITICAL)
def test_process_injection_library_alerts(self):
proc = FakeProc(
pid=355, comm="sshd",
memory_maps=[MemoryMap(0xb000, 0xc000, "r-xp", "/tmp/libinject.so")],
)
alerts = list(memory_obfuscation.detect(SystemState(processes=[proc]), cfg()))
self.assertEqual(len(alerts), 1)
self.assertEqual(alerts[0].signature, "process_injection_library")
self.assertEqual(alerts[0].sid,
memory_obfuscation.SID_PROCESS_INJECTION_LIBRARY)
self.assertEqual(alerts[0].classtype, "process-injection")
self.assertEqual(alerts[0].severity, Severity.HIGH)
def test_process_injection_library_allow_path(self):
c = cfg()
c.memory_obfuscation_allow_paths = ("/tmp/known-profiler/",)
proc = FakeProc(
pid=356, comm="python3",
memory_maps=[
MemoryMap(0xd000, 0xe000, "r-xp", "/tmp/known-profiler/libhook.so"),
],
)
self.assertEqual(list(memory_obfuscation.detect(SystemState(processes=[proc]), c)), [])
def test_jit_allowlisted_comm_ignored(self):
proc = FakeProc(
pid=353, comm="node",

View file

@ -83,6 +83,18 @@ class TestRuleEventCompatibility(unittest.TestCase):
}
self.assertIn(100067, self._sids(event))
def test_host_event_accepts_listen_shape(self):
event = {
"type": "listen",
"pid": "4242",
"ppid": "1",
"uid": "1000",
"comm": "python3",
"bind_ip": "0.0.0.0",
"bind_port": "4444",
}
self.assertIn(100068, self._sids(event))
def test_unknown_event_shape_still_errors(self):
with self.assertRaisesRegex(
ValueError, "event JSON must be an exec, syscall, or host event"

View file

@ -19,12 +19,16 @@ class TestRuleOps(unittest.TestCase):
self.assertIn(100001, sids)
self.assertIn(100060, sids)
self.assertIn(100067, sids)
self.assertIn(100068, sids)
exec_rule = next(r for r in rules if r["sid"] == 100002)
self.assertEqual(exec_rule["event"], "exec")
self.assertIn("argv_regex", exec_rule["conditions"])
host_rule = next(r for r in rules if r["sid"] == 100067)
self.assertEqual(host_rule["event"], "tcp_connect")
self.assertIn("peer_port_exclude", host_rule["conditions"])
listener_rule = next(r for r in rules if r["sid"] == 100068)
self.assertEqual(listener_rule["event"], "listen")
self.assertIn("local_port_exclude", listener_rule["conditions"])
def test_configured_exec_rule_is_listed(self):
with tempfile.TemporaryDirectory() as d:
@ -97,6 +101,33 @@ exec_comm = ["id"]
}
self.assertEqual(ruleops.test_event(cfg, event), [])
def test_matches_host_listen_event_json(self):
cfg = Config()
event = {
"event": "listen",
"pid": 42,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"local_ip": "0.0.0.0",
"local_port": 4444,
}
alerts = ruleops.test_event(cfg, event)
self.assertIn(100068, {a.sid for a in alerts})
def test_host_listen_common_port_no_match(self):
cfg = Config()
event = {
"event": "listen",
"pid": 42,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"local_ip": "0.0.0.0",
"local_port": 443,
}
self.assertEqual(ruleops.test_event(cfg, event), [])
def test_render_markdown_documents_rules(self):
text = ruleops.render_markdown(Config())
self.assertIn("# Enodia Sentinel Event Rule Reference", text)
@ -168,6 +199,7 @@ class TestRulesCli(unittest.TestCase):
self.assertIn("Enodia Sentinel Event Rule Reference", out)
self.assertIn("SID 100060", out)
self.assertIn("SID 100067", out)
self.assertIn("SID 100068", out)
self.assertIn("Expected false positives", out)

View file

@ -20,7 +20,7 @@ class TestRegistry(unittest.TestCase):
self.assertEqual(len(nums), len(set(nums)))
def test_count_and_helpers(self):
self.assertEqual(len(sids.BUILTIN_SIDS), 53)
self.assertEqual(len(sids.BUILTIN_SIDS), 55)
self.assertEqual(
sids.all_sids(), frozenset(s.sid for s in sids.BUILTIN_SIDS)
)

View file

@ -112,6 +112,14 @@ class TestOtherSignatures(unittest.TestCase):
self.assertIn("memory_obfuscation_allow_comms", v.suggest)
self.assertIn('"jit-runtime"', v.suggest)
def test_process_injection_library_is_review(self):
a = alert("process_injection_library",
"pid=45 comm=sshd executable library mapped from writable/runtime path: /tmp/libinject.so",
sid=100049)
v = triage_alert(a, [], Config(), is_owned=NONE)
self.assertEqual(v.label, REVIEW)
self.assertIn("writable path", v.reason)
if __name__ == "__main__":
unittest.main()

View file

@ -52,6 +52,19 @@ class TestTuiCore(unittest.TestCase):
run_tui_command(state, "quit")
self.assertTrue(state.should_quit)
def test_beginner_workflow_commands(self):
state = TuiState()
run_tui_command(state, "next")
self.assertEqual(state.view, "incidents")
run_tui_command(state, "next")
self.assertEqual(state.view, "timeline")
run_tui_command(state, "back")
self.assertEqual(state.view, "incidents")
run_tui_command(state, "home")
self.assertEqual(state.view, "status")
run_tui_command(state, "guide")
self.assertEqual(state.view, "help")
def test_collect_model_contains_dashboard_parity_sections(self):
model = collect_model(self.cfg)
for key in (
@ -64,6 +77,8 @@ class TestTuiCore(unittest.TestCase):
lines = render_lines(state, width=80)
self.assertTrue(any("Daemon:" in line for line in lines))
self.assertTrue(any("Heartbeat:" in line for line in lines))
self.assertTrue(any("Plain language:" in line for line in lines))
self.assertTrue(any("fresh heartbeat" in line for line in lines))
def test_render_alerts_and_filter(self):
(self.tmp / "alert-1.json").write_text(json.dumps({
@ -161,10 +176,13 @@ class TestTuiCore(unittest.TestCase):
self.assertTrue(any("Overall:" in line for line in integrity))
self.assertTrue(any("ttl expired" in line for line in integrity))
events = render_lines(TuiState(view="events", model=model), width=100)
self.assertEqual(events, ["2026-07-09T01:02:03 event happened"])
self.assertTrue(any("technical logs" in line for line in events))
self.assertTrue(any("2026-07-09T01:02:03 event happened" in line
for line in events))
response = render_lines(TuiState(view="response", model=model), width=100)
self.assertTrue(any("inc-1" in line for line in response))
self.assertTrue(any("tar -czf /tmp/evidence.tgz" in line for line in response))
self.assertTrue(any("not executed" in line for line in response))
def test_cli_completion_outputs_scripts(self):
for shell in ("bash", "zsh"):
@ -182,6 +200,9 @@ class TestTuiCore(unittest.TestCase):
help_lines = render_lines(TuiState(view="help"), width=100)
for command in TUI_COMMANDS:
self.assertTrue(any(command in line for line in help_lines), command)
self.assertTrue(any("New User Workflow" in line for line in help_lines))
self.assertTrue(any("read-only" in line for line in help_lines))
self.assertTrue(any("n/b" in line for line in help_lines))
def test_header_scales_to_terminal_width(self):
state = TuiState(view="integrity")
@ -189,7 +210,7 @@ class TestTuiCore(unittest.TestCase):
with self.subTest(width=width):
header = _header_text(state, width)
self.assertLessEqual(len(header), width - 1)
self.assertIn("view=integrity", _header_text(state, 60))
self.assertIn("Integrity", _header_text(state, 60))
self.assertIn("6 Int", _header_text(state, 100))
self.assertIn("6 Integrity", _header_text(state, 180))