@vulcn/plugin-report 0.2.0 → 0.4.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 CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  default: () => index_default,
35
35
  generateHtml: () => generateHtml,
36
36
  generateJson: () => generateJson,
37
+ generateSarif: () => generateSarif,
37
38
  generateYaml: () => generateYaml
38
39
  });
39
40
  module.exports = __toCommonJS(index_exports);
@@ -966,6 +967,248 @@ function generateYaml(session, result, generatedAt, engineVersion) {
966
967
  return header + (0, import_yaml.stringify)(report, { indent: 2 });
967
968
  }
968
969
 
970
+ // src/sarif.ts
971
+ var CWE_MAP = {
972
+ xss: {
973
+ id: 79,
974
+ name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
975
+ },
976
+ sqli: {
977
+ id: 89,
978
+ name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
979
+ },
980
+ ssrf: { id: 918, name: "Server-Side Request Forgery (SSRF)" },
981
+ xxe: {
982
+ id: 611,
983
+ name: "Improper Restriction of XML External Entity Reference"
984
+ },
985
+ "command-injection": {
986
+ id: 78,
987
+ name: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
988
+ },
989
+ "path-traversal": {
990
+ id: 22,
991
+ name: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
992
+ },
993
+ "open-redirect": {
994
+ id: 601,
995
+ name: "URL Redirection to Untrusted Site ('Open Redirect')"
996
+ },
997
+ reflection: {
998
+ id: 200,
999
+ name: "Exposure of Sensitive Information to an Unauthorized Actor"
1000
+ },
1001
+ "security-misconfiguration": {
1002
+ id: 16,
1003
+ name: "Configuration"
1004
+ },
1005
+ "information-disclosure": {
1006
+ id: 200,
1007
+ name: "Exposure of Sensitive Information to an Unauthorized Actor"
1008
+ },
1009
+ custom: { id: 20, name: "Improper Input Validation" }
1010
+ };
1011
+ function toSarifLevel(severity) {
1012
+ switch (severity) {
1013
+ case "critical":
1014
+ case "high":
1015
+ return "error";
1016
+ case "medium":
1017
+ return "warning";
1018
+ case "low":
1019
+ case "info":
1020
+ return "note";
1021
+ default:
1022
+ return "warning";
1023
+ }
1024
+ }
1025
+ function toSecuritySeverity(severity) {
1026
+ switch (severity) {
1027
+ case "critical":
1028
+ return "9.0";
1029
+ case "high":
1030
+ return "7.0";
1031
+ case "medium":
1032
+ return "4.0";
1033
+ case "low":
1034
+ return "2.0";
1035
+ case "info":
1036
+ return "0.0";
1037
+ default:
1038
+ return "4.0";
1039
+ }
1040
+ }
1041
+ function toPrecision(severity) {
1042
+ switch (severity) {
1043
+ case "critical":
1044
+ return "very-high";
1045
+ case "high":
1046
+ return "high";
1047
+ case "medium":
1048
+ return "medium";
1049
+ case "low":
1050
+ case "info":
1051
+ return "low";
1052
+ default:
1053
+ return "medium";
1054
+ }
1055
+ }
1056
+ function toRuleId(type) {
1057
+ return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`;
1058
+ }
1059
+ function buildRules(findings) {
1060
+ const seenTypes = /* @__PURE__ */ new Map();
1061
+ for (const f of findings) {
1062
+ if (!seenTypes.has(f.type)) {
1063
+ seenTypes.set(f.type, f);
1064
+ }
1065
+ }
1066
+ return Array.from(seenTypes.entries()).map(([type, sampleFinding]) => {
1067
+ const cwe = CWE_MAP[type] || CWE_MAP.custom;
1068
+ const ruleId = toRuleId(type);
1069
+ return {
1070
+ id: ruleId,
1071
+ name: type,
1072
+ shortDescription: {
1073
+ text: `${cwe.name} (CWE-${cwe.id})`
1074
+ },
1075
+ fullDescription: {
1076
+ text: `Vulcn detected a potential ${type} vulnerability. ${cwe.name}. See CWE-${cwe.id} for details.`
1077
+ },
1078
+ helpUri: `https://cwe.mitre.org/data/definitions/${cwe.id}.html`,
1079
+ help: {
1080
+ text: `## ${cwe.name}
1081
+
1082
+ CWE-${cwe.id}: ${cwe.name}
1083
+
1084
+ This rule detects ${type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
1085
+
1086
+ ### Remediation
1087
+
1088
+ See https://cwe.mitre.org/data/definitions/${cwe.id}.html for detailed remediation guidance.`,
1089
+ markdown: `## ${cwe.name}
1090
+
1091
+ **CWE-${cwe.id}**: ${cwe.name}
1092
+
1093
+ This rule detects \`${type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
1094
+
1095
+ ### Remediation
1096
+
1097
+ See [CWE-${cwe.id}](https://cwe.mitre.org/data/definitions/${cwe.id}.html) for detailed remediation guidance.`
1098
+ },
1099
+ properties: {
1100
+ tags: ["security", `CWE-${cwe.id}`, `external/cwe/cwe-${cwe.id}`],
1101
+ precision: toPrecision(sampleFinding.severity),
1102
+ "security-severity": toSecuritySeverity(sampleFinding.severity)
1103
+ },
1104
+ defaultConfiguration: {
1105
+ level: toSarifLevel(sampleFinding.severity)
1106
+ }
1107
+ };
1108
+ });
1109
+ }
1110
+ function toSarifResult(finding, rules) {
1111
+ const ruleId = toRuleId(finding.type);
1112
+ const ruleIndex = rules.findIndex((r) => r.id === ruleId);
1113
+ let messageText = `${finding.title}
1114
+
1115
+ ${finding.description}`;
1116
+ if (finding.evidence) {
1117
+ messageText += `
1118
+
1119
+ Evidence: ${finding.evidence}`;
1120
+ }
1121
+ messageText += `
1122
+
1123
+ Payload: ${finding.payload}`;
1124
+ const uri = finding.url || "unknown";
1125
+ const fingerprint = `${finding.type}:${finding.stepId}:${finding.payload.slice(0, 50)}`;
1126
+ return {
1127
+ ruleId,
1128
+ ruleIndex: Math.max(ruleIndex, 0),
1129
+ level: toSarifLevel(finding.severity),
1130
+ message: { text: messageText },
1131
+ locations: [
1132
+ {
1133
+ physicalLocation: {
1134
+ artifactLocation: {
1135
+ uri
1136
+ },
1137
+ region: {
1138
+ startLine: 1
1139
+ }
1140
+ },
1141
+ logicalLocations: [
1142
+ {
1143
+ name: finding.stepId,
1144
+ kind: "test-step"
1145
+ }
1146
+ ]
1147
+ }
1148
+ ],
1149
+ fingerprints: {
1150
+ vulcnFindingV1: fingerprint
1151
+ },
1152
+ partialFingerprints: {
1153
+ vulcnType: finding.type,
1154
+ vulcnStepId: finding.stepId
1155
+ },
1156
+ properties: {
1157
+ severity: finding.severity,
1158
+ payload: finding.payload,
1159
+ stepId: finding.stepId,
1160
+ ...finding.evidence ? { evidence: finding.evidence } : {},
1161
+ ...finding.metadata || {}
1162
+ }
1163
+ };
1164
+ }
1165
+ function generateSarif(session, result, generatedAt, engineVersion) {
1166
+ const rules = buildRules(result.findings);
1167
+ const results = result.findings.map((f) => toSarifResult(f, rules));
1168
+ const uniqueUrls = [
1169
+ ...new Set(result.findings.map((f) => f.url).filter(Boolean))
1170
+ ];
1171
+ const artifacts = uniqueUrls.map((url) => ({
1172
+ location: { uri: url }
1173
+ }));
1174
+ const startDate = new Date(generatedAt);
1175
+ const endDate = new Date(startDate.getTime() + result.duration);
1176
+ const sarifLog = {
1177
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
1178
+ version: "2.1.0",
1179
+ runs: [
1180
+ {
1181
+ tool: {
1182
+ driver: {
1183
+ name: "Vulcn",
1184
+ version: engineVersion,
1185
+ semanticVersion: engineVersion,
1186
+ informationUri: "https://vulcn.dev",
1187
+ rules
1188
+ }
1189
+ },
1190
+ results,
1191
+ invocations: [
1192
+ {
1193
+ executionSuccessful: result.errors.length === 0,
1194
+ startTimeUtc: generatedAt,
1195
+ endTimeUtc: endDate.toISOString(),
1196
+ properties: {
1197
+ sessionName: session.name,
1198
+ stepsExecuted: result.stepsExecuted,
1199
+ payloadsTested: result.payloadsTested,
1200
+ durationMs: result.duration,
1201
+ ...result.errors.length > 0 ? { errors: result.errors } : {}
1202
+ }
1203
+ }
1204
+ ],
1205
+ ...artifacts.length > 0 ? { artifacts } : {}
1206
+ }
1207
+ ]
1208
+ };
1209
+ return sarifLog;
1210
+ }
1211
+
969
1212
  // src/index.ts
970
1213
  var configSchema = import_zod.z.object({
971
1214
  /**
@@ -973,10 +1216,11 @@ var configSchema = import_zod.z.object({
973
1216
  * - "html": Beautiful dark-themed HTML report
974
1217
  * - "json": Machine-readable structured JSON
975
1218
  * - "yaml": Human-readable YAML
976
- * - "all": Generate all three formats
1219
+ * - "sarif": SARIF v2.1.0 for GitHub Code Scanning
1220
+ * - "all": Generate all formats
977
1221
  * @default "html"
978
1222
  */
979
- format: import_zod.z.enum(["html", "json", "yaml", "all"]).default("html"),
1223
+ format: import_zod.z.enum(["html", "json", "yaml", "sarif", "all"]).default("html"),
980
1224
  /**
981
1225
  * Output directory for report files
982
1226
  * @default "."
@@ -994,14 +1238,14 @@ var configSchema = import_zod.z.object({
994
1238
  open: import_zod.z.boolean().default(false)
995
1239
  });
996
1240
  function getFormats(format) {
997
- if (format === "all") return ["html", "json", "yaml"];
1241
+ if (format === "all") return ["html", "json", "yaml", "sarif"];
998
1242
  return [format];
999
1243
  }
1000
1244
  var plugin = {
1001
1245
  name: "@vulcn/plugin-report",
1002
1246
  version: "0.1.0",
1003
1247
  apiVersion: 1,
1004
- description: "Report generation plugin \u2014 generates beautiful HTML, JSON, and YAML security reports",
1248
+ description: "Report generation plugin \u2014 generates HTML, JSON, YAML, and SARIF security reports",
1005
1249
  configSchema,
1006
1250
  hooks: {
1007
1251
  onInit: async (ctx) => {
@@ -1069,6 +1313,23 @@ var plugin = {
1069
1313
  ctx.logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
1070
1314
  break;
1071
1315
  }
1316
+ case "sarif": {
1317
+ const sarifReport = generateSarif(
1318
+ ctx.session,
1319
+ result,
1320
+ generatedAt,
1321
+ engineVersion
1322
+ );
1323
+ const sarifPath = `${basePath}.sarif`;
1324
+ await (0, import_promises.writeFile)(
1325
+ sarifPath,
1326
+ JSON.stringify(sarifReport, null, 2),
1327
+ "utf-8"
1328
+ );
1329
+ writtenFiles.push(sarifPath);
1330
+ ctx.logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
1331
+ break;
1332
+ }
1072
1333
  }
1073
1334
  } catch (err) {
1074
1335
  ctx.logger.error(
@@ -1095,6 +1356,7 @@ var index_default = plugin;
1095
1356
  configSchema,
1096
1357
  generateHtml,
1097
1358
  generateJson,
1359
+ generateSarif,
1098
1360
  generateYaml
1099
1361
  });
1100
1362
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/html.ts","../src/json.ts","../src/yaml.ts"],"sourcesContent":["/**\n * @vulcn/plugin-report\n * Report Generation Plugin for Vulcn\n *\n * Generates security reports in HTML, JSON, and YAML 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 *\n * Configuration:\n * format: \"html\" | \"json\" | \"yaml\" | \"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\";\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 * - \"all\": Generate all three formats\n * @default \"html\"\n */\n format: z.enum([\"html\", \"json\", \"yaml\", \"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\"];\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 beautiful HTML, JSON, and YAML 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 } 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 { configSchema, generateHtml, generateJson, generateYaml };\nexport type { HtmlReportData, JsonReport };\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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,iBAAkB;AAClB,sBAAiC;AACjC,uBAAiC;;;ACEjC,IAAM,SAAS;AAAA,EACb,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AAAA,EACN,WAAW;AAAA,EACX,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,UAAU;AAAA,EACV,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AACX;AAEA,SAAS,cAAc,UAA0B;AAC/C,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB;AACE,aAAO,OAAO;AAAA,EAClB;AACF;AAEA,SAAS,cAAc,UAA0B;AAC/C,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,WAAW,KAAqB;AACvC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,SAAS,eAAe,IAAoB;AAC1C,MAAI,KAAK,IAAM,QAAO,GAAG,EAAE;AAC3B,QAAM,WAAW,KAAK,KAAM,QAAQ,CAAC;AACrC,SAAO,GAAG,OAAO;AACnB;AAEA,SAAS,WAAW,KAAqB;AACvC,QAAM,IAAI,IAAI,KAAK,GAAG;AACtB,SAAO,EAAE,mBAAmB,SAAS;AAAA,IACnC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB,CAAC;AACH;AAGA,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBhB,SAAS,aAAa,MAA8B;AACzD,QAAM,EAAE,SAAS,QAAQ,aAAa,cAAc,IAAI;AACxD,QAAM,WAAW,CAAC,GAAG,OAAO,QAAQ,EAAE;AAAA,IACpC,CAAC,GAAG,MAAM,cAAc,EAAE,QAAQ,IAAI,cAAc,EAAE,QAAQ;AAAA,EAChE;AAGA,QAAM,SAAiC;AAAA,IACrC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,MAAM;AAAA,EACR;AACA,aAAW,KAAK,UAAU;AACxB,WAAO,EAAE,QAAQ,KAAK,OAAO,EAAE,QAAQ,KAAK,KAAK;AAAA,EACnD;AAEA,QAAM,gBAAgB,SAAS;AAC/B,QAAM,cAAc,gBAAgB;AAGpC,QAAM,YACJ,OAAO,WAAW,KAAK,OAAO,OAAO,IAAI,OAAO,SAAS,IAAI,OAAO,MAAM;AAC5E,QAAM,UAAU,gBAAgB,MAAM;AACtC,QAAM,cAAc,KAAK,IAAI,KAAK,KAAK,MAAO,YAAY,UAAW,GAAG,CAAC;AACzE,QAAM,YACJ,eAAe,KACX,aACA,eAAe,KACb,SACA,eAAe,KACb,WACA,cAAc,IACZ,QACA;AACZ,QAAM,YACJ,eAAe,KACX,OAAO,WACP,eAAe,KACb,OAAO,OACP,eAAe,KACb,OAAO,SACP,cAAc,IACZ,OAAO,MACP,OAAO;AAGnB,QAAM,WAAW,cAAc,QAAQ,aAAa;AAGpD,QAAM,eAAe,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAG5D,QAAM,YAAY,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAE1D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,wCAK0B,WAAW,QAAQ,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAO7C,OAAO,EAAE;AAAA,mBACJ,OAAO,OAAO;AAAA,yBACR,OAAO,YAAY;AAAA,kBAC1B,OAAO,MAAM;AAAA,yBACN,OAAO,YAAY;AAAA,gBAC5B,OAAO,IAAI;AAAA,sBACL,OAAO,SAAS;AAAA,oBAClB,OAAO,OAAO;AAAA,kBAChB,OAAO,MAAM;AAAA,uBACR,OAAO,UAAU;AAAA,wBAChB,OAAO,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iEAsBuB,OAAO,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["formatDuration"]}
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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["formatDuration"]}