@cyvest/cyvest-vis 2.0.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,1293 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CyvestGraph: () => CyvestGraph
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/components/CyvestGraph.tsx
38
+ var import_react14 = require("react");
39
+
40
+ // src/components/ObservablesGraph.tsx
41
+ var import_react7 = __toESM(require("react"));
42
+ var import_react8 = require("@xyflow/react");
43
+ var import_style = require("@xyflow/react/dist/style.css");
44
+ var import_cyvest_js = require("@cyvest/cyvest-js");
45
+
46
+ // src/types.ts
47
+ var DEFAULT_FORCE_CONFIG = {
48
+ chargeStrength: -200,
49
+ linkDistance: 80,
50
+ centerStrength: 0.05,
51
+ collisionRadius: 40,
52
+ iterations: 300
53
+ };
54
+
55
+ // src/components/ObservableNode.tsx
56
+ var import_react = require("react");
57
+ var import_react2 = require("@xyflow/react");
58
+
59
+ // src/utils/observables.ts
60
+ var OBSERVABLE_EMOJI_MAP = {
61
+ // Network
62
+ "ipv4-addr": "\u{1F310}",
63
+ "ipv6-addr": "\u{1F310}",
64
+ "domain-name": "\u{1F3E0}",
65
+ url: "\u{1F517}",
66
+ "autonomous-system": "\u{1F30D}",
67
+ "mac-addr": "\u{1F4F6}",
68
+ // Email
69
+ "email-addr": "\u{1F4E7}",
70
+ "email-message": "\u2709\uFE0F",
71
+ // File
72
+ file: "\u{1F4C4}",
73
+ "file-hash": "\u{1F510}",
74
+ "file:hash:md5": "\u{1F510}",
75
+ "file:hash:sha1": "\u{1F510}",
76
+ "file:hash:sha256": "\u{1F510}",
77
+ // User/Identity
78
+ user: "\u{1F464}",
79
+ "user-account": "\u{1F464}",
80
+ identity: "\u{1FAAA}",
81
+ // Process/System
82
+ process: "\u2699\uFE0F",
83
+ software: "\u{1F4BF}",
84
+ "windows-registry-key": "\u{1F4DD}",
85
+ // Threat Intelligence
86
+ "threat-actor": "\u{1F479}",
87
+ malware: "\u{1F9A0}",
88
+ "attack-pattern": "\u2694\uFE0F",
89
+ campaign: "\u{1F3AF}",
90
+ indicator: "\u{1F6A8}",
91
+ // Artifacts
92
+ artifact: "\u{1F9EA}",
93
+ certificate: "\u{1F4DC}",
94
+ "x509-certificate": "\u{1F4DC}",
95
+ // Default
96
+ unknown: "\u2753"
97
+ };
98
+ function getObservableEmoji(observableType) {
99
+ const normalized = observableType.toLowerCase().trim();
100
+ return OBSERVABLE_EMOJI_MAP[normalized] ?? OBSERVABLE_EMOJI_MAP.unknown;
101
+ }
102
+ var OBSERVABLE_SHAPE_MAP = {
103
+ // Domains get squares
104
+ "domain-name": "square",
105
+ // URLs get circles
106
+ url: "circle",
107
+ // IPs get triangles
108
+ "ipv4-addr": "triangle",
109
+ "ipv6-addr": "triangle",
110
+ // Root/files get rectangles (default for root)
111
+ file: "rectangle",
112
+ "email-message": "rectangle"
113
+ };
114
+ function getObservableShape(observableType, isRoot) {
115
+ if (isRoot) {
116
+ return "rectangle";
117
+ }
118
+ const normalized = observableType.toLowerCase().trim();
119
+ return OBSERVABLE_SHAPE_MAP[normalized] ?? "circle";
120
+ }
121
+ function truncateLabel(value, maxLength = 20, truncateMiddle = true) {
122
+ if (value.length <= maxLength) {
123
+ return value;
124
+ }
125
+ if (truncateMiddle) {
126
+ const halfLen = Math.floor((maxLength - 3) / 2);
127
+ return `${value.slice(0, halfLen)}\u2026${value.slice(-halfLen)}`;
128
+ }
129
+ return `${value.slice(0, maxLength - 1)}\u2026`;
130
+ }
131
+ function getLevelColor(level) {
132
+ const colors = {
133
+ NONE: "#6b7280",
134
+ // gray-500
135
+ TRUSTED: "#22c55e",
136
+ // green-500
137
+ INFO: "#3b82f6",
138
+ // blue-500
139
+ SAFE: "#22c55e",
140
+ // green-500
141
+ NOTABLE: "#eab308",
142
+ // yellow-500
143
+ SUSPICIOUS: "#f97316",
144
+ // orange-500
145
+ MALICIOUS: "#ef4444"
146
+ // red-500
147
+ };
148
+ return colors[level] ?? colors.NONE;
149
+ }
150
+ function getLevelBackgroundColor(level) {
151
+ const colors = {
152
+ NONE: "#f3f4f6",
153
+ // gray-100
154
+ TRUSTED: "#dcfce7",
155
+ // green-100
156
+ INFO: "#dbeafe",
157
+ // blue-100
158
+ SAFE: "#dcfce7",
159
+ // green-100
160
+ NOTABLE: "#fef9c3",
161
+ // yellow-100
162
+ SUSPICIOUS: "#ffedd5",
163
+ // orange-100
164
+ MALICIOUS: "#fee2e2"
165
+ // red-100
166
+ };
167
+ return colors[level] ?? colors.NONE;
168
+ }
169
+ var INVESTIGATION_NODE_EMOJI = {
170
+ root: "\u{1F3AF}",
171
+ check: "\u2705",
172
+ container: "\u{1F4E6}"
173
+ };
174
+ function getInvestigationNodeEmoji(nodeType) {
175
+ return INVESTIGATION_NODE_EMOJI[nodeType] ?? "\u2753";
176
+ }
177
+
178
+ // src/components/ObservableNode.tsx
179
+ var import_jsx_runtime = (
180
+ // Triangle using SVG
181
+ require("react/jsx-runtime")
182
+ );
183
+ var NODE_SIZE = 28;
184
+ var ROOT_NODE_SIZE = 36;
185
+ function ObservableNodeComponent({
186
+ data,
187
+ selected
188
+ }) {
189
+ const nodeData = data;
190
+ const {
191
+ label,
192
+ emoji,
193
+ shape,
194
+ level,
195
+ isRoot,
196
+ whitelisted,
197
+ fullValue
198
+ } = nodeData;
199
+ const size = isRoot ? ROOT_NODE_SIZE : NODE_SIZE;
200
+ const borderColor = getLevelColor(level);
201
+ const backgroundColor = getLevelBackgroundColor(level);
202
+ const getShapeStyle = () => {
203
+ const baseStyle = {
204
+ width: size,
205
+ height: size,
206
+ display: "flex",
207
+ alignItems: "center",
208
+ justifyContent: "center",
209
+ backgroundColor,
210
+ border: `${selected ? 3 : 2}px solid ${borderColor}`,
211
+ opacity: whitelisted ? 0.5 : 1,
212
+ fontSize: isRoot ? 14 : 12
213
+ };
214
+ switch (shape) {
215
+ case "square":
216
+ return { ...baseStyle, borderRadius: 4 };
217
+ case "circle":
218
+ return { ...baseStyle, borderRadius: "50%" };
219
+ case "triangle":
220
+ return {
221
+ ...baseStyle,
222
+ borderRadius: 0,
223
+ border: "none",
224
+ background: `linear-gradient(to bottom right, ${backgroundColor} 50%, transparent 50%)`,
225
+ clipPath: "polygon(50% 0%, 100% 100%, 0% 100%)",
226
+ position: "relative"
227
+ };
228
+ case "rectangle":
229
+ default:
230
+ return { ...baseStyle, width: size * 1.4, borderRadius: 6 };
231
+ }
232
+ };
233
+ const isTriangle = shape === "triangle";
234
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
235
+ "div",
236
+ {
237
+ className: "observable-node",
238
+ style: {
239
+ display: "flex",
240
+ flexDirection: "column",
241
+ alignItems: "center",
242
+ cursor: "pointer"
243
+ },
244
+ children: [
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { position: "relative" }, children: [
246
+ isTriangle ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: size, height: size, viewBox: "0 0 100 100", children: [
247
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
248
+ "polygon",
249
+ {
250
+ points: "50,10 90,90 10,90",
251
+ fill: backgroundColor,
252
+ stroke: borderColor,
253
+ strokeWidth: selected ? 6 : 4,
254
+ opacity: whitelisted ? 0.5 : 1
255
+ }
256
+ ),
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
258
+ "text",
259
+ {
260
+ x: "50",
261
+ y: "65",
262
+ textAnchor: "middle",
263
+ fontSize: "32",
264
+ dominantBaseline: "middle",
265
+ children: emoji
266
+ }
267
+ )
268
+ ] }) : (
269
+ // Other shapes using CSS
270
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: getShapeStyle(), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { userSelect: "none" }, children: emoji }) })
271
+ ),
272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
273
+ import_react2.Handle,
274
+ {
275
+ type: "source",
276
+ position: import_react2.Position.Right,
277
+ id: "source",
278
+ style: {
279
+ position: "absolute",
280
+ top: "50%",
281
+ left: "50%",
282
+ transform: "translate(-50%, -50%)",
283
+ width: 1,
284
+ height: 1,
285
+ background: "transparent",
286
+ border: "none",
287
+ opacity: 0
288
+ }
289
+ }
290
+ ),
291
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
292
+ import_react2.Handle,
293
+ {
294
+ type: "target",
295
+ position: import_react2.Position.Left,
296
+ id: "target",
297
+ style: {
298
+ position: "absolute",
299
+ top: "50%",
300
+ left: "50%",
301
+ transform: "translate(-50%, -50%)",
302
+ width: 1,
303
+ height: 1,
304
+ background: "transparent",
305
+ border: "none",
306
+ opacity: 0
307
+ }
308
+ }
309
+ )
310
+ ] }),
311
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
312
+ "div",
313
+ {
314
+ style: {
315
+ marginTop: 2,
316
+ fontSize: 9,
317
+ maxWidth: 70,
318
+ textAlign: "center",
319
+ overflow: "hidden",
320
+ textOverflow: "ellipsis",
321
+ whiteSpace: "nowrap",
322
+ color: "#374151",
323
+ fontFamily: "system-ui, sans-serif"
324
+ },
325
+ title: fullValue,
326
+ children: label
327
+ }
328
+ )
329
+ ]
330
+ }
331
+ );
332
+ }
333
+ var ObservableNode = (0, import_react.memo)(ObservableNodeComponent);
334
+
335
+ // src/components/FloatingEdge.tsx
336
+ var import_react3 = require("react");
337
+ var import_react4 = require("@xyflow/react");
338
+ var import_jsx_runtime2 = require("react/jsx-runtime");
339
+ function FloatingEdgeComponent({
340
+ id,
341
+ sourceX,
342
+ sourceY,
343
+ targetX,
344
+ targetY,
345
+ style,
346
+ markerEnd
347
+ }) {
348
+ const [edgePath] = (0, import_react4.getStraightPath)({
349
+ sourceX,
350
+ sourceY,
351
+ targetX,
352
+ targetY
353
+ });
354
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
355
+ import_react4.BaseEdge,
356
+ {
357
+ id,
358
+ path: edgePath,
359
+ style: {
360
+ strokeWidth: 1.5,
361
+ stroke: "#94a3b8",
362
+ ...style
363
+ },
364
+ markerEnd
365
+ }
366
+ );
367
+ }
368
+ var FloatingEdge = (0, import_react3.memo)(FloatingEdgeComponent);
369
+
370
+ // src/hooks/useForceLayout.ts
371
+ var import_react5 = require("react");
372
+ var import_d3_force = require("d3-force");
373
+ var import_react6 = require("@xyflow/react");
374
+ var nodeCountSelector = (state) => state.nodeLookup.size;
375
+ function useForceLayout(config = {}, rootNodeId) {
376
+ const { getNodes, getEdges, setNodes } = (0, import_react6.useReactFlow)();
377
+ const nodesInitialized = (0, import_react6.useNodesInitialized)();
378
+ const nodeCount = (0, import_react6.useStore)(nodeCountSelector);
379
+ const forceConfig = (0, import_react5.useMemo)(
380
+ () => ({ ...DEFAULT_FORCE_CONFIG, ...config }),
381
+ [config]
382
+ );
383
+ const simulationRef = (0, import_react5.useRef)(null);
384
+ const draggingNodeRef = (0, import_react5.useRef)(null);
385
+ (0, import_react5.useEffect)(() => {
386
+ if (!nodesInitialized || nodeCount === 0) {
387
+ return;
388
+ }
389
+ const nodes = getNodes();
390
+ const edges = getEdges();
391
+ const simNodes = nodes.map((node) => {
392
+ const existingNode = simulationRef.current?.nodes().find((n) => n.id === node.id);
393
+ return {
394
+ id: node.id,
395
+ // Use existing simulation position or node position
396
+ x: existingNode?.x ?? node.position.x ?? Math.random() * 500 - 250,
397
+ y: existingNode?.y ?? node.position.y ?? Math.random() * 500 - 250,
398
+ // Preserve fixed positions for dragged nodes
399
+ fx: existingNode?.fx ?? null,
400
+ fy: existingNode?.fy ?? null
401
+ };
402
+ });
403
+ if (rootNodeId) {
404
+ const rootNode = simNodes.find((n) => n.id === rootNodeId);
405
+ if (rootNode) {
406
+ rootNode.x = 0;
407
+ rootNode.y = 0;
408
+ rootNode.fx = 0;
409
+ rootNode.fy = 0;
410
+ }
411
+ }
412
+ const simLinks = edges.map((edge) => ({
413
+ source: edge.source,
414
+ target: edge.target
415
+ }));
416
+ if (simulationRef.current) {
417
+ simulationRef.current.stop();
418
+ }
419
+ const simulation = (0, import_d3_force.forceSimulation)(simNodes).force(
420
+ "link",
421
+ (0, import_d3_force.forceLink)(simLinks).id((d) => d.id).distance(forceConfig.linkDistance).strength(0.5)
422
+ ).force(
423
+ "charge",
424
+ (0, import_d3_force.forceManyBody)().strength(forceConfig.chargeStrength)
425
+ ).force(
426
+ "center",
427
+ (0, import_d3_force.forceCenter)(0, 0).strength(forceConfig.centerStrength)
428
+ ).force(
429
+ "collision",
430
+ (0, import_d3_force.forceCollide)(forceConfig.collisionRadius)
431
+ ).force(
432
+ "x",
433
+ (0, import_d3_force.forceX)(0).strength(0.01)
434
+ ).force(
435
+ "y",
436
+ (0, import_d3_force.forceY)(0).strength(0.01)
437
+ ).alphaDecay(0.02).velocityDecay(0.4);
438
+ simulation.on("tick", () => {
439
+ setNodes(
440
+ (currentNodes) => currentNodes.map((node) => {
441
+ const simNode = simulation.nodes().find((n) => n.id === node.id);
442
+ if (!simNode) return node;
443
+ return {
444
+ ...node,
445
+ position: {
446
+ x: simNode.x,
447
+ y: simNode.y
448
+ }
449
+ };
450
+ })
451
+ );
452
+ });
453
+ simulationRef.current = simulation;
454
+ return () => {
455
+ simulation.stop();
456
+ };
457
+ }, [
458
+ nodesInitialized,
459
+ nodeCount,
460
+ getNodes,
461
+ getEdges,
462
+ setNodes,
463
+ forceConfig,
464
+ rootNodeId
465
+ ]);
466
+ const onNodeDragStart = (0, import_react5.useCallback)(
467
+ (_, node) => {
468
+ const simulation = simulationRef.current;
469
+ if (!simulation) return;
470
+ draggingNodeRef.current = node.id;
471
+ simulation.alphaTarget(0.3).restart();
472
+ const simNode = simulation.nodes().find((n) => n.id === node.id);
473
+ if (simNode) {
474
+ simNode.fx = simNode.x;
475
+ simNode.fy = simNode.y;
476
+ }
477
+ },
478
+ []
479
+ );
480
+ const onNodeDrag = (0, import_react5.useCallback)(
481
+ (_, node) => {
482
+ const simulation = simulationRef.current;
483
+ if (!simulation) return;
484
+ const simNode = simulation.nodes().find((n) => n.id === node.id);
485
+ if (simNode) {
486
+ simNode.fx = node.position.x;
487
+ simNode.fy = node.position.y;
488
+ }
489
+ },
490
+ []
491
+ );
492
+ const onNodeDragStop = (0, import_react5.useCallback)(
493
+ (_, node) => {
494
+ const simulation = simulationRef.current;
495
+ if (!simulation) return;
496
+ draggingNodeRef.current = null;
497
+ simulation.alphaTarget(0);
498
+ if (node.id !== rootNodeId) {
499
+ const simNode = simulation.nodes().find((n) => n.id === node.id);
500
+ if (simNode) {
501
+ simNode.fx = null;
502
+ simNode.fy = null;
503
+ }
504
+ }
505
+ },
506
+ [rootNodeId]
507
+ );
508
+ const updateForceConfig = (0, import_react5.useCallback)(
509
+ (updates) => {
510
+ const simulation = simulationRef.current;
511
+ if (!simulation) return;
512
+ if (updates.chargeStrength !== void 0) {
513
+ simulation.force(
514
+ "charge",
515
+ (0, import_d3_force.forceManyBody)().strength(updates.chargeStrength)
516
+ );
517
+ }
518
+ if (updates.linkDistance !== void 0) {
519
+ const linkForce = simulation.force("link");
520
+ if (linkForce) {
521
+ linkForce.distance(updates.linkDistance);
522
+ }
523
+ }
524
+ if (updates.collisionRadius !== void 0) {
525
+ simulation.force(
526
+ "collision",
527
+ (0, import_d3_force.forceCollide)(updates.collisionRadius)
528
+ );
529
+ }
530
+ simulation.alpha(0.5).restart();
531
+ },
532
+ []
533
+ );
534
+ const restartSimulation = (0, import_react5.useCallback)(() => {
535
+ const simulation = simulationRef.current;
536
+ if (!simulation) return;
537
+ simulation.alpha(1).restart();
538
+ }, []);
539
+ return {
540
+ onNodeDragStart,
541
+ onNodeDrag,
542
+ onNodeDragStop,
543
+ updateForceConfig,
544
+ restartSimulation
545
+ };
546
+ }
547
+
548
+ // src/components/ObservablesGraph.tsx
549
+ var import_jsx_runtime3 = require("react/jsx-runtime");
550
+ var nodeTypes = {
551
+ observable: ObservableNode
552
+ };
553
+ var edgeTypes = {
554
+ floating: FloatingEdge
555
+ };
556
+ function createObservableNodes(investigation, rootObservableIds) {
557
+ const graph = (0, import_cyvest_js.getObservableGraph)(investigation);
558
+ return graph.nodes.map((graphNode, index) => {
559
+ const isRoot = rootObservableIds.has(graphNode.id);
560
+ const shape = getObservableShape(graphNode.type, isRoot);
561
+ const nodeData = {
562
+ label: truncateLabel(graphNode.value, 18),
563
+ fullValue: graphNode.value,
564
+ observableType: graphNode.type,
565
+ level: graphNode.level,
566
+ score: graphNode.score,
567
+ emoji: getObservableEmoji(graphNode.type),
568
+ shape,
569
+ isRoot,
570
+ whitelisted: graphNode.whitelisted,
571
+ internal: graphNode.internal
572
+ };
573
+ const angle = index / graph.nodes.length * 2 * Math.PI;
574
+ const radius = isRoot ? 0 : 150;
575
+ return {
576
+ id: graphNode.id,
577
+ type: "observable",
578
+ position: {
579
+ x: Math.cos(angle) * radius,
580
+ y: Math.sin(angle) * radius
581
+ },
582
+ data: nodeData
583
+ };
584
+ });
585
+ }
586
+ function createObservableEdges(investigation) {
587
+ const graph = (0, import_cyvest_js.getObservableGraph)(investigation);
588
+ return graph.edges.map((graphEdge, index) => {
589
+ const edgeData = {
590
+ relationshipType: graphEdge.type,
591
+ bidirectional: graphEdge.direction === "bidirectional"
592
+ };
593
+ return {
594
+ id: `edge-${graphEdge.source}-${graphEdge.target}-${index}`,
595
+ source: graphEdge.source,
596
+ target: graphEdge.target,
597
+ type: "floating",
598
+ data: edgeData,
599
+ style: { stroke: "#94a3b8", strokeWidth: 1.5 }
600
+ };
601
+ });
602
+ }
603
+ var ForceControls = ({ config, onChange, onRestart }) => {
604
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
605
+ "div",
606
+ {
607
+ style: {
608
+ position: "absolute",
609
+ top: 10,
610
+ right: 10,
611
+ background: "white",
612
+ padding: 12,
613
+ borderRadius: 8,
614
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
615
+ fontSize: 12,
616
+ fontFamily: "system-ui, sans-serif",
617
+ zIndex: 10,
618
+ minWidth: 160
619
+ },
620
+ children: [
621
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontWeight: 600, marginBottom: 8 }, children: "Force Layout" }),
622
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: 8 }, children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: { display: "block", marginBottom: 2 }, children: [
624
+ "Repulsion: ",
625
+ config.chargeStrength
626
+ ] }),
627
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
628
+ "input",
629
+ {
630
+ type: "range",
631
+ min: "-500",
632
+ max: "-50",
633
+ value: config.chargeStrength,
634
+ onChange: (e) => onChange({ chargeStrength: Number(e.target.value) }),
635
+ style: { width: "100%" }
636
+ }
637
+ )
638
+ ] }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: 8 }, children: [
640
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: { display: "block", marginBottom: 2 }, children: [
641
+ "Link Distance: ",
642
+ config.linkDistance
643
+ ] }),
644
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
645
+ "input",
646
+ {
647
+ type: "range",
648
+ min: "30",
649
+ max: "200",
650
+ value: config.linkDistance,
651
+ onChange: (e) => onChange({ linkDistance: Number(e.target.value) }),
652
+ style: { width: "100%" }
653
+ }
654
+ )
655
+ ] }),
656
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: 8 }, children: [
657
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: { display: "block", marginBottom: 2 }, children: [
658
+ "Collision: ",
659
+ config.collisionRadius
660
+ ] }),
661
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
662
+ "input",
663
+ {
664
+ type: "range",
665
+ min: "10",
666
+ max: "80",
667
+ value: config.collisionRadius,
668
+ onChange: (e) => onChange({ collisionRadius: Number(e.target.value) }),
669
+ style: { width: "100%" }
670
+ }
671
+ )
672
+ ] }),
673
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
674
+ "button",
675
+ {
676
+ onClick: onRestart,
677
+ style: {
678
+ width: "100%",
679
+ padding: "6px 12px",
680
+ border: "none",
681
+ borderRadius: 4,
682
+ background: "#3b82f6",
683
+ color: "white",
684
+ cursor: "pointer",
685
+ fontSize: 12
686
+ },
687
+ children: "Restart Simulation"
688
+ }
689
+ )
690
+ ]
691
+ }
692
+ );
693
+ };
694
+ var ObservablesGraphInner = ({
695
+ initialNodes,
696
+ initialEdges,
697
+ primaryRootId,
698
+ height,
699
+ width,
700
+ forceConfig: initialForceConfig = {},
701
+ onNodeClick,
702
+ onNodeDoubleClick,
703
+ className,
704
+ showControls = true
705
+ }) => {
706
+ const [forceConfig, setForceConfig] = (0, import_react7.useState)({
707
+ ...DEFAULT_FORCE_CONFIG,
708
+ ...initialForceConfig
709
+ });
710
+ const [nodes, setNodes, onNodesChange] = (0, import_react8.useNodesState)(initialNodes);
711
+ const [edges, setEdges, onEdgesChange] = (0, import_react8.useEdgesState)(initialEdges);
712
+ import_react7.default.useEffect(() => {
713
+ setNodes(initialNodes);
714
+ setEdges(initialEdges);
715
+ }, [initialNodes, initialEdges, setNodes, setEdges]);
716
+ const {
717
+ onNodeDragStart,
718
+ onNodeDrag,
719
+ onNodeDragStop,
720
+ updateForceConfig,
721
+ restartSimulation
722
+ } = useForceLayout(forceConfig, primaryRootId);
723
+ const handleNodeClick = (0, import_react7.useCallback)(
724
+ (_, node) => {
725
+ onNodeClick?.(node.id);
726
+ },
727
+ [onNodeClick]
728
+ );
729
+ const handleNodeDoubleClick = (0, import_react7.useCallback)(
730
+ (_, node) => {
731
+ onNodeDoubleClick?.(node.id);
732
+ },
733
+ [onNodeDoubleClick]
734
+ );
735
+ const handleConfigChange = (0, import_react7.useCallback)(
736
+ (updates) => {
737
+ setForceConfig((prev) => ({ ...prev, ...updates }));
738
+ updateForceConfig(updates);
739
+ },
740
+ [updateForceConfig]
741
+ );
742
+ const miniMapNodeColor = (0, import_react7.useCallback)((node) => {
743
+ const data = node.data;
744
+ return getLevelColor(data.level);
745
+ }, []);
746
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
747
+ "div",
748
+ {
749
+ className,
750
+ style: {
751
+ width,
752
+ height,
753
+ position: "relative"
754
+ },
755
+ children: [
756
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
757
+ import_react8.ReactFlow,
758
+ {
759
+ nodes,
760
+ edges,
761
+ onNodesChange,
762
+ onEdgesChange,
763
+ onNodeClick: handleNodeClick,
764
+ onNodeDoubleClick: handleNodeDoubleClick,
765
+ onNodeDragStart,
766
+ onNodeDrag,
767
+ onNodeDragStop,
768
+ nodeTypes,
769
+ edgeTypes,
770
+ connectionMode: import_react8.ConnectionMode.Loose,
771
+ fitView: true,
772
+ fitViewOptions: { padding: 0.3 },
773
+ minZoom: 0.1,
774
+ maxZoom: 2,
775
+ proOptions: { hideAttribution: true },
776
+ children: [
777
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react8.Background, {}),
778
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react8.Controls, {}),
779
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react8.MiniMap, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
780
+ ]
781
+ }
782
+ ),
783
+ showControls && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
784
+ ForceControls,
785
+ {
786
+ config: forceConfig,
787
+ onChange: handleConfigChange,
788
+ onRestart: restartSimulation
789
+ }
790
+ )
791
+ ]
792
+ }
793
+ );
794
+ };
795
+ var ObservablesGraph = (props) => {
796
+ const { investigation } = props;
797
+ const rootObservables = (0, import_react7.useMemo)(() => {
798
+ const roots = (0, import_cyvest_js.findRootObservables)(investigation);
799
+ return new Set(roots.map((r) => r.key));
800
+ }, [investigation]);
801
+ const primaryRootId = (0, import_react7.useMemo)(() => {
802
+ const roots = (0, import_cyvest_js.findRootObservables)(investigation);
803
+ return roots.length > 0 ? roots[0].key : void 0;
804
+ }, [investigation]);
805
+ const { initialNodes, initialEdges } = (0, import_react7.useMemo)(() => {
806
+ const nodes = createObservableNodes(investigation, rootObservables);
807
+ const edges = createObservableEdges(investigation);
808
+ return { initialNodes: nodes, initialEdges: edges };
809
+ }, [investigation, rootObservables]);
810
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react8.ReactFlowProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
811
+ ObservablesGraphInner,
812
+ {
813
+ ...props,
814
+ initialNodes,
815
+ initialEdges,
816
+ primaryRootId
817
+ }
818
+ ) });
819
+ };
820
+
821
+ // src/components/InvestigationGraph.tsx
822
+ var import_react12 = __toESM(require("react"));
823
+ var import_react13 = require("@xyflow/react");
824
+ var import_style2 = require("@xyflow/react/dist/style.css");
825
+ var import_cyvest_js2 = require("@cyvest/cyvest-js");
826
+
827
+ // src/components/InvestigationNode.tsx
828
+ var import_react9 = require("react");
829
+ var import_react10 = require("@xyflow/react");
830
+ var import_jsx_runtime4 = require("react/jsx-runtime");
831
+ function InvestigationNodeComponent({
832
+ data,
833
+ selected
834
+ }) {
835
+ const nodeData = data;
836
+ const {
837
+ label,
838
+ emoji,
839
+ nodeType,
840
+ level,
841
+ description
842
+ } = nodeData;
843
+ const borderColor = getLevelColor(level);
844
+ const backgroundColor = getLevelBackgroundColor(level);
845
+ const getNodeStyle = () => {
846
+ switch (nodeType) {
847
+ case "root":
848
+ return {
849
+ minWidth: 120,
850
+ padding: "8px 16px",
851
+ borderRadius: 8,
852
+ fontWeight: 600
853
+ };
854
+ case "check":
855
+ return {
856
+ minWidth: 100,
857
+ padding: "6px 12px",
858
+ borderRadius: 4,
859
+ fontWeight: 400
860
+ };
861
+ case "container":
862
+ return {
863
+ minWidth: 100,
864
+ padding: "6px 12px",
865
+ borderRadius: 12,
866
+ fontWeight: 400
867
+ };
868
+ default:
869
+ return {
870
+ minWidth: 80,
871
+ padding: "6px 12px",
872
+ borderRadius: 4,
873
+ fontWeight: 400
874
+ };
875
+ }
876
+ };
877
+ const style = getNodeStyle();
878
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
879
+ "div",
880
+ {
881
+ className: "investigation-node",
882
+ style: {
883
+ ...style,
884
+ display: "flex",
885
+ flexDirection: "column",
886
+ alignItems: "center",
887
+ backgroundColor,
888
+ border: `${selected ? 3 : 2}px solid ${borderColor}`,
889
+ cursor: "pointer",
890
+ fontFamily: "system-ui, sans-serif"
891
+ },
892
+ children: [
893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
894
+ "div",
895
+ {
896
+ style: {
897
+ display: "flex",
898
+ alignItems: "center",
899
+ gap: 6
900
+ },
901
+ children: [
902
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 14 }, children: emoji }),
903
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
904
+ "span",
905
+ {
906
+ style: {
907
+ fontSize: 12,
908
+ fontWeight: style.fontWeight,
909
+ maxWidth: 150,
910
+ overflow: "hidden",
911
+ textOverflow: "ellipsis",
912
+ whiteSpace: "nowrap"
913
+ },
914
+ title: label,
915
+ children: label
916
+ }
917
+ )
918
+ ]
919
+ }
920
+ ),
921
+ description && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
922
+ "div",
923
+ {
924
+ style: {
925
+ marginTop: 4,
926
+ fontSize: 10,
927
+ color: "#6b7280",
928
+ maxWidth: 140,
929
+ overflow: "hidden",
930
+ textOverflow: "ellipsis",
931
+ whiteSpace: "nowrap"
932
+ },
933
+ title: description,
934
+ children: description
935
+ }
936
+ ),
937
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
938
+ import_react10.Handle,
939
+ {
940
+ type: "target",
941
+ position: import_react10.Position.Left,
942
+ style: {
943
+ width: 8,
944
+ height: 8,
945
+ background: borderColor
946
+ }
947
+ }
948
+ ),
949
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
950
+ import_react10.Handle,
951
+ {
952
+ type: "source",
953
+ position: import_react10.Position.Right,
954
+ style: {
955
+ width: 8,
956
+ height: 8,
957
+ background: borderColor
958
+ }
959
+ }
960
+ )
961
+ ]
962
+ }
963
+ );
964
+ }
965
+ var InvestigationNode = (0, import_react9.memo)(InvestigationNodeComponent);
966
+
967
+ // src/hooks/useDagreLayout.ts
968
+ var import_react11 = require("react");
969
+ var import_dagre = __toESM(require("@dagrejs/dagre"));
970
+ var DEFAULT_OPTIONS = {
971
+ direction: "LR",
972
+ // Horizontal layout by default
973
+ nodeSpacing: 50,
974
+ rankSpacing: 100
975
+ };
976
+ function computeDagreLayout(nodes, edges, options = {}) {
977
+ if (nodes.length === 0) {
978
+ return { nodes, edges };
979
+ }
980
+ const opts = { ...DEFAULT_OPTIONS, ...options };
981
+ const g = new import_dagre.default.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
982
+ g.setGraph({
983
+ rankdir: opts.direction,
984
+ nodesep: opts.nodeSpacing,
985
+ ranksep: opts.rankSpacing,
986
+ marginx: 20,
987
+ marginy: 20
988
+ });
989
+ for (const node of nodes) {
990
+ const width = node.measured?.width ?? 150;
991
+ const height = node.measured?.height ?? 50;
992
+ g.setNode(node.id, { width, height });
993
+ }
994
+ for (const edge of edges) {
995
+ g.setEdge(edge.source, edge.target);
996
+ }
997
+ import_dagre.default.layout(g);
998
+ const positionedNodes = nodes.map((node) => {
999
+ const dagNode = g.node(node.id);
1000
+ const width = node.measured?.width ?? 150;
1001
+ const height = node.measured?.height ?? 50;
1002
+ return {
1003
+ ...node,
1004
+ position: {
1005
+ x: dagNode.x - width / 2,
1006
+ y: dagNode.y - height / 2
1007
+ }
1008
+ };
1009
+ });
1010
+ return { nodes: positionedNodes, edges };
1011
+ }
1012
+
1013
+ // src/components/InvestigationGraph.tsx
1014
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1015
+ var nodeTypes2 = {
1016
+ investigation: InvestigationNode
1017
+ };
1018
+ function flattenContainers(containers) {
1019
+ const result = [];
1020
+ for (const container of Object.values(containers)) {
1021
+ result.push(container);
1022
+ if (container.sub_containers) {
1023
+ result.push(...flattenContainers(container.sub_containers));
1024
+ }
1025
+ }
1026
+ return result;
1027
+ }
1028
+ function createInvestigationGraph(investigation) {
1029
+ const nodes = [];
1030
+ const edges = [];
1031
+ const roots = (0, import_cyvest_js2.findRootObservables)(investigation);
1032
+ const primaryRoot = roots.length > 0 ? roots[0] : null;
1033
+ const rootKey = primaryRoot?.key ?? "investigation-root";
1034
+ const rootValue = primaryRoot?.value ?? "Investigation";
1035
+ const rootLevel = primaryRoot?.level ?? investigation.level;
1036
+ const rootNodeData = {
1037
+ label: truncateLabel(rootValue, 24),
1038
+ nodeType: "root",
1039
+ level: rootLevel,
1040
+ score: primaryRoot?.score ?? investigation.score,
1041
+ emoji: getInvestigationNodeEmoji("root")
1042
+ };
1043
+ nodes.push({
1044
+ id: rootKey,
1045
+ type: "investigation",
1046
+ position: { x: 0, y: 0 },
1047
+ data: rootNodeData
1048
+ });
1049
+ const allChecks = [];
1050
+ for (const checksForKey of Object.values(investigation.checks)) {
1051
+ allChecks.push(...checksForKey);
1052
+ }
1053
+ const seenCheckIds = /* @__PURE__ */ new Set();
1054
+ for (const check of allChecks) {
1055
+ if (seenCheckIds.has(check.key)) continue;
1056
+ seenCheckIds.add(check.key);
1057
+ const checkNodeData = {
1058
+ label: truncateLabel(check.check_id, 20),
1059
+ nodeType: "check",
1060
+ level: check.level,
1061
+ score: check.score,
1062
+ description: truncateLabel(check.description, 30),
1063
+ emoji: getInvestigationNodeEmoji("check")
1064
+ };
1065
+ nodes.push({
1066
+ id: `check-${check.key}`,
1067
+ type: "investigation",
1068
+ position: { x: 0, y: 0 },
1069
+ data: checkNodeData
1070
+ });
1071
+ edges.push({
1072
+ id: `edge-root-${check.key}`,
1073
+ source: rootKey,
1074
+ target: `check-${check.key}`,
1075
+ type: "default"
1076
+ });
1077
+ }
1078
+ const allContainers = flattenContainers(investigation.containers);
1079
+ for (const container of allContainers) {
1080
+ const containerNodeData = {
1081
+ label: truncateLabel(container.path.split("/").pop() ?? container.path, 20),
1082
+ nodeType: "container",
1083
+ level: container.aggregated_level,
1084
+ score: container.aggregated_score,
1085
+ path: container.path,
1086
+ emoji: getInvestigationNodeEmoji("container")
1087
+ };
1088
+ nodes.push({
1089
+ id: `container-${container.key}`,
1090
+ type: "investigation",
1091
+ position: { x: 0, y: 0 },
1092
+ data: containerNodeData
1093
+ });
1094
+ edges.push({
1095
+ id: `edge-root-container-${container.key}`,
1096
+ source: rootKey,
1097
+ target: `container-${container.key}`,
1098
+ type: "default"
1099
+ });
1100
+ for (const checkKey of container.checks) {
1101
+ if (seenCheckIds.has(checkKey)) {
1102
+ edges.push({
1103
+ id: `edge-container-check-${container.key}-${checkKey}`,
1104
+ source: `container-${container.key}`,
1105
+ target: `check-${checkKey}`,
1106
+ type: "default",
1107
+ style: { strokeDasharray: "5,5" }
1108
+ });
1109
+ }
1110
+ }
1111
+ }
1112
+ return { nodes, edges };
1113
+ }
1114
+ var InvestigationGraph = ({
1115
+ investigation,
1116
+ height = 500,
1117
+ width = "100%",
1118
+ onNodeClick,
1119
+ className
1120
+ }) => {
1121
+ const { initialNodes, initialEdges } = (0, import_react12.useMemo)(() => {
1122
+ const { nodes: nodes2, edges: edges2 } = createInvestigationGraph(investigation);
1123
+ return { initialNodes: nodes2, initialEdges: edges2 };
1124
+ }, [investigation]);
1125
+ const { nodes: layoutNodes, edges: layoutEdges } = (0, import_react12.useMemo)(() => {
1126
+ return computeDagreLayout(initialNodes, initialEdges, {
1127
+ direction: "LR",
1128
+ nodeSpacing: 30,
1129
+ rankSpacing: 120
1130
+ });
1131
+ }, [initialNodes, initialEdges]);
1132
+ const [nodes, setNodes, onNodesChange] = (0, import_react13.useNodesState)(layoutNodes);
1133
+ const [edges, setEdges, onEdgesChange] = (0, import_react13.useEdgesState)(layoutEdges);
1134
+ import_react12.default.useEffect(() => {
1135
+ setNodes(layoutNodes);
1136
+ setEdges(layoutEdges);
1137
+ }, [layoutNodes, layoutEdges, setNodes, setEdges]);
1138
+ const handleNodeClick = (0, import_react12.useCallback)(
1139
+ (_, node) => {
1140
+ const data = node.data;
1141
+ onNodeClick?.(node.id, data.nodeType);
1142
+ },
1143
+ [onNodeClick]
1144
+ );
1145
+ const miniMapNodeColor = (0, import_react12.useCallback)((node) => {
1146
+ const data = node.data;
1147
+ return getLevelColor(data.level);
1148
+ }, []);
1149
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1150
+ "div",
1151
+ {
1152
+ className,
1153
+ style: {
1154
+ width,
1155
+ height,
1156
+ position: "relative"
1157
+ },
1158
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1159
+ import_react13.ReactFlow,
1160
+ {
1161
+ nodes,
1162
+ edges,
1163
+ onNodesChange,
1164
+ onEdgesChange,
1165
+ onNodeClick: handleNodeClick,
1166
+ nodeTypes: nodeTypes2,
1167
+ fitView: true,
1168
+ fitViewOptions: { padding: 0.2 },
1169
+ minZoom: 0.1,
1170
+ maxZoom: 2,
1171
+ proOptions: { hideAttribution: true },
1172
+ children: [
1173
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react13.Background, {}),
1174
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react13.Controls, {}),
1175
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react13.MiniMap, { nodeColor: miniMapNodeColor, zoomable: true, pannable: true })
1176
+ ]
1177
+ }
1178
+ )
1179
+ }
1180
+ );
1181
+ };
1182
+
1183
+ // src/components/CyvestGraph.tsx
1184
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1185
+ var ViewToggle = ({ currentView, onChange }) => {
1186
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1187
+ "div",
1188
+ {
1189
+ style: {
1190
+ position: "absolute",
1191
+ top: 10,
1192
+ left: 10,
1193
+ display: "flex",
1194
+ gap: 4,
1195
+ background: "white",
1196
+ padding: 4,
1197
+ borderRadius: 8,
1198
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
1199
+ zIndex: 10,
1200
+ fontFamily: "system-ui, sans-serif"
1201
+ },
1202
+ children: [
1203
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1204
+ "button",
1205
+ {
1206
+ onClick: () => onChange("observables"),
1207
+ style: {
1208
+ padding: "6px 12px",
1209
+ border: "none",
1210
+ borderRadius: 4,
1211
+ cursor: "pointer",
1212
+ fontSize: 12,
1213
+ fontWeight: currentView === "observables" ? 600 : 400,
1214
+ background: currentView === "observables" ? "#3b82f6" : "#f3f4f6",
1215
+ color: currentView === "observables" ? "white" : "#374151"
1216
+ },
1217
+ children: "Observables"
1218
+ }
1219
+ ),
1220
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1221
+ "button",
1222
+ {
1223
+ onClick: () => onChange("investigation"),
1224
+ style: {
1225
+ padding: "6px 12px",
1226
+ border: "none",
1227
+ borderRadius: 4,
1228
+ cursor: "pointer",
1229
+ fontSize: 12,
1230
+ fontWeight: currentView === "investigation" ? 600 : 400,
1231
+ background: currentView === "investigation" ? "#3b82f6" : "#f3f4f6",
1232
+ color: currentView === "investigation" ? "white" : "#374151"
1233
+ },
1234
+ children: "Investigation"
1235
+ }
1236
+ )
1237
+ ]
1238
+ }
1239
+ );
1240
+ };
1241
+ var CyvestGraph = ({
1242
+ investigation,
1243
+ height = 500,
1244
+ width = "100%",
1245
+ initialView = "observables",
1246
+ onNodeClick,
1247
+ className,
1248
+ showViewToggle = true
1249
+ }) => {
1250
+ const [view, setView] = (0, import_react14.useState)(initialView);
1251
+ const handleNodeClick = (0, import_react14.useCallback)(
1252
+ (nodeId, _nodeType) => {
1253
+ onNodeClick?.(nodeId);
1254
+ },
1255
+ [onNodeClick]
1256
+ );
1257
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1258
+ "div",
1259
+ {
1260
+ className,
1261
+ style: {
1262
+ width,
1263
+ height,
1264
+ position: "relative"
1265
+ },
1266
+ children: [
1267
+ showViewToggle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ViewToggle, { currentView: view, onChange: setView }),
1268
+ view === "observables" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1269
+ ObservablesGraph,
1270
+ {
1271
+ investigation,
1272
+ height: "100%",
1273
+ width: "100%",
1274
+ onNodeClick: handleNodeClick,
1275
+ showControls: true
1276
+ }
1277
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1278
+ InvestigationGraph,
1279
+ {
1280
+ investigation,
1281
+ height: "100%",
1282
+ width: "100%",
1283
+ onNodeClick: handleNodeClick
1284
+ }
1285
+ )
1286
+ ]
1287
+ }
1288
+ );
1289
+ };
1290
+ // Annotate the CommonJS export names for ESM import in node:
1291
+ 0 && (module.exports = {
1292
+ CyvestGraph
1293
+ });