Source code for suricata_check.checkers.styleguide.overall

  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