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