k8s-av 1.0.2 → 1.0.4
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/server/routes/graph.js +1 -1
- package/package.json +1 -1
- package/ui/package.json +2 -1
- package/ui/src/App.tsx +0 -3
- package/ui/src/components/Box.tsx +0 -1
- package/ui/src/components/DetailPanel.tsx +0 -8
- package/ui/src/components/Sidebar.tsx +0 -4
- package/ui/src/components/graph/CustomNode.tsx +0 -4
- package/ui/src/components/graph/GraphCanvas.tsx +0 -3
- package/ui/src/views/CriticalNodeView.tsx +0 -4
- package/ui/src/views/OverviewView.tsx +0 -3
- package/ui/src/views/PathsView.tsx +0 -11
- package/ui/src/views/ReportView.tsx +4 -28
- package/ui/src/views/VulnerabilitiesView.tsx +0 -6
|
@@ -7,7 +7,7 @@ router.get('/', async (_req, res, next) => {
|
|
|
7
7
|
try {
|
|
8
8
|
const [nodesResult, edgesResult] = await Promise.all([
|
|
9
9
|
(0, neo4j_client_1.runQuery)(`
|
|
10
|
-
MATCH (n:K8sNode)
|
|
10
|
+
MATCH (n:K8sNode)
|
|
11
11
|
RETURN
|
|
12
12
|
n.id AS id,
|
|
13
13
|
n.type AS type,
|
package/package.json
CHANGED
package/ui/package.json
CHANGED
package/ui/src/App.tsx
CHANGED
|
@@ -11,7 +11,6 @@ import { useAppStore } from './store/useAppStore';
|
|
|
11
11
|
export default function App() {
|
|
12
12
|
const { activeView, selectedNodeId, fetchGraph, fetchVulnerabilities } = useAppStore();
|
|
13
13
|
|
|
14
|
-
// Fetch graph + vulnerabilities on mount
|
|
15
14
|
useEffect(() => {
|
|
16
15
|
fetchGraph();
|
|
17
16
|
fetchVulnerabilities();
|
|
@@ -21,7 +20,6 @@ export default function App() {
|
|
|
21
20
|
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden', background: '#0a0a0f' }}>
|
|
22
21
|
<Sidebar />
|
|
23
22
|
|
|
24
|
-
{/* Main */}
|
|
25
23
|
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative' }}>
|
|
26
24
|
{activeView === 'overview' && <OverviewView />}
|
|
27
25
|
{activeView === 'paths' && <PathsView />}
|
|
@@ -30,7 +28,6 @@ export default function App() {
|
|
|
30
28
|
{activeView === 'report' && <ReportView />}
|
|
31
29
|
</main>
|
|
32
30
|
|
|
33
|
-
{/* Detail panel slides in from right when a node is selected */}
|
|
34
31
|
{selectedNodeId && <DetailPanel />}
|
|
35
32
|
</div>
|
|
36
33
|
);
|
|
@@ -15,13 +15,11 @@ export function DetailPanel() {
|
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
17
|
<>
|
|
18
|
-
{/* Backdrop */}
|
|
19
18
|
<div
|
|
20
19
|
style={{ position: 'fixed', inset: 0, zIndex: 40 }}
|
|
21
20
|
onClick={() => selectNode(null)}
|
|
22
21
|
/>
|
|
23
22
|
|
|
24
|
-
{/* Panel */}
|
|
25
23
|
<aside
|
|
26
24
|
style={{
|
|
27
25
|
position: 'fixed',
|
|
@@ -44,7 +42,6 @@ export function DetailPanel() {
|
|
|
44
42
|
}
|
|
45
43
|
`}</style>
|
|
46
44
|
|
|
47
|
-
{/* Header */}
|
|
48
45
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', padding: '16px', borderBottom: '1px solid #1F1F1F' }}>
|
|
49
46
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
50
47
|
<div style={{ fontSize: 9, fontFamily: 'monospace', color: '#FF6A00', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 5 }}>
|
|
@@ -68,14 +65,12 @@ export function DetailPanel() {
|
|
|
68
65
|
</div>
|
|
69
66
|
|
|
70
67
|
<div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
71
|
-
{/* Tags */}
|
|
72
68
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
73
69
|
{node.isEntryPoint && <Tag label="Entry Point" color="blue" />}
|
|
74
70
|
{node.isCrownJewel && <Tag label="Crown Jewel" color="amber" />}
|
|
75
71
|
{vuln && <RiskBadge score={vuln.riskScore} />}
|
|
76
72
|
</div>
|
|
77
73
|
|
|
78
|
-
{/* Image */}
|
|
79
74
|
{node.image && (
|
|
80
75
|
<InfoBox>
|
|
81
76
|
<Label>Image</Label>
|
|
@@ -83,7 +78,6 @@ export function DetailPanel() {
|
|
|
83
78
|
</InfoBox>
|
|
84
79
|
)}
|
|
85
80
|
|
|
86
|
-
{/* CVEs */}
|
|
87
81
|
{(node.cve?.length ?? 0) > 0 && (
|
|
88
82
|
<InfoBox>
|
|
89
83
|
<Label>CVEs</Label>
|
|
@@ -108,7 +102,6 @@ export function DetailPanel() {
|
|
|
108
102
|
</InfoBox>
|
|
109
103
|
)}
|
|
110
104
|
|
|
111
|
-
{/* Risk Analysis */}
|
|
112
105
|
{vuln && (
|
|
113
106
|
<InfoBox>
|
|
114
107
|
<Label>Risk Analysis</Label>
|
|
@@ -123,7 +116,6 @@ export function DetailPanel() {
|
|
|
123
116
|
</InfoBox>
|
|
124
117
|
)}
|
|
125
118
|
|
|
126
|
-
{/* Simulate Removal */}
|
|
127
119
|
<InfoBox>
|
|
128
120
|
<Label>Simulate Removal</Label>
|
|
129
121
|
<p style={{ fontSize: 11, color: '#555555', marginBottom: 10, marginTop: 0 }}>
|
|
@@ -17,7 +17,6 @@ export function Sidebar() {
|
|
|
17
17
|
<aside
|
|
18
18
|
style={{ width: 192, flexShrink: 0, display: 'flex', flexDirection: 'column', background: '#0B0B0B', borderRight: '1px solid #1F1F1F' }}
|
|
19
19
|
>
|
|
20
|
-
{/* Logo */}
|
|
21
20
|
<div style={{ padding: '20px 16px 16px', borderBottom: '1px solid #1F1F1F' }}>
|
|
22
21
|
<div style={{ fontSize: 14, fontFamily: 'monospace', color: '#FF6A00', letterSpacing: '0.15em', textTransform: 'uppercase', fontWeight: 600 }}>
|
|
23
22
|
K8s-AV
|
|
@@ -25,12 +24,10 @@ export function Sidebar() {
|
|
|
25
24
|
<div style={{ fontSize: 12, color: '#888888', marginTop: 3 }}>Attack Path Visualizer</div>
|
|
26
25
|
</div>
|
|
27
26
|
|
|
28
|
-
{/* Section label */}
|
|
29
27
|
<div style={{ padding: '16px 16px 8px', fontSize: 11, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em' }}>
|
|
30
28
|
Navigation
|
|
31
29
|
</div>
|
|
32
30
|
|
|
33
|
-
{/* Nav */}
|
|
34
31
|
<nav style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 2, padding: '0 8px' }}>
|
|
35
32
|
{NAV.map(({ id, label, icon }) => {
|
|
36
33
|
const active = activeView === id;
|
|
@@ -74,7 +71,6 @@ export function Sidebar() {
|
|
|
74
71
|
})}
|
|
75
72
|
</nav>
|
|
76
73
|
|
|
77
|
-
{/* Stats footer */}
|
|
78
74
|
<div style={{ padding: '12px 16px', borderTop: '1px solid #1F1F1F', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
79
75
|
<div style={{ fontSize: 11, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 2 }}>
|
|
80
76
|
Cluster Stats
|
|
@@ -62,7 +62,6 @@ export const CustomNode = memo(({ data }: NodeProps) => {
|
|
|
62
62
|
<Handle type="target" position={Position.Left} style={{ background: borderColor, width: 5, height: 5, border: 'none' }} />
|
|
63
63
|
<Handle type="source" position={Position.Right} style={{ background: borderColor, width: 5, height: 5, border: 'none' }} />
|
|
64
64
|
|
|
65
|
-
{/* CVE dot */}
|
|
66
65
|
{d.hasCve && (
|
|
67
66
|
<span
|
|
68
67
|
title="Has CVEs"
|
|
@@ -74,17 +73,14 @@ export const CustomNode = memo(({ data }: NodeProps) => {
|
|
|
74
73
|
/>
|
|
75
74
|
)}
|
|
76
75
|
|
|
77
|
-
{/* Type badge */}
|
|
78
76
|
<div style={{ fontSize: 9, color: borderColor, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 3, opacity: 0.9 }}>
|
|
79
77
|
{d.isEntryPoint ? '⤳ ' : ''}{d.isCrownJewel ? '★ ' : ''}{d.nodeType}
|
|
80
78
|
</div>
|
|
81
79
|
|
|
82
|
-
{/* Label */}
|
|
83
80
|
<div style={{ fontSize: 11, color: '#EAEAEA', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
84
81
|
{d.label}
|
|
85
82
|
</div>
|
|
86
83
|
|
|
87
|
-
{/* Risk score */}
|
|
88
84
|
{d.riskScore > 0 && (
|
|
89
85
|
<div style={{
|
|
90
86
|
fontSize: 9,
|
|
@@ -16,7 +16,6 @@ const NODE_W = 180;
|
|
|
16
16
|
const NODE_H = 54;
|
|
17
17
|
const nodeTypes = { k8s: CustomNode };
|
|
18
18
|
|
|
19
|
-
// ─── Dagre layout ─────────────────────────────────────────────────────────────
|
|
20
19
|
function applyLayout(nodes: Node[], edges: Edge[]): Node[] {
|
|
21
20
|
const g = new dagre.graphlib.Graph();
|
|
22
21
|
g.setDefaultEdgeLabel(() => ({}));
|
|
@@ -30,7 +29,6 @@ function applyLayout(nodes: Node[], edges: Edge[]): Node[] {
|
|
|
30
29
|
});
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
// ─── Transform backend data → React Flow ─────────────────────────────────────
|
|
34
32
|
function buildFlowGraph(
|
|
35
33
|
rawNodes: GraphNode[],
|
|
36
34
|
rawEdges: GraphEdge[],
|
|
@@ -87,7 +85,6 @@ function buildFlowGraph(
|
|
|
87
85
|
return { nodes: applyLayout(nodes, edges), edges };
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
// ─── Component ────────────────────────────────────────────────────────────────
|
|
91
88
|
interface Props {
|
|
92
89
|
highlightedNodeIds?: Set<string>;
|
|
93
90
|
highlightedEdgeKeys?: Set<string>;
|
|
@@ -10,7 +10,6 @@ export function CriticalNodeView() {
|
|
|
10
10
|
return (
|
|
11
11
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
12
12
|
|
|
13
|
-
{/* Info panel — full height */}
|
|
14
13
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: 14 }}>
|
|
15
14
|
|
|
16
15
|
{loading['critical'] && (
|
|
@@ -30,7 +29,6 @@ export function CriticalNodeView() {
|
|
|
30
29
|
{top && (
|
|
31
30
|
<div style={{ background: '#121212', border: '1px solid #1F1F1F', borderTop: '2px solid #FF6A00', borderRadius: 12, overflow: 'hidden' }}>
|
|
32
31
|
|
|
33
|
-
{/* Node info */}
|
|
34
32
|
<div style={{ padding: '16px 20px', borderBottom: '1px solid #1F1F1F' }}>
|
|
35
33
|
<div style={{ fontSize: 11, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 10 }}>
|
|
36
34
|
Critical Node
|
|
@@ -53,7 +51,6 @@ export function CriticalNodeView() {
|
|
|
53
51
|
</div>
|
|
54
52
|
</div>
|
|
55
53
|
|
|
56
|
-
{/* Path elimination */}
|
|
57
54
|
{elim && (
|
|
58
55
|
<div style={{ padding: '16px 20px', borderBottom: '1px solid #1F1F1F' }}>
|
|
59
56
|
<div style={{ fontSize: 11, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 12 }}>
|
|
@@ -68,7 +65,6 @@ export function CriticalNodeView() {
|
|
|
68
65
|
</div>
|
|
69
66
|
)}
|
|
70
67
|
|
|
71
|
-
{/* Simulate */}
|
|
72
68
|
<div style={{ padding: '16px 20px' }}>
|
|
73
69
|
<div style={{ fontSize: 11, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 12 }}>
|
|
74
70
|
Simulate Removal
|
|
@@ -6,7 +6,6 @@ export function OverviewView() {
|
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
8
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
9
|
-
{/* Summary cards */}
|
|
10
9
|
{(graphMeta || loading['graph']) && (
|
|
11
10
|
<div style={{ flexShrink: 0, display: 'flex', gap: 10, padding: '14px 16px', borderBottom: '1px solid #1F1F1F' }}>
|
|
12
11
|
{loading['graph']
|
|
@@ -35,14 +34,12 @@ export function OverviewView() {
|
|
|
35
34
|
</div>
|
|
36
35
|
)}
|
|
37
36
|
|
|
38
|
-
{/* Error */}
|
|
39
37
|
{errors['graph'] && (
|
|
40
38
|
<div style={{ flexShrink: 0, margin: '12px 16px 0', padding: '8px 12px', borderLeft: '2px solid #FF3B3B', background: '#FF3B3B10', color: '#FF3B3B', fontSize: 12 }}>
|
|
41
39
|
{errors['graph']}
|
|
42
40
|
</div>
|
|
43
41
|
)}
|
|
44
42
|
|
|
45
|
-
{/* Graph container */}
|
|
46
43
|
<div style={{ flex: 1, minHeight: 0, padding: 12, display: 'flex', flexDirection: 'column' }}>
|
|
47
44
|
<div style={{ flex: 1, minHeight: 0, background: '#121212', border: '1px solid #1F1F1F', borderRadius: 16, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
|
48
45
|
<GraphCanvas />
|
|
@@ -28,7 +28,6 @@ export function PathsView() {
|
|
|
28
28
|
return (
|
|
29
29
|
<div style={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
|
|
30
30
|
|
|
31
|
-
{/* ── Path List (full width) ───────────────────────────────────── */}
|
|
32
31
|
<div style={{
|
|
33
32
|
flex: 1,
|
|
34
33
|
display: 'flex',
|
|
@@ -36,7 +35,6 @@ export function PathsView() {
|
|
|
36
35
|
background: '#0B0B0B',
|
|
37
36
|
overflow: 'hidden',
|
|
38
37
|
}}>
|
|
39
|
-
{/* Header */}
|
|
40
38
|
<div style={{ padding: '16px 20px 14px', borderBottom: '1px solid #1F1F1F', flexShrink: 0 }}>
|
|
41
39
|
<div style={{ fontSize: 14, color: '#EAEAEA', fontWeight: 500, marginBottom: 6 }}>Attack Paths</div>
|
|
42
40
|
{pathsSummary && (
|
|
@@ -49,7 +47,6 @@ export function PathsView() {
|
|
|
49
47
|
)}
|
|
50
48
|
</div>
|
|
51
49
|
|
|
52
|
-
{/* List */}
|
|
53
50
|
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 20px', display: 'flex', flexDirection: 'column', gap: 0, maxWidth: 900, width: '100%', alignSelf: 'center', boxSizing: 'border-box' }}>
|
|
54
51
|
|
|
55
52
|
{loading['paths'] && Array.from({ length: 3 }).map((_, i) => (
|
|
@@ -109,8 +106,6 @@ export function PathsView() {
|
|
|
109
106
|
);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
// ─── PathCard ──────────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
109
|
interface PathCardProps {
|
|
115
110
|
index: number;
|
|
116
111
|
nodes: string[];
|
|
@@ -122,7 +117,6 @@ interface PathCardProps {
|
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel, active }: PathCardProps) {
|
|
125
|
-
// Chunk nodes into rows of max 3 (so arrows don't overflow)
|
|
126
120
|
const COLS = 3;
|
|
127
121
|
const chunks: string[][] = [];
|
|
128
122
|
for (let i = 0; i < nodes.length; i += COLS) {
|
|
@@ -155,7 +149,6 @@ function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel,
|
|
|
155
149
|
}
|
|
156
150
|
}}
|
|
157
151
|
>
|
|
158
|
-
{/* ── Top row: index + risk badge ── */}
|
|
159
152
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
160
153
|
<span style={{
|
|
161
154
|
fontSize: 10,
|
|
@@ -182,7 +175,6 @@ function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel,
|
|
|
182
175
|
</span>
|
|
183
176
|
</div>
|
|
184
177
|
|
|
185
|
-
{/* ── Node chain grid ── */}
|
|
186
178
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 16 }}>
|
|
187
179
|
{chunks.map((chunk, rowIdx) => {
|
|
188
180
|
const globalOffset = rowIdx * COLS;
|
|
@@ -194,7 +186,6 @@ function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel,
|
|
|
194
186
|
const isLast = globalIdx === nodes.length - 1;
|
|
195
187
|
const isArrow = colIdx < chunk.length - 1 || (rowIdx < chunks.length - 1 && colIdx === chunk.length - 1);
|
|
196
188
|
|
|
197
|
-
// determine chip style
|
|
198
189
|
let chipBg = '#1e1e26';
|
|
199
190
|
let chipBorder = '#2e2e3a';
|
|
200
191
|
let chipColor = '#9090a0';
|
|
@@ -259,7 +250,6 @@ function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel,
|
|
|
259
250
|
})}
|
|
260
251
|
</div>
|
|
261
252
|
|
|
262
|
-
{/* ── Divider ── */}
|
|
263
253
|
<div style={{
|
|
264
254
|
height: 1,
|
|
265
255
|
background: 'linear-gradient(90deg, #1a6070, #0a2a35 80%, transparent)',
|
|
@@ -267,7 +257,6 @@ function PathCard({ index, nodes, description, riskScore, riskColor, riskLabel,
|
|
|
267
257
|
borderRadius: 1,
|
|
268
258
|
}} />
|
|
269
259
|
|
|
270
|
-
{/* ── Description ── */}
|
|
271
260
|
<p style={{
|
|
272
261
|
margin: 0,
|
|
273
262
|
fontSize: 12,
|
|
@@ -13,10 +13,9 @@ export function ReportView() {
|
|
|
13
13
|
const downloadPdf = () => {
|
|
14
14
|
if (!reportData?.formatted) return;
|
|
15
15
|
|
|
16
|
-
// ── 1. Sanitise: strip all chars the built-in Courier font can't render ──
|
|
17
16
|
const sanitise = (s: string) =>
|
|
18
17
|
s
|
|
19
|
-
.replace(/[╔╗╚╝║]/g, '')
|
|
18
|
+
.replace(/[╔╗╚╝║]/g, '')
|
|
20
19
|
.replace(/═+/g, (m) => '='.repeat(m.length))
|
|
21
20
|
.replace(/─+/g, (m) => '-'.repeat(m.length))
|
|
22
21
|
.replace(/█/g, '#')
|
|
@@ -24,9 +23,8 @@ export function ReportView() {
|
|
|
24
23
|
.replace(/[→]/g, '->')
|
|
25
24
|
.replace(/[≥]/g, '>=')
|
|
26
25
|
.replace(/[✔]/g, 'OK')
|
|
27
|
-
.replace(/[^\x00-\x7F]/g, '?');
|
|
26
|
+
.replace(/[^\x00-\x7F]/g, '?');
|
|
28
27
|
|
|
29
|
-
// ── 2. Classify each source line ────────────────────────────────────────
|
|
30
28
|
type LineKind = 'blank' | 'divider' | 'title-box' | 'section' | 'path-header' | 'kv' | 'text';
|
|
31
29
|
|
|
32
30
|
interface ParsedLine { kind: LineKind; raw: string; clean: string }
|
|
@@ -48,16 +46,14 @@ export function ReportView() {
|
|
|
48
46
|
return { kind: 'text', raw, clean };
|
|
49
47
|
});
|
|
50
48
|
|
|
51
|
-
// ── 3. Build PDF ─────────────────────────────────────────────────────────
|
|
52
49
|
const doc = new jsPDF({ unit: 'pt', format: 'a4' });
|
|
53
50
|
const pageW = doc.internal.pageSize.getWidth();
|
|
54
51
|
const pageH = doc.internal.pageSize.getHeight();
|
|
55
|
-
const ML = 48;
|
|
56
|
-
const MR = 48;
|
|
52
|
+
const ML = 48;
|
|
53
|
+
const MR = 48;
|
|
57
54
|
const bodyW = pageW - ML - MR;
|
|
58
55
|
const date = new Date().toISOString().slice(0, 10);
|
|
59
56
|
|
|
60
|
-
// colours (RGB)
|
|
61
57
|
const C = {
|
|
62
58
|
bg: [11, 11, 11] as [number,number,number],
|
|
63
59
|
orange: [255, 106, 0] as [number,number,number],
|
|
@@ -72,7 +68,6 @@ export function ReportView() {
|
|
|
72
68
|
let y = 0;
|
|
73
69
|
let pageNum = 1;
|
|
74
70
|
|
|
75
|
-
// ── helpers ──────────────────────────────────────────────────────
|
|
76
71
|
const fill = (c: [number,number,number]) => doc.setFillColor(...c);
|
|
77
72
|
const text = (c: [number,number,number]) => doc.setTextColor(...c);
|
|
78
73
|
const draw = (c: [number,number,number]) => doc.setDrawColor(...c);
|
|
@@ -81,10 +76,8 @@ export function ReportView() {
|
|
|
81
76
|
|
|
82
77
|
const drawPageHeader = () => {
|
|
83
78
|
drawBg();
|
|
84
|
-
// orange top strip
|
|
85
79
|
fill(C.orange);
|
|
86
80
|
doc.rect(0, 0, pageW, 3, 'F');
|
|
87
|
-
// left accent line
|
|
88
81
|
fill(C.orange);
|
|
89
82
|
doc.rect(ML - 12, 24, 2, pageH - 48, 'F');
|
|
90
83
|
};
|
|
@@ -109,31 +102,26 @@ export function ReportView() {
|
|
|
109
102
|
if (y + needed > pageH - 40) newPage();
|
|
110
103
|
};
|
|
111
104
|
|
|
112
|
-
// ── cover page ───────────────────────────────────────────────────
|
|
113
105
|
drawPageHeader();
|
|
114
106
|
y = 52;
|
|
115
107
|
|
|
116
|
-
// Title
|
|
117
108
|
doc.setFont('courier', 'bold');
|
|
118
109
|
doc.setFontSize(22);
|
|
119
110
|
text(C.white);
|
|
120
111
|
doc.text('K8s Attack Path Report', ML, y);
|
|
121
112
|
y += 26;
|
|
122
113
|
|
|
123
|
-
// Subtitle row
|
|
124
114
|
doc.setFont('courier', 'normal');
|
|
125
115
|
doc.setFontSize(8.5);
|
|
126
116
|
text(C.dim);
|
|
127
117
|
doc.text(`Generated ${date} | Kubernetes RBAC Attack Path Visualizer`, ML, y);
|
|
128
118
|
y += 18;
|
|
129
119
|
|
|
130
|
-
// Full-width divider under header
|
|
131
120
|
draw(C.faint);
|
|
132
121
|
doc.setLineWidth(0.4);
|
|
133
122
|
doc.line(ML, y, pageW - MR, y);
|
|
134
123
|
y += 14;
|
|
135
124
|
|
|
136
|
-
// ── render body lines ────────────────────────────────────────────
|
|
137
125
|
for (const pl of parsed) {
|
|
138
126
|
switch (pl.kind) {
|
|
139
127
|
|
|
@@ -151,13 +139,11 @@ export function ReportView() {
|
|
|
151
139
|
}
|
|
152
140
|
|
|
153
141
|
case 'title-box':
|
|
154
|
-
// skip — we already drew a nicer cover header
|
|
155
142
|
break;
|
|
156
143
|
|
|
157
144
|
case 'section': {
|
|
158
145
|
ensureSpace(32);
|
|
159
146
|
y += 6;
|
|
160
|
-
// orange pill background
|
|
161
147
|
const label = pl.clean.trim();
|
|
162
148
|
fill(C.orange);
|
|
163
149
|
doc.roundedRect(ML, y - 10, bodyW, 16, 2, 2, 'F');
|
|
@@ -172,12 +158,10 @@ export function ReportView() {
|
|
|
172
158
|
case 'path-header': {
|
|
173
159
|
ensureSpace(22);
|
|
174
160
|
y += 4;
|
|
175
|
-
// strip the risk bar text, parse score
|
|
176
161
|
const riskMatch = pl.raw.match(/([\d.]+)\/10/);
|
|
177
162
|
const score = riskMatch ? parseFloat(riskMatch[1]) : 0;
|
|
178
163
|
const riskColor = score >= 8 ? C.danger : score >= 5 ? C.orange : [76, 175, 80] as [number,number,number];
|
|
179
164
|
|
|
180
|
-
// label (Path N / Dijkstra N / Cycle N)
|
|
181
165
|
const labelMatch = pl.clean.trim().match(/^(\w+\s+\d+)/);
|
|
182
166
|
const pathLabel = labelMatch ? labelMatch[1] : pl.clean.trim();
|
|
183
167
|
|
|
@@ -186,7 +170,6 @@ export function ReportView() {
|
|
|
186
170
|
text(C.white);
|
|
187
171
|
doc.text(pathLabel, ML, y);
|
|
188
172
|
|
|
189
|
-
// score badge
|
|
190
173
|
if (score > 0) {
|
|
191
174
|
const badge = `${score.toFixed(1)} / 10`;
|
|
192
175
|
doc.setFont('courier', 'bold');
|
|
@@ -194,7 +177,6 @@ export function ReportView() {
|
|
|
194
177
|
text(riskColor);
|
|
195
178
|
doc.text(badge, pageW - MR, y, { align: 'right' });
|
|
196
179
|
|
|
197
|
-
// mini bar: 10 segments
|
|
198
180
|
const barW = 60;
|
|
199
181
|
const barH = 4;
|
|
200
182
|
const barX = pageW - MR - barW - 48;
|
|
@@ -212,7 +194,6 @@ export function ReportView() {
|
|
|
212
194
|
|
|
213
195
|
case 'kv': {
|
|
214
196
|
ensureSpace(14);
|
|
215
|
-
// split " Key : value" into key + value parts
|
|
216
197
|
const colonIdx = pl.clean.indexOf(':');
|
|
217
198
|
const key = colonIdx > -1 ? pl.clean.slice(0, colonIdx + 1).trim() : pl.clean.trim();
|
|
218
199
|
const value = colonIdx > -1 ? pl.clean.slice(colonIdx + 1).trim() : '';
|
|
@@ -220,11 +201,9 @@ export function ReportView() {
|
|
|
220
201
|
doc.setFont('courier', 'normal');
|
|
221
202
|
doc.setFontSize(8);
|
|
222
203
|
|
|
223
|
-
// key
|
|
224
204
|
text(C.key);
|
|
225
205
|
doc.text(key, ML + 8, y);
|
|
226
206
|
|
|
227
|
-
// value (wrap if long)
|
|
228
207
|
text(C.muted);
|
|
229
208
|
const keyWidth = doc.getTextWidth(key + ' ');
|
|
230
209
|
const valueLines = doc.splitTextToSize(value, bodyW - keyWidth - 8) as string[];
|
|
@@ -239,7 +218,6 @@ export function ReportView() {
|
|
|
239
218
|
}
|
|
240
219
|
|
|
241
220
|
default: {
|
|
242
|
-
// generic text — wrap to page width
|
|
243
221
|
ensureSpace(13);
|
|
244
222
|
doc.setFont('courier', 'normal');
|
|
245
223
|
doc.setFontSize(8);
|
|
@@ -262,7 +240,6 @@ export function ReportView() {
|
|
|
262
240
|
return (
|
|
263
241
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', padding: 14, gap: 12 }}>
|
|
264
242
|
|
|
265
|
-
{/* Header box */}
|
|
266
243
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
|
|
267
244
|
<div>
|
|
268
245
|
<div style={{ fontSize: 9, color: '#555555', textTransform: 'uppercase', letterSpacing: '0.12em' }}>Attack Report</div>
|
|
@@ -275,7 +252,6 @@ export function ReportView() {
|
|
|
275
252
|
)}
|
|
276
253
|
</div>
|
|
277
254
|
|
|
278
|
-
{/* Content box */}
|
|
279
255
|
<div
|
|
280
256
|
style={{
|
|
281
257
|
flex: 1,
|
|
@@ -14,10 +14,8 @@ export function VulnerabilitiesView() {
|
|
|
14
14
|
return (
|
|
15
15
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
16
16
|
|
|
17
|
-
{/* Table panel — fills full height */}
|
|
18
17
|
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
19
18
|
|
|
20
|
-
{/* Summary bar */}
|
|
21
19
|
{vulnSummary && (
|
|
22
20
|
<div style={{ display: 'flex', gap: 20, padding: '10px 16px', borderBottom: '1px solid #1F1F1F' }}>
|
|
23
21
|
<span style={{ fontSize: 13, color: '#555555' }}>
|
|
@@ -41,7 +39,6 @@ export function VulnerabilitiesView() {
|
|
|
41
39
|
</div>
|
|
42
40
|
)}
|
|
43
41
|
|
|
44
|
-
{/* Loading */}
|
|
45
42
|
{loading['vulns'] && (
|
|
46
43
|
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
47
44
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
@@ -50,19 +47,16 @@ export function VulnerabilitiesView() {
|
|
|
50
47
|
</div>
|
|
51
48
|
)}
|
|
52
49
|
|
|
53
|
-
{/* Error */}
|
|
54
50
|
{errors['vulns'] && (
|
|
55
51
|
<div style={{ margin: 12, padding: '8px 12px', borderLeft: '2px solid #FF3B3B', background: '#FF3B3B10', color: '#FF3B3B', fontSize: 12 }}>
|
|
56
52
|
{errors['vulns']}
|
|
57
53
|
</div>
|
|
58
54
|
)}
|
|
59
55
|
|
|
60
|
-
{/* Empty */}
|
|
61
56
|
{!loading['vulns'] && vulnerabilities.length === 0 && !errors['vulns'] && (
|
|
62
57
|
<div style={{ padding: 16, textAlign: 'center', fontSize: 12, color: '#555555' }}>No vulnerabilities above threshold.</div>
|
|
63
58
|
)}
|
|
64
59
|
|
|
65
|
-
{/* Table */}
|
|
66
60
|
{vulnerabilities.length > 0 && (
|
|
67
61
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
|
68
62
|
<thead>
|