cyvest 1.0.1__tar.gz → 3.0.0__tar.gz
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-1.0.1 → cyvest-3.0.0}/PKG-INFO +24 -112
- {cyvest-1.0.1 → cyvest-3.0.0}/README.md +21 -111
- {cyvest-1.0.1 → cyvest-3.0.0}/pyproject.toml +4 -1
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/__init__.py +8 -3
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/cli.py +28 -6
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/cyvest.py +45 -33
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/investigation.py +161 -42
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/io_rich.py +19 -15
- cyvest-3.0.0/src/cyvest/io_schema.py +35 -0
- cyvest-3.0.0/src/cyvest/io_serialization.py +410 -0
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/levels.py +39 -13
- cyvest-3.0.0/src/cyvest/model.py +822 -0
- cyvest-3.0.0/src/cyvest/model_schema.py +173 -0
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/proxies.py +3 -72
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/score.py +33 -12
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/stats.py +24 -21
- cyvest-1.0.1/src/cyvest/io_serialization.py +0 -514
- cyvest-1.0.1/src/cyvest/model.py +0 -625
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/io_visualization.py +0 -0
- {cyvest-1.0.1 → cyvest-3.0.0}/src/cyvest/keys.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: cyvest
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Cybersecurity investigation model
|
|
5
5
|
Keywords: cybersecurity,investigation,threat-intel,security-analysis
|
|
6
6
|
Author: PakitoSec
|
|
@@ -16,7 +16,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Topic :: Security
|
|
17
17
|
Requires-Dist: click>=8
|
|
18
18
|
Requires-Dist: logurich[click]>=0.1
|
|
19
|
+
Requires-Dist: pydantic>=2.12.5
|
|
19
20
|
Requires-Dist: rich>=13
|
|
21
|
+
Requires-Dist: typing-extensions>=4.15
|
|
20
22
|
Requires-Dist: pyvis>=0.3.2 ; extra == 'visualization'
|
|
21
23
|
Requires-Python: >=3.10
|
|
22
24
|
Project-URL: Homepage, https://github.com/PakitoSec/cyvest
|
|
@@ -37,8 +39,8 @@ Description-Content-Type: text/markdown
|
|
|
37
39
|
- 🔍 **Structured Investigation Modeling**: Model investigations with observables, checks, threat intelligence, and enrichments
|
|
38
40
|
- 📊 **Automatic Scoring**: Dynamic score calculation and propagation through investigation hierarchy
|
|
39
41
|
- 🎯 **Level Classification**: Automatic security level assignment (TRUSTED, INFO, SAFE, NOTABLE, SUSPICIOUS, MALICIOUS)
|
|
40
|
-
- 🔗 **Relationship Tracking**:
|
|
41
|
-
- 🏷️ **
|
|
42
|
+
- 🔗 **Relationship Tracking**: Lightweight relationship modeling between observables
|
|
43
|
+
- 🏷️ **Typed Helpers**: Built-in enums for observable types and relationships with autocomplete
|
|
42
44
|
- 📈 **Real-time Statistics**: Live metrics and aggregations throughout the investigation
|
|
43
45
|
- 🔄 **Investigation Merging**: Combine investigations from multiple threads or processes
|
|
44
46
|
- 🧵 **Multi-Threading Support**: Advanced thread-safe shared context available via `cyvest.investigation` module
|
|
@@ -82,7 +84,7 @@ from cyvest import Cyvest, Level, ObservableType, RelationshipType
|
|
|
82
84
|
|
|
83
85
|
# Create an investigation
|
|
84
86
|
with Cyvest(data={"type": "email"}) as cv:
|
|
85
|
-
# Create observables
|
|
87
|
+
# Create observables
|
|
86
88
|
url = (
|
|
87
89
|
cv.observable(ObservableType.URL, "https://phishing-site.com", internal=False)
|
|
88
90
|
.with_ti("virustotal", score=Decimal("8.5"), level=Level.MALICIOUS)
|
|
@@ -126,108 +128,30 @@ Dictionary fields merge by default; pass `merge_extra=False` (or `merge_data=Fal
|
|
|
126
128
|
|
|
127
129
|
### Observables
|
|
128
130
|
|
|
129
|
-
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
131
|
+
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
130
132
|
|
|
131
133
|
```python
|
|
132
|
-
from cyvest import ObservableType, RelationshipType
|
|
134
|
+
from cyvest import ObservableType, RelationshipType, RelationshipDirection
|
|
133
135
|
|
|
134
|
-
# Create observable with STIX2 type enum
|
|
135
136
|
url_obs = cv.observable_create(
|
|
136
137
|
ObservableType.URL,
|
|
137
138
|
"https://malicious.com",
|
|
138
139
|
internal=False
|
|
139
140
|
)
|
|
140
141
|
|
|
141
|
-
# Or use strings (backward compatible)
|
|
142
142
|
ip_obs = cv.observable_create("ipv4-addr", "192.0.2.1", internal=False)
|
|
143
143
|
|
|
144
|
-
# Add threat intelligence
|
|
145
|
-
cv.observable_add_threat_intel(
|
|
146
|
-
url_obs.key,
|
|
147
|
-
source="virustotal",
|
|
148
|
-
score=Decimal("9.0"),
|
|
149
|
-
level=Level.MALICIOUS,
|
|
150
|
-
comment="Detected as malware distribution site"
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
# Create relationships with STIX2 relationship types
|
|
154
|
-
# Accepts observable proxies or string keys
|
|
155
144
|
cv.observable_add_relationship(
|
|
156
145
|
url_obs, # Can pass ObservableProxy directly
|
|
157
146
|
ip_obs, # Or use .key for string keys
|
|
158
|
-
RelationshipType.
|
|
147
|
+
RelationshipType.RELATED_TO,
|
|
148
|
+
RelationshipDirection.BIDIRECTIONAL,
|
|
159
149
|
)
|
|
160
150
|
```
|
|
161
151
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- Email: `EMAIL_ADDR`, `EMAIL_MESSAGE`, `EMAIL_MIME_PART`
|
|
166
|
-
- File: `FILE`, `DIRECTORY`, `ARTIFACT`
|
|
167
|
-
- System: `PROCESS`, `SOFTWARE`, `USER_ACCOUNT`, `WINDOWS_REGISTRY_KEY`
|
|
168
|
-
- Other: `AUTONOMOUS_SYSTEM`, `MUTEX`, `X509_CERTIFICATE`
|
|
169
|
-
|
|
170
|
-
**Available STIX2 Relationship Types:**
|
|
171
|
-
|
|
172
|
-
- Network: `RESOLVES_TO`, `BELONGS_TO`, `COMMUNICATES_WITH`
|
|
173
|
-
- File: `CONTAINS`, `DOWNLOADED`, `DROPPED`
|
|
174
|
-
- Email: `FROM`, `SENDER`, `TO`, `CC`, `BCC`
|
|
175
|
-
- Process: `CREATED`, `OPENED`, `PARENT`, `CHILD`
|
|
176
|
-
- General: `RELATED_TO`, `DERIVED_FROM`, `DUPLICATE_OF`
|
|
177
|
-
|
|
178
|
-
**Relationship Direction:**
|
|
179
|
-
|
|
180
|
-
Relationships support directional semantics with **automatic semantic defaults** that determine **hierarchical score propagation**:
|
|
181
|
-
|
|
182
|
-
```python
|
|
183
|
-
from cyvest import RelationshipType, RelationshipDirection
|
|
184
|
-
|
|
185
|
-
# Automatically gets OUTBOUND (domain → IP)
|
|
186
|
-
# IP is a child of domain, IP's score propagates UP to domain
|
|
187
|
-
# Accepts observable proxies directly
|
|
188
|
-
cv.observable_add_relationship(domain, ip, RelationshipType.RESOLVES_TO)
|
|
189
|
-
|
|
190
|
-
# Automatically gets INBOUND (file ← URL)
|
|
191
|
-
# URL is parent of file, file's score propagates UP to URL
|
|
192
|
-
cv.observable_add_relationship(malware, url, RelationshipType.DOWNLOADED)
|
|
193
|
-
|
|
194
|
-
# Automatically gets BIDIRECTIONAL (host ↔ host)
|
|
195
|
-
# No hierarchical propagation - scores remain independent
|
|
196
|
-
cv.observable_add_relationship(host1, host2, RelationshipType.COMMUNICATES_WITH)
|
|
197
|
-
|
|
198
|
-
# Can override semantic defaults if needed
|
|
199
|
-
cv.observable_add_relationship(
|
|
200
|
-
domain, ip, # Accepts observable proxies
|
|
201
|
-
RelationshipType.RESOLVES_TO,
|
|
202
|
-
RelationshipDirection.INBOUND # explicit override
|
|
203
|
-
)
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**Direction-Based Hierarchical Scoring:**
|
|
207
|
-
|
|
208
|
-
Relationship directions define parent-child hierarchies for score propagation:
|
|
209
|
-
|
|
210
|
-
- **OUTBOUND (→)**: `source → target` — Target is a **child** of source
|
|
211
|
-
- Source's score includes child's score: `score = max(TI_scores, child_scores)`
|
|
212
|
-
- Example: `domain → IP` means IP score flows up to domain
|
|
213
|
-
|
|
214
|
-
- **INBOUND (←)**: `source ← target` — Target is a **parent** of source
|
|
215
|
-
- Target's score includes source's score
|
|
216
|
-
- Example: `file ← URL` means file score flows up to URL
|
|
217
|
-
|
|
218
|
-
- **BIDIRECTIONAL (↔)**: `source ↔ target` — **No hierarchy**
|
|
219
|
-
- Scores do NOT propagate between observables
|
|
220
|
-
- Each maintains independent score from its own threat intel
|
|
221
|
-
- Example: `host1 ↔ host2` keeps separate scores
|
|
222
|
-
|
|
223
|
-
**Semantic Default Directions:**
|
|
224
|
-
|
|
225
|
-
Each relationship type has an intuitive default direction:
|
|
226
|
-
- **OUTBOUND (→)**: `RESOLVES_TO`, `BELONGS_TO`, `CONTAINS`, `TO`, `CC`, `BCC`, `CREATED`, `OPENED`, `PARENT`, `VALUES`
|
|
227
|
-
- **INBOUND (←)**: `DOWNLOADED`, `DROPPED`, `FROM`, `SENDER`, `CHILD`, `DERIVED_FROM`
|
|
228
|
-
- **BIDIRECTIONAL (↔)**: `COMMUNICATES_WITH`, `RELATED_TO`, `DUPLICATE_OF`
|
|
229
|
-
|
|
230
|
-
Direction symbols appear in visualizations, markdown exports, and determine score flow.
|
|
152
|
+
Cyvest ships enums for the most common observable types; you can still pass strings for custom types.
|
|
153
|
+
Relationships are intentionally simple for now: use `RelationshipType.RELATED_TO` to link observables
|
|
154
|
+
and optionally choose a direction (`OUTBOUND`, `INBOUND`, or `BIDIRECTIONAL`) to control score propagation.
|
|
231
155
|
|
|
232
156
|
### Checks
|
|
233
157
|
|
|
@@ -430,6 +354,9 @@ cyvest merge inv1.json inv2.json -o merged.json -f rich --stats
|
|
|
430
354
|
|
|
431
355
|
# Generate an interactive visualization (requires visualization extra)
|
|
432
356
|
cyvest visualize investigation.json --min-level SUSPICIOUS --group-by-type
|
|
357
|
+
|
|
358
|
+
# Output the JSON Schema describing serialized investigations and generate types
|
|
359
|
+
uv run cyvest schema -o ./schema/cyvest.schema.json && pnpm -C js/packages/cyvest-js run generate:types
|
|
433
360
|
```
|
|
434
361
|
|
|
435
362
|
## Development
|
|
@@ -484,29 +411,15 @@ mkdocs serve
|
|
|
484
411
|
mkdocs build
|
|
485
412
|
```
|
|
486
413
|
|
|
487
|
-
##
|
|
414
|
+
## JavaScript packages
|
|
488
415
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
│ ├── levels.py # Level enum and scoring logic
|
|
497
|
-
│ ├── keys.py # Key generation utilities
|
|
498
|
-
│ ├── model.py # Core data models
|
|
499
|
-
│ ├── score.py # Scoring and propagation engine
|
|
500
|
-
│ ├── stats.py # Statistics and aggregations
|
|
501
|
-
│ ├── io_serialization.py # JSON and Markdown export
|
|
502
|
-
│ ├── io_rich.py # Rich console output
|
|
503
|
-
│ └── cli.py # CLI interface
|
|
504
|
-
├── examples/ # Example scripts
|
|
505
|
-
├── tests/ # Test suite
|
|
506
|
-
├── docs/ # Documentation
|
|
507
|
-
├── pyproject.toml # Project configuration
|
|
508
|
-
└── README.md # This file
|
|
509
|
-
```
|
|
416
|
+
The repo includes a PNPM workspace under `js/` with three packages:
|
|
417
|
+
|
|
418
|
+
- `@cyvest/cyvest-js`: TypeScript types, schema validation, and helpers for Cyvest investigations.
|
|
419
|
+
- `@cyvest/cyvest-vis`: React components for graph visualization (depends on `@cyvest/cyvest-js`).
|
|
420
|
+
- `@cyvest/cyvest-app`: Vite demo that bundles the JS packages with sample investigations.
|
|
421
|
+
|
|
422
|
+
See `docs/js-packages.md` for workspace commands and usage snippets.
|
|
510
423
|
|
|
511
424
|
## Contributing
|
|
512
425
|
|
|
@@ -539,7 +452,6 @@ Cyvest is designed for:
|
|
|
539
452
|
- **Deterministic Keys**: Same objects always generate same keys for merging
|
|
540
453
|
- **Score Propagation**: Automatic hierarchical score calculation
|
|
541
454
|
- **Flexible Export**: JSON for storage, Markdown for LLM analysis
|
|
542
|
-
- **STIX2 Relationships**: Industry-standard relationship modeling
|
|
543
455
|
- **Audit Trail**: Score change history for debugging
|
|
544
456
|
|
|
545
457
|
## Future Enhancements
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
- 🔍 **Structured Investigation Modeling**: Model investigations with observables, checks, threat intelligence, and enrichments
|
|
11
11
|
- 📊 **Automatic Scoring**: Dynamic score calculation and propagation through investigation hierarchy
|
|
12
12
|
- 🎯 **Level Classification**: Automatic security level assignment (TRUSTED, INFO, SAFE, NOTABLE, SUSPICIOUS, MALICIOUS)
|
|
13
|
-
- 🔗 **Relationship Tracking**:
|
|
14
|
-
- 🏷️ **
|
|
13
|
+
- 🔗 **Relationship Tracking**: Lightweight relationship modeling between observables
|
|
14
|
+
- 🏷️ **Typed Helpers**: Built-in enums for observable types and relationships with autocomplete
|
|
15
15
|
- 📈 **Real-time Statistics**: Live metrics and aggregations throughout the investigation
|
|
16
16
|
- 🔄 **Investigation Merging**: Combine investigations from multiple threads or processes
|
|
17
17
|
- 🧵 **Multi-Threading Support**: Advanced thread-safe shared context available via `cyvest.investigation` module
|
|
@@ -55,7 +55,7 @@ from cyvest import Cyvest, Level, ObservableType, RelationshipType
|
|
|
55
55
|
|
|
56
56
|
# Create an investigation
|
|
57
57
|
with Cyvest(data={"type": "email"}) as cv:
|
|
58
|
-
# Create observables
|
|
58
|
+
# Create observables
|
|
59
59
|
url = (
|
|
60
60
|
cv.observable(ObservableType.URL, "https://phishing-site.com", internal=False)
|
|
61
61
|
.with_ti("virustotal", score=Decimal("8.5"), level=Level.MALICIOUS)
|
|
@@ -99,108 +99,30 @@ Dictionary fields merge by default; pass `merge_extra=False` (or `merge_data=Fal
|
|
|
99
99
|
|
|
100
100
|
### Observables
|
|
101
101
|
|
|
102
|
-
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
102
|
+
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
103
103
|
|
|
104
104
|
```python
|
|
105
|
-
from cyvest import ObservableType, RelationshipType
|
|
105
|
+
from cyvest import ObservableType, RelationshipType, RelationshipDirection
|
|
106
106
|
|
|
107
|
-
# Create observable with STIX2 type enum
|
|
108
107
|
url_obs = cv.observable_create(
|
|
109
108
|
ObservableType.URL,
|
|
110
109
|
"https://malicious.com",
|
|
111
110
|
internal=False
|
|
112
111
|
)
|
|
113
112
|
|
|
114
|
-
# Or use strings (backward compatible)
|
|
115
113
|
ip_obs = cv.observable_create("ipv4-addr", "192.0.2.1", internal=False)
|
|
116
114
|
|
|
117
|
-
# Add threat intelligence
|
|
118
|
-
cv.observable_add_threat_intel(
|
|
119
|
-
url_obs.key,
|
|
120
|
-
source="virustotal",
|
|
121
|
-
score=Decimal("9.0"),
|
|
122
|
-
level=Level.MALICIOUS,
|
|
123
|
-
comment="Detected as malware distribution site"
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
# Create relationships with STIX2 relationship types
|
|
127
|
-
# Accepts observable proxies or string keys
|
|
128
115
|
cv.observable_add_relationship(
|
|
129
116
|
url_obs, # Can pass ObservableProxy directly
|
|
130
117
|
ip_obs, # Or use .key for string keys
|
|
131
|
-
RelationshipType.
|
|
118
|
+
RelationshipType.RELATED_TO,
|
|
119
|
+
RelationshipDirection.BIDIRECTIONAL,
|
|
132
120
|
)
|
|
133
121
|
```
|
|
134
122
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- Email: `EMAIL_ADDR`, `EMAIL_MESSAGE`, `EMAIL_MIME_PART`
|
|
139
|
-
- File: `FILE`, `DIRECTORY`, `ARTIFACT`
|
|
140
|
-
- System: `PROCESS`, `SOFTWARE`, `USER_ACCOUNT`, `WINDOWS_REGISTRY_KEY`
|
|
141
|
-
- Other: `AUTONOMOUS_SYSTEM`, `MUTEX`, `X509_CERTIFICATE`
|
|
142
|
-
|
|
143
|
-
**Available STIX2 Relationship Types:**
|
|
144
|
-
|
|
145
|
-
- Network: `RESOLVES_TO`, `BELONGS_TO`, `COMMUNICATES_WITH`
|
|
146
|
-
- File: `CONTAINS`, `DOWNLOADED`, `DROPPED`
|
|
147
|
-
- Email: `FROM`, `SENDER`, `TO`, `CC`, `BCC`
|
|
148
|
-
- Process: `CREATED`, `OPENED`, `PARENT`, `CHILD`
|
|
149
|
-
- General: `RELATED_TO`, `DERIVED_FROM`, `DUPLICATE_OF`
|
|
150
|
-
|
|
151
|
-
**Relationship Direction:**
|
|
152
|
-
|
|
153
|
-
Relationships support directional semantics with **automatic semantic defaults** that determine **hierarchical score propagation**:
|
|
154
|
-
|
|
155
|
-
```python
|
|
156
|
-
from cyvest import RelationshipType, RelationshipDirection
|
|
157
|
-
|
|
158
|
-
# Automatically gets OUTBOUND (domain → IP)
|
|
159
|
-
# IP is a child of domain, IP's score propagates UP to domain
|
|
160
|
-
# Accepts observable proxies directly
|
|
161
|
-
cv.observable_add_relationship(domain, ip, RelationshipType.RESOLVES_TO)
|
|
162
|
-
|
|
163
|
-
# Automatically gets INBOUND (file ← URL)
|
|
164
|
-
# URL is parent of file, file's score propagates UP to URL
|
|
165
|
-
cv.observable_add_relationship(malware, url, RelationshipType.DOWNLOADED)
|
|
166
|
-
|
|
167
|
-
# Automatically gets BIDIRECTIONAL (host ↔ host)
|
|
168
|
-
# No hierarchical propagation - scores remain independent
|
|
169
|
-
cv.observable_add_relationship(host1, host2, RelationshipType.COMMUNICATES_WITH)
|
|
170
|
-
|
|
171
|
-
# Can override semantic defaults if needed
|
|
172
|
-
cv.observable_add_relationship(
|
|
173
|
-
domain, ip, # Accepts observable proxies
|
|
174
|
-
RelationshipType.RESOLVES_TO,
|
|
175
|
-
RelationshipDirection.INBOUND # explicit override
|
|
176
|
-
)
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Direction-Based Hierarchical Scoring:**
|
|
180
|
-
|
|
181
|
-
Relationship directions define parent-child hierarchies for score propagation:
|
|
182
|
-
|
|
183
|
-
- **OUTBOUND (→)**: `source → target` — Target is a **child** of source
|
|
184
|
-
- Source's score includes child's score: `score = max(TI_scores, child_scores)`
|
|
185
|
-
- Example: `domain → IP` means IP score flows up to domain
|
|
186
|
-
|
|
187
|
-
- **INBOUND (←)**: `source ← target` — Target is a **parent** of source
|
|
188
|
-
- Target's score includes source's score
|
|
189
|
-
- Example: `file ← URL` means file score flows up to URL
|
|
190
|
-
|
|
191
|
-
- **BIDIRECTIONAL (↔)**: `source ↔ target` — **No hierarchy**
|
|
192
|
-
- Scores do NOT propagate between observables
|
|
193
|
-
- Each maintains independent score from its own threat intel
|
|
194
|
-
- Example: `host1 ↔ host2` keeps separate scores
|
|
195
|
-
|
|
196
|
-
**Semantic Default Directions:**
|
|
197
|
-
|
|
198
|
-
Each relationship type has an intuitive default direction:
|
|
199
|
-
- **OUTBOUND (→)**: `RESOLVES_TO`, `BELONGS_TO`, `CONTAINS`, `TO`, `CC`, `BCC`, `CREATED`, `OPENED`, `PARENT`, `VALUES`
|
|
200
|
-
- **INBOUND (←)**: `DOWNLOADED`, `DROPPED`, `FROM`, `SENDER`, `CHILD`, `DERIVED_FROM`
|
|
201
|
-
- **BIDIRECTIONAL (↔)**: `COMMUNICATES_WITH`, `RELATED_TO`, `DUPLICATE_OF`
|
|
202
|
-
|
|
203
|
-
Direction symbols appear in visualizations, markdown exports, and determine score flow.
|
|
123
|
+
Cyvest ships enums for the most common observable types; you can still pass strings for custom types.
|
|
124
|
+
Relationships are intentionally simple for now: use `RelationshipType.RELATED_TO` to link observables
|
|
125
|
+
and optionally choose a direction (`OUTBOUND`, `INBOUND`, or `BIDIRECTIONAL`) to control score propagation.
|
|
204
126
|
|
|
205
127
|
### Checks
|
|
206
128
|
|
|
@@ -403,6 +325,9 @@ cyvest merge inv1.json inv2.json -o merged.json -f rich --stats
|
|
|
403
325
|
|
|
404
326
|
# Generate an interactive visualization (requires visualization extra)
|
|
405
327
|
cyvest visualize investigation.json --min-level SUSPICIOUS --group-by-type
|
|
328
|
+
|
|
329
|
+
# Output the JSON Schema describing serialized investigations and generate types
|
|
330
|
+
uv run cyvest schema -o ./schema/cyvest.schema.json && pnpm -C js/packages/cyvest-js run generate:types
|
|
406
331
|
```
|
|
407
332
|
|
|
408
333
|
## Development
|
|
@@ -457,29 +382,15 @@ mkdocs serve
|
|
|
457
382
|
mkdocs build
|
|
458
383
|
```
|
|
459
384
|
|
|
460
|
-
##
|
|
385
|
+
## JavaScript packages
|
|
461
386
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
│ ├── levels.py # Level enum and scoring logic
|
|
470
|
-
│ ├── keys.py # Key generation utilities
|
|
471
|
-
│ ├── model.py # Core data models
|
|
472
|
-
│ ├── score.py # Scoring and propagation engine
|
|
473
|
-
│ ├── stats.py # Statistics and aggregations
|
|
474
|
-
│ ├── io_serialization.py # JSON and Markdown export
|
|
475
|
-
│ ├── io_rich.py # Rich console output
|
|
476
|
-
│ └── cli.py # CLI interface
|
|
477
|
-
├── examples/ # Example scripts
|
|
478
|
-
├── tests/ # Test suite
|
|
479
|
-
├── docs/ # Documentation
|
|
480
|
-
├── pyproject.toml # Project configuration
|
|
481
|
-
└── README.md # This file
|
|
482
|
-
```
|
|
387
|
+
The repo includes a PNPM workspace under `js/` with three packages:
|
|
388
|
+
|
|
389
|
+
- `@cyvest/cyvest-js`: TypeScript types, schema validation, and helpers for Cyvest investigations.
|
|
390
|
+
- `@cyvest/cyvest-vis`: React components for graph visualization (depends on `@cyvest/cyvest-js`).
|
|
391
|
+
- `@cyvest/cyvest-app`: Vite demo that bundles the JS packages with sample investigations.
|
|
392
|
+
|
|
393
|
+
See `docs/js-packages.md` for workspace commands and usage snippets.
|
|
483
394
|
|
|
484
395
|
## Contributing
|
|
485
396
|
|
|
@@ -512,7 +423,6 @@ Cyvest is designed for:
|
|
|
512
423
|
- **Deterministic Keys**: Same objects always generate same keys for merging
|
|
513
424
|
- **Score Propagation**: Automatic hierarchical score calculation
|
|
514
425
|
- **Flexible Export**: JSON for storage, Markdown for LLM analysis
|
|
515
|
-
- **STIX2 Relationships**: Industry-standard relationship modeling
|
|
516
426
|
- **Audit Trail**: Score change history for debugging
|
|
517
427
|
|
|
518
428
|
## Future Enhancements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cyvest"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "Cybersecurity investigation model"
|
|
5
5
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -11,7 +11,9 @@ authors = [
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"click>=8",
|
|
13
13
|
"logurich[click]>=0.1",
|
|
14
|
+
"pydantic>=2.12.5",
|
|
14
15
|
"rich>=13",
|
|
16
|
+
"typing-extensions>=4.15",
|
|
15
17
|
]
|
|
16
18
|
keywords = ["cybersecurity", "investigation", "threat-intel", "security-analysis"]
|
|
17
19
|
classifiers = [
|
|
@@ -40,6 +42,7 @@ build-backend = "uv_build"
|
|
|
40
42
|
[dependency-groups]
|
|
41
43
|
dev = [
|
|
42
44
|
"debugpy>=1.8.17",
|
|
45
|
+
"jsonschema>=4.23.0",
|
|
43
46
|
"pre-commit>=4.4.0",
|
|
44
47
|
"pytest>=8.4.2",
|
|
45
48
|
"pytest-cov>=6.0.0",
|
|
@@ -8,12 +8,17 @@ programmatically with automatic scoring, level calculation, and rich reporting c
|
|
|
8
8
|
from logurich import logger
|
|
9
9
|
|
|
10
10
|
from cyvest.cyvest import Cyvest
|
|
11
|
-
from cyvest.investigation import InvestigationWhitelist
|
|
12
11
|
from cyvest.levels import Level
|
|
13
|
-
from cyvest.model import
|
|
12
|
+
from cyvest.model import (
|
|
13
|
+
CheckScorePolicy,
|
|
14
|
+
InvestigationWhitelist,
|
|
15
|
+
ObservableType,
|
|
16
|
+
RelationshipDirection,
|
|
17
|
+
RelationshipType,
|
|
18
|
+
)
|
|
14
19
|
from cyvest.proxies import CheckProxy, ContainerProxy, EnrichmentProxy, ObservableProxy, ThreatIntelProxy
|
|
15
20
|
|
|
16
|
-
__version__ = "
|
|
21
|
+
__version__ = "3.0.0"
|
|
17
22
|
|
|
18
23
|
logger.disable("cyvest")
|
|
19
24
|
|
|
@@ -12,11 +12,12 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
|
-
from logurich import
|
|
15
|
+
from logurich import logger
|
|
16
16
|
from logurich.opt_click import click_logger_params
|
|
17
17
|
from rich.console import Console
|
|
18
18
|
|
|
19
19
|
from cyvest import __version__
|
|
20
|
+
from cyvest.io_schema import get_investigation_schema
|
|
20
21
|
from cyvest.io_serialization import load_investigation_json
|
|
21
22
|
from cyvest.io_visualization import VisualizationDependencyMissingError
|
|
22
23
|
|
|
@@ -69,7 +70,6 @@ def _write_markdown(data: dict[str, Any], output_path: Path) -> None:
|
|
|
69
70
|
@click.version_option(__version__, prog_name="Cyvest")
|
|
70
71
|
def cli() -> None:
|
|
71
72
|
"""Cyvest - Cybersecurity Investigation Framework."""
|
|
72
|
-
init_logger("INFO")
|
|
73
73
|
logger.enable("cyvest")
|
|
74
74
|
logger.info("> [green bold]CYVEST[/green bold]")
|
|
75
75
|
|
|
@@ -166,10 +166,10 @@ def merge(inputs: tuple[Path, ...], output: Path, output_format: str, stats: boo
|
|
|
166
166
|
if stats:
|
|
167
167
|
logger.info("[bold]Merged Investigation Statistics:[/bold]")
|
|
168
168
|
investigation_stats = main_investigation.get_statistics()
|
|
169
|
-
logger.info(f" Total Observables: {investigation_stats.
|
|
170
|
-
logger.info(f" Total Checks: {investigation_stats.
|
|
171
|
-
logger.info(f" Total Threat Intel: {investigation_stats.
|
|
172
|
-
logger.info(f" Total Containers: {investigation_stats.
|
|
169
|
+
logger.info(f" Total Observables: {investigation_stats.total_observables}")
|
|
170
|
+
logger.info(f" Total Checks: {investigation_stats.total_checks}")
|
|
171
|
+
logger.info(f" Total Threat Intel: {investigation_stats.total_threat_intel}")
|
|
172
|
+
logger.info(f" Total Containers: {investigation_stats.total_containers}")
|
|
173
173
|
logger.info(f" Global Score: {main_investigation.get_global_score()}")
|
|
174
174
|
logger.info(f" Global Level: {main_investigation.get_global_level()}\n")
|
|
175
175
|
|
|
@@ -221,6 +221,28 @@ def export(input: Path, output: Path, export_format: str) -> None:
|
|
|
221
221
|
logger.info(f"[green]Exported to Markdown: {output_path}[/green]")
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
@cli.command(name="schema")
|
|
225
|
+
@click.option(
|
|
226
|
+
"-o",
|
|
227
|
+
"--output",
|
|
228
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
229
|
+
help="Write the JSON Schema to a file instead of stdout.",
|
|
230
|
+
)
|
|
231
|
+
def schema_cmd(output: Path | None) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Emit the JSON Schema describing serialized investigations.
|
|
234
|
+
"""
|
|
235
|
+
schema = get_investigation_schema()
|
|
236
|
+
if output:
|
|
237
|
+
output_path = output.resolve()
|
|
238
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
output_path.write_text(json.dumps(schema, indent=2) + "\n", encoding="utf-8")
|
|
240
|
+
logger.info(f"[green]Schema written to: {output_path}[/green]")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
logger.rich("INFO", json.dumps(schema, indent=2), prefix=False)
|
|
244
|
+
|
|
245
|
+
|
|
224
246
|
@cli.command()
|
|
225
247
|
@click.argument("input", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
226
248
|
@click.option(
|