1"""The `suricata_check.typing` module contains all types used by the `suricata-check` package."""
2
3import json
4from collections.abc import Iterable, MutableMapping, MutableSequence
5from dataclasses import dataclass, field
6from typing import (
7 Optional,
8 TypeVar,
9)
10
11import idstools.rule
12
13
[docs]
14class InvalidRuleError(RuntimeError):
15 """Raised when an invalid rule is detected.
16
17 Note that some rules may be invalid due to not following the Suricata rule syntax.
18 Rules following the syntax, but considered invalid by Suricata due to missing options need not raise this error.
19 Rules for which this error is not raised are not neccessarily syntactically correct but can be processed by suricata-check.
20 """
21
22 def __init__(self: "InvalidRuleError", message: str) -> None:
23 """Initializes the `InvalidRuleError` with the raw rule as message."""
24 super().__init__(message)
25
26
[docs]
27@dataclass
28class Issue:
29 """The `Issue` dataclass represents a single issue found in a rule."""
30
31 code: str
32 message: str
33 severity: Optional[int] = None
34 checker: Optional[str] = None
35
[docs]
36 def to_dict(self: "Issue") -> dict[str, str]:
37 """Returns the Issue represented as a dictionary."""
38 d = {
39 "code": self.code,
40 "message": self.message,
41 }
42
43 if self.checker is not None:
44 d["checker"] = self.checker
45
46 return d
47
48 @property
49 def hash(self: "Issue") -> int:
50 """Returns a unique hash that can be used as a fingerprint for the issue."""
51 return hash(tuple(sorted(self.to_dict().items())))
52
[docs]
53 def __repr__(self: "Issue") -> str:
54 """Returns the Issue represented as a string."""
55 return json.dumps(self.to_dict())
56
57
58ISSUES_TYPE = MutableSequence[Issue]
59SIMPLE_SUMMARY_TYPE = MutableMapping[str, int]
60RULE_SUMMARY_TYPE = SIMPLE_SUMMARY_TYPE
61EXTENSIVE_SUMMARY_TYPE = MutableMapping[str, SIMPLE_SUMMARY_TYPE]
62
63Cls = TypeVar("Cls")
64
65
[docs]
66def get_all_subclasses(cls: type[Cls]) -> Iterable[type[Cls]]:
67 """Returns all class types that subclass the provided type."""
68 all_subclasses = []
69
70 for subclass in cls.__subclasses__():
71 all_subclasses.append(subclass)
72 all_subclasses.extend(get_all_subclasses(subclass))
73
74 return all_subclasses
75
76
[docs]
77@dataclass
78class RuleReport:
79 """The `RuleReport` dataclass represents a rule, together with information on its location and detected issues."""
80
81 rule: idstools.rule.Rule
82 summary: Optional[RULE_SUMMARY_TYPE] = None
83 line_begin: Optional[int] = None
84 line_end: Optional[int] = None
85
86 _issues: ISSUES_TYPE = field(default_factory=list, init=False)
87
88 @property
89 def issues(self: "RuleReport") -> ISSUES_TYPE:
90 """List of issues found in the rule."""
91 return self._issues
92
[docs]
93 def add_issue(self: "RuleReport", issue: Issue) -> None:
94 """Adds an issue to the report."""
95 self._issues.append(issue)
96
[docs]
97 def add_issues(self: "RuleReport", issues: ISSUES_TYPE) -> None:
98 """Adds an issue to the report."""
99 for issue in issues:
100 self._issues.append(issue)
101
[docs]
102 def to_dict(self: "RuleReport") -> dict[str, str]:
103 """Returns the RuleReport represented as a dictionary."""
104 d = {
105 "rule": self.rule["raw"],
106 "issues": [issue.to_dict() for issue in self.issues],
107 }
108
109 if self.summary is not None:
110 d["summary"] = self.summary
111
112 if self.line_begin is not None or self.line_end is not None:
113 d["lines"] = {}
114
115 if self.line_begin is not None:
116 d["lines"]["begin"] = self.line_begin
117
118 if self.line_begin is not None:
119 d["lines"]["end"] = self.line_end
120
121 return d
122
[docs]
123 def __repr__(self: "RuleReport") -> str:
124 """Returns the RuleReport represented as a string."""
125 return json.dumps(self.to_dict())
126
127
128RULE_REPORTS_TYPE = MutableSequence[RuleReport]
129
130
[docs]
131@dataclass
132class OutputSummary:
133 """The `OutputSummary` dataclass represent a collection of summaries on the output of `suricata_check`."""
134
135 overall_summary: SIMPLE_SUMMARY_TYPE
136 issues_by_group: SIMPLE_SUMMARY_TYPE
137 issues_by_type: EXTENSIVE_SUMMARY_TYPE
138
139
[docs]
140@dataclass
141class OutputReport:
142 """The `OutputSummary` dataclass represent the `suricata_check`, consisting of rule reports and summaries."""
143
144 _rules: RULE_REPORTS_TYPE = field(default_factory=list, init=False)
145 summary: Optional[OutputSummary] = None
146
147 def __init__(
148 self: "OutputReport",
149 rules: RULE_REPORTS_TYPE = [],
150 summary: Optional[OutputSummary] = None,
151 ) -> None:
152 """Initialized the `OutputReport`, optionally with a list of rules and/or a summary."""
153 self._rules = []
154 for rule in rules:
155 self.add_rule(rule)
156 self.summary = summary
157 super().__init__()
158
159 @property
160 def rules(self: "OutputReport") -> RULE_REPORTS_TYPE:
161 """List of rules contained in the report."""
162 return self._rules
163
[docs]
164 def add_rule(self: "OutputReport", rule_report: RuleReport) -> None:
165 """Adds an rule to the report."""
166 self._rules.append(rule_report)