Add typed host event egress rule
This commit is contained in:
parent
0b010df514
commit
3b037646d2
15 changed files with 360 additions and 31 deletions
14
tests/fixtures/sids/100067-interpreter-unusual-public-port.json
vendored
Normal file
14
tests/fixtures/sids/100067-interpreter-unusual-public-port.json
vendored
Normal 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
|
||||
}
|
||||
|
|
@ -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})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue