cyvest 4.4.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.
Potentially problematic release.
This version of cyvest might be problematic. Click here for more details.
- cyvest/__init__.py +38 -0
- cyvest/cli.py +365 -0
- cyvest/cyvest.py +1261 -0
- cyvest/investigation.py +1644 -0
- cyvest/io_rich.py +579 -0
- cyvest/io_schema.py +35 -0
- cyvest/io_serialization.py +459 -0
- cyvest/io_visualization.py +358 -0
- cyvest/keys.py +194 -0
- cyvest/level_score_rules.py +78 -0
- cyvest/levels.py +175 -0
- cyvest/model.py +583 -0
- cyvest/model_enums.py +69 -0
- cyvest/model_schema.py +164 -0
- cyvest/proxies.py +582 -0
- cyvest/score.py +473 -0
- cyvest/shared.py +496 -0
- cyvest/stats.py +316 -0
- cyvest/ulid.py +36 -0
- cyvest-4.4.0.dist-info/METADATA +538 -0
- cyvest-4.4.0.dist-info/RECORD +23 -0
- cyvest-4.4.0.dist-info/WHEEL +4 -0
- cyvest-4.4.0.dist-info/entry_points.txt +3 -0
cyvest/stats.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistics and aggregation engine for Cyvest investigations.
|
|
3
|
+
|
|
4
|
+
Provides live counters and aggregations for observables, checks, threat intel,
|
|
5
|
+
and other investigation metrics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
|
|
12
|
+
from cyvest.levels import Level
|
|
13
|
+
from cyvest.model import Check, Container, Observable, ThreatIntel
|
|
14
|
+
from cyvest.model_schema import StatisticsSchema
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InvestigationStats:
|
|
18
|
+
"""
|
|
19
|
+
Tracks and aggregates statistics for an investigation.
|
|
20
|
+
|
|
21
|
+
Provides real-time metrics about observables, checks, threat intel,
|
|
22
|
+
and other investigation components.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""Initialize statistics tracking."""
|
|
27
|
+
self._observables: dict[str, Observable] = {}
|
|
28
|
+
self._checks: dict[str, Check] = {}
|
|
29
|
+
self._threat_intels: dict[str, ThreatIntel] = {}
|
|
30
|
+
self._containers: dict[str, Container] = {}
|
|
31
|
+
|
|
32
|
+
def register_observable(self, observable: Observable) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Register an observable for statistics tracking.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
observable: Observable to track
|
|
38
|
+
"""
|
|
39
|
+
self._observables[observable.key] = observable
|
|
40
|
+
|
|
41
|
+
def register_check(self, check: Check) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Register a check for statistics tracking.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
check: Check to track
|
|
47
|
+
"""
|
|
48
|
+
self._checks[check.key] = check
|
|
49
|
+
|
|
50
|
+
def register_threat_intel(self, ti: ThreatIntel) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Register threat intel for statistics tracking.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ti: Threat intel to track
|
|
56
|
+
"""
|
|
57
|
+
self._threat_intels[ti.key] = ti
|
|
58
|
+
|
|
59
|
+
def register_container(self, container: Container) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Register a container for statistics tracking.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
container: Container to track
|
|
65
|
+
"""
|
|
66
|
+
self._containers[container.key] = container
|
|
67
|
+
|
|
68
|
+
def get_observable_count_by_type(self) -> dict[str, int]:
|
|
69
|
+
"""
|
|
70
|
+
Get count of observables by type.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary mapping observable type to count
|
|
74
|
+
"""
|
|
75
|
+
counts: dict[str, int] = defaultdict(int)
|
|
76
|
+
for obs in self._observables.values():
|
|
77
|
+
counts[obs.obs_type] += 1
|
|
78
|
+
return dict(counts)
|
|
79
|
+
|
|
80
|
+
def get_observable_count_by_level(self, obs_type: str | None = None) -> dict[Level, int]:
|
|
81
|
+
"""
|
|
82
|
+
Get count of observables by level, optionally filtered by type.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
obs_type: Optional observable type to filter by
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dictionary mapping level to count
|
|
89
|
+
"""
|
|
90
|
+
counts: dict[Level, int] = defaultdict(int)
|
|
91
|
+
for obs in self._observables.values():
|
|
92
|
+
if obs_type is None or obs.obs_type == obs_type:
|
|
93
|
+
counts[obs.level] += 1
|
|
94
|
+
return dict(counts)
|
|
95
|
+
|
|
96
|
+
def get_observable_count_by_type_and_level(self) -> dict[str, dict[Level, int]]:
|
|
97
|
+
"""
|
|
98
|
+
Get count of observables by type and level.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Nested dictionary: {type: {level: count}}
|
|
102
|
+
"""
|
|
103
|
+
counts: dict[str, dict[Level, int]] = defaultdict(lambda: defaultdict(int))
|
|
104
|
+
for obs in self._observables.values():
|
|
105
|
+
counts[obs.obs_type][obs.level] += 1
|
|
106
|
+
# Convert defaultdicts to regular dicts
|
|
107
|
+
return {k: dict(v) for k, v in counts.items()}
|
|
108
|
+
|
|
109
|
+
def get_total_observable_count(self) -> int:
|
|
110
|
+
"""
|
|
111
|
+
Get total number of observables.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Total observable count
|
|
115
|
+
"""
|
|
116
|
+
return len(self._observables)
|
|
117
|
+
|
|
118
|
+
def get_internal_observable_count(self) -> int:
|
|
119
|
+
"""
|
|
120
|
+
Get count of internal observables.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Count of internal observables
|
|
124
|
+
"""
|
|
125
|
+
return sum(1 for obs in self._observables.values() if obs.internal)
|
|
126
|
+
|
|
127
|
+
def get_external_observable_count(self) -> int:
|
|
128
|
+
"""
|
|
129
|
+
Get count of external observables.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Count of external observables
|
|
133
|
+
"""
|
|
134
|
+
return sum(1 for obs in self._observables.values() if not obs.internal)
|
|
135
|
+
|
|
136
|
+
def get_whitelisted_observable_count(self) -> int:
|
|
137
|
+
"""
|
|
138
|
+
Get count of whitelisted observables.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Count of whitelisted observables
|
|
142
|
+
"""
|
|
143
|
+
return sum(1 for obs in self._observables.values() if obs.whitelisted)
|
|
144
|
+
|
|
145
|
+
def get_check_count_by_scope(self) -> dict[str, int]:
|
|
146
|
+
"""
|
|
147
|
+
Get count of checks by scope.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dictionary mapping scope to count
|
|
151
|
+
"""
|
|
152
|
+
counts: dict[str, int] = defaultdict(int)
|
|
153
|
+
for check in self._checks.values():
|
|
154
|
+
counts[check.scope] += 1
|
|
155
|
+
return dict(counts)
|
|
156
|
+
|
|
157
|
+
def get_check_keys_by_scope(self) -> dict[str, list[str]]:
|
|
158
|
+
"""
|
|
159
|
+
Get check keys grouped by scope.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary mapping scope to list of check keys
|
|
163
|
+
"""
|
|
164
|
+
keys: dict[str, list[str]] = defaultdict(list)
|
|
165
|
+
for check in self._checks.values():
|
|
166
|
+
keys[check.scope].append(check.key)
|
|
167
|
+
return dict(keys)
|
|
168
|
+
|
|
169
|
+
def get_check_count_by_level(self) -> dict[Level, int]:
|
|
170
|
+
"""
|
|
171
|
+
Get count of checks by level.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Dictionary mapping level to count
|
|
175
|
+
"""
|
|
176
|
+
counts: dict[Level, int] = defaultdict(int)
|
|
177
|
+
for check in self._checks.values():
|
|
178
|
+
counts[check.level] += 1
|
|
179
|
+
return dict(counts)
|
|
180
|
+
|
|
181
|
+
def get_check_keys_by_level(self) -> dict[Level, list[str]]:
|
|
182
|
+
"""
|
|
183
|
+
Get check keys grouped by level.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dictionary mapping level to list of check keys
|
|
187
|
+
"""
|
|
188
|
+
keys: dict[Level, list[str]] = defaultdict(list)
|
|
189
|
+
for check in self._checks.values():
|
|
190
|
+
keys[check.level].append(check.key)
|
|
191
|
+
return dict(keys)
|
|
192
|
+
|
|
193
|
+
def get_applied_check_count(self) -> int:
|
|
194
|
+
"""
|
|
195
|
+
Get count of checks that were applied (level != NONE).
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Count of applied checks
|
|
199
|
+
"""
|
|
200
|
+
return sum(1 for check in self._checks.values() if check.level != Level.NONE)
|
|
201
|
+
|
|
202
|
+
def get_total_check_count(self) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Get total number of checks.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Total check count
|
|
208
|
+
"""
|
|
209
|
+
return len(self._checks)
|
|
210
|
+
|
|
211
|
+
def get_threat_intel_count(self) -> int:
|
|
212
|
+
"""
|
|
213
|
+
Get total number of threat intel sources queried.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Total threat intel count
|
|
217
|
+
"""
|
|
218
|
+
return len(self._threat_intels)
|
|
219
|
+
|
|
220
|
+
def get_threat_intel_count_by_source(self) -> dict[str, int]:
|
|
221
|
+
"""
|
|
222
|
+
Get count of threat intel by source.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Dictionary mapping source name to count
|
|
226
|
+
"""
|
|
227
|
+
counts: dict[str, int] = defaultdict(int)
|
|
228
|
+
for ti in self._threat_intels.values():
|
|
229
|
+
counts[ti.source] += 1
|
|
230
|
+
return dict(counts)
|
|
231
|
+
|
|
232
|
+
def get_threat_intel_count_by_level(self) -> dict[Level, int]:
|
|
233
|
+
"""
|
|
234
|
+
Get count of threat intel by level.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dictionary mapping level to count
|
|
238
|
+
"""
|
|
239
|
+
counts: dict[Level, int] = defaultdict(int)
|
|
240
|
+
for ti in self._threat_intels.values():
|
|
241
|
+
counts[ti.level] += 1
|
|
242
|
+
return dict(counts)
|
|
243
|
+
|
|
244
|
+
def get_container_count(self) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Get total number of containers.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Total container count
|
|
250
|
+
"""
|
|
251
|
+
return len(self._containers)
|
|
252
|
+
|
|
253
|
+
def get_checks_by_level(self, level: Level) -> list[Check]:
|
|
254
|
+
"""
|
|
255
|
+
Get all checks with a specific level.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
level: Level to filter by
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of checks with the specified level
|
|
262
|
+
"""
|
|
263
|
+
return [check for check in self._checks.values() if check.level == level]
|
|
264
|
+
|
|
265
|
+
def get_observables_by_level(self, level: Level) -> list[Observable]:
|
|
266
|
+
"""
|
|
267
|
+
Get all observables with a specific level.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
level: Level to filter by
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of observables with the specified level
|
|
274
|
+
"""
|
|
275
|
+
return [obs for obs in self._observables.values() if obs.level == level]
|
|
276
|
+
|
|
277
|
+
def get_observables_by_type(self, obs_type: str) -> list[Observable]:
|
|
278
|
+
"""
|
|
279
|
+
Get all observables of a specific type.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
obs_type: Type to filter by
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of observables with the specified type
|
|
286
|
+
"""
|
|
287
|
+
return [obs for obs in self._observables.values() if obs.obs_type == obs_type]
|
|
288
|
+
|
|
289
|
+
def get_summary(self) -> StatisticsSchema:
|
|
290
|
+
"""
|
|
291
|
+
Get a comprehensive summary of all statistics.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
StatisticsSchema instance with all statistics
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
return StatisticsSchema(
|
|
298
|
+
total_observables=self.get_total_observable_count(),
|
|
299
|
+
internal_observables=self.get_internal_observable_count(),
|
|
300
|
+
external_observables=self.get_external_observable_count(),
|
|
301
|
+
whitelisted_observables=self.get_whitelisted_observable_count(),
|
|
302
|
+
observables_by_type=self.get_observable_count_by_type(),
|
|
303
|
+
observables_by_level={str(k): v for k, v in self.get_observable_count_by_level().items()},
|
|
304
|
+
observables_by_type_and_level={
|
|
305
|
+
obs_type: {str(lvl): count for lvl, count in levels.items()}
|
|
306
|
+
for obs_type, levels in self.get_observable_count_by_type_and_level().items()
|
|
307
|
+
},
|
|
308
|
+
total_checks=self.get_total_check_count(),
|
|
309
|
+
applied_checks=self.get_applied_check_count(),
|
|
310
|
+
checks_by_scope=self.get_check_keys_by_scope(),
|
|
311
|
+
checks_by_level={str(k): v for k, v in self.get_check_keys_by_level().items()},
|
|
312
|
+
total_threat_intel=self.get_threat_intel_count(),
|
|
313
|
+
threat_intel_by_source=self.get_threat_intel_count_by_source(),
|
|
314
|
+
threat_intel_by_level={str(k): v for k, v in self.get_threat_intel_count_by_level().items()},
|
|
315
|
+
total_containers=self.get_container_count(),
|
|
316
|
+
)
|
cyvest/ulid.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ULID generator for stable investigation identities.
|
|
3
|
+
|
|
4
|
+
Cyvest uses ULIDs to tag investigations and to stamp provenance on Check↔Observable
|
|
5
|
+
links. This implementation is dependency-free and follows the 26-char Crockford
|
|
6
|
+
Base32 ULID encoding.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import secrets
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
_CROCKFORD_BASE32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_ulid(*, timestamp_ms: int | None = None) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Generate a ULID string.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
timestamp_ms: Optional millisecond timestamp (48-bit). Defaults to current time.
|
|
23
|
+
"""
|
|
24
|
+
if timestamp_ms is None:
|
|
25
|
+
timestamp_ms = int(time.time() * 1000)
|
|
26
|
+
if timestamp_ms < 0 or timestamp_ms >= (1 << 48):
|
|
27
|
+
raise ValueError("timestamp_ms must fit in 48 bits")
|
|
28
|
+
|
|
29
|
+
randomness = secrets.token_bytes(10) # 80 bits
|
|
30
|
+
value = (timestamp_ms << 80) | int.from_bytes(randomness, "big")
|
|
31
|
+
|
|
32
|
+
chars: list[str] = []
|
|
33
|
+
for _ in range(26):
|
|
34
|
+
chars.append(_CROCKFORD_BASE32_ALPHABET[value & 0x1F])
|
|
35
|
+
value >>= 5
|
|
36
|
+
return "".join(reversed(chars))
|