diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 467da9f..8d71653 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -175,7 +175,7 @@ turning Sentinel into a noisy rules dump. event type, match fields, expected false positives, and drill coverage. - ✅ Require a fixture or safe red-team drill for every built-in SID, including event-only and correlation SIDs. -- Add compatibility tests for rule JSON/event schema evolution. +- ✅ Add compatibility tests for rule JSON/event schema evolution. - ✅ Extend triage so false-positive suggestions are tied to SID/classtype and can emit TOML snippets for allowlists, correlation windows, or severity overrides. - ✅ Add post-alert enrichment consistently across detections: diff --git a/tests/test_rule_compat.py b/tests/test_rule_compat.py new file mode 100644 index 0000000..793341d --- /dev/null +++ b/tests/test_rule_compat.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +"""Compatibility tests for rule event JSON evolution.""" +import unittest + +from enodia_sentinel import ruleops +from enodia_sentinel.config import Config + + +class TestRuleEventCompatibility(unittest.TestCase): + def setUp(self): + self.cfg = Config() + + def _sids(self, event): + return {a.sid for a in ruleops.test_event(self.cfg, event)} + + def test_exec_event_accepts_type_alias(self): + event = { + "type": "execve", + "pid": "4242", + "ppid": "1", + "uid": "1000", + "parent_comm": "nginx", + "filename": "/usr/bin/sh", + "argv": ["-c", "id"], + } + self.assertIn(100003, self._sids(event)) + + def test_exec_event_kind_can_be_inferred_from_fields(self): + event = { + "pid": 4242, + "ppid": 1, + "uid": 1000, + "parent_comm": "bash", + "filename": "/tmp/payload", + "argv": [], + } + self.assertIn(100001, self._sids(event)) + + def test_syscall_event_accepts_type_alias_and_numeric_strings(self): + event = { + "type": "sys", + "pid": "4242", + "ppid": "1", + "uid": "1000", + "comm": "loader", + "syscall": "mprotect", + "args": ["0x1000", "0x1000", "0x6"], + } + self.assertIn(100060, self._sids(event)) + + def test_syscall_event_kind_can_be_inferred_from_fields(self): + event = { + "pid": 4242, + "ppid": 1, + "uid": 1000, + "comm": "loader", + "syscall": "memfd_create", + } + self.assertIn(100062, self._sids(event)) + + def test_syscall_arg_fields_override_args_array(self): + event = { + "event": "syscall", + "pid": 4242, + "ppid": 1, + "uid": 1000, + "comm": "loader", + "syscall": "mmap", + "args": [0, 4096, 0], + "arg2": "0x6", + } + self.assertIn(100061, self._sids(event)) + + def test_unknown_event_shape_still_errors(self): + with self.assertRaisesRegex( + ValueError, "event JSON must be an exec or syscall event" + ): + ruleops.test_event(self.cfg, {"pid": 4242}) + + +if __name__ == "__main__": + unittest.main()