1"""`SidChecker`."""
2
3import logging
4from collections.abc import Mapping, Sequence
5from typing import Optional
6
7import idstools.rule
8
9from suricata_check.checkers.interface import CheckerInterface
10from suricata_check.utils.checker import get_rule_option
11from suricata_check.utils.checker_typing import ISSUES_TYPE, Issue
12from suricata_check.utils.regex import get_regex_provider
13
14SID_ALLOCATION: Mapping[str, Sequence[tuple[int, int]]] = {
15 "local": [(1000000, 1999999)],
16 "ET OPEN": [
17 (2000000, 2103999),
18 (2400000, 2609999),
19 ],
20 "ET": [(2700000, 2799999)],
21 "ETPRO": [(2800000, 2899999)],
22}
23
24_regex_provider = get_regex_provider()
25
26_MSG_PREFIX_REGEX = _regex_provider.compile(r"^\"([A-Z0-9 ]*).*\"$")
27
28_logger = logging.getLogger(__name__)
29
30
[docs]
31class SidChecker(CheckerInterface):
32 """The `SidChecker` contains several checks based on the Suricata SID allocation.
33
34 Specifically, the `SidChecker` checks for the following:
35 S300: Allocation to reserved SID range, whereas no range is reserved for the rule.
36
37 S301: Allocation to unallocated SID range, whereas local range should be used.
38
39 S302: Allocation to wrong reserved SID range, whereas another reserved range should be used.
40
41 S303: Allocation to unallocated SID range, whereas a reserved range should be used.
42 """
43
44 codes = {
45 "S300": {"severity": logging.INFO},
46 "S301": {"severity": logging.INFO},
47 "S302": {"severity": logging.INFO},
48 "S303": {"severity": logging.INFO},
49 }
50
51 def _check_rule(
52 self: "SidChecker",
53 rule: idstools.rule.Rule,
54 ) -> ISSUES_TYPE:
55 issues: ISSUES_TYPE = []
56
57 sid = get_rule_option(rule, "sid")
58 msg = get_rule_option(rule, "msg")
59
60 if sid is not None and msg is not None:
61 sid = int(sid)
62 range_name = self.__get_range_name(sid, SID_ALLOCATION)
63 prefix = self.__get_msg_prefix(msg)
64
65 if (
66 prefix not in SID_ALLOCATION.keys()
67 and range_name is not None
68 and range_name != "local"
69 ):
70 issues.append(
71 Issue(
72 code="S300",
73 message=f"""\
74Allocation to reserved SID range, whereas no range is reserved for the rule.
75Consider using an sid in one of the following ranges: {SID_ALLOCATION["local"]}.\
76""",
77 ),
78 )
79
80 if prefix not in SID_ALLOCATION.keys() and range_name is None:
81 issues.append(
82 Issue(
83 code="S301",
84 message=f"""\
85Allocation to unallocated SID range, whereas local range should be used.
86Consider using an sid in one of the following ranges: {SID_ALLOCATION["local"]}.\
87""",
88 ),
89 )
90
91 if prefix in SID_ALLOCATION.keys() and (
92 range_name is not None
93 and not (prefix + " ").startswith(range_name + " ")
94 and not (range_name + " ").startswith(prefix + " ")
95 ):
96 issues.append(
97 Issue(
98 code="S302",
99 message=f"""\
100Allocation to wrong reserved SID range, whereas another reserved range should be used.
101Consider using an sid in one of the following ranges: {SID_ALLOCATION[prefix]}.\
102""",
103 ),
104 )
105
106 if prefix in SID_ALLOCATION.keys() and range_name is None:
107 issues.append(
108 Issue(
109 code="S303",
110 message=f"""\
111Allocation to unallocated SID range, whereas a reserved range should be used.
112Consider using an sid in one of the following ranges: {SID_ALLOCATION[prefix]}.\
113""",
114 ),
115 )
116
117 return issues
118
119 @staticmethod
120 def __in_range(sid: int, sid_range: Sequence[tuple[int, int]]) -> bool:
121 for start, end in sid_range:
122 if start <= sid <= end:
123 return True
124
125 return False
126
127 @staticmethod
128 def __get_range_name(
129 sid: int,
130 ranges: Mapping[str, Sequence[tuple[int, int]]],
131 ) -> Optional[str]:
132 for range_name, sid_range in ranges.items():
133 for start, end in sid_range:
134 if start <= sid <= end:
135 _logger.debug("Detected sid from range: %s", range_name)
136 return range_name
137 return None
138
139 @staticmethod
140 def __get_msg_prefix(msg: str) -> str:
141 match = _MSG_PREFIX_REGEX.match(msg)
142 assert match is not None
143
144 parts = match.group(1).strip().split(" ")
145 prefix: str = ""
146 for i in list(reversed(range(len(parts)))):
147 prefix = " ".join(parts[: i + 1])
148 if prefix in SID_ALLOCATION.keys() or " " not in prefix:
149 break
150
151 _logger.debug("Detected prefix: %s", prefix)
152
153 return prefix