@vulcn/plugin-report 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +650 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -46
- package/dist/index.d.ts +174 -46
- package/dist/index.js +649 -263
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
buildReport: () => buildReport,
|
|
33
34
|
configSchema: () => configSchema,
|
|
34
35
|
default: () => index_default,
|
|
35
36
|
generateHtml: () => generateHtml,
|
|
@@ -42,6 +43,262 @@ var import_zod = require("zod");
|
|
|
42
43
|
var import_promises = require("fs/promises");
|
|
43
44
|
var import_node_path = require("path");
|
|
44
45
|
|
|
46
|
+
// src/report-model.ts
|
|
47
|
+
var CWE_MAP = {
|
|
48
|
+
xss: {
|
|
49
|
+
id: 79,
|
|
50
|
+
name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
|
|
51
|
+
},
|
|
52
|
+
sqli: {
|
|
53
|
+
id: 89,
|
|
54
|
+
name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
|
|
55
|
+
},
|
|
56
|
+
ssrf: { id: 918, name: "Server-Side Request Forgery (SSRF)" },
|
|
57
|
+
xxe: {
|
|
58
|
+
id: 611,
|
|
59
|
+
name: "Improper Restriction of XML External Entity Reference"
|
|
60
|
+
},
|
|
61
|
+
"command-injection": {
|
|
62
|
+
id: 78,
|
|
63
|
+
name: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
|
|
64
|
+
},
|
|
65
|
+
"path-traversal": {
|
|
66
|
+
id: 22,
|
|
67
|
+
name: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
|
|
68
|
+
},
|
|
69
|
+
"open-redirect": {
|
|
70
|
+
id: 601,
|
|
71
|
+
name: "URL Redirection to Untrusted Site ('Open Redirect')"
|
|
72
|
+
},
|
|
73
|
+
reflection: {
|
|
74
|
+
id: 200,
|
|
75
|
+
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
76
|
+
},
|
|
77
|
+
"security-misconfiguration": {
|
|
78
|
+
id: 16,
|
|
79
|
+
name: "Configuration"
|
|
80
|
+
},
|
|
81
|
+
"information-disclosure": {
|
|
82
|
+
id: 200,
|
|
83
|
+
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
84
|
+
},
|
|
85
|
+
custom: { id: 20, name: "Improper Input Validation" }
|
|
86
|
+
};
|
|
87
|
+
var SEVERITY_WEIGHTS = {
|
|
88
|
+
critical: 10,
|
|
89
|
+
high: 7,
|
|
90
|
+
medium: 4,
|
|
91
|
+
low: 1,
|
|
92
|
+
info: 0
|
|
93
|
+
};
|
|
94
|
+
var SECURITY_SEVERITY = {
|
|
95
|
+
critical: "9.0",
|
|
96
|
+
high: "7.0",
|
|
97
|
+
medium: "4.0",
|
|
98
|
+
low: "2.0",
|
|
99
|
+
info: "0.0"
|
|
100
|
+
};
|
|
101
|
+
var PASSIVE_CATEGORIES = [
|
|
102
|
+
{
|
|
103
|
+
id: "security-headers",
|
|
104
|
+
label: "Security Headers",
|
|
105
|
+
icon: "\u{1F512}",
|
|
106
|
+
color: "#42a5f5",
|
|
107
|
+
remedy: "Add the recommended security headers to your server configuration. Most web servers and frameworks support these via middleware.",
|
|
108
|
+
checks: [
|
|
109
|
+
"Strict-Transport-Security (HSTS)",
|
|
110
|
+
"Content-Security-Policy (CSP)",
|
|
111
|
+
"X-Content-Type-Options",
|
|
112
|
+
"X-Frame-Options",
|
|
113
|
+
"Referrer-Policy",
|
|
114
|
+
"Permissions-Policy"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "cookie-security",
|
|
119
|
+
label: "Cookie Security",
|
|
120
|
+
icon: "\u{1F36A}",
|
|
121
|
+
color: "#ffab40",
|
|
122
|
+
remedy: "Set the Secure, HttpOnly, and SameSite attributes on all session cookies. Configure your framework's session middleware accordingly.",
|
|
123
|
+
checks: ["Secure flag", "HttpOnly flag", "SameSite attribute"]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "information-disclosure",
|
|
127
|
+
label: "Information Disclosure",
|
|
128
|
+
icon: "\u{1F50D}",
|
|
129
|
+
color: "#66bb6a",
|
|
130
|
+
remedy: "Remove or obfuscate server version headers (Server, X-Powered-By). Disable debug mode in production environments.",
|
|
131
|
+
checks: ["Server version", "X-Powered-By", "Debug tokens"]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "cors",
|
|
135
|
+
label: "CORS Configuration",
|
|
136
|
+
icon: "\u{1F310}",
|
|
137
|
+
color: "#ce93d8",
|
|
138
|
+
remedy: "Replace wildcard origins with specific trusted domains. Never combine Access-Control-Allow-Credentials with wildcard origins.",
|
|
139
|
+
checks: ["Wildcard origin", "Credentials with wildcard"]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "mixed-content",
|
|
143
|
+
label: "Mixed Content",
|
|
144
|
+
icon: "\u26A0\uFE0F",
|
|
145
|
+
color: "#ff8a65",
|
|
146
|
+
remedy: "Replace all HTTP resource URLs with HTTPS. Use Content-Security-Policy: upgrade-insecure-requests as a fallback.",
|
|
147
|
+
checks: ["HTTP resources on HTTPS"]
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
function toRuleId(type) {
|
|
151
|
+
return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`;
|
|
152
|
+
}
|
|
153
|
+
function fingerprint(f) {
|
|
154
|
+
return `${f.type}:${f.stepId}:${f.payload.slice(0, 50)}`;
|
|
155
|
+
}
|
|
156
|
+
function detectMethod(f) {
|
|
157
|
+
return f.metadata?.detectionMethod === "passive" ? "passive" : "active";
|
|
158
|
+
}
|
|
159
|
+
function passiveCat(f) {
|
|
160
|
+
const method = detectMethod(f);
|
|
161
|
+
if (method !== "passive") return void 0;
|
|
162
|
+
return f.metadata?.category || "other";
|
|
163
|
+
}
|
|
164
|
+
function enrichFinding(f) {
|
|
165
|
+
const cwe = CWE_MAP[f.type] || CWE_MAP.custom;
|
|
166
|
+
const sev = f.severity;
|
|
167
|
+
return {
|
|
168
|
+
// Original fields
|
|
169
|
+
type: f.type,
|
|
170
|
+
severity: sev,
|
|
171
|
+
title: f.title,
|
|
172
|
+
description: f.description,
|
|
173
|
+
stepId: f.stepId,
|
|
174
|
+
payload: f.payload,
|
|
175
|
+
url: f.url,
|
|
176
|
+
evidence: f.evidence,
|
|
177
|
+
metadata: f.metadata,
|
|
178
|
+
// Enriched
|
|
179
|
+
ruleId: toRuleId(f.type),
|
|
180
|
+
cwe,
|
|
181
|
+
securitySeverity: SECURITY_SEVERITY[sev] || "4.0",
|
|
182
|
+
fingerprint: fingerprint(f),
|
|
183
|
+
detectionMethod: detectMethod(f),
|
|
184
|
+
passiveCategory: passiveCat(f)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function buildRules(enriched) {
|
|
188
|
+
const seen = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const f of enriched) {
|
|
190
|
+
if (!seen.has(f.type)) seen.set(f.type, f);
|
|
191
|
+
}
|
|
192
|
+
return Array.from(seen.entries()).map(([type, sample]) => ({
|
|
193
|
+
id: toRuleId(type),
|
|
194
|
+
type,
|
|
195
|
+
cwe: sample.cwe,
|
|
196
|
+
severity: sample.severity,
|
|
197
|
+
securitySeverity: sample.securitySeverity,
|
|
198
|
+
description: `Vulcn detected a potential ${type} vulnerability. ${sample.cwe.name}.`
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
function countSeverities(findings) {
|
|
202
|
+
const counts = {
|
|
203
|
+
critical: 0,
|
|
204
|
+
high: 0,
|
|
205
|
+
medium: 0,
|
|
206
|
+
low: 0,
|
|
207
|
+
info: 0
|
|
208
|
+
};
|
|
209
|
+
for (const f of findings) {
|
|
210
|
+
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
211
|
+
}
|
|
212
|
+
return counts;
|
|
213
|
+
}
|
|
214
|
+
function assessRisk(counts, total) {
|
|
215
|
+
const score = counts.critical * SEVERITY_WEIGHTS.critical + counts.high * SEVERITY_WEIGHTS.high + counts.medium * SEVERITY_WEIGHTS.medium + counts.low * SEVERITY_WEIGHTS.low;
|
|
216
|
+
const maxRisk = total * SEVERITY_WEIGHTS.critical || 1;
|
|
217
|
+
const percent = Math.min(100, Math.round(score / maxRisk * 100));
|
|
218
|
+
const label = percent >= 80 ? "Critical" : percent >= 50 ? "High" : percent >= 25 ? "Medium" : percent > 0 ? "Low" : "Clear";
|
|
219
|
+
return { score, percent, label };
|
|
220
|
+
}
|
|
221
|
+
function buildPassiveAnalysis(passiveFindings) {
|
|
222
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const f of passiveFindings) {
|
|
224
|
+
const cat = f.passiveCategory || "other";
|
|
225
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
226
|
+
grouped.get(cat).push(f);
|
|
227
|
+
}
|
|
228
|
+
const categories = PASSIVE_CATEGORIES.map((def) => {
|
|
229
|
+
const findings = grouped.get(def.id) || [];
|
|
230
|
+
const issueCount = findings.length;
|
|
231
|
+
const totalChecks = def.checks.length;
|
|
232
|
+
const passedChecks = Math.max(0, totalChecks - issueCount);
|
|
233
|
+
const status = issueCount === 0 ? "pass" : issueCount >= 3 ? "fail" : "warn";
|
|
234
|
+
return {
|
|
235
|
+
definition: def,
|
|
236
|
+
findings,
|
|
237
|
+
issueCount,
|
|
238
|
+
passedChecks,
|
|
239
|
+
totalChecks,
|
|
240
|
+
status
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
totalIssues: passiveFindings.length,
|
|
245
|
+
categories
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function buildReport(session, result, generatedAt, engineVersion) {
|
|
249
|
+
const severityOrder = {
|
|
250
|
+
critical: 0,
|
|
251
|
+
high: 1,
|
|
252
|
+
medium: 2,
|
|
253
|
+
low: 3,
|
|
254
|
+
info: 4
|
|
255
|
+
};
|
|
256
|
+
const sortedFindings = [...result.findings].sort(
|
|
257
|
+
(a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5)
|
|
258
|
+
);
|
|
259
|
+
const findings = sortedFindings.map(enrichFinding);
|
|
260
|
+
const activeFindings = findings.filter((f) => f.detectionMethod === "active");
|
|
261
|
+
const passiveFindings = findings.filter(
|
|
262
|
+
(f) => f.detectionMethod === "passive"
|
|
263
|
+
);
|
|
264
|
+
const counts = countSeverities(findings);
|
|
265
|
+
const risk = assessRisk(counts, findings.length);
|
|
266
|
+
const rules = buildRules(findings);
|
|
267
|
+
return {
|
|
268
|
+
reportVersion: "2.0",
|
|
269
|
+
engineVersion,
|
|
270
|
+
generatedAt,
|
|
271
|
+
session: {
|
|
272
|
+
name: session.name,
|
|
273
|
+
driver: session.driver,
|
|
274
|
+
driverConfig: session.driverConfig,
|
|
275
|
+
stepsCount: session.steps.length,
|
|
276
|
+
metadata: session.metadata
|
|
277
|
+
},
|
|
278
|
+
stats: {
|
|
279
|
+
stepsExecuted: result.stepsExecuted,
|
|
280
|
+
payloadsTested: result.payloadsTested,
|
|
281
|
+
durationMs: result.duration,
|
|
282
|
+
errors: result.errors
|
|
283
|
+
},
|
|
284
|
+
summary: {
|
|
285
|
+
totalFindings: findings.length,
|
|
286
|
+
severityCounts: counts,
|
|
287
|
+
risk,
|
|
288
|
+
vulnerabilityTypes: [...new Set(findings.map((f) => f.type))],
|
|
289
|
+
affectedUrls: [...new Set(findings.map((f) => f.url))]
|
|
290
|
+
},
|
|
291
|
+
rules,
|
|
292
|
+
findings,
|
|
293
|
+
activeFindings,
|
|
294
|
+
passiveAnalysis: buildPassiveAnalysis(passiveFindings)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function formatDuration(ms) {
|
|
298
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
299
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
300
|
+
}
|
|
301
|
+
|
|
45
302
|
// src/html.ts
|
|
46
303
|
var COLORS = {
|
|
47
304
|
bg: "#0a0a0f",
|
|
@@ -78,30 +335,9 @@ function severityColor(severity) {
|
|
|
78
335
|
return COLORS.textMuted;
|
|
79
336
|
}
|
|
80
337
|
}
|
|
81
|
-
function severityOrder(severity) {
|
|
82
|
-
switch (severity) {
|
|
83
|
-
case "critical":
|
|
84
|
-
return 0;
|
|
85
|
-
case "high":
|
|
86
|
-
return 1;
|
|
87
|
-
case "medium":
|
|
88
|
-
return 2;
|
|
89
|
-
case "low":
|
|
90
|
-
return 3;
|
|
91
|
-
case "info":
|
|
92
|
-
return 4;
|
|
93
|
-
default:
|
|
94
|
-
return 5;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
338
|
function escapeHtml(str) {
|
|
98
339
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
99
340
|
}
|
|
100
|
-
function formatDuration(ms) {
|
|
101
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
102
|
-
const seconds = (ms / 1e3).toFixed(1);
|
|
103
|
-
return `${seconds}s`;
|
|
104
|
-
}
|
|
105
341
|
function formatDate(iso) {
|
|
106
342
|
const d = new Date(iso);
|
|
107
343
|
return d.toLocaleDateString("en-US", {
|
|
@@ -128,31 +364,95 @@ var VULCN_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20
|
|
|
128
364
|
<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"/>
|
|
129
365
|
<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"/>
|
|
130
366
|
</svg>`;
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
367
|
+
function renderPassiveSection(analysis) {
|
|
368
|
+
if (analysis.totalIssues === 0) {
|
|
369
|
+
return `
|
|
370
|
+
<div class="passive-section animate-in-delay-3">
|
|
371
|
+
<div class="section-header">
|
|
372
|
+
<h3>\u{1F6E1}\uFE0F Passive Security Analysis</h3>
|
|
373
|
+
<span class="findings-count">All clear</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="passive-clear">
|
|
376
|
+
<div class="icon">\u2705</div>
|
|
377
|
+
<h4>All Passive Checks Passed</h4>
|
|
378
|
+
<p>No security header, cookie, CORS, or information disclosure issues detected.</p>
|
|
379
|
+
</div>
|
|
380
|
+
</div>`;
|
|
145
381
|
}
|
|
146
|
-
const
|
|
382
|
+
const categoryCards = analysis.categories.map((cat) => {
|
|
383
|
+
const statusColor = cat.status === "pass" ? COLORS.success : cat.status === "fail" ? COLORS.high : COLORS.medium;
|
|
384
|
+
return `
|
|
385
|
+
<div class="passive-category-card">
|
|
386
|
+
<div class="passive-cat-header">
|
|
387
|
+
<div class="passive-cat-icon">${cat.definition.icon}</div>
|
|
388
|
+
<div class="passive-cat-info">
|
|
389
|
+
<div class="passive-cat-title">${cat.definition.label}</div>
|
|
390
|
+
<div class="passive-cat-count" style="color: ${statusColor}">
|
|
391
|
+
${cat.issueCount === 0 ? "\u2713 All clear" : `${cat.issueCount} issue${cat.issueCount !== 1 ? "s" : ""}`}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="passive-cat-badge" style="background: ${statusColor}20; color: ${statusColor}; border-color: ${statusColor}30">
|
|
395
|
+
${cat.status.toUpperCase()}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
${cat.findings.length > 0 ? `
|
|
399
|
+
<div class="passive-cat-findings">
|
|
400
|
+
${cat.findings.map(
|
|
401
|
+
(f) => `
|
|
402
|
+
<div class="passive-finding-row">
|
|
403
|
+
<div class="passive-finding-dot" style="background: ${severityColor(f.severity)}"></div>
|
|
404
|
+
<div class="passive-finding-content">
|
|
405
|
+
<div class="passive-finding-title">${escapeHtml(f.title)}</div>
|
|
406
|
+
<div class="passive-finding-desc">${escapeHtml(f.description)}</div>
|
|
407
|
+
${f.evidence ? `<div class="passive-finding-evidence">${escapeHtml(f.evidence)}</div>` : ""}
|
|
408
|
+
</div>
|
|
409
|
+
<span class="passive-sev-tag" style="color: ${severityColor(f.severity)}">${f.severity.toUpperCase()}</span>
|
|
410
|
+
</div>
|
|
411
|
+
`
|
|
412
|
+
).join("")}
|
|
413
|
+
</div>
|
|
414
|
+
<div class="passive-remedy">
|
|
415
|
+
<span class="passive-remedy-label">\u{1F4A1} Remediation</span>
|
|
416
|
+
<span>${cat.definition.remedy}</span>
|
|
417
|
+
</div>
|
|
418
|
+
` : `
|
|
419
|
+
<div class="passive-checks-passed">
|
|
420
|
+
${cat.definition.checks.map(
|
|
421
|
+
(check) => `
|
|
422
|
+
<div class="passive-check-item">
|
|
423
|
+
<span class="passive-check-icon">\u2713</span>
|
|
424
|
+
<span>${check}</span>
|
|
425
|
+
</div>
|
|
426
|
+
`
|
|
427
|
+
).join("")}
|
|
428
|
+
</div>
|
|
429
|
+
`}
|
|
430
|
+
</div>`;
|
|
431
|
+
}).join("");
|
|
432
|
+
return `
|
|
433
|
+
<div class="passive-section animate-in-delay-3">
|
|
434
|
+
<div class="section-header">
|
|
435
|
+
<h3>\u{1F6E1}\uFE0F Passive Security Analysis</h3>
|
|
436
|
+
<span class="findings-count">${analysis.totalIssues} issue${analysis.totalIssues !== 1 ? "s" : ""}</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="passive-grid">
|
|
439
|
+
${categoryCards}
|
|
440
|
+
</div>
|
|
441
|
+
</div>`;
|
|
442
|
+
}
|
|
443
|
+
function generateHtml(report) {
|
|
444
|
+
const { summary, stats, session, passiveAnalysis, activeFindings, findings } = report;
|
|
445
|
+
const {
|
|
446
|
+
severityCounts: counts,
|
|
447
|
+
risk,
|
|
448
|
+
totalFindings,
|
|
449
|
+
affectedUrls,
|
|
450
|
+
vulnerabilityTypes: vulnTypes
|
|
451
|
+
} = summary;
|
|
452
|
+
const { stepsExecuted, payloadsTested, durationMs, errors } = stats;
|
|
147
453
|
const hasFindings = totalFindings > 0;
|
|
148
|
-
const
|
|
149
|
-
const maxRisk = totalFindings * 10 || 1;
|
|
150
|
-
const riskPercent = Math.min(100, Math.round(riskScore / maxRisk * 100));
|
|
151
|
-
const riskLabel = riskPercent >= 80 ? "Critical" : riskPercent >= 50 ? "High" : riskPercent >= 25 ? "Medium" : riskPercent > 0 ? "Low" : "Clear";
|
|
152
|
-
const riskColor = riskPercent >= 80 ? COLORS.critical : riskPercent >= 50 ? COLORS.high : riskPercent >= 25 ? COLORS.medium : riskPercent > 0 ? COLORS.low : COLORS.success;
|
|
454
|
+
const riskColor = risk.percent >= 80 ? COLORS.critical : risk.percent >= 50 ? COLORS.high : risk.percent >= 25 ? COLORS.medium : risk.percent > 0 ? COLORS.low : COLORS.success;
|
|
153
455
|
const donutSvg = generateDonut(counts, totalFindings);
|
|
154
|
-
const affectedUrls = [...new Set(findings.map((f) => f.url))];
|
|
155
|
-
const vulnTypes = [...new Set(findings.map((f) => f.type))];
|
|
156
456
|
return `<!DOCTYPE html>
|
|
157
457
|
<html lang="en">
|
|
158
458
|
<head>
|
|
@@ -644,6 +944,180 @@ function generateHtml(data) {
|
|
|
644
944
|
.no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }
|
|
645
945
|
.no-findings p { font-size: 14px; color: var(--text-muted); }
|
|
646
946
|
|
|
947
|
+
/* \u2500\u2500 Passive Security Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
948
|
+
.passive-section { margin-bottom: 32px; }
|
|
949
|
+
|
|
950
|
+
.section-header {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: space-between;
|
|
954
|
+
margin-bottom: 16px;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.section-header h3 {
|
|
958
|
+
font-size: 18px;
|
|
959
|
+
font-weight: 700;
|
|
960
|
+
letter-spacing: -0.01em;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.passive-grid {
|
|
964
|
+
display: grid;
|
|
965
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
966
|
+
gap: 12px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.passive-category-card {
|
|
970
|
+
background: var(--surface);
|
|
971
|
+
border: 1px solid var(--border);
|
|
972
|
+
border-radius: var(--radius);
|
|
973
|
+
overflow: hidden;
|
|
974
|
+
transition: border-color 0.2s;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.passive-category-card:hover { border-color: var(--border-active); }
|
|
978
|
+
|
|
979
|
+
.passive-cat-header {
|
|
980
|
+
display: flex;
|
|
981
|
+
align-items: center;
|
|
982
|
+
gap: 12px;
|
|
983
|
+
padding: 16px 20px;
|
|
984
|
+
border-bottom: 1px solid var(--border);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.passive-cat-icon {
|
|
988
|
+
font-size: 20px;
|
|
989
|
+
width: 36px;
|
|
990
|
+
height: 36px;
|
|
991
|
+
display: flex;
|
|
992
|
+
align-items: center;
|
|
993
|
+
justify-content: center;
|
|
994
|
+
background: rgba(255,255,255,0.03);
|
|
995
|
+
border-radius: var(--radius-xs);
|
|
996
|
+
flex-shrink: 0;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.passive-cat-info { flex: 1; min-width: 0; }
|
|
1000
|
+
|
|
1001
|
+
.passive-cat-title {
|
|
1002
|
+
font-size: 14px;
|
|
1003
|
+
font-weight: 600;
|
|
1004
|
+
letter-spacing: -0.01em;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.passive-cat-count {
|
|
1008
|
+
font-size: 12px;
|
|
1009
|
+
font-weight: 500;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.passive-cat-badge {
|
|
1013
|
+
font-size: 10px;
|
|
1014
|
+
font-weight: 700;
|
|
1015
|
+
letter-spacing: 0.05em;
|
|
1016
|
+
padding: 3px 8px;
|
|
1017
|
+
border-radius: 100px;
|
|
1018
|
+
border: 1px solid;
|
|
1019
|
+
flex-shrink: 0;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.passive-cat-findings {
|
|
1023
|
+
padding: 12px 20px;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.passive-finding-row {
|
|
1027
|
+
display: flex;
|
|
1028
|
+
align-items: flex-start;
|
|
1029
|
+
gap: 10px;
|
|
1030
|
+
padding: 8px 0;
|
|
1031
|
+
border-bottom: 1px solid rgba(255,255,255,0.03);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.passive-finding-row:last-child { border-bottom: none; }
|
|
1035
|
+
|
|
1036
|
+
.passive-finding-dot {
|
|
1037
|
+
width: 6px;
|
|
1038
|
+
height: 6px;
|
|
1039
|
+
border-radius: 50%;
|
|
1040
|
+
flex-shrink: 0;
|
|
1041
|
+
margin-top: 7px;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.passive-finding-content { flex: 1; min-width: 0; }
|
|
1045
|
+
|
|
1046
|
+
.passive-finding-title {
|
|
1047
|
+
font-size: 13px;
|
|
1048
|
+
font-weight: 500;
|
|
1049
|
+
margin-bottom: 2px;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.passive-finding-desc {
|
|
1053
|
+
font-size: 12px;
|
|
1054
|
+
color: var(--text-muted);
|
|
1055
|
+
line-height: 1.4;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.passive-finding-evidence {
|
|
1059
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1060
|
+
font-size: 11px;
|
|
1061
|
+
color: var(--text-dim);
|
|
1062
|
+
margin-top: 4px;
|
|
1063
|
+
padding: 4px 8px;
|
|
1064
|
+
background: rgba(255,255,255,0.02);
|
|
1065
|
+
border-radius: 4px;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.passive-sev-tag {
|
|
1069
|
+
font-size: 10px;
|
|
1070
|
+
font-weight: 700;
|
|
1071
|
+
letter-spacing: 0.04em;
|
|
1072
|
+
flex-shrink: 0;
|
|
1073
|
+
margin-top: 2px;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.passive-remedy {
|
|
1077
|
+
padding: 12px 20px;
|
|
1078
|
+
background: rgba(66, 165, 245, 0.04);
|
|
1079
|
+
border-top: 1px solid rgba(66, 165, 245, 0.08);
|
|
1080
|
+
font-size: 12px;
|
|
1081
|
+
color: var(--text-muted);
|
|
1082
|
+
line-height: 1.5;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.passive-remedy-label {
|
|
1086
|
+
font-weight: 600;
|
|
1087
|
+
margin-right: 6px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.passive-checks-passed {
|
|
1091
|
+
padding: 12px 20px;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.passive-check-item {
|
|
1095
|
+
display: flex;
|
|
1096
|
+
align-items: center;
|
|
1097
|
+
gap: 8px;
|
|
1098
|
+
padding: 4px 0;
|
|
1099
|
+
font-size: 12px;
|
|
1100
|
+
color: var(--text-muted);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.passive-check-icon {
|
|
1104
|
+
color: ${COLORS.success};
|
|
1105
|
+
font-weight: 700;
|
|
1106
|
+
font-size: 11px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.passive-clear {
|
|
1110
|
+
text-align: center;
|
|
1111
|
+
padding: 40px 24px;
|
|
1112
|
+
background: var(--surface);
|
|
1113
|
+
border: 1px solid var(--border);
|
|
1114
|
+
border-radius: var(--radius);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.passive-clear .icon { font-size: 36px; margin-bottom: 12px; }
|
|
1118
|
+
.passive-clear h4 { font-size: 16px; font-weight: 600; color: ${COLORS.success}; margin-bottom: 6px; }
|
|
1119
|
+
.passive-clear p { font-size: 13px; color: var(--text-muted); }
|
|
1120
|
+
|
|
647
1121
|
/* Errors section */
|
|
648
1122
|
.errors-section {
|
|
649
1123
|
margin-bottom: 32px;
|
|
@@ -708,6 +1182,7 @@ function generateHtml(data) {
|
|
|
708
1182
|
body::before { display: none; }
|
|
709
1183
|
.finding-details { display: block !important; padding-top: 12px !important; }
|
|
710
1184
|
.finding-card { page-break-inside: avoid; }
|
|
1185
|
+
.passive-category-card { page-break-inside: avoid; }
|
|
711
1186
|
}
|
|
712
1187
|
</style>
|
|
713
1188
|
</head>
|
|
@@ -723,8 +1198,8 @@ function generateHtml(data) {
|
|
|
723
1198
|
</div>
|
|
724
1199
|
</div>
|
|
725
1200
|
<div class="header-meta">
|
|
726
|
-
<div>${formatDate(generatedAt)}</div>
|
|
727
|
-
<div>Engine v${escapeHtml(engineVersion)}</div>
|
|
1201
|
+
<div>${formatDate(report.generatedAt)}</div>
|
|
1202
|
+
<div>Engine v${escapeHtml(report.engineVersion)}</div>
|
|
728
1203
|
</div>
|
|
729
1204
|
</div>
|
|
730
1205
|
|
|
@@ -739,11 +1214,11 @@ function generateHtml(data) {
|
|
|
739
1214
|
${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>` : ""}
|
|
740
1215
|
<div class="meta-item">
|
|
741
1216
|
<span class="meta-label">Duration</span>
|
|
742
|
-
<span class="meta-value">${formatDuration(
|
|
1217
|
+
<span class="meta-value">${formatDuration(durationMs)}</span>
|
|
743
1218
|
</div>
|
|
744
1219
|
<div class="meta-item">
|
|
745
1220
|
<span class="meta-label">Generated</span>
|
|
746
|
-
<span class="meta-value">${formatDate(generatedAt)}</span>
|
|
1221
|
+
<span class="meta-value">${formatDate(report.generatedAt)}</span>
|
|
747
1222
|
</div>
|
|
748
1223
|
</div>
|
|
749
1224
|
</div>
|
|
@@ -756,13 +1231,13 @@ function generateHtml(data) {
|
|
|
756
1231
|
<svg viewBox="0 0 160 160" width="160" height="160">
|
|
757
1232
|
<circle cx="80" cy="80" r="68" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="10"/>
|
|
758
1233
|
<circle cx="80" cy="80" r="68" fill="none" stroke="${riskColor}" stroke-width="10"
|
|
759
|
-
stroke-dasharray="${
|
|
1234
|
+
stroke-dasharray="${risk.percent / 100 * 427} 427"
|
|
760
1235
|
stroke-linecap="round"
|
|
761
1236
|
style="filter: drop-shadow(0 0 6px ${riskColor});"/>
|
|
762
1237
|
</svg>
|
|
763
1238
|
<div class="risk-gauge-label">
|
|
764
|
-
<div class="score" style="color: ${riskColor}">${hasFindings ?
|
|
765
|
-
<div class="label">${
|
|
1239
|
+
<div class="score" style="color: ${riskColor}">${hasFindings ? risk.percent : 0}</div>
|
|
1240
|
+
<div class="label">${risk.label}</div>
|
|
766
1241
|
</div>
|
|
767
1242
|
</div>
|
|
768
1243
|
</div>
|
|
@@ -775,11 +1250,11 @@ function generateHtml(data) {
|
|
|
775
1250
|
<div class="stat-label">Findings</div>
|
|
776
1251
|
</div>
|
|
777
1252
|
<div class="stat-box">
|
|
778
|
-
<div class="stat-number">${
|
|
1253
|
+
<div class="stat-number">${payloadsTested}</div>
|
|
779
1254
|
<div class="stat-label">Payloads Tested</div>
|
|
780
1255
|
</div>
|
|
781
1256
|
<div class="stat-box">
|
|
782
|
-
<div class="stat-number">${
|
|
1257
|
+
<div class="stat-number">${stepsExecuted}</div>
|
|
783
1258
|
<div class="stat-label">Steps Executed</div>
|
|
784
1259
|
</div>
|
|
785
1260
|
<div class="stat-box">
|
|
@@ -807,14 +1282,17 @@ function generateHtml(data) {
|
|
|
807
1282
|
</div>
|
|
808
1283
|
</div>
|
|
809
1284
|
|
|
810
|
-
<!--
|
|
1285
|
+
<!-- Passive Security Analysis -->
|
|
1286
|
+
${renderPassiveSection(passiveAnalysis)}
|
|
1287
|
+
|
|
1288
|
+
<!-- Active Findings -->
|
|
811
1289
|
<div class="findings-section animate-in-delay-3">
|
|
812
1290
|
<div class="findings-header">
|
|
813
|
-
<h3
|
|
814
|
-
<span class="findings-count">${
|
|
1291
|
+
<h3>\u{1F3AF} Active Scan Findings</h3>
|
|
1292
|
+
<span class="findings-count">${activeFindings.length} finding${activeFindings.length !== 1 ? "s" : ""}</span>
|
|
815
1293
|
</div>
|
|
816
1294
|
|
|
817
|
-
${
|
|
1295
|
+
${activeFindings.length > 0 ? activeFindings.map(
|
|
818
1296
|
(f, i) => `
|
|
819
1297
|
<div class="finding-card" onclick="this.classList.toggle('open')">
|
|
820
1298
|
<div class="finding-header">
|
|
@@ -860,16 +1338,16 @@ function generateHtml(data) {
|
|
|
860
1338
|
).join("") : `
|
|
861
1339
|
<div class="no-findings">
|
|
862
1340
|
<div class="icon">\u{1F6E1}\uFE0F</div>
|
|
863
|
-
<h3>No Vulnerabilities Detected</h3>
|
|
864
|
-
<p>${
|
|
1341
|
+
<h3>No Active Vulnerabilities Detected</h3>
|
|
1342
|
+
<p>${payloadsTested} payloads were tested across ${stepsExecuted} steps with no findings.</p>
|
|
865
1343
|
</div>
|
|
866
1344
|
`}
|
|
867
1345
|
</div>
|
|
868
1346
|
|
|
869
|
-
${
|
|
1347
|
+
${errors.length > 0 ? `
|
|
870
1348
|
<div class="errors-section">
|
|
871
|
-
<h3>\u26A0\uFE0F Errors During Execution (${
|
|
872
|
-
${
|
|
1349
|
+
<h3>\u26A0\uFE0F Errors During Execution (${errors.length})</h3>
|
|
1350
|
+
${errors.map((e) => `<div class="error-item">${escapeHtml(e)}</div>`).join("")}
|
|
873
1351
|
</div>
|
|
874
1352
|
` : ""}
|
|
875
1353
|
|
|
@@ -904,110 +1382,63 @@ function generateDonut(counts, total) {
|
|
|
904
1382
|
}
|
|
905
1383
|
|
|
906
1384
|
// src/json.ts
|
|
907
|
-
function
|
|
908
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
909
|
-
return `${(ms / 1e3).toFixed(1)}s`;
|
|
910
|
-
}
|
|
911
|
-
function generateJson(session, result, generatedAt, engineVersion) {
|
|
912
|
-
const counts = {
|
|
913
|
-
critical: 0,
|
|
914
|
-
high: 0,
|
|
915
|
-
medium: 0,
|
|
916
|
-
low: 0,
|
|
917
|
-
info: 0
|
|
918
|
-
};
|
|
919
|
-
for (const f of result.findings) {
|
|
920
|
-
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
921
|
-
}
|
|
922
|
-
const riskScore = counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;
|
|
1385
|
+
function generateJson(report) {
|
|
923
1386
|
return {
|
|
924
1387
|
vulcn: {
|
|
925
|
-
version: engineVersion,
|
|
926
|
-
reportVersion:
|
|
927
|
-
generatedAt
|
|
928
|
-
},
|
|
929
|
-
session: {
|
|
930
|
-
name: session.name,
|
|
931
|
-
driver: session.driver,
|
|
932
|
-
driverConfig: session.driverConfig,
|
|
933
|
-
stepsCount: session.steps.length,
|
|
934
|
-
metadata: session.metadata
|
|
1388
|
+
version: report.engineVersion,
|
|
1389
|
+
reportVersion: report.reportVersion,
|
|
1390
|
+
generatedAt: report.generatedAt
|
|
935
1391
|
},
|
|
1392
|
+
session: report.session,
|
|
936
1393
|
execution: {
|
|
937
|
-
stepsExecuted:
|
|
938
|
-
payloadsTested:
|
|
939
|
-
durationMs:
|
|
940
|
-
durationFormatted:
|
|
941
|
-
errors:
|
|
1394
|
+
stepsExecuted: report.stats.stepsExecuted,
|
|
1395
|
+
payloadsTested: report.stats.payloadsTested,
|
|
1396
|
+
durationMs: report.stats.durationMs,
|
|
1397
|
+
durationFormatted: formatDuration(report.stats.durationMs),
|
|
1398
|
+
errors: report.stats.errors
|
|
942
1399
|
},
|
|
943
1400
|
summary: {
|
|
944
|
-
totalFindings:
|
|
945
|
-
riskScore,
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1401
|
+
totalFindings: report.summary.totalFindings,
|
|
1402
|
+
riskScore: report.summary.risk.score,
|
|
1403
|
+
riskLabel: report.summary.risk.label,
|
|
1404
|
+
severityCounts: report.summary.severityCounts,
|
|
1405
|
+
vulnerabilityTypes: report.summary.vulnerabilityTypes,
|
|
1406
|
+
affectedUrls: report.summary.affectedUrls
|
|
1407
|
+
},
|
|
1408
|
+
findings: report.findings,
|
|
1409
|
+
passiveAnalysis: {
|
|
1410
|
+
totalIssues: report.passiveAnalysis.totalIssues,
|
|
1411
|
+
categories: report.passiveAnalysis.categories.map((c) => ({
|
|
1412
|
+
id: c.definition.id,
|
|
1413
|
+
label: c.definition.label,
|
|
1414
|
+
status: c.status,
|
|
1415
|
+
issueCount: c.issueCount,
|
|
1416
|
+
passedChecks: c.passedChecks,
|
|
1417
|
+
totalChecks: c.totalChecks,
|
|
1418
|
+
remedy: c.definition.remedy
|
|
1419
|
+
}))
|
|
949
1420
|
},
|
|
950
|
-
|
|
1421
|
+
rules: report.rules
|
|
951
1422
|
};
|
|
952
1423
|
}
|
|
953
1424
|
|
|
954
1425
|
// src/yaml.ts
|
|
955
1426
|
var import_yaml = require("yaml");
|
|
956
|
-
function generateYaml(
|
|
957
|
-
const
|
|
1427
|
+
function generateYaml(report) {
|
|
1428
|
+
const jsonReport = generateJson(report);
|
|
958
1429
|
const header = [
|
|
959
1430
|
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
960
1431
|
"# Vulcn Security Report",
|
|
961
|
-
`# Generated: ${generatedAt}`,
|
|
962
|
-
`# Session: ${session.name}`,
|
|
963
|
-
`# Findings: ${
|
|
1432
|
+
`# Generated: ${report.generatedAt}`,
|
|
1433
|
+
`# Session: ${report.session.name}`,
|
|
1434
|
+
`# Findings: ${report.summary.totalFindings}`,
|
|
964
1435
|
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
965
1436
|
""
|
|
966
1437
|
].join("\n");
|
|
967
|
-
return header + (0, import_yaml.stringify)(
|
|
1438
|
+
return header + (0, import_yaml.stringify)(jsonReport, { indent: 2 });
|
|
968
1439
|
}
|
|
969
1440
|
|
|
970
1441
|
// 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
1442
|
function toSarifLevel(severity) {
|
|
1012
1443
|
switch (severity) {
|
|
1013
1444
|
case "critical":
|
|
@@ -1022,22 +1453,6 @@ function toSarifLevel(severity) {
|
|
|
1022
1453
|
return "warning";
|
|
1023
1454
|
}
|
|
1024
1455
|
}
|
|
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
1456
|
function toPrecision(severity) {
|
|
1042
1457
|
switch (severity) {
|
|
1043
1458
|
case "critical":
|
|
@@ -1053,63 +1468,53 @@ function toPrecision(severity) {
|
|
|
1053
1468
|
return "medium";
|
|
1054
1469
|
}
|
|
1055
1470
|
}
|
|
1056
|
-
function
|
|
1057
|
-
return
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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.
|
|
1471
|
+
function toSarifRule(rule) {
|
|
1472
|
+
return {
|
|
1473
|
+
id: rule.id,
|
|
1474
|
+
name: rule.type,
|
|
1475
|
+
shortDescription: {
|
|
1476
|
+
text: `${rule.cwe.name} (CWE-${rule.cwe.id})`
|
|
1477
|
+
},
|
|
1478
|
+
fullDescription: {
|
|
1479
|
+
text: rule.description
|
|
1480
|
+
},
|
|
1481
|
+
helpUri: `https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html`,
|
|
1482
|
+
help: {
|
|
1483
|
+
text: `## ${rule.cwe.name}
|
|
1484
|
+
|
|
1485
|
+
CWE-${rule.cwe.id}: ${rule.cwe.name}
|
|
1486
|
+
|
|
1487
|
+
This rule detects ${rule.type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1085
1488
|
|
|
1086
1489
|
### Remediation
|
|
1087
1490
|
|
|
1088
|
-
See https://cwe.mitre.org/data/definitions/${cwe.id}.html for detailed remediation guidance.`,
|
|
1089
|
-
|
|
1491
|
+
See https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html for detailed remediation guidance.`,
|
|
1492
|
+
markdown: `## ${rule.cwe.name}
|
|
1090
1493
|
|
|
1091
|
-
**CWE-${cwe.id}**: ${cwe.name}
|
|
1494
|
+
**CWE-${rule.cwe.id}**: ${rule.cwe.name}
|
|
1092
1495
|
|
|
1093
|
-
This rule detects \`${type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1496
|
+
This rule detects \`${rule.type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1094
1497
|
|
|
1095
1498
|
### Remediation
|
|
1096
1499
|
|
|
1097
|
-
See [CWE-${cwe.id}](https://cwe.mitre.org/data/definitions/${cwe.id}.html) for detailed remediation guidance.`
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1500
|
+
See [CWE-${rule.cwe.id}](https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html) for detailed remediation guidance.`
|
|
1501
|
+
},
|
|
1502
|
+
properties: {
|
|
1503
|
+
tags: [
|
|
1504
|
+
"security",
|
|
1505
|
+
`CWE-${rule.cwe.id}`,
|
|
1506
|
+
`external/cwe/cwe-${rule.cwe.id}`
|
|
1507
|
+
],
|
|
1508
|
+
precision: toPrecision(rule.severity),
|
|
1509
|
+
"security-severity": rule.securitySeverity
|
|
1510
|
+
},
|
|
1511
|
+
defaultConfiguration: {
|
|
1512
|
+
level: toSarifLevel(rule.severity)
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1109
1515
|
}
|
|
1110
|
-
function toSarifResult(finding,
|
|
1111
|
-
const
|
|
1112
|
-
const ruleIndex = rules.findIndex((r) => r.id === ruleId);
|
|
1516
|
+
function toSarifResult(finding, sarifRules) {
|
|
1517
|
+
const ruleIndex = sarifRules.findIndex((r) => r.id === finding.ruleId);
|
|
1113
1518
|
let messageText = `${finding.title}
|
|
1114
1519
|
|
|
1115
1520
|
${finding.description}`;
|
|
@@ -1121,10 +1526,8 @@ Evidence: ${finding.evidence}`;
|
|
|
1121
1526
|
messageText += `
|
|
1122
1527
|
|
|
1123
1528
|
Payload: ${finding.payload}`;
|
|
1124
|
-
const uri = finding.url || "unknown";
|
|
1125
|
-
const fingerprint = `${finding.type}:${finding.stepId}:${finding.payload.slice(0, 50)}`;
|
|
1126
1529
|
return {
|
|
1127
|
-
ruleId,
|
|
1530
|
+
ruleId: finding.ruleId,
|
|
1128
1531
|
ruleIndex: Math.max(ruleIndex, 0),
|
|
1129
1532
|
level: toSarifLevel(finding.severity),
|
|
1130
1533
|
message: { text: messageText },
|
|
@@ -1132,7 +1535,7 @@ Payload: ${finding.payload}`;
|
|
|
1132
1535
|
{
|
|
1133
1536
|
physicalLocation: {
|
|
1134
1537
|
artifactLocation: {
|
|
1135
|
-
uri
|
|
1538
|
+
uri: finding.url || "unknown"
|
|
1136
1539
|
},
|
|
1137
1540
|
region: {
|
|
1138
1541
|
startLine: 1
|
|
@@ -1147,7 +1550,7 @@ Payload: ${finding.payload}`;
|
|
|
1147
1550
|
}
|
|
1148
1551
|
],
|
|
1149
1552
|
fingerprints: {
|
|
1150
|
-
vulcnFindingV1: fingerprint
|
|
1553
|
+
vulcnFindingV1: finding.fingerprint
|
|
1151
1554
|
},
|
|
1152
1555
|
partialFingerprints: {
|
|
1153
1556
|
vulcnType: finding.type,
|
|
@@ -1157,23 +1560,21 @@ Payload: ${finding.payload}`;
|
|
|
1157
1560
|
severity: finding.severity,
|
|
1158
1561
|
payload: finding.payload,
|
|
1159
1562
|
stepId: finding.stepId,
|
|
1563
|
+
detectionMethod: finding.detectionMethod,
|
|
1160
1564
|
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
1161
|
-
...finding.
|
|
1565
|
+
...finding.passiveCategory ? { passiveCategory: finding.passiveCategory } : {}
|
|
1162
1566
|
}
|
|
1163
1567
|
};
|
|
1164
1568
|
}
|
|
1165
|
-
function generateSarif(
|
|
1166
|
-
const
|
|
1167
|
-
const results =
|
|
1168
|
-
const
|
|
1169
|
-
...new Set(result.findings.map((f) => f.url).filter(Boolean))
|
|
1170
|
-
];
|
|
1171
|
-
const artifacts = uniqueUrls.map((url) => ({
|
|
1569
|
+
function generateSarif(report) {
|
|
1570
|
+
const sarifRules = report.rules.map(toSarifRule);
|
|
1571
|
+
const results = report.findings.map((f) => toSarifResult(f, sarifRules));
|
|
1572
|
+
const artifacts = report.summary.affectedUrls.map((url) => ({
|
|
1172
1573
|
location: { uri: url }
|
|
1173
1574
|
}));
|
|
1174
|
-
const startDate = new Date(generatedAt);
|
|
1175
|
-
const endDate = new Date(startDate.getTime() +
|
|
1176
|
-
|
|
1575
|
+
const startDate = new Date(report.generatedAt);
|
|
1576
|
+
const endDate = new Date(startDate.getTime() + report.stats.durationMs);
|
|
1577
|
+
return {
|
|
1177
1578
|
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1178
1579
|
version: "2.1.0",
|
|
1179
1580
|
runs: [
|
|
@@ -1181,24 +1582,24 @@ function generateSarif(session, result, generatedAt, engineVersion) {
|
|
|
1181
1582
|
tool: {
|
|
1182
1583
|
driver: {
|
|
1183
1584
|
name: "Vulcn",
|
|
1184
|
-
version: engineVersion,
|
|
1185
|
-
semanticVersion: engineVersion,
|
|
1585
|
+
version: report.engineVersion,
|
|
1586
|
+
semanticVersion: report.engineVersion,
|
|
1186
1587
|
informationUri: "https://vulcn.dev",
|
|
1187
|
-
rules
|
|
1588
|
+
rules: sarifRules
|
|
1188
1589
|
}
|
|
1189
1590
|
},
|
|
1190
1591
|
results,
|
|
1191
1592
|
invocations: [
|
|
1192
1593
|
{
|
|
1193
|
-
executionSuccessful:
|
|
1194
|
-
startTimeUtc: generatedAt,
|
|
1594
|
+
executionSuccessful: report.stats.errors.length === 0,
|
|
1595
|
+
startTimeUtc: report.generatedAt,
|
|
1195
1596
|
endTimeUtc: endDate.toISOString(),
|
|
1196
1597
|
properties: {
|
|
1197
|
-
sessionName: session.name,
|
|
1198
|
-
stepsExecuted:
|
|
1199
|
-
payloadsTested:
|
|
1200
|
-
durationMs:
|
|
1201
|
-
...
|
|
1598
|
+
sessionName: report.session.name,
|
|
1599
|
+
stepsExecuted: report.stats.stepsExecuted,
|
|
1600
|
+
payloadsTested: report.stats.payloadsTested,
|
|
1601
|
+
durationMs: report.stats.durationMs,
|
|
1602
|
+
...report.stats.errors.length > 0 ? { errors: report.stats.errors } : {}
|
|
1202
1603
|
}
|
|
1203
1604
|
}
|
|
1204
1605
|
],
|
|
@@ -1206,7 +1607,6 @@ function generateSarif(session, result, generatedAt, engineVersion) {
|
|
|
1206
1607
|
}
|
|
1207
1608
|
]
|
|
1208
1609
|
};
|
|
1209
|
-
return sarifLog;
|
|
1210
1610
|
}
|
|
1211
1611
|
|
|
1212
1612
|
// src/index.ts
|
|
@@ -1255,13 +1655,20 @@ var plugin = {
|
|
|
1255
1655
|
);
|
|
1256
1656
|
},
|
|
1257
1657
|
/**
|
|
1258
|
-
* Generate report(s) after run completes
|
|
1658
|
+
* Generate report(s) after run completes.
|
|
1659
|
+
*
|
|
1660
|
+
* Architecture: RunResult + Session → buildReport() → VulcnReport
|
|
1661
|
+
* Each output format is a pure projection of the canonical model.
|
|
1259
1662
|
*/
|
|
1260
1663
|
onRunEnd: async (result, ctx) => {
|
|
1261
1664
|
const config = configSchema.parse(ctx.config);
|
|
1262
1665
|
const formats = getFormats(config.format);
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1666
|
+
const report = buildReport(
|
|
1667
|
+
ctx.session,
|
|
1668
|
+
result,
|
|
1669
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1670
|
+
ctx.engine.version
|
|
1671
|
+
);
|
|
1265
1672
|
const outDir = (0, import_node_path.resolve)(config.outputDir);
|
|
1266
1673
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
1267
1674
|
const basePath = (0, import_node_path.resolve)(outDir, config.filename);
|
|
@@ -1270,13 +1677,7 @@ var plugin = {
|
|
|
1270
1677
|
try {
|
|
1271
1678
|
switch (fmt) {
|
|
1272
1679
|
case "html": {
|
|
1273
|
-
const
|
|
1274
|
-
session: ctx.session,
|
|
1275
|
-
result,
|
|
1276
|
-
generatedAt,
|
|
1277
|
-
engineVersion
|
|
1278
|
-
};
|
|
1279
|
-
const html = generateHtml(htmlData);
|
|
1680
|
+
const html = generateHtml(report);
|
|
1280
1681
|
const htmlPath = `${basePath}.html`;
|
|
1281
1682
|
await (0, import_promises.writeFile)(htmlPath, html, "utf-8");
|
|
1282
1683
|
writtenFiles.push(htmlPath);
|
|
@@ -1284,12 +1685,7 @@ var plugin = {
|
|
|
1284
1685
|
break;
|
|
1285
1686
|
}
|
|
1286
1687
|
case "json": {
|
|
1287
|
-
const jsonReport = generateJson(
|
|
1288
|
-
ctx.session,
|
|
1289
|
-
result,
|
|
1290
|
-
generatedAt,
|
|
1291
|
-
engineVersion
|
|
1292
|
-
);
|
|
1688
|
+
const jsonReport = generateJson(report);
|
|
1293
1689
|
const jsonPath = `${basePath}.json`;
|
|
1294
1690
|
await (0, import_promises.writeFile)(
|
|
1295
1691
|
jsonPath,
|
|
@@ -1301,12 +1697,7 @@ var plugin = {
|
|
|
1301
1697
|
break;
|
|
1302
1698
|
}
|
|
1303
1699
|
case "yaml": {
|
|
1304
|
-
const yamlContent = generateYaml(
|
|
1305
|
-
ctx.session,
|
|
1306
|
-
result,
|
|
1307
|
-
generatedAt,
|
|
1308
|
-
engineVersion
|
|
1309
|
-
);
|
|
1700
|
+
const yamlContent = generateYaml(report);
|
|
1310
1701
|
const yamlPath = `${basePath}.yml`;
|
|
1311
1702
|
await (0, import_promises.writeFile)(yamlPath, yamlContent, "utf-8");
|
|
1312
1703
|
writtenFiles.push(yamlPath);
|
|
@@ -1314,12 +1705,7 @@ var plugin = {
|
|
|
1314
1705
|
break;
|
|
1315
1706
|
}
|
|
1316
1707
|
case "sarif": {
|
|
1317
|
-
const sarifReport = generateSarif(
|
|
1318
|
-
ctx.session,
|
|
1319
|
-
result,
|
|
1320
|
-
generatedAt,
|
|
1321
|
-
engineVersion
|
|
1322
|
-
);
|
|
1708
|
+
const sarifReport = generateSarif(report);
|
|
1323
1709
|
const sarifPath = `${basePath}.sarif`;
|
|
1324
1710
|
await (0, import_promises.writeFile)(
|
|
1325
1711
|
sarifPath,
|
|
@@ -1353,6 +1739,7 @@ var plugin = {
|
|
|
1353
1739
|
var index_default = plugin;
|
|
1354
1740
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1355
1741
|
0 && (module.exports = {
|
|
1742
|
+
buildReport,
|
|
1356
1743
|
configSchema,
|
|
1357
1744
|
generateHtml,
|
|
1358
1745
|
generateJson,
|