Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ in an audit log.
| Scoring | Configurable score aggregation and level propagation across observable and Finding links |
| Composition | Deterministic merging, shared context for parallel tasks, and investigation comparison |
| Serialization | Versioned JSON schema, JSON/Markdown export, migration from v5, and generated TypeScript types |
| Tooling | CLI inspection, statistics, IOC extraction, Rich output, and optional graph visualization |
| Tooling | CLI inspection, statistics, IOC extraction, and Rich output |

Cyvest 6 uses a strict `schema_version: "6.0.0"`. Existing v5 integrations
should follow the [migration guide](docs/migration-v5-to-v6.md).
Expand All @@ -41,11 +41,8 @@ uv pip install -e .
pip install -e .
```

The optional visualization dependencies are available with:

```bash
pip install -e ".[visualization]"
```
Graph visualization is provided by the `@cyvest/cyvest-vis` React package, see
[docs/js-packages.md](docs/js-packages.md).

## Quick Start

Expand Down Expand Up @@ -538,15 +535,15 @@ See the `examples/` directory for complete examples:
- **02_urls_and_ips.py**: Network investigation with URLs and IPs
- **03_merge_demo.py**: Multi-process investigation merging
- **04_email.py**: Multi-threaded investigation with SharedInvestigationContext
- **05_visualization.py**: Interactive HTML visualization showcasing scores, levels, and relationship flows
- **05_graph_dataset.py**: Rich investigation exported as JSON for the `@cyvest/cyvest-vis` graph renderer
- **06_compare_investigations.py**: Compare investigations with tolerance rules and visual diff output

Run an example:

```bash
python examples/01_email_basic.py
python examples/04_email.py
python examples/05_visualization.py
python examples/05_graph_dataset.py
```

## CLI Usage
Expand All @@ -572,9 +569,6 @@ cyvest merge inv1.json inv2.json -o merged.json --stats
# Merge and display rich summary
cyvest merge inv1.json inv2.json -o merged.json -f rich --stats

# Generate an interactive visualization (requires visualization extra)
cyvest visualize investigation.json --min-level SUSPICIOUS --group-by-type

# Extract observables (IOCs) from text
echo "Check IP 192.168.1.1 and https://evil.com" | cyvest extract
cyvest extract report.txt -t url -t ip -o iocs.txt
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ When dependencies change upstream, re-run `uv sync --all-extras` (or `pip instal
## Optional Integrations

- `mkdocs` + `mkdocs-material` for local docs previews (`mkdocs serve`)
- `pyvis` via `pip install "cyvest[visualization]"` for the interactive network graph CLI
- `@cyvest/cyvest-vis` for interactive graph rendering, see [JS packages](../js-packages.md)
- Any asyncio, queue, or orchestration library—Cyvest stays synchronous but interoperates through shared context managers

---
Expand Down
12 changes: 3 additions & 9 deletions examples/04_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ def run(self, cy: Cyvest) -> None:
cy.enrichment_create("receiver", {"receiver": ["ok"]}, context="from splunk")
receiver = cy.observable(cy.OBS.EMAIL, "user@company.com")
cy.root().relate_to(receiver, cy.REL.EXTRACTION)
cy.finding("receiver", "description", "> receiver").with_score(0.1).link_observable(receiver).tagged(
"emails"
)
cy.finding("receiver", "description", "> receiver").with_score(0.1).link_observable(receiver).tagged("emails")

logger.info("Email receiver analysis complete")

Expand Down Expand Up @@ -364,9 +362,7 @@ def run(self, cy: Cyvest) -> None:

# Build file observable with hash observables
file_obs = cy.observable(cy.OBS.FILE, filename)
hash_obs = cy.observable(cy.OBS.HASH, f"MD5:{md5_hash}").with_ti(
"VT", score, "MD5 hash analysis"
)
hash_obs = cy.observable(cy.OBS.HASH, f"MD5:{md5_hash}").with_ti("VT", score, "MD5 hash analysis")
cy.root().relate_to(file_obs, cy.REL.EXTRACTION)
hash_obs.relate_to(file_obs, cy.REL.EXTRACTION, direction=cy.DIR.INBOUND)

Expand Down Expand Up @@ -492,7 +488,6 @@ def run(self, cy: Cyvest) -> None:
@click.command()
@click_logger_params
@click.option("-w", "--workers", type=int, default=1)
@click.option("--browser", "browser", is_flag=True, default=False)
@click.option("--stats", "stats", is_flag=True, default=False)
@click.option("--audit", "audit", is_flag=True, default=False)
@click.option(
Expand All @@ -503,7 +498,7 @@ def run(self, cy: Cyvest) -> None:
help="Exclude audit log from JSON output for deterministic output",
)
@click.option("-o", "--output", type=click.Path(dir_okay=False, path_type=Path), default=None)
def main(workers, browser, stats, audit, no_audit_log, output):
def main(workers, stats, audit, no_audit_log, output):
"""Main execution demonstrating multi-threaded investigation."""

# Prepare input data
Expand Down Expand Up @@ -561,7 +556,6 @@ def main(workers, browser, stats, audit, no_audit_log, output):
cy.display_summary(show_audit_log=audit)
if stats:
cy.display_statistics()
cy.display_network(open_browser=browser)

if output is not None:
logger.info("[bold cyan]Generating json...[/bold cyan]")
Expand Down
19 changes: 4 additions & 15 deletions examples/05_visualization.py → examples/05_graph_dataset.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
"""
Example: Network Visualization with Pyvis
Example: Graph dataset for the @cyvest/cyvest-vis renderer

This example demonstrates the network visualization feature that generates
an interactive HTML graph showing observables and their relationships.

The visualization uses the Rich color scheme:
- Nodes are colored by security level (red=malicious, yellow=suspicious, green=safe, etc.)
- Node sizes are based on scores (higher scores = larger nodes)
- Node shapes represent observable types (diamonds=domains, dots=IPs, boxes=URLs, etc.)
- Edges are colored by relationship direction (blue=outbound, pink=inbound, purple=bidirectional)
- Edge labels show the relationship type
Builds a rich investigation covering scores, levels, and relationship flows, then
exports it as JSON. Feed the resulting file to the `@cyvest/cyvest-vis` React
component to explore the graph interactively.
"""

import tempfile
Expand Down Expand Up @@ -169,11 +163,6 @@ def main(no_audit_log: bool = False, output: Path | None = None) -> None:
size_kb = Path(json_path).stat().st_size / 1024
logger.info("[green]✓ Full json saved to: %s (%.2f KB)[/green]", json_path, size_kb)

# Generate and open network visualization
logger.info("[bold cyan]Generating Network Visualization...[/bold cyan]")
html_path = cv.display_network(open_browser=False)
logger.info(f"[green]✓ Full html visualization saved to: {html_path}[/green]")

# Example: Export to markdown (with optional sections)
logger.info("[bold cyan]Exporting to Markdown...[/bold cyan]")
# Export with tags and enrichments included
Expand Down
2 changes: 1 addition & 1 deletion js/packages/cyvest-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cyvest/cyvest-app",
"version": "6.2.0",
"version": "7.0.0",
"private": true,
"scripts": {
"dev": "vite",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -916,4 +916,4 @@
"score_mode_obs": "max"
},
"score_display": "36.10"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -754,4 +754,4 @@
"score_mode_obs": "max"
},
"score_display": "37.50"
}
}
2 changes: 1 addition & 1 deletion js/packages/cyvest-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cyvest/cyvest-js",
"version": "6.2.0",
"version": "7.0.0",
"type": "module",
"files": [
"dist"
Expand Down
2 changes: 1 addition & 1 deletion js/packages/cyvest-vis/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cyvest/cyvest-vis",
"version": "6.2.0",
"version": "7.0.0",
"type": "module",
"files": [
"dist"
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cyvest"
version = "6.2.0"
version = "7.0.0"
description = "Cybersecurity investigation model"
readme = {file = "README.md", content-type = "text/markdown"}
requires-python = ">=3.10"
Expand Down Expand Up @@ -57,9 +57,6 @@ docs = [
"pymdown-extensions>=10.11",
]

[project.optional-dependencies]
visualization = ["pyvis>=0.3.2"]

[tool.ruff]
target-version = "py310"
line-length = 120
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
uv run cyvest schema -o ./schema/cyvest.schema.json
pnpm -C js/packages/cyvest-js run generate:types
uv run examples/04_email.py --no-audit-log -o ./js/packages/cyvest-app/src/investigations/cyvest_email.json
uv run examples/05_visualization.py --no-audit-log -o ./js/packages/cyvest-app/src/investigations/cyvest_visual.json
uv run examples/05_graph_dataset.py --no-audit-log -o ./js/packages/cyvest-app/src/investigations/cyvest_visual.json
pnpm -C js run build
pnpm -C js run test:ci
2 changes: 1 addition & 1 deletion src/cyvest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from cyvest.proxies import EnrichmentProxy, EvidenceProxy, FindingProxy, ObservableProxy, TagProxy, ThreatIntelProxy
from cyvest.resolvers import ObservableResolution, ObservableResolver

__version__ = "6.2.0"
__version__ = "7.0.0"

__all__ = [
# Core class
Expand Down
111 changes: 0 additions & 111 deletions src/cyvest/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from cyvest.io_rich import display_diff, display_finding_query, display_observable_query, display_threat_intel_query
from cyvest.io_schema import get_investigation_schema
from cyvest.io_serialization import load_investigation_json, migrate_v5_to_v6
from cyvest.io_visualization import VisualizationDependencyMissingError
from cyvest.keys import parse_key_type
from cyvest.model_enums import ObservableType

Expand Down Expand Up @@ -282,116 +281,6 @@ def migrate(input: Path, output: Path) -> None:
logger.info(f"[green]Migrated investigation written to: {output_path}[/green]")


@cli.command()
@click.argument("input", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--output-dir",
type=click.Path(file_okay=False, path_type=Path),
help="Directory to save HTML file (defaults to temporary directory).",
)
@click.option(
"--no-browser",
is_flag=True,
help="Do not automatically open the visualization in a browser.",
)
@click.option(
"--min-level",
type=click.Choice(["TRUSTED", "INFO", "SAFE", "NOTABLE", "SUSPICIOUS", "MALICIOUS"], case_sensitive=False),
help="Minimum security level to include in the visualization.",
)
@click.option(
"--types",
help="Comma-separated list of observable types to include (e.g., 'ipv4,domain,url').",
)
@click.option(
"--title",
default="Cyvest Investigation Network",
show_default=True,
help="Title for the network graph.",
)
@click.option(
"--physics",
is_flag=True,
help="Enable physics simulation for organic layout (default: static layout).",
)
@click.option(
"--group-by-type",
is_flag=True,
help="Group observables by type using hierarchical layout.",
)
def visualize(
input: Path,
output_dir: Path | None,
no_browser: bool,
min_level: str | None,
types: str | None,
title: str,
physics: bool,
group_by_type: bool,
) -> None:
"""
Generate an interactive network graph visualization of an investigation.

This command creates an HTML file with a pyvis network graph showing
observables as nodes (colored by level, sized by score, shaped by type)
and relationships as edges (colored by direction, labeled by type).

The visualization is saved to a temporary directory by default, or to
the specified output directory. The HTML file automatically opens in
your default browser unless --no-browser is specified.
"""
from cyvest.levels import Level
from cyvest.model_enums import ObservableType

cv = load_investigation_json(input)

# Parse min_level if provided
min_level_enum = None
if min_level is not None:
min_level_enum = Level[min_level.upper()]

# Parse observable types if provided
observable_types = None
if types is not None:
parsed_types: list[ObservableType] = []
for token in types.split(","):
token = token.strip()
if not token:
continue
try:
parsed_types.append(ObservableType(token.lower()))
except ValueError:
try:
parsed_types.append(ObservableType[token.upper()])
except KeyError as exc:
raise click.ClickException(f"Unknown observable type: {token}") from exc
observable_types = parsed_types or None

# Convert output_dir to string if provided
output_dir_str = str(output_dir.resolve()) if output_dir is not None else None

# Generate visualization
logger.info(f"[cyan]Generating network visualization for: {input}[/cyan]")

try:
html_path = cv.display_network(
output_dir=output_dir_str,
open_browser=not no_browser,
min_level=min_level_enum,
observable_types=observable_types,
title=title,
physics=physics,
group_by_type=group_by_type,
)
except VisualizationDependencyMissingError as exc:
raise click.ClickException(str(exc)) from exc

logger.info(f"[green]✓ Visualization saved to: {html_path}[/green]")

if not no_browser:
logger.info("[cyan]Opening visualization in browser...[/cyan]")


@cli.command()
@click.argument("actual", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.argument("expected", type=click.Path(exists=True, dir_okay=False, path_type=Path))
Expand Down
Loading
Loading