@vulcn/plugin-report 0.1.1 → 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 +266 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -7
- package/dist/index.d.ts +127 -7
- package/dist/index.js +265 -4
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
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
|
-
* - "
|
|
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
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const seconds = (ms / 1000).toFixed(1);\n return `${seconds}s`;\n}\n\nfunction formatDate(iso: string): string {\n const d = new Date(iso);\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n });\n}\n\n// Inline SVG logo matching the vulcn shield branding\nconst VULCN_LOGO_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"32\" height=\"32\">\n <defs>\n <linearGradient id=\"lg1\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(7 -13 13 7 7 17)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#fa1b1b\"/>\n <stop offset=\"1\" stop-color=\"#ff9c9c\"/>\n </linearGradient>\n <linearGradient id=\"lg2\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(3 -6 6 3 13 14)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#ff9c9c\"/>\n <stop offset=\"1\" stop-color=\"#ffffff\"/>\n </linearGradient>\n </defs>\n <path fill=\"url(#lg1)\" d=\"m 11,17 c 0,0.552 -0.448,1 -1,1 -0.552,0 -1,-0.448 -1,-1 0,-0.552 0.448,-1 1,-1 0.552,0 1,0.448 1,1 z M 10,15 C 8,15 7.839,16.622 7.803,16.68 7.51,17.147 6.892,17.288 6.425,16.995 3.592,15.216 2.389,11.366 2.014,9.168 1.977,8.951 1.952,8.743 1.936,8.547 1.936,8.544 1.935,8.541 1.935,8.538 1.844,7.291 2.572,6.13 3.733,5.667 3.736,5.666 3.738,5.665 3.74,5.664 4.948,5.193 5.913,4.705 6.583,3.641 6.586,3.636 6.588,3.632 6.591,3.628 7.235,2.637 8.332,2.035 9.506,2.023 9.817,2.001 10.141,2 10.451,2 c 0,0 0,0 0,0 1.202,0 2.322,0.608 2.977,1.616 0.005,0.008 0.01,0.017 0.015,0.025 0.651,1.07 1.614,1.554 2.817,2.022 0.002,0 0.005,10e-4 0.007,0.002 1.162,0.463 1.89,1.626 1.799,2.873 0,0.006 -10e-4,0.012 -10e-4,0.018 -0.018,0.193 -0.043,0.397 -0.079,0.612 -0.375,2.198 -1.578,6.048 -4.411,7.827 C 13.108,17.288 12.49,17.147 12.197,16.68 12.161,16.622 12,15 10,15 Z\"/>\n <path fill=\"#dc2626\" d=\"m 13.0058,9.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z\"/>\n <path fill=\"url(#lg2)\" d=\"m 14.0058,8.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z\"/>\n</svg>`;\n\nexport function generateHtml(data: HtmlReportData): string {\n const { session, result, generatedAt, engineVersion } = data;\n const findings = [...result.findings].sort(\n (a, b) => severityOrder(a.severity) - severityOrder(b.severity),\n );\n\n // Severity counts for donut chart\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const totalFindings = findings.length;\n const hasFindings = totalFindings > 0;\n\n // Overall risk score\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n const maxRisk = totalFindings * 10 || 1;\n const riskPercent = Math.min(100, Math.round((riskScore / maxRisk) * 100));\n const riskLabel =\n riskPercent >= 80\n ? \"Critical\"\n : riskPercent >= 50\n ? \"High\"\n : riskPercent >= 25\n ? \"Medium\"\n : riskPercent > 0\n ? \"Low\"\n : \"Clear\";\n const riskColor =\n riskPercent >= 80\n ? COLORS.critical\n : riskPercent >= 50\n ? COLORS.high\n : riskPercent >= 25\n ? COLORS.medium\n : riskPercent > 0\n ? COLORS.low\n : COLORS.success;\n\n // Donut chart SVG segments\n const donutSvg = generateDonut(counts, totalFindings);\n\n // Unique URLs affected\n const affectedUrls = [...new Set(findings.map((f) => f.url))];\n\n // Unique vuln types\n const vulnTypes = [...new Set(findings.map((f) => f.type))];\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Vulcn Security Report — ${escapeHtml(session.name)}</title>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n :root {\n --bg: ${COLORS.bg};\n --surface: ${COLORS.surface};\n --surface-hover: ${COLORS.surfaceHover};\n --border: ${COLORS.border};\n --border-active: ${COLORS.borderActive};\n --text: ${COLORS.text};\n --text-muted: ${COLORS.textMuted};\n --text-dim: ${COLORS.textDim};\n --accent: ${COLORS.accent};\n --accent-glow: ${COLORS.accentGlow};\n --accent-light: ${COLORS.accentLight};\n --radius: 12px;\n --radius-sm: 8px;\n --radius-xs: 6px;\n }\n\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n }\n\n /* Ambient gradient background */\n body::before {\n content: '';\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 600px;\n background: radial-gradient(ellipse 80% 50% at 50% -20%, ${COLORS.accentGlow} 0%, transparent 100%);\n pointer-events: none;\n z-index: 0;\n }\n\n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 24px;\n position: relative;\n z-index: 1;\n }\n\n /* Header */\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 48px;\n padding-bottom: 24px;\n border-bottom: 1px solid var(--border);\n }\n\n .header-brand {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .header-brand svg {\n filter: drop-shadow(0 0 8px rgba(250, 27, 27, 0.3));\n }\n\n .header-brand h1 {\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -0.02em;\n background: linear-gradient(135deg, #fa1b1b, #ff9c9c);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .header-brand span {\n font-size: 11px;\n font-weight: 500;\n color: var(--text-dim);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .header-meta {\n text-align: right;\n font-size: 12px;\n color: var(--text-dim);\n line-height: 1.8;\n }\n\n /* Session info */\n .session-info {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 24px;\n margin-bottom: 32px;\n }\n\n .session-info h2 {\n font-size: 22px;\n font-weight: 700;\n margin-bottom: 16px;\n letter-spacing: -0.02em;\n }\n\n .session-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n }\n\n .meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .meta-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .meta-value {\n font-size: 14px;\n font-weight: 500;\n color: var(--text);\n font-family: 'JetBrains Mono', monospace;\n font-size: 13px;\n }\n\n /* Stats grid */\n .stats-grid {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 24px;\n margin-bottom: 32px;\n }\n\n @media (max-width: 768px) {\n .stats-grid { grid-template-columns: 1fr; }\n }\n\n /* Risk gauge */\n .risk-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n }\n\n .risk-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n width: 100%;\n }\n\n .risk-gauge {\n position: relative;\n width: 160px;\n height: 160px;\n }\n\n .risk-gauge svg {\n transform: rotate(-90deg);\n }\n\n .risk-gauge-label {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .risk-gauge-label .score {\n font-size: 36px;\n font-weight: 800;\n letter-spacing: -0.03em;\n }\n\n .risk-gauge-label .label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n }\n\n /* Summary card */\n .summary-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n }\n\n .summary-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n margin-bottom: 20px;\n }\n\n .summary-stats {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 20px;\n }\n\n .stat-box {\n padding: 16px;\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n transition: border-color 0.2s;\n }\n\n .stat-box:hover { border-color: var(--border-active); }\n\n .stat-number {\n font-size: 28px;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1;\n margin-bottom: 4px;\n }\n\n .stat-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--text-muted);\n }\n\n /* Severity breakdown */\n .severity-breakdown {\n margin-bottom: 32px;\n }\n\n .severity-section-header {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 16px;\n }\n\n .severity-section-header h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .severity-bars {\n display: flex;\n gap: 8px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 20px 24px;\n }\n\n .severity-bar-item {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n }\n\n .severity-bar-track {\n width: 100%;\n height: 6px;\n background: rgba(255,255,255,0.04);\n border-radius: 3px;\n overflow: hidden;\n }\n\n .severity-bar-fill {\n height: 100%;\n border-radius: 3px;\n transition: width 0.5s ease;\n }\n\n .severity-bar-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .severity-bar-count {\n font-size: 18px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n }\n\n /* Findings section */\n .findings-section {\n margin-bottom: 32px;\n }\n\n .findings-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n }\n\n .findings-header h3 {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .findings-count {\n font-size: 12px;\n font-weight: 600;\n color: var(--text-dim);\n padding: 4px 12px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 100px;\n }\n\n /* Finding card */\n .finding-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n margin-bottom: 12px;\n overflow: hidden;\n transition: border-color 0.2s;\n }\n\n .finding-card:hover { border-color: var(--border-active); }\n\n .finding-header {\n padding: 20px 24px;\n display: flex;\n align-items: flex-start;\n gap: 16px;\n cursor: pointer;\n user-select: none;\n }\n\n .finding-severity-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 6px;\n box-shadow: 0 0 8px currentColor;\n }\n\n .finding-info {\n flex: 1;\n min-width: 0;\n }\n\n .finding-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 4px;\n letter-spacing: -0.01em;\n }\n\n .finding-subtitle {\n font-size: 12px;\n color: var(--text-muted);\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n }\n\n .finding-tag {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n }\n\n .finding-expand-icon {\n font-size: 18px;\n color: var(--text-dim);\n transition: transform 0.2s;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .finding-card.open .finding-expand-icon {\n transform: rotate(180deg);\n }\n\n .finding-details {\n display: none;\n padding: 0 24px 20px;\n border-top: 1px solid var(--border);\n }\n\n .finding-card.open .finding-details {\n display: block;\n padding-top: 20px;\n }\n\n .detail-row {\n display: grid;\n grid-template-columns: 120px 1fr;\n gap: 8px;\n margin-bottom: 12px;\n align-items: baseline;\n }\n\n .detail-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .detail-value {\n font-size: 13px;\n color: var(--text);\n word-break: break-all;\n }\n\n .evidence-box {\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-xs);\n padding: 12px 16px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.5;\n overflow-x: auto;\n white-space: pre-wrap;\n }\n\n .payload-box {\n background: rgba(250, 27, 27, 0.06);\n border: 1px solid rgba(250, 27, 27, 0.15);\n border-radius: var(--radius-xs);\n padding: 8px 12px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--accent-light);\n word-break: break-all;\n }\n\n /* No findings */\n .no-findings {\n text-align: center;\n padding: 60px 24px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n\n .no-findings .icon { font-size: 48px; margin-bottom: 16px; }\n .no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }\n .no-findings p { font-size: 14px; color: var(--text-muted); }\n\n /* Errors section */\n .errors-section {\n margin-bottom: 32px;\n }\n\n .errors-section h3 {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-muted);\n margin-bottom: 12px;\n }\n\n .error-item {\n padding: 10px 16px;\n background: rgba(255, 171, 64, 0.04);\n border: 1px solid rgba(255, 171, 64, 0.1);\n border-radius: var(--radius-xs);\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: ${COLORS.medium};\n margin-bottom: 6px;\n }\n\n /* Footer */\n .footer {\n text-align: center;\n padding: 32px 0;\n border-top: 1px solid var(--border);\n margin-top: 48px;\n color: var(--text-dim);\n font-size: 12px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n }\n\n .footer a {\n color: var(--accent-light);\n text-decoration: none;\n }\n\n .footer a:hover { text-decoration: underline; }\n\n /* Animations */\n @keyframes fadeIn {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .animate-in {\n animation: fadeIn 0.4s ease-out;\n }\n\n .animate-in-delay { animation: fadeIn 0.4s ease-out 0.1s both; }\n .animate-in-delay-2 { animation: fadeIn 0.4s ease-out 0.2s both; }\n .animate-in-delay-3 { animation: fadeIn 0.4s ease-out 0.3s both; }\n\n /* Print styles */\n @media print {\n body { background: white; color: #111; }\n body::before { display: none; }\n .finding-details { display: block !important; padding-top: 12px !important; }\n .finding-card { page-break-inside: avoid; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header animate-in\">\n <div class=\"header-brand\">\n ${VULCN_LOGO_SVG}\n <div>\n <h1>vulcn</h1>\n <span>Security Report</span>\n </div>\n </div>\n <div class=\"header-meta\">\n <div>${formatDate(generatedAt)}</div>\n <div>Engine v${escapeHtml(engineVersion)}</div>\n </div>\n </div>\n\n <!-- Session info -->\n <div class=\"session-info animate-in-delay\">\n <h2>${escapeHtml(session.name)}</h2>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\">Driver</span>\n <span class=\"meta-value\">${escapeHtml(session.driver)}</span>\n </div>\n ${session.driverConfig?.startUrl ? `<div class=\"meta-item\"><span class=\"meta-label\">Target URL</span><span class=\"meta-value\">${escapeHtml(String(session.driverConfig.startUrl))}</span></div>` : \"\"}\n <div class=\"meta-item\">\n <span class=\"meta-label\">Duration</span>\n <span class=\"meta-value\">${formatDuration(result.duration)}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\">Generated</span>\n <span class=\"meta-value\">${formatDate(generatedAt)}</span>\n </div>\n </div>\n </div>\n\n <!-- Stats grid: Risk + Summary -->\n <div class=\"stats-grid animate-in-delay-2\">\n <div class=\"risk-card\">\n <h3>Risk Level</h3>\n <div class=\"risk-gauge\">\n <svg viewBox=\"0 0 160 160\" width=\"160\" height=\"160\">\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"10\"/>\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"${riskColor}\" stroke-width=\"10\"\n stroke-dasharray=\"${(riskPercent / 100) * 427} 427\"\n stroke-linecap=\"round\"\n style=\"filter: drop-shadow(0 0 6px ${riskColor});\"/>\n </svg>\n <div class=\"risk-gauge-label\">\n <div class=\"score\" style=\"color: ${riskColor}\">${hasFindings ? riskPercent : 0}</div>\n <div class=\"label\">${riskLabel}</div>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <h3>Execution Summary</h3>\n <div class=\"summary-stats\">\n <div class=\"stat-box\">\n <div class=\"stat-number\" style=\"color: ${hasFindings ? COLORS.high : COLORS.success}\">${totalFindings}</div>\n <div class=\"stat-label\">Findings</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.payloadsTested}</div>\n <div class=\"stat-label\">Payloads Tested</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.stepsExecuted}</div>\n <div class=\"stat-label\">Steps Executed</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${affectedUrls.length}</div>\n <div class=\"stat-label\">URLs Affected</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Severity breakdown -->\n <div class=\"severity-breakdown animate-in-delay-2\">\n <div class=\"severity-bars\">\n ${[\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .map(\n (sev) => `\n <div class=\"severity-bar-item\">\n <div class=\"severity-bar-count\" style=\"color: ${severityColor(sev)}\">${counts[sev]}</div>\n <div class=\"severity-bar-track\">\n <div class=\"severity-bar-fill\" style=\"width: ${totalFindings ? (counts[sev] / totalFindings) * 100 : 0}%; background: ${severityColor(sev)};\"></div>\n </div>\n <div class=\"severity-bar-label\">${sev}</div>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n </div>\n\n <!-- Findings -->\n <div class=\"findings-section animate-in-delay-3\">\n <div class=\"findings-header\">\n <h3>Findings</h3>\n <span class=\"findings-count\">${totalFindings} total</span>\n </div>\n\n ${\n hasFindings\n ? findings\n .map(\n (f, i) => `\n <div class=\"finding-card\" onclick=\"this.classList.toggle('open')\">\n <div class=\"finding-header\">\n <div class=\"finding-severity-dot\" style=\"color: ${severityColor(f.severity)}; background: ${severityColor(f.severity)};\"></div>\n <div class=\"finding-info\">\n <div class=\"finding-title\">${escapeHtml(f.title)}</div>\n <div class=\"finding-subtitle\">\n <span class=\"finding-tag\" style=\"color: ${severityColor(f.severity)}\">${f.severity.toUpperCase()}</span>\n <span class=\"finding-tag\">${escapeHtml(f.type)}</span>\n <span class=\"finding-tag\">${escapeHtml(f.stepId)}</span>\n </div>\n </div>\n <span class=\"finding-expand-icon\">▾</span>\n </div>\n <div class=\"finding-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Description</span>\n <span class=\"detail-value\">${escapeHtml(f.description)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">URL</span>\n <span class=\"detail-value\">${escapeHtml(f.url)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Payload</span>\n <div class=\"payload-box\">${escapeHtml(f.payload)}</div>\n </div>\n ${\n f.evidence\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Evidence</span>\n <div class=\"evidence-box\">${escapeHtml(f.evidence)}</div>\n </div>\n `\n : \"\"\n }\n ${\n f.metadata\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Metadata</span>\n <div class=\"evidence-box\">${escapeHtml(JSON.stringify(f.metadata, null, 2))}</div>\n </div>\n `\n : \"\"\n }\n </div>\n </div>\n `,\n )\n .join(\"\")\n : `\n <div class=\"no-findings\">\n <div class=\"icon\">🛡️</div>\n <h3>No Vulnerabilities Detected</h3>\n <p>${result.payloadsTested} payloads were tested across ${result.stepsExecuted} steps with no findings.</p>\n </div>\n `\n }\n </div>\n\n ${\n result.errors.length > 0\n ? `\n <div class=\"errors-section\">\n <h3>⚠️ Errors During Execution (${result.errors.length})</h3>\n ${result.errors.map((e) => `<div class=\"error-item\">${escapeHtml(e)}</div>`).join(\"\")}\n </div>\n `\n : \"\"\n }\n\n <!-- Footer -->\n <div class=\"footer\">\n <div>Generated by ${VULCN_LOGO_SVG.replace(/width=\"32\"/g, 'width=\"16\"').replace(/height=\"32\"/g, 'height=\"16\"')} <strong>Vulcn</strong> — Security Testing Engine</div>\n <div><a href=\"https://docs.vulcn.dev\">docs.vulcn.dev</a></div>\n </div>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Generate SVG donut chart segments (unused in current layout but available)\n */\nfunction generateDonut(counts: Record<string, number>, total: number): string {\n if (total === 0) return \"\";\n const radius = 60;\n const circumference = 2 * Math.PI * radius;\n let offset = 0;\n\n const segments = [\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .filter((sev) => counts[sev] > 0)\n .map((sev) => {\n const pct = counts[sev] / total;\n const dash = pct * circumference;\n const seg = `<circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"${severityColor(sev)}\" stroke-width=\"14\"\n stroke-dasharray=\"${dash} ${circumference - dash}\"\n stroke-dashoffset=\"${-offset}\"\n opacity=\"0.9\"/>`;\n offset += dash;\n return seg;\n });\n\n return `<svg viewBox=\"0 0 160 160\" width=\"120\" height=\"120\" style=\"transform:rotate(-90deg)\">\n <circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"14\"/>\n ${segments.join(\"\\n \")}\n </svg>`;\n}\n","/**\n * JSON Report Generator for Vulcn\n *\n * Produces a structured, machine-readable JSON report.\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\nexport interface JsonReport {\n vulcn: {\n version: string;\n reportVersion: string;\n generatedAt: string;\n };\n session: {\n name: string;\n driver: string;\n driverConfig: Record<string, unknown>;\n stepsCount: number;\n metadata?: Record<string, unknown>;\n };\n execution: {\n stepsExecuted: number;\n payloadsTested: number;\n durationMs: number;\n durationFormatted: string;\n errors: string[];\n };\n summary: {\n totalFindings: number;\n riskScore: number;\n severityCounts: Record<string, number>;\n vulnerabilityTypes: string[];\n affectedUrls: string[];\n };\n findings: Finding[];\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport function generateJson(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): JsonReport {\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of result.findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n\n return {\n vulcn: {\n version: engineVersion,\n reportVersion: \"1.0\",\n generatedAt,\n },\n session: {\n name: session.name,\n driver: session.driver,\n driverConfig: session.driverConfig,\n stepsCount: session.steps.length,\n metadata: session.metadata,\n },\n execution: {\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n durationFormatted: formatDuration(result.duration),\n errors: result.errors,\n },\n summary: {\n totalFindings: result.findings.length,\n riskScore,\n severityCounts: counts,\n vulnerabilityTypes: [...new Set(result.findings.map((f) => f.type))],\n affectedUrls: [...new Set(result.findings.map((f) => f.url))],\n },\n findings: result.findings,\n };\n}\n","/**\n * YAML Report Generator for Vulcn\n *\n * Produces a human-readable YAML report.\n */\n\nimport { stringify } from \"yaml\";\nimport type { RunResult, Session } from \"@vulcn/engine\";\nimport { generateJson } from \"./json\";\n\nexport function generateYaml(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): string {\n const report = generateJson(session, result, generatedAt, engineVersion);\n\n // YAML with header comment\n const header = [\n \"# ──────────────────────────────────────────────\",\n \"# Vulcn Security Report\",\n `# Generated: ${generatedAt}`,\n `# Session: ${session.name}`,\n `# Findings: ${result.findings.length}`,\n \"# ──────────────────────────────────────────────\",\n \"\",\n ].join(\"\\n\");\n\n return header + stringify(report, { indent: 2 });\n}\n"],"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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/html.ts","../src/json.ts","../src/yaml.ts","../src/sarif.ts"],"sourcesContent":["/**\n * @vulcn/plugin-report\n * Report Generation Plugin for Vulcn\n *\n * Generates security reports in HTML, JSON, YAML, and SARIF formats\n * after a run completes. Features:\n * - Modern dark-themed HTML report with Vulcn branding\n * - Machine-readable JSON for CI/CD integration\n * - Human-readable YAML for documentation\n * - SARIF v2.1.0 for GitHub Code Scanning and IDE integration\n *\n * Configuration:\n * format: \"html\" | \"json\" | \"yaml\" | \"sarif\" | \"all\" (default: \"html\")\n * outputDir: directory for reports (default: \".\")\n * filename: base filename (no extension) (default: \"vulcn-report\")\n * open: auto-open HTML in browser (default: false)\n */\n\nimport { z } from \"zod\";\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { resolve, dirname } from \"node:path\";\nimport type {\n VulcnPlugin,\n PluginContext,\n PluginRunContext,\n RunResult,\n} from \"@vulcn/engine\";\n\nimport { generateHtml, type HtmlReportData } from \"./html\";\nimport { generateJson, type JsonReport } from \"./json\";\nimport { generateYaml } from \"./yaml\";\nimport { generateSarif, type SarifLog } from \"./sarif\";\n\n/**\n * Plugin configuration schema\n */\nconst configSchema = z.object({\n /**\n * Report format(s) to generate\n * - \"html\": Beautiful dark-themed HTML report\n * - \"json\": Machine-readable structured JSON\n * - \"yaml\": Human-readable YAML\n * - \"sarif\": SARIF v2.1.0 for GitHub Code Scanning\n * - \"all\": Generate all formats\n * @default \"html\"\n */\n format: z.enum([\"html\", \"json\", \"yaml\", \"sarif\", \"all\"]).default(\"html\"),\n\n /**\n * Output directory for report files\n * @default \".\"\n */\n outputDir: z.string().default(\".\"),\n\n /**\n * Base filename (without extension) for the report\n * @default \"vulcn-report\"\n */\n filename: z.string().default(\"vulcn-report\"),\n\n /**\n * Auto-open HTML report in default browser after generation\n * @default false\n */\n open: z.boolean().default(false),\n});\n\nexport type ReportConfig = z.infer<typeof configSchema>;\n\n/**\n * Determine which formats to generate\n */\nfunction getFormats(format: ReportConfig[\"format\"]): string[] {\n if (format === \"all\") return [\"html\", \"json\", \"yaml\", \"sarif\"];\n return [format];\n}\n\n/**\n * Report Plugin\n */\nconst plugin: VulcnPlugin = {\n name: \"@vulcn/plugin-report\",\n version: \"0.1.0\",\n apiVersion: 1,\n description:\n \"Report generation plugin — generates HTML, JSON, YAML, and SARIF security reports\",\n\n configSchema,\n\n hooks: {\n onInit: async (ctx: PluginContext) => {\n const config = configSchema.parse(ctx.config);\n ctx.logger.info(\n `Report plugin initialized (format: ${config.format}, output: ${config.outputDir}/${config.filename})`,\n );\n },\n\n /**\n * Generate report(s) after run completes\n */\n onRunEnd: async (\n result: RunResult,\n ctx: PluginRunContext,\n ): Promise<RunResult> => {\n const config = configSchema.parse(ctx.config);\n const formats = getFormats(config.format);\n const generatedAt = new Date().toISOString();\n const engineVersion = ctx.engine.version;\n\n // Ensure output directory exists\n const outDir = resolve(config.outputDir);\n await mkdir(outDir, { recursive: true });\n\n const basePath = resolve(outDir, config.filename);\n const writtenFiles: string[] = [];\n\n for (const fmt of formats) {\n try {\n switch (fmt) {\n case \"html\": {\n const htmlData: HtmlReportData = {\n session: ctx.session,\n result,\n generatedAt,\n engineVersion,\n };\n const html = generateHtml(htmlData);\n const htmlPath = `${basePath}.html`;\n await writeFile(htmlPath, html, \"utf-8\");\n writtenFiles.push(htmlPath);\n ctx.logger.info(`📄 HTML report: ${htmlPath}`);\n break;\n }\n\n case \"json\": {\n const jsonReport = generateJson(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const jsonPath = `${basePath}.json`;\n await writeFile(\n jsonPath,\n JSON.stringify(jsonReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(jsonPath);\n ctx.logger.info(`📄 JSON report: ${jsonPath}`);\n break;\n }\n\n case \"yaml\": {\n const yamlContent = generateYaml(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const yamlPath = `${basePath}.yml`;\n await writeFile(yamlPath, yamlContent, \"utf-8\");\n writtenFiles.push(yamlPath);\n ctx.logger.info(`📄 YAML report: ${yamlPath}`);\n break;\n }\n\n case \"sarif\": {\n const sarifReport = generateSarif(\n ctx.session,\n result,\n generatedAt,\n engineVersion,\n );\n const sarifPath = `${basePath}.sarif`;\n await writeFile(\n sarifPath,\n JSON.stringify(sarifReport, null, 2),\n \"utf-8\",\n );\n writtenFiles.push(sarifPath);\n ctx.logger.info(`📄 SARIF report: ${sarifPath}`);\n break;\n }\n }\n } catch (err) {\n ctx.logger.error(\n `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Auto-open HTML report if configured\n if (config.open && formats.includes(\"html\")) {\n const htmlPath = `${basePath}.html`;\n try {\n const { exec } = await import(\"node:child_process\");\n const openCmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"start\"\n : \"xdg-open\";\n exec(`${openCmd} \"${htmlPath}\"`);\n } catch {\n // Silently ignore if can't open browser\n }\n }\n\n return result;\n },\n },\n};\n\nexport default plugin;\n\n// Named exports for programmatic usage\nexport {\n configSchema,\n generateHtml,\n generateJson,\n generateYaml,\n generateSarif,\n};\nexport type { HtmlReportData, JsonReport, SarifLog };\n","/**\n * HTML Report Generator for Vulcn\n *\n * Generates a modern, dark-themed security report with:\n * - Vulcn branding (shield gradient logo)\n * - Executive summary with severity donut chart\n * - Detailed findings with expandable evidence\n * - Timeline of execution\n * - Responsive design\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\nexport interface HtmlReportData {\n session: Session;\n result: RunResult;\n generatedAt: string;\n engineVersion: string;\n}\n\n// Vulcn brand colors\nconst COLORS = {\n bg: \"#0a0a0f\",\n surface: \"#12121a\",\n surfaceHover: \"#1a1a26\",\n border: \"#1e1e2e\",\n borderActive: \"#2a2a3e\",\n text: \"#e4e4ef\",\n textMuted: \"#8888a0\",\n textDim: \"#555570\",\n accent: \"#fa1b1b\",\n accentGlow: \"rgba(250, 27, 27, 0.15)\",\n accentLight: \"#ff9c9c\",\n critical: \"#ff1744\",\n high: \"#ff5252\",\n medium: \"#ffab40\",\n low: \"#66bb6a\",\n info: \"#42a5f5\",\n success: \"#00e676\",\n};\n\nfunction severityColor(severity: string): string {\n switch (severity) {\n case \"critical\":\n return COLORS.critical;\n case \"high\":\n return COLORS.high;\n case \"medium\":\n return COLORS.medium;\n case \"low\":\n return COLORS.low;\n case \"info\":\n return COLORS.info;\n default:\n return COLORS.textMuted;\n }\n}\n\nfunction severityOrder(severity: string): number {\n switch (severity) {\n case \"critical\":\n return 0;\n case \"high\":\n return 1;\n case \"medium\":\n return 2;\n case \"low\":\n return 3;\n case \"info\":\n return 4;\n default:\n return 5;\n }\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const seconds = (ms / 1000).toFixed(1);\n return `${seconds}s`;\n}\n\nfunction formatDate(iso: string): string {\n const d = new Date(iso);\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n });\n}\n\n// Inline SVG logo matching the vulcn shield branding\nconst VULCN_LOGO_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"32\" height=\"32\">\n <defs>\n <linearGradient id=\"lg1\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(7 -13 13 7 7 17)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#fa1b1b\"/>\n <stop offset=\"1\" stop-color=\"#ff9c9c\"/>\n </linearGradient>\n <linearGradient id=\"lg2\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\" gradientTransform=\"matrix(3 -6 6 3 13 14)\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\"0\" stop-color=\"#ff9c9c\"/>\n <stop offset=\"1\" stop-color=\"#ffffff\"/>\n </linearGradient>\n </defs>\n <path fill=\"url(#lg1)\" d=\"m 11,17 c 0,0.552 -0.448,1 -1,1 -0.552,0 -1,-0.448 -1,-1 0,-0.552 0.448,-1 1,-1 0.552,0 1,0.448 1,1 z M 10,15 C 8,15 7.839,16.622 7.803,16.68 7.51,17.147 6.892,17.288 6.425,16.995 3.592,15.216 2.389,11.366 2.014,9.168 1.977,8.951 1.952,8.743 1.936,8.547 1.936,8.544 1.935,8.541 1.935,8.538 1.844,7.291 2.572,6.13 3.733,5.667 3.736,5.666 3.738,5.665 3.74,5.664 4.948,5.193 5.913,4.705 6.583,3.641 6.586,3.636 6.588,3.632 6.591,3.628 7.235,2.637 8.332,2.035 9.506,2.023 9.817,2.001 10.141,2 10.451,2 c 0,0 0,0 0,0 1.202,0 2.322,0.608 2.977,1.616 0.005,0.008 0.01,0.017 0.015,0.025 0.651,1.07 1.614,1.554 2.817,2.022 0.002,0 0.005,10e-4 0.007,0.002 1.162,0.463 1.89,1.626 1.799,2.873 0,0.006 -10e-4,0.012 -10e-4,0.018 -0.018,0.193 -0.043,0.397 -0.079,0.612 -0.375,2.198 -1.578,6.048 -4.411,7.827 C 13.108,17.288 12.49,17.147 12.197,16.68 12.161,16.622 12,15 10,15 Z\"/>\n <path fill=\"#dc2626\" d=\"m 13.0058,9.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z\"/>\n <path fill=\"url(#lg2)\" d=\"m 14.0058,8.89 c -0.164,1.484 -0.749,2.568 -1.659,3.353 -0.418,0.36 -0.465,0.992 -0.104,1.41 0.36,0.418 0.992,0.465 1.41,0.104 1.266,-1.092 2.112,-2.583 2.341,-4.647 0.061,-0.548 -0.335,-1.043 -0.884,-1.104 -0.548,-0.061 -1.043,0.335 -1.104,0.884 z\"/>\n</svg>`;\n\nexport function generateHtml(data: HtmlReportData): string {\n const { session, result, generatedAt, engineVersion } = data;\n const findings = [...result.findings].sort(\n (a, b) => severityOrder(a.severity) - severityOrder(b.severity),\n );\n\n // Severity counts for donut chart\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const totalFindings = findings.length;\n const hasFindings = totalFindings > 0;\n\n // Overall risk score\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n const maxRisk = totalFindings * 10 || 1;\n const riskPercent = Math.min(100, Math.round((riskScore / maxRisk) * 100));\n const riskLabel =\n riskPercent >= 80\n ? \"Critical\"\n : riskPercent >= 50\n ? \"High\"\n : riskPercent >= 25\n ? \"Medium\"\n : riskPercent > 0\n ? \"Low\"\n : \"Clear\";\n const riskColor =\n riskPercent >= 80\n ? COLORS.critical\n : riskPercent >= 50\n ? COLORS.high\n : riskPercent >= 25\n ? COLORS.medium\n : riskPercent > 0\n ? COLORS.low\n : COLORS.success;\n\n // Donut chart SVG segments\n const donutSvg = generateDonut(counts, totalFindings);\n\n // Unique URLs affected\n const affectedUrls = [...new Set(findings.map((f) => f.url))];\n\n // Unique vuln types\n const vulnTypes = [...new Set(findings.map((f) => f.type))];\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Vulcn Security Report — ${escapeHtml(session.name)}</title>\n <style>\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n :root {\n --bg: ${COLORS.bg};\n --surface: ${COLORS.surface};\n --surface-hover: ${COLORS.surfaceHover};\n --border: ${COLORS.border};\n --border-active: ${COLORS.borderActive};\n --text: ${COLORS.text};\n --text-muted: ${COLORS.textMuted};\n --text-dim: ${COLORS.textDim};\n --accent: ${COLORS.accent};\n --accent-glow: ${COLORS.accentGlow};\n --accent-light: ${COLORS.accentLight};\n --radius: 12px;\n --radius-sm: 8px;\n --radius-xs: 6px;\n }\n\n body {\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.6;\n min-height: 100vh;\n }\n\n /* Ambient gradient background */\n body::before {\n content: '';\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 600px;\n background: radial-gradient(ellipse 80% 50% at 50% -20%, ${COLORS.accentGlow} 0%, transparent 100%);\n pointer-events: none;\n z-index: 0;\n }\n\n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 24px;\n position: relative;\n z-index: 1;\n }\n\n /* Header */\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 48px;\n padding-bottom: 24px;\n border-bottom: 1px solid var(--border);\n }\n\n .header-brand {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .header-brand svg {\n filter: drop-shadow(0 0 8px rgba(250, 27, 27, 0.3));\n }\n\n .header-brand h1 {\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -0.02em;\n background: linear-gradient(135deg, #fa1b1b, #ff9c9c);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .header-brand span {\n font-size: 11px;\n font-weight: 500;\n color: var(--text-dim);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .header-meta {\n text-align: right;\n font-size: 12px;\n color: var(--text-dim);\n line-height: 1.8;\n }\n\n /* Session info */\n .session-info {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 24px;\n margin-bottom: 32px;\n }\n\n .session-info h2 {\n font-size: 22px;\n font-weight: 700;\n margin-bottom: 16px;\n letter-spacing: -0.02em;\n }\n\n .session-meta {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n }\n\n .meta-item {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .meta-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .meta-value {\n font-size: 14px;\n font-weight: 500;\n color: var(--text);\n font-family: 'JetBrains Mono', monospace;\n font-size: 13px;\n }\n\n /* Stats grid */\n .stats-grid {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 24px;\n margin-bottom: 32px;\n }\n\n @media (max-width: 768px) {\n .stats-grid { grid-template-columns: 1fr; }\n }\n\n /* Risk gauge */\n .risk-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n }\n\n .risk-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n width: 100%;\n }\n\n .risk-gauge {\n position: relative;\n width: 160px;\n height: 160px;\n }\n\n .risk-gauge svg {\n transform: rotate(-90deg);\n }\n\n .risk-gauge-label {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .risk-gauge-label .score {\n font-size: 36px;\n font-weight: 800;\n letter-spacing: -0.03em;\n }\n\n .risk-gauge-label .label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n }\n\n /* Summary card */\n .summary-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 32px;\n }\n\n .summary-card h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n margin-bottom: 20px;\n }\n\n .summary-stats {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 20px;\n }\n\n .stat-box {\n padding: 16px;\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-sm);\n transition: border-color 0.2s;\n }\n\n .stat-box:hover { border-color: var(--border-active); }\n\n .stat-number {\n font-size: 28px;\n font-weight: 800;\n letter-spacing: -0.03em;\n line-height: 1;\n margin-bottom: 4px;\n }\n\n .stat-label {\n font-size: 12px;\n font-weight: 500;\n color: var(--text-muted);\n }\n\n /* Severity breakdown */\n .severity-breakdown {\n margin-bottom: 32px;\n }\n\n .severity-section-header {\n display: flex;\n align-items: center;\n gap: 12px;\n margin-bottom: 16px;\n }\n\n .severity-section-header h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-dim);\n }\n\n .severity-bars {\n display: flex;\n gap: 8px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 20px 24px;\n }\n\n .severity-bar-item {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 8px;\n align-items: center;\n }\n\n .severity-bar-track {\n width: 100%;\n height: 6px;\n background: rgba(255,255,255,0.04);\n border-radius: 3px;\n overflow: hidden;\n }\n\n .severity-bar-fill {\n height: 100%;\n border-radius: 3px;\n transition: width 0.5s ease;\n }\n\n .severity-bar-label {\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .severity-bar-count {\n font-size: 18px;\n font-weight: 700;\n font-family: 'JetBrains Mono', monospace;\n }\n\n /* Findings section */\n .findings-section {\n margin-bottom: 32px;\n }\n\n .findings-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 16px;\n }\n\n .findings-header h3 {\n font-size: 18px;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .findings-count {\n font-size: 12px;\n font-weight: 600;\n color: var(--text-dim);\n padding: 4px 12px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 100px;\n }\n\n /* Finding card */\n .finding-card {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n margin-bottom: 12px;\n overflow: hidden;\n transition: border-color 0.2s;\n }\n\n .finding-card:hover { border-color: var(--border-active); }\n\n .finding-header {\n padding: 20px 24px;\n display: flex;\n align-items: flex-start;\n gap: 16px;\n cursor: pointer;\n user-select: none;\n }\n\n .finding-severity-dot {\n width: 10px;\n height: 10px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-top: 6px;\n box-shadow: 0 0 8px currentColor;\n }\n\n .finding-info {\n flex: 1;\n min-width: 0;\n }\n\n .finding-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 4px;\n letter-spacing: -0.01em;\n }\n\n .finding-subtitle {\n font-size: 12px;\n color: var(--text-muted);\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n }\n\n .finding-tag {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n }\n\n .finding-expand-icon {\n font-size: 18px;\n color: var(--text-dim);\n transition: transform 0.2s;\n flex-shrink: 0;\n margin-top: 2px;\n }\n\n .finding-card.open .finding-expand-icon {\n transform: rotate(180deg);\n }\n\n .finding-details {\n display: none;\n padding: 0 24px 20px;\n border-top: 1px solid var(--border);\n }\n\n .finding-card.open .finding-details {\n display: block;\n padding-top: 20px;\n }\n\n .detail-row {\n display: grid;\n grid-template-columns: 120px 1fr;\n gap: 8px;\n margin-bottom: 12px;\n align-items: baseline;\n }\n\n .detail-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--text-dim);\n }\n\n .detail-value {\n font-size: 13px;\n color: var(--text);\n word-break: break-all;\n }\n\n .evidence-box {\n background: rgba(255,255,255,0.02);\n border: 1px solid var(--border);\n border-radius: var(--radius-xs);\n padding: 12px 16px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--text-muted);\n line-height: 1.5;\n overflow-x: auto;\n white-space: pre-wrap;\n }\n\n .payload-box {\n background: rgba(250, 27, 27, 0.06);\n border: 1px solid rgba(250, 27, 27, 0.15);\n border-radius: var(--radius-xs);\n padding: 8px 12px;\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: var(--accent-light);\n word-break: break-all;\n }\n\n /* No findings */\n .no-findings {\n text-align: center;\n padding: 60px 24px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n\n .no-findings .icon { font-size: 48px; margin-bottom: 16px; }\n .no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }\n .no-findings p { font-size: 14px; color: var(--text-muted); }\n\n /* Errors section */\n .errors-section {\n margin-bottom: 32px;\n }\n\n .errors-section h3 {\n font-size: 14px;\n font-weight: 600;\n color: var(--text-muted);\n margin-bottom: 12px;\n }\n\n .error-item {\n padding: 10px 16px;\n background: rgba(255, 171, 64, 0.04);\n border: 1px solid rgba(255, 171, 64, 0.1);\n border-radius: var(--radius-xs);\n font-family: 'JetBrains Mono', monospace;\n font-size: 12px;\n color: ${COLORS.medium};\n margin-bottom: 6px;\n }\n\n /* Footer */\n .footer {\n text-align: center;\n padding: 32px 0;\n border-top: 1px solid var(--border);\n margin-top: 48px;\n color: var(--text-dim);\n font-size: 12px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n }\n\n .footer a {\n color: var(--accent-light);\n text-decoration: none;\n }\n\n .footer a:hover { text-decoration: underline; }\n\n /* Animations */\n @keyframes fadeIn {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .animate-in {\n animation: fadeIn 0.4s ease-out;\n }\n\n .animate-in-delay { animation: fadeIn 0.4s ease-out 0.1s both; }\n .animate-in-delay-2 { animation: fadeIn 0.4s ease-out 0.2s both; }\n .animate-in-delay-3 { animation: fadeIn 0.4s ease-out 0.3s both; }\n\n /* Print styles */\n @media print {\n body { background: white; color: #111; }\n body::before { display: none; }\n .finding-details { display: block !important; padding-top: 12px !important; }\n .finding-card { page-break-inside: avoid; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header animate-in\">\n <div class=\"header-brand\">\n ${VULCN_LOGO_SVG}\n <div>\n <h1>vulcn</h1>\n <span>Security Report</span>\n </div>\n </div>\n <div class=\"header-meta\">\n <div>${formatDate(generatedAt)}</div>\n <div>Engine v${escapeHtml(engineVersion)}</div>\n </div>\n </div>\n\n <!-- Session info -->\n <div class=\"session-info animate-in-delay\">\n <h2>${escapeHtml(session.name)}</h2>\n <div class=\"session-meta\">\n <div class=\"meta-item\">\n <span class=\"meta-label\">Driver</span>\n <span class=\"meta-value\">${escapeHtml(session.driver)}</span>\n </div>\n ${session.driverConfig?.startUrl ? `<div class=\"meta-item\"><span class=\"meta-label\">Target URL</span><span class=\"meta-value\">${escapeHtml(String(session.driverConfig.startUrl))}</span></div>` : \"\"}\n <div class=\"meta-item\">\n <span class=\"meta-label\">Duration</span>\n <span class=\"meta-value\">${formatDuration(result.duration)}</span>\n </div>\n <div class=\"meta-item\">\n <span class=\"meta-label\">Generated</span>\n <span class=\"meta-value\">${formatDate(generatedAt)}</span>\n </div>\n </div>\n </div>\n\n <!-- Stats grid: Risk + Summary -->\n <div class=\"stats-grid animate-in-delay-2\">\n <div class=\"risk-card\">\n <h3>Risk Level</h3>\n <div class=\"risk-gauge\">\n <svg viewBox=\"0 0 160 160\" width=\"160\" height=\"160\">\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"10\"/>\n <circle cx=\"80\" cy=\"80\" r=\"68\" fill=\"none\" stroke=\"${riskColor}\" stroke-width=\"10\"\n stroke-dasharray=\"${(riskPercent / 100) * 427} 427\"\n stroke-linecap=\"round\"\n style=\"filter: drop-shadow(0 0 6px ${riskColor});\"/>\n </svg>\n <div class=\"risk-gauge-label\">\n <div class=\"score\" style=\"color: ${riskColor}\">${hasFindings ? riskPercent : 0}</div>\n <div class=\"label\">${riskLabel}</div>\n </div>\n </div>\n </div>\n\n <div class=\"summary-card\">\n <h3>Execution Summary</h3>\n <div class=\"summary-stats\">\n <div class=\"stat-box\">\n <div class=\"stat-number\" style=\"color: ${hasFindings ? COLORS.high : COLORS.success}\">${totalFindings}</div>\n <div class=\"stat-label\">Findings</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.payloadsTested}</div>\n <div class=\"stat-label\">Payloads Tested</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${result.stepsExecuted}</div>\n <div class=\"stat-label\">Steps Executed</div>\n </div>\n <div class=\"stat-box\">\n <div class=\"stat-number\">${affectedUrls.length}</div>\n <div class=\"stat-label\">URLs Affected</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Severity breakdown -->\n <div class=\"severity-breakdown animate-in-delay-2\">\n <div class=\"severity-bars\">\n ${[\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .map(\n (sev) => `\n <div class=\"severity-bar-item\">\n <div class=\"severity-bar-count\" style=\"color: ${severityColor(sev)}\">${counts[sev]}</div>\n <div class=\"severity-bar-track\">\n <div class=\"severity-bar-fill\" style=\"width: ${totalFindings ? (counts[sev] / totalFindings) * 100 : 0}%; background: ${severityColor(sev)};\"></div>\n </div>\n <div class=\"severity-bar-label\">${sev}</div>\n </div>\n `,\n )\n .join(\"\")}\n </div>\n </div>\n\n <!-- Findings -->\n <div class=\"findings-section animate-in-delay-3\">\n <div class=\"findings-header\">\n <h3>Findings</h3>\n <span class=\"findings-count\">${totalFindings} total</span>\n </div>\n\n ${\n hasFindings\n ? findings\n .map(\n (f, i) => `\n <div class=\"finding-card\" onclick=\"this.classList.toggle('open')\">\n <div class=\"finding-header\">\n <div class=\"finding-severity-dot\" style=\"color: ${severityColor(f.severity)}; background: ${severityColor(f.severity)};\"></div>\n <div class=\"finding-info\">\n <div class=\"finding-title\">${escapeHtml(f.title)}</div>\n <div class=\"finding-subtitle\">\n <span class=\"finding-tag\" style=\"color: ${severityColor(f.severity)}\">${f.severity.toUpperCase()}</span>\n <span class=\"finding-tag\">${escapeHtml(f.type)}</span>\n <span class=\"finding-tag\">${escapeHtml(f.stepId)}</span>\n </div>\n </div>\n <span class=\"finding-expand-icon\">▾</span>\n </div>\n <div class=\"finding-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Description</span>\n <span class=\"detail-value\">${escapeHtml(f.description)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">URL</span>\n <span class=\"detail-value\">${escapeHtml(f.url)}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Payload</span>\n <div class=\"payload-box\">${escapeHtml(f.payload)}</div>\n </div>\n ${\n f.evidence\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Evidence</span>\n <div class=\"evidence-box\">${escapeHtml(f.evidence)}</div>\n </div>\n `\n : \"\"\n }\n ${\n f.metadata\n ? `\n <div class=\"detail-row\">\n <span class=\"detail-label\">Metadata</span>\n <div class=\"evidence-box\">${escapeHtml(JSON.stringify(f.metadata, null, 2))}</div>\n </div>\n `\n : \"\"\n }\n </div>\n </div>\n `,\n )\n .join(\"\")\n : `\n <div class=\"no-findings\">\n <div class=\"icon\">🛡️</div>\n <h3>No Vulnerabilities Detected</h3>\n <p>${result.payloadsTested} payloads were tested across ${result.stepsExecuted} steps with no findings.</p>\n </div>\n `\n }\n </div>\n\n ${\n result.errors.length > 0\n ? `\n <div class=\"errors-section\">\n <h3>⚠️ Errors During Execution (${result.errors.length})</h3>\n ${result.errors.map((e) => `<div class=\"error-item\">${escapeHtml(e)}</div>`).join(\"\")}\n </div>\n `\n : \"\"\n }\n\n <!-- Footer -->\n <div class=\"footer\">\n <div>Generated by ${VULCN_LOGO_SVG.replace(/width=\"32\"/g, 'width=\"16\"').replace(/height=\"32\"/g, 'height=\"16\"')} <strong>Vulcn</strong> — Security Testing Engine</div>\n <div><a href=\"https://docs.vulcn.dev\">docs.vulcn.dev</a></div>\n </div>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Generate SVG donut chart segments (unused in current layout but available)\n */\nfunction generateDonut(counts: Record<string, number>, total: number): string {\n if (total === 0) return \"\";\n const radius = 60;\n const circumference = 2 * Math.PI * radius;\n let offset = 0;\n\n const segments = [\"critical\", \"high\", \"medium\", \"low\", \"info\"]\n .filter((sev) => counts[sev] > 0)\n .map((sev) => {\n const pct = counts[sev] / total;\n const dash = pct * circumference;\n const seg = `<circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"${severityColor(sev)}\" stroke-width=\"14\"\n stroke-dasharray=\"${dash} ${circumference - dash}\"\n stroke-dashoffset=\"${-offset}\"\n opacity=\"0.9\"/>`;\n offset += dash;\n return seg;\n });\n\n return `<svg viewBox=\"0 0 160 160\" width=\"120\" height=\"120\" style=\"transform:rotate(-90deg)\">\n <circle cx=\"80\" cy=\"80\" r=\"${radius}\" fill=\"none\" stroke=\"rgba(255,255,255,0.04)\" stroke-width=\"14\"/>\n ${segments.join(\"\\n \")}\n </svg>`;\n}\n","/**\n * JSON Report Generator for Vulcn\n *\n * Produces a structured, machine-readable JSON report.\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\nexport interface JsonReport {\n vulcn: {\n version: string;\n reportVersion: string;\n generatedAt: string;\n };\n session: {\n name: string;\n driver: string;\n driverConfig: Record<string, unknown>;\n stepsCount: number;\n metadata?: Record<string, unknown>;\n };\n execution: {\n stepsExecuted: number;\n payloadsTested: number;\n durationMs: number;\n durationFormatted: string;\n errors: string[];\n };\n summary: {\n totalFindings: number;\n riskScore: number;\n severityCounts: Record<string, number>;\n vulnerabilityTypes: string[];\n affectedUrls: string[];\n };\n findings: Finding[];\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport function generateJson(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): JsonReport {\n const counts: Record<string, number> = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n };\n for (const f of result.findings) {\n counts[f.severity] = (counts[f.severity] || 0) + 1;\n }\n\n const riskScore =\n counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;\n\n return {\n vulcn: {\n version: engineVersion,\n reportVersion: \"1.0\",\n generatedAt,\n },\n session: {\n name: session.name,\n driver: session.driver,\n driverConfig: session.driverConfig,\n stepsCount: session.steps.length,\n metadata: session.metadata,\n },\n execution: {\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n durationFormatted: formatDuration(result.duration),\n errors: result.errors,\n },\n summary: {\n totalFindings: result.findings.length,\n riskScore,\n severityCounts: counts,\n vulnerabilityTypes: [...new Set(result.findings.map((f) => f.type))],\n affectedUrls: [...new Set(result.findings.map((f) => f.url))],\n },\n findings: result.findings,\n };\n}\n","/**\n * YAML Report Generator for Vulcn\n *\n * Produces a human-readable YAML report.\n */\n\nimport { stringify } from \"yaml\";\nimport type { RunResult, Session } from \"@vulcn/engine\";\nimport { generateJson } from \"./json\";\n\nexport function generateYaml(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): string {\n const report = generateJson(session, result, generatedAt, engineVersion);\n\n // YAML with header comment\n const header = [\n \"# ──────────────────────────────────────────────\",\n \"# Vulcn Security Report\",\n `# Generated: ${generatedAt}`,\n `# Session: ${session.name}`,\n `# Findings: ${result.findings.length}`,\n \"# ──────────────────────────────────────────────\",\n \"\",\n ].join(\"\\n\");\n\n return header + stringify(report, { indent: 2 });\n}\n","/**\n * SARIF Report Generator for Vulcn\n *\n * Produces SARIF v2.1.0 (Static Analysis Results Interchange Format)\n * compatible with GitHub Code Scanning, Azure DevOps, and other\n * SARIF-consuming tools.\n *\n * @see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html\n * @see https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning\n */\n\nimport type { Finding, RunResult, Session } from \"@vulcn/engine\";\n\n// ── SARIF Types ────────────────────────────────────────────────────────\n\n/**\n * SARIF v2.1.0 Log — top-level structure\n */\nexport interface SarifLog {\n $schema: string;\n version: \"2.1.0\";\n runs: SarifRun[];\n}\n\ninterface SarifRun {\n tool: SarifTool;\n results: SarifResult[];\n invocations: SarifInvocation[];\n artifacts?: SarifArtifact[];\n}\n\ninterface SarifTool {\n driver: SarifToolComponent;\n}\n\ninterface SarifToolComponent {\n name: string;\n version: string;\n informationUri: string;\n semanticVersion: string;\n rules: SarifRule[];\n}\n\ninterface SarifRule {\n id: string;\n name: string;\n shortDescription: { text: string };\n fullDescription: { text: string };\n helpUri: string;\n help: { text: string; markdown: string };\n properties: {\n tags: string[];\n precision: \"very-high\" | \"high\" | \"medium\" | \"low\";\n \"security-severity\": string;\n };\n defaultConfiguration: {\n level: SarifLevel;\n };\n}\n\ntype SarifLevel = \"error\" | \"warning\" | \"note\" | \"none\";\n\ninterface SarifResult {\n ruleId: string;\n ruleIndex: number;\n level: SarifLevel;\n message: { text: string };\n locations: SarifLocation[];\n fingerprints: Record<string, string>;\n partialFingerprints: Record<string, string>;\n properties: Record<string, unknown>;\n}\n\ninterface SarifLocation {\n physicalLocation: {\n artifactLocation: {\n uri: string;\n uriBaseId?: string;\n };\n region?: {\n startLine: number;\n startColumn?: number;\n };\n };\n logicalLocations?: Array<{\n name: string;\n kind: string;\n }>;\n}\n\ninterface SarifInvocation {\n executionSuccessful: boolean;\n startTimeUtc?: string;\n endTimeUtc?: string;\n properties?: Record<string, unknown>;\n}\n\ninterface SarifArtifact {\n location: {\n uri: string;\n };\n length?: number;\n}\n\n// ── CWE Mappings ───────────────────────────────────────────────────────\n\n/**\n * Map Vulcn vulnerability types to CWE IDs.\n * These are the most specific CWE entries for each category.\n */\nconst CWE_MAP: Record<string, { id: number; name: string }> = {\n xss: {\n id: 79,\n name: \"Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')\",\n },\n sqli: {\n id: 89,\n name: \"Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')\",\n },\n ssrf: { id: 918, name: \"Server-Side Request Forgery (SSRF)\" },\n xxe: {\n id: 611,\n name: \"Improper Restriction of XML External Entity Reference\",\n },\n \"command-injection\": {\n id: 78,\n name: \"Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')\",\n },\n \"path-traversal\": {\n id: 22,\n name: \"Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')\",\n },\n \"open-redirect\": {\n id: 601,\n name: \"URL Redirection to Untrusted Site ('Open Redirect')\",\n },\n reflection: {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n \"security-misconfiguration\": {\n id: 16,\n name: \"Configuration\",\n },\n \"information-disclosure\": {\n id: 200,\n name: \"Exposure of Sensitive Information to an Unauthorized Actor\",\n },\n custom: { id: 20, name: \"Improper Input Validation\" },\n};\n\n/**\n * Map Vulcn severities to SARIF levels.\n *\n * SARIF only has: error, warning, note, none\n * - critical/high → error\n * - medium → warning\n * - low/info → note\n */\nfunction toSarifLevel(severity: Finding[\"severity\"]): SarifLevel {\n switch (severity) {\n case \"critical\":\n case \"high\":\n return \"error\";\n case \"medium\":\n return \"warning\";\n case \"low\":\n case \"info\":\n return \"note\";\n default:\n return \"warning\";\n }\n}\n\n/**\n * Map Vulcn severities to CVSS-like security-severity scores.\n * GitHub uses this for sorting in the Security tab.\n *\n * Scale: 0.0–10.0\n * critical: 9.0\n * high: 7.0\n * medium: 4.0\n * low: 2.0\n * info: 0.0\n */\nfunction toSecuritySeverity(severity: Finding[\"severity\"]): string {\n switch (severity) {\n case \"critical\":\n return \"9.0\";\n case \"high\":\n return \"7.0\";\n case \"medium\":\n return \"4.0\";\n case \"low\":\n return \"2.0\";\n case \"info\":\n return \"0.0\";\n default:\n return \"4.0\";\n }\n}\n\n/**\n * Map Vulcn severities to SARIF precision.\n */\nfunction toPrecision(\n severity: Finding[\"severity\"],\n): SarifRule[\"properties\"][\"precision\"] {\n switch (severity) {\n case \"critical\":\n return \"very-high\";\n case \"high\":\n return \"high\";\n case \"medium\":\n return \"medium\";\n case \"low\":\n case \"info\":\n return \"low\";\n default:\n return \"medium\";\n }\n}\n\n// ── Rule Generation ────────────────────────────────────────────────────\n\n/**\n * Generate a unique rule ID from a finding type.\n *\n * Format: VULCN-<TYPE>\n * Example: VULCN-XSS, VULCN-SQLI\n */\nfunction toRuleId(type: string): string {\n return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, \"-\")}`;\n}\n\n/**\n * Build SARIF rules from unique finding types.\n * Each unique vulnerability type becomes one rule.\n */\nfunction buildRules(findings: Finding[]): SarifRule[] {\n const seenTypes = new Map<string, Finding>();\n\n for (const f of findings) {\n if (!seenTypes.has(f.type)) {\n seenTypes.set(f.type, f);\n }\n }\n\n return Array.from(seenTypes.entries()).map(([type, sampleFinding]) => {\n const cwe = CWE_MAP[type] || CWE_MAP.custom;\n const ruleId = toRuleId(type);\n\n return {\n id: ruleId,\n name: type,\n shortDescription: {\n text: `${cwe.name} (CWE-${cwe.id})`,\n },\n fullDescription: {\n text: `Vulcn detected a potential ${type} vulnerability. ${cwe.name}. See CWE-${cwe.id} for details.`,\n },\n helpUri: `https://cwe.mitre.org/data/definitions/${cwe.id}.html`,\n help: {\n text: `## ${cwe.name}\\n\\nCWE-${cwe.id}: ${cwe.name}\\n\\nThis rule detects ${type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee https://cwe.mitre.org/data/definitions/${cwe.id}.html for detailed remediation guidance.`,\n markdown: `## ${cwe.name}\\n\\n**CWE-${cwe.id}**: ${cwe.name}\\n\\nThis rule detects \\`${type}\\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.\\n\\n### Remediation\\n\\nSee [CWE-${cwe.id}](https://cwe.mitre.org/data/definitions/${cwe.id}.html) for detailed remediation guidance.`,\n },\n properties: {\n tags: [\"security\", `CWE-${cwe.id}`, `external/cwe/cwe-${cwe.id}`],\n precision: toPrecision(sampleFinding.severity),\n \"security-severity\": toSecuritySeverity(sampleFinding.severity),\n },\n defaultConfiguration: {\n level: toSarifLevel(sampleFinding.severity),\n },\n };\n });\n}\n\n// ── Result Generation ──────────────────────────────────────────────────\n\n/**\n * Convert a Vulcn Finding to a SARIF Result.\n */\nfunction toSarifResult(finding: Finding, rules: SarifRule[]): SarifResult {\n const ruleId = toRuleId(finding.type);\n const ruleIndex = rules.findIndex((r) => r.id === ruleId);\n\n // Build message with evidence if available\n let messageText = `${finding.title}\\n\\n${finding.description}`;\n if (finding.evidence) {\n messageText += `\\n\\nEvidence: ${finding.evidence}`;\n }\n messageText += `\\n\\nPayload: ${finding.payload}`;\n\n // Build location from URL\n const uri = finding.url || \"unknown\";\n\n // Generate fingerprint from finding properties\n const fingerprint = `${finding.type}:${finding.stepId}:${finding.payload.slice(0, 50)}`;\n\n return {\n ruleId,\n ruleIndex: Math.max(ruleIndex, 0),\n level: toSarifLevel(finding.severity),\n message: { text: messageText },\n locations: [\n {\n physicalLocation: {\n artifactLocation: {\n uri,\n },\n region: {\n startLine: 1,\n },\n },\n logicalLocations: [\n {\n name: finding.stepId,\n kind: \"test-step\",\n },\n ],\n },\n ],\n fingerprints: {\n vulcnFindingV1: fingerprint,\n },\n partialFingerprints: {\n vulcnType: finding.type,\n vulcnStepId: finding.stepId,\n },\n properties: {\n severity: finding.severity,\n payload: finding.payload,\n stepId: finding.stepId,\n ...(finding.evidence ? { evidence: finding.evidence } : {}),\n ...(finding.metadata || {}),\n },\n };\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\n/**\n * Generate a SARIF v2.1.0 log from Vulcn scan results.\n *\n * Usage:\n * const sarif = generateSarif(session, result, generatedAt, \"0.4.0\");\n * await writeFile(\"vulcn-report.sarif\", JSON.stringify(sarif, null, 2));\n *\n * The output can be uploaded to:\n * - GitHub Code Scanning: `gh api /repos/{owner}/{repo}/code-scanning/sarifs`\n * - GitHub Actions: `github/codeql-action/upload-sarif@v3`\n * - Azure DevOps: SARIF SAST Scans Tab extension\n *\n * @param session - The session that was executed\n * @param result - The run result with findings\n * @param generatedAt - ISO timestamp\n * @param engineVersion - Vulcn engine version\n */\nexport function generateSarif(\n session: Session,\n result: RunResult,\n generatedAt: string,\n engineVersion: string,\n): SarifLog {\n const rules = buildRules(result.findings);\n const results = result.findings.map((f) => toSarifResult(f, rules));\n\n // Build artifact list from unique URLs\n const uniqueUrls = [\n ...new Set(result.findings.map((f) => f.url).filter(Boolean)),\n ];\n const artifacts: SarifArtifact[] = uniqueUrls.map((url) => ({\n location: { uri: url },\n }));\n\n // Calculate end time from duration\n const startDate = new Date(generatedAt);\n const endDate = new Date(startDate.getTime() + result.duration);\n\n const sarifLog: SarifLog = {\n $schema:\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"Vulcn\",\n version: engineVersion,\n semanticVersion: engineVersion,\n informationUri: \"https://vulcn.dev\",\n rules,\n },\n },\n results,\n invocations: [\n {\n executionSuccessful: result.errors.length === 0,\n startTimeUtc: generatedAt,\n endTimeUtc: endDate.toISOString(),\n properties: {\n sessionName: session.name,\n stepsExecuted: result.stepsExecuted,\n payloadsTested: result.payloadsTested,\n durationMs: result.duration,\n ...(result.errors.length > 0 ? { errors: result.errors } : {}),\n },\n },\n ],\n ...(artifacts.length > 0 ? { artifacts } : {}),\n },\n ],\n };\n\n return sarifLog;\n}\n"],"mappings":"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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"]}
|