cyvest 4.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cyvest might be problematic. Click here for more details.

@@ -0,0 +1,538 @@
1
+ Metadata-Version: 2.3
2
+ Name: cyvest
3
+ Version: 4.4.0
4
+ Summary: Cybersecurity investigation model
5
+ Keywords: cybersecurity,investigation,threat-intel,security-analysis
6
+ Author: PakitoSec
7
+ Author-email: PakitoSec <jeromep83@gmail.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security
17
+ Requires-Dist: click>=8
18
+ Requires-Dist: logurich[click]>=0.6.0
19
+ Requires-Dist: pydantic>=2.12.5
20
+ Requires-Dist: rich>=13
21
+ Requires-Dist: typing-extensions>=4.15
22
+ Requires-Dist: pyvis>=0.3.2 ; extra == 'visualization'
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/PakitoSec/cyvest
25
+ Project-URL: Issues, https://github.com/PakitoSec/cyvest/issues
26
+ Project-URL: Repository, https://github.com/PakitoSec/cyvest
27
+ Provides-Extra: visualization
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Cyvest - Cybersecurity Investigation Framework
31
+
32
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+
35
+ **Cyvest** is a Python framework for building, analyzing, and structuring cybersecurity investigations programmatically. It provides automatic scoring, level calculation, relationship tracking, and rich reporting capabilities.
36
+
37
+ ## Features
38
+
39
+ - ๐Ÿ” **Structured Investigation Modeling**: Model investigations with observables, checks, threat intelligence, and enrichments
40
+ - ๐Ÿ“Š **Automatic Scoring**: Dynamic score calculation and propagation through investigation hierarchy
41
+ - ๐ŸŽฏ **Level Classification**: Automatic security level assignment (TRUSTED, INFO, SAFE, NOTABLE, SUSPICIOUS, MALICIOUS)
42
+ - ๐Ÿ”— **Relationship Tracking**: Lightweight relationship modeling between observables
43
+ - ๐Ÿท๏ธ **Typed Helpers**: Built-in enums for observable types and relationships with autocomplete
44
+ - ๐Ÿ“ˆ **Real-time Statistics**: Live metrics and aggregations throughout the investigation
45
+ - ๐Ÿ”„ **Investigation Merging**: Combine investigations from multiple threads or processes
46
+ - ๐Ÿงต **Multi-Threading Support**: Advanced thread-safe shared context available via `cyvest.shared`
47
+ - ๐Ÿ’พ **Multiple Export Formats**: JSON and Markdown output for reporting and LLM consumption
48
+ - ๐ŸŽจ **Rich Console Output**: Beautiful terminal displays with the Rich library
49
+ - ๐Ÿงฉ **Fluent helpers**: Convenient API with method chaining for rapid development
50
+
51
+ ## Installation
52
+
53
+ ### Using uv (recommended)
54
+
55
+ ```bash
56
+ # Install uv if not already installed
57
+ curl -LsSf https://astral.sh/uv/install.sh | sh
58
+
59
+ # Clone the repository
60
+ git clone https://github.com/PakitoSec/cyvest.git
61
+ cd cyvest
62
+
63
+ # Install dependencies
64
+ uv sync
65
+
66
+ # Install in development mode
67
+ uv pip install -e .
68
+ ```
69
+
70
+ ### Using pip
71
+
72
+ ```bash
73
+ pip install -e .
74
+ ```
75
+
76
+ > Install the optional visualization extra with\
77
+ > `pip install "cyvest[visualization]"` (or `uv pip install -e ".[visualization]"`).
78
+
79
+ ## Quick Start
80
+
81
+ ```python
82
+ from decimal import Decimal
83
+ from cyvest import Cyvest
84
+
85
+ # Create an investigation (root_data becomes the root observable extra)
86
+ cv = Cyvest(root_data={"type": "email"})
87
+
88
+ # For deterministic reports (enables diffing between runs), pass a custom investigation_id:
89
+ # cv = Cyvest(root_data={"type": "email"}, investigation_id="email-analysis-v1")
90
+
91
+ # Create observables
92
+ url = (
93
+ cv.observable(cv.OBS.URL, "https://phishing-site.com", internal=False)
94
+ .with_ti("virustotal", score=Decimal("8.5"), level=cv.LVL.MALICIOUS)
95
+ .relate_to(cv.root(), cv.REL.RELATED_TO)
96
+ )
97
+
98
+ # Create checks
99
+ check = cv.check("url_analysis", "email_body", "Analyze suspicious URL")
100
+ check.link_observable(url)
101
+ check.with_score(Decimal("8.5"), "Malicious URL detected")
102
+
103
+ # Display results
104
+ print(f"Global Score: {cv.get_global_score()}")
105
+ print(f"Global Level: {cv.get_global_level()}")
106
+
107
+ # Export
108
+ cv.io_save_json("investigation.json")
109
+ ```
110
+
111
+ ### Model Proxies
112
+
113
+ Cyvest only exposes immutable model proxies. Helpers like `observable_create`, `check_create`, and the
114
+ fluent `cv.observable()`/`cv.check()` convenience methods return `ObservableProxy`, `CheckProxy`, `ContainerProxy`, etc.
115
+ These proxies reflect the live investigation state but raise `AttributeError` if you try to assign to their attributes.
116
+ All mutations are routed through the Investigation layer, so use the facade helpers (`cv.observable_set_level`,
117
+ `cv.check_update_score`, `cv.observable_add_threat_intel`) or the built-in fluent methods on the proxies themselves
118
+ (`with_ti`, `relate_to`, `link_observable`, `with_score`, โ€ฆ) so the score engine and audit log stay consistent.
119
+
120
+ Mutation helpers that reference existing objects (for example, `cv.observable_add_relationship`,
121
+ `cv.check_link_observable`, `cv.container_add_check`) raise `KeyError` when a key is missing.
122
+
123
+ Safe metadata fields like `comment`, `extra`, or `internal` can be updated through the proxies without breaking score
124
+ consistency:
125
+
126
+ ```python
127
+ url_obs.update_metadata(comment="triaged", internal=False, extra={"ticket": "INC-4242"})
128
+ check.update_metadata(description="New scope", extra={"playbook": "url-analysis"})
129
+ ```
130
+
131
+ Dictionary fields merge by default; pass `merge_extra=False` (or `merge_data=False` for enrichments) to overwrite them.
132
+
133
+ ### Threat Intel Drafts
134
+
135
+ When the observable is unknown yet, create a draft and attach it later:
136
+
137
+ ```python
138
+ draft = cv.threat_intel_draft("vt", score=Decimal("4.2"), comment="Initial lookup")
139
+ obs = cv.observable(cv.OBS.DOMAIN, "example.com")
140
+ obs.with_ti_draft(draft)
141
+ ```
142
+
143
+ Drafts are plain `ThreatIntel` objects with no `observable_key` yet; attaching generates the key.
144
+
145
+ ## Core Concepts
146
+
147
+ ### Observables
148
+
149
+ Observables represent cyber artifacts (URLs, IPs, domains, hashes, files, etc.).
150
+
151
+ ```python
152
+ from cyvest import Cyvest
153
+
154
+ cv = Cyvest()
155
+
156
+ url_obs = cv.observable_create(cv.OBS.URL, "https://malicious.com", internal=False)
157
+
158
+ ip_obs = cv.observable_create(cv.OBS.IPV4_ADDR, "192.0.2.1", internal=False)
159
+
160
+ cv.observable_add_relationship(
161
+ url_obs, # Can pass ObservableProxy directly
162
+ ip_obs, # Or use .key for string keys
163
+ cv.REL.RELATED_TO,
164
+ cv.DIR.BIDIRECTIONAL,
165
+ )
166
+ ```
167
+
168
+ Cyvest exposes enums for observable types and relationships via the facade (`cv.OBS`, `cv.REL`, `cv.DIR`)
169
+ so IDEs can autocomplete the official vocabulary without extra imports.
170
+
171
+ ### Checks
172
+
173
+ Checks represent verification steps in your investigation:
174
+
175
+ ```python
176
+ check = cv.check_create(
177
+ check_id="malware_detection",
178
+ scope="endpoint",
179
+ description="Verify file hash against threat intel",
180
+ score=Decimal("8.0"),
181
+ level=cv.LVL.MALICIOUS
182
+ )
183
+
184
+ # Link observables to checks
185
+ cv.check_link_observable(check.key, file_hash_obs.key)
186
+ ```
187
+
188
+ ### Threat Intelligence
189
+
190
+ Threat intelligence provides verdicts from external sources:
191
+
192
+ ```python
193
+ cv.observable_add_threat_intel(
194
+ observable.key,
195
+ source="virustotal",
196
+ score=Decimal("7.5"),
197
+ level=cv.LVL.SUSPICIOUS,
198
+ comment="15/70 vendors flagged as malicious",
199
+ taxonomies=[cv.taxonomy(level=cv.LVL.MALICIOUS, name="scan", value="trojan")]
200
+ )
201
+ ```
202
+
203
+ Taxonomies are unique by name per threat intel entry. Use the fluent helpers to add or remove them:
204
+
205
+ ```python
206
+ ti = cv.observable_add_threat_intel(observable.key, source="vt", score=Decimal("7.5"))
207
+ ti.add_taxonomy(level=cv.LVL.SUSPICIOUS, name="confidence", value="medium")
208
+ ti.remove_taxonomy("confidence")
209
+ ```
210
+
211
+ ### Containers
212
+
213
+ Containers organize checks hierarchically:
214
+
215
+ ```python
216
+ with cv.container("network_analysis") as network:
217
+ with network.sub_container("c2_detection") as c2:
218
+ check = cv.check("beacon_detection", "network", "Detect C2 beacons")
219
+ c2.add_check(check)
220
+ ```
221
+
222
+ ### Lookup Helpers
223
+
224
+ Use facade getters with either key strings or component parameters:
225
+
226
+ ```python
227
+ url_obs = cv.observable_create(cv.OBS.URL, "https://malicious.com")
228
+ same_url = cv.observable_get(cv.OBS.URL, "https://malicious.com")
229
+ same_url_by_key = cv.observable_get(url_obs.key)
230
+
231
+ check = cv.check_create("malware_detection", "endpoint", "Verify file hash")
232
+ same_check = cv.check_get("malware_detection", "endpoint")
233
+ same_check_by_key = cv.check_get(check.key)
234
+
235
+ container = cv.container_create("network_analysis")
236
+ same_container = cv.container_get("network_analysis")
237
+ same_container_by_key = cv.container_get(container.key)
238
+
239
+ enrichment = cv.enrichment_create("whois", {"registrar": "Example Inc"})
240
+ same_enrichment = cv.enrichment_get("whois")
241
+ same_enrichment_by_key = cv.enrichment_get(enrichment.key)
242
+ ```
243
+
244
+ Low-level `Investigation` getters accept keys only; use the facade for component-based lookups.
245
+
246
+ ### Multi-Threaded Investigations
247
+
248
+ **Advanced Feature**: Use `Cyvest.shared_context()` (or `SharedInvestigationContext` from `cyvest.shared`) for safe parallel task execution with automatic observable sharing:
249
+
250
+ ```python
251
+ from cyvest import Cyvest
252
+ from concurrent.futures import ThreadPoolExecutor, as_completed
253
+
254
+ def email_analysis(shared_context):
255
+ # create_cyvest() yields a task-local Cyvest that auto-merges on context exit
256
+ with shared_context.create_cyvest() as cy:
257
+ data = cy.root().extra
258
+ cy.observable(cy.OBS.DOMAIN_NAME, data.get("domain"))
259
+
260
+ # Create shared context
261
+ main_cy = Cyvest(root_data=email_data, root_type=Cyvest.OBS.ARTIFACT)
262
+ shared = main_cy.shared_context()
263
+
264
+ # Run tasks in parallel - they can reference each other's observables
265
+ with ThreadPoolExecutor(max_workers=4) as executor:
266
+ futures = [executor.submit(email_analysis, shared) for _ in tasks]
267
+ for future in as_completed(futures):
268
+ future.result() # Auto-reconciled
269
+
270
+ # Get merged investigation (same object passed to shared_context)
271
+ final_cy = main_cy
272
+ ```
273
+
274
+ See `examples/04_email.py` for a complete multi-threaded investigation example.
275
+
276
+ ### Scoring & Levels
277
+
278
+ Scores and levels are automatically calculated and propagated:
279
+
280
+ - **Threat Intel โ†’ Observable**: Observable score = **max** of all threat intel scores (not sum)
281
+ - **Observable Hierarchy**: Parent observable scores include child observable scores based on relationship direction:
282
+ - **OUTBOUND relationships**: target scores propagate to source (source is parent)
283
+ - **INBOUND relationships**: source scores propagate to target (target is parent)
284
+ - **BIDIRECTIONAL relationships**: no hierarchical propagation
285
+ - **Observable โ†’ Check (provenance-aware)**: Check score/level only considers observables reachable through *effective* links (`observable_links`)
286
+ - A link is effective when `propagation_mode="GLOBAL"` or when the check's `origin_investigation_id` matches the current investigation id
287
+ - **Check โ†’ Global**: All check scores sum to global investigation score
288
+
289
+ Observable score aggregation is configurable via `score_mode_obs`:
290
+
291
+ ```python
292
+ from cyvest import Cyvest
293
+ from cyvest.score import ScoreMode
294
+
295
+ cv = Cyvest(score_mode_obs=ScoreMode.MAX) # default
296
+ cv = Cyvest(score_mode_obs=ScoreMode.SUM) # accumulative children
297
+ ```
298
+
299
+ **Provenance model**
300
+
301
+ - `Investigation.investigation_id` is a stable ULID included in exports.
302
+ - Checks keep a *canonical origin* (`origin_investigation_id`) for LOCAL_ONLY propagation; it is compared against the current investigation id.
303
+
304
+ **Audit log**
305
+
306
+ - All meaningful changes (including score/level changes) are recorded in the investigation-level audit log.
307
+ - Per-object histories are not stored; use `cv.investigation_get_audit_log()` to review changes.
308
+ - For compact, deterministic JSON output (useful for testing/diffing), exclude the audit log:
309
+ ```python
310
+ cv.io_save_json("output.json", include_audit_log=False) # audit_log: null
311
+ cv.io_to_invest(include_audit_log=False) # schema.audit_log is None
312
+ ```
313
+
314
+ To force cross-investigation propagation for a specific link, use a GLOBAL link:
315
+
316
+ ```python
317
+ cv.check_link_observable(check.key, observable.key, propagation_mode="GLOBAL")
318
+ # or fluent:
319
+ cv.check("id", "scope", "desc").link_observable(observable, propagation_mode="GLOBAL")
320
+ ```
321
+
322
+ Score to Level mapping:
323
+
324
+ - `< 0.0` โ†’ TRUSTED
325
+ - `== 0.0` โ†’ INFO
326
+ - `< 3.0` โ†’ NOTABLE
327
+ - `< 5.0` โ†’ SUSPICIOUS
328
+ - `>= 5.0` โ†’ MALICIOUS
329
+
330
+ **SAFE Level Protection:**
331
+
332
+ The SAFE level has special protection for trusted/whitelisted observables:
333
+
334
+ ```python
335
+ # Mark a known-good domain as SAFE
336
+ trusted = cv.observable_create(
337
+ cv.OBS.DOMAIN_NAME,
338
+ "trusted.example.com",
339
+ level=cv.LVL.SAFE
340
+ )
341
+
342
+ # Adding low-score threat intel won't downgrade to TRUSTED or INFO
343
+ cv.observable_add_threat_intel(trusted.key, "source1", score=Decimal("0"))
344
+ # Level stays SAFE, score updates to 0
345
+
346
+ # But high-score threat intel can still upgrade to MALICIOUS if warranted
347
+ cv.observable_add_threat_intel(trusted.key, "source2", score=Decimal("6.0"))
348
+ # Level upgrades to MALICIOUS, score updates to 6.0
349
+
350
+ # Threat intel with SAFE level can also mark observables as SAFE
351
+ uncertain = cv.observable_create(cv.OBS.DOMAIN_NAME, "example.com")
352
+ cv.observable_add_threat_intel(
353
+ uncertain.key,
354
+ "whitelist_service",
355
+ score=Decimal("0"),
356
+ level=cv.LVL.SAFE
357
+ )
358
+ # Observable upgraded to SAFE level with automatic downgrade protection
359
+ ```
360
+
361
+ SAFE observables:
362
+ - Cannot be downgraded to lower levels (NONE, TRUSTED, INFO)
363
+ - Can be upgraded to higher levels (NOTABLE, SUSPICIOUS, MALICIOUS)
364
+ - Score values still update based on threat intelligence
365
+ - Protection is preserved during investigation merges
366
+ - Can be marked SAFE by threat intel sources (e.g., whitelists, reputation databases)
367
+
368
+ SAFE checks:
369
+ - Automatically inherit SAFE level when linked to SAFE observables (if all other observables are โ‰ค SAFE)
370
+ - Can still upgrade to higher levels when NOTABLE/SUSPICIOUS/MALICIOUS observables are linked
371
+
372
+ **Root Observable Barrier:**
373
+
374
+ The root observable (the investigation's entry point with `value="root"`) acts as a special barrier to prevent cross-contamination:
375
+ Its key is derived from type + value (e.g. `obs:file:root` or `obs:artifact:root`).
376
+
377
+ **Barrier as Child** - When root appears as a child of other observables, it is **skipped** in their score calculations.
378
+
379
+ **Barrier as Parent** - Root's propagation is asymmetric:
380
+ - Root **CAN** be updated when children change (aggregates child scores)
381
+ - Root **does NOT** propagate upward beyond itself (stops recursive propagation)
382
+ - Root **DOES** propagate to checks normally
383
+
384
+ This design enables flexible investigation structures while preventing unintended score contamination.
385
+
386
+ ## Examples
387
+
388
+ See the `examples/` directory for complete examples:
389
+
390
+ - **01_email_basic.py**: Basic email phishing investigation
391
+ - **02_urls_and_ips.py**: Network investigation with URLs and IPs
392
+ - **03_merge_demo.py**: Multi-process investigation merging
393
+ - **04_email.py**: Multi-threaded investigation with SharedInvestigationContext
394
+ - **05_visualization.py**: Interactive HTML visualization showcasing scores, levels, and relationship flows
395
+
396
+ Run an example:
397
+
398
+ ```bash
399
+ python examples/01_email_basic.py
400
+ python examples/04_email.py
401
+ python examples/05_visualization.py
402
+ ```
403
+
404
+ ## CLI Usage
405
+
406
+ Cyvest includes a command-line interface for working with investigation files:
407
+
408
+ ```bash
409
+ # Display investigation
410
+ cyvest show investigation.json --graph
411
+
412
+ # Show statistics
413
+ cyvest stats investigation.json --detailed
414
+
415
+ # Export to markdown
416
+ cyvest export investigation.json -o report.md -f markdown
417
+
418
+ # Merge investigations with automatic deduplication
419
+ cyvest merge inv1.json inv2.json inv3.json -o merged.json
420
+
421
+ # Merge with statistics display
422
+ cyvest merge inv1.json inv2.json -o merged.json --stats
423
+
424
+ # Merge and display rich summary
425
+ cyvest merge inv1.json inv2.json -o merged.json -f rich --stats
426
+
427
+ # Generate an interactive visualization (requires visualization extra)
428
+ cyvest visualize investigation.json --min-level SUSPICIOUS --group-by-type
429
+
430
+ # Output the JSON Schema describing serialized investigations and generate types
431
+ uv run cyvest schema -o ./schema/cyvest.schema.json && pnpm -C js/packages/cyvest-js run generate:types
432
+ ```
433
+
434
+ ## Development
435
+
436
+ ### Setup Development Environment
437
+
438
+ ```bash
439
+ # Install development dependencies
440
+ uv sync --all-extras
441
+
442
+ # Run tests
443
+ pytest
444
+
445
+ # Run tests with coverage
446
+ pytest --cov=cyvest --cov-report=html
447
+
448
+ # Format code
449
+ ruff format .
450
+
451
+ # Lint code
452
+ ruff check .
453
+ ```
454
+
455
+ ### Running Tests
456
+
457
+ ```bash
458
+ # Run all tests
459
+ pytest
460
+
461
+ # Run specific test file
462
+ pytest tests/test_score.py
463
+
464
+ # Run with verbose output
465
+ pytest -v
466
+
467
+ # Run with coverage
468
+ pytest --cov=cyvest
469
+ ```
470
+
471
+ ## Documentation
472
+
473
+ Build the documentation with MkDocs:
474
+
475
+ ```bash
476
+ # Install docs dependencies
477
+ uv sync --all-extras
478
+
479
+ # Serve documentation locally
480
+ mkdocs serve
481
+
482
+ # Build documentation
483
+ mkdocs build
484
+ ```
485
+
486
+ ## JavaScript packages
487
+
488
+ The repo includes a PNPM workspace under `js/` with three packages:
489
+
490
+ - `@cyvest/cyvest-js`: TypeScript types, schema validation, and helpers for Cyvest investigations.
491
+ - `@cyvest/cyvest-vis`: React components for graph visualization (depends on `@cyvest/cyvest-js`).
492
+ - `@cyvest/cyvest-app`: Vite demo that bundles the JS packages with sample investigations.
493
+
494
+ The JS packages track the generated schema; serialized investigations should include fields like
495
+ `investigation_id`, `investigation_name`, `audit_log`, `score_display`, `check_links`, and
496
+ `observable_links`. The investigation start time is recorded as an `INVESTIGATION_STARTED` event
497
+ in the `audit_log`.
498
+
499
+ See `docs/js-packages.md` for workspace commands and usage snippets.
500
+
501
+ ## Contributing
502
+
503
+ Contributions are welcome! Please:
504
+
505
+ 1. Fork the repository
506
+ 2. Create a feature branch
507
+ 3. Make your changes with tests
508
+ 4. Run the test suite
509
+ 5. Submit a pull request
510
+
511
+ ## License
512
+
513
+ This project is licensed under the MIT License - see the LICENSE file for details.
514
+
515
+ ## Use Cases
516
+
517
+ Cyvest is designed for:
518
+
519
+ - **Security Operations Centers (SOCs)**: Automate investigation workflows
520
+ - **Incident Response**: Structure and document incident investigations
521
+ - **Threat Hunting**: Build repeatable hunting methodologies
522
+ - **Malware Analysis**: Track relationships between artifacts
523
+ - **Phishing Analysis**: Analyze emails and linked resources
524
+ - **Integration**: Combine results from multiple security tools
525
+
526
+ ## Architecture Highlights
527
+
528
+ - **Concurrency**: Advanced `SharedInvestigationContext` (via `cyvest.shared`) enables safe parallel task execution
529
+ - **Deterministic Keys**: Same objects always generate same keys for merging
530
+ - **Deterministic IDs**: Optional `investigation_id` parameter for reproducible reports and diffing
531
+ - **Score Propagation**: Automatic hierarchical score calculation
532
+ - **Flexible Export**: JSON for storage, Markdown for LLM analysis
533
+ - **Audit Trail**: Score change history for debugging
534
+
535
+ ## Future Enhancements
536
+
537
+ - Database persistence layer
538
+ - Additional export formats (PDF, HTML)
@@ -0,0 +1,23 @@
1
+ cyvest/__init__.py,sha256=ISyIqEKzFg5WvCKWyAa1k6B3-bSdC8mxhWXkIhuiIYA,1046
2
+ cyvest/cli.py,sha256=Zj8RVCG4BXFTNQt0Oo2GgwviF1aY5by_69eZE2XOgkg,12271
3
+ cyvest/cyvest.py,sha256=82DSGeog6YjEyZagkD5E2e5Vmxk7hSERfhOA_5YCF0I,45041
4
+ cyvest/investigation.py,sha256=LKiP3p74m5UnC1QnCZ5WEX6xtxIyrzI-chdd9nE8YLE,61074
5
+ cyvest/io_rich.py,sha256=46mPidDNzqo1hgcWxVrEFRNMdto_nEe8vaaxaOwGqFk,21932
6
+ cyvest/io_schema.py,sha256=vhreQyKQbVrXFVj1ddIoAuEXc6zGhTVndHZZdDxTd0c,1302
7
+ cyvest/io_serialization.py,sha256=V0KAxGZu11t7LA413L7QlJyh5o2lwytQCLllDflH6I4,18478
8
+ cyvest/io_visualization.py,sha256=kPT8cAqZJ3liSA2BzwvQXMaejnk496m0Ck11QXJCQLc,11010
9
+ cyvest/keys.py,sha256=9S7ELhloRzKrLPlpfRnwYtL7WXbKIsyhteh62mn3j2E,4614
10
+ cyvest/level_score_rules.py,sha256=MoCCYCLsTDCM-OqLV4Ln_GEH7H_tjNpK4tte89IEQeU,2391
11
+ cyvest/levels.py,sha256=1d3YHCbP2aptSGyIwVzLF4rJjAYdpSzmOnzjs7ZLsdg,4556
12
+ cyvest/model.py,sha256=Bi1IRXM2Ca47t5KpJ1vtSnPyeJhHZ1ZnezE4-JkSwv0,18184
13
+ cyvest/model_enums.py,sha256=t7uFDnGcLXLMZdwgHMxk3HU5KHQgqUvBNZ5i_H2_Jvc,2050
14
+ cyvest/model_schema.py,sha256=LVJ99i-_T6cSJ3N7OJO2F0k1LjpA23S-PKSk6RYzwks,6119
15
+ cyvest/proxies.py,sha256=s_vc4KPWvta7saNNR92WHgzn73V-BdOYhZoRJ_0evHw,19581
16
+ cyvest/score.py,sha256=CiD-dcjEVr2oLHPJ4MlcQWv5338Tq-Zn4Q7lXIEq9fY,19807
17
+ cyvest/shared.py,sha256=217ufZ-sBAMr0CMflBEpfeTSXs2eRlgNIul1fm0l_Qw,19505
18
+ cyvest/stats.py,sha256=mBHgLaRPs1R9l775_K3zQKN6ujup5sGzD5o5CQCPSK8,9926
19
+ cyvest/ulid.py,sha256=NA3k6hlgMZyfzLEMJEIMqt73CY2V07Bupqk7gemshDM,1069
20
+ cyvest-4.4.0.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
21
+ cyvest-4.4.0.dist-info/entry_points.txt,sha256=bwtGzs4Eh9i3WEhW4dhxHaYP8qf8qUN4sDnt4xXywUk,44
22
+ cyvest-4.4.0.dist-info/METADATA,sha256=8PLZvgm-LAEV_fxfDLupzWHcPa-_mqqCT2rcG0gzDps,18718
23
+ cyvest-4.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cyvest = cyvest.cli:main
3
+