Add typed host event egress rule

This commit is contained in:
Luna 2026-07-09 06:04:30 -07:00
parent 0b010df514
commit 3b037646d2
15 changed files with 360 additions and 31 deletions

View file

@ -0,0 +1,14 @@
{
"event": "tcp_connect",
"pid": 4242,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"parent_comm": "bash",
"path": "/usr/bin/python3",
"argv": ["-c", "connect back"],
"peer_ip": "8.8.8.8",
"peer_port": 4444,
"local_ip": "10.0.0.5",
"local_port": 51510
}

View file

@ -71,9 +71,21 @@ class TestRuleEventCompatibility(unittest.TestCase):
}
self.assertIn(100061, self._sids(event))
def test_host_event_accepts_tcp_connect_shape(self):
event = {
"event": "tcp_connect",
"pid": "4242",
"ppid": "1",
"uid": "1000",
"comm": "python3",
"remote_ip": "8.8.8.8",
"remote_port": "4444",
}
self.assertIn(100067, self._sids(event))
def test_unknown_event_shape_still_errors(self):
with self.assertRaisesRegex(
ValueError, "event JSON must be an exec or syscall event"
ValueError, "event JSON must be an exec, syscall, or host event"
):
ruleops.test_event(self.cfg, {"pid": 4242})

View file

@ -18,9 +18,13 @@ class TestRuleOps(unittest.TestCase):
sids = {r["sid"] for r in rules}
self.assertIn(100001, sids)
self.assertIn(100060, sids)
self.assertIn(100067, 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"])
def test_configured_exec_rule_is_listed(self):
with tempfile.TemporaryDirectory() as d:
@ -66,6 +70,33 @@ exec_comm = ["id"]
alerts = ruleops.test_event(cfg, event)
self.assertIn(100060, {a.sid for a in alerts})
def test_matches_host_event_json(self):
cfg = Config()
event = {
"event": "tcp_connect",
"pid": 42,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"peer_ip": "8.8.8.8",
"peer_port": 4444,
}
alerts = ruleops.test_event(cfg, event)
self.assertIn(100067, {a.sid for a in alerts})
def test_host_event_common_public_port_no_match(self):
cfg = Config()
event = {
"event": "tcp_connect",
"pid": 42,
"ppid": 1,
"uid": 1000,
"comm": "python3",
"peer_ip": "8.8.8.8",
"peer_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)
@ -136,6 +167,7 @@ class TestRulesCli(unittest.TestCase):
self.assertEqual(code, 0)
self.assertIn("Enodia Sentinel Event Rule Reference", out)
self.assertIn("SID 100060", out)
self.assertIn("SID 100067", out)
self.assertIn("Expected false positives", out)

View file

@ -32,7 +32,11 @@ def fixture_sids() -> dict[int, set[int]]:
class TestEventFixtures(unittest.TestCase):
def test_every_event_rule_sid_has_an_emitting_fixture(self):
covered = fixture_sids()
wanted = sids.engine_sids("exec_rule") | sids.engine_sids("syscall_rule")
wanted = (
sids.engine_sids("exec_rule")
| sids.engine_sids("syscall_rule")
| sids.engine_sids("host_rule")
)
for sid in sorted(wanted):
self.assertIn(sid, covered,
f"no fixture file for sid {sid} in {FIXTURE_DIR}")
@ -54,7 +58,11 @@ def drill_sids() -> dict[int, set[int]]:
class TestSidCoverageGate(unittest.TestCase):
def test_every_non_event_sid_has_an_emitting_drill(self):
covered = drill_sids()
event_sids = sids.engine_sids("exec_rule") | sids.engine_sids("syscall_rule")
event_sids = (
sids.engine_sids("exec_rule")
| sids.engine_sids("syscall_rule")
| sids.engine_sids("host_rule")
)
wanted = sids.all_sids() - event_sids
self.assertEqual(set(covered), wanted)
for sid in sorted(wanted):

View file

@ -10,6 +10,7 @@ from enodia_sentinel.detectors import (
stealth_network,
)
from enodia_sentinel.events.rules import DEFAULT_EXEC_RULES
from enodia_sentinel.events.host_rules import DEFAULT_HOST_RULES
from enodia_sentinel.events.syscall_rules import DEFAULT_SYSCALL_RULES
@ -19,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), 52)
self.assertEqual(len(sids.BUILTIN_SIDS), 53)
self.assertEqual(
sids.all_sids(), frozenset(s.sid for s in sids.BUILTIN_SIDS)
)
@ -37,6 +38,10 @@ class TestRegistry(unittest.TestCase):
sids.engine_sids("syscall_rule"),
frozenset(r.sid for r in DEFAULT_SYSCALL_RULES),
)
self.assertEqual(
sids.engine_sids("host_rule"),
frozenset(r.sid for r in DEFAULT_HOST_RULES),
)
def test_registry_uses_the_source_constants(self):
by_sid = {s.sid: s for s in sids.BUILTIN_SIDS}