1"""`WhitespaceChecker`."""
2
3import logging
4from types import MappingProxyType
5
6from suricata_check.checkers.interface import CheckerInterface
7from suricata_check.utils.checker import (
8 is_rule_option_equal_to_regex,
9)
10from suricata_check.utils.checker_typing import ISSUES_TYPE, Issue
11from suricata_check.utils.regex import (
12 HEADER_REGEX,
13)
14from suricata_check.utils.regex_provider import get_regex_provider
15from suricata_check.utils.rule import Rule
16
17_regex_provider = get_regex_provider()
18
19# Regular expressions are placed here such that they are compiled only once.
20# This has a significant impact on the performance.
21REGEX_S100 = _regex_provider.compile(
22 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\( .*\)\s*(#.*)?$",
23)
24REGEX_S101 = _regex_provider.compile(
25 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.* \)\s*(#.*)?$",
26)
27REGEX_S102 = _regex_provider.compile(
28 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.+ :.+\)\s*(#.*)?$",
29)
30REGEX_S103 = _regex_provider.compile(
31 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.+: .+\)\s*(#.*)?$",
32)
33REGEX_S104 = _regex_provider.compile(
34 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.+ ;.+\)\s*(#.*)?$",
35)
36REGEX_S105 = _regex_provider.compile(
37 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.+; \s+.+\)\s*(#.*)?$",
38)
39REGEX_S106 = _regex_provider.compile(r'^".*\|.* .*\|.*"$')
40REGEX_S110 = _regex_provider.compile(
41 rf"^(\s*#)?\s*{HEADER_REGEX.pattern}\s*\(.+;(?! ).+\)\s*(#.*)?$",
42)
43REGEX_S111 = _regex_provider.compile(r'^".*\|.*[a-fA-F0-9]{4}.*\|.*"$')
44REGEX_S120 = _regex_provider.compile(
45 r'^"([^\|]*|(\|[\sa-zA-Z0-9]*\|))*(\\?[\x3a\x3b\x20\x27\x7b\x5c\x2f\x60\x24\x28\x29]+|\\[\x22\x7c]+)([^\|]*|(\|[\sa-zA-Z0-9]*\|))*"$',
46)
47REGEX_S121 = _regex_provider.compile(
48 r"^\"/.*(\\?[\x3a\x3b\x20\x22\x27\x2f\x60]+|\\[\x7b\x5c\x7c\x24\x28\x29]+).*/[ism]*\"$",
49)
50REGEX_S122 = _regex_provider.compile(r'^".*\\.*"$')
51REGEX_S123 = _regex_provider.compile(
52 r'^".*(?!\\(a|c[0-127]|e|f|n|r|t|0[0-9]{2}|[0-9]{3}|0\{[0-9]{3}\}|x[0-9a-f]{2}|x[0-9a-f]{3}|u[0-9a-f]{4}|d|D|h|H|s|S|v|V|w|W))(\\.).*"$',
53)
54
55
[docs]
56class WhitespaceChecker(CheckerInterface):
57 """The `WhitespaceChecker` contains several checks based on the Suricata Style guide relating to formatting rules.
58
59 Codes S100-S109 report on unneccessary whitespace that should be removed.
60
61 Codes S110-S119 report on missing whitespace that should be added.
62
63 Codes S120-S129 report on non-standard escaping of special characters.
64 """
65
66 codes = MappingProxyType(
67 {
68 "S100": {"severity": logging.INFO},
69 "S101": {"severity": logging.INFO},
70 "S102": {"severity": logging.INFO},
71 "S103": {"severity": logging.INFO},
72 "S104": {"severity": logging.INFO},
73 "S105": {"severity": logging.INFO},
74 "S106": {"severity": logging.INFO},
75 "S110": {"severity": logging.INFO},
76 "S111": {"severity": logging.INFO},
77 "S120": {"severity": logging.INFO},
78 "S121": {"severity": logging.INFO},
79 "S122": {"severity": logging.INFO},
80 "S123": {"severity": logging.INFO},
81 },
82 )
83
84 def _check_rule( # noqa: C901, PLR0912
85 self: "WhitespaceChecker",
86 rule: Rule,
87 ) -> ISSUES_TYPE:
88 issues: ISSUES_TYPE = []
89
90 if (
91 REGEX_S100.match(
92 rule.raw.strip(),
93 )
94 is not None
95 ):
96 issues.append(
97 Issue(
98 code="S100",
99 message="""The rule contains unneccessary whitespace after opening the rule body with.
100Consider removing the unneccessary whitespace.""",
101 ),
102 )
103
104 if (
105 REGEX_S101.match(
106 rule.raw.strip(),
107 )
108 is not None
109 ):
110 issues.append(
111 Issue(
112 code="S101",
113 message="""The rule contains unneccessary whitespace before closing the rule body with.
114Consider removing the unneccessary whitespace.""",
115 ),
116 )
117
118 if (
119 REGEX_S102.match(
120 rule.raw.strip(),
121 )
122 is not None
123 ):
124 issues.append(
125 Issue(
126 code="S102",
127 message="""The rule contains unneccessary whitespace before the colon (:) after an option name.
128Consider removing the unneccessary whitespace.""",
129 ),
130 )
131
132 if (
133 REGEX_S103.match(
134 rule.raw.strip(),
135 )
136 is not None
137 ):
138 issues.append(
139 Issue(
140 code="S103",
141 message="""The rule contains unneccessary whitespace before the colon (:) after an option name.
142Consider removing the unneccessary whitespace.""",
143 ),
144 )
145
146 if (
147 REGEX_S104.match(
148 rule.raw.strip(),
149 )
150 is not None
151 ):
152 issues.append(
153 Issue(
154 code="S104",
155 message="""The rule contains unneccessary whitespace before the semicolon (;) after an option value.
156Consider removing the unneccessary whitespace.""",
157 ),
158 )
159
160 if (
161 REGEX_S105.match(
162 rule.raw.strip(),
163 )
164 is not None
165 ):
166 issues.append(
167 Issue(
168 code="S105",
169 message="""The rule contains more than one space between options after an option value.
170Consider replacing the unneccessary whitespace by a single space.""",
171 ),
172 )
173
174 if is_rule_option_equal_to_regex(
175 rule,
176 "content",
177 REGEX_S106,
178 ):
179 issues.append(
180 Issue(
181 code="S106",
182 message="""The rule contains more than one space between bytes in content.
183Consider replacing the unneccessary whitespace by a single space.""",
184 ),
185 )
186
187 if (
188 REGEX_S110.match(
189 rule.raw.strip(),
190 )
191 is not None
192 ):
193 issues.append(
194 Issue(
195 code="S110",
196 message="""The rule does not contain a space between the end of after an option value.
197Consider adding a single space.""",
198 ),
199 )
200
201 if is_rule_option_equal_to_regex(
202 rule,
203 "content",
204 REGEX_S111,
205 ):
206 issues.append(
207 Issue(
208 code="S111",
209 message="""The rule contains more than no spaces between bytes in content.
210Consider replacing adding a single space.""",
211 ),
212 )
213
214 if is_rule_option_equal_to_regex(
215 rule,
216 "content",
217 REGEX_S120,
218 ):
219 issues.append(
220 Issue(
221 code="S120",
222 message="""The rule did not escape \
223(\\x3a\\x3b\\x20\\x22\\x27\\x7b\\x7c\\x5c\\x2f\\x60\\x24\\x28\\x29) in a content field.
224Consider using hex encoding instead.""",
225 ),
226 )
227
228 if is_rule_option_equal_to_regex(
229 rule,
230 "pcre",
231 REGEX_S121,
232 ):
233 issues.append(
234 Issue(
235 code="S121",
236 message="""The rule did escape \
237(\\x3a\\x3b\\x20\\x22\\x27\\x7b\\x7c\\x5c\\x2f\\x60\\x24\\x28\\x29) in a pcre field.
238Consider using hex encoding instead.""",
239 ),
240 )
241
242 if is_rule_option_equal_to_regex(rule, "content", REGEX_S122):
243 issues.append(
244 Issue(
245 code="S122",
246 message="""The rule escaped special characters using a blackslash (\\) in a content field.
247Consider using hex encoding instead.""",
248 ),
249 )
250
251 if is_rule_option_equal_to_regex(rule, "pcre", REGEX_S123):
252 issues.append(
253 Issue(
254 code="S123",
255 message="""The rule escaped special characters using a blackslash (\\) in a pcre field.
256Consider using hex encoding instead.""",
257 ),
258 )
259
260 return issues