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