1"""`MsgChecker`."""
2
3import logging
4
5import idstools.rule
6
7from suricata_check.checkers.interface import CheckerInterface
8from suricata_check.utils.checker import (
9 get_rule_option,
10 is_rule_option_equal_to_regex,
11 is_rule_option_set,
12 is_rule_suboption_set,
13)
14from suricata_check.utils.checker_typing import ISSUES_TYPE, Issue
15from suricata_check.utils.regex import get_regex_provider
16
17_regex_provider = get_regex_provider()
18
19_S400_REGEX = _regex_provider.compile(r"""^"[A-Z0-9 ]+ [A-Z0-9_]+ .*"$""")
20_MALWARE_REGEX = _regex_provider.compile(r"^.*(malware).*$", _regex_provider.IGNORECASE)
21_S401_REGEX = _regex_provider.compile(r"""^".* [a-zA-Z0-9]+/[a-zA-Z0-9]+ .*"$""")
22_VAGUE_KEYWORDS = ("possible", "unknown")
23_S402_REGEX = _regex_provider.compile(
24 r"^.*({}).*$".format("|".join(_VAGUE_KEYWORDS)), _regex_provider.IGNORECASE
25)
26_UNDESIRABLE_DATE_REGEXES = (
27 _regex_provider.compile(r"^.*(\d{4}/\d{2}/\d{2}).*$", _regex_provider.IGNORECASE),
28 _regex_provider.compile(r"^.*(\d{4}-[2-9]\d-\d{2}).*$", _regex_provider.IGNORECASE),
29) # Desirable format is ISO (YYYY-MM-DD)
30_S404_REGEX = _regex_provider.compile(
31 r"^.*(C2|C&C|Command and Control|Command & Control).*$", _regex_provider.IGNORECASE
32)
33_S405_REGEX = _regex_provider.compile(
34 r"^.*(Go|MSIL|ELF64|MSIL|JS|Win32|DOS|Amiga|C64|Plan9).*$",
35 _regex_provider.IGNORECASE,
36)
37_S406_REGEX = _regex_provider.compile(
38 r"^.*((\w+\.)+[a-z]{2,}).*$",
39 _regex_provider.IGNORECASE,
40)
41_S407_REGEX = _regex_provider.compile(
42 r"^.*((\w+\[\.\])+[a-z]{2,}).*$",
43 _regex_provider.IGNORECASE,
44)
45_S408_REGEX = _regex_provider.compile(
46 r"^.*((\w+\. )+[a-z]{2,}).*$",
47 _regex_provider.IGNORECASE,
48)
49
50_logger = logging.getLogger(__name__)
51
52
[docs]
53class MsgChecker(CheckerInterface):
54 """The `MsgChecker` contains several checks based for the Msg option in Suricata rules.
55
56 Codes S400-S410 report on non-standard `msg` fields.
57 """
58
59 codes = {
60 "S400": {"severity": logging.INFO},
61 "S401": {"severity": logging.INFO},
62 "S402": {"severity": logging.INFO},
63 "S403": {"severity": logging.INFO},
64 "S404": {"severity": logging.INFO},
65 "S405": {"severity": logging.INFO},
66 "S406": {"severity": logging.WARNING},
67 "S407": {"severity": logging.INFO},
68 "S408": {"severity": logging.INFO},
69 "S409": {"severity": logging.INFO},
70 }
71
72 def _check_rule( # noqa: C901
73 self: "MsgChecker",
74 rule: idstools.rule.Rule,
75 ) -> ISSUES_TYPE:
76 issues: ISSUES_TYPE = []
77
78 if is_rule_option_set(rule, "msg") and not is_rule_option_equal_to_regex(
79 rule, "msg", _S400_REGEX
80 ):
81 issues.append(
82 Issue(
83 code="S400",
84 message="""\
85The rule has a non-standard format for the msg field.
86Consider changing the msg field to `RULESET CATEGORY Description`.\
87""",
88 ),
89 )
90
91 if (
92 is_rule_option_set(rule, "msg")
93 and self.__desribes_malware(rule)
94 and not is_rule_option_equal_to_regex(rule, "msg", _S401_REGEX)
95 ):
96 issues.append(
97 Issue(
98 code="S401",
99 message="""\
100The rule describes malware but does not specify the paltform or malware family in the msg field.
101Consider changing the msg field to include `Platform/malfamily`.\
102""",
103 ),
104 )
105
106 if not (
107 is_rule_option_set(rule, "noalert")
108 or is_rule_suboption_set(rule, "flowbits", "noalert")
109 ) and is_rule_option_equal_to_regex(rule, "msg", _S402_REGEX):
110 issues.append(
111 Issue(
112 code="S402",
113 message="""\
114The rule uses vague keywords such as possible or unknown in the msg field.
115Consider rephrasing to provide a more clear message for interpreting generated alerts.\
116""",
117 ),
118 )
119
120 for regex in _UNDESIRABLE_DATE_REGEXES:
121 if is_rule_option_equal_to_regex(rule, "msg", regex):
122 issues.append(
123 Issue(
124 code="S403",
125 message="""\
126The rule uses a non-ISO date in the msg field.
127Consider reformatting the date to ISO format (YYYY-MM-DD).\
128""",
129 ),
130 )
131 break
132
133 if is_rule_option_equal_to_regex(rule, "msg", _S404_REGEX):
134 issues.append(
135 Issue(
136 code="S404",
137 message="""\
138The rule uses a different way of writing CnC (Command & Control) in the msg field.
139Consider writing CnC instead.\
140""",
141 ),
142 )
143
144 if self.__desribes_malware(rule) and not is_rule_option_equal_to_regex(
145 rule, "msg", _S405_REGEX
146 ):
147 issues.append(
148 Issue(
149 code="S405",
150 message="""\
151The rule likely detects malware but does not specify the file type in the msg field.
152Consider specifying a file type such as `DOS` or `ELF64`.\
153""",
154 ),
155 )
156
157 if is_rule_option_equal_to_regex(rule, "msg", _S406_REGEX):
158 issues.append(
159 Issue(
160 code="S406",
161 message="""\
162The rule specifies a domain name without escaping the label seperators.
163Consider escaping the domain names by putting a space before the dot like `foo .bar` to prevent information leaks.\
164""",
165 ),
166 )
167
168 if is_rule_option_equal_to_regex(rule, "msg", _S407_REGEX):
169 issues.append(
170 Issue(
171 code="S407",
172 message="""\
173The rule specifies a domain name and escapes it in a non-standard way in the msg field.
174Consider escaping the domain names by putting a space before the dot like `foo .bar`.\
175""",
176 ),
177 )
178
179 if is_rule_option_equal_to_regex(rule, "msg", _S408_REGEX):
180 issues.append(
181 Issue(
182 code="S408",
183 message="""\
184The rule specifies a domain name and escapes it in a non-standard way in the msg field.
185Consider escaping the domain names by putting a space before the dot like `foo .bar`.\
186""",
187 ),
188 )
189
190 # Note that all characters under 128 are ASCII
191 if is_rule_option_set(rule, "msg") and any(
192 ord(c) > 128 # noqa: PLR2004
193 for c in get_rule_option(rule, "msg") # type: ignore reportOptionalIterable
194 ):
195 issues.append(
196 Issue(
197 code="S409",
198 message="""\
199The rule uses non-ASCII characters in the msg field.
200Consider removing non-ASCII characters.\
201""",
202 ),
203 )
204
205 return issues
206
207 @staticmethod
208 def __desribes_malware(rule: idstools.rule.Rule) -> bool:
209 if is_rule_suboption_set(rule, "metadata", "malware_family"):
210 return True
211
212 if is_rule_option_equal_to_regex(rule, "msg", _MALWARE_REGEX):
213 return True
214
215 _logger.debug("Rule does not describe malware: %s", rule["raw"])
216
217 return False