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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cyvest
3
- Version: 1.0.1
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**: STIX2-compliant relationship modeling between observables
41
- - 🏷️ **STIX2 Type Support**: Built-in enums for STIX2 Observable and Relationship types with autocomplete
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 with STIX2 types
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.). Use STIX2-compliant types for standardization:
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.RESOLVES_TO
147
+ RelationshipType.RELATED_TO,
148
+ RelationshipDirection.BIDIRECTIONAL,
159
149
  )
160
150
  ```
161
151
 
162
- **Available STIX2 Observable Types:**
163
-
164
- - Network: `IPV4_ADDR`, `IPV6_ADDR`, `DOMAIN_NAME`, `URL`, `MAC_ADDR`, `NETWORK_TRAFFIC`
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
- ## Project Structure
414
+ ## JavaScript packages
488
415
 
489
- ```
490
- cyvest/
491
- ├── src/cyvest/
492
- │ ├── __init__.py # Package initialization
493
- │ ├── cyvest.py # High-level API facade
494
- │ ├── investigation.py # Core state management with merge-on-create
495
- │ ├── proxies.py # Read-only proxies + fluent helper methods
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**: STIX2-compliant relationship modeling between observables
14
- - 🏷️ **STIX2 Type Support**: Built-in enums for STIX2 Observable and Relationship types with autocomplete
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 with STIX2 types
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.). Use STIX2-compliant types for standardization:
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.RESOLVES_TO
118
+ RelationshipType.RELATED_TO,
119
+ RelationshipDirection.BIDIRECTIONAL,
132
120
  )
133
121
  ```
134
122
 
135
- **Available STIX2 Observable Types:**
136
-
137
- - Network: `IPV4_ADDR`, `IPV6_ADDR`, `DOMAIN_NAME`, `URL`, `MAC_ADDR`, `NETWORK_TRAFFIC`
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
- ## Project Structure
385
+ ## JavaScript packages
461
386
 
462
- ```
463
- cyvest/
464
- ├── src/cyvest/
465
- │ ├── __init__.py # Package initialization
466
- │ ├── cyvest.py # High-level API facade
467
- │ ├── investigation.py # Core state management with merge-on-create
468
- │ ├── proxies.py # Read-only proxies + fluent helper methods
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 = "1.0.1"
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 CheckScorePolicy, ObservableType, RelationshipDirection, RelationshipType
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__ = "1.0.1"
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 init_logger, logger
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.get('total_observables', 0)}")
170
- logger.info(f" Total Checks: {investigation_stats.get('total_checks', 0)}")
171
- logger.info(f" Total Threat Intel: {investigation_stats.get('total_threat_intel', 0)}")
172
- logger.info(f" Total Containers: {investigation_stats.get('total_containers', 0)}")
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(