Source code for suricata_check.checkers.styleguide._overall

  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