cyvest 0.1.0__py3-none-any.whl → 5.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterable
4
- from typing import Any
5
-
6
- from .models import Level, Observable
7
-
8
-
9
- class ObservableRegistry:
10
- """Central store for observables shared across result checks."""
11
-
12
- def __init__(self) -> None:
13
- self._observables: dict[str, Observable] = {}
14
-
15
- def get(self, full_key: str) -> Observable | None:
16
- return self._observables.get(full_key)
17
-
18
- def values(self) -> Iterable[Observable]:
19
- return self._observables.values()
20
-
21
- def upsert(self, observable: Observable) -> Observable:
22
- stored = self._observables.get(observable.full_key)
23
- if stored:
24
- stored.update(observable)
25
- target = stored
26
- else:
27
- self._observables[observable.full_key] = observable
28
- target = observable
29
- self._propagate_whitelist(target)
30
- return target
31
-
32
- def mark_whitelisted(self, observable: Observable) -> None:
33
- observable.whitelisted = True
34
- self._propagate_whitelist(observable)
35
-
36
- def _propagate_whitelist(self, observable: Observable) -> None:
37
- if not observable.whitelisted:
38
- for intel in observable.threat_intels.values():
39
- extra = intel.extra or {}
40
- if extra.get("whitelisted") is True:
41
- observable.whitelisted = True
42
- break
43
- if extra.get("warning_lists"):
44
- observable.whitelisted = True
45
- break
46
- tags = extra.get("tags") or extra.get("labels")
47
- if isinstance(tags, str):
48
- tags_iterable: Iterable[Any] = [tags]
49
- elif isinstance(tags, Iterable):
50
- tags_iterable = tags
51
- else:
52
- tags_iterable = []
53
- if any(str(tag).lower() in {"allow", "trusted", "whitelist"} for tag in tags_iterable):
54
- observable.whitelisted = True
55
- break
56
- if intel.level == Level.TRUSTED:
57
- observable.whitelisted = True
58
- break
59
- if observable.whitelisted:
60
- for parent in observable.observables_parents.values():
61
- parent.whitelisted = True
62
-
63
- def root_observables(self) -> list[Observable]:
64
- return [obs for obs in self._observables.values() if not obs.observables_parents]
65
-
66
- def whitelisted(self, obs_type: str, obs_value: str) -> bool:
67
- key = f"{obs_type}.{obs_value}"
68
- observable = self._observables.get(key)
69
- return bool(observable and observable.whitelisted)
cyvest/report_render.py DELETED
@@ -1,306 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import math
4
- import re
5
- from collections.abc import Iterable
6
- from typing import TYPE_CHECKING, Any
7
-
8
- from logurich import logger
9
- from rich.align import Align
10
- from rich.rule import Rule
11
- from rich.table import Table
12
- from rich.tree import Tree
13
-
14
- from .models import get_color_level, get_color_score
15
-
16
- if TYPE_CHECKING:
17
- from .visitors import Report
18
-
19
-
20
- def _log_rich(level: str, renderable: Any) -> None:
21
- logger.rich(level, renderable)
22
-
23
-
24
- def _to_stdout_check(check: dict[str, Any], *, indent: int = 0, short_name: bool = False) -> list[str] | None:
25
- level = check["level"]
26
- if level == "NONE":
27
- return None
28
- score = check["score"]
29
- color_level = get_color_level(level)
30
- color_score = get_color_score(score)
31
- space = indent * " "
32
- name = check["short_key"] if short_name else check["full_key"]
33
- return [f"{space}{name}", f"[{color_score}]{score}[/{color_score}]", f"[{color_level}]{level}[/{color_level}]"]
34
-
35
-
36
- def _to_stdout_rec_checks(contained: dict[str, Any], *, indent: int = 0) -> list[list[str]]:
37
- rows: list[list[str]] = []
38
- for value in contained.values():
39
- values = value if isinstance(value, list) else [value]
40
- for entry in values:
41
- is_container = "container" in entry
42
- row = _to_stdout_check(entry, indent=indent, short_name=not is_container)
43
- if row:
44
- rows.append(row)
45
- if is_container:
46
- rows.extend(_to_stdout_rec_checks(entry["container"], indent=indent + 1))
47
- return rows
48
-
49
-
50
- def _to_stdout_rec_obs(tree: Tree, obs: dict[str, Any], *, indent: int = 0) -> None:
51
- level = obs["level"]
52
- score = obs["score"]
53
- generates_by_checks = (
54
- "[cyan][[/cyan]" + "[cyan]][/cyan],[cyan][[/cyan]".join(obs["generated_by"]) + "[cyan]][/cyan]"
55
- if obs["generated_by"]
56
- else ""
57
- )
58
- color_level = get_color_level(level)
59
- color_score = get_color_score(score)
60
- more_detail = " [green]WHITELISTED[/green]" if obs["whitelisted"] is True else ""
61
- full_key = obs["full_key"]
62
- data = (
63
- f"{generates_by_checks} {full_key} -> "
64
- f"[{color_score}]{score}[/{color_score}] [{color_level}]{level}[/{color_level}] {more_detail}"
65
- )
66
- child_tree = tree.add(data)
67
- for child in obs["observables_children"]:
68
- _to_stdout_rec_obs(child_tree, child, indent=indent + 1)
69
-
70
-
71
- def _get_check(full_key: str, json_data: dict[str, Any]) -> dict[str, Any] | None:
72
- root = json_data["checks"]
73
- match = re.match(r"^([^\.]+)\.(.*)", full_key)
74
- if match is None:
75
- return None
76
- scope = match.group(1)
77
- root = root.get(scope, {})
78
- current_path = match.group(2)
79
- while current_path:
80
- match = re.match(r"^(([^\#\.]+)(\#[^\#]+\#)?)\.?(.*)", current_path)
81
- if not match:
82
- logger.error("Impossible to match full key pattern {}", full_key)
83
- return None
84
- current_path = match.group(2)
85
- if match.group(3):
86
- identifier = match.group(3).strip("#")
87
- root = root.get(current_path, [])
88
- root = next((x for x in root if x["identifier"] == identifier), None)
89
- if root is None:
90
- return None
91
- else:
92
- root = root.get(current_path, {})
93
- if "container" in root:
94
- root = root.get("container", {})
95
- current_path = match.group(4)
96
- return root
97
-
98
-
99
- def stdout_from_json(report: Report, json_data: dict[str, Any]) -> None:
100
- logger.info("[cyan]### JSON CONSOLE REPORT[/cyan]")
101
- checks = json_data["stats_checks"]["checks"]
102
- applied = json_data["stats_checks"]["applied"]
103
- table_report = Table(
104
- title="Report",
105
- caption=f"RESULT CHECKS: {checks} - APPLIED: {applied}",
106
- )
107
- table_report.add_column("Name")
108
- table_report.add_column("Score", justify="right")
109
- table_report.add_column("Level", justify="center")
110
-
111
- rule = Rule("[bold magenta]CHECKS[/bold magenta]")
112
- table_report.add_row(rule, "-", "-")
113
- for scope_name, checks in json_data["checks"].items():
114
- scope_rule = Align(f"[bold magenta]{scope_name}[/bold magenta]", align="left")
115
- table_report.add_row(scope_rule, "-", "-")
116
- rows = _to_stdout_rec_checks(checks, indent=1)
117
- for row in rows:
118
- table_report.add_row(*row)
119
-
120
- if report.graph:
121
- tree = Tree("Observables Graph")
122
- logger.info("[magenta]GRAPH: {}[/magenta]", len(json_data["graph"]))
123
- for obs in json_data["graph"]:
124
- _to_stdout_rec_obs(tree, obs)
125
- _log_rich("INFO", tree)
126
-
127
- table_report.add_section()
128
- rule = Rule("[bold magenta]BY LEVEL[/bold magenta]")
129
- table_report.add_row(rule, "-", "-")
130
- for level_name, list_checks in json_data["checks_by_level"].items():
131
- color_level = get_color_level(level_name)
132
- level_rule = Align(
133
- f"[bold {color_level}]{level_name}: {len(list_checks)} check(s)[/bold {color_level}]",
134
- align="center",
135
- )
136
- table_report.add_row(level_rule, "-", "-")
137
- for check_full_key in list_checks:
138
- check = _get_check(check_full_key, json_data)
139
- if check:
140
- row = _to_stdout_check(check)
141
- if row:
142
- table_report.add_row(*row)
143
-
144
- table_report.add_section()
145
- enrichment_rule = Rule(f"[bold magenta]ENRICHMENTS[/bold magenta]: {len(report.enrichments)} enrichments")
146
- table_report.add_row(enrichment_rule, "-", "-")
147
- for enrichment in report.enrichments:
148
- table_report.add_row(str(enrichment), "-", "-")
149
-
150
- table_report.add_section()
151
- stats_rule = Rule("[bold magenta]STATISTICS[/bold magenta]")
152
- table_report.add_row(stats_rule, "-", "-")
153
- for stat_name, stat_value in json_data["stats"].items():
154
- table_report.add_row(" ".join(stat_name.split("_")).title(), str(stat_value), "-")
155
-
156
- global_level = json_data["level"]
157
- global_score = json_data["score"]
158
- color_level = get_color_level(global_level)
159
- color_score = get_color_score(global_score)
160
- table_report.add_section()
161
- table_report.add_row(
162
- Align("[bold]GLOBAL SCORE[/bold]", align="center"),
163
- f"[{color_score}]{global_score}[/{color_score}]",
164
- f"[{color_level}]{global_level}[/{color_level}]",
165
- )
166
- _log_rich("INFO", table_report)
167
-
168
-
169
- def markdown_summary(json_data: dict[str, Any], *, exclude_checks: list[str] | None = None) -> str:
170
- def _first(keys: Iterable[str], src: dict[str, Any], default: Any | None = None) -> Any | None:
171
- for key in keys:
172
- if key in src and src[key] not in (None, ""):
173
- return src[key]
174
- return default
175
-
176
- def _result_of(check: dict[str, Any]) -> str:
177
- value = _first(("result", "status", "outcome"), check)
178
- if value is not None:
179
- return str(value)
180
- if isinstance(check.get("passed"), bool):
181
- return "PASS" if check["passed"] else "FAIL"
182
- if isinstance(check.get("ok"), bool):
183
- return "OK" if check["ok"] else "NOT OK"
184
- return "n/a"
185
-
186
- def _score_of(check: dict[str, Any]) -> str:
187
- score_value = check.get("score")
188
- if score_value is None:
189
- return "n/a"
190
- if isinstance(score_value, int):
191
- return str(score_value)
192
- if isinstance(score_value, float):
193
- return str(int(score_value)) if score_value.is_integer() else f"{score_value:.2f}"
194
- return str(score_value)
195
-
196
- def _level_of(check: dict[str, Any]) -> str:
197
- level_value = _first(("level", "severity"), check, default="UNKNOWN")
198
- return str(level_value).upper()
199
-
200
- def _name_of(check: dict[str, Any], fallback_key: str) -> str:
201
- return str(_first(("name", "title", "id"), check, default=fallback_key))
202
-
203
- def _desc_of(check: dict[str, Any]) -> str:
204
- desc_value = _first(("description", "desc", "details", "message"), check)
205
- return str(desc_value).strip() if desc_value is not None else "_No description provided._"
206
-
207
- def _is_check_node(node: Any) -> bool:
208
- if not isinstance(node, dict):
209
- return False
210
- keys = ("level", "severity", "description", "result", "status", "name", "score", "passed", "ok")
211
- return any(key in node for key in keys)
212
-
213
- def _flatten_checks(
214
- container: Any, path: tuple[str, ...] = ()
215
- ) -> Iterable[tuple[str, dict[str, Any], tuple[str, ...]]]:
216
- if isinstance(container, dict):
217
- if _is_check_node(container):
218
- key = path[-1] if path else "check"
219
- yield (key, container, path)
220
- else:
221
- for key, value in container.items():
222
- yield from _flatten_checks(value, path + (str(key),))
223
- elif isinstance(container, list):
224
- for idx, value in enumerate(container):
225
- yield from _flatten_checks(value, path + (str(idx),))
226
-
227
- def _order_levels(levels: Iterable[str]) -> list[str]:
228
- priority: dict[str, int] = {
229
- "MALICIOUS": 0,
230
- "SUSPICIOUS": 1,
231
- "NOTABLE": 2,
232
- "INFO": 3,
233
- "SAFE": 4,
234
- }
235
- return sorted(set(levels), key=lambda level: priority.get(level, 999))
236
-
237
- global_level = str(json_data.get("level", "UNKNOWN")).upper()
238
- global_score = json_data.get("score", "n/a")
239
- if isinstance(global_score, (int, float)):
240
- global_score_str = (
241
- str(int(global_score))
242
- if isinstance(global_score, int) or (isinstance(global_score, float) and float(global_score).is_integer())
243
- else str(global_score)
244
- )
245
- else:
246
- global_score_str = str(global_score)
247
-
248
- raw_checks_root = json_data.get("checks") or {}
249
- flattened = list(_flatten_checks(raw_checks_root))
250
-
251
- groups: dict[str, list[tuple[str, dict[str, Any], tuple[str, ...]]]] = {}
252
- for key, check, path in flattened:
253
- level = _level_of(check)
254
- groups.setdefault(level, []).append((key, check, path))
255
-
256
- ordered_levels = _order_levels(groups.keys())
257
- md: list[str] = []
258
- md.append("# Report Summary")
259
- md.append("")
260
- md.append(f"**Global Score:** `{global_score_str}` | **Global Level:** {global_level}")
261
- md.append("")
262
-
263
- if not flattened:
264
- md.append("_No checks found in the report._")
265
- else:
266
- for level in ordered_levels:
267
- entries = groups[level]
268
- md.append(f"## {level} — {len(entries)} check(s)")
269
- md.append("")
270
-
271
- def _score_key(entry: tuple[str, dict[str, Any], tuple[str, ...]]) -> float:
272
- score_raw = entry[1].get("score")
273
- try:
274
- return -float(score_raw) if score_raw is not None else math.inf
275
- except Exception:
276
- return math.inf
277
-
278
- entries_sorted = sorted(
279
- entries,
280
- key=lambda entry: (_score_key(entry), _name_of(entry[1], entry[0]).lower()),
281
- )
282
-
283
- for key, check, path in entries_sorted:
284
- name = _name_of(check, key)
285
- if exclude_checks and name in exclude_checks:
286
- continue
287
- result = _result_of(check)
288
- score_value = _score_of(check)
289
- desc = _desc_of(check)
290
- scope = " › ".join(path[:-1]) if len(path) > 1 else (path[0] if path else "")
291
- scope_note = f" _(scope: `{scope}`)_" if scope else ""
292
- comment = None
293
- details = check.get("details", {})
294
- if details:
295
- comment = details.get("comment")
296
-
297
- md.append(f"- **{name}**{scope_note}")
298
- md.append(f" - Level: {_level_of(check)}")
299
- md.append(f" - Result: `{result}`")
300
- md.append(f" - Score: `{score_value}`")
301
- md.append(f" - Description: {desc}")
302
- if comment:
303
- md.append(f" - Note: {comment}")
304
- md.append("")
305
-
306
- return "\n".join(md)
@@ -1,237 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections import defaultdict
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from .models import Container, Level, Observable, ResultCheck, ThreatIntel
7
-
8
- if TYPE_CHECKING:
9
- from .visitors import Report
10
-
11
-
12
- def _to_json_threat_intel(ti: ThreatIntel) -> dict[str, Any]:
13
- return {
14
- "name": ti.name,
15
- "analyzer": ti.name,
16
- "display_name": ti.display_name,
17
- "score": ti.score,
18
- "level": ti.level.name,
19
- "comment": ti.comment,
20
- "extra": ti.extra,
21
- "taxonomies": ti.taxonomies,
22
- }
23
-
24
-
25
- def _observable_to_json(
26
- report: Report,
27
- observable: Observable,
28
- *,
29
- max_deep_child: int | None,
30
- max_deep_parent: int | None,
31
- ) -> dict[str, Any]:
32
- def _rec(
33
- obj: Observable,
34
- deep_child: int | None,
35
- deep_parent: int | None,
36
- already_seen: list[str] | None = None,
37
- ) -> dict[str, Any]:
38
- if already_seen is None:
39
- already_seen = []
40
- threat_intel_json = {k: _to_json_threat_intel(v) for k, v in obj.threat_intels.items()}
41
- if obj.full_key in already_seen:
42
- return {
43
- "full_key": obj.full_key,
44
- "score": obj.score,
45
- "level": obj.level.name,
46
- "obs_type": obj.obs_type.name,
47
- "obs_value": obj.obs_value,
48
- "whitelisted": obj.whitelisted,
49
- "threat_intels": threat_intel_json,
50
- "observables_children": [child for child in obj.observables_children],
51
- "observables_parents": [parent for parent in obj.observables_parents],
52
- "generated_by": list(obj.generated_by),
53
- }
54
- new_seen = [*already_seen, obj.full_key]
55
- if deep_child is not None:
56
- if (max_deep_child is not None and deep_child >= max_deep_child) or deep_child < 0:
57
- observables_children: list[Any] = [child for child in obj.observables_children]
58
- else:
59
- rec_deep_child = deep_child + 1 if deep_child is not None else None
60
- observables_children = [
61
- _rec(child, rec_deep_child, -1, new_seen) for child in obj.observables_children.values()
62
- ]
63
- else:
64
- observables_children = [_rec(child, None, -1, new_seen) for child in obj.observables_children.values()]
65
- if deep_parent is not None:
66
- if (max_deep_parent is not None and deep_parent >= max_deep_parent) or deep_parent < 0:
67
- observables_parents: list[Any] = [parent for parent in obj.observables_parents]
68
- else:
69
- rec_deep_parent = deep_parent + 1 if deep_parent is not None else None
70
- observables_parents = [
71
- _rec(parent, -1, rec_deep_parent, new_seen) for parent in obj.observables_parents.values()
72
- ]
73
- else:
74
- observables_parents = [_rec(parent, -1, None, new_seen) for parent in obj.observables_parents.values()]
75
- return {
76
- "full_key": obj.full_key,
77
- "score": obj.score,
78
- "level": obj.level.name,
79
- "obs_type": obj.obs_type.name,
80
- "obs_value": obj.obs_value,
81
- "whitelisted": obj.whitelisted,
82
- "threat_intels": threat_intel_json,
83
- "observables_children": observables_children,
84
- "observables_parents": observables_parents,
85
- "generated_by": list(obj.generated_by),
86
- }
87
-
88
- if max_deep_child is None:
89
- start_child = None
90
- else:
91
- start_child = -1 if max_deep_child < 0 else 0
92
- if max_deep_parent is None:
93
- start_parent = None
94
- else:
95
- start_parent = -1 if max_deep_parent < 0 else 0
96
- return _rec(observable, start_child, start_parent)
97
-
98
-
99
- def _result_check_to_json(report: Report, rc: ResultCheck) -> dict[str, Any]:
100
- observables_json = [
101
- _observable_to_json(report, observable, max_deep_child=2, max_deep_parent=1)
102
- for observable in rc.observables.values()
103
- ]
104
- return {
105
- "full_key": rc.full_key,
106
- "short_key": rc.short_key,
107
- "scope": rc.scope.name if rc.scope else "UNKNOWN",
108
- "path": rc.path,
109
- "identifier": rc.identifier,
110
- "score": rc.score,
111
- "level": rc.level.name,
112
- "description": rc.description,
113
- "details": rc.details,
114
- "observables": observables_json,
115
- }
116
-
117
-
118
- def _container_to_json(report: Report, container: Container) -> dict[str, Any]:
119
- payload = {
120
- "full_key": container.full_key,
121
- "scope": container.scope.name if container.scope else "UNKNOWN",
122
- "path": container.path,
123
- "identifier": container.identifier,
124
- "score": container.score,
125
- "level": container.level.name,
126
- "description": container.description,
127
- "details": container.details,
128
- "nb_checks": container.nb_checks,
129
- "container": {},
130
- }
131
- for child in container.children:
132
- _insert_node(report, payload["container"], child)
133
- return payload
134
-
135
-
136
- def _insert_node(report: Report, target: dict[str, Any], node: Container | ResultCheck) -> None:
137
- if isinstance(node, Container):
138
- serialized = _container_to_json(report, node)
139
- else:
140
- serialized = _result_check_to_json(report, node)
141
- if node.identifier:
142
- bucket = target.setdefault(node.path, [])
143
- bucket.append(serialized)
144
- else:
145
- target[node.path] = serialized
146
-
147
-
148
- def report_to_json(report: Report) -> dict[str, Any]:
149
- data = dict(report.json)
150
- data.pop("@meta", None)
151
-
152
- checks_root: dict[str, Any] = {}
153
- for scope_name, nodes in report.tree.iter_roots():
154
- scope_dict: dict[str, Any] = {}
155
- for node in nodes:
156
- _insert_node(report, scope_dict, node)
157
- checks_root[scope_name] = scope_dict
158
-
159
- result_checks = list(report.tree.result_checks())
160
- applied_checks = [rc for rc in result_checks if rc.level > Level.NONE]
161
-
162
- checks_by_level: dict[str, list[str]] = {}
163
- grouped = defaultdict(list)
164
- for rc in result_checks:
165
- grouped[rc.level.name].append((rc.score, rc.full_key))
166
- for level_name, entries in grouped.items():
167
- entries.sort(key=lambda item: item[0], reverse=True)
168
- checks_by_level[level_name] = [full_key for _, full_key in entries]
169
-
170
- json_graph = [
171
- _observable_to_json(report, observable, max_deep_child=None, max_deep_parent=-1)
172
- for observable in report.observable_registry.root_observables()
173
- ]
174
-
175
- return {
176
- "score": report.global_score,
177
- "level": report.global_level.name,
178
- "stats": report.reduce_stats({}),
179
- "stats_checks": {
180
- "applied": len(applied_checks),
181
- "checks": len(result_checks),
182
- "obs": len(report.observables),
183
- "enrichments": len(report.enrichments),
184
- },
185
- "checks_by_level": checks_by_level,
186
- "checks": checks_root,
187
- "graph": json_graph,
188
- "data": data,
189
- "annotations": report.annotations,
190
- }
191
-
192
-
193
- def reduce_report_json(json_report: dict[str, Any]) -> dict[str, Any]:
194
- checks_root = json_report.get("checks", {})
195
- flattened_checks: dict[str, Any] = {}
196
- if isinstance(checks_root, dict):
197
- _reduce_report_rec(checks_root, delete_none=True, all_checks=flattened_checks)
198
- json_report["checks"] = flattened_checks
199
- json_report.pop("graph", None)
200
- json_report.pop("html_report", None)
201
- return json_report
202
-
203
-
204
- def _reduce_report_rec(container, delete_none=False, all_checks=None):
205
- if all_checks is None:
206
- all_checks = {}
207
- to_remove = []
208
- for key, value in container.items():
209
- if isinstance(value, list):
210
- to_remove_in_list = []
211
- for data in value:
212
- if data["level"] == "NONE" and delete_none is True:
213
- to_remove_in_list.append(key)
214
- elif "container" in data:
215
- _reduce_report_rec(data["container"], delete_none=delete_none, all_checks=all_checks)
216
- else:
217
- all_checks[data["full_key"]] = data
218
- for obs in data["observables"]:
219
- obs["observables_children"] = [
220
- child_obs["full_key"] for child_obs in obs["observables_children"]
221
- ]
222
- obs["observables_parents"] = [
223
- parent_obs["full_key"] for parent_obs in obs["observables_parents"]
224
- ]
225
- for ti in obs["threat_intels"].values():
226
- ti.pop("extra", None)
227
- for entry in to_remove_in_list:
228
- value.remove(entry)
229
- else:
230
- if value["level"] == "NONE" and delete_none is True:
231
- to_remove.append(key)
232
- elif "container" in value:
233
- _reduce_report_rec(value["container"], delete_none=delete_none, all_checks=all_checks)
234
- else:
235
- all_checks[value["full_key"]] = value
236
- for key in to_remove:
237
- del container[key]