1"""`OverallChecker`."""
2
3import logging
4from types import MappingProxyType
5
6from suricata_check.checkers.interface import CheckerInterface
7from suricata_check.utils.checker import (
8 count_rule_options,
9 get_all_variable_groups,
10 get_rule_option,
11 get_rule_sticky_buffer_naming,
12 is_rule_option_equal_to,
13 is_rule_option_equal_to_regex,
14 is_rule_option_one_of,
15 is_rule_option_set,
16)
17from suricata_check.utils.checker_typing import ISSUES_TYPE, Issue
18from suricata_check.utils.regex import (
19 ALL_VARIABLES,
20 CLASSTYPES,
21)
22from suricata_check.utils.regex_provider import get_regex_provider
23from suricata_check.utils.rule import Rule
24
25_regex_provider = get_regex_provider()
26
27
28# Regular expressions are placed here such that they are compiled only once.
29# This has a significant impact on the performance.
30_REGEX_S002a = _regex_provider.compile(
31 r"^.*(EXPLOIT|CVE).*$",
32 _regex_provider.IGNORECASE,
33)
34_REGEX_S002b = _regex_provider.compile(
35 r"^.*(Internal|Inbound|Outbound).*$",
36 _regex_provider.IGNORECASE,
37)
38_REGEX_S030 = _regex_provider.compile(r"^[a-z\-]+$")
39_REGEX_S031 = _regex_provider.compile(r"^[^\|]*\|[^\|]*[A-Z]+[^\|]*\|[^\|]*$")
40
41
[docs]
42class OverallChecker(CheckerInterface):
43 """The `OverallChecker` contains several the most basic checks for Suricata rules.
44
45 Codes S000-S009 report on issues with the direction of the rule.
46
47 Codes S010-S019 report on issues pertaining to the usage of non-standard options.
48
49 Codes S020-S029 report on issues pertaining to the non-usage of recommended options.
50
51 Codes S020-S029 report on issues pertaining to the non-usage of recommended options.
52
53 Codes S031-S039 report on issues pertaining to the inappropriate usage of options.
54 """
55
56 codes = MappingProxyType(
57 {
58 "S000": {"severity": logging.INFO},
59 "S001": {"severity": logging.INFO},
60 "S002": {"severity": logging.INFO},
61 "S010": {"severity": logging.INFO},
62 "S011": {"severity": logging.INFO},
63 "S012": {"severity": logging.INFO},
64 "S013": {"severity": logging.INFO},
65 "S014": {"severity": logging.INFO},
66 "S020": {"severity": logging.INFO},
67 "S021": {"severity": logging.INFO},
68 "S030": {"severity": logging.INFO},
69 "S031": {"severity": logging.INFO},
70 },
71 )
72
73 def _check_rule( # noqa: C901
74 self: "OverallChecker",
75 rule: Rule,
76 ) -> ISSUES_TYPE:
77 issues: ISSUES_TYPE = []
78
79 if is_rule_option_equal_to(rule, "direction", "<->") or (
80 is_rule_option_equal_to(rule, "source_addr", "any")
81 and is_rule_option_equal_to(rule, "dest_addr", "any")
82 ):
83 issues.append(
84 Issue(
85 code="S000",
86 message="""The rule did not specificy an inbound or outbound direction.
87Consider constraining the rule to a specific direction such as INBOUND or OUTBOUND traffic.""",
88 ),
89 )
90
91 if is_rule_option_set(rule, "dns.query") and not is_rule_option_equal_to(
92 rule,
93 "dest_addr",
94 "any",
95 ):
96 issues.append(
97 Issue(
98 code="S001",
99 message="""The rule detects certain dns queries and has dest_addr not set to any \
100causing the rule to be specific to either local or external resolvers.
101Consider setting dest_addr to any.""",
102 ),
103 )
104
105 if (
106 is_rule_option_equal_to_regex(rule, "msg", _REGEX_S002a)
107 and not (
108 is_rule_option_equal_to(rule, "source_addr", "any")
109 and is_rule_option_equal_to(rule, "dest_addr", "any")
110 )
111 and not is_rule_option_equal_to_regex(rule, "msg", _REGEX_S002b)
112 ):
113 issues.append(
114 Issue(
115 code="S002",
116 message="""The rule detects exploitation attempts in a constrained direction \
117without specifying the direction in the rule msg. \
118Consider setting `src_addr` and `dest_addr` to any to also account for lateral movement scenarios. \
119Alternatively, you can specify the direction (i.e., `Internal` or `Inbound`) in the rule `msg`.""",
120 ),
121 )
122
123 # In the suricata style guide, this is mentioned as `packet_data`
124 if is_rule_option_set(rule, "pkt_data"):
125 issues.append(
126 Issue(
127 code="S010",
128 message="""The rule uses the pkt_data option, \
129which resets the inspection pointer resulting in confusing and disjoint logic.
130Consider replacing the detection logic.""",
131 ),
132 )
133
134 if is_rule_option_set(rule, "priority"):
135 issues.append(
136 Issue(
137 code="S011",
138 message="""The rule uses priority option, which overrides operator tuning via classification.conf.
139Consider removing the option.""",
140 ),
141 )
142
143 for sticky_buffer, modifier_alternative in get_rule_sticky_buffer_naming(rule):
144 issues.append(
145 Issue(
146 code="S012",
147 message=f"""The rule uses sticky buffer naming in the {sticky_buffer} option, which is complicated.
148Consider using the {modifier_alternative} option instead.""",
149 ),
150 )
151
152 for variable_group in self.__get_invented_variable_groups(rule):
153 issues.append(
154 Issue(
155 code="S013",
156 message=f"""The rule uses a self-invented variable group ({variable_group}), \
157which may be undefined in many environments.
158Consider using the a standard variable group instead.""",
159 ),
160 )
161
162 if is_rule_option_set(rule, "classtype") and not is_rule_option_one_of(
163 rule,
164 "classtype",
165 CLASSTYPES,
166 ):
167 issues.append(
168 Issue(
169 code="S014",
170 message=f"""The rule uses a self-invented classtype ({get_rule_option(rule, 'classtype')}), \
171which may be undefined in many environments.
172Consider using the a standard classtype instead.""",
173 ),
174 )
175
176 if not is_rule_option_set(rule, "content"):
177 issues.append(
178 Issue(
179 code="S020",
180 message="""The detection logic does not use the content option, \
181which is can cause significant runtime overhead.
182Consider adding a content match.""",
183 ),
184 )
185
186 if (
187 not is_rule_option_set(rule, "fast_pattern")
188 and count_rule_options(rule, "content") > 1
189 ):
190 issues.append(
191 Issue(
192 code="S021",
193 message="""The rule has multiple content matches but does not use fast_pattern.
194Consider assigning fast_pattern to the most unique content match.""",
195 ),
196 )
197
198 if is_rule_option_equal_to_regex(
199 rule,
200 "app-layer-protocol",
201 _REGEX_S030,
202 ):
203 issues.append(
204 Issue(
205 code="S030",
206 message="""The rule uses app-layer-protocol to assert the protocol.
207Consider asserting this in the head instead using {} {} {} {} {} {} {}""".format(
208 get_rule_option(rule, "action"),
209 get_rule_option(rule, "app-layer-protocol"),
210 get_rule_option(rule, "source_addr"),
211 get_rule_option(rule, "source_port"),
212 get_rule_option(rule, "direction"),
213 get_rule_option(rule, "dest_addr"),
214 get_rule_option(rule, "dest_port"),
215 ),
216 ),
217 )
218
219 if is_rule_option_equal_to_regex(
220 rule,
221 "content",
222 _REGEX_S031,
223 ):
224 issues.append(
225 Issue(
226 code="S031",
227 message="The rule uses uppercase A-F in a hex content match.\nConsider using lowercase a-f instead.",
228 ),
229 )
230
231 return issues
232
233 @staticmethod
234 def __get_invented_variable_groups(
235 rule: Rule,
236 ) -> list[str]:
237 variable_groups = get_all_variable_groups(rule)
238
239 invented_variable_groups = []
240
241 for variable_group in variable_groups:
242 if variable_group not in ALL_VARIABLES:
243 invented_variable_groups.append(variable_group)
244
245 return invented_variable_groups