@vulcn/plugin-report 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +779 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -32
- package/dist/index.d.ts +280 -32
- package/dist/index.js +777 -130
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,12 @@ 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,
|
|
36
37
|
generateJson: () => generateJson,
|
|
38
|
+
generateSarif: () => generateSarif,
|
|
37
39
|
generateYaml: () => generateYaml
|
|
38
40
|
});
|
|
39
41
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -41,6 +43,262 @@ var import_zod = require("zod");
|
|
|
41
43
|
var import_promises = require("fs/promises");
|
|
42
44
|
var import_node_path = require("path");
|
|
43
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
|
+
|
|
44
302
|
// src/html.ts
|
|
45
303
|
var COLORS = {
|
|
46
304
|
bg: "#0a0a0f",
|
|
@@ -77,30 +335,9 @@ function severityColor(severity) {
|
|
|
77
335
|
return COLORS.textMuted;
|
|
78
336
|
}
|
|
79
337
|
}
|
|
80
|
-
function severityOrder(severity) {
|
|
81
|
-
switch (severity) {
|
|
82
|
-
case "critical":
|
|
83
|
-
return 0;
|
|
84
|
-
case "high":
|
|
85
|
-
return 1;
|
|
86
|
-
case "medium":
|
|
87
|
-
return 2;
|
|
88
|
-
case "low":
|
|
89
|
-
return 3;
|
|
90
|
-
case "info":
|
|
91
|
-
return 4;
|
|
92
|
-
default:
|
|
93
|
-
return 5;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
338
|
function escapeHtml(str) {
|
|
97
339
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
98
340
|
}
|
|
99
|
-
function formatDuration(ms) {
|
|
100
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
101
|
-
const seconds = (ms / 1e3).toFixed(1);
|
|
102
|
-
return `${seconds}s`;
|
|
103
|
-
}
|
|
104
341
|
function formatDate(iso) {
|
|
105
342
|
const d = new Date(iso);
|
|
106
343
|
return d.toLocaleDateString("en-US", {
|
|
@@ -127,31 +364,95 @@ var VULCN_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20
|
|
|
127
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"/>
|
|
128
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"/>
|
|
129
366
|
</svg>`;
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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>`;
|
|
144
381
|
}
|
|
145
|
-
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;
|
|
146
453
|
const hasFindings = totalFindings > 0;
|
|
147
|
-
const
|
|
148
|
-
const maxRisk = totalFindings * 10 || 1;
|
|
149
|
-
const riskPercent = Math.min(100, Math.round(riskScore / maxRisk * 100));
|
|
150
|
-
const riskLabel = riskPercent >= 80 ? "Critical" : riskPercent >= 50 ? "High" : riskPercent >= 25 ? "Medium" : riskPercent > 0 ? "Low" : "Clear";
|
|
151
|
-
const riskColor = riskPercent >= 80 ? COLORS.critical : riskPercent >= 50 ? COLORS.high : riskPercent >= 25 ? COLORS.medium : riskPercent > 0 ? COLORS.low : COLORS.success;
|
|
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;
|
|
152
455
|
const donutSvg = generateDonut(counts, totalFindings);
|
|
153
|
-
const affectedUrls = [...new Set(findings.map((f) => f.url))];
|
|
154
|
-
const vulnTypes = [...new Set(findings.map((f) => f.type))];
|
|
155
456
|
return `<!DOCTYPE html>
|
|
156
457
|
<html lang="en">
|
|
157
458
|
<head>
|
|
@@ -643,6 +944,180 @@ function generateHtml(data) {
|
|
|
643
944
|
.no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }
|
|
644
945
|
.no-findings p { font-size: 14px; color: var(--text-muted); }
|
|
645
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
|
+
|
|
646
1121
|
/* Errors section */
|
|
647
1122
|
.errors-section {
|
|
648
1123
|
margin-bottom: 32px;
|
|
@@ -707,6 +1182,7 @@ function generateHtml(data) {
|
|
|
707
1182
|
body::before { display: none; }
|
|
708
1183
|
.finding-details { display: block !important; padding-top: 12px !important; }
|
|
709
1184
|
.finding-card { page-break-inside: avoid; }
|
|
1185
|
+
.passive-category-card { page-break-inside: avoid; }
|
|
710
1186
|
}
|
|
711
1187
|
</style>
|
|
712
1188
|
</head>
|
|
@@ -722,8 +1198,8 @@ function generateHtml(data) {
|
|
|
722
1198
|
</div>
|
|
723
1199
|
</div>
|
|
724
1200
|
<div class="header-meta">
|
|
725
|
-
<div>${formatDate(generatedAt)}</div>
|
|
726
|
-
<div>Engine v${escapeHtml(engineVersion)}</div>
|
|
1201
|
+
<div>${formatDate(report.generatedAt)}</div>
|
|
1202
|
+
<div>Engine v${escapeHtml(report.engineVersion)}</div>
|
|
727
1203
|
</div>
|
|
728
1204
|
</div>
|
|
729
1205
|
|
|
@@ -738,11 +1214,11 @@ function generateHtml(data) {
|
|
|
738
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>` : ""}
|
|
739
1215
|
<div class="meta-item">
|
|
740
1216
|
<span class="meta-label">Duration</span>
|
|
741
|
-
<span class="meta-value">${formatDuration(
|
|
1217
|
+
<span class="meta-value">${formatDuration(durationMs)}</span>
|
|
742
1218
|
</div>
|
|
743
1219
|
<div class="meta-item">
|
|
744
1220
|
<span class="meta-label">Generated</span>
|
|
745
|
-
<span class="meta-value">${formatDate(generatedAt)}</span>
|
|
1221
|
+
<span class="meta-value">${formatDate(report.generatedAt)}</span>
|
|
746
1222
|
</div>
|
|
747
1223
|
</div>
|
|
748
1224
|
</div>
|
|
@@ -755,13 +1231,13 @@ function generateHtml(data) {
|
|
|
755
1231
|
<svg viewBox="0 0 160 160" width="160" height="160">
|
|
756
1232
|
<circle cx="80" cy="80" r="68" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="10"/>
|
|
757
1233
|
<circle cx="80" cy="80" r="68" fill="none" stroke="${riskColor}" stroke-width="10"
|
|
758
|
-
stroke-dasharray="${
|
|
1234
|
+
stroke-dasharray="${risk.percent / 100 * 427} 427"
|
|
759
1235
|
stroke-linecap="round"
|
|
760
1236
|
style="filter: drop-shadow(0 0 6px ${riskColor});"/>
|
|
761
1237
|
</svg>
|
|
762
1238
|
<div class="risk-gauge-label">
|
|
763
|
-
<div class="score" style="color: ${riskColor}">${hasFindings ?
|
|
764
|
-
<div class="label">${
|
|
1239
|
+
<div class="score" style="color: ${riskColor}">${hasFindings ? risk.percent : 0}</div>
|
|
1240
|
+
<div class="label">${risk.label}</div>
|
|
765
1241
|
</div>
|
|
766
1242
|
</div>
|
|
767
1243
|
</div>
|
|
@@ -774,11 +1250,11 @@ function generateHtml(data) {
|
|
|
774
1250
|
<div class="stat-label">Findings</div>
|
|
775
1251
|
</div>
|
|
776
1252
|
<div class="stat-box">
|
|
777
|
-
<div class="stat-number">${
|
|
1253
|
+
<div class="stat-number">${payloadsTested}</div>
|
|
778
1254
|
<div class="stat-label">Payloads Tested</div>
|
|
779
1255
|
</div>
|
|
780
1256
|
<div class="stat-box">
|
|
781
|
-
<div class="stat-number">${
|
|
1257
|
+
<div class="stat-number">${stepsExecuted}</div>
|
|
782
1258
|
<div class="stat-label">Steps Executed</div>
|
|
783
1259
|
</div>
|
|
784
1260
|
<div class="stat-box">
|
|
@@ -806,14 +1282,17 @@ function generateHtml(data) {
|
|
|
806
1282
|
</div>
|
|
807
1283
|
</div>
|
|
808
1284
|
|
|
809
|
-
<!--
|
|
1285
|
+
<!-- Passive Security Analysis -->
|
|
1286
|
+
${renderPassiveSection(passiveAnalysis)}
|
|
1287
|
+
|
|
1288
|
+
<!-- Active Findings -->
|
|
810
1289
|
<div class="findings-section animate-in-delay-3">
|
|
811
1290
|
<div class="findings-header">
|
|
812
|
-
<h3
|
|
813
|
-
<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>
|
|
814
1293
|
</div>
|
|
815
1294
|
|
|
816
|
-
${
|
|
1295
|
+
${activeFindings.length > 0 ? activeFindings.map(
|
|
817
1296
|
(f, i) => `
|
|
818
1297
|
<div class="finding-card" onclick="this.classList.toggle('open')">
|
|
819
1298
|
<div class="finding-header">
|
|
@@ -859,16 +1338,16 @@ function generateHtml(data) {
|
|
|
859
1338
|
).join("") : `
|
|
860
1339
|
<div class="no-findings">
|
|
861
1340
|
<div class="icon">\u{1F6E1}\uFE0F</div>
|
|
862
|
-
<h3>No Vulnerabilities Detected</h3>
|
|
863
|
-
<p>${
|
|
1341
|
+
<h3>No Active Vulnerabilities Detected</h3>
|
|
1342
|
+
<p>${payloadsTested} payloads were tested across ${stepsExecuted} steps with no findings.</p>
|
|
864
1343
|
</div>
|
|
865
1344
|
`}
|
|
866
1345
|
</div>
|
|
867
1346
|
|
|
868
|
-
${
|
|
1347
|
+
${errors.length > 0 ? `
|
|
869
1348
|
<div class="errors-section">
|
|
870
|
-
<h3>\u26A0\uFE0F Errors During Execution (${
|
|
871
|
-
${
|
|
1349
|
+
<h3>\u26A0\uFE0F Errors During Execution (${errors.length})</h3>
|
|
1350
|
+
${errors.map((e) => `<div class="error-item">${escapeHtml(e)}</div>`).join("")}
|
|
872
1351
|
</div>
|
|
873
1352
|
` : ""}
|
|
874
1353
|
|
|
@@ -903,67 +1382,231 @@ function generateDonut(counts, total) {
|
|
|
903
1382
|
}
|
|
904
1383
|
|
|
905
1384
|
// src/json.ts
|
|
906
|
-
function
|
|
907
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
908
|
-
return `${(ms / 1e3).toFixed(1)}s`;
|
|
909
|
-
}
|
|
910
|
-
function generateJson(session, result, generatedAt, engineVersion) {
|
|
911
|
-
const counts = {
|
|
912
|
-
critical: 0,
|
|
913
|
-
high: 0,
|
|
914
|
-
medium: 0,
|
|
915
|
-
low: 0,
|
|
916
|
-
info: 0
|
|
917
|
-
};
|
|
918
|
-
for (const f of result.findings) {
|
|
919
|
-
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
920
|
-
}
|
|
921
|
-
const riskScore = counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;
|
|
1385
|
+
function generateJson(report) {
|
|
922
1386
|
return {
|
|
923
1387
|
vulcn: {
|
|
924
|
-
version: engineVersion,
|
|
925
|
-
reportVersion:
|
|
926
|
-
generatedAt
|
|
927
|
-
},
|
|
928
|
-
session: {
|
|
929
|
-
name: session.name,
|
|
930
|
-
driver: session.driver,
|
|
931
|
-
driverConfig: session.driverConfig,
|
|
932
|
-
stepsCount: session.steps.length,
|
|
933
|
-
metadata: session.metadata
|
|
1388
|
+
version: report.engineVersion,
|
|
1389
|
+
reportVersion: report.reportVersion,
|
|
1390
|
+
generatedAt: report.generatedAt
|
|
934
1391
|
},
|
|
1392
|
+
session: report.session,
|
|
935
1393
|
execution: {
|
|
936
|
-
stepsExecuted:
|
|
937
|
-
payloadsTested:
|
|
938
|
-
durationMs:
|
|
939
|
-
durationFormatted:
|
|
940
|
-
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
|
|
941
1399
|
},
|
|
942
1400
|
summary: {
|
|
943
|
-
totalFindings:
|
|
944
|
-
riskScore,
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
|
948
1407
|
},
|
|
949
|
-
findings:
|
|
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
|
+
}))
|
|
1420
|
+
},
|
|
1421
|
+
rules: report.rules
|
|
950
1422
|
};
|
|
951
1423
|
}
|
|
952
1424
|
|
|
953
1425
|
// src/yaml.ts
|
|
954
1426
|
var import_yaml = require("yaml");
|
|
955
|
-
function generateYaml(
|
|
956
|
-
const
|
|
1427
|
+
function generateYaml(report) {
|
|
1428
|
+
const jsonReport = generateJson(report);
|
|
957
1429
|
const header = [
|
|
958
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",
|
|
959
1431
|
"# Vulcn Security Report",
|
|
960
|
-
`# Generated: ${generatedAt}`,
|
|
961
|
-
`# Session: ${session.name}`,
|
|
962
|
-
`# Findings: ${
|
|
1432
|
+
`# Generated: ${report.generatedAt}`,
|
|
1433
|
+
`# Session: ${report.session.name}`,
|
|
1434
|
+
`# Findings: ${report.summary.totalFindings}`,
|
|
963
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",
|
|
964
1436
|
""
|
|
965
1437
|
].join("\n");
|
|
966
|
-
return header + (0, import_yaml.stringify)(
|
|
1438
|
+
return header + (0, import_yaml.stringify)(jsonReport, { indent: 2 });
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/sarif.ts
|
|
1442
|
+
function toSarifLevel(severity) {
|
|
1443
|
+
switch (severity) {
|
|
1444
|
+
case "critical":
|
|
1445
|
+
case "high":
|
|
1446
|
+
return "error";
|
|
1447
|
+
case "medium":
|
|
1448
|
+
return "warning";
|
|
1449
|
+
case "low":
|
|
1450
|
+
case "info":
|
|
1451
|
+
return "note";
|
|
1452
|
+
default:
|
|
1453
|
+
return "warning";
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function toPrecision(severity) {
|
|
1457
|
+
switch (severity) {
|
|
1458
|
+
case "critical":
|
|
1459
|
+
return "very-high";
|
|
1460
|
+
case "high":
|
|
1461
|
+
return "high";
|
|
1462
|
+
case "medium":
|
|
1463
|
+
return "medium";
|
|
1464
|
+
case "low":
|
|
1465
|
+
case "info":
|
|
1466
|
+
return "low";
|
|
1467
|
+
default:
|
|
1468
|
+
return "medium";
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
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.
|
|
1488
|
+
|
|
1489
|
+
### Remediation
|
|
1490
|
+
|
|
1491
|
+
See https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html for detailed remediation guidance.`,
|
|
1492
|
+
markdown: `## ${rule.cwe.name}
|
|
1493
|
+
|
|
1494
|
+
**CWE-${rule.cwe.id}**: ${rule.cwe.name}
|
|
1495
|
+
|
|
1496
|
+
This rule detects \`${rule.type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1497
|
+
|
|
1498
|
+
### Remediation
|
|
1499
|
+
|
|
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
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function toSarifResult(finding, sarifRules) {
|
|
1517
|
+
const ruleIndex = sarifRules.findIndex((r) => r.id === finding.ruleId);
|
|
1518
|
+
let messageText = `${finding.title}
|
|
1519
|
+
|
|
1520
|
+
${finding.description}`;
|
|
1521
|
+
if (finding.evidence) {
|
|
1522
|
+
messageText += `
|
|
1523
|
+
|
|
1524
|
+
Evidence: ${finding.evidence}`;
|
|
1525
|
+
}
|
|
1526
|
+
messageText += `
|
|
1527
|
+
|
|
1528
|
+
Payload: ${finding.payload}`;
|
|
1529
|
+
return {
|
|
1530
|
+
ruleId: finding.ruleId,
|
|
1531
|
+
ruleIndex: Math.max(ruleIndex, 0),
|
|
1532
|
+
level: toSarifLevel(finding.severity),
|
|
1533
|
+
message: { text: messageText },
|
|
1534
|
+
locations: [
|
|
1535
|
+
{
|
|
1536
|
+
physicalLocation: {
|
|
1537
|
+
artifactLocation: {
|
|
1538
|
+
uri: finding.url || "unknown"
|
|
1539
|
+
},
|
|
1540
|
+
region: {
|
|
1541
|
+
startLine: 1
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
logicalLocations: [
|
|
1545
|
+
{
|
|
1546
|
+
name: finding.stepId,
|
|
1547
|
+
kind: "test-step"
|
|
1548
|
+
}
|
|
1549
|
+
]
|
|
1550
|
+
}
|
|
1551
|
+
],
|
|
1552
|
+
fingerprints: {
|
|
1553
|
+
vulcnFindingV1: finding.fingerprint
|
|
1554
|
+
},
|
|
1555
|
+
partialFingerprints: {
|
|
1556
|
+
vulcnType: finding.type,
|
|
1557
|
+
vulcnStepId: finding.stepId
|
|
1558
|
+
},
|
|
1559
|
+
properties: {
|
|
1560
|
+
severity: finding.severity,
|
|
1561
|
+
payload: finding.payload,
|
|
1562
|
+
stepId: finding.stepId,
|
|
1563
|
+
detectionMethod: finding.detectionMethod,
|
|
1564
|
+
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
1565
|
+
...finding.passiveCategory ? { passiveCategory: finding.passiveCategory } : {}
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
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) => ({
|
|
1573
|
+
location: { uri: url }
|
|
1574
|
+
}));
|
|
1575
|
+
const startDate = new Date(report.generatedAt);
|
|
1576
|
+
const endDate = new Date(startDate.getTime() + report.stats.durationMs);
|
|
1577
|
+
return {
|
|
1578
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1579
|
+
version: "2.1.0",
|
|
1580
|
+
runs: [
|
|
1581
|
+
{
|
|
1582
|
+
tool: {
|
|
1583
|
+
driver: {
|
|
1584
|
+
name: "Vulcn",
|
|
1585
|
+
version: report.engineVersion,
|
|
1586
|
+
semanticVersion: report.engineVersion,
|
|
1587
|
+
informationUri: "https://vulcn.dev",
|
|
1588
|
+
rules: sarifRules
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
results,
|
|
1592
|
+
invocations: [
|
|
1593
|
+
{
|
|
1594
|
+
executionSuccessful: report.stats.errors.length === 0,
|
|
1595
|
+
startTimeUtc: report.generatedAt,
|
|
1596
|
+
endTimeUtc: endDate.toISOString(),
|
|
1597
|
+
properties: {
|
|
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 } : {}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
],
|
|
1606
|
+
...artifacts.length > 0 ? { artifacts } : {}
|
|
1607
|
+
}
|
|
1608
|
+
]
|
|
1609
|
+
};
|
|
967
1610
|
}
|
|
968
1611
|
|
|
969
1612
|
// src/index.ts
|
|
@@ -973,10 +1616,11 @@ var configSchema = import_zod.z.object({
|
|
|
973
1616
|
* - "html": Beautiful dark-themed HTML report
|
|
974
1617
|
* - "json": Machine-readable structured JSON
|
|
975
1618
|
* - "yaml": Human-readable YAML
|
|
976
|
-
* - "
|
|
1619
|
+
* - "sarif": SARIF v2.1.0 for GitHub Code Scanning
|
|
1620
|
+
* - "all": Generate all formats
|
|
977
1621
|
* @default "html"
|
|
978
1622
|
*/
|
|
979
|
-
format: import_zod.z.enum(["html", "json", "yaml", "all"]).default("html"),
|
|
1623
|
+
format: import_zod.z.enum(["html", "json", "yaml", "sarif", "all"]).default("html"),
|
|
980
1624
|
/**
|
|
981
1625
|
* Output directory for report files
|
|
982
1626
|
* @default "."
|
|
@@ -994,14 +1638,14 @@ var configSchema = import_zod.z.object({
|
|
|
994
1638
|
open: import_zod.z.boolean().default(false)
|
|
995
1639
|
});
|
|
996
1640
|
function getFormats(format) {
|
|
997
|
-
if (format === "all") return ["html", "json", "yaml"];
|
|
1641
|
+
if (format === "all") return ["html", "json", "yaml", "sarif"];
|
|
998
1642
|
return [format];
|
|
999
1643
|
}
|
|
1000
1644
|
var plugin = {
|
|
1001
1645
|
name: "@vulcn/plugin-report",
|
|
1002
1646
|
version: "0.1.0",
|
|
1003
1647
|
apiVersion: 1,
|
|
1004
|
-
description: "Report generation plugin \u2014 generates
|
|
1648
|
+
description: "Report generation plugin \u2014 generates HTML, JSON, YAML, and SARIF security reports",
|
|
1005
1649
|
configSchema,
|
|
1006
1650
|
hooks: {
|
|
1007
1651
|
onInit: async (ctx) => {
|
|
@@ -1011,13 +1655,20 @@ var plugin = {
|
|
|
1011
1655
|
);
|
|
1012
1656
|
},
|
|
1013
1657
|
/**
|
|
1014
|
-
* 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.
|
|
1015
1662
|
*/
|
|
1016
1663
|
onRunEnd: async (result, ctx) => {
|
|
1017
1664
|
const config = configSchema.parse(ctx.config);
|
|
1018
1665
|
const formats = getFormats(config.format);
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1666
|
+
const report = buildReport(
|
|
1667
|
+
ctx.session,
|
|
1668
|
+
result,
|
|
1669
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1670
|
+
ctx.engine.version
|
|
1671
|
+
);
|
|
1021
1672
|
const outDir = (0, import_node_path.resolve)(config.outputDir);
|
|
1022
1673
|
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
1023
1674
|
const basePath = (0, import_node_path.resolve)(outDir, config.filename);
|
|
@@ -1026,13 +1677,7 @@ var plugin = {
|
|
|
1026
1677
|
try {
|
|
1027
1678
|
switch (fmt) {
|
|
1028
1679
|
case "html": {
|
|
1029
|
-
const
|
|
1030
|
-
session: ctx.session,
|
|
1031
|
-
result,
|
|
1032
|
-
generatedAt,
|
|
1033
|
-
engineVersion
|
|
1034
|
-
};
|
|
1035
|
-
const html = generateHtml(htmlData);
|
|
1680
|
+
const html = generateHtml(report);
|
|
1036
1681
|
const htmlPath = `${basePath}.html`;
|
|
1037
1682
|
await (0, import_promises.writeFile)(htmlPath, html, "utf-8");
|
|
1038
1683
|
writtenFiles.push(htmlPath);
|
|
@@ -1040,12 +1685,7 @@ var plugin = {
|
|
|
1040
1685
|
break;
|
|
1041
1686
|
}
|
|
1042
1687
|
case "json": {
|
|
1043
|
-
const jsonReport = generateJson(
|
|
1044
|
-
ctx.session,
|
|
1045
|
-
result,
|
|
1046
|
-
generatedAt,
|
|
1047
|
-
engineVersion
|
|
1048
|
-
);
|
|
1688
|
+
const jsonReport = generateJson(report);
|
|
1049
1689
|
const jsonPath = `${basePath}.json`;
|
|
1050
1690
|
await (0, import_promises.writeFile)(
|
|
1051
1691
|
jsonPath,
|
|
@@ -1057,18 +1697,25 @@ var plugin = {
|
|
|
1057
1697
|
break;
|
|
1058
1698
|
}
|
|
1059
1699
|
case "yaml": {
|
|
1060
|
-
const yamlContent = generateYaml(
|
|
1061
|
-
ctx.session,
|
|
1062
|
-
result,
|
|
1063
|
-
generatedAt,
|
|
1064
|
-
engineVersion
|
|
1065
|
-
);
|
|
1700
|
+
const yamlContent = generateYaml(report);
|
|
1066
1701
|
const yamlPath = `${basePath}.yml`;
|
|
1067
1702
|
await (0, import_promises.writeFile)(yamlPath, yamlContent, "utf-8");
|
|
1068
1703
|
writtenFiles.push(yamlPath);
|
|
1069
1704
|
ctx.logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
|
|
1070
1705
|
break;
|
|
1071
1706
|
}
|
|
1707
|
+
case "sarif": {
|
|
1708
|
+
const sarifReport = generateSarif(report);
|
|
1709
|
+
const sarifPath = `${basePath}.sarif`;
|
|
1710
|
+
await (0, import_promises.writeFile)(
|
|
1711
|
+
sarifPath,
|
|
1712
|
+
JSON.stringify(sarifReport, null, 2),
|
|
1713
|
+
"utf-8"
|
|
1714
|
+
);
|
|
1715
|
+
writtenFiles.push(sarifPath);
|
|
1716
|
+
ctx.logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1072
1719
|
}
|
|
1073
1720
|
} catch (err) {
|
|
1074
1721
|
ctx.logger.error(
|
|
@@ -1092,9 +1739,11 @@ var plugin = {
|
|
|
1092
1739
|
var index_default = plugin;
|
|
1093
1740
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1094
1741
|
0 && (module.exports = {
|
|
1742
|
+
buildReport,
|
|
1095
1743
|
configSchema,
|
|
1096
1744
|
generateHtml,
|
|
1097
1745
|
generateJson,
|
|
1746
|
+
generateSarif,
|
|
1098
1747
|
generateYaml
|
|
1099
1748
|
});
|
|
1100
1749
|
//# sourceMappingURL=index.cjs.map
|