Source code for suricata_check.checkers.styleguide.overall

  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