@vulcn/plugin-report 0.4.0 → 0.5.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 +650 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -46
- package/dist/index.d.ts +174 -46
- package/dist/index.js +649 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/html.ts","../src/json.ts","../src/yaml.ts","../src/sarif.ts"],"sourcesContent":["/**\n * @vulcn/plugin-report\n * Report Generation Plugin for Vulcn\n *\n * Generates security reports in HTML, JSON, YAML, and SARIF formats\n * after a run completes. Features:\n * - Modern dark-themed HTML report with Vulcn branding\n * - Machine-readable JSON for CI/CD integration\n * - Human-readable YAML for documentation\n * - SARIF v2.1.0 for GitHub Code Scanning and IDE integration\n *\n * Configuration:\n * format: \"html\" | \"json\" | \"yaml\" | \"sarif\" | \"all\" (default: \"html\")\n * outputDir: directory for reports (default: \".\")\n * filename: base filename (no extension) (default: \"vulcn-report\")\n * open: auto-open HTML in browser (default: false)\n */\n\nimport { z } from \"zod\";\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport type {\n VulcnPlugin,\n PluginContext,\n PluginRunContext,\n RunResult,\n} from \"@vulcn/engine\";\n\nimport { generateHtml, type HtmlReportData } from \"./html\";\nimport { generateJson, type JsonReport } from \"./json\";\nimport { generateYaml } from \"./yaml\";\nimport { generateSarif, type SarifLog } from \"./sarif\";\n\n/**\n * Plugin configuration schema\n */\nconst configSchema = z.object({\n /**\n * Report format(s) to generate\n * - \"html\": Beautiful dark-themed HTML report\n * - \"json\": Machine-readable structured JSON\n * - \"yaml\": Human-readable YAML\n * - \"sarif\": SARIF v2.1.0 for GitHub Code Scanning\n * - \"all\": Generate all formats\n * @default \"html\"\n */\n format: z.enum([\"html\", \"json\", \"yaml\", \"sarif\", \"all\"]).default(\"html\"),\n\n /**\n * Output directory for report files\n * @default \".\"\n */\n outputDir: z.string().default(\".\"),\n\n /**\n * Base filename (without extension) for the report\n * @default \"vulcn-report\"\n */\n filename: z.string().default(\"vulcn-report\"),\n\n /**\n * Auto-open HTML report in default browser after generation\n * @default false\n */\n open: z.boolean().default(false),\n});\n\nexport type ReportConfig = z.infer<typeof configSchema>;\n\n/**\n * Determine which formats to generate\n */\nfunction getFormats(format: ReportConfig[\"format\"]): string[] {\n if (format === \"all\") return [\"html\", \"json\", \"yaml\", \"sarif\"];\n return [format];\n}\n\n/**\n * Report Plugin\n */\nconst plugin: VulcnPlugin = {\n name: \"@vulcn/plugin-report\",\n version: \"0.1.0\",\n apiVersion: 1,\n description:\n \"Report generation plugin — generates HTML, JSON, YAML, and SARIF security reports\",\n\n configSchema,\n\n hooks: {\n onInit: async (ctx: PluginContext) => {\n const config = configSchema.parse(ctx.config);\n ctx.logger.info(\n `Report plugin initialized (format: ${config.format}, output: ${config.outputDir}/${config.filename})`,\n );\n },\n\n /**\n * Generate report(s) after run completes\n */\n onRunEnd: async (\n result: RunResult,\n ctx: PluginRunContext,\n ): Promise<RunResult> => {\n const config = configSchema.parse(ctx.config);\n const formats = getFormats(config.format);\n const generatedAt = new Date().toISOString();\n const engineVersion = ctx.engine.version;\n\n // Ensure output directory exists\n const outDir = resolve(config.outputDir);\n await mkdir(outDir, { recursive: true });\n\n const basePath = resolve(outDir, config.filename);\n const writtenFiles: string[] = [];\n\n for (const fmt of formats) {\n try {\n switch (fmt) {\n case \"html\": {\n const htmlData: HtmlReportData = {\n session: ctx.session,\n result,\n generatedAt,\n engineVersion,\n };\n const html = generateHtml(htmlData);\n const htmlPath = `${basePath}.html`;\n await writeFile(htmlPath, html, \"utf-8\");\n writtenFiles.push(htmlPath);\n ctx.logger.info(`📄 HTML report: ${htmlPath}`);\n break;\n }\n\n case \"json\": {\n const jsonReport = generateJson(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const jsonPath = `${basePath}.json`;\n await writeFile(\n jsonPath,\n JSON.stringify(jsonReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(jsonPath);\n ctx.logger.info(`📄 JSON report: ${jsonPath}`);\n break;\n }\n\n case \"yaml\": {\n const yamlContent = generateYaml(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const yamlPath = `${basePath}.yml`;\n await writeFile(yamlPath, yamlContent, \"utf-8\");\n writtenFiles.push(yamlPath);\n ctx.logger.info(`📄 YAML report: ${yamlPath}`);\n break;\n }\n\n case \"sarif\": {\n const sarifReport = generateSarif(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const sarifPath = `${basePath}.sarif`;\n await writeFile(\n sarifPath,\n JSON.stringify(sarifReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(sarifPath);\n ctx.logger.info(`📄 SARIF report: ${sarifPath}`);\n break;\n }\n }\n } catch (err) {\n ctx.logger.error(\n `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Auto-open HTML report if configured\n if (config.open && formats.includes(\"html\")) {\n const htmlPath = `${basePath}.html`;\n try {\n const { exec } = await import(\"node:child_process\");\n const openCmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${openCmd} \"${htmlPath}\"`);\n } catch {\n // Silently ignore if can't open browser\n }\n }\n\n return result;\n },\n },\n};\n\nexport default plugin;\n\n// Named exports for programmatic usage\nexport {\n configSchema,\n generateHtml,\n generateJson,\n generateYaml,\n generateSarif,\n};\nexport type { HtmlReportData, JsonReport, SarifLog };\n","/**\n * HTML Report Generator for Vulcn\n *\n * Generates a modern, dark-themed security report with:\n * - Vulcn branding (shield gradient logo)\n * - Executive summary with severity donut chart\n * - Detailed findings with expandable evidence\n * - Timeline of execution\n * - Responsive design\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\nexport interface HtmlReportData {\n session: Session;\n result: RunResult;\n generatedAt: string;\n engineVersion: string;\n}\n\n// Vulcn brand colors\nconst COLORS = {\n bg: \"#0a0a0f\",\n surface: \"#12121a\",\n surfaceHover: \"#1a1a26\",\n border: \"#1e1e2e\",\n borderActive: \"#2a2a3e\",\n text: \"#e4e4ef\",\n textMuted: \"#8888a0\",\n textDim: \"#555570\",\n accent: \"#fa1b1b\",\n accentGlow: \"rgba(250, 27, 27, 0.15)\",\n accentLight: \"#ff9c9c\",\n critical: \"#ff1744\",\n high: \"#ff5252\",\n medium: \"#ffab40\",\n low: \"#66bb6a\",\n info: \"#42a5f5\",\n success: \"#00e676\",\n};\n\nfunction severityColor(severity: string): string {\n switch (severity) {\n case \"critical\":\n return COLORS.critical;\n case \"high\":\n return COLORS.high;\n case \"medium\":\n return COLORS.medium;\n case \"low\":\n return COLORS.low;\n case \"info\":\n return COLORS.info;\n default:\n return COLORS.textMuted;\n }\n}\n\nfunction severityOrder(severity: string): number {\n switch (severity) {\n case \"critical\":\n return 0;\n case \"high\":\n return 1;\n case \"medium\":\n return 2;\n case \"low\":\n return 3;\n case \"info\":\n return 4;\n default:\n return 5;\n }\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const seconds = (ms / 1000).toFixed(1);\n return `${seconds}s`;\n}\n\nfunction formatDate(iso: string): string {\n const d = new Date(iso);\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n });\n}\n\n// Inline SVG logo matching the vulcn shield branding\nconst VULCN_LOGO_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"32\" height=\"32\">\n <defs>\n <linearGradient id=\"lg1\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(7 -13 13 7 7 17)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#fa1b1b\"/>\n <stop offset=\"1\" stop-color=\"#ff9c9c\"/>\n </linearGradient>\n <linearGradient id=\"lg2\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(3 -6 6 3 13 14)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#ff9c9c\"/>\n <stop offset=\"1\" stop-color=\"#ffffff\"/>\n </linearGradient>\n </defs>\n <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\"/>\n <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\"/>\n <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\"/>\n</svg>`;\n\nexport function generateHtml(data: HtmlReportData): string {\n const { session, result, generatedAt, engineVersion } = data;\n const findings = [...result.findings].sort(\n (a, b) => severityOrder(a.severity) - severityOrder(b.severity),\n );\n\n // Severity counts for donut chart\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const totalFindings = findings.length;\n const hasFindings = totalFindings > 0;\n\n // Overall risk score\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n const maxRisk = totalFindings * 10 || 1;\n const riskPercent = Math.min(100, Math.round((riskScore / maxRisk) * 100));\n const riskLabel =\n riskPercent >= 80\n ? \"Critical\"\n : riskPercent >= 50\n ? \"High\"\n : riskPercent >= 25\n ? \"Medium\"\n : riskPercent > 0\n ? \"Low\"\n : \"Clear\";\n const riskColor =\n riskPercent >= 80\n ? COLORS.critical\n : riskPercent >= 50\n ? COLORS.high\n : riskPercent >= 25\n ? COLORS.medium\n : riskPercent > 0\n ? COLORS.low\n : COLORS.success;\n\n // Donut chart SVG segments\n const donutSvg = generateDonut(counts, totalFindings);\n\n // Unique URLs affected\n const affectedUrls = [...new Set(findings.map((f) => f.url))];\n\n // Unique vuln types\n const vulnTypes = [...new Set(findings.map((f) => f.type))];\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Vulcn Security Report — ${escapeHtml(session.name)}</title>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n :root {\n --bg: ${COLORS.bg};\n --surface: ${COLORS.surface};\n --surface-hover: ${COLORS.surfaceHover};\n --border: ${COLORS.border};\n --border-active: ${COLORS.borderActive};\n --text: ${COLORS.text};\n --text-muted: ${COLORS.textMuted};\n --text-dim: ${COLORS.textDim};\n --accent: ${COLORS.accent};\n --accent-glow: ${COLORS.accentGlow};\n --accent-light: ${COLORS.accentLight};\n --radius: 12px;\n --radius-sm: 8px;\n --radius-xs: 6px;\n }\n\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n }\n\n /* Ambient gradient background */\n body::before {\n content: '';\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 600px;\n background: radial-gradient(ellipse 80% 50% at 50% -20%, ${COLORS.accentGlow} 0%, transparent 100%);\n pointer-events: none;\n z-index: 0;\n }\n\n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 24px;\n position: relative;\n z-index: 1;\n }\n\n /* Header */\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 48px;\n padding-bottom: 24px;\n border-bottom: 1px solid var(--border);\n }\n\n .header-brand {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .header-brand svg {\n filter: drop-shadow(0 0 8px rgba(250, 27, 27, 0.3));\n }\n\n .header-brand h1 {\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -0.02em;\n background: linear-gradient(135deg, #fa1b1b, #ff9c9c);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .header-brand span {\n font-size: 11px;\n font-weight: 500;\n color: var(--text-dim);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .header-meta {\n text-align: right;\n font-size: 12px;\n color: var(--text-dim);\n line-height: 1.8;\n }\n\n /* Session info */\n .session-info {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 24px;\n margin-bottom: 32px;\n }\n\n .session-info h2 {\n font-size: 22px;\n font-weight: 700;\n margin-bottom: 16px;\n letter-spacing: -0.02em;\n }\n\n .session-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n }\n\n .meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .meta-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .meta-value {\n font-size: 14px;\n font-weight: 500;\n color: var(--text);\n font-family: 'JetBrains Mono', monospace;\n font-size: 13px;\n }\n\n /* Stats grid */\n .stats-grid {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 24px;\n margin-bottom: 32px;\n }\n\n @media (max-width: 768px) {\n .stats-grid { grid-template-columns: 1fr; }\n }\n\n /* Risk gauge */\n .risk-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n }\n\n .risk-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n width: 100%;\n }\n\n .risk-gauge {\n position: relative;\n width: 160px;\n height: 160px;\n }\n\n .risk-gauge svg {\n transform: rotate(-90deg);\n }\n\n .risk-gauge-label {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .risk-gauge-label .score {\n font-size: 36px;\n font-weight: 800;\n letter-spacing: -0.03em;\n }\n\n .risk-gauge-label .label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n }\n\n /* Summary card */\n .summary-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n }\n\n .summary-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n margin-bottom: 20px;\n }\n\n .summary-stats {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 20px;\n }\n\n .stat-box {\n padding: 16px;\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n transition: border-color 0.2s;\n }\n\n .stat-box:hover { border-color: var(--border-active); }\n\n .stat-number {\n font-size: 28px;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1;\n margin-bottom: 4px;\n }\n\n .stat-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--text-muted);\n }\n\n /* Severity breakdown */\n .severity-breakdown {\n margin-bottom: 32px;\n }\n\n .severity-section-header {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 16px;\n }\n\n .severity-section-header h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .severity-bars {\n display: flex;\n gap: 8px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 20px 24px;\n }\n\n .severity-bar-item {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n }\n\n .severity-bar-track {\n width: 100%;\n height: 6px;\n background: rgba(255,255,255,0.04);\n border-radius: 3px;\n overflow: hidden;\n }\n\n .severity-bar-fill {\n height: 100%;\n border-radius: 3px;\n transition: width 0.5s ease;\n }\n\n .severity-bar-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .severity-bar-count {\n font-size: 18px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n }\n\n /* Findings section */\n .findings-section {\n margin-bottom: 32px;\n }\n\n .findings-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n }\n\n .findings-header h3 {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .findings-count {\n font-size: 12px;\n font-weight: 600;\n color: var(--text-dim);\n padding: 4px 12px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 100px;\n }\n\n /* Finding card */\n .finding-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n margin-bottom: 12px;\n overflow: hidden;\n transition: border-color 0.2s;\n }\n\n .finding-card:hover { border-color: var(--border-active); }\n\n .finding-header {\n padding: 20px 24px;\n display: flex;\n align-items: flex-start;\n gap: 16px;\n cursor: pointer;\n user-select: none;\n }\n\n .finding-severity-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 6px;\n box-shadow: 0 0 8px currentColor;\n }\n\n .finding-info {\n flex: 1;\n min-width: 0;\n }\n\n .finding-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 4px;\n letter-spacing: -0.01em;\n }\n\n .finding-subtitle {\n font-size: 12px;\n color: var(--text-muted);\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n }\n\n .finding-tag {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n }\n\n .finding-expand-icon {\n font-size: 18px;\n color: var(--text-dim);\n transition: transform 0.2s;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .finding-card.open .finding-expand-icon {\n transform: rotate(180deg);\n }\n\n .finding-details {\n display: none;\n padding: 0 24px 20px;\n border-top: 1px solid var(--border);\n }\n\n .finding-card.open .finding-details {\n display: block;\n padding-top: 20px;\n }\n\n .detail-row {\n display: grid;\n grid-template-columns: 120px 1fr;\n gap: 8px;\n margin-bottom: 12px;\n align-items: baseline;\n }\n\n .detail-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .detail-value {\n font-size: 13px;\n color: var(--text);\n word-break: break-all;\n }\n\n .evidence-box {\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-xs);\n padding: 12px 16px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.5;\n overflow-x: auto;\n white-space: pre-wrap;\n }\n\n .payload-box {\n background: rgba(250, 27, 27, 0.06);\n border: 1px solid rgba(250, 27, 27, 0.15);\n border-radius: var(--radius-xs);\n padding: 8px 12px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--accent-light);\n word-break: break-all;\n }\n\n /* No findings */\n .no-findings {\n text-align: center;\n padding: 60px 24px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n\n .no-findings .icon { font-size: 48px; margin-bottom: 16px; }\n .no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }\n .no-findings p { font-size: 14px; color: var(--text-muted); }\n\n /* Errors section */\n .errors-section {\n margin-bottom: 32px;\n }\n\n .errors-section h3 {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-muted);\n margin-bottom: 12px;\n }\n\n .error-item {\n padding: 10px 16px;\n background: rgba(255, 171, 64, 0.04);\n border: 1px solid rgba(255, 171, 64, 0.1);\n border-radius: var(--radius-xs);\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: ${COLORS.medium};\n margin-bottom: 6px;\n }\n\n /* Footer */\n .footer {\n text-align: center;\n padding: 32px 0;\n border-top: 1px solid var(--border);\n margin-top: 48px;\n color: var(--text-dim);\n font-size: 12px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n }\n\n .footer a {\n color: var(--accent-light);\n text-decoration: none;\n }\n\n .footer a:hover { text-decoration: underline; }\n\n /* Animations */\n @keyframes fadeIn {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .animate-in {\n animation: fadeIn 0.4s ease-out;\n }\n\n .animate-in-delay { animation: fadeIn 0.4s ease-out 0.1s both; }\n .animate-in-delay-2 { animation: fadeIn 0.4s ease-out 0.2s both; }\n .animate-in-delay-3 { animation: fadeIn 0.4s ease-out 0.3s both; }\n\n /* Print styles */\n @media print {\n body { background: white; color: #111; }\n body::before { display: none; }\n .finding-details { display: block !important; padding-top: 12px !important; }\n .finding-card { page-break-inside: avoid; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header animate-in\">\n <div class=\"header-brand\">\n ${VULCN_LOGO_SVG}\n <div>\n <h1>vulcn</h1>\n <span>Security Report</span>\n </div>\n </div>\n <div class=\"header-meta\">\n <div>${formatDate(generatedAt)}</div>\n <div>Engine v${escapeHtml(engineVersion)}</div>\n </div>\n </div>\n\n <!-- Session info -->\n <div class=\"session-info animate-in-delay\">\n <h2>${escapeHtml(session.name)}</h2>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\">Driver</span>\n <span class=\"meta-value\">${escapeHtml(session.driver)}</span>\n </div>\n ${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>` : \"\"}\n <div class=\"meta-item\">\n <span class=\"meta-label\">Duration</span>\n <span class=\"meta-value\">${formatDuration(result.duration)}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\">Generated</span>\n <span class=\"meta-value\">${formatDate(generatedAt)}</span>\n </div>\n </div>\n </div>\n\n <!-- Stats grid: Risk + Summary -->\n <div class=\"stats-grid animate-in-delay-2\">\n <div class=\"risk-card\">\n <h3>Risk Level</h3>\n <div class=\"risk-gauge\">\n <svg viewBox=\"0 0 160 160\" width=\"160\" height=\"160\">\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"10\"/>\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"${riskColor}\" stroke-width=\"10\"\n stroke-dasharray=\"${(riskPercent / 100) * 427} 427\"\n stroke-linecap=\"round\"\n style=\"filter: drop-shadow(0 0 6px ${riskColor});\"/>\n </svg>\n <div class=\"risk-gauge-label\">\n <div class=\"score\" style=\"color: ${riskColor}\">${hasFindings ? riskPercent : 0}</div>\n <div class=\"label\">${riskLabel}</div>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <h3>Execution Summary</h3>\n <div class=\"summary-stats\">\n <div class=\"stat-box\">\n <div class=\"stat-number\" style=\"color: ${hasFindings ? COLORS.high : COLORS.success}\">${totalFindings}</div>\n <div class=\"stat-label\">Findings</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.payloadsTested}</div>\n <div class=\"stat-label\">Payloads Tested</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.stepsExecuted}</div>\n <div class=\"stat-label\">Steps Executed</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${affectedUrls.length}</div>\n <div class=\"stat-label\">URLs Affected</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Severity breakdown -->\n <div class=\"severity-breakdown animate-in-delay-2\">\n <div class=\"severity-bars\">\n ${[\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .map(\n (sev) => `\n <div class=\"severity-bar-item\">\n <div class=\"severity-bar-count\" style=\"color: ${severityColor(sev)}\">${counts[sev]}</div>\n <div class=\"severity-bar-track\">\n <div class=\"severity-bar-fill\" style=\"width: ${totalFindings ? (counts[sev] / totalFindings) * 100 : 0}%; background: ${severityColor(sev)};\"></div>\n </div>\n <div class=\"severity-bar-label\">${sev}</div>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n </div>\n\n <!-- Findings -->\n <div class=\"findings-section animate-in-delay-3\">\n <div class=\"findings-header\">\n <h3>Findings</h3>\n <span class=\"findings-count\">${totalFindings} total</span>\n </div>\n\n ${\n hasFindings\n ? findings\n .map(\n (f, i) => `\n <div class=\"finding-card\" onclick=\"this.classList.toggle('open')\">\n <div class=\"finding-header\">\n <div class=\"finding-severity-dot\" style=\"color: ${severityColor(f.severity)}; background: ${severityColor(f.severity)};\"></div>\n <div class=\"finding-info\">\n <div class=\"finding-title\">${escapeHtml(f.title)}</div>\n <div class=\"finding-subtitle\">\n <span class=\"finding-tag\" style=\"color: ${severityColor(f.severity)}\">${f.severity.toUpperCase()}</span>\n <span class=\"finding-tag\">${escapeHtml(f.type)}</span>\n <span class=\"finding-tag\">${escapeHtml(f.stepId)}</span>\n </div>\n </div>\n <span class=\"finding-expand-icon\">▾</span>\n </div>\n <div class=\"finding-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Description</span>\n <span class=\"detail-value\">${escapeHtml(f.description)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">URL</span>\n <span class=\"detail-value\">${escapeHtml(f.url)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Payload</span>\n <div class=\"payload-box\">${escapeHtml(f.payload)}</div>\n </div>\n ${\n f.evidence\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Evidence</span>\n <div class=\"evidence-box\">${escapeHtml(f.evidence)}</div>\n </div>\n `\n : \"\"\n }\n ${\n f.metadata\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Metadata</span>\n <div class=\"evidence-box\">${escapeHtml(JSON.stringify(f.metadata, null, 2))}</div>\n </div>\n `\n : \"\"\n }\n </div>\n </div>\n `,\n )\n .join(\"\")\n : `\n <div class=\"no-findings\">\n <div class=\"icon\">🛡️</div>\n <h3>No Vulnerabilities Detected</h3>\n <p>${result.payloadsTested} payloads were tested across ${result.stepsExecuted} steps with no findings.</p>\n </div>\n `\n }\n </div>\n\n ${\n result.errors.length > 0\n ? `\n <div class=\"errors-section\">\n <h3>⚠️ Errors During Execution (${result.errors.length})</h3>\n ${result.errors.map((e) => `<div class=\"error-item\">${escapeHtml(e)}</div>`).join(\"\")}\n </div>\n `\n : \"\"\n }\n\n <!-- Footer -->\n <div class=\"footer\">\n <div>Generated by ${VULCN_LOGO_SVG.replace(/width=\"32\"/g, 'width=\"16\"').replace(/height=\"32\"/g, 'height=\"16\"')} <strong>Vulcn</strong> — Security Testing Engine</div>\n <div><a href=\"https://docs.vulcn.dev\">docs.vulcn.dev</a></div>\n </div>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Generate SVG donut chart segments (unused in current layout but available)\n */\nfunction generateDonut(counts: Record<string, number>, total: number): string {\n if (total === 0) return \"\";\n const radius = 60;\n const circumference = 2 * Math.PI * radius;\n let offset = 0;\n\n const segments = [\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .filter((sev) => counts[sev] > 0)\n .map((sev) => {\n const pct = counts[sev] / total;\n const dash = pct * circumference;\n const seg = `<circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"${severityColor(sev)}\" stroke-width=\"14\"\n stroke-dasharray=\"${dash} ${circumference - dash}\"\n stroke-dashoffset=\"${-offset}\"\n opacity=\"0.9\"/>`;\n offset += dash;\n return seg;\n });\n\n return `<svg viewBox=\"0 0 160 160\" width=\"120\" height=\"120\" style=\"transform:rotate(-90deg)\">\n <circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"14\"/>\n ${segments.join(\"\\n \")}\n </svg>`;\n}\n","/**\n * JSON Report Generator for Vulcn\n *\n * Produces a structured, machine-readable JSON report.\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\nexport interface JsonReport {\n vulcn: {\n version: string;\n reportVersion: string;\n generatedAt: string;\n };\n session: {\n name: string;\n driver: string;\n driverConfig: Record<string, unknown>;\n stepsCount: number;\n metadata?: Record<string, unknown>;\n };\n execution: {\n stepsExecuted: number;\n payloadsTested: number;\n durationMs: number;\n durationFormatted: string;\n errors: string[];\n };\n summary: {\n totalFindings: number;\n riskScore: number;\n severityCounts: Record<string, number>;\n vulnerabilityTypes: string[];\n affectedUrls: string[];\n };\n findings: Finding[];\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport function generateJson(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): JsonReport {\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of result.findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n\n return {\n vulcn: {\n version: engineVersion,\n reportVersion: \"1.0\",\n generatedAt,\n },\n session: {\n name: session.name,\n driver: session.driver,\n driverConfig: session.driverConfig,\n stepsCount: session.steps.length,\n metadata: session.metadata,\n },\n execution: {\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n durationFormatted: formatDuration(result.duration),\n errors: result.errors,\n },\n summary: {\n totalFindings: result.findings.length,\n riskScore,\n severityCounts: counts,\n vulnerabilityTypes: [...new Set(result.findings.map((f) => f.type))],\n affectedUrls: [...new Set(result.findings.map((f) => f.url))],\n },\n findings: result.findings,\n };\n}\n","/**\n * YAML Report Generator for Vulcn\n *\n * Produces a human-readable YAML report.\n */\n\nimport { stringify } from \"yaml\";\nimport type { RunResult, Session } from \"@vulcn/engine\";\nimport { generateJson } from \"./json\";\n\nexport function generateYaml(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): string {\n const report = generateJson(session, result, generatedAt, engineVersion);\n\n // YAML with header comment\n const header = [\n \"# ──────────────────────────────────────────────\",\n \"# Vulcn Security Report\",\n `# Generated: ${generatedAt}`,\n `# Session: ${session.name}`,\n `# Findings: ${result.findings.length}`,\n \"# ──────────────────────────────────────────────\",\n \"\",\n ].join(\"\\n\");\n\n return header + stringify(report, { indent: 2 });\n}\n","/**\n * SARIF Report Generator for Vulcn\n *\n * Produces SARIF v2.1.0 (Static Analysis Results Interchange Format)\n * compatible with GitHub Code Scanning, Azure DevOps, and other\n * SARIF-consuming tools.\n *\n * @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html\n * @see https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\n// ── SARIF Types ────────────────────────────────────────────────────────\n\n/**\n * SARIF v2.1.0 Log — top-level structure\n */\nexport interface SarifLog {\n $schema: string;\n version: \"2.1.0\";\n runs: SarifRun[];\n}\n\ninterface SarifRun {\n tool: SarifTool;\n results: SarifResult[];\n invocations: SarifInvocation[];\n artifacts?: SarifArtifact[];\n}\n\ninterface SarifTool {\n driver: SarifToolComponent;\n}\n\ninterface SarifToolComponent {\n name: string;\n version: string;\n informationUri: string;\n semanticVersion: string;\n rules: SarifRule[];\n}\n\ninterface SarifRule {\n id: string;\n name: string;\n shortDescription: { text: string };\n fullDescription: { text: string };\n helpUri: string;\n help: { text: string; markdown: string };\n properties: {\n tags: string[];\n precision: \"very-high\" | \"high\" | \"medium\" | \"low\";\n \"security-severity\": string;\n };\n defaultConfiguration: {\n level: SarifLevel;\n };\n}\n\ntype SarifLevel = \"error\" | \"warning\" | \"note\" | \"none\";\n\ninterface SarifResult {\n ruleId: string;\n ruleIndex: number;\n level: SarifLevel;\n message: { text: string };\n locations: SarifLocation[];\n fingerprints: Record<string, string>;\n partialFingerprints: Record<string, string>;\n properties: Record<string, unknown>;\n}\n\ninterface SarifLocation {\n physicalLocation: {\n artifactLocation: {\n uri: string;\n uriBaseId?: string;\n };\n region?: {\n startLine: number;\n startColumn?: number;\n };\n };\n logicalLocations?: Array<{\n name: string;\n kind: string;\n }>;\n}\n\ninterface SarifInvocation {\n executionSuccessful: boolean;\n startTimeUtc?: string;\n endTimeUtc?: string;\n properties?: Record<string, unknown>;\n}\n\ninterface SarifArtifact {\n location: {\n uri: string;\n };\n length?: number;\n}\n\n// ── CWE Mappings ───────────────────────────────────────────────────────\n\n/**\n * Map Vulcn vulnerability types to CWE IDs.\n * These are the most specific CWE entries for each category.\n */\nconst CWE_MAP: Record<string, { id: number; name: string }> = {\n xss: {\n id: 79,\n name: \"Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')\",\n },\n sqli: {\n id: 89,\n name: \"Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')\",\n },\n ssrf: { id: 918, name: \"Server-Side Request Forgery (SSRF)\" },\n xxe: {\n id: 611,\n name: \"Improper Restriction of XML External Entity Reference\",\n },\n \"command-injection\": {\n id: 78,\n name: \"Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')\",\n },\n \"path-traversal\": {\n id: 22,\n name: \"Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\",\n },\n \"open-redirect\": {\n id: 601,\n name: \"URL Redirection to Untrusted Site ('Open Redirect')\",\n },\n reflection: {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n \"security-misconfiguration\": {\n id: 16,\n name: \"Configuration\",\n },\n \"information-disclosure\": {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n custom: { id: 20, name: \"Improper Input Validation\" },\n};\n\n/**\n * Map Vulcn severities to SARIF levels.\n *\n * SARIF only has: error, warning, note, none\n * - critical/high → error\n * - medium → warning\n * - low/info → note\n */\nfunction toSarifLevel(severity: Finding[\"severity\"]): SarifLevel {\n switch (severity) {\n case \"critical\":\n case \"high\":\n return \"error\";\n case \"medium\":\n return \"warning\";\n case \"low\":\n case \"info\":\n return \"note\";\n default:\n return \"warning\";\n }\n}\n\n/**\n * Map Vulcn severities to CVSS-like security-severity scores.\n * GitHub uses this for sorting in the Security tab.\n *\n * Scale: 0.0–10.0\n * critical: 9.0\n * high: 7.0\n * medium: 4.0\n * low: 2.0\n * info: 0.0\n */\nfunction toSecuritySeverity(severity: Finding[\"severity\"]): string {\n switch (severity) {\n case \"critical\":\n return \"9.0\";\n case \"high\":\n return \"7.0\";\n case \"medium\":\n return \"4.0\";\n case \"low\":\n return \"2.0\";\n case \"info\":\n return \"0.0\";\n default:\n return \"4.0\";\n }\n}\n\n/**\n * Map Vulcn severities to SARIF precision.\n */\nfunction toPrecision(\n severity: Finding[\"severity\"],\n): SarifRule[\"properties\"][\"precision\"] {\n switch (severity) {\n case \"critical\":\n return \"very-high\";\n case \"high\":\n return \"high\";\n case \"medium\":\n return \"medium\";\n case \"low\":\n case \"info\":\n return \"low\";\n default:\n return \"medium\";\n }\n}\n\n// ── Rule Generation ────────────────────────────────────────────────────\n\n/**\n * Generate a unique rule ID from a finding type.\n *\n * Format: VULCN-<TYPE>\n * Example: VULCN-XSS, VULCN-SQLI\n */\nfunction toRuleId(type: string): string {\n return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, \"-\")}`;\n}\n\n/**\n * Build SARIF rules from unique finding types.\n * Each unique vulnerability type becomes one rule.\n */\nfunction buildRules(findings: Finding[]): SarifRule[] {\n const seenTypes = new Map<string, Finding>();\n\n for (const f of findings) {\n if (!seenTypes.has(f.type)) {\n seenTypes.set(f.type, f);\n }\n }\n\n return Array.from(seenTypes.entries()).map(([type, sampleFinding]) => {\n const cwe = CWE_MAP[type] || CWE_MAP.custom;\n const ruleId = toRuleId(type);\n\n return {\n id: ruleId,\n name: type,\n shortDescription: {\n text: `${cwe.name} (CWE-${cwe.id})`,\n },\n fullDescription: {\n text: `Vulcn detected a potential ${type} vulnerability. ${cwe.name}. See CWE-${cwe.id} for details.`,\n },\n helpUri: `https://cwe.mitre.org/data/definitions/${cwe.id}.html`,\n help: {\n text: `## ${cwe.name}\\n\\nCWE-${cwe.id}: ${cwe.name}\\n\\nThis rule detects ${type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee https://cwe.mitre.org/data/definitions/${cwe.id}.html for detailed remediation guidance.`,\n markdown: `## ${cwe.name}\\n\\n**CWE-${cwe.id}**: ${cwe.name}\\n\\nThis rule detects \\`${type}\\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee [CWE-${cwe.id}](https://cwe.mitre.org/data/definitions/${cwe.id}.html) for detailed remediation guidance.`,\n },\n properties: {\n tags: [\"security\", `CWE-${cwe.id}`, `external/cwe/cwe-${cwe.id}`],\n precision: toPrecision(sampleFinding.severity),\n \"security-severity\": toSecuritySeverity(sampleFinding.severity),\n },\n defaultConfiguration: {\n level: toSarifLevel(sampleFinding.severity),\n },\n };\n });\n}\n\n// ── Result Generation ──────────────────────────────────────────────────\n\n/**\n * Convert a Vulcn Finding to a SARIF Result.\n */\nfunction toSarifResult(finding: Finding, rules: SarifRule[]): SarifResult {\n const ruleId = toRuleId(finding.type);\n const ruleIndex = rules.findIndex((r) => r.id === ruleId);\n\n // Build message with evidence if available\n let messageText = `${finding.title}\\n\\n${finding.description}`;\n if (finding.evidence) {\n messageText += `\\n\\nEvidence: ${finding.evidence}`;\n }\n messageText += `\\n\\nPayload: ${finding.payload}`;\n\n // Build location from URL\n const uri = finding.url || \"unknown\";\n\n // Generate fingerprint from finding properties\n const fingerprint = `${finding.type}:${finding.stepId}:${finding.payload.slice(0, 50)}`;\n\n return {\n ruleId,\n ruleIndex: Math.max(ruleIndex, 0),\n level: toSarifLevel(finding.severity),\n message: { text: messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: {\n uri,\n },\n region: {\n startLine: 1,\n },\n },\n logicalLocations: [\n {\n name: finding.stepId,\n kind: \"test-step\",\n },\n ],\n },\n ],\n fingerprints: {\n vulcnFindingV1: fingerprint,\n },\n partialFingerprints: {\n vulcnType: finding.type,\n vulcnStepId: finding.stepId,\n },\n properties: {\n severity: finding.severity,\n payload: finding.payload,\n stepId: finding.stepId,\n ...(finding.evidence ? { evidence: finding.evidence } : {}),\n ...(finding.metadata || {}),\n },\n };\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\n/**\n * Generate a SARIF v2.1.0 log from Vulcn scan results.\n *\n * Usage:\n * const sarif = generateSarif(session, result, generatedAt, \"0.4.0\");\n * await writeFile(\"vulcn-report.sarif\", JSON.stringify(sarif, null, 2));\n *\n * The output can be uploaded to:\n * - GitHub Code Scanning: `gh api /repos/{owner}/{repo}/code-scanning/sarifs`\n * - GitHub Actions: `github/codeql-action/upload-sarif@v3`\n * - Azure DevOps: SARIF SAST Scans Tab extension\n *\n * @param session - The session that was executed\n * @param result - The run result with findings\n * @param generatedAt - ISO timestamp\n * @param engineVersion - Vulcn engine version\n */\nexport function generateSarif(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): SarifLog {\n const rules = buildRules(result.findings);\n const results = result.findings.map((f) => toSarifResult(f, rules));\n\n // Build artifact list from unique URLs\n const uniqueUrls = [\n ...new Set(result.findings.map((f) => f.url).filter(Boolean)),\n ];\n const artifacts: SarifArtifact[] = uniqueUrls.map((url) => ({\n location: { uri: url },\n }));\n\n // Calculate end time from duration\n const startDate = new Date(generatedAt);\n const endDate = new Date(startDate.getTime() + result.duration);\n\n const sarifLog: SarifLog = {\n $schema:\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"Vulcn\",\n version: engineVersion,\n semanticVersion: engineVersion,\n informationUri: \"https://vulcn.dev\",\n rules,\n },\n },\n results,\n invocations: [\n {\n executionSuccessful: result.errors.length === 0,\n startTimeUtc: generatedAt,\n endTimeUtc: endDate.toISOString(),\n properties: {\n sessionName: session.name,\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n ...(result.errors.length > 0 ? { errors: result.errors } : {}),\n },\n },\n ],\n ...(artifacts.length > 0 ? { artifacts } : {}),\n },\n ],\n };\n\n return sarifLog;\n}\n"],"mappings":"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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":["formatDuration"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/report-model.ts","../src/html.ts","../src/json.ts","../src/yaml.ts","../src/sarif.ts"],"sourcesContent":["/**\n * @vulcn/plugin-report\n * Report Generation Plugin for Vulcn\n *\n * Generates security reports in HTML, JSON, YAML, and SARIF formats\n * after a run completes. All formats are projections of the canonical\n * VulcnReport model, built once via buildReport().\n *\n * Configuration:\n * format: \"html\" | \"json\" | \"yaml\" | \"sarif\" | \"all\" (default: \"html\")\n * outputDir: directory for reports (default: \".\")\n * filename: base filename (no extension) (default: \"vulcn-report\")\n * open: auto-open HTML in browser (default: false)\n */\n\nimport { z } from \"zod\";\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport type {\n VulcnPlugin,\n PluginContext,\n PluginRunContext,\n RunResult,\n} from \"@vulcn/engine\";\n\nimport { buildReport } from \"./report-model\";\nimport { generateHtml } from \"./html\";\nimport { generateJson } from \"./json\";\nimport { generateYaml } from \"./yaml\";\nimport { generateSarif } from \"./sarif\";\n\n/**\n * Plugin configuration schema\n */\nconst configSchema = z.object({\n /**\n * Report format(s) to generate\n * - \"html\": Beautiful dark-themed HTML report\n * - \"json\": Machine-readable structured JSON\n * - \"yaml\": Human-readable YAML\n * - \"sarif\": SARIF v2.1.0 for GitHub Code Scanning\n * - \"all\": Generate all formats\n * @default \"html\"\n */\n format: z.enum([\"html\", \"json\", \"yaml\", \"sarif\", \"all\"]).default(\"html\"),\n\n /**\n * Output directory for report files\n * @default \".\"\n */\n outputDir: z.string().default(\".\"),\n\n /**\n * Base filename (without extension) for the report\n * @default \"vulcn-report\"\n */\n filename: z.string().default(\"vulcn-report\"),\n\n /**\n * Auto-open HTML report in default browser after generation\n * @default false\n */\n open: z.boolean().default(false),\n});\n\nexport type ReportConfig = z.infer<typeof configSchema>;\n\n/**\n * Determine which formats to generate\n */\nfunction getFormats(format: ReportConfig[\"format\"]): string[] {\n if (format === \"all\") return [\"html\", \"json\", \"yaml\", \"sarif\"];\n return [format];\n}\n\n/**\n * Report Plugin\n */\nconst plugin: VulcnPlugin = {\n name: \"@vulcn/plugin-report\",\n version: \"0.1.0\",\n apiVersion: 1,\n description:\n \"Report generation plugin — generates HTML, JSON, YAML, and SARIF security reports\",\n\n configSchema,\n\n hooks: {\n onInit: async (ctx: PluginContext) => {\n const config = configSchema.parse(ctx.config);\n ctx.logger.info(\n `Report plugin initialized (format: ${config.format}, output: ${config.outputDir}/${config.filename})`,\n );\n },\n\n /**\n * Generate report(s) after run completes.\n *\n * Architecture: RunResult + Session → buildReport() → VulcnReport\n * Each output format is a pure projection of the canonical model.\n */\n onRunEnd: async (\n result: RunResult,\n ctx: PluginRunContext,\n ): Promise<RunResult> => {\n const config = configSchema.parse(ctx.config);\n const formats = getFormats(config.format);\n\n // Build the canonical report model once — all formats derive from it\n const report = buildReport(\n ctx.session,\n result,\n new Date().toISOString(),\n ctx.engine.version,\n );\n\n // Ensure output directory exists\n const outDir = resolve(config.outputDir);\n await mkdir(outDir, { recursive: true });\n\n const basePath = resolve(outDir, config.filename);\n const writtenFiles: string[] = [];\n\n for (const fmt of formats) {\n try {\n switch (fmt) {\n case \"html\": {\n const html = generateHtml(report);\n const htmlPath = `${basePath}.html`;\n await writeFile(htmlPath, html, \"utf-8\");\n writtenFiles.push(htmlPath);\n ctx.logger.info(`📄 HTML report: ${htmlPath}`);\n break;\n }\n\n case \"json\": {\n const jsonReport = generateJson(report);\n const jsonPath = `${basePath}.json`;\n await writeFile(\n jsonPath,\n JSON.stringify(jsonReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(jsonPath);\n ctx.logger.info(`📄 JSON report: ${jsonPath}`);\n break;\n }\n\n case \"yaml\": {\n const yamlContent = generateYaml(report);\n const yamlPath = `${basePath}.yml`;\n await writeFile(yamlPath, yamlContent, \"utf-8\");\n writtenFiles.push(yamlPath);\n ctx.logger.info(`📄 YAML report: ${yamlPath}`);\n break;\n }\n\n case \"sarif\": {\n const sarifReport = generateSarif(report);\n const sarifPath = `${basePath}.sarif`;\n await writeFile(\n sarifPath,\n JSON.stringify(sarifReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(sarifPath);\n ctx.logger.info(`📄 SARIF report: ${sarifPath}`);\n break;\n }\n }\n } catch (err) {\n ctx.logger.error(\n `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Auto-open HTML report if configured\n if (config.open && formats.includes(\"html\")) {\n const htmlPath = `${basePath}.html`;\n try {\n const { exec } = await import(\"node:child_process\");\n const openCmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${openCmd} \"${htmlPath}\"`);\n } catch {\n // Silently ignore if can't open browser\n }\n }\n\n return result;\n },\n },\n};\n\nexport default plugin;\n\n// Named exports for programmatic usage\nexport {\n configSchema,\n generateHtml,\n generateJson,\n generateYaml,\n generateSarif,\n buildReport,\n};\n\n// Re-export the canonical model types\nexport type {\n VulcnReport,\n EnrichedFinding,\n ReportRule,\n PassiveAnalysis,\n PassiveCategorySummary,\n RiskAssessment,\n SeverityCounts,\n CweEntry,\n PassiveCheckDefinition,\n} from \"./report-model\";\n\nexport type { JsonReport } from \"./json\";\nexport type { SarifLog } from \"./sarif\";\n","/**\n * Vulcn Report Model\n *\n * The canonical data model from which all output formats (HTML, JSON,\n * YAML, SARIF) are derived. This is the single source of truth for\n * CWE mappings, severity scores, fingerprinting, risk computation,\n * and passive analysis categorisation.\n *\n * RunResult + Session\n * ↓\n * buildReport()\n * ↓\n * VulcnReport (canonical)\n * ↓\n * ┌──────┴──────────┬──────────┬──────────┐\n * HTML JSON YAML SARIF\n *\n * Every output format is a projection of this model.\n */\n\nimport type {\n Finding,\n RunResult,\n Session,\n PayloadCategory,\n} from \"@vulcn/engine\";\n\n// ── CWE Registry ───────────────────────────────────────────────────────\n\nexport interface CweEntry {\n id: number;\n name: string;\n}\n\n/**\n * Map Vulcn vulnerability types to CWE IDs.\n * Single source — used by every output format.\n */\nexport const CWE_MAP: Record<string, CweEntry> = {\n xss: {\n id: 79,\n name: \"Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')\",\n },\n sqli: {\n id: 89,\n name: \"Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')\",\n },\n ssrf: { id: 918, name: \"Server-Side Request Forgery (SSRF)\" },\n xxe: {\n id: 611,\n name: \"Improper Restriction of XML External Entity Reference\",\n },\n \"command-injection\": {\n id: 78,\n name: \"Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')\",\n },\n \"path-traversal\": {\n id: 22,\n name: \"Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\",\n },\n \"open-redirect\": {\n id: 601,\n name: \"URL Redirection to Untrusted Site ('Open Redirect')\",\n },\n reflection: {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n \"security-misconfiguration\": {\n id: 16,\n name: \"Configuration\",\n },\n \"information-disclosure\": {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n custom: { id: 20, name: \"Improper Input Validation\" },\n};\n\n// ── Severity Scoring ───────────────────────────────────────────────────\n\nexport type Severity = \"critical\" | \"high\" | \"medium\" | \"low\" | \"info\";\n\nconst SEVERITY_WEIGHTS: Record<Severity, number> = {\n critical: 10,\n high: 7,\n medium: 4,\n low: 1,\n info: 0,\n};\n\n/**\n * CVSS-like security-severity scores (0.0–10.0).\n * Used by GitHub Security tab for sorting.\n */\nexport const SECURITY_SEVERITY: Record<Severity, string> = {\n critical: \"9.0\",\n high: \"7.0\",\n medium: \"4.0\",\n low: \"2.0\",\n info: \"0.0\",\n};\n\n// ── Passive Analysis Types ─────────────────────────────────────────────\n\nexport interface PassiveCheckDefinition {\n id: string;\n label: string;\n icon: string;\n color: string;\n remedy: string;\n checks: string[];\n}\n\n/**\n * All passive check categories with their check lists and remediation\n * guidance. Single source — used by HTML and JSON renderers alike.\n */\nexport const PASSIVE_CATEGORIES: PassiveCheckDefinition[] = [\n {\n id: \"security-headers\",\n label: \"Security Headers\",\n icon: \"🔒\",\n color: \"#42a5f5\",\n remedy:\n \"Add the recommended security headers to your server configuration. Most web servers and frameworks support these via middleware.\",\n checks: [\n \"Strict-Transport-Security (HSTS)\",\n \"Content-Security-Policy (CSP)\",\n \"X-Content-Type-Options\",\n \"X-Frame-Options\",\n \"Referrer-Policy\",\n \"Permissions-Policy\",\n ],\n },\n {\n id: \"cookie-security\",\n label: \"Cookie Security\",\n icon: \"🍪\",\n color: \"#ffab40\",\n remedy:\n \"Set the Secure, HttpOnly, and SameSite attributes on all session cookies. Configure your framework's session middleware accordingly.\",\n checks: [\"Secure flag\", \"HttpOnly flag\", \"SameSite attribute\"],\n },\n {\n id: \"information-disclosure\",\n label: \"Information Disclosure\",\n icon: \"🔍\",\n color: \"#66bb6a\",\n remedy:\n \"Remove or obfuscate server version headers (Server, X-Powered-By). Disable debug mode in production environments.\",\n checks: [\"Server version\", \"X-Powered-By\", \"Debug tokens\"],\n },\n {\n id: \"cors\",\n label: \"CORS Configuration\",\n icon: \"🌐\",\n color: \"#ce93d8\",\n remedy:\n \"Replace wildcard origins with specific trusted domains. Never combine Access-Control-Allow-Credentials with wildcard origins.\",\n checks: [\"Wildcard origin\", \"Credentials with wildcard\"],\n },\n {\n id: \"mixed-content\",\n label: \"Mixed Content\",\n icon: \"⚠️\",\n color: \"#ff8a65\",\n remedy:\n \"Replace all HTTP resource URLs with HTTPS. Use Content-Security-Policy: upgrade-insecure-requests as a fallback.\",\n checks: [\"HTTP resources on HTTPS\"],\n },\n];\n\n// ── Enriched Finding ───────────────────────────────────────────────────\n\nexport interface EnrichedFinding {\n /** Original raw finding fields */\n type: PayloadCategory;\n severity: Severity;\n title: string;\n description: string;\n stepId: string;\n payload: string;\n url: string;\n evidence?: string;\n metadata?: Record<string, unknown>;\n\n /** Enriched fields — computed by buildReport() */\n ruleId: string;\n cwe: CweEntry;\n securitySeverity: string;\n fingerprint: string;\n\n /** Detection classification */\n detectionMethod: \"active\" | \"passive\";\n /** Passive category (only set when detectionMethod === \"passive\") */\n passiveCategory?: string;\n}\n\n// ── Rule ────────────────────────────────────────────────────────────────\n\nexport interface ReportRule {\n /** e.g. \"VULCN-XSS\" */\n id: string;\n /** Raw type name e.g. \"xss\" */\n type: string;\n /** CWE info */\n cwe: CweEntry;\n /** Severity of the first finding with this rule */\n severity: Severity;\n securitySeverity: string;\n /** Description for help text */\n description: string;\n}\n\n// ── Passive Category Summary ───────────────────────────────────────────\n\nexport interface PassiveCategorySummary {\n definition: PassiveCheckDefinition;\n /** Findings in this category */\n findings: EnrichedFinding[];\n /** Issue count */\n issueCount: number;\n /** Number of checks that passed */\n passedChecks: number;\n /** Total checks for this category */\n totalChecks: number;\n /** PASS | WARN | FAIL */\n status: \"pass\" | \"warn\" | \"fail\";\n}\n\nexport interface PassiveAnalysis {\n totalIssues: number;\n categories: PassiveCategorySummary[];\n}\n\n// ── Risk Assessment ────────────────────────────────────────────────────\n\nexport interface RiskAssessment {\n score: number;\n percent: number;\n label: \"Critical\" | \"High\" | \"Medium\" | \"Low\" | \"Clear\";\n}\n\n// ── Severity Counts ────────────────────────────────────────────────────\n\nexport type SeverityCounts = Record<Severity, number>;\n\n// ── The Canonical Model ────────────────────────────────────────────────\n\nexport interface VulcnReport {\n /** Report format version */\n reportVersion: \"2.0\";\n\n /** Engine version that generated the scan */\n engineVersion: string;\n\n /** ISO timestamp when this report was generated */\n generatedAt: string;\n\n /** Session metadata */\n session: {\n name: string;\n driver: string;\n driverConfig: Record<string, unknown>;\n stepsCount: number;\n metadata?: Record<string, unknown>;\n };\n\n /** Execution statistics */\n stats: {\n stepsExecuted: number;\n payloadsTested: number;\n durationMs: number;\n errors: string[];\n };\n\n /** Summary / overview data */\n summary: {\n totalFindings: number;\n severityCounts: SeverityCounts;\n risk: RiskAssessment;\n vulnerabilityTypes: string[];\n affectedUrls: string[];\n };\n\n /** Rule registry — one rule per unique finding type */\n rules: ReportRule[];\n\n /** All findings — enriched with CWE, fingerprints, classification */\n findings: EnrichedFinding[];\n\n /** Active findings (subset) */\n activeFindings: EnrichedFinding[];\n\n /** Passive analysis summary */\n passiveAnalysis: PassiveAnalysis;\n}\n\n// ── Builder ────────────────────────────────────────────────────────────\n\n/**\n * Generate a stable rule ID from a finding type.\n * Format: VULCN-<TYPE> e.g. VULCN-XSS, VULCN-SQLI\n */\nexport function toRuleId(type: string): string {\n return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, \"-\")}`;\n}\n\n/**\n * Generate a stable fingerprint for deduplication across runs.\n */\nfunction fingerprint(f: Finding): string {\n return `${f.type}:${f.stepId}:${f.payload.slice(0, 50)}`;\n}\n\n/**\n * Determine detection method from finding metadata.\n */\nfunction detectMethod(f: Finding): \"active\" | \"passive\" {\n return (f.metadata as Record<string, unknown>)?.detectionMethod === \"passive\"\n ? \"passive\"\n : \"active\";\n}\n\n/**\n * Get passive category from finding metadata.\n */\nfunction passiveCat(f: Finding): string | undefined {\n const method = detectMethod(f);\n if (method !== \"passive\") return undefined;\n return (\n ((f.metadata as Record<string, unknown>)?.category as string) || \"other\"\n );\n}\n\n/**\n * Enrich a raw Finding with CWE, fingerprint, classification.\n */\nfunction enrichFinding(f: Finding): EnrichedFinding {\n const cwe = CWE_MAP[f.type] || CWE_MAP.custom;\n const sev = f.severity as Severity;\n\n return {\n // Original fields\n type: f.type,\n severity: sev,\n title: f.title,\n description: f.description,\n stepId: f.stepId,\n payload: f.payload,\n url: f.url,\n evidence: f.evidence,\n metadata: f.metadata,\n\n // Enriched\n ruleId: toRuleId(f.type),\n cwe,\n securitySeverity: SECURITY_SEVERITY[sev] || \"4.0\",\n fingerprint: fingerprint(f),\n detectionMethod: detectMethod(f),\n passiveCategory: passiveCat(f),\n };\n}\n\n/**\n * Build rules from unique finding types.\n */\nfunction buildRules(enriched: EnrichedFinding[]): ReportRule[] {\n const seen = new Map<string, EnrichedFinding>();\n for (const f of enriched) {\n if (!seen.has(f.type)) seen.set(f.type, f);\n }\n\n return Array.from(seen.entries()).map(([type, sample]) => ({\n id: toRuleId(type),\n type,\n cwe: sample.cwe,\n severity: sample.severity,\n securitySeverity: sample.securitySeverity,\n description: `Vulcn detected a potential ${type} vulnerability. ${sample.cwe.name}.`,\n }));\n}\n\n/**\n * Compute severity counts.\n */\nfunction countSeverities(findings: EnrichedFinding[]): SeverityCounts {\n const counts: SeverityCounts = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n return counts;\n}\n\n/**\n * Compute overall risk assessment.\n */\nfunction assessRisk(counts: SeverityCounts, total: number): RiskAssessment {\n const score =\n counts.critical * SEVERITY_WEIGHTS.critical +\n counts.high * SEVERITY_WEIGHTS.high +\n counts.medium * SEVERITY_WEIGHTS.medium +\n counts.low * SEVERITY_WEIGHTS.low;\n\n const maxRisk = total * SEVERITY_WEIGHTS.critical || 1;\n const percent = Math.min(100, Math.round((score / maxRisk) * 100));\n\n const label: RiskAssessment[\"label\"] =\n percent >= 80\n ? \"Critical\"\n : percent >= 50\n ? \"High\"\n : percent >= 25\n ? \"Medium\"\n : percent > 0\n ? \"Low\"\n : \"Clear\";\n\n return { score, percent, label };\n}\n\n/**\n * Build the passive analysis summary.\n */\nfunction buildPassiveAnalysis(\n passiveFindings: EnrichedFinding[],\n): PassiveAnalysis {\n // Group by category\n const grouped = new Map<string, EnrichedFinding[]>();\n for (const f of passiveFindings) {\n const cat = f.passiveCategory || \"other\";\n if (!grouped.has(cat)) grouped.set(cat, []);\n grouped.get(cat)!.push(f);\n }\n\n const categories: PassiveCategorySummary[] = PASSIVE_CATEGORIES.map((def) => {\n const findings = grouped.get(def.id) || [];\n const issueCount = findings.length;\n const totalChecks = def.checks.length;\n const passedChecks = Math.max(0, totalChecks - issueCount);\n const status: PassiveCategorySummary[\"status\"] =\n issueCount === 0 ? \"pass\" : issueCount >= 3 ? \"fail\" : \"warn\";\n\n return {\n definition: def,\n findings,\n issueCount,\n passedChecks,\n totalChecks,\n status,\n };\n });\n\n return {\n totalIssues: passiveFindings.length,\n categories,\n };\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\n/**\n * Build a VulcnReport from raw scan output.\n *\n * This is the single transformation point. All output formats\n * (HTML, JSON, YAML, SARIF) operate on the returned model.\n */\nexport function buildReport(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): VulcnReport {\n // Sort by severity (critical first)\n const severityOrder: Record<string, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n info: 4,\n };\n\n const sortedFindings = [...result.findings].sort(\n (a, b) =>\n (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5),\n );\n\n // Enrich all findings\n const findings = sortedFindings.map(enrichFinding);\n\n // Partition\n const activeFindings = findings.filter((f) => f.detectionMethod === \"active\");\n const passiveFindings = findings.filter(\n (f) => f.detectionMethod === \"passive\",\n );\n\n // Compute aggregates\n const counts = countSeverities(findings);\n const risk = assessRisk(counts, findings.length);\n const rules = buildRules(findings);\n\n return {\n reportVersion: \"2.0\",\n engineVersion,\n generatedAt,\n\n session: {\n name: session.name,\n driver: session.driver,\n driverConfig: session.driverConfig,\n stepsCount: session.steps.length,\n metadata: session.metadata,\n },\n\n stats: {\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n errors: result.errors,\n },\n\n summary: {\n totalFindings: findings.length,\n severityCounts: counts,\n risk,\n vulnerabilityTypes: [...new Set(findings.map((f) => f.type))],\n affectedUrls: [...new Set(findings.map((f) => f.url))],\n },\n\n rules,\n findings,\n activeFindings,\n passiveAnalysis: buildPassiveAnalysis(passiveFindings),\n };\n}\n\n/**\n * Format milliseconds to human-readable duration.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n","/**\n * HTML Report Generator for Vulcn\n *\n * Generates a modern, dark-themed security report with:\n * - Vulcn branding (shield gradient logo)\n * - Executive summary with severity donut chart\n * - Detailed findings with expandable evidence\n * - Timeline of execution\n * - Responsive design\n */\n\nimport type {\n VulcnReport,\n EnrichedFinding,\n PassiveCategorySummary,\n} from \"./report-model\";\nimport { formatDuration } from \"./report-model\";\n\n// Vulcn brand colors\nconst COLORS = {\n bg: \"#0a0a0f\",\n surface: \"#12121a\",\n surfaceHover: \"#1a1a26\",\n border: \"#1e1e2e\",\n borderActive: \"#2a2a3e\",\n text: \"#e4e4ef\",\n textMuted: \"#8888a0\",\n textDim: \"#555570\",\n accent: \"#fa1b1b\",\n accentGlow: \"rgba(250, 27, 27, 0.15)\",\n accentLight: \"#ff9c9c\",\n critical: \"#ff1744\",\n high: \"#ff5252\",\n medium: \"#ffab40\",\n low: \"#66bb6a\",\n info: \"#42a5f5\",\n success: \"#00e676\",\n};\n\nfunction severityColor(severity: string): string {\n switch (severity) {\n case \"critical\":\n return COLORS.critical;\n case \"high\":\n return COLORS.high;\n case \"medium\":\n return COLORS.medium;\n case \"low\":\n return COLORS.low;\n case \"info\":\n return COLORS.info;\n default:\n return COLORS.textMuted;\n }\n}\n\nfunction severityOrder(severity: string): number {\n switch (severity) {\n case \"critical\":\n return 0;\n case \"high\":\n return 1;\n case \"medium\":\n return 2;\n case \"low\":\n return 3;\n case \"info\":\n return 4;\n default:\n return 5;\n }\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction formatDate(iso: string): string {\n const d = new Date(iso);\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n });\n}\n\n// Inline SVG logo matching the vulcn shield branding\nconst VULCN_LOGO_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"32\" height=\"32\">\n <defs>\n <linearGradient id=\"lg1\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(7 -13 13 7 7 17)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#fa1b1b\"/>\n <stop offset=\"1\" stop-color=\"#ff9c9c\"/>\n </linearGradient>\n <linearGradient id=\"lg2\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(3 -6 6 3 13 14)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#ff9c9c\"/>\n <stop offset=\"1\" stop-color=\"#ffffff\"/>\n </linearGradient>\n </defs>\n <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\"/>\n <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\"/>\n <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\"/>\n</svg>`;\n\nfunction renderPassiveSection(\n analysis: VulcnReport[\"passiveAnalysis\"],\n): string {\n if (analysis.totalIssues === 0) {\n return `\n <div class=\"passive-section animate-in-delay-3\">\n <div class=\"section-header\">\n <h3>🛡️ Passive Security Analysis</h3>\n <span class=\"findings-count\">All clear</span>\n </div>\n <div class=\"passive-clear\">\n <div class=\"icon\">✅</div>\n <h4>All Passive Checks Passed</h4>\n <p>No security header, cookie, CORS, or information disclosure issues detected.</p>\n </div>\n </div>`;\n }\n\n const categoryCards = analysis.categories\n .map((cat) => {\n const statusColor =\n cat.status === \"pass\"\n ? COLORS.success\n : cat.status === \"fail\"\n ? COLORS.high\n : COLORS.medium;\n\n return `\n <div class=\"passive-category-card\">\n <div class=\"passive-cat-header\">\n <div class=\"passive-cat-icon\">${cat.definition.icon}</div>\n <div class=\"passive-cat-info\">\n <div class=\"passive-cat-title\">${cat.definition.label}</div>\n <div class=\"passive-cat-count\" style=\"color: ${statusColor}\">\n ${cat.issueCount === 0 ? \"✓ All clear\" : `${cat.issueCount} issue${cat.issueCount !== 1 ? \"s\" : \"\"}`}\n </div>\n </div>\n <div class=\"passive-cat-badge\" style=\"background: ${statusColor}20; color: ${statusColor}; border-color: ${statusColor}30\">\n ${cat.status.toUpperCase()}\n </div>\n </div>\n ${\n cat.findings.length > 0\n ? `\n <div class=\"passive-cat-findings\">\n ${cat.findings\n .map(\n (f) => `\n <div class=\"passive-finding-row\">\n <div class=\"passive-finding-dot\" style=\"background: ${severityColor(f.severity)}\"></div>\n <div class=\"passive-finding-content\">\n <div class=\"passive-finding-title\">${escapeHtml(f.title)}</div>\n <div class=\"passive-finding-desc\">${escapeHtml(f.description)}</div>\n ${f.evidence ? `<div class=\"passive-finding-evidence\">${escapeHtml(f.evidence)}</div>` : \"\"}\n </div>\n <span class=\"passive-sev-tag\" style=\"color: ${severityColor(f.severity)}\">${f.severity.toUpperCase()}</span>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n <div class=\"passive-remedy\">\n <span class=\"passive-remedy-label\">💡 Remediation</span>\n <span>${cat.definition.remedy}</span>\n </div>\n `\n : `\n <div class=\"passive-checks-passed\">\n ${cat.definition.checks\n .map(\n (check) => `\n <div class=\"passive-check-item\">\n <span class=\"passive-check-icon\">✓</span>\n <span>${check}</span>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n `\n }\n </div>`;\n })\n .join(\"\");\n\n return `\n <div class=\"passive-section animate-in-delay-3\">\n <div class=\"section-header\">\n <h3>🛡️ Passive Security Analysis</h3>\n <span class=\"findings-count\">${analysis.totalIssues} issue${analysis.totalIssues !== 1 ? \"s\" : \"\"}</span>\n </div>\n <div class=\"passive-grid\">\n ${categoryCards}\n </div>\n </div>`;\n}\n\nexport function generateHtml(report: VulcnReport): string {\n // All data comes from the canonical model — no duplicate computation\n const { summary, stats, session, passiveAnalysis, activeFindings, findings } =\n report;\n const {\n severityCounts: counts,\n risk,\n totalFindings,\n affectedUrls,\n vulnerabilityTypes: vulnTypes,\n } = summary;\n const { stepsExecuted, payloadsTested, durationMs, errors } = stats;\n const hasFindings = totalFindings > 0;\n\n const riskColor =\n risk.percent >= 80\n ? COLORS.critical\n : risk.percent >= 50\n ? COLORS.high\n : risk.percent >= 25\n ? COLORS.medium\n : risk.percent > 0\n ? COLORS.low\n : COLORS.success;\n\n // Donut chart SVG segments\n const donutSvg = generateDonut(counts, totalFindings);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Vulcn Security Report — ${escapeHtml(session.name)}</title>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n :root {\n --bg: ${COLORS.bg};\n --surface: ${COLORS.surface};\n --surface-hover: ${COLORS.surfaceHover};\n --border: ${COLORS.border};\n --border-active: ${COLORS.borderActive};\n --text: ${COLORS.text};\n --text-muted: ${COLORS.textMuted};\n --text-dim: ${COLORS.textDim};\n --accent: ${COLORS.accent};\n --accent-glow: ${COLORS.accentGlow};\n --accent-light: ${COLORS.accentLight};\n --radius: 12px;\n --radius-sm: 8px;\n --radius-xs: 6px;\n }\n\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n }\n\n /* Ambient gradient background */\n body::before {\n content: '';\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 600px;\n background: radial-gradient(ellipse 80% 50% at 50% -20%, ${COLORS.accentGlow} 0%, transparent 100%);\n pointer-events: none;\n z-index: 0;\n }\n\n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 24px;\n position: relative;\n z-index: 1;\n }\n\n /* Header */\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 48px;\n padding-bottom: 24px;\n border-bottom: 1px solid var(--border);\n }\n\n .header-brand {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .header-brand svg {\n filter: drop-shadow(0 0 8px rgba(250, 27, 27, 0.3));\n }\n\n .header-brand h1 {\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -0.02em;\n background: linear-gradient(135deg, #fa1b1b, #ff9c9c);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .header-brand span {\n font-size: 11px;\n font-weight: 500;\n color: var(--text-dim);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .header-meta {\n text-align: right;\n font-size: 12px;\n color: var(--text-dim);\n line-height: 1.8;\n }\n\n /* Session info */\n .session-info {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 24px;\n margin-bottom: 32px;\n }\n\n .session-info h2 {\n font-size: 22px;\n font-weight: 700;\n margin-bottom: 16px;\n letter-spacing: -0.02em;\n }\n\n .session-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n }\n\n .meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .meta-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .meta-value {\n font-size: 14px;\n font-weight: 500;\n color: var(--text);\n font-family: 'JetBrains Mono', monospace;\n font-size: 13px;\n }\n\n /* Stats grid */\n .stats-grid {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 24px;\n margin-bottom: 32px;\n }\n\n @media (max-width: 768px) {\n .stats-grid { grid-template-columns: 1fr; }\n }\n\n /* Risk gauge */\n .risk-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n }\n\n .risk-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n width: 100%;\n }\n\n .risk-gauge {\n position: relative;\n width: 160px;\n height: 160px;\n }\n\n .risk-gauge svg {\n transform: rotate(-90deg);\n }\n\n .risk-gauge-label {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .risk-gauge-label .score {\n font-size: 36px;\n font-weight: 800;\n letter-spacing: -0.03em;\n }\n\n .risk-gauge-label .label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n }\n\n /* Summary card */\n .summary-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n }\n\n .summary-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n margin-bottom: 20px;\n }\n\n .summary-stats {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 20px;\n }\n\n .stat-box {\n padding: 16px;\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n transition: border-color 0.2s;\n }\n\n .stat-box:hover { border-color: var(--border-active); }\n\n .stat-number {\n font-size: 28px;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1;\n margin-bottom: 4px;\n }\n\n .stat-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--text-muted);\n }\n\n /* Severity breakdown */\n .severity-breakdown {\n margin-bottom: 32px;\n }\n\n .severity-section-header {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 16px;\n }\n\n .severity-section-header h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .severity-bars {\n display: flex;\n gap: 8px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 20px 24px;\n }\n\n .severity-bar-item {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n }\n\n .severity-bar-track {\n width: 100%;\n height: 6px;\n background: rgba(255,255,255,0.04);\n border-radius: 3px;\n overflow: hidden;\n }\n\n .severity-bar-fill {\n height: 100%;\n border-radius: 3px;\n transition: width 0.5s ease;\n }\n\n .severity-bar-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .severity-bar-count {\n font-size: 18px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n }\n\n /* Findings section */\n .findings-section {\n margin-bottom: 32px;\n }\n\n .findings-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n }\n\n .findings-header h3 {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .findings-count {\n font-size: 12px;\n font-weight: 600;\n color: var(--text-dim);\n padding: 4px 12px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 100px;\n }\n\n /* Finding card */\n .finding-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n margin-bottom: 12px;\n overflow: hidden;\n transition: border-color 0.2s;\n }\n\n .finding-card:hover { border-color: var(--border-active); }\n\n .finding-header {\n padding: 20px 24px;\n display: flex;\n align-items: flex-start;\n gap: 16px;\n cursor: pointer;\n user-select: none;\n }\n\n .finding-severity-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 6px;\n box-shadow: 0 0 8px currentColor;\n }\n\n .finding-info {\n flex: 1;\n min-width: 0;\n }\n\n .finding-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 4px;\n letter-spacing: -0.01em;\n }\n\n .finding-subtitle {\n font-size: 12px;\n color: var(--text-muted);\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n }\n\n .finding-tag {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n }\n\n .finding-expand-icon {\n font-size: 18px;\n color: var(--text-dim);\n transition: transform 0.2s;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .finding-card.open .finding-expand-icon {\n transform: rotate(180deg);\n }\n\n .finding-details {\n display: none;\n padding: 0 24px 20px;\n border-top: 1px solid var(--border);\n }\n\n .finding-card.open .finding-details {\n display: block;\n padding-top: 20px;\n }\n\n .detail-row {\n display: grid;\n grid-template-columns: 120px 1fr;\n gap: 8px;\n margin-bottom: 12px;\n align-items: baseline;\n }\n\n .detail-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .detail-value {\n font-size: 13px;\n color: var(--text);\n word-break: break-all;\n }\n\n .evidence-box {\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-xs);\n padding: 12px 16px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.5;\n overflow-x: auto;\n white-space: pre-wrap;\n }\n\n .payload-box {\n background: rgba(250, 27, 27, 0.06);\n border: 1px solid rgba(250, 27, 27, 0.15);\n border-radius: var(--radius-xs);\n padding: 8px 12px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--accent-light);\n word-break: break-all;\n }\n\n /* No findings */\n .no-findings {\n text-align: center;\n padding: 60px 24px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n\n .no-findings .icon { font-size: 48px; margin-bottom: 16px; }\n .no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }\n .no-findings p { font-size: 14px; color: var(--text-muted); }\n\n /* ── Passive Security Analysis ──────────────────────────────────── */\n .passive-section { margin-bottom: 32px; }\n\n .section-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n }\n\n .section-header h3 {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .passive-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n }\n\n .passive-category-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n overflow: hidden;\n transition: border-color 0.2s;\n }\n\n .passive-category-card:hover { border-color: var(--border-active); }\n\n .passive-cat-header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 16px 20px;\n border-bottom: 1px solid var(--border);\n }\n\n .passive-cat-icon {\n font-size: 20px;\n width: 36px;\n height: 36px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(255,255,255,0.03);\n border-radius: var(--radius-xs);\n flex-shrink: 0;\n }\n\n .passive-cat-info { flex: 1; min-width: 0; }\n\n .passive-cat-title {\n font-size: 14px;\n font-weight: 600;\n letter-spacing: -0.01em;\n }\n\n .passive-cat-count {\n font-size: 12px;\n font-weight: 500;\n }\n\n .passive-cat-badge {\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.05em;\n padding: 3px 8px;\n border-radius: 100px;\n border: 1px solid;\n flex-shrink: 0;\n }\n\n .passive-cat-findings {\n padding: 12px 20px;\n }\n\n .passive-finding-row {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 8px 0;\n border-bottom: 1px solid rgba(255,255,255,0.03);\n }\n\n .passive-finding-row:last-child { border-bottom: none; }\n\n .passive-finding-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 7px;\n }\n\n .passive-finding-content { flex: 1; min-width: 0; }\n\n .passive-finding-title {\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 2px;\n }\n\n .passive-finding-desc {\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.4;\n }\n\n .passive-finding-evidence {\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n color: var(--text-dim);\n margin-top: 4px;\n padding: 4px 8px;\n background: rgba(255,255,255,0.02);\n border-radius: 4px;\n }\n\n .passive-sev-tag {\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .passive-remedy {\n padding: 12px 20px;\n background: rgba(66, 165, 245, 0.04);\n border-top: 1px solid rgba(66, 165, 245, 0.08);\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.5;\n }\n\n .passive-remedy-label {\n font-weight: 600;\n margin-right: 6px;\n }\n\n .passive-checks-passed {\n padding: 12px 20px;\n }\n\n .passive-check-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 0;\n font-size: 12px;\n color: var(--text-muted);\n }\n\n .passive-check-icon {\n color: ${COLORS.success};\n font-weight: 700;\n font-size: 11px;\n }\n\n .passive-clear {\n text-align: center;\n padding: 40px 24px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n\n .passive-clear .icon { font-size: 36px; margin-bottom: 12px; }\n .passive-clear h4 { font-size: 16px; font-weight: 600; color: ${COLORS.success}; margin-bottom: 6px; }\n .passive-clear p { font-size: 13px; color: var(--text-muted); }\n\n /* Errors section */\n .errors-section {\n margin-bottom: 32px;\n }\n\n .errors-section h3 {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-muted);\n margin-bottom: 12px;\n }\n\n .error-item {\n padding: 10px 16px;\n background: rgba(255, 171, 64, 0.04);\n border: 1px solid rgba(255, 171, 64, 0.1);\n border-radius: var(--radius-xs);\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: ${COLORS.medium};\n margin-bottom: 6px;\n }\n\n /* Footer */\n .footer {\n text-align: center;\n padding: 32px 0;\n border-top: 1px solid var(--border);\n margin-top: 48px;\n color: var(--text-dim);\n font-size: 12px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n }\n\n .footer a {\n color: var(--accent-light);\n text-decoration: none;\n }\n\n .footer a:hover { text-decoration: underline; }\n\n /* Animations */\n @keyframes fadeIn {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .animate-in {\n animation: fadeIn 0.4s ease-out;\n }\n\n .animate-in-delay { animation: fadeIn 0.4s ease-out 0.1s both; }\n .animate-in-delay-2 { animation: fadeIn 0.4s ease-out 0.2s both; }\n .animate-in-delay-3 { animation: fadeIn 0.4s ease-out 0.3s both; }\n\n /* Print styles */\n @media print {\n body { background: white; color: #111; }\n body::before { display: none; }\n .finding-details { display: block !important; padding-top: 12px !important; }\n .finding-card { page-break-inside: avoid; }\n .passive-category-card { page-break-inside: avoid; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header animate-in\">\n <div class=\"header-brand\">\n ${VULCN_LOGO_SVG}\n <div>\n <h1>vulcn</h1>\n <span>Security Report</span>\n </div>\n </div>\n <div class=\"header-meta\">\n <div>${formatDate(report.generatedAt)}</div>\n <div>Engine v${escapeHtml(report.engineVersion)}</div>\n </div>\n </div>\n\n <!-- Session info -->\n <div class=\"session-info animate-in-delay\">\n <h2>${escapeHtml(session.name)}</h2>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\">Driver</span>\n <span class=\"meta-value\">${escapeHtml(session.driver)}</span>\n </div>\n ${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>` : \"\"}\n <div class=\"meta-item\">\n <span class=\"meta-label\">Duration</span>\n <span class=\"meta-value\">${formatDuration(durationMs)}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\">Generated</span>\n <span class=\"meta-value\">${formatDate(report.generatedAt)}</span>\n </div>\n </div>\n </div>\n\n <!-- Stats grid: Risk + Summary -->\n <div class=\"stats-grid animate-in-delay-2\">\n <div class=\"risk-card\">\n <h3>Risk Level</h3>\n <div class=\"risk-gauge\">\n <svg viewBox=\"0 0 160 160\" width=\"160\" height=\"160\">\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"10\"/>\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"${riskColor}\" stroke-width=\"10\"\n stroke-dasharray=\"${(risk.percent / 100) * 427} 427\"\n stroke-linecap=\"round\"\n style=\"filter: drop-shadow(0 0 6px ${riskColor});\"/>\n </svg>\n <div class=\"risk-gauge-label\">\n <div class=\"score\" style=\"color: ${riskColor}\">${hasFindings ? risk.percent : 0}</div>\n <div class=\"label\">${risk.label}</div>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <h3>Execution Summary</h3>\n <div class=\"summary-stats\">\n <div class=\"stat-box\">\n <div class=\"stat-number\" style=\"color: ${hasFindings ? COLORS.high : COLORS.success}\">${totalFindings}</div>\n <div class=\"stat-label\">Findings</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${payloadsTested}</div>\n <div class=\"stat-label\">Payloads Tested</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${stepsExecuted}</div>\n <div class=\"stat-label\">Steps Executed</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${affectedUrls.length}</div>\n <div class=\"stat-label\">URLs Affected</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Severity breakdown -->\n <div class=\"severity-breakdown animate-in-delay-2\">\n <div class=\"severity-bars\">\n ${([\"critical\", \"high\", \"medium\", \"low\", \"info\"] as const)\n .map(\n (sev) => `\n <div class=\"severity-bar-item\">\n <div class=\"severity-bar-count\" style=\"color: ${severityColor(sev)}\">${counts[sev]}</div>\n <div class=\"severity-bar-track\">\n <div class=\"severity-bar-fill\" style=\"width: ${totalFindings ? (counts[sev] / totalFindings) * 100 : 0}%; background: ${severityColor(sev)};\"></div>\n </div>\n <div class=\"severity-bar-label\">${sev}</div>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n </div>\n\n <!-- Passive Security Analysis -->\n ${renderPassiveSection(passiveAnalysis)}\n\n <!-- Active Findings -->\n <div class=\"findings-section animate-in-delay-3\">\n <div class=\"findings-header\">\n <h3>🎯 Active Scan Findings</h3>\n <span class=\"findings-count\">${activeFindings.length} finding${activeFindings.length !== 1 ? \"s\" : \"\"}</span>\n </div>\n\n ${\n activeFindings.length > 0\n ? activeFindings\n .map(\n (f, i) => `\n <div class=\"finding-card\" onclick=\"this.classList.toggle('open')\">\n <div class=\"finding-header\">\n <div class=\"finding-severity-dot\" style=\"color: ${severityColor(f.severity)}; background: ${severityColor(f.severity)};\"></div>\n <div class=\"finding-info\">\n <div class=\"finding-title\">${escapeHtml(f.title)}</div>\n <div class=\"finding-subtitle\">\n <span class=\"finding-tag\" style=\"color: ${severityColor(f.severity)}\">${f.severity.toUpperCase()}</span>\n <span class=\"finding-tag\">${escapeHtml(f.type)}</span>\n <span class=\"finding-tag\">${escapeHtml(f.stepId)}</span>\n </div>\n </div>\n <span class=\"finding-expand-icon\">▾</span>\n </div>\n <div class=\"finding-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Description</span>\n <span class=\"detail-value\">${escapeHtml(f.description)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">URL</span>\n <span class=\"detail-value\">${escapeHtml(f.url)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Payload</span>\n <div class=\"payload-box\">${escapeHtml(f.payload)}</div>\n </div>\n ${\n f.evidence\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Evidence</span>\n <div class=\"evidence-box\">${escapeHtml(f.evidence)}</div>\n </div>\n `\n : \"\"\n }\n ${\n f.metadata\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Metadata</span>\n <div class=\"evidence-box\">${escapeHtml(JSON.stringify(f.metadata, null, 2))}</div>\n </div>\n `\n : \"\"\n }\n </div>\n </div>\n `,\n )\n .join(\"\")\n : `\n <div class=\"no-findings\">\n <div class=\"icon\">🛡️</div>\n <h3>No Active Vulnerabilities Detected</h3>\n <p>${payloadsTested} payloads were tested across ${stepsExecuted} steps with no findings.</p>\n </div>\n `\n }\n </div>\n\n ${\n errors.length > 0\n ? `\n <div class=\"errors-section\">\n <h3>⚠️ Errors During Execution (${errors.length})</h3>\n ${errors.map((e: string) => `<div class=\"error-item\">${escapeHtml(e)}</div>`).join(\"\")}\n </div>\n `\n : \"\"\n }\n\n <!-- Footer -->\n <div class=\"footer\">\n <div>Generated by ${VULCN_LOGO_SVG.replace(/width=\"32\"/g, 'width=\"16\"').replace(/height=\"32\"/g, 'height=\"16\"')} <strong>Vulcn</strong> — Security Testing Engine</div>\n <div><a href=\"https://docs.vulcn.dev\">docs.vulcn.dev</a></div>\n </div>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Generate SVG donut chart segments (unused in current layout but available)\n */\nfunction generateDonut(counts: Record<string, number>, total: number): string {\n if (total === 0) return \"\";\n const radius = 60;\n const circumference = 2 * Math.PI * radius;\n let offset = 0;\n\n const segments = [\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .filter((sev) => counts[sev] > 0)\n .map((sev) => {\n const pct = counts[sev] / total;\n const dash = pct * circumference;\n const seg = `<circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"${severityColor(sev)}\" stroke-width=\"14\"\n stroke-dasharray=\"${dash} ${circumference - dash}\"\n stroke-dashoffset=\"${-offset}\"\n opacity=\"0.9\"/>`;\n offset += dash;\n return seg;\n });\n\n return `<svg viewBox=\"0 0 160 160\" width=\"120\" height=\"120\" style=\"transform:rotate(-90deg)\">\n <circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"14\"/>\n ${segments.join(\"\\n \")}\n </svg>`;\n}\n","/**\n * JSON Report Generator for Vulcn\n *\n * Projects the canonical VulcnReport into a clean JSON structure\n * suitable for CI/CD pipelines and programmatic consumption.\n */\n\nimport type { VulcnReport } from \"./report-model\";\nimport { formatDuration } from \"./report-model\";\n\n/**\n * The JSON output shape — a clean projection of VulcnReport.\n */\nexport interface JsonReport {\n vulcn: {\n version: string;\n reportVersion: string;\n generatedAt: string;\n };\n session: VulcnReport[\"session\"];\n execution: {\n stepsExecuted: number;\n payloadsTested: number;\n durationMs: number;\n durationFormatted: string;\n errors: string[];\n };\n summary: {\n totalFindings: number;\n riskScore: number;\n riskLabel: string;\n severityCounts: VulcnReport[\"summary\"][\"severityCounts\"];\n vulnerabilityTypes: string[];\n affectedUrls: string[];\n };\n findings: VulcnReport[\"findings\"];\n passiveAnalysis: {\n totalIssues: number;\n categories: Array<{\n id: string;\n label: string;\n status: string;\n issueCount: number;\n passedChecks: number;\n totalChecks: number;\n remedy: string;\n }>;\n };\n rules: VulcnReport[\"rules\"];\n}\n\nexport function generateJson(report: VulcnReport): JsonReport {\n return {\n vulcn: {\n version: report.engineVersion,\n reportVersion: report.reportVersion,\n generatedAt: report.generatedAt,\n },\n session: report.session,\n execution: {\n stepsExecuted: report.stats.stepsExecuted,\n payloadsTested: report.stats.payloadsTested,\n durationMs: report.stats.durationMs,\n durationFormatted: formatDuration(report.stats.durationMs),\n errors: report.stats.errors,\n },\n summary: {\n totalFindings: report.summary.totalFindings,\n riskScore: report.summary.risk.score,\n riskLabel: report.summary.risk.label,\n severityCounts: report.summary.severityCounts,\n vulnerabilityTypes: report.summary.vulnerabilityTypes,\n affectedUrls: report.summary.affectedUrls,\n },\n findings: report.findings,\n passiveAnalysis: {\n totalIssues: report.passiveAnalysis.totalIssues,\n categories: report.passiveAnalysis.categories.map((c) => ({\n id: c.definition.id,\n label: c.definition.label,\n status: c.status,\n issueCount: c.issueCount,\n passedChecks: c.passedChecks,\n totalChecks: c.totalChecks,\n remedy: c.definition.remedy,\n })),\n },\n rules: report.rules,\n };\n}\n","/**\n * YAML Report Generator for Vulcn\n *\n * Produces a human-readable YAML report from the canonical VulcnReport.\n */\n\nimport { stringify } from \"yaml\";\nimport type { VulcnReport } from \"./report-model\";\nimport { generateJson } from \"./json\";\n\nexport function generateYaml(report: VulcnReport): string {\n const jsonReport = generateJson(report);\n\n const header = [\n \"# ──────────────────────────────────────────────\",\n \"# Vulcn Security Report\",\n `# Generated: ${report.generatedAt}`,\n `# Session: ${report.session.name}`,\n `# Findings: ${report.summary.totalFindings}`,\n \"# ──────────────────────────────────────────────\",\n \"\",\n ].join(\"\\n\");\n\n return header + stringify(jsonReport, { indent: 2 });\n}\n","/**\n * SARIF Report Generator for Vulcn\n *\n * Projects the canonical VulcnReport into SARIF v2.1.0 (Static Analysis\n * Results Interchange Format) compatible with GitHub Code Scanning,\n * Azure DevOps, and other SARIF-consuming tools.\n *\n * @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html\n * @see https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning\n */\n\nimport type {\n VulcnReport,\n EnrichedFinding,\n ReportRule,\n Severity,\n} from \"./report-model\";\n\n// ── SARIF Types ────────────────────────────────────────────────────────\n\nexport interface SarifLog {\n $schema: string;\n version: \"2.1.0\";\n runs: SarifRun[];\n}\n\ninterface SarifRun {\n tool: SarifTool;\n results: SarifResult[];\n invocations: SarifInvocation[];\n artifacts?: SarifArtifact[];\n}\n\ninterface SarifTool {\n driver: SarifToolComponent;\n}\n\ninterface SarifToolComponent {\n name: string;\n version: string;\n informationUri: string;\n semanticVersion: string;\n rules: SarifRule[];\n}\n\ninterface SarifRule {\n id: string;\n name: string;\n shortDescription: { text: string };\n fullDescription: { text: string };\n helpUri: string;\n help: { text: string; markdown: string };\n properties: {\n tags: string[];\n precision: \"very-high\" | \"high\" | \"medium\" | \"low\";\n \"security-severity\": string;\n };\n defaultConfiguration: {\n level: SarifLevel;\n };\n}\n\ntype SarifLevel = \"error\" | \"warning\" | \"note\" | \"none\";\n\ninterface SarifResult {\n ruleId: string;\n ruleIndex: number;\n level: SarifLevel;\n message: { text: string };\n locations: SarifLocation[];\n fingerprints: Record<string, string>;\n partialFingerprints: Record<string, string>;\n properties: Record<string, unknown>;\n}\n\ninterface SarifLocation {\n physicalLocation: {\n artifactLocation: {\n uri: string;\n uriBaseId?: string;\n };\n region?: {\n startLine: number;\n startColumn?: number;\n };\n };\n logicalLocations?: Array<{\n name: string;\n kind: string;\n }>;\n}\n\ninterface SarifInvocation {\n executionSuccessful: boolean;\n startTimeUtc?: string;\n endTimeUtc?: string;\n properties?: Record<string, unknown>;\n}\n\ninterface SarifArtifact {\n location: {\n uri: string;\n };\n length?: number;\n}\n\n// ── Mapping Helpers ────────────────────────────────────────────────────\n\nfunction toSarifLevel(severity: Severity): SarifLevel {\n switch (severity) {\n case \"critical\":\n case \"high\":\n return \"error\";\n case \"medium\":\n return \"warning\";\n case \"low\":\n case \"info\":\n return \"note\";\n default:\n return \"warning\";\n }\n}\n\nfunction toPrecision(severity: Severity): SarifRule[\"properties\"][\"precision\"] {\n switch (severity) {\n case \"critical\":\n return \"very-high\";\n case \"high\":\n return \"high\";\n case \"medium\":\n return \"medium\";\n case \"low\":\n case \"info\":\n return \"low\";\n default:\n return \"medium\";\n }\n}\n\n// ── Rule & Result Projection ───────────────────────────────────────────\n\nfunction toSarifRule(rule: ReportRule): SarifRule {\n return {\n id: rule.id,\n name: rule.type,\n shortDescription: {\n text: `${rule.cwe.name} (CWE-${rule.cwe.id})`,\n },\n fullDescription: {\n text: rule.description,\n },\n helpUri: `https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html`,\n help: {\n text: `## ${rule.cwe.name}\\n\\nCWE-${rule.cwe.id}: ${rule.cwe.name}\\n\\nThis rule detects ${rule.type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html for detailed remediation guidance.`,\n markdown: `## ${rule.cwe.name}\\n\\n**CWE-${rule.cwe.id}**: ${rule.cwe.name}\\n\\nThis rule detects \\`${rule.type}\\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee [CWE-${rule.cwe.id}](https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html) for detailed remediation guidance.`,\n },\n properties: {\n tags: [\n \"security\",\n `CWE-${rule.cwe.id}`,\n `external/cwe/cwe-${rule.cwe.id}`,\n ],\n precision: toPrecision(rule.severity),\n \"security-severity\": rule.securitySeverity,\n },\n defaultConfiguration: {\n level: toSarifLevel(rule.severity),\n },\n };\n}\n\nfunction toSarifResult(\n finding: EnrichedFinding,\n sarifRules: SarifRule[],\n): SarifResult {\n const ruleIndex = sarifRules.findIndex((r) => r.id === finding.ruleId);\n\n let messageText = `${finding.title}\\n\\n${finding.description}`;\n if (finding.evidence) {\n messageText += `\\n\\nEvidence: ${finding.evidence}`;\n }\n messageText += `\\n\\nPayload: ${finding.payload}`;\n\n return {\n ruleId: finding.ruleId,\n ruleIndex: Math.max(ruleIndex, 0),\n level: toSarifLevel(finding.severity),\n message: { text: messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: {\n uri: finding.url || \"unknown\",\n },\n region: {\n startLine: 1,\n },\n },\n logicalLocations: [\n {\n name: finding.stepId,\n kind: \"test-step\",\n },\n ],\n },\n ],\n fingerprints: {\n vulcnFindingV1: finding.fingerprint,\n },\n partialFingerprints: {\n vulcnType: finding.type,\n vulcnStepId: finding.stepId,\n },\n properties: {\n severity: finding.severity,\n payload: finding.payload,\n stepId: finding.stepId,\n detectionMethod: finding.detectionMethod,\n ...(finding.evidence ? { evidence: finding.evidence } : {}),\n ...(finding.passiveCategory\n ? { passiveCategory: finding.passiveCategory }\n : {}),\n },\n };\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\n/**\n * Generate a SARIF v2.1.0 log from a VulcnReport.\n *\n * Usage:\n * const report = buildReport(session, result, generatedAt, \"0.5.0\");\n * const sarif = generateSarif(report);\n * await writeFile(\"vulcn-report.sarif\", JSON.stringify(sarif, null, 2));\n */\nexport function generateSarif(report: VulcnReport): SarifLog {\n const sarifRules = report.rules.map(toSarifRule);\n const results = report.findings.map((f) => toSarifResult(f, sarifRules));\n\n // Build artifact list from unique URLs\n const artifacts: SarifArtifact[] = report.summary.affectedUrls.map((url) => ({\n location: { uri: url },\n }));\n\n // Calculate end time from duration\n const startDate = new Date(report.generatedAt);\n const endDate = new Date(startDate.getTime() + report.stats.durationMs);\n\n return {\n $schema:\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"Vulcn\",\n version: report.engineVersion,\n semanticVersion: report.engineVersion,\n informationUri: \"https://vulcn.dev\",\n rules: sarifRules,\n },\n },\n results,\n invocations: [\n {\n executionSuccessful: report.stats.errors.length === 0,\n startTimeUtc: report.generatedAt,\n endTimeUtc: endDate.toISOString(),\n properties: {\n sessionName: report.session.name,\n stepsExecuted: report.stats.stepsExecuted,\n payloadsTested: report.stats.payloadsTested,\n durationMs: report.stats.durationMs,\n ...(report.stats.errors.length > 0\n ? { errors: report.stats.errors }\n : {}),\n },\n },\n ],\n ...(artifacts.length > 0 ? { artifacts } : {}),\n },\n ],\n };\n}\n"],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kEA4bhB,OAAO,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vulcn/plugin-report",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Security report generation plugin for Vulcn — produces HTML dashboards, JSON, YAML, and SARIF v2.1.0 reports for GitHub Code Scanning and CI/CD integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|