Expand TUI guidance and host detection coverage
This commit is contained in:
parent
3b037646d2
commit
8194d13734
20 changed files with 414 additions and 42 deletions
12
tests/fixtures/sids/100068-interpreter-unusual-listener.json
vendored
Normal file
12
tests/fixtures/sids/100068-interpreter-unusual-listener.json
vendored
Normal 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
|
||||
}
|
||||
|
|
@ -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",)),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue