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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ mkdocs build
The repo includes a PNPM workspace under `js/` with three packages:

- `@cyvest/cyvest-js`: TypeScript types, schema validation, and helpers for Cyvest investigations.
- `@cyvest/cyvest-vis`: React components for graph visualization (depends on `@cyvest/cyvest-js`).
- `@cyvest/cyvest-vis`: React components for graph visualization (Cytoscape + ELK/Dagre, depends on `@cyvest/cyvest-js`).
- `@cyvest/cyvest-app`: Vite demo that bundles the JS packages with sample investigations.

The JS packages track the generated schema; serialized investigations should include fields like
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Cyvest (facade + fluent proxies)
## JavaScript Packages

- `@cyvest/cyvest-js`: TypeScript types, schema validation, and graph helpers for Cyvest investigations.
- `@cyvest/cyvest-vis`: React components (XYFlow + D3) to visualize investigations.
- `@cyvest/cyvest-vis`: React components (Cytoscape + ELK/Dagre) to visualize investigations.
- `@cyvest/cyvest-app`: Vite demo bundling the JS packages with sample investigations.

[See JavaScript packages guide](js-packages.md) for install and workspace commands.
Expand Down
19 changes: 10 additions & 9 deletions docs/js-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,32 @@ recorded as an `INVESTIGATION_STARTED` event in the `audit_log`.
## Packages

- **@cyvest/cyvest-js** — Generated types, schema validation, graph builders, tag hierarchy utilities (including aggregated score/level), and helper functions for Cyvest investigation JSON. Ships ESM/CJS builds and `.d.ts` files.
- **@cyvest/cyvest-vis** — React 19+ visualization components (powered by React Flow + D3) to visualize investigations with level-aware styling. Depends on `@cyvest/cyvest-js`.
- **@cyvest/cyvest-vis** — React 19+ visualization components (powered by Cytoscape) to visualize investigations with level-aware styling. Uses ELK for observables and Dagre for investigation hierarchy. Depends on `@cyvest/cyvest-js`.
- **@cyvest/cyvest-app** — Private Vite demo that bundles sample investigations and renders them via `CyvestGraph`. Useful for tweaking visuals and testing UI flows.

## @cyvest/cyvest-vis

Interactive graph visualization for Cyvest investigations.
Interactive graph visualization for Cyvest investigations with a clean v2 API.

### Features

- **Observables Graph**: Force-directed layout showing all observables and relationships
- **Investigation Graph**: Hierarchical Dagre layout showing root → tags → checks
- **Observables Graph**: ELK `stress` layout showing all observables and relationships
- **Investigation Graph**: Dagre `LR` layout showing root → tags → checks
- **Professional icons**: SVG icons for all observable types (IPs, domains, emails, files, etc.)
- **Interactive controls**: Drag nodes, adjust force parameters, zoom/pan
- **Interactive controls**: Pan/zoom, fit, and re-run layout
- **Level-aware colors**: Nodes styled by security level (SAFE → MALICIOUS)

### Quick Start

```tsx
import { CyvestGraph } from "@cyvest/cyvest-vis";
import "@cyvest/cyvest-vis/styles.css";

<CyvestGraph
investigation={investigation}
height={600}
showViewToggle
onNodeClick={(id) => console.log(id)}
onNodeSelect={(event) => console.log(event.nodeId, event.label)}
/>
```

Expand All @@ -43,10 +44,10 @@ import { CyvestGraph } from "@cyvest/cyvest-vis";
| Component | Description |
|-----------|-------------|
| `CyvestGraph` | Combined view with toggle between Observables and Investigation |
| `ObservablesGraph` | Force-directed graph of observables and relationships |
| `InvestigationGraph` | Hierarchical graph of root, checks, and tags |
| `CyvestObservablesView` | ELK-based graph of observables and relationships |
| `CyvestInvestigationView` | Dagre-based graph of root, checks, and tags |

See `js/packages/cyvest-vis/src/components` for advanced hooks and utilities.
See `js/packages/cyvest-vis/README.md` for full v2 API and theming details.

## Workspace commands

Expand Down
18 changes: 8 additions & 10 deletions js/packages/cyvest-app/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# @cyvest/cyvest-app

Vite-based demo app that ships with sample Cyvest investigations and renders them with `@cyvest/cyvest-vis`.
Vite demo application for the `@cyvest/cyvest-vis` Cytoscape visualization library (ELK for observables, Dagre for investigation view).

## What it does

- Loads bundled investigations (`src/investigations/*.json`) and validates them with `@cyvest/cyvest-js`.
- Visualizes the graph and levels via the `CyvestGraph` component.
- Serves as a quick playground for design tweaks to the visualization layer.
- Loads bundled investigations (`src/investigations/*.json`) and validates them with `@cyvest/cyvest-js`
- Renders both observables and investigation views with `CyvestGraph`
- Demonstrates node selection events and basic layout customization

## Run locally

```bash
pnpm install # from repo root
pnpm install
pnpm --filter @cyvest/cyvest-app dev
```

Expand All @@ -22,9 +22,7 @@ pnpm --filter @cyvest/cyvest-app build
pnpm --filter @cyvest/cyvest-app preview
```

## Customize the demo
## Notes

- Drop new investigations under `src/investigations/` and register them in `src/api.ts`.
- Adjust layout/controls in `src/App.tsx` to try new `CyvestGraph` props or styling.

Note: The app is marked `private` and intended for demos and development, not publishing.
- The app imports `@cyvest/cyvest-vis/styles.css` for default visual styling.
- The package is private and intended as a development/demo surface.
92 changes: 67 additions & 25 deletions js/packages/cyvest-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import type { CyvestInvestigation } from "@cyvest/cyvest-js";
import { getStartedAt } from "@cyvest/cyvest-js";
import { CyvestGraph } from "@cyvest/cyvest-vis";
import { CyvestGraph, type CyNodeSelectEvent } from "@cyvest/cyvest-vis";
import React, { useEffect, useState } from "react";
import { loadInvestigation, INVESTIGATIONS, type InvestigationKey } from "./api";

export const App: React.FC = () => {
const [investigation, setInvestigation] =
useState<CyvestInvestigation | null>(null);
const [selectedKey, setSelectedKey] = useState<InvestigationKey>("cyvest_visual");
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [selectedKey, setSelectedKey] =
useState<InvestigationKey>("cyvest_visual");
const [selectedNode, setSelectedNode] = useState<CyNodeSelectEvent | null>(
null
);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
setLoading(true);
setError(null);
setSelectedNodeId(null);
setSelectedNode(null);
loadInvestigation(selectedKey)
.then(setInvestigation)
.catch((err) => {
Expand All @@ -25,27 +28,43 @@ export const App: React.FC = () => {
.finally(() => setLoading(false));
}, [selectedKey]);

if (error) return <div style={{ padding: 16, color: "red" }}>{error}</div>;
if (error) {
return <div style={{ padding: 16, color: "#b91c1c" }}>{error}</div>;
}

return (
<div style={{
height: "100%",
display: "flex",
flexDirection: "column",
fontFamily: "system-ui, sans-serif"
}}>
<div style={{ display: "flex", alignItems: "center", gap: 16, padding: 16 }}>
<h1 style={{ margin: 0 }}>Cyvest Demo</h1>
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
fontFamily:
"'IBM Plex Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif",
background: "#eff3f8",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
padding: 16,
}}
>
<h1 style={{ margin: 0, color: "#0f172a" }}>Cyvest Demo</h1>
<select
value={selectedKey}
onChange={(e) => setSelectedKey(e.target.value as InvestigationKey)}
onChange={(event) =>
setSelectedKey(event.target.value as InvestigationKey)
}
style={{
padding: "8px 12px",
fontSize: 14,
borderRadius: 6,
border: "1px solid #d1d5db",
background: "white",
borderRadius: 8,
border: "1px solid #cfd7e4",
background: "#ffffff",
cursor: "pointer",
color: "#12213a",
}}
>
{Object.entries(INVESTIGATIONS).map(([key, { name }]) => (
Expand All @@ -57,32 +76,55 @@ export const App: React.FC = () => {
</div>

{loading ? (
<div style={{ padding: 16 }}>Loading…</div>
<div style={{ padding: 16, color: "#334155" }}>Loading…</div>
) : investigation ? (
<>
<div style={{ padding: "0 16px 8px" }}>
<h2 style={{ margin: 0 }}>
<h2 style={{ margin: 0, color: "#0f172a" }}>
{investigation.investigation_name ?? investigation.investigation_id}
</h2>
<div style={{ color: "#6b7280", fontSize: 13, marginTop: 4 }}>
<div style={{ color: "#5a667f", fontSize: 13, marginTop: 4 }}>
<span>Score {investigation.score_display}</span>
<span> | Level {investigation.level}</span>
<span> | Started {getStartedAt(investigation) ?? "N/A"}</span>
</div>
</div>

<div style={{ flex: 1, minHeight: 0 }}>
<div style={{ flex: 1, minHeight: 0, padding: "0 16px 0" }}>
<CyvestGraph
investigation={investigation}
height="100%"
onNodeClick={setSelectedNodeId}
onNodeSelect={setSelectedNode}
showViewToggle={true}
showToolbar={true}
observablesLayout={{
algorithm: "stress",
spacingNodeNode: 72,
}}
investigationLayout={{
algorithm: "layered",
direction: "RIGHT",
}}
/>
</div>

{selectedNodeId && (
<div style={{ padding: 12, background: "#f3f4f6", borderRadius: 8, margin: 16 }}>
<strong>Selected node:</strong> {selectedNodeId}
{selectedNode && (
<div
style={{
margin: "12px 16px 16px",
padding: 12,
borderRadius: 10,
border: "1px solid #cfd7e4",
background: "#ffffff",
color: "#172033",
fontSize: 13,
}}
>
<strong>Selected node:</strong> {selectedNode.label}
<span style={{ color: "#5a667f" }}>
{" "}
({selectedNode.nodeType}, {selectedNode.nodeId})
</span>
</div>
)}
</>
Expand Down
5 changes: 4 additions & 1 deletion js/packages/cyvest-app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import "@cyvest/cyvest-vis/styles.css";
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App";

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Missing #root");
if (!rootElement) {
throw new Error("Missing #root");
}

ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
Expand Down
11 changes: 11 additions & 0 deletions js/packages/cyvest-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { resolve } from "node:path";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@cyvest/cyvest-vis/styles.css": resolve(
__dirname,
"../cyvest-vis/src/styles.css"
),
"@cyvest/cyvest-vis": resolve(__dirname, "../cyvest-vis/src/index.ts"),
"@cyvest/cyvest-js": resolve(__dirname, "../cyvest-js/src/index.ts"),
},
},
server: {
port: 5173,
},
Expand Down
Loading