k8s-av 1.0.0

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.
Files changed (84) hide show
  1. package/.env.example +18 -0
  2. package/dist/cli/docker.d.ts +29 -0
  3. package/dist/cli/docker.js +241 -0
  4. package/dist/cli/index.d.ts +16 -0
  5. package/dist/cli/index.js +155 -0
  6. package/dist/cli/scan.d.ts +16 -0
  7. package/dist/cli/scan.js +151 -0
  8. package/dist/cli/start.d.ts +20 -0
  9. package/dist/cli/start.js +261 -0
  10. package/dist/core/attack-path.d.ts +26 -0
  11. package/dist/core/attack-path.js +191 -0
  12. package/dist/core/cve-enricher.d.ts +10 -0
  13. package/dist/core/cve-enricher.js +175 -0
  14. package/dist/core/fetcher.d.ts +26 -0
  15. package/dist/core/fetcher.js +130 -0
  16. package/dist/core/schema.d.ts +290 -0
  17. package/dist/core/schema.js +125 -0
  18. package/dist/core/transformer.d.ts +19 -0
  19. package/dist/core/transformer.js +510 -0
  20. package/dist/db/loader.d.ts +16 -0
  21. package/dist/db/loader.js +261 -0
  22. package/dist/db/neo4j-client.d.ts +35 -0
  23. package/dist/db/neo4j-client.js +218 -0
  24. package/dist/db/queries.d.ts +71 -0
  25. package/dist/db/queries.js +290 -0
  26. package/dist/db/test.d.ts +19 -0
  27. package/dist/db/test.js +268 -0
  28. package/dist/db/types.d.ts +137 -0
  29. package/dist/db/types.js +37 -0
  30. package/dist/schemas/index.d.ts +70 -0
  31. package/dist/schemas/index.js +43 -0
  32. package/dist/server/routes/blast.d.ts +3 -0
  33. package/dist/server/routes/blast.js +44 -0
  34. package/dist/server/routes/critical.d.ts +3 -0
  35. package/dist/server/routes/critical.js +80 -0
  36. package/dist/server/routes/cycles.d.ts +3 -0
  37. package/dist/server/routes/cycles.js +41 -0
  38. package/dist/server/routes/graph.d.ts +3 -0
  39. package/dist/server/routes/graph.js +57 -0
  40. package/dist/server/routes/ingest.d.ts +3 -0
  41. package/dist/server/routes/ingest.js +82 -0
  42. package/dist/server/routes/paths.d.ts +3 -0
  43. package/dist/server/routes/paths.js +66 -0
  44. package/dist/server/routes/report.d.ts +3 -0
  45. package/dist/server/routes/report.js +47 -0
  46. package/dist/server/routes/simulate.d.ts +3 -0
  47. package/dist/server/routes/simulate.js +75 -0
  48. package/dist/server/routes/vulnerabilities.d.ts +3 -0
  49. package/dist/server/routes/vulnerabilities.js +129 -0
  50. package/dist/server/server.d.ts +15 -0
  51. package/dist/server/server.js +136 -0
  52. package/dist/services/ingestion.service.d.ts +25 -0
  53. package/dist/services/ingestion.service.js +100 -0
  54. package/dist/services/report/formatter.d.ts +12 -0
  55. package/dist/services/report/formatter.js +138 -0
  56. package/dist/services/report/generator.d.ts +27 -0
  57. package/dist/services/report/generator.js +68 -0
  58. package/dist/services/risk-explainer.d.ts +67 -0
  59. package/dist/services/risk-explainer.js +285 -0
  60. package/docker/docker-compose.yml +66 -0
  61. package/package.json +75 -0
  62. package/ui/index.html +12 -0
  63. package/ui/package-lock.json +3150 -0
  64. package/ui/package.json +30 -0
  65. package/ui/postcss.config.cjs +6 -0
  66. package/ui/src/App.tsx +37 -0
  67. package/ui/src/components/Box.tsx +33 -0
  68. package/ui/src/components/DetailPanel.tsx +239 -0
  69. package/ui/src/components/RiskBadge.tsx +38 -0
  70. package/ui/src/components/Sidebar.tsx +107 -0
  71. package/ui/src/components/graph/CustomNode.tsx +102 -0
  72. package/ui/src/components/graph/GraphCanvas.tsx +174 -0
  73. package/ui/src/index.css +48 -0
  74. package/ui/src/lib/api.ts +161 -0
  75. package/ui/src/main.tsx +10 -0
  76. package/ui/src/store/useAppStore.ts +168 -0
  77. package/ui/src/views/CriticalNodeView.tsx +150 -0
  78. package/ui/src/views/OverviewView.tsx +76 -0
  79. package/ui/src/views/PathsView.tsx +280 -0
  80. package/ui/src/views/ReportView.tsx +367 -0
  81. package/ui/src/views/VulnerabilitiesView.tsx +135 -0
  82. package/ui/tailwind.config.ts +14 -0
  83. package/ui/tsconfig.json +20 -0
  84. package/ui/vite.config.ts +19 -0
@@ -0,0 +1,174 @@
1
+ import { useMemo, useCallback } from 'react';
2
+ import {
3
+ ReactFlow, Background, Controls, MiniMap,
4
+ useNodesState, useEdgesState,
5
+ type Node, type Edge,
6
+ BackgroundVariant, MarkerType,
7
+ } from '@xyflow/react';
8
+ import '@xyflow/react/dist/style.css';
9
+ import dagre from '@dagrejs/dagre';
10
+
11
+ import { CustomNode, type K8sNodeData } from './CustomNode';
12
+ import { useAppStore } from '../../store/useAppStore';
13
+ import type { GraphNode, GraphEdge } from '../../lib/api';
14
+
15
+ const NODE_W = 180;
16
+ const NODE_H = 54;
17
+ const nodeTypes = { k8s: CustomNode };
18
+
19
+ // ─── Dagre layout ─────────────────────────────────────────────────────────────
20
+ function applyLayout(nodes: Node[], edges: Edge[]): Node[] {
21
+ const g = new dagre.graphlib.Graph();
22
+ g.setDefaultEdgeLabel(() => ({}));
23
+ g.setGraph({ rankdir: 'LR', nodesep: 60, ranksep: 130 });
24
+ nodes.forEach((n) => g.setNode(n.id, { width: NODE_W, height: NODE_H }));
25
+ edges.forEach((e) => g.setEdge(e.source, e.target));
26
+ dagre.layout(g);
27
+ return nodes.map((n) => {
28
+ const pos = g.node(n.id);
29
+ return { ...n, position: { x: pos.x - NODE_W / 2, y: pos.y - NODE_H / 2 } };
30
+ });
31
+ }
32
+
33
+ // ─── Transform backend data → React Flow ─────────────────────────────────────
34
+ function buildFlowGraph(
35
+ rawNodes: GraphNode[],
36
+ rawEdges: GraphEdge[],
37
+ selectedNodeId: string | null,
38
+ highlightedNodeIds: Set<string>,
39
+ highlightedEdgeIds: Set<string>,
40
+ criticalNodeId: string | null,
41
+ vulnMap: Map<string, number>,
42
+ ): { nodes: Node[]; edges: Edge[] } {
43
+ const hasHighlight = highlightedNodeIds.size > 0;
44
+
45
+ const nodes: Node[] = rawNodes.map((n) => ({
46
+ id: n.id,
47
+ type: 'k8s',
48
+ position: { x: 0, y: 0 },
49
+ data: {
50
+ label: n.name || n.id,
51
+ nodeType: n.type,
52
+ riskScore: n.riskScore,
53
+ isEntryPoint: n.isEntryPoint,
54
+ isCrownJewel: n.isCrownJewel,
55
+ hasCve: (n.cve?.length ?? 0) > 0,
56
+ highlighted: highlightedNodeIds.has(n.id),
57
+ dimmed: hasHighlight && !highlightedNodeIds.has(n.id) && n.id !== selectedNodeId,
58
+ isCritical: n.id === criticalNodeId,
59
+ vuln: vulnMap.get(n.id) ?? null,
60
+ } satisfies K8sNodeData,
61
+ }));
62
+
63
+ const edges: Edge[] = rawEdges.map((e, i) => {
64
+ const edgeId = `${e.from}-${e.to}-${i}`;
65
+ const isHighlighted = highlightedEdgeIds.has(edgeId) || highlightedEdgeIds.has(`${e.from}-${e.to}`);
66
+ return {
67
+ id: edgeId,
68
+ source: e.from,
69
+ target: e.to,
70
+ label: e.type,
71
+ animated: isHighlighted,
72
+ style: {
73
+ stroke: isHighlighted ? '#FF6A00' : '#2a2a2a',
74
+ strokeWidth: isHighlighted ? 2 : 1,
75
+ opacity: hasHighlight && !isHighlighted ? 0.1 : 0.6,
76
+ strokeDasharray: isHighlighted ? '6 3' : undefined,
77
+ },
78
+ markerEnd: {
79
+ type: MarkerType.ArrowClosed,
80
+ color: isHighlighted ? '#FF6A00' : '#2a2a2a',
81
+ },
82
+ labelStyle: { fill: '#555555', fontSize: 9 },
83
+ labelBgStyle: { fill: '#0B0B0B' },
84
+ };
85
+ });
86
+
87
+ return { nodes: applyLayout(nodes, edges), edges };
88
+ }
89
+
90
+ // ─── Component ────────────────────────────────────────────────────────────────
91
+ interface Props {
92
+ highlightedNodeIds?: Set<string>;
93
+ highlightedEdgeKeys?: Set<string>;
94
+ criticalNodeId?: string | null;
95
+ }
96
+
97
+ export function GraphCanvas({
98
+ highlightedNodeIds = new Set(),
99
+ highlightedEdgeKeys = new Set(),
100
+ criticalNodeId = null,
101
+ }: Props) {
102
+ const { graphNodes, graphEdges, selectedNodeId, vulnerabilities, selectNode } = useAppStore();
103
+
104
+ const vulnMap = useMemo(() => {
105
+ const m = new Map<string, number>();
106
+ vulnerabilities.forEach((v) => m.set(v.nodeId, v.riskScore));
107
+ return m;
108
+ }, [vulnerabilities]);
109
+
110
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(
111
+ () => buildFlowGraph(graphNodes, graphEdges, selectedNodeId, highlightedNodeIds, highlightedEdgeKeys, criticalNodeId, vulnMap),
112
+ [graphNodes, graphEdges, selectedNodeId, highlightedNodeIds, highlightedEdgeKeys, criticalNodeId, vulnMap],
113
+ );
114
+
115
+ const [nodes, , onNodesChange] = useNodesState(initialNodes);
116
+ const [edges, , onEdgesChange] = useEdgesState(initialEdges);
117
+
118
+ const syncedNodes = useMemo(() => {
119
+ return initialNodes.map((n) => {
120
+ const existing = nodes.find((en) => en.id === n.id);
121
+ return existing ? { ...n, position: existing.position } : n;
122
+ });
123
+ }, [initialNodes]); // eslint-disable-line
124
+
125
+ const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
126
+ selectNode(node.id);
127
+ }, [selectNode]);
128
+
129
+ const onPaneClick = useCallback(() => {
130
+ selectNode(null);
131
+ }, [selectNode]);
132
+
133
+ if (graphNodes.length === 0) {
134
+ return (
135
+ <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
136
+ <div style={{ textAlign: 'center' }}>
137
+ <div style={{ fontSize: 13, color: '#555555' }}>No cluster data loaded.</div>
138
+ <div style={{ fontSize: 11, color: '#333333', marginTop: 6 }}>
139
+ Run: <span style={{ fontFamily: 'monospace', color: '#FF6A00' }}>npm run ingest</span>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ return (
147
+ <div style={{ flex: 1, minHeight: 0, borderRadius: 12, overflow: 'hidden' }}>
148
+ <ReactFlow
149
+ nodes={syncedNodes}
150
+ edges={initialEdges}
151
+ onNodesChange={onNodesChange}
152
+ onEdgesChange={onEdgesChange}
153
+ onNodeClick={onNodeClick}
154
+ onPaneClick={onPaneClick}
155
+ nodeTypes={nodeTypes}
156
+ fitView
157
+ fitViewOptions={{ padding: 0.12 }}
158
+ minZoom={0.08}
159
+ maxZoom={2}
160
+ attributionPosition={undefined}
161
+ >
162
+ <Background variant={BackgroundVariant.Dots} color="#1F1F1F" gap={28} size={1} />
163
+ <Controls showInteractive={false} />
164
+ <MiniMap
165
+ nodeColor={(n) => {
166
+ const d = n.data as K8sNodeData;
167
+ return d.riskScore >= 8 ? '#FF3B3B' : d.riskScore >= 5 ? '#FFA726' : '#3B82F6';
168
+ }}
169
+ maskColor="#0B0B0BCC"
170
+ />
171
+ </ReactFlow>
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,48 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ *, *::before, *::after { box-sizing: border-box; }
6
+
7
+ html, body, #root {
8
+ height: 100%;
9
+ margin: 0;
10
+ padding: 0;
11
+ background: #0B0B0B;
12
+ color: #EAEAEA;
13
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
14
+ font-size: 14px;
15
+ }
16
+
17
+ /* React Flow */
18
+ .react-flow__attribution { display: none !important; }
19
+ .react-flow__background { background: #0B0B0B; }
20
+ .react-flow__minimap { background: #121212 !important; border: 1px solid #1F1F1F; border-radius: 8px; }
21
+ .react-flow__controls { background: #121212 !important; border: 1px solid #1F1F1F; border-radius: 8px; }
22
+ .react-flow__controls-button {
23
+ background: #121212 !important;
24
+ border-bottom: 1px solid #1F1F1F !important;
25
+ color: #888888 !important;
26
+ fill: #888888 !important;
27
+ }
28
+ .react-flow__controls-button:hover {
29
+ background: #1F1F1F !important;
30
+ fill: #EAEAEA !important;
31
+ }
32
+
33
+ /* Scrollbar */
34
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
35
+ ::-webkit-scrollbar-track { background: transparent; }
36
+ ::-webkit-scrollbar-thumb { background: #1F1F1F; border-radius: 4px; }
37
+ ::-webkit-scrollbar-thumb:hover { background: #2a2a2a; }
38
+
39
+ /* Pulse animation for skeleton loaders */
40
+ @keyframes pulse {
41
+ 0%, 100% { opacity: 1; }
42
+ 50% { opacity: 0.4; }
43
+ }
44
+
45
+ /* Orange glow utility */
46
+ .glow-orange {
47
+ box-shadow: 0 0 0 1px #FF6A0040, 0 0 12px #FF6A0020;
48
+ }
@@ -0,0 +1,161 @@
1
+ // All paths are relative — Vite proxies /api/* → http://localhost:3001
2
+
3
+ async function get<T>(path: string): Promise<T> {
4
+ const res = await fetch(path);
5
+ if (!res.ok) {
6
+ const text = await res.text().catch(() => res.statusText);
7
+ throw new Error(`GET ${path} → ${res.status}: ${text}`);
8
+ }
9
+ return res.json() as Promise<T>;
10
+ }
11
+
12
+ async function post<T>(path: string, body: unknown): Promise<T> {
13
+ const res = await fetch(path, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify(body),
17
+ });
18
+ if (!res.ok) {
19
+ const text = await res.text().catch(() => res.statusText);
20
+ throw new Error(`POST ${path} → ${res.status}: ${text}`);
21
+ }
22
+ return res.json() as Promise<T>;
23
+ }
24
+
25
+ // ─── Domain types (match actual backend responses) ───────────────────────────
26
+
27
+ export interface GraphNode {
28
+ id: string;
29
+ type: string;
30
+ name: string;
31
+ namespace: string;
32
+ riskScore: number;
33
+ isEntryPoint: boolean;
34
+ isCrownJewel: boolean;
35
+ image: string | null;
36
+ cve: string[];
37
+ }
38
+
39
+ export interface GraphEdge {
40
+ from: string;
41
+ to: string;
42
+ type: string;
43
+ weight: number;
44
+ verbs: string[];
45
+ resources: string[];
46
+ }
47
+
48
+ export interface GraphResponse {
49
+ nodes: GraphNode[];
50
+ edges: GraphEdge[];
51
+ metadata: {
52
+ totalNodes: number;
53
+ totalEdges: number;
54
+ entryPoints: number;
55
+ crownJewels: number;
56
+ retrievedAt: string;
57
+ };
58
+ }
59
+
60
+ export interface Vulnerability {
61
+ nodeId: string;
62
+ type: string;
63
+ namespace: string;
64
+ riskScore: number;
65
+ isEntryPoint: boolean;
66
+ isCrownJewel: boolean;
67
+ cves: string[];
68
+ reason: string;
69
+ explanation: string;
70
+ connections: { out: number; in: number };
71
+ }
72
+
73
+ export interface VulnsResponse {
74
+ vulnerabilities: Vulnerability[];
75
+ summary: {
76
+ total: number;
77
+ critical: number;
78
+ high: number;
79
+ medium: number;
80
+ entryPoints: number;
81
+ crownJewels: number;
82
+ withCves: number;
83
+ };
84
+ }
85
+
86
+ export interface AttackPath {
87
+ nodes: string[];
88
+ riskScore: number;
89
+ entryPoint: string;
90
+ crownJewel: string;
91
+ hops: number;
92
+ totalWeight: number;
93
+ description: string;
94
+ }
95
+
96
+ export interface PathsResponse {
97
+ paths: AttackPath[];
98
+ summary: {
99
+ total: number;
100
+ critical: number;
101
+ uniqueEntryPoints: number;
102
+ uniqueCrownJewels: number;
103
+ avgHops: number;
104
+ };
105
+ }
106
+
107
+ export interface CriticalNodeResult {
108
+ nodeId: string;
109
+ name: string;
110
+ type: string;
111
+ namespace: string;
112
+ betweennessScore: number;
113
+ isEntryPoint: boolean;
114
+ isCrownJewel: boolean;
115
+ riskScore: number;
116
+ }
117
+
118
+ export interface CriticalResponse {
119
+ criticalNodes: CriticalNodeResult[];
120
+ pathElimination: {
121
+ nodeId: string;
122
+ totalPaths: number;
123
+ pathsEliminated: number;
124
+ pathsRemaining: number;
125
+ reductionPercent: number;
126
+ note: string;
127
+ } | null;
128
+ }
129
+
130
+ export interface SimulateResponse {
131
+ simulation: {
132
+ nodeId: string;
133
+ maxHops: number;
134
+ graphMutated: boolean;
135
+ durationMs: number;
136
+ };
137
+ results: {
138
+ baselinePathCount: number;
139
+ filteredPathCount: number;
140
+ pathsEliminated: number;
141
+ reductionPercent: number;
142
+ verdict: string;
143
+ };
144
+ }
145
+
146
+ export interface ReportResponse {
147
+ report: unknown;
148
+ formatted: string;
149
+ }
150
+
151
+ // ─── API surface ─────────────────────────────────────────────────────────────
152
+
153
+ export const api = {
154
+ getGraph: () => get<GraphResponse>('/api/graph'),
155
+ getVulnerabilities: () => get<VulnsResponse>('/api/vulnerabilities'),
156
+ getPaths: () => get<PathsResponse>('/api/paths'),
157
+ getCriticalNode: () => get<CriticalResponse>('/api/critical-node'),
158
+ getReport: () => get<ReportResponse>('/api/report'),
159
+ simulate: (nodeId: string) =>
160
+ post<SimulateResponse>('/api/simulate', { nodeId }),
161
+ };
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
@@ -0,0 +1,168 @@
1
+ import { create } from 'zustand';
2
+ import {
3
+ api,
4
+ GraphNode, GraphEdge,
5
+ Vulnerability, AttackPath,
6
+ CriticalResponse, SimulateResponse, ReportResponse,
7
+ } from '../lib/api';
8
+
9
+ type View = 'overview' | 'paths' | 'vulnerabilities' | 'critical' | 'report';
10
+
11
+ interface AppState {
12
+ // ── Data ──────────────────────────────────────────────────────────────────
13
+ graphNodes: GraphNode[];
14
+ graphEdges: GraphEdge[];
15
+ graphMeta: { totalNodes: number; totalEdges: number; entryPoints: number; crownJewels: number } | null;
16
+ vulnerabilities: Vulnerability[];
17
+ vulnSummary: { total: number; critical: number; high: number; medium: number } | null;
18
+ paths: AttackPath[];
19
+ pathsSummary: { total: number; critical: number } | null;
20
+ criticalData: CriticalResponse | null;
21
+ simulateResult: SimulateResponse | null;
22
+ reportData: ReportResponse | null;
23
+
24
+ // ── UI state ──────────────────────────────────────────────────────────────
25
+ activeView: View;
26
+ selectedNodeId: string | null;
27
+ selectedPathIdx: number | null;
28
+ loading: Record<string, boolean>;
29
+ errors: Record<string, string | null>;
30
+
31
+ // ── Actions ───────────────────────────────────────────────────────────────
32
+ setView: (v: View) => void;
33
+ selectNode: (id: string | null) => void;
34
+ selectPath: (idx: number | null) => void;
35
+ clearSimulate: () => void;
36
+
37
+ fetchGraph: () => Promise<void>;
38
+ fetchVulnerabilities: () => Promise<void>;
39
+ fetchPaths: () => Promise<void>;
40
+ fetchCritical: () => Promise<void>;
41
+ fetchReport: () => Promise<void>;
42
+ simulate: (nodeId: string) => Promise<void>;
43
+ }
44
+
45
+ function setLoading(set: (fn: (s: AppState) => Partial<AppState>) => void, key: string, val: boolean) {
46
+ set((s) => ({ loading: { ...s.loading, [key]: val } }));
47
+ }
48
+ function setError(set: (fn: (s: AppState) => Partial<AppState>) => void, key: string, msg: string | null) {
49
+ set((s) => ({ errors: { ...s.errors, [key]: msg } }));
50
+ }
51
+
52
+ export const useAppStore = create<AppState>((set, get) => ({
53
+ // ── Initial state ─────────────────────────────────────────────────────────
54
+ graphNodes: [],
55
+ graphEdges: [],
56
+ graphMeta: null,
57
+ vulnerabilities: [],
58
+ vulnSummary: null,
59
+ paths: [],
60
+ pathsSummary: null,
61
+ criticalData: null,
62
+ simulateResult: null,
63
+ reportData: null,
64
+
65
+ activeView: 'overview',
66
+ selectedNodeId: null,
67
+ selectedPathIdx: null,
68
+ loading: {},
69
+ errors: {},
70
+
71
+ // ── UI actions ─────────────────────────────────────────────────────────────
72
+ setView: (v) => {
73
+ set({ activeView: v, selectedPathIdx: null });
74
+ // Lazy-fetch on first activation
75
+ const s = get();
76
+ if (v === 'paths' && s.paths.length === 0) s.fetchPaths();
77
+ if (v === 'vulnerabilities' && s.vulnerabilities.length === 0) s.fetchVulnerabilities();
78
+ if (v === 'critical' && !s.criticalData) s.fetchCritical();
79
+ if (v === 'report' && !s.reportData) s.fetchReport();
80
+ },
81
+
82
+ selectNode: (id) => set({ selectedNodeId: id, simulateResult: null }),
83
+ selectPath: (idx) => set({ selectedPathIdx: idx }),
84
+ clearSimulate: () => set({ simulateResult: null }),
85
+
86
+ // ── Fetch actions ──────────────────────────────────────────────────────────
87
+ fetchGraph: async () => {
88
+ setLoading(set, 'graph', true);
89
+ setError(set, 'graph', null);
90
+ try {
91
+ const data = await api.getGraph();
92
+ set({
93
+ graphNodes: data.nodes,
94
+ graphEdges: data.edges,
95
+ graphMeta: data.metadata,
96
+ });
97
+ } catch (e) {
98
+ setError(set, 'graph', (e as Error).message);
99
+ } finally {
100
+ setLoading(set, 'graph', false);
101
+ }
102
+ },
103
+
104
+ fetchVulnerabilities: async () => {
105
+ setLoading(set, 'vulns', true);
106
+ setError(set, 'vulns', null);
107
+ try {
108
+ const data = await api.getVulnerabilities();
109
+ set({ vulnerabilities: data.vulnerabilities, vulnSummary: data.summary });
110
+ } catch (e) {
111
+ setError(set, 'vulns', (e as Error).message);
112
+ } finally {
113
+ setLoading(set, 'vulns', false);
114
+ }
115
+ },
116
+
117
+ fetchPaths: async () => {
118
+ setLoading(set, 'paths', true);
119
+ setError(set, 'paths', null);
120
+ try {
121
+ const data = await api.getPaths();
122
+ set({ paths: data.paths, pathsSummary: data.summary });
123
+ } catch (e) {
124
+ setError(set, 'paths', (e as Error).message);
125
+ } finally {
126
+ setLoading(set, 'paths', false);
127
+ }
128
+ },
129
+
130
+ fetchCritical: async () => {
131
+ setLoading(set, 'critical', true);
132
+ setError(set, 'critical', null);
133
+ try {
134
+ const data = await api.getCriticalNode();
135
+ set({ criticalData: data });
136
+ } catch (e) {
137
+ setError(set, 'critical', (e as Error).message);
138
+ } finally {
139
+ setLoading(set, 'critical', false);
140
+ }
141
+ },
142
+
143
+ fetchReport: async () => {
144
+ setLoading(set, 'report', true);
145
+ setError(set, 'report', null);
146
+ try {
147
+ const data = await api.getReport();
148
+ set({ reportData: data });
149
+ } catch (e) {
150
+ setError(set, 'report', (e as Error).message);
151
+ } finally {
152
+ setLoading(set, 'report', false);
153
+ }
154
+ },
155
+
156
+ simulate: async (nodeId: string) => {
157
+ setLoading(set, 'simulate', true);
158
+ setError(set, 'simulate', null);
159
+ try {
160
+ const data = await api.simulate(nodeId);
161
+ set({ simulateResult: data });
162
+ } catch (e) {
163
+ setError(set, 'simulate', (e as Error).message);
164
+ } finally {
165
+ setLoading(set, 'simulate', false);
166
+ }
167
+ },
168
+ }));