1"""The `suricata_check.checkers.interface.checker` module contains the `CheckerInterface`.
2
3Implementation of the `CheckerInterface` is neccessary for checker auto-discovery.
4"""
5
6import abc
7import logging
8from collections.abc import Iterable, Mapping
9from typing import Optional
10
11from suricata_check.utils.checker import get_rule_option, is_rule_option_set
12from suricata_check.utils.checker_typing import ISSUES_TYPE
13from suricata_check.utils.rule import Rule
14
15_logger = logging.getLogger(__name__)
16
17
[docs]
18class CheckerInterface:
19 """Interface for rule checkers returning a list of issues.
20
21 These checkers are automatically discovered through `suricata_check.get_checkers()`.
22
23 Each code should start with an upper case letter (may be multiple), followed by three digits.
24 In other words, each code should follow the following regex `[A-Z]{1,}[0-9]{3}`
25
26 We recommend using a letter to indicate the category of the issue, such as described in `README.md`.
27 Moreover, we suggest to reserve certain ranges of numbers for each checker.
28
29 """
30
31 codes: Mapping[str, Mapping[str, int]]
32 """A Mapping of issue codes emitted by the checker to metadata for those issue types.
33 The metadata is structured in the form of a Mapping from attribute name to attribute value.
34 The one mandatory metadata attribute is severity, which must be one of the levels provided by the `logging` module"""
35
36 enabled_by_default: bool = True
37 """A boolean indicating if the checker is enabled by default when discovered automatically."""
38
39 def __init__(
40 self: "CheckerInterface",
41 include: Optional[Iterable[str]] = None,
42 ) -> None:
43 """Initializes the checker given a list of issue codes to emit."""
44 if include is None:
45 include = self.codes
46 _logger.debug("%s enabled with codes: %s", type(self).__name__, include)
47 self.include = include
48
49 super().__init__()
50
[docs]
51 def check_rule(
52 self: "CheckerInterface",
53 rule: Rule,
54 ) -> ISSUES_TYPE:
55 """Checks a rule and returns a list of issues found."""
56 self.__log_rule_processing(rule)
57 return self.__add_checker_metadata(
58 self.__add_issue_metadata(self.__filter_issues(self._check_rule(rule))),
59 )
60
61 @abc.abstractmethod
62 def _check_rule(
63 self: "CheckerInterface",
64 rule: Rule,
65 ) -> ISSUES_TYPE:
66 """Checks a rule and returns a list of issues found."""
67
68 def __log_rule_processing(
69 self: "CheckerInterface",
70 rule: Rule,
71 ) -> None:
72 sid: Optional[int] = None
73 if is_rule_option_set(rule, "sid"):
74 sid_str = get_rule_option(rule, "sid")
75 assert sid_str is not None
76 sid = int(sid_str)
77
78 _logger.debug("Running %s on rule %s", self.__class__.__name__, sid)
79
80 def __add_issue_metadata(
81 self: "CheckerInterface",
82 issues: ISSUES_TYPE,
83 ) -> ISSUES_TYPE:
84 """Given a list of issues, return the same list with metadata from the issue types."""
85 for issue in issues:
86 metadata = self.codes[issue.code]
87 if "severity" in metadata:
88 issue.severity = metadata["severity"]
89
90 return issues
91
92 def __add_checker_metadata(
93 self: "CheckerInterface",
94 issues: ISSUES_TYPE,
95 ) -> ISSUES_TYPE:
96 """Given a list of issues, return the same list with metadata from the checker."""
97 name = self.__class__.__name__
98
99 for issue in issues:
100 issue.checker = name
101
102 return issues
103
104 def __filter_issues(
105 self: "CheckerInterface",
106 issues: ISSUES_TYPE,
107 ) -> ISSUES_TYPE:
108 """Given a list of issues, return the same list having filtered out disabled issue types."""
109 filtered_issues = []
110
111 for issue in issues:
112 if issue.code in self.include:
113 filtered_issues.append(issue)
114 elif issue.code not in self.codes:
115 _logger.warning(
116 "Issue with filtered code %s not found in checker %s",
117 issue.code,
118 self.__class__.__name__,
119 )
120
121 return filtered_issues