cyvest 1.0.1__tar.gz → 2.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-2.0.0}/PKG-INFO +22 -112
- {cyvest-1.0.1 → cyvest-2.0.0}/README.md +21 -111
- {cyvest-1.0.1 → cyvest-2.0.0}/pyproject.toml +2 -1
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/__init__.py +1 -1
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/cli.py +24 -2
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/cyvest.py +30 -10
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/investigation.py +146 -6
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/io_rich.py +9 -5
- cyvest-2.0.0/src/cyvest/io_schema.py +393 -0
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/io_serialization.py +50 -41
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/model.py +10 -98
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/proxies.py +3 -3
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/score.py +19 -3
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/io_visualization.py +0 -0
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/keys.py +0 -0
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/levels.py +0 -0
- {cyvest-1.0.1 → cyvest-2.0.0}/src/cyvest/stats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: cyvest
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Cybersecurity investigation model
|
|
5
5
|
Keywords: cybersecurity,investigation,threat-intel,security-analysis
|
|
6
6
|
Author: PakitoSec
|
|
@@ -37,8 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
- 🔍 **Structured Investigation Modeling**: Model investigations with observables, checks, threat intelligence, and enrichments
|
|
38
38
|
- 📊 **Automatic Scoring**: Dynamic score calculation and propagation through investigation hierarchy
|
|
39
39
|
- 🎯 **Level Classification**: Automatic security level assignment (TRUSTED, INFO, SAFE, NOTABLE, SUSPICIOUS, MALICIOUS)
|
|
40
|
-
- 🔗 **Relationship Tracking**:
|
|
41
|
-
- 🏷️ **
|
|
40
|
+
- 🔗 **Relationship Tracking**: Lightweight relationship modeling between observables
|
|
41
|
+
- 🏷️ **Typed Helpers**: Built-in enums for observable types and relationships with autocomplete
|
|
42
42
|
- 📈 **Real-time Statistics**: Live metrics and aggregations throughout the investigation
|
|
43
43
|
- 🔄 **Investigation Merging**: Combine investigations from multiple threads or processes
|
|
44
44
|
- 🧵 **Multi-Threading Support**: Advanced thread-safe shared context available via `cyvest.investigation` module
|
|
@@ -82,7 +82,7 @@ from cyvest import Cyvest, Level, ObservableType, RelationshipType
|
|
|
82
82
|
|
|
83
83
|
# Create an investigation
|
|
84
84
|
with Cyvest(data={"type": "email"}) as cv:
|
|
85
|
-
# Create observables
|
|
85
|
+
# Create observables
|
|
86
86
|
url = (
|
|
87
87
|
cv.observable(ObservableType.URL, "https://phishing-site.com", internal=False)
|
|
88
88
|
.with_ti("virustotal", score=Decimal("8.5"), level=Level.MALICIOUS)
|
|
@@ -126,108 +126,30 @@ Dictionary fields merge by default; pass `merge_extra=False` (or `merge_data=Fal
|
|
|
126
126
|
|
|
127
127
|
### Observables
|
|
128
128
|
|
|
129
|
-
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
129
|
+
Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
|
|
130
130
|
|
|
131
131
|
```python
|
|
132
|
-
from cyvest import ObservableType, RelationshipType
|
|
132
|
+
from cyvest import ObservableType, RelationshipType, RelationshipDirection
|
|
133
133
|
|
|
134
|
-
# Create observable with STIX2 type enum
|
|
135
134
|
url_obs = cv.observable_create(
|
|
136
135
|
ObservableType.URL,
|
|
137
136
|
"https://malicious.com",
|
|
138
137
|
internal=False
|
|
139
138
|
)
|
|
140
139
|
|
|
141
|
-
# Or use strings (backward compatible)
|
|
142
140
|
ip_obs = cv.observable_create("ipv4-addr", "192.0.2.1", internal=False)
|
|
143
141
|
|
|
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
142
|
cv.observable_add_relationship(
|
|
156
143
|
url_obs, # Can pass ObservableProxy directly
|
|
157
144
|
ip_obs, # Or use .key for string keys
|
|
158
|
-
RelationshipType.
|
|
145
|
+
RelationshipType.RELATED_TO,
|
|
146
|
+
RelationshipDirection.BIDIRECTIONAL,
|
|
159
147
|
)
|
|
160
148
|
```
|
|
161
149
|
|
|
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.
|
|
150
|
+
Cyvest ships enums for the most common observable types; you can still pass strings for custom types.
|
|
151
|
+
Relationships are intentionally simple for now: use `RelationshipType.RELATED_TO` to link observables
|
|
152
|
+
and optionally choose a direction (`OUTBOUND`, `INBOUND`, or `BIDIRECTIONAL`) to control score propagation.
|
|
231
153
|
|
|
232
154
|
### Checks
|
|
233
155
|
|
|
@@ -430,6 +352,9 @@ cyvest merge inv1.json inv2.json -o merged.json -f rich --stats
|
|
|
430
352
|
|
|
431
353
|
# Generate an interactive visualization (requires visualization extra)
|
|
432
354
|
cyvest visualize investigation.json --min-level SUSPICIOUS --group-by-type
|
|
355
|
+
|
|
356
|
+
# Output the JSON Schema describing serialized investigations
|
|
357
|
+
cyvest schema > schema.json
|
|
433
358
|
```
|
|
434
359
|
|
|
435
360
|
## Development
|
|
@@ -484,29 +409,15 @@ mkdocs serve
|
|
|
484
409
|
mkdocs build
|
|
485
410
|
```
|
|
486
411
|
|
|
487
|
-
##
|
|
412
|
+
## JavaScript packages
|
|
488
413
|
|
|
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
|
-
```
|
|
414
|
+
The repo includes a PNPM workspace under `js/` with three packages:
|
|
415
|
+
|
|
416
|
+
- `@cyvest/cyvest-js`: TypeScript types, schema validation, and helpers for Cyvest investigations.
|
|
417
|
+
- `@cyvest/cyvest-vis`: React components for graph visualization (depends on `@cyvest/cyvest-js`).
|
|
418
|
+
- `@cyvest/cyvest-app`: Vite demo that bundles the JS packages with sample investigations.
|
|
419
|
+
|
|
420
|
+
See `docs/js-packages.md` for workspace commands and usage snippets.
|
|
510
421
|
|
|
511
422
|
## Contributing
|
|
512
423
|
|
|
@@ -539,7 +450,6 @@ Cyvest is designed for:
|
|
|
539
450
|
- **Deterministic Keys**: Same objects always generate same keys for merging
|
|
540
451
|
- **Score Propagation**: Automatic hierarchical score calculation
|
|
541
452
|
- **Flexible Export**: JSON for storage, Markdown for LLM analysis
|
|
542
|
-
- **STIX2 Relationships**: Industry-standard relationship modeling
|
|
543
453
|
- **Audit Trail**: Score change history for debugging
|
|
544
454
|
|
|
545
455
|
## 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
|
|
330
|
+
cyvest schema > schema.json
|
|
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 = "2.0.0"
|
|
4
4
|
description = "Cybersecurity investigation model"
|
|
5
5
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -40,6 +40,7 @@ build-backend = "uv_build"
|
|
|
40
40
|
[dependency-groups]
|
|
41
41
|
dev = [
|
|
42
42
|
"debugpy>=1.8.17",
|
|
43
|
+
"jsonschema>=4.23.0",
|
|
43
44
|
"pre-commit>=4.4.0",
|
|
44
45
|
"pytest>=8.4.2",
|
|
45
46
|
"pytest-cov>=6.0.0",
|
|
@@ -13,7 +13,7 @@ from cyvest.levels import Level
|
|
|
13
13
|
from cyvest.model import CheckScorePolicy, ObservableType, RelationshipDirection, RelationshipType
|
|
14
14
|
from cyvest.proxies import CheckProxy, ContainerProxy, EnrichmentProxy, ObservableProxy, ThreatIntelProxy
|
|
15
15
|
|
|
16
|
-
__version__ = "
|
|
16
|
+
__version__ = "2.0.0"
|
|
17
17
|
|
|
18
18
|
logger.disable("cyvest")
|
|
19
19
|
|
|
@@ -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
|
|
|
@@ -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), 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(
|
|
@@ -29,7 +29,7 @@ from cyvest.io_serialization import (
|
|
|
29
29
|
from cyvest.levels import Level, normalize_level
|
|
30
30
|
from cyvest.model import Check, CheckScorePolicy, Container, Enrichment, Observable, ThreatIntel
|
|
31
31
|
from cyvest.proxies import CheckProxy, ContainerProxy, EnrichmentProxy, ObservableProxy, ThreatIntelProxy
|
|
32
|
-
from cyvest.score import ScoreMode
|
|
32
|
+
from cyvest.score import ScoreMode, normalize_score_mode
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Cyvest:
|
|
@@ -44,7 +44,7 @@ class Cyvest:
|
|
|
44
44
|
self,
|
|
45
45
|
data: Any = None,
|
|
46
46
|
root_type: Literal["file", "artifact"] = "file",
|
|
47
|
-
score_mode: ScoreMode = ScoreMode.MAX,
|
|
47
|
+
score_mode: ScoreMode | Literal["max", "sum"] = ScoreMode.MAX,
|
|
48
48
|
) -> None:
|
|
49
49
|
"""
|
|
50
50
|
Initialize a new investigation.
|
|
@@ -54,7 +54,8 @@ class Cyvest:
|
|
|
54
54
|
root_type: Type of root observable ("file" or "artifact")
|
|
55
55
|
score_mode: Score calculation mode (MAX or SUM)
|
|
56
56
|
"""
|
|
57
|
-
|
|
57
|
+
normalized_score_mode = normalize_score_mode(score_mode)
|
|
58
|
+
self._investigation = Investigation(data, root_type=root_type, score_mode=normalized_score_mode)
|
|
58
59
|
|
|
59
60
|
def __enter__(self) -> Cyvest:
|
|
60
61
|
"""Context manager entry."""
|
|
@@ -263,7 +264,7 @@ class Cyvest:
|
|
|
263
264
|
Args:
|
|
264
265
|
source: Source observable or its key
|
|
265
266
|
target: Target observable or its key
|
|
266
|
-
relationship_type: Type of relationship
|
|
267
|
+
relationship_type: Type of relationship
|
|
267
268
|
direction: Direction of the relationship (None = use semantic default for relationship type)
|
|
268
269
|
|
|
269
270
|
Returns:
|
|
@@ -353,7 +354,7 @@ class Cyvest:
|
|
|
353
354
|
extra: dict[str, Any] | None = None,
|
|
354
355
|
score: Decimal | float | None = None,
|
|
355
356
|
level: Level | str | None = None,
|
|
356
|
-
score_policy: CheckScorePolicy |
|
|
357
|
+
score_policy: CheckScorePolicy | Literal["auto", "manual"] | None = None,
|
|
357
358
|
) -> CheckProxy:
|
|
358
359
|
"""
|
|
359
360
|
Create a new check.
|
|
@@ -566,7 +567,13 @@ class Cyvest:
|
|
|
566
567
|
save_investigation_json(self, filepath)
|
|
567
568
|
return str(Path(filepath).resolve())
|
|
568
569
|
|
|
569
|
-
def io_save_markdown(
|
|
570
|
+
def io_save_markdown(
|
|
571
|
+
self,
|
|
572
|
+
filepath: str | Path,
|
|
573
|
+
include_containers: bool = False,
|
|
574
|
+
include_enrichments: bool = False,
|
|
575
|
+
include_observables: bool = True,
|
|
576
|
+
) -> str:
|
|
570
577
|
"""
|
|
571
578
|
Save the investigation as a Markdown report.
|
|
572
579
|
|
|
@@ -574,6 +581,9 @@ class Cyvest:
|
|
|
574
581
|
|
|
575
582
|
Args:
|
|
576
583
|
filepath: Path to save the Markdown file (relative or absolute)
|
|
584
|
+
include_containers: Include containers section in the report (default: False)
|
|
585
|
+
include_enrichments: Include enrichments section in the report (default: False)
|
|
586
|
+
include_observables: Include observables section in the report (default: True)
|
|
577
587
|
|
|
578
588
|
Returns:
|
|
579
589
|
Absolute path to the saved file as a string
|
|
@@ -587,13 +597,23 @@ class Cyvest:
|
|
|
587
597
|
>>> path = cv.io_save_markdown("report.md")
|
|
588
598
|
>>> print(path) # /absolute/path/to/report.md
|
|
589
599
|
"""
|
|
590
|
-
save_investigation_markdown(self, filepath)
|
|
600
|
+
save_investigation_markdown(self, filepath, include_containers, include_enrichments, include_observables)
|
|
591
601
|
return str(Path(filepath).resolve())
|
|
592
602
|
|
|
593
|
-
def io_to_markdown(
|
|
603
|
+
def io_to_markdown(
|
|
604
|
+
self,
|
|
605
|
+
include_containers: bool = False,
|
|
606
|
+
include_enrichments: bool = False,
|
|
607
|
+
include_observables: bool = True,
|
|
608
|
+
) -> str:
|
|
594
609
|
"""
|
|
595
610
|
Generate a Markdown report of the investigation.
|
|
596
611
|
|
|
612
|
+
Args:
|
|
613
|
+
include_containers: Include containers section in the report (default: False)
|
|
614
|
+
include_enrichments: Include enrichments section in the report (default: False)
|
|
615
|
+
include_observables: Include observables section in the report (default: True)
|
|
616
|
+
|
|
597
617
|
Returns:
|
|
598
618
|
Markdown formatted report as a string
|
|
599
619
|
|
|
@@ -604,7 +624,7 @@ class Cyvest:
|
|
|
604
624
|
# Cybersecurity Investigation Report
|
|
605
625
|
...
|
|
606
626
|
"""
|
|
607
|
-
return generate_markdown_report(self)
|
|
627
|
+
return generate_markdown_report(self, include_containers, include_enrichments, include_observables)
|
|
608
628
|
|
|
609
629
|
def io_to_dict(self) -> dict[str, Any]:
|
|
610
630
|
"""
|
|
@@ -782,7 +802,7 @@ class Cyvest:
|
|
|
782
802
|
extra: dict[str, Any] | None = None,
|
|
783
803
|
score: Decimal | float | None = None,
|
|
784
804
|
level: Level | str | None = None,
|
|
785
|
-
score_policy: CheckScorePolicy |
|
|
805
|
+
score_policy: CheckScorePolicy | Literal["auto", "manual"] | None = None,
|
|
786
806
|
) -> CheckProxy:
|
|
787
807
|
"""
|
|
788
808
|
Create a check with fluent helper methods.
|