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