@vulcn/plugin-report 0.1.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.
- package/dist/index.cjs +1100 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +1062 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// src/html.ts
|
|
7
|
+
var COLORS = {
|
|
8
|
+
bg: "#0a0a0f",
|
|
9
|
+
surface: "#12121a",
|
|
10
|
+
surfaceHover: "#1a1a26",
|
|
11
|
+
border: "#1e1e2e",
|
|
12
|
+
borderActive: "#2a2a3e",
|
|
13
|
+
text: "#e4e4ef",
|
|
14
|
+
textMuted: "#8888a0",
|
|
15
|
+
textDim: "#555570",
|
|
16
|
+
accent: "#fa1b1b",
|
|
17
|
+
accentGlow: "rgba(250, 27, 27, 0.15)",
|
|
18
|
+
accentLight: "#ff9c9c",
|
|
19
|
+
critical: "#ff1744",
|
|
20
|
+
high: "#ff5252",
|
|
21
|
+
medium: "#ffab40",
|
|
22
|
+
low: "#66bb6a",
|
|
23
|
+
info: "#42a5f5",
|
|
24
|
+
success: "#00e676"
|
|
25
|
+
};
|
|
26
|
+
function severityColor(severity) {
|
|
27
|
+
switch (severity) {
|
|
28
|
+
case "critical":
|
|
29
|
+
return COLORS.critical;
|
|
30
|
+
case "high":
|
|
31
|
+
return COLORS.high;
|
|
32
|
+
case "medium":
|
|
33
|
+
return COLORS.medium;
|
|
34
|
+
case "low":
|
|
35
|
+
return COLORS.low;
|
|
36
|
+
case "info":
|
|
37
|
+
return COLORS.info;
|
|
38
|
+
default:
|
|
39
|
+
return COLORS.textMuted;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function severityOrder(severity) {
|
|
43
|
+
switch (severity) {
|
|
44
|
+
case "critical":
|
|
45
|
+
return 0;
|
|
46
|
+
case "high":
|
|
47
|
+
return 1;
|
|
48
|
+
case "medium":
|
|
49
|
+
return 2;
|
|
50
|
+
case "low":
|
|
51
|
+
return 3;
|
|
52
|
+
case "info":
|
|
53
|
+
return 4;
|
|
54
|
+
default:
|
|
55
|
+
return 5;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function escapeHtml(str) {
|
|
59
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
60
|
+
}
|
|
61
|
+
function formatDuration(ms) {
|
|
62
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
63
|
+
const seconds = (ms / 1e3).toFixed(1);
|
|
64
|
+
return `${seconds}s`;
|
|
65
|
+
}
|
|
66
|
+
function formatDate(iso) {
|
|
67
|
+
const d = new Date(iso);
|
|
68
|
+
return d.toLocaleDateString("en-US", {
|
|
69
|
+
year: "numeric",
|
|
70
|
+
month: "long",
|
|
71
|
+
day: "numeric",
|
|
72
|
+
hour: "2-digit",
|
|
73
|
+
minute: "2-digit",
|
|
74
|
+
timeZoneName: "short"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
var VULCN_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="32" height="32">
|
|
78
|
+
<defs>
|
|
79
|
+
<linearGradient id="lg1" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(7 -13 13 7 7 17)" gradientUnits="userSpaceOnUse">
|
|
80
|
+
<stop offset="0" stop-color="#fa1b1b"/>
|
|
81
|
+
<stop offset="1" stop-color="#ff9c9c"/>
|
|
82
|
+
</linearGradient>
|
|
83
|
+
<linearGradient id="lg2" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(3 -6 6 3 13 14)" gradientUnits="userSpaceOnUse">
|
|
84
|
+
<stop offset="0" stop-color="#ff9c9c"/>
|
|
85
|
+
<stop offset="1" stop-color="#ffffff"/>
|
|
86
|
+
</linearGradient>
|
|
87
|
+
</defs>
|
|
88
|
+
<path fill="url(#lg1)" d="m 11,17 c 0,0.552 -0.448,1 -1,1 -0.552,0 -1,-0.448 -1,-1 0,-0.552 0.448,-1 1,-1 0.552,0 1,0.448 1,1 z M 10,15 C 8,15 7.839,16.622 7.803,16.68 7.51,17.147 6.892,17.288 6.425,16.995 3.592,15.216 2.389,11.366 2.014,9.168 1.977,8.951 1.952,8.743 1.936,8.547 1.936,8.544 1.935,8.541 1.935,8.538 1.844,7.291 2.572,6.13 3.733,5.667 3.736,5.666 3.738,5.665 3.74,5.664 4.948,5.193 5.913,4.705 6.583,3.641 6.586,3.636 6.588,3.632 6.591,3.628 7.235,2.637 8.332,2.035 9.506,2.023 9.817,2.001 10.141,2 10.451,2 c 0,0 0,0 0,0 1.202,0 2.322,0.608 2.977,1.616 0.005,0.008 0.01,0.017 0.015,0.025 0.651,1.07 1.614,1.554 2.817,2.022 0.002,0 0.005,10e-4 0.007,0.002 1.162,0.463 1.89,1.626 1.799,2.873 0,0.006 -10e-4,0.012 -10e-4,0.018 -0.018,0.193 -0.043,0.397 -0.079,0.612 -0.375,2.198 -1.578,6.048 -4.411,7.827 C 13.108,17.288 12.49,17.147 12.197,16.68 12.161,16.622 12,15 10,15 Z"/>
|
|
89
|
+
<path fill="#dc2626" d="m 13.0058,9.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z"/>
|
|
90
|
+
<path fill="url(#lg2)" d="m 14.0058,8.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z"/>
|
|
91
|
+
</svg>`;
|
|
92
|
+
function generateHtml(data) {
|
|
93
|
+
const { session, result, generatedAt, engineVersion } = data;
|
|
94
|
+
const findings = [...result.findings].sort(
|
|
95
|
+
(a, b) => severityOrder(a.severity) - severityOrder(b.severity)
|
|
96
|
+
);
|
|
97
|
+
const counts = {
|
|
98
|
+
critical: 0,
|
|
99
|
+
high: 0,
|
|
100
|
+
medium: 0,
|
|
101
|
+
low: 0,
|
|
102
|
+
info: 0
|
|
103
|
+
};
|
|
104
|
+
for (const f of findings) {
|
|
105
|
+
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
const totalFindings = findings.length;
|
|
108
|
+
const hasFindings = totalFindings > 0;
|
|
109
|
+
const riskScore = counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;
|
|
110
|
+
const maxRisk = totalFindings * 10 || 1;
|
|
111
|
+
const riskPercent = Math.min(100, Math.round(riskScore / maxRisk * 100));
|
|
112
|
+
const riskLabel = riskPercent >= 80 ? "Critical" : riskPercent >= 50 ? "High" : riskPercent >= 25 ? "Medium" : riskPercent > 0 ? "Low" : "Clear";
|
|
113
|
+
const riskColor = riskPercent >= 80 ? COLORS.critical : riskPercent >= 50 ? COLORS.high : riskPercent >= 25 ? COLORS.medium : riskPercent > 0 ? COLORS.low : COLORS.success;
|
|
114
|
+
const donutSvg = generateDonut(counts, totalFindings);
|
|
115
|
+
const affectedUrls = [...new Set(findings.map((f) => f.url))];
|
|
116
|
+
const vulnTypes = [...new Set(findings.map((f) => f.type))];
|
|
117
|
+
return `<!DOCTYPE html>
|
|
118
|
+
<html lang="en">
|
|
119
|
+
<head>
|
|
120
|
+
<meta charset="UTF-8">
|
|
121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
122
|
+
<title>Vulcn Security Report \u2014 ${escapeHtml(session.name)}</title>
|
|
123
|
+
<style>
|
|
124
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
125
|
+
|
|
126
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
127
|
+
|
|
128
|
+
:root {
|
|
129
|
+
--bg: ${COLORS.bg};
|
|
130
|
+
--surface: ${COLORS.surface};
|
|
131
|
+
--surface-hover: ${COLORS.surfaceHover};
|
|
132
|
+
--border: ${COLORS.border};
|
|
133
|
+
--border-active: ${COLORS.borderActive};
|
|
134
|
+
--text: ${COLORS.text};
|
|
135
|
+
--text-muted: ${COLORS.textMuted};
|
|
136
|
+
--text-dim: ${COLORS.textDim};
|
|
137
|
+
--accent: ${COLORS.accent};
|
|
138
|
+
--accent-glow: ${COLORS.accentGlow};
|
|
139
|
+
--accent-light: ${COLORS.accentLight};
|
|
140
|
+
--radius: 12px;
|
|
141
|
+
--radius-sm: 8px;
|
|
142
|
+
--radius-xs: 6px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
body {
|
|
146
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
147
|
+
background: var(--bg);
|
|
148
|
+
color: var(--text);
|
|
149
|
+
line-height: 1.6;
|
|
150
|
+
min-height: 100vh;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Ambient gradient background */
|
|
154
|
+
body::before {
|
|
155
|
+
content: '';
|
|
156
|
+
position: fixed;
|
|
157
|
+
top: 0;
|
|
158
|
+
left: 0;
|
|
159
|
+
right: 0;
|
|
160
|
+
height: 600px;
|
|
161
|
+
background: radial-gradient(ellipse 80% 50% at 50% -20%, ${COLORS.accentGlow} 0%, transparent 100%);
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
z-index: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.container {
|
|
167
|
+
max-width: 1100px;
|
|
168
|
+
margin: 0 auto;
|
|
169
|
+
padding: 40px 24px;
|
|
170
|
+
position: relative;
|
|
171
|
+
z-index: 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Header */
|
|
175
|
+
.header {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: space-between;
|
|
179
|
+
margin-bottom: 48px;
|
|
180
|
+
padding-bottom: 24px;
|
|
181
|
+
border-bottom: 1px solid var(--border);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.header-brand {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 12px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.header-brand svg {
|
|
191
|
+
filter: drop-shadow(0 0 8px rgba(250, 27, 27, 0.3));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.header-brand h1 {
|
|
195
|
+
font-size: 20px;
|
|
196
|
+
font-weight: 700;
|
|
197
|
+
letter-spacing: -0.02em;
|
|
198
|
+
background: linear-gradient(135deg, #fa1b1b, #ff9c9c);
|
|
199
|
+
-webkit-background-clip: text;
|
|
200
|
+
-webkit-text-fill-color: transparent;
|
|
201
|
+
background-clip: text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.header-brand span {
|
|
205
|
+
font-size: 11px;
|
|
206
|
+
font-weight: 500;
|
|
207
|
+
color: var(--text-dim);
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
letter-spacing: 0.1em;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.header-meta {
|
|
213
|
+
text-align: right;
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
color: var(--text-dim);
|
|
216
|
+
line-height: 1.8;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Session info */
|
|
220
|
+
.session-info {
|
|
221
|
+
background: var(--surface);
|
|
222
|
+
border: 1px solid var(--border);
|
|
223
|
+
border-radius: var(--radius);
|
|
224
|
+
padding: 24px;
|
|
225
|
+
margin-bottom: 32px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.session-info h2 {
|
|
229
|
+
font-size: 22px;
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
margin-bottom: 16px;
|
|
232
|
+
letter-spacing: -0.02em;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.session-meta {
|
|
236
|
+
display: grid;
|
|
237
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
238
|
+
gap: 16px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.meta-item {
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: column;
|
|
244
|
+
gap: 4px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.meta-label {
|
|
248
|
+
font-size: 11px;
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
text-transform: uppercase;
|
|
251
|
+
letter-spacing: 0.08em;
|
|
252
|
+
color: var(--text-dim);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.meta-value {
|
|
256
|
+
font-size: 14px;
|
|
257
|
+
font-weight: 500;
|
|
258
|
+
color: var(--text);
|
|
259
|
+
font-family: 'JetBrains Mono', monospace;
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Stats grid */
|
|
264
|
+
.stats-grid {
|
|
265
|
+
display: grid;
|
|
266
|
+
grid-template-columns: 1fr 1.5fr;
|
|
267
|
+
gap: 24px;
|
|
268
|
+
margin-bottom: 32px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@media (max-width: 768px) {
|
|
272
|
+
.stats-grid { grid-template-columns: 1fr; }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Risk gauge */
|
|
276
|
+
.risk-card {
|
|
277
|
+
background: var(--surface);
|
|
278
|
+
border: 1px solid var(--border);
|
|
279
|
+
border-radius: var(--radius);
|
|
280
|
+
padding: 32px;
|
|
281
|
+
display: flex;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 20px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.risk-card h3 {
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
text-transform: uppercase;
|
|
291
|
+
letter-spacing: 0.08em;
|
|
292
|
+
color: var(--text-dim);
|
|
293
|
+
width: 100%;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.risk-gauge {
|
|
297
|
+
position: relative;
|
|
298
|
+
width: 160px;
|
|
299
|
+
height: 160px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.risk-gauge svg {
|
|
303
|
+
transform: rotate(-90deg);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.risk-gauge-label {
|
|
307
|
+
position: absolute;
|
|
308
|
+
top: 50%;
|
|
309
|
+
left: 50%;
|
|
310
|
+
transform: translate(-50%, -50%);
|
|
311
|
+
text-align: center;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.risk-gauge-label .score {
|
|
315
|
+
font-size: 36px;
|
|
316
|
+
font-weight: 800;
|
|
317
|
+
letter-spacing: -0.03em;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.risk-gauge-label .label {
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
text-transform: uppercase;
|
|
324
|
+
letter-spacing: 0.08em;
|
|
325
|
+
color: var(--text-muted);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Summary card */
|
|
329
|
+
.summary-card {
|
|
330
|
+
background: var(--surface);
|
|
331
|
+
border: 1px solid var(--border);
|
|
332
|
+
border-radius: var(--radius);
|
|
333
|
+
padding: 32px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.summary-card h3 {
|
|
337
|
+
font-size: 13px;
|
|
338
|
+
font-weight: 600;
|
|
339
|
+
text-transform: uppercase;
|
|
340
|
+
letter-spacing: 0.08em;
|
|
341
|
+
color: var(--text-dim);
|
|
342
|
+
margin-bottom: 20px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.summary-stats {
|
|
346
|
+
display: grid;
|
|
347
|
+
grid-template-columns: repeat(2, 1fr);
|
|
348
|
+
gap: 20px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.stat-box {
|
|
352
|
+
padding: 16px;
|
|
353
|
+
background: rgba(255,255,255,0.02);
|
|
354
|
+
border: 1px solid var(--border);
|
|
355
|
+
border-radius: var(--radius-sm);
|
|
356
|
+
transition: border-color 0.2s;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.stat-box:hover { border-color: var(--border-active); }
|
|
360
|
+
|
|
361
|
+
.stat-number {
|
|
362
|
+
font-size: 28px;
|
|
363
|
+
font-weight: 800;
|
|
364
|
+
letter-spacing: -0.03em;
|
|
365
|
+
line-height: 1;
|
|
366
|
+
margin-bottom: 4px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.stat-label {
|
|
370
|
+
font-size: 12px;
|
|
371
|
+
font-weight: 500;
|
|
372
|
+
color: var(--text-muted);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Severity breakdown */
|
|
376
|
+
.severity-breakdown {
|
|
377
|
+
margin-bottom: 32px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.severity-section-header {
|
|
381
|
+
display: flex;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: 12px;
|
|
384
|
+
margin-bottom: 16px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.severity-section-header h3 {
|
|
388
|
+
font-size: 13px;
|
|
389
|
+
font-weight: 600;
|
|
390
|
+
text-transform: uppercase;
|
|
391
|
+
letter-spacing: 0.08em;
|
|
392
|
+
color: var(--text-dim);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.severity-bars {
|
|
396
|
+
display: flex;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
background: var(--surface);
|
|
399
|
+
border: 1px solid var(--border);
|
|
400
|
+
border-radius: var(--radius);
|
|
401
|
+
padding: 20px 24px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.severity-bar-item {
|
|
405
|
+
flex: 1;
|
|
406
|
+
display: flex;
|
|
407
|
+
flex-direction: column;
|
|
408
|
+
gap: 8px;
|
|
409
|
+
align-items: center;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.severity-bar-track {
|
|
413
|
+
width: 100%;
|
|
414
|
+
height: 6px;
|
|
415
|
+
background: rgba(255,255,255,0.04);
|
|
416
|
+
border-radius: 3px;
|
|
417
|
+
overflow: hidden;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.severity-bar-fill {
|
|
421
|
+
height: 100%;
|
|
422
|
+
border-radius: 3px;
|
|
423
|
+
transition: width 0.5s ease;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.severity-bar-label {
|
|
427
|
+
font-size: 10px;
|
|
428
|
+
font-weight: 600;
|
|
429
|
+
text-transform: uppercase;
|
|
430
|
+
letter-spacing: 0.06em;
|
|
431
|
+
color: var(--text-dim);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.severity-bar-count {
|
|
435
|
+
font-size: 18px;
|
|
436
|
+
font-weight: 700;
|
|
437
|
+
font-family: 'JetBrains Mono', monospace;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* Findings section */
|
|
441
|
+
.findings-section {
|
|
442
|
+
margin-bottom: 32px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.findings-header {
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
justify-content: space-between;
|
|
449
|
+
margin-bottom: 16px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.findings-header h3 {
|
|
453
|
+
font-size: 18px;
|
|
454
|
+
font-weight: 700;
|
|
455
|
+
letter-spacing: -0.01em;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.findings-count {
|
|
459
|
+
font-size: 12px;
|
|
460
|
+
font-weight: 600;
|
|
461
|
+
color: var(--text-dim);
|
|
462
|
+
padding: 4px 12px;
|
|
463
|
+
background: var(--surface);
|
|
464
|
+
border: 1px solid var(--border);
|
|
465
|
+
border-radius: 100px;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* Finding card */
|
|
469
|
+
.finding-card {
|
|
470
|
+
background: var(--surface);
|
|
471
|
+
border: 1px solid var(--border);
|
|
472
|
+
border-radius: var(--radius);
|
|
473
|
+
margin-bottom: 12px;
|
|
474
|
+
overflow: hidden;
|
|
475
|
+
transition: border-color 0.2s;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.finding-card:hover { border-color: var(--border-active); }
|
|
479
|
+
|
|
480
|
+
.finding-header {
|
|
481
|
+
padding: 20px 24px;
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: flex-start;
|
|
484
|
+
gap: 16px;
|
|
485
|
+
cursor: pointer;
|
|
486
|
+
user-select: none;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.finding-severity-dot {
|
|
490
|
+
width: 10px;
|
|
491
|
+
height: 10px;
|
|
492
|
+
border-radius: 50%;
|
|
493
|
+
flex-shrink: 0;
|
|
494
|
+
margin-top: 6px;
|
|
495
|
+
box-shadow: 0 0 8px currentColor;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.finding-info {
|
|
499
|
+
flex: 1;
|
|
500
|
+
min-width: 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.finding-title {
|
|
504
|
+
font-size: 15px;
|
|
505
|
+
font-weight: 600;
|
|
506
|
+
margin-bottom: 4px;
|
|
507
|
+
letter-spacing: -0.01em;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.finding-subtitle {
|
|
511
|
+
font-size: 12px;
|
|
512
|
+
color: var(--text-muted);
|
|
513
|
+
display: flex;
|
|
514
|
+
gap: 16px;
|
|
515
|
+
flex-wrap: wrap;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.finding-tag {
|
|
519
|
+
display: inline-flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
gap: 4px;
|
|
522
|
+
font-family: 'JetBrains Mono', monospace;
|
|
523
|
+
font-size: 11px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.finding-expand-icon {
|
|
527
|
+
font-size: 18px;
|
|
528
|
+
color: var(--text-dim);
|
|
529
|
+
transition: transform 0.2s;
|
|
530
|
+
flex-shrink: 0;
|
|
531
|
+
margin-top: 2px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.finding-card.open .finding-expand-icon {
|
|
535
|
+
transform: rotate(180deg);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.finding-details {
|
|
539
|
+
display: none;
|
|
540
|
+
padding: 0 24px 20px;
|
|
541
|
+
border-top: 1px solid var(--border);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.finding-card.open .finding-details {
|
|
545
|
+
display: block;
|
|
546
|
+
padding-top: 20px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.detail-row {
|
|
550
|
+
display: grid;
|
|
551
|
+
grid-template-columns: 120px 1fr;
|
|
552
|
+
gap: 8px;
|
|
553
|
+
margin-bottom: 12px;
|
|
554
|
+
align-items: baseline;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.detail-label {
|
|
558
|
+
font-size: 11px;
|
|
559
|
+
font-weight: 600;
|
|
560
|
+
text-transform: uppercase;
|
|
561
|
+
letter-spacing: 0.06em;
|
|
562
|
+
color: var(--text-dim);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.detail-value {
|
|
566
|
+
font-size: 13px;
|
|
567
|
+
color: var(--text);
|
|
568
|
+
word-break: break-all;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.evidence-box {
|
|
572
|
+
background: rgba(255,255,255,0.02);
|
|
573
|
+
border: 1px solid var(--border);
|
|
574
|
+
border-radius: var(--radius-xs);
|
|
575
|
+
padding: 12px 16px;
|
|
576
|
+
font-family: 'JetBrains Mono', monospace;
|
|
577
|
+
font-size: 12px;
|
|
578
|
+
color: var(--text-muted);
|
|
579
|
+
line-height: 1.5;
|
|
580
|
+
overflow-x: auto;
|
|
581
|
+
white-space: pre-wrap;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.payload-box {
|
|
585
|
+
background: rgba(250, 27, 27, 0.06);
|
|
586
|
+
border: 1px solid rgba(250, 27, 27, 0.15);
|
|
587
|
+
border-radius: var(--radius-xs);
|
|
588
|
+
padding: 8px 12px;
|
|
589
|
+
font-family: 'JetBrains Mono', monospace;
|
|
590
|
+
font-size: 12px;
|
|
591
|
+
color: var(--accent-light);
|
|
592
|
+
word-break: break-all;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/* No findings */
|
|
596
|
+
.no-findings {
|
|
597
|
+
text-align: center;
|
|
598
|
+
padding: 60px 24px;
|
|
599
|
+
background: var(--surface);
|
|
600
|
+
border: 1px solid var(--border);
|
|
601
|
+
border-radius: var(--radius);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.no-findings .icon { font-size: 48px; margin-bottom: 16px; }
|
|
605
|
+
.no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }
|
|
606
|
+
.no-findings p { font-size: 14px; color: var(--text-muted); }
|
|
607
|
+
|
|
608
|
+
/* Errors section */
|
|
609
|
+
.errors-section {
|
|
610
|
+
margin-bottom: 32px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.errors-section h3 {
|
|
614
|
+
font-size: 14px;
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
color: var(--text-muted);
|
|
617
|
+
margin-bottom: 12px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.error-item {
|
|
621
|
+
padding: 10px 16px;
|
|
622
|
+
background: rgba(255, 171, 64, 0.04);
|
|
623
|
+
border: 1px solid rgba(255, 171, 64, 0.1);
|
|
624
|
+
border-radius: var(--radius-xs);
|
|
625
|
+
font-family: 'JetBrains Mono', monospace;
|
|
626
|
+
font-size: 12px;
|
|
627
|
+
color: ${COLORS.medium};
|
|
628
|
+
margin-bottom: 6px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* Footer */
|
|
632
|
+
.footer {
|
|
633
|
+
text-align: center;
|
|
634
|
+
padding: 32px 0;
|
|
635
|
+
border-top: 1px solid var(--border);
|
|
636
|
+
margin-top: 48px;
|
|
637
|
+
color: var(--text-dim);
|
|
638
|
+
font-size: 12px;
|
|
639
|
+
display: flex;
|
|
640
|
+
flex-direction: column;
|
|
641
|
+
align-items: center;
|
|
642
|
+
gap: 8px;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.footer a {
|
|
646
|
+
color: var(--accent-light);
|
|
647
|
+
text-decoration: none;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.footer a:hover { text-decoration: underline; }
|
|
651
|
+
|
|
652
|
+
/* Animations */
|
|
653
|
+
@keyframes fadeIn {
|
|
654
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
655
|
+
to { opacity: 1; transform: translateY(0); }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.animate-in {
|
|
659
|
+
animation: fadeIn 0.4s ease-out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.animate-in-delay { animation: fadeIn 0.4s ease-out 0.1s both; }
|
|
663
|
+
.animate-in-delay-2 { animation: fadeIn 0.4s ease-out 0.2s both; }
|
|
664
|
+
.animate-in-delay-3 { animation: fadeIn 0.4s ease-out 0.3s both; }
|
|
665
|
+
|
|
666
|
+
/* Print styles */
|
|
667
|
+
@media print {
|
|
668
|
+
body { background: white; color: #111; }
|
|
669
|
+
body::before { display: none; }
|
|
670
|
+
.finding-details { display: block !important; padding-top: 12px !important; }
|
|
671
|
+
.finding-card { page-break-inside: avoid; }
|
|
672
|
+
}
|
|
673
|
+
</style>
|
|
674
|
+
</head>
|
|
675
|
+
<body>
|
|
676
|
+
<div class="container">
|
|
677
|
+
<!-- Header -->
|
|
678
|
+
<div class="header animate-in">
|
|
679
|
+
<div class="header-brand">
|
|
680
|
+
${VULCN_LOGO_SVG}
|
|
681
|
+
<div>
|
|
682
|
+
<h1>vulcn</h1>
|
|
683
|
+
<span>Security Report</span>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
<div class="header-meta">
|
|
687
|
+
<div>${formatDate(generatedAt)}</div>
|
|
688
|
+
<div>Engine v${escapeHtml(engineVersion)}</div>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<!-- Session info -->
|
|
693
|
+
<div class="session-info animate-in-delay">
|
|
694
|
+
<h2>${escapeHtml(session.name)}</h2>
|
|
695
|
+
<div class="session-meta">
|
|
696
|
+
<div class="meta-item">
|
|
697
|
+
<span class="meta-label">Driver</span>
|
|
698
|
+
<span class="meta-value">${escapeHtml(session.driver)}</span>
|
|
699
|
+
</div>
|
|
700
|
+
${session.driverConfig?.startUrl ? `<div class="meta-item"><span class="meta-label">Target URL</span><span class="meta-value">${escapeHtml(String(session.driverConfig.startUrl))}</span></div>` : ""}
|
|
701
|
+
<div class="meta-item">
|
|
702
|
+
<span class="meta-label">Duration</span>
|
|
703
|
+
<span class="meta-value">${formatDuration(result.duration)}</span>
|
|
704
|
+
</div>
|
|
705
|
+
<div class="meta-item">
|
|
706
|
+
<span class="meta-label">Generated</span>
|
|
707
|
+
<span class="meta-value">${formatDate(generatedAt)}</span>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
<!-- Stats grid: Risk + Summary -->
|
|
713
|
+
<div class="stats-grid animate-in-delay-2">
|
|
714
|
+
<div class="risk-card">
|
|
715
|
+
<h3>Risk Level</h3>
|
|
716
|
+
<div class="risk-gauge">
|
|
717
|
+
<svg viewBox="0 0 160 160" width="160" height="160">
|
|
718
|
+
<circle cx="80" cy="80" r="68" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="10"/>
|
|
719
|
+
<circle cx="80" cy="80" r="68" fill="none" stroke="${riskColor}" stroke-width="10"
|
|
720
|
+
stroke-dasharray="${riskPercent / 100 * 427} 427"
|
|
721
|
+
stroke-linecap="round"
|
|
722
|
+
style="filter: drop-shadow(0 0 6px ${riskColor});"/>
|
|
723
|
+
</svg>
|
|
724
|
+
<div class="risk-gauge-label">
|
|
725
|
+
<div class="score" style="color: ${riskColor}">${hasFindings ? riskPercent : 0}</div>
|
|
726
|
+
<div class="label">${riskLabel}</div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
|
|
731
|
+
<div class="summary-card">
|
|
732
|
+
<h3>Execution Summary</h3>
|
|
733
|
+
<div class="summary-stats">
|
|
734
|
+
<div class="stat-box">
|
|
735
|
+
<div class="stat-number" style="color: ${hasFindings ? COLORS.high : COLORS.success}">${totalFindings}</div>
|
|
736
|
+
<div class="stat-label">Findings</div>
|
|
737
|
+
</div>
|
|
738
|
+
<div class="stat-box">
|
|
739
|
+
<div class="stat-number">${result.payloadsTested}</div>
|
|
740
|
+
<div class="stat-label">Payloads Tested</div>
|
|
741
|
+
</div>
|
|
742
|
+
<div class="stat-box">
|
|
743
|
+
<div class="stat-number">${result.stepsExecuted}</div>
|
|
744
|
+
<div class="stat-label">Steps Executed</div>
|
|
745
|
+
</div>
|
|
746
|
+
<div class="stat-box">
|
|
747
|
+
<div class="stat-number">${affectedUrls.length}</div>
|
|
748
|
+
<div class="stat-label">URLs Affected</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
|
|
754
|
+
<!-- Severity breakdown -->
|
|
755
|
+
<div class="severity-breakdown animate-in-delay-2">
|
|
756
|
+
<div class="severity-bars">
|
|
757
|
+
${["critical", "high", "medium", "low", "info"].map(
|
|
758
|
+
(sev) => `
|
|
759
|
+
<div class="severity-bar-item">
|
|
760
|
+
<div class="severity-bar-count" style="color: ${severityColor(sev)}">${counts[sev]}</div>
|
|
761
|
+
<div class="severity-bar-track">
|
|
762
|
+
<div class="severity-bar-fill" style="width: ${totalFindings ? counts[sev] / totalFindings * 100 : 0}%; background: ${severityColor(sev)};"></div>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="severity-bar-label">${sev}</div>
|
|
765
|
+
</div>
|
|
766
|
+
`
|
|
767
|
+
).join("")}
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<!-- Findings -->
|
|
772
|
+
<div class="findings-section animate-in-delay-3">
|
|
773
|
+
<div class="findings-header">
|
|
774
|
+
<h3>Findings</h3>
|
|
775
|
+
<span class="findings-count">${totalFindings} total</span>
|
|
776
|
+
</div>
|
|
777
|
+
|
|
778
|
+
${hasFindings ? findings.map(
|
|
779
|
+
(f, i) => `
|
|
780
|
+
<div class="finding-card" onclick="this.classList.toggle('open')">
|
|
781
|
+
<div class="finding-header">
|
|
782
|
+
<div class="finding-severity-dot" style="color: ${severityColor(f.severity)}; background: ${severityColor(f.severity)};"></div>
|
|
783
|
+
<div class="finding-info">
|
|
784
|
+
<div class="finding-title">${escapeHtml(f.title)}</div>
|
|
785
|
+
<div class="finding-subtitle">
|
|
786
|
+
<span class="finding-tag" style="color: ${severityColor(f.severity)}">${f.severity.toUpperCase()}</span>
|
|
787
|
+
<span class="finding-tag">${escapeHtml(f.type)}</span>
|
|
788
|
+
<span class="finding-tag">${escapeHtml(f.stepId)}</span>
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
<span class="finding-expand-icon">\u25BE</span>
|
|
792
|
+
</div>
|
|
793
|
+
<div class="finding-details">
|
|
794
|
+
<div class="detail-row">
|
|
795
|
+
<span class="detail-label">Description</span>
|
|
796
|
+
<span class="detail-value">${escapeHtml(f.description)}</span>
|
|
797
|
+
</div>
|
|
798
|
+
<div class="detail-row">
|
|
799
|
+
<span class="detail-label">URL</span>
|
|
800
|
+
<span class="detail-value">${escapeHtml(f.url)}</span>
|
|
801
|
+
</div>
|
|
802
|
+
<div class="detail-row">
|
|
803
|
+
<span class="detail-label">Payload</span>
|
|
804
|
+
<div class="payload-box">${escapeHtml(f.payload)}</div>
|
|
805
|
+
</div>
|
|
806
|
+
${f.evidence ? `
|
|
807
|
+
<div class="detail-row">
|
|
808
|
+
<span class="detail-label">Evidence</span>
|
|
809
|
+
<div class="evidence-box">${escapeHtml(f.evidence)}</div>
|
|
810
|
+
</div>
|
|
811
|
+
` : ""}
|
|
812
|
+
${f.metadata ? `
|
|
813
|
+
<div class="detail-row">
|
|
814
|
+
<span class="detail-label">Metadata</span>
|
|
815
|
+
<div class="evidence-box">${escapeHtml(JSON.stringify(f.metadata, null, 2))}</div>
|
|
816
|
+
</div>
|
|
817
|
+
` : ""}
|
|
818
|
+
</div>
|
|
819
|
+
</div>
|
|
820
|
+
`
|
|
821
|
+
).join("") : `
|
|
822
|
+
<div class="no-findings">
|
|
823
|
+
<div class="icon">\u{1F6E1}\uFE0F</div>
|
|
824
|
+
<h3>No Vulnerabilities Detected</h3>
|
|
825
|
+
<p>${result.payloadsTested} payloads were tested across ${result.stepsExecuted} steps with no findings.</p>
|
|
826
|
+
</div>
|
|
827
|
+
`}
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
${result.errors.length > 0 ? `
|
|
831
|
+
<div class="errors-section">
|
|
832
|
+
<h3>\u26A0\uFE0F Errors During Execution (${result.errors.length})</h3>
|
|
833
|
+
${result.errors.map((e) => `<div class="error-item">${escapeHtml(e)}</div>`).join("")}
|
|
834
|
+
</div>
|
|
835
|
+
` : ""}
|
|
836
|
+
|
|
837
|
+
<!-- Footer -->
|
|
838
|
+
<div class="footer">
|
|
839
|
+
<div>Generated by ${VULCN_LOGO_SVG.replace(/width="32"/g, 'width="16"').replace(/height="32"/g, 'height="16"')} <strong>Vulcn</strong> \u2014 Security Testing Engine</div>
|
|
840
|
+
<div><a href="https://docs.vulcn.dev">docs.vulcn.dev</a></div>
|
|
841
|
+
</div>
|
|
842
|
+
</div>
|
|
843
|
+
</body>
|
|
844
|
+
</html>`;
|
|
845
|
+
}
|
|
846
|
+
function generateDonut(counts, total) {
|
|
847
|
+
if (total === 0) return "";
|
|
848
|
+
const radius = 60;
|
|
849
|
+
const circumference = 2 * Math.PI * radius;
|
|
850
|
+
let offset = 0;
|
|
851
|
+
const segments = ["critical", "high", "medium", "low", "info"].filter((sev) => counts[sev] > 0).map((sev) => {
|
|
852
|
+
const pct = counts[sev] / total;
|
|
853
|
+
const dash = pct * circumference;
|
|
854
|
+
const seg = `<circle cx="80" cy="80" r="${radius}" fill="none" stroke="${severityColor(sev)}" stroke-width="14"
|
|
855
|
+
stroke-dasharray="${dash} ${circumference - dash}"
|
|
856
|
+
stroke-dashoffset="${-offset}"
|
|
857
|
+
opacity="0.9"/>`;
|
|
858
|
+
offset += dash;
|
|
859
|
+
return seg;
|
|
860
|
+
});
|
|
861
|
+
return `<svg viewBox="0 0 160 160" width="120" height="120" style="transform:rotate(-90deg)">
|
|
862
|
+
<circle cx="80" cy="80" r="${radius}" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="14"/>
|
|
863
|
+
${segments.join("\n ")}
|
|
864
|
+
</svg>`;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/json.ts
|
|
868
|
+
function formatDuration2(ms) {
|
|
869
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
870
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
871
|
+
}
|
|
872
|
+
function generateJson(session, result, generatedAt, engineVersion) {
|
|
873
|
+
const counts = {
|
|
874
|
+
critical: 0,
|
|
875
|
+
high: 0,
|
|
876
|
+
medium: 0,
|
|
877
|
+
low: 0,
|
|
878
|
+
info: 0
|
|
879
|
+
};
|
|
880
|
+
for (const f of result.findings) {
|
|
881
|
+
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
882
|
+
}
|
|
883
|
+
const riskScore = counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;
|
|
884
|
+
return {
|
|
885
|
+
vulcn: {
|
|
886
|
+
version: engineVersion,
|
|
887
|
+
reportVersion: "1.0",
|
|
888
|
+
generatedAt
|
|
889
|
+
},
|
|
890
|
+
session: {
|
|
891
|
+
name: session.name,
|
|
892
|
+
driver: session.driver,
|
|
893
|
+
driverConfig: session.driverConfig,
|
|
894
|
+
stepsCount: session.steps.length,
|
|
895
|
+
metadata: session.metadata
|
|
896
|
+
},
|
|
897
|
+
execution: {
|
|
898
|
+
stepsExecuted: result.stepsExecuted,
|
|
899
|
+
payloadsTested: result.payloadsTested,
|
|
900
|
+
durationMs: result.duration,
|
|
901
|
+
durationFormatted: formatDuration2(result.duration),
|
|
902
|
+
errors: result.errors
|
|
903
|
+
},
|
|
904
|
+
summary: {
|
|
905
|
+
totalFindings: result.findings.length,
|
|
906
|
+
riskScore,
|
|
907
|
+
severityCounts: counts,
|
|
908
|
+
vulnerabilityTypes: [...new Set(result.findings.map((f) => f.type))],
|
|
909
|
+
affectedUrls: [...new Set(result.findings.map((f) => f.url))]
|
|
910
|
+
},
|
|
911
|
+
findings: result.findings
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/yaml.ts
|
|
916
|
+
import { stringify } from "yaml";
|
|
917
|
+
function generateYaml(session, result, generatedAt, engineVersion) {
|
|
918
|
+
const report = generateJson(session, result, generatedAt, engineVersion);
|
|
919
|
+
const header = [
|
|
920
|
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
921
|
+
"# Vulcn Security Report",
|
|
922
|
+
`# Generated: ${generatedAt}`,
|
|
923
|
+
`# Session: ${session.name}`,
|
|
924
|
+
`# Findings: ${result.findings.length}`,
|
|
925
|
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
926
|
+
""
|
|
927
|
+
].join("\n");
|
|
928
|
+
return header + stringify(report, { indent: 2 });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/index.ts
|
|
932
|
+
var configSchema = z.object({
|
|
933
|
+
/**
|
|
934
|
+
* Report format(s) to generate
|
|
935
|
+
* - "html": Beautiful dark-themed HTML report
|
|
936
|
+
* - "json": Machine-readable structured JSON
|
|
937
|
+
* - "yaml": Human-readable YAML
|
|
938
|
+
* - "all": Generate all three formats
|
|
939
|
+
* @default "html"
|
|
940
|
+
*/
|
|
941
|
+
format: z.enum(["html", "json", "yaml", "all"]).default("html"),
|
|
942
|
+
/**
|
|
943
|
+
* Output directory for report files
|
|
944
|
+
* @default "."
|
|
945
|
+
*/
|
|
946
|
+
outputDir: z.string().default("."),
|
|
947
|
+
/**
|
|
948
|
+
* Base filename (without extension) for the report
|
|
949
|
+
* @default "vulcn-report"
|
|
950
|
+
*/
|
|
951
|
+
filename: z.string().default("vulcn-report"),
|
|
952
|
+
/**
|
|
953
|
+
* Auto-open HTML report in default browser after generation
|
|
954
|
+
* @default false
|
|
955
|
+
*/
|
|
956
|
+
open: z.boolean().default(false)
|
|
957
|
+
});
|
|
958
|
+
function getFormats(format) {
|
|
959
|
+
if (format === "all") return ["html", "json", "yaml"];
|
|
960
|
+
return [format];
|
|
961
|
+
}
|
|
962
|
+
var plugin = {
|
|
963
|
+
name: "@vulcn/plugin-report",
|
|
964
|
+
version: "0.1.0",
|
|
965
|
+
apiVersion: 1,
|
|
966
|
+
description: "Report generation plugin \u2014 generates beautiful HTML, JSON, and YAML security reports",
|
|
967
|
+
configSchema,
|
|
968
|
+
hooks: {
|
|
969
|
+
onInit: async (ctx) => {
|
|
970
|
+
const config = configSchema.parse(ctx.config);
|
|
971
|
+
ctx.logger.info(
|
|
972
|
+
`Report plugin initialized (format: ${config.format}, output: ${config.outputDir}/${config.filename})`
|
|
973
|
+
);
|
|
974
|
+
},
|
|
975
|
+
/**
|
|
976
|
+
* Generate report(s) after run completes
|
|
977
|
+
*/
|
|
978
|
+
onRunEnd: async (result, ctx) => {
|
|
979
|
+
const config = configSchema.parse(ctx.config);
|
|
980
|
+
const formats = getFormats(config.format);
|
|
981
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
982
|
+
const engineVersion = ctx.engine.version;
|
|
983
|
+
const outDir = resolve(config.outputDir);
|
|
984
|
+
await mkdir(outDir, { recursive: true });
|
|
985
|
+
const basePath = resolve(outDir, config.filename);
|
|
986
|
+
const writtenFiles = [];
|
|
987
|
+
for (const fmt of formats) {
|
|
988
|
+
try {
|
|
989
|
+
switch (fmt) {
|
|
990
|
+
case "html": {
|
|
991
|
+
const htmlData = {
|
|
992
|
+
session: ctx.session,
|
|
993
|
+
result,
|
|
994
|
+
generatedAt,
|
|
995
|
+
engineVersion
|
|
996
|
+
};
|
|
997
|
+
const html = generateHtml(htmlData);
|
|
998
|
+
const htmlPath = `${basePath}.html`;
|
|
999
|
+
await writeFile(htmlPath, html, "utf-8");
|
|
1000
|
+
writtenFiles.push(htmlPath);
|
|
1001
|
+
ctx.logger.info(`\u{1F4C4} HTML report: ${htmlPath}`);
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
case "json": {
|
|
1005
|
+
const jsonReport = generateJson(
|
|
1006
|
+
ctx.session,
|
|
1007
|
+
result,
|
|
1008
|
+
generatedAt,
|
|
1009
|
+
engineVersion
|
|
1010
|
+
);
|
|
1011
|
+
const jsonPath = `${basePath}.json`;
|
|
1012
|
+
await writeFile(
|
|
1013
|
+
jsonPath,
|
|
1014
|
+
JSON.stringify(jsonReport, null, 2),
|
|
1015
|
+
"utf-8"
|
|
1016
|
+
);
|
|
1017
|
+
writtenFiles.push(jsonPath);
|
|
1018
|
+
ctx.logger.info(`\u{1F4C4} JSON report: ${jsonPath}`);
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
case "yaml": {
|
|
1022
|
+
const yamlContent = generateYaml(
|
|
1023
|
+
ctx.session,
|
|
1024
|
+
result,
|
|
1025
|
+
generatedAt,
|
|
1026
|
+
engineVersion
|
|
1027
|
+
);
|
|
1028
|
+
const yamlPath = `${basePath}.yml`;
|
|
1029
|
+
await writeFile(yamlPath, yamlContent, "utf-8");
|
|
1030
|
+
writtenFiles.push(yamlPath);
|
|
1031
|
+
ctx.logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
ctx.logger.error(
|
|
1037
|
+
`Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (config.open && formats.includes("html")) {
|
|
1042
|
+
const htmlPath = `${basePath}.html`;
|
|
1043
|
+
try {
|
|
1044
|
+
const { exec } = await import("child_process");
|
|
1045
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1046
|
+
exec(`${openCmd} "${htmlPath}"`);
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
var index_default = plugin;
|
|
1055
|
+
export {
|
|
1056
|
+
configSchema,
|
|
1057
|
+
index_default as default,
|
|
1058
|
+
generateHtml,
|
|
1059
|
+
generateJson,
|
|
1060
|
+
generateYaml
|
|
1061
|
+
};
|
|
1062
|
+
//# sourceMappingURL=index.js.map
|