cyvest 0.1.0__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.
- cyvest/__init__.py +47 -0
- cyvest/builder.py +182 -0
- cyvest/check_tree.py +117 -0
- cyvest/models.py +785 -0
- cyvest/observable_registry.py +69 -0
- cyvest/report_render.py +306 -0
- cyvest/report_serialization.py +237 -0
- cyvest/visitors.py +332 -0
- cyvest-0.1.0.dist-info/METADATA +110 -0
- cyvest-0.1.0.dist-info/RECORD +13 -0
- cyvest-0.1.0.dist-info/WHEEL +5 -0
- cyvest-0.1.0.dist-info/licenses/LICENSE +21 -0
- cyvest-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,69 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
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)
|
|
@@ -0,0 +1,237 @@
|
|
|
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]
|