1"""`SidChecker`."""
2
3import logging
4from collections.abc import Mapping, Sequence
5from types import MappingProxyType
6from typing import Optional
7
8from suricata_check.checkers.interface import CheckerInterface
9from suricata_check.utils.checker import get_rule_option
10from suricata_check.utils.checker_typing import ISSUES_TYPE, Issue
11from suricata_check.utils.regex_provider import get_regex_provider
12from suricata_check.utils.rule import Rule
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 = MappingProxyType(
45 {
46 "S300": {"severity": logging.INFO},
47 "S301": {"severity": logging.INFO},
48 "S302": {"severity": logging.INFO},
49 "S303": {"severity": logging.INFO},
50 },
51 )
52
53 def _check_rule(
54 self: "SidChecker",
55 rule: Rule,
56 ) -> ISSUES_TYPE:
57 issues: ISSUES_TYPE = []
58
59 sid = get_rule_option(rule, "sid")
60 msg = get_rule_option(rule, "msg")
61
62 if sid is not None and msg is not None:
63 sid = int(sid)
64 range_name = self.__get_range_name(sid, SID_ALLOCATION)
65 prefix = self.__get_msg_prefix(msg)
66
67 if (
68 prefix not in SID_ALLOCATION.keys()
69 and range_name is not None
70 and range_name != "local"
71 ):
72 issues.append(
73 Issue(
74 code="S300",
75 message=f"""\
76Allocation to reserved SID range, whereas no range is reserved for the rule.
77Consider using an sid in one of the following ranges: {SID_ALLOCATION["local"]}.\
78""",
79 ),
80 )
81
82 if prefix not in SID_ALLOCATION.keys() and range_name is None:
83 issues.append(
84 Issue(
85 code="S301",
86 message=f"""\
87Allocation to unallocated SID range, whereas local range should be used.
88Consider using an sid in one of the following ranges: {SID_ALLOCATION["local"]}.\
89""",
90 ),
91 )
92
93 if prefix in SID_ALLOCATION.keys() and (
94 range_name is not None
95 and not (prefix + " ").startswith(range_name + " ")
96 and not (range_name + " ").startswith(prefix + " ")
97 ):
98 issues.append(
99 Issue(
100 code="S302",
101 message=f"""\
102Allocation to wrong reserved SID range, whereas another reserved range should be used.
103Consider using an sid in one of the following ranges: {SID_ALLOCATION[prefix]}.\
104""",
105 ),
106 )
107
108 if prefix in SID_ALLOCATION.keys() and range_name is None:
109 issues.append(
110 Issue(
111 code="S303",
112 message=f"""\
113Allocation to unallocated SID range, whereas a reserved range should be used.
114Consider using an sid in one of the following ranges: {SID_ALLOCATION[prefix]}.\
115""",
116 ),
117 )
118
119 return issues
120
121 @staticmethod
122 def __in_range(sid: int, sid_range: Sequence[tuple[int, int]]) -> bool:
123 for start, end in sid_range:
124 if start <= sid <= end:
125 return True
126
127 return False
128
129 @staticmethod
130 def __get_range_name(
131 sid: int,
132 ranges: Mapping[str, Sequence[tuple[int, int]]],
133 ) -> Optional[str]:
134 for range_name, sid_range in ranges.items():
135 for start, end in sid_range:
136 if start <= sid <= end:
137 _logger.debug("Detected sid from range: %s", range_name)
138 return range_name
139 return None
140
141 @staticmethod
142 def __get_msg_prefix(msg: str) -> str:
143 match = _MSG_PREFIX_REGEX.match(msg)
144 assert match is not None
145
146 parts = match.group(1).strip().split(" ")
147 prefix: str = ""
148 for i in list(reversed(range(len(parts)))):
149 prefix = " ".join(parts[: i + 1])
150 if prefix in SID_ALLOCATION.keys() or " " not in prefix:
151 break
152
153 _logger.debug("Detected prefix: %s", prefix)
154
155 return prefix