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