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
cyvest/visitors.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from logurich import logger
|
|
8
|
+
|
|
9
|
+
from .check_tree import CheckTree
|
|
10
|
+
from .models import (
|
|
11
|
+
Container,
|
|
12
|
+
Enrichment,
|
|
13
|
+
Level,
|
|
14
|
+
Model,
|
|
15
|
+
Observable,
|
|
16
|
+
ObsType,
|
|
17
|
+
ResultCheck,
|
|
18
|
+
Scope,
|
|
19
|
+
ThreatIntel,
|
|
20
|
+
get_level_from_score,
|
|
21
|
+
)
|
|
22
|
+
from .observable_registry import ObservableRegistry
|
|
23
|
+
from .report_render import markdown_summary, stdout_from_json
|
|
24
|
+
from .report_serialization import reduce_report_json, report_to_json
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Visitor(ABC):
|
|
28
|
+
def __init__(self, *, graph: bool = False) -> None:
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.graph = graph
|
|
31
|
+
self._stats = {}
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def stats(self):
|
|
35
|
+
return copy.deepcopy(self._stats)
|
|
36
|
+
|
|
37
|
+
def init_stat(self, key: str, value: int = 0) -> None:
|
|
38
|
+
self._stats[key] = value
|
|
39
|
+
|
|
40
|
+
def increment_stat(self, key: str, value: int = 1) -> None:
|
|
41
|
+
self._stats[key] += value
|
|
42
|
+
|
|
43
|
+
def decrement_stat(self, key: str, value: int = 1) -> None:
|
|
44
|
+
self._stats[key] -= value
|
|
45
|
+
|
|
46
|
+
def reduce_stats(self, stats: dict) -> dict:
|
|
47
|
+
for k, v in self._stats.items():
|
|
48
|
+
if not isinstance(v, int):
|
|
49
|
+
raise Exception("Reduce Stats can reduce only integers")
|
|
50
|
+
if stats.get(k):
|
|
51
|
+
stats[k] += v
|
|
52
|
+
else:
|
|
53
|
+
stats[k] = v
|
|
54
|
+
return stats
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def visit_threat_intel(self, threat_intel: ThreatIntel) -> ThreatIntel:
|
|
58
|
+
"""Method that returns models"""
|
|
59
|
+
raise NotImplementedError("missing method check")
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def visit_observable(self, observable: Observable) -> Observable:
|
|
63
|
+
"""Method that visit an observable"""
|
|
64
|
+
raise NotImplementedError("missing method visit_observable")
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def visit_result_check(self, result_check: ResultCheck) -> ResultCheck:
|
|
68
|
+
"""Method that visit a result entry"""
|
|
69
|
+
raise NotImplementedError("missing method visit_result_check")
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def visit_container(self, container: Container) -> Container:
|
|
73
|
+
"""Method that visit a result entry"""
|
|
74
|
+
raise NotImplementedError("missing method visit_container")
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def visit_enrichment(self, enrichment: Enrichment) -> Enrichment:
|
|
78
|
+
"""Method that visit an enrichment"""
|
|
79
|
+
raise NotImplementedError("missing method visit_enrichment")
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def to_json(self):
|
|
83
|
+
"""Method that convert the object to json"""
|
|
84
|
+
raise NotImplementedError("missing method to_json")
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def get_check(self, path: str):
|
|
88
|
+
"""Method that convert the object to json"""
|
|
89
|
+
raise NotImplementedError("missing method get_check")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Report(Visitor):
|
|
93
|
+
map_stats_suspicious = {
|
|
94
|
+
ObsType.IP.name: "ips",
|
|
95
|
+
ObsType.URL.name: "urls",
|
|
96
|
+
ObsType.DOMAIN.name: "domains",
|
|
97
|
+
ObsType.FILE.name: "files",
|
|
98
|
+
Scope.FULL.name: "full",
|
|
99
|
+
Scope.HEADER.name: "header",
|
|
100
|
+
Scope.BODY.name: "body",
|
|
101
|
+
Scope.ATTACHMENT.name: "attachment",
|
|
102
|
+
"threat_intel": "threat_intels",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
json_structure: dict[str, Any] | None = None,
|
|
108
|
+
linked_reports: dict[str, Report] | None = None,
|
|
109
|
+
*,
|
|
110
|
+
graph: bool = False,
|
|
111
|
+
) -> None:
|
|
112
|
+
if linked_reports is None:
|
|
113
|
+
linked_reports = {}
|
|
114
|
+
super().__init__(graph=graph)
|
|
115
|
+
payload = copy.deepcopy(json_structure) if json_structure is not None else self.default_payload()
|
|
116
|
+
self.json = payload
|
|
117
|
+
self.linked_reports = linked_reports
|
|
118
|
+
self._seen_stats = {}
|
|
119
|
+
self.tree = CheckTree()
|
|
120
|
+
self.observable_registry = ObservableRegistry()
|
|
121
|
+
self.observables = self.observable_registry._observables
|
|
122
|
+
self.enrichments = []
|
|
123
|
+
self.global_score = 0.0
|
|
124
|
+
self.global_level = get_level_from_score(self.global_score) or Level.INFO
|
|
125
|
+
self.annotations: list[dict[str, str]] = []
|
|
126
|
+
# init statistics
|
|
127
|
+
for type_data, key in Report.map_stats_suspicious.items():
|
|
128
|
+
self.init_stat(key)
|
|
129
|
+
for level in Level:
|
|
130
|
+
stat_name = self._get_stat(type_data, level)
|
|
131
|
+
if stat_name is not None:
|
|
132
|
+
self.init_stat(stat_name)
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def default_payload() -> dict[str, Any]:
|
|
136
|
+
return {"checks": {}, "stats": {}, "data": {"header": {}}, "graph": []}
|
|
137
|
+
|
|
138
|
+
def __getitem__(self, key):
|
|
139
|
+
return self.json[key]
|
|
140
|
+
|
|
141
|
+
def _get_stat(self, type_data: str, level: Level) -> str:
|
|
142
|
+
stat_name = Report.map_stats_suspicious.get(type_data)
|
|
143
|
+
if stat_name is None:
|
|
144
|
+
return None
|
|
145
|
+
suffix = ""
|
|
146
|
+
if level <= Level.NONE:
|
|
147
|
+
return None
|
|
148
|
+
if level >= Level.SUSPICIOUS:
|
|
149
|
+
suffix = f"_{level.name}"
|
|
150
|
+
return f"{stat_name}{suffix}".lower()
|
|
151
|
+
|
|
152
|
+
def get_dot_path(self, obj, path):
|
|
153
|
+
s_path = path.split(".")
|
|
154
|
+
struct = obj
|
|
155
|
+
for p in s_path[:-1]:
|
|
156
|
+
struct.setdefault(p, {})
|
|
157
|
+
return struct
|
|
158
|
+
|
|
159
|
+
def get(self, path: str, default: Any = None) -> Any:
|
|
160
|
+
return self.json.get(path, default)
|
|
161
|
+
|
|
162
|
+
def get_check(self, path: str) -> Any:
|
|
163
|
+
node = self.tree.get(path)
|
|
164
|
+
if isinstance(node, ResultCheck):
|
|
165
|
+
return node
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def get_observable_per_type(self, type: ObsType) -> list[Observable]:
|
|
169
|
+
return [obs for obs in self.observable_registry.values() if obs.obs_type == type]
|
|
170
|
+
|
|
171
|
+
def increment_stat(self, full_key: str, type_data: str, level: Level, model: Model) -> bool:
|
|
172
|
+
stat_name = self._get_stat(type_data, level)
|
|
173
|
+
default_stat_name = self._get_stat(type_data, Level.INFO)
|
|
174
|
+
if stat_name is None:
|
|
175
|
+
return False
|
|
176
|
+
if self._seen_stats.get(full_key):
|
|
177
|
+
# Update
|
|
178
|
+
old_level = self._seen_stats.get(full_key)
|
|
179
|
+
old_stat_name = self._get_stat(type_data, old_level)
|
|
180
|
+
if stat_name and old_stat_name != stat_name and level > old_level:
|
|
181
|
+
self._seen_stats[full_key] = level
|
|
182
|
+
logger.debug(
|
|
183
|
+
"{}: update stat {} -> {} - default: {} (gen by: {})",
|
|
184
|
+
full_key,
|
|
185
|
+
old_stat_name,
|
|
186
|
+
stat_name,
|
|
187
|
+
default_stat_name,
|
|
188
|
+
model.generated_by,
|
|
189
|
+
)
|
|
190
|
+
if old_stat_name != default_stat_name:
|
|
191
|
+
super().decrement_stat(old_stat_name)
|
|
192
|
+
if stat_name != default_stat_name:
|
|
193
|
+
super().increment_stat(stat_name)
|
|
194
|
+
else:
|
|
195
|
+
# New
|
|
196
|
+
logger.debug(
|
|
197
|
+
"{}: add stat {} - default: {} (gen by: {})", full_key, stat_name, default_stat_name, model.generated_by
|
|
198
|
+
)
|
|
199
|
+
if stat_name:
|
|
200
|
+
super().increment_stat(stat_name)
|
|
201
|
+
if stat_name != default_stat_name:
|
|
202
|
+
super().increment_stat(default_stat_name)
|
|
203
|
+
self._seen_stats[full_key] = level
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
def get_linked_reports(self, sha256_file: str) -> Report:
|
|
207
|
+
return self.linked_reports.get(sha256_file)
|
|
208
|
+
|
|
209
|
+
def is_whitelisted(self, type: ObsType, value: str) -> bool:
|
|
210
|
+
full_key = f"{type.name}.{value}"
|
|
211
|
+
observable = self.observable_registry.get(full_key)
|
|
212
|
+
return bool(observable and observable.whitelisted)
|
|
213
|
+
|
|
214
|
+
def add_annotation(self, name: str, message: str) -> list[dict[str, str]]:
|
|
215
|
+
annotation = {"name": name, "message": message}
|
|
216
|
+
self.annotations.append(annotation)
|
|
217
|
+
return self.annotations
|
|
218
|
+
|
|
219
|
+
def has_annotations(self) -> bool:
|
|
220
|
+
return bool(self.annotations)
|
|
221
|
+
|
|
222
|
+
def visit_threat_intel(self, threat_intel: ThreatIntel) -> ThreatIntel:
|
|
223
|
+
full_key = threat_intel.full_key
|
|
224
|
+
# Statistic
|
|
225
|
+
self.increment_stat(full_key, "threat_intel", threat_intel.level, threat_intel)
|
|
226
|
+
return threat_intel
|
|
227
|
+
|
|
228
|
+
def visit_result_check(self, result_check: ResultCheck) -> ResultCheck:
|
|
229
|
+
full_key = result_check.full_key
|
|
230
|
+
self.increment_stat(full_key, result_check.scope.name, result_check.level, result_check)
|
|
231
|
+
rc = self.tree.integrate_result_check(result_check)
|
|
232
|
+
logger.debug("Integrated ResultCheck [{}] -> score {}", full_key, rc.score)
|
|
233
|
+
self.global_score = round(self.tree.total_score(), 2)
|
|
234
|
+
self.global_level = self.tree.highest_level()
|
|
235
|
+
return rc
|
|
236
|
+
|
|
237
|
+
def visit_container(self, container: Container) -> Container:
|
|
238
|
+
integrated = self.tree.integrate_container(container)
|
|
239
|
+
self.global_score = round(self.tree.total_score(), 2)
|
|
240
|
+
self.global_level = self.tree.highest_level()
|
|
241
|
+
return integrated
|
|
242
|
+
|
|
243
|
+
def visit_observable(self, observable: Observable) -> Observable:
|
|
244
|
+
full_key = f"{observable.obs_type.name}.{observable.obs_value}"
|
|
245
|
+
self.increment_stat(full_key, observable.obs_type.name, observable.level, observable)
|
|
246
|
+
ref = self.observable_registry.upsert(observable)
|
|
247
|
+
if self.is_whitelisted(ref.obs_type, ref.obs_value):
|
|
248
|
+
ref.whitelisted = True
|
|
249
|
+
return ref
|
|
250
|
+
|
|
251
|
+
def visit_enrichment(self, enrichment: Enrichment) -> Enrichment:
|
|
252
|
+
enrichment.ref_struct[enrichment.key] = enrichment.data
|
|
253
|
+
self.enrichments.append(enrichment)
|
|
254
|
+
return enrichment
|
|
255
|
+
|
|
256
|
+
def to_json(self) -> dict[str, Any]:
|
|
257
|
+
return report_to_json(self)
|
|
258
|
+
|
|
259
|
+
def to_stdout_from_json(self, json_data: dict[str, Any]) -> None:
|
|
260
|
+
stdout_from_json(self, json_data)
|
|
261
|
+
|
|
262
|
+
def reduce_json_report(self, json_report: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
return reduce_report_json(json_report)
|
|
264
|
+
|
|
265
|
+
def to_markdown_summary(self, json_data: dict[str, Any], exclude_checks: list[str] | None = None) -> str:
|
|
266
|
+
return markdown_summary(json_data, exclude_checks=exclude_checks)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class Action(Visitor):
|
|
270
|
+
"""Minimal visitor that records remediation ideas.
|
|
271
|
+
|
|
272
|
+
Projects are encouraged to subclass this visitor and override the
|
|
273
|
+
``visit_*`` methods to trigger concrete actions (ticketing, EDR tasks,
|
|
274
|
+
notifications, ...). The default implementation keeps things simple by
|
|
275
|
+
logging observables or threat intelligence above a configurable level.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self, *, level_threshold: Level = Level.SUSPICIOUS, graph: bool = False) -> None:
|
|
279
|
+
super().__init__(graph=graph)
|
|
280
|
+
self.level_threshold = level_threshold
|
|
281
|
+
self.actions: list[dict[str, Any]] = []
|
|
282
|
+
|
|
283
|
+
def record_action(
|
|
284
|
+
self,
|
|
285
|
+
*,
|
|
286
|
+
name: str,
|
|
287
|
+
description: str,
|
|
288
|
+
impact: str | None = None,
|
|
289
|
+
context: dict[str, Any] | None = None,
|
|
290
|
+
) -> dict[str, Any]:
|
|
291
|
+
action = {
|
|
292
|
+
"name": name,
|
|
293
|
+
"description": description,
|
|
294
|
+
"impact": impact,
|
|
295
|
+
"context": context or {},
|
|
296
|
+
}
|
|
297
|
+
logger.info("[ACTION] {} -> {}", name, description)
|
|
298
|
+
self.actions.append(action)
|
|
299
|
+
return action
|
|
300
|
+
|
|
301
|
+
def visit_threat_intel(self, threat_intel: ThreatIntel) -> ThreatIntel:
|
|
302
|
+
if threat_intel.level >= self.level_threshold:
|
|
303
|
+
desc = f"{threat_intel.obs_type.name} {threat_intel.obs_value} flagged as {threat_intel.level.name}"
|
|
304
|
+
self.record_action(
|
|
305
|
+
name=f"threat_intel::{threat_intel.name}",
|
|
306
|
+
description=desc,
|
|
307
|
+
impact=threat_intel.level.name,
|
|
308
|
+
context={
|
|
309
|
+
"score": threat_intel.score,
|
|
310
|
+
"details": threat_intel.details,
|
|
311
|
+
"extra": threat_intel.extra,
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
return threat_intel
|
|
315
|
+
|
|
316
|
+
def visit_observable(self, observable: Observable) -> Observable:
|
|
317
|
+
return observable
|
|
318
|
+
|
|
319
|
+
def visit_result_check(self, result_check: ResultCheck) -> ResultCheck:
|
|
320
|
+
return result_check
|
|
321
|
+
|
|
322
|
+
def visit_container(self, container: Container) -> Container:
|
|
323
|
+
return container
|
|
324
|
+
|
|
325
|
+
def visit_enrichment(self, enrichment: Enrichment) -> Enrichment:
|
|
326
|
+
return enrichment
|
|
327
|
+
|
|
328
|
+
def to_json(self) -> list[dict[str, Any]]:
|
|
329
|
+
return copy.deepcopy(self.actions)
|
|
330
|
+
|
|
331
|
+
def get_check(self, path: str) -> Any:
|
|
332
|
+
return None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyvest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cybersecurity investigation model
|
|
5
|
+
Author-email: PakitoSec <jeromep83@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PakitoSec/cyvest
|
|
8
|
+
Project-URL: Repository, https://github.com/PakitoSec/cyvest
|
|
9
|
+
Project-URL: Issues, https://github.com/PakitoSec/cyvest/issues
|
|
10
|
+
Keywords: cybersecurity,incident-response,observability,threat-intelligence,reporting,graph
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: logurich>=0.1
|
|
24
|
+
Requires-Dist: rich>=13
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Cyvest – Cyber Investigation Model
|
|
28
|
+
|
|
29
|
+
Reusable investigation domain models, visitor helpers, and reporting utilities for incident responders. Cyvest provides
|
|
30
|
+
a consistent data model for threat intelligence, observables, and result checks while keeping the visitor layer
|
|
31
|
+
extensible for bespoke workflows.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Composition-friendly report builder that nests containers and checks.
|
|
36
|
+
- Observable graph with automatic score/level propagation across relationships.
|
|
37
|
+
- Visitor implementations for generating JSON/markdown reports or capturing follow-up actions.
|
|
38
|
+
- Tested patterns for merging external intel feeds (VirusTotal, sandbox runs, allow-lists).
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Cyvest targets Python 3.10+ and is published on PyPI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install cyvest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
Create a new report with nested containers and observables:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from cyvest import Level, ObsType, ReportBuilder, Scope
|
|
54
|
+
|
|
55
|
+
builder = ReportBuilder(graph=True)
|
|
56
|
+
|
|
57
|
+
with builder.container("body", scope=Scope.BODY) as body:
|
|
58
|
+
check = body.add_check("url_scan", description="Detected suspicious URL")
|
|
59
|
+
check.add_observable_chain(
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
"obs_type": ObsType.URL,
|
|
63
|
+
"value": "http://example.test",
|
|
64
|
+
"intel": {"name": "sandbox", "score": 4, "level": Level.SUSPICIOUS},
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
report = builder.build()
|
|
70
|
+
print(report.to_json())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run the bundled example:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync
|
|
77
|
+
uv run python examples/basic_report.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Development workflow
|
|
81
|
+
|
|
82
|
+
Set up dependencies with uv:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv sync
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Execute the unit suite:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run pytest tests
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Lint and format using Ruff:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv run ruff check
|
|
98
|
+
uv run ruff format --check
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Graph & model axioms
|
|
102
|
+
|
|
103
|
+
1. Cyclic graphs on observables or containables are not supported.
|
|
104
|
+
2. Every root containable model must be visited. (Observables may be skipped because parent links are tracked.)
|
|
105
|
+
3. Child observables do not update result checks linked only to their parents.
|
|
106
|
+
4. A `ResultCheck` score cannot be changed by an observable that is mutated elsewhere.
|
|
107
|
+
5. Adding an observable to a `ResultCheck` promotes the check to at least `Level.INFO` (a `Level.NONE` check becomes INFO).
|
|
108
|
+
|
|
109
|
+
See `examples/` and the tests under `tests/` for more scenarios, including how to subclass the provided visitors to
|
|
110
|
+
integrate your own tooling.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
cyvest/__init__.py,sha256=DGa89MSj15F2VmdrC4g8UadNA4A3y95QI46RmGWWmkA,818
|
|
2
|
+
cyvest/builder.py,sha256=j9B-xs5eYDX213RqYxWPzBTNOVE8vMgf1XtXDHaqf4w,5811
|
|
3
|
+
cyvest/check_tree.py,sha256=SzAnnC0OuEOITWS-ptb1sXb9SKGwkvQgeGGApQ5ovF4,4539
|
|
4
|
+
cyvest/models.py,sha256=Dl27Xj0MNKKAqYtS5-_M0s0Rp0EQ_dtIGt41ILNAMuE,26911
|
|
5
|
+
cyvest/observable_registry.py,sha256=1LPTjhw9y_N9TdTX77cnvPJEwKGB-J_roGbl7YdC3K8,2623
|
|
6
|
+
cyvest/report_render.py,sha256=VWWfIvDVn_9pEYy9gGUBcHP1EhAoWvv--WVmmpCNogc,12000
|
|
7
|
+
cyvest/report_serialization.py,sha256=mrDHiW_0gM4JSnYzP3pxNo1HdHVSrYtwYX5EqguX5l8,9069
|
|
8
|
+
cyvest/visitors.py,sha256=LJ1szpNrg0noZxkigvS4ZcVzQPDKM8kBbt2hd02k95E,12115
|
|
9
|
+
cyvest-0.1.0.dist-info/licenses/LICENSE,sha256=EikdGl2Ew1IWY48h_1Ppwfh0QoIxXHQadQDovHSZZdE,1066
|
|
10
|
+
cyvest-0.1.0.dist-info/METADATA,sha256=SuprQJa9USk9YGPrco2NGOFGAJ_6Qp7SyW9IPauCY_w,3367
|
|
11
|
+
cyvest-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
cyvest-0.1.0.dist-info/top_level.txt,sha256=IC_VayuEXgD7GqVsyh1h4g8P5WgXJ2ZrbNvdKhnh_Hw,7
|
|
13
|
+
cyvest-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PakitoSec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cyvest
|