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