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 suppressed_issues: int = 0
78
79 _issues: ISSUES_TYPE = field(default_factory=list, init=False)
80
81 @property
82 def issues(self: "RuleReport") -> ISSUES_TYPE:
83 """List of issues found in the rule."""
84 return self._issues
85
[docs]
86 def add_issue(self: "RuleReport", issue: Issue) -> None:
87 """Adds an issue to the report."""
88 self._issues.append(issue)
89
[docs]
90 def add_issues(self: "RuleReport", issues: ISSUES_TYPE) -> None:
91 """Adds an issue to the report."""
92 for issue in issues:
93 self._issues.append(issue)
94
[docs]
95 def to_dict(self: "RuleReport") -> dict[str, str]:
96 """Returns the RuleReport represented as a dictionary."""
97 d = {
98 "rule": self.rule.raw,
99 "issues": [issue.to_dict() for issue in self.issues],
100 }
101
102 if self.summary is not None:
103 d["summary"] = self.summary
104
105 if self.line_begin is not None or self.line_end is not None:
106 d["lines"] = {}
107
108 if self.line_begin is not None:
109 d["lines"]["begin"] = self.line_begin
110
111 if self.line_begin is not None:
112 d["lines"]["end"] = self.line_end
113
114 return d
115
[docs]
116 def __repr__(self: "RuleReport") -> str:
117 """Returns the RuleReport represented as a string."""
118 return json.dumps(self.to_dict())
119
120
121RULE_REPORTS_TYPE = MutableSequence[RuleReport]
122"""Type representing a sequence of multiple `RuleReport` instances."""
123
124
[docs]
125@dataclass
126class OutputSummary:
127 """The `OutputSummary` dataclass represent a collection of summaries on the output of `suricata_check`."""
128
129 overall_summary: SIMPLE_SUMMARY_TYPE
130 issues_by_group: SIMPLE_SUMMARY_TYPE
131 issues_by_type: EXTENSIVE_SUMMARY_TYPE
132
133
[docs]
134@dataclass
135class OutputReport:
136 """The `OutputSummary` dataclass represent the `suricata_check`, consisting of rule reports and summaries."""
137
138 _rules: RULE_REPORTS_TYPE = field(default_factory=list, init=False)
139 summary: Optional[OutputSummary] = None
140
141 def __init__(
142 self: "OutputReport",
143 rules: RULE_REPORTS_TYPE = [],
144 summary: Optional[OutputSummary] = None,
145 ) -> None:
146 """Initialized the `OutputReport`, optionally with a list of rules and/or a summary."""
147 self._rules = []
148 for rule in rules:
149 self.add_rule(rule)
150 self.summary = summary
151 super().__init__()
152
153 @property
154 def rules(self: "OutputReport") -> RULE_REPORTS_TYPE:
155 """List of rules contained in the report."""
156 return self._rules
157
[docs]
158 def add_rule(self: "OutputReport", rule_report: RuleReport) -> None:
159 """Adds an rule to the report."""
160 self._rules.append(rule_report)