@vulcn/plugin-report 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +650 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -46
- package/dist/index.d.ts +174 -46
- package/dist/index.js +649 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,262 @@ import { z } from "zod";
|
|
|
3
3
|
import { writeFile, mkdir } from "fs/promises";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
|
|
6
|
+
// src/report-model.ts
|
|
7
|
+
var CWE_MAP = {
|
|
8
|
+
xss: {
|
|
9
|
+
id: 79,
|
|
10
|
+
name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
|
|
11
|
+
},
|
|
12
|
+
sqli: {
|
|
13
|
+
id: 89,
|
|
14
|
+
name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
|
|
15
|
+
},
|
|
16
|
+
ssrf: { id: 918, name: "Server-Side Request Forgery (SSRF)" },
|
|
17
|
+
xxe: {
|
|
18
|
+
id: 611,
|
|
19
|
+
name: "Improper Restriction of XML External Entity Reference"
|
|
20
|
+
},
|
|
21
|
+
"command-injection": {
|
|
22
|
+
id: 78,
|
|
23
|
+
name: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
|
|
24
|
+
},
|
|
25
|
+
"path-traversal": {
|
|
26
|
+
id: 22,
|
|
27
|
+
name: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
|
|
28
|
+
},
|
|
29
|
+
"open-redirect": {
|
|
30
|
+
id: 601,
|
|
31
|
+
name: "URL Redirection to Untrusted Site ('Open Redirect')"
|
|
32
|
+
},
|
|
33
|
+
reflection: {
|
|
34
|
+
id: 200,
|
|
35
|
+
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
36
|
+
},
|
|
37
|
+
"security-misconfiguration": {
|
|
38
|
+
id: 16,
|
|
39
|
+
name: "Configuration"
|
|
40
|
+
},
|
|
41
|
+
"information-disclosure": {
|
|
42
|
+
id: 200,
|
|
43
|
+
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
44
|
+
},
|
|
45
|
+
custom: { id: 20, name: "Improper Input Validation" }
|
|
46
|
+
};
|
|
47
|
+
var SEVERITY_WEIGHTS = {
|
|
48
|
+
critical: 10,
|
|
49
|
+
high: 7,
|
|
50
|
+
medium: 4,
|
|
51
|
+
low: 1,
|
|
52
|
+
info: 0
|
|
53
|
+
};
|
|
54
|
+
var SECURITY_SEVERITY = {
|
|
55
|
+
critical: "9.0",
|
|
56
|
+
high: "7.0",
|
|
57
|
+
medium: "4.0",
|
|
58
|
+
low: "2.0",
|
|
59
|
+
info: "0.0"
|
|
60
|
+
};
|
|
61
|
+
var PASSIVE_CATEGORIES = [
|
|
62
|
+
{
|
|
63
|
+
id: "security-headers",
|
|
64
|
+
label: "Security Headers",
|
|
65
|
+
icon: "\u{1F512}",
|
|
66
|
+
color: "#42a5f5",
|
|
67
|
+
remedy: "Add the recommended security headers to your server configuration. Most web servers and frameworks support these via middleware.",
|
|
68
|
+
checks: [
|
|
69
|
+
"Strict-Transport-Security (HSTS)",
|
|
70
|
+
"Content-Security-Policy (CSP)",
|
|
71
|
+
"X-Content-Type-Options",
|
|
72
|
+
"X-Frame-Options",
|
|
73
|
+
"Referrer-Policy",
|
|
74
|
+
"Permissions-Policy"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "cookie-security",
|
|
79
|
+
label: "Cookie Security",
|
|
80
|
+
icon: "\u{1F36A}",
|
|
81
|
+
color: "#ffab40",
|
|
82
|
+
remedy: "Set the Secure, HttpOnly, and SameSite attributes on all session cookies. Configure your framework's session middleware accordingly.",
|
|
83
|
+
checks: ["Secure flag", "HttpOnly flag", "SameSite attribute"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "information-disclosure",
|
|
87
|
+
label: "Information Disclosure",
|
|
88
|
+
icon: "\u{1F50D}",
|
|
89
|
+
color: "#66bb6a",
|
|
90
|
+
remedy: "Remove or obfuscate server version headers (Server, X-Powered-By). Disable debug mode in production environments.",
|
|
91
|
+
checks: ["Server version", "X-Powered-By", "Debug tokens"]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "cors",
|
|
95
|
+
label: "CORS Configuration",
|
|
96
|
+
icon: "\u{1F310}",
|
|
97
|
+
color: "#ce93d8",
|
|
98
|
+
remedy: "Replace wildcard origins with specific trusted domains. Never combine Access-Control-Allow-Credentials with wildcard origins.",
|
|
99
|
+
checks: ["Wildcard origin", "Credentials with wildcard"]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "mixed-content",
|
|
103
|
+
label: "Mixed Content",
|
|
104
|
+
icon: "\u26A0\uFE0F",
|
|
105
|
+
color: "#ff8a65",
|
|
106
|
+
remedy: "Replace all HTTP resource URLs with HTTPS. Use Content-Security-Policy: upgrade-insecure-requests as a fallback.",
|
|
107
|
+
checks: ["HTTP resources on HTTPS"]
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
function toRuleId(type) {
|
|
111
|
+
return `VULCN-${type.toUpperCase().replace(/[^A-Z0-9]+/g, "-")}`;
|
|
112
|
+
}
|
|
113
|
+
function fingerprint(f) {
|
|
114
|
+
return `${f.type}:${f.stepId}:${f.payload.slice(0, 50)}`;
|
|
115
|
+
}
|
|
116
|
+
function detectMethod(f) {
|
|
117
|
+
return f.metadata?.detectionMethod === "passive" ? "passive" : "active";
|
|
118
|
+
}
|
|
119
|
+
function passiveCat(f) {
|
|
120
|
+
const method = detectMethod(f);
|
|
121
|
+
if (method !== "passive") return void 0;
|
|
122
|
+
return f.metadata?.category || "other";
|
|
123
|
+
}
|
|
124
|
+
function enrichFinding(f) {
|
|
125
|
+
const cwe = CWE_MAP[f.type] || CWE_MAP.custom;
|
|
126
|
+
const sev = f.severity;
|
|
127
|
+
return {
|
|
128
|
+
// Original fields
|
|
129
|
+
type: f.type,
|
|
130
|
+
severity: sev,
|
|
131
|
+
title: f.title,
|
|
132
|
+
description: f.description,
|
|
133
|
+
stepId: f.stepId,
|
|
134
|
+
payload: f.payload,
|
|
135
|
+
url: f.url,
|
|
136
|
+
evidence: f.evidence,
|
|
137
|
+
metadata: f.metadata,
|
|
138
|
+
// Enriched
|
|
139
|
+
ruleId: toRuleId(f.type),
|
|
140
|
+
cwe,
|
|
141
|
+
securitySeverity: SECURITY_SEVERITY[sev] || "4.0",
|
|
142
|
+
fingerprint: fingerprint(f),
|
|
143
|
+
detectionMethod: detectMethod(f),
|
|
144
|
+
passiveCategory: passiveCat(f)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function buildRules(enriched) {
|
|
148
|
+
const seen = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const f of enriched) {
|
|
150
|
+
if (!seen.has(f.type)) seen.set(f.type, f);
|
|
151
|
+
}
|
|
152
|
+
return Array.from(seen.entries()).map(([type, sample]) => ({
|
|
153
|
+
id: toRuleId(type),
|
|
154
|
+
type,
|
|
155
|
+
cwe: sample.cwe,
|
|
156
|
+
severity: sample.severity,
|
|
157
|
+
securitySeverity: sample.securitySeverity,
|
|
158
|
+
description: `Vulcn detected a potential ${type} vulnerability. ${sample.cwe.name}.`
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function countSeverities(findings) {
|
|
162
|
+
const counts = {
|
|
163
|
+
critical: 0,
|
|
164
|
+
high: 0,
|
|
165
|
+
medium: 0,
|
|
166
|
+
low: 0,
|
|
167
|
+
info: 0
|
|
168
|
+
};
|
|
169
|
+
for (const f of findings) {
|
|
170
|
+
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
171
|
+
}
|
|
172
|
+
return counts;
|
|
173
|
+
}
|
|
174
|
+
function assessRisk(counts, total) {
|
|
175
|
+
const score = counts.critical * SEVERITY_WEIGHTS.critical + counts.high * SEVERITY_WEIGHTS.high + counts.medium * SEVERITY_WEIGHTS.medium + counts.low * SEVERITY_WEIGHTS.low;
|
|
176
|
+
const maxRisk = total * SEVERITY_WEIGHTS.critical || 1;
|
|
177
|
+
const percent = Math.min(100, Math.round(score / maxRisk * 100));
|
|
178
|
+
const label = percent >= 80 ? "Critical" : percent >= 50 ? "High" : percent >= 25 ? "Medium" : percent > 0 ? "Low" : "Clear";
|
|
179
|
+
return { score, percent, label };
|
|
180
|
+
}
|
|
181
|
+
function buildPassiveAnalysis(passiveFindings) {
|
|
182
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const f of passiveFindings) {
|
|
184
|
+
const cat = f.passiveCategory || "other";
|
|
185
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
186
|
+
grouped.get(cat).push(f);
|
|
187
|
+
}
|
|
188
|
+
const categories = PASSIVE_CATEGORIES.map((def) => {
|
|
189
|
+
const findings = grouped.get(def.id) || [];
|
|
190
|
+
const issueCount = findings.length;
|
|
191
|
+
const totalChecks = def.checks.length;
|
|
192
|
+
const passedChecks = Math.max(0, totalChecks - issueCount);
|
|
193
|
+
const status = issueCount === 0 ? "pass" : issueCount >= 3 ? "fail" : "warn";
|
|
194
|
+
return {
|
|
195
|
+
definition: def,
|
|
196
|
+
findings,
|
|
197
|
+
issueCount,
|
|
198
|
+
passedChecks,
|
|
199
|
+
totalChecks,
|
|
200
|
+
status
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
totalIssues: passiveFindings.length,
|
|
205
|
+
categories
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildReport(session, result, generatedAt, engineVersion) {
|
|
209
|
+
const severityOrder = {
|
|
210
|
+
critical: 0,
|
|
211
|
+
high: 1,
|
|
212
|
+
medium: 2,
|
|
213
|
+
low: 3,
|
|
214
|
+
info: 4
|
|
215
|
+
};
|
|
216
|
+
const sortedFindings = [...result.findings].sort(
|
|
217
|
+
(a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5)
|
|
218
|
+
);
|
|
219
|
+
const findings = sortedFindings.map(enrichFinding);
|
|
220
|
+
const activeFindings = findings.filter((f) => f.detectionMethod === "active");
|
|
221
|
+
const passiveFindings = findings.filter(
|
|
222
|
+
(f) => f.detectionMethod === "passive"
|
|
223
|
+
);
|
|
224
|
+
const counts = countSeverities(findings);
|
|
225
|
+
const risk = assessRisk(counts, findings.length);
|
|
226
|
+
const rules = buildRules(findings);
|
|
227
|
+
return {
|
|
228
|
+
reportVersion: "2.0",
|
|
229
|
+
engineVersion,
|
|
230
|
+
generatedAt,
|
|
231
|
+
session: {
|
|
232
|
+
name: session.name,
|
|
233
|
+
driver: session.driver,
|
|
234
|
+
driverConfig: session.driverConfig,
|
|
235
|
+
stepsCount: session.steps.length,
|
|
236
|
+
metadata: session.metadata
|
|
237
|
+
},
|
|
238
|
+
stats: {
|
|
239
|
+
stepsExecuted: result.stepsExecuted,
|
|
240
|
+
payloadsTested: result.payloadsTested,
|
|
241
|
+
durationMs: result.duration,
|
|
242
|
+
errors: result.errors
|
|
243
|
+
},
|
|
244
|
+
summary: {
|
|
245
|
+
totalFindings: findings.length,
|
|
246
|
+
severityCounts: counts,
|
|
247
|
+
risk,
|
|
248
|
+
vulnerabilityTypes: [...new Set(findings.map((f) => f.type))],
|
|
249
|
+
affectedUrls: [...new Set(findings.map((f) => f.url))]
|
|
250
|
+
},
|
|
251
|
+
rules,
|
|
252
|
+
findings,
|
|
253
|
+
activeFindings,
|
|
254
|
+
passiveAnalysis: buildPassiveAnalysis(passiveFindings)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function formatDuration(ms) {
|
|
258
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
259
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
260
|
+
}
|
|
261
|
+
|
|
6
262
|
// src/html.ts
|
|
7
263
|
var COLORS = {
|
|
8
264
|
bg: "#0a0a0f",
|
|
@@ -39,30 +295,9 @@ function severityColor(severity) {
|
|
|
39
295
|
return COLORS.textMuted;
|
|
40
296
|
}
|
|
41
297
|
}
|
|
42
|
-
function severityOrder(severity) {
|
|
43
|
-
switch (severity) {
|
|
44
|
-
case "critical":
|
|
45
|
-
return 0;
|
|
46
|
-
case "high":
|
|
47
|
-
return 1;
|
|
48
|
-
case "medium":
|
|
49
|
-
return 2;
|
|
50
|
-
case "low":
|
|
51
|
-
return 3;
|
|
52
|
-
case "info":
|
|
53
|
-
return 4;
|
|
54
|
-
default:
|
|
55
|
-
return 5;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
298
|
function escapeHtml(str) {
|
|
59
299
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
60
300
|
}
|
|
61
|
-
function formatDuration(ms) {
|
|
62
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
63
|
-
const seconds = (ms / 1e3).toFixed(1);
|
|
64
|
-
return `${seconds}s`;
|
|
65
|
-
}
|
|
66
301
|
function formatDate(iso) {
|
|
67
302
|
const d = new Date(iso);
|
|
68
303
|
return d.toLocaleDateString("en-US", {
|
|
@@ -89,31 +324,95 @@ var VULCN_LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20
|
|
|
89
324
|
<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"/>
|
|
90
325
|
<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"/>
|
|
91
326
|
</svg>`;
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
327
|
+
function renderPassiveSection(analysis) {
|
|
328
|
+
if (analysis.totalIssues === 0) {
|
|
329
|
+
return `
|
|
330
|
+
<div class="passive-section animate-in-delay-3">
|
|
331
|
+
<div class="section-header">
|
|
332
|
+
<h3>\u{1F6E1}\uFE0F Passive Security Analysis</h3>
|
|
333
|
+
<span class="findings-count">All clear</span>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="passive-clear">
|
|
336
|
+
<div class="icon">\u2705</div>
|
|
337
|
+
<h4>All Passive Checks Passed</h4>
|
|
338
|
+
<p>No security header, cookie, CORS, or information disclosure issues detected.</p>
|
|
339
|
+
</div>
|
|
340
|
+
</div>`;
|
|
106
341
|
}
|
|
107
|
-
const
|
|
342
|
+
const categoryCards = analysis.categories.map((cat) => {
|
|
343
|
+
const statusColor = cat.status === "pass" ? COLORS.success : cat.status === "fail" ? COLORS.high : COLORS.medium;
|
|
344
|
+
return `
|
|
345
|
+
<div class="passive-category-card">
|
|
346
|
+
<div class="passive-cat-header">
|
|
347
|
+
<div class="passive-cat-icon">${cat.definition.icon}</div>
|
|
348
|
+
<div class="passive-cat-info">
|
|
349
|
+
<div class="passive-cat-title">${cat.definition.label}</div>
|
|
350
|
+
<div class="passive-cat-count" style="color: ${statusColor}">
|
|
351
|
+
${cat.issueCount === 0 ? "\u2713 All clear" : `${cat.issueCount} issue${cat.issueCount !== 1 ? "s" : ""}`}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="passive-cat-badge" style="background: ${statusColor}20; color: ${statusColor}; border-color: ${statusColor}30">
|
|
355
|
+
${cat.status.toUpperCase()}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
${cat.findings.length > 0 ? `
|
|
359
|
+
<div class="passive-cat-findings">
|
|
360
|
+
${cat.findings.map(
|
|
361
|
+
(f) => `
|
|
362
|
+
<div class="passive-finding-row">
|
|
363
|
+
<div class="passive-finding-dot" style="background: ${severityColor(f.severity)}"></div>
|
|
364
|
+
<div class="passive-finding-content">
|
|
365
|
+
<div class="passive-finding-title">${escapeHtml(f.title)}</div>
|
|
366
|
+
<div class="passive-finding-desc">${escapeHtml(f.description)}</div>
|
|
367
|
+
${f.evidence ? `<div class="passive-finding-evidence">${escapeHtml(f.evidence)}</div>` : ""}
|
|
368
|
+
</div>
|
|
369
|
+
<span class="passive-sev-tag" style="color: ${severityColor(f.severity)}">${f.severity.toUpperCase()}</span>
|
|
370
|
+
</div>
|
|
371
|
+
`
|
|
372
|
+
).join("")}
|
|
373
|
+
</div>
|
|
374
|
+
<div class="passive-remedy">
|
|
375
|
+
<span class="passive-remedy-label">\u{1F4A1} Remediation</span>
|
|
376
|
+
<span>${cat.definition.remedy}</span>
|
|
377
|
+
</div>
|
|
378
|
+
` : `
|
|
379
|
+
<div class="passive-checks-passed">
|
|
380
|
+
${cat.definition.checks.map(
|
|
381
|
+
(check) => `
|
|
382
|
+
<div class="passive-check-item">
|
|
383
|
+
<span class="passive-check-icon">\u2713</span>
|
|
384
|
+
<span>${check}</span>
|
|
385
|
+
</div>
|
|
386
|
+
`
|
|
387
|
+
).join("")}
|
|
388
|
+
</div>
|
|
389
|
+
`}
|
|
390
|
+
</div>`;
|
|
391
|
+
}).join("");
|
|
392
|
+
return `
|
|
393
|
+
<div class="passive-section animate-in-delay-3">
|
|
394
|
+
<div class="section-header">
|
|
395
|
+
<h3>\u{1F6E1}\uFE0F Passive Security Analysis</h3>
|
|
396
|
+
<span class="findings-count">${analysis.totalIssues} issue${analysis.totalIssues !== 1 ? "s" : ""}</span>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="passive-grid">
|
|
399
|
+
${categoryCards}
|
|
400
|
+
</div>
|
|
401
|
+
</div>`;
|
|
402
|
+
}
|
|
403
|
+
function generateHtml(report) {
|
|
404
|
+
const { summary, stats, session, passiveAnalysis, activeFindings, findings } = report;
|
|
405
|
+
const {
|
|
406
|
+
severityCounts: counts,
|
|
407
|
+
risk,
|
|
408
|
+
totalFindings,
|
|
409
|
+
affectedUrls,
|
|
410
|
+
vulnerabilityTypes: vulnTypes
|
|
411
|
+
} = summary;
|
|
412
|
+
const { stepsExecuted, payloadsTested, durationMs, errors } = stats;
|
|
108
413
|
const hasFindings = totalFindings > 0;
|
|
109
|
-
const
|
|
110
|
-
const maxRisk = totalFindings * 10 || 1;
|
|
111
|
-
const riskPercent = Math.min(100, Math.round(riskScore / maxRisk * 100));
|
|
112
|
-
const riskLabel = riskPercent >= 80 ? "Critical" : riskPercent >= 50 ? "High" : riskPercent >= 25 ? "Medium" : riskPercent > 0 ? "Low" : "Clear";
|
|
113
|
-
const riskColor = riskPercent >= 80 ? COLORS.critical : riskPercent >= 50 ? COLORS.high : riskPercent >= 25 ? COLORS.medium : riskPercent > 0 ? COLORS.low : COLORS.success;
|
|
414
|
+
const riskColor = risk.percent >= 80 ? COLORS.critical : risk.percent >= 50 ? COLORS.high : risk.percent >= 25 ? COLORS.medium : risk.percent > 0 ? COLORS.low : COLORS.success;
|
|
114
415
|
const donutSvg = generateDonut(counts, totalFindings);
|
|
115
|
-
const affectedUrls = [...new Set(findings.map((f) => f.url))];
|
|
116
|
-
const vulnTypes = [...new Set(findings.map((f) => f.type))];
|
|
117
416
|
return `<!DOCTYPE html>
|
|
118
417
|
<html lang="en">
|
|
119
418
|
<head>
|
|
@@ -605,6 +904,180 @@ function generateHtml(data) {
|
|
|
605
904
|
.no-findings h3 { font-size: 20px; font-weight: 700; color: ${COLORS.success}; margin-bottom: 8px; }
|
|
606
905
|
.no-findings p { font-size: 14px; color: var(--text-muted); }
|
|
607
906
|
|
|
907
|
+
/* \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 */
|
|
908
|
+
.passive-section { margin-bottom: 32px; }
|
|
909
|
+
|
|
910
|
+
.section-header {
|
|
911
|
+
display: flex;
|
|
912
|
+
align-items: center;
|
|
913
|
+
justify-content: space-between;
|
|
914
|
+
margin-bottom: 16px;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.section-header h3 {
|
|
918
|
+
font-size: 18px;
|
|
919
|
+
font-weight: 700;
|
|
920
|
+
letter-spacing: -0.01em;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.passive-grid {
|
|
924
|
+
display: grid;
|
|
925
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
926
|
+
gap: 12px;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.passive-category-card {
|
|
930
|
+
background: var(--surface);
|
|
931
|
+
border: 1px solid var(--border);
|
|
932
|
+
border-radius: var(--radius);
|
|
933
|
+
overflow: hidden;
|
|
934
|
+
transition: border-color 0.2s;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.passive-category-card:hover { border-color: var(--border-active); }
|
|
938
|
+
|
|
939
|
+
.passive-cat-header {
|
|
940
|
+
display: flex;
|
|
941
|
+
align-items: center;
|
|
942
|
+
gap: 12px;
|
|
943
|
+
padding: 16px 20px;
|
|
944
|
+
border-bottom: 1px solid var(--border);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.passive-cat-icon {
|
|
948
|
+
font-size: 20px;
|
|
949
|
+
width: 36px;
|
|
950
|
+
height: 36px;
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: center;
|
|
954
|
+
background: rgba(255,255,255,0.03);
|
|
955
|
+
border-radius: var(--radius-xs);
|
|
956
|
+
flex-shrink: 0;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.passive-cat-info { flex: 1; min-width: 0; }
|
|
960
|
+
|
|
961
|
+
.passive-cat-title {
|
|
962
|
+
font-size: 14px;
|
|
963
|
+
font-weight: 600;
|
|
964
|
+
letter-spacing: -0.01em;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.passive-cat-count {
|
|
968
|
+
font-size: 12px;
|
|
969
|
+
font-weight: 500;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.passive-cat-badge {
|
|
973
|
+
font-size: 10px;
|
|
974
|
+
font-weight: 700;
|
|
975
|
+
letter-spacing: 0.05em;
|
|
976
|
+
padding: 3px 8px;
|
|
977
|
+
border-radius: 100px;
|
|
978
|
+
border: 1px solid;
|
|
979
|
+
flex-shrink: 0;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.passive-cat-findings {
|
|
983
|
+
padding: 12px 20px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.passive-finding-row {
|
|
987
|
+
display: flex;
|
|
988
|
+
align-items: flex-start;
|
|
989
|
+
gap: 10px;
|
|
990
|
+
padding: 8px 0;
|
|
991
|
+
border-bottom: 1px solid rgba(255,255,255,0.03);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.passive-finding-row:last-child { border-bottom: none; }
|
|
995
|
+
|
|
996
|
+
.passive-finding-dot {
|
|
997
|
+
width: 6px;
|
|
998
|
+
height: 6px;
|
|
999
|
+
border-radius: 50%;
|
|
1000
|
+
flex-shrink: 0;
|
|
1001
|
+
margin-top: 7px;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.passive-finding-content { flex: 1; min-width: 0; }
|
|
1005
|
+
|
|
1006
|
+
.passive-finding-title {
|
|
1007
|
+
font-size: 13px;
|
|
1008
|
+
font-weight: 500;
|
|
1009
|
+
margin-bottom: 2px;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.passive-finding-desc {
|
|
1013
|
+
font-size: 12px;
|
|
1014
|
+
color: var(--text-muted);
|
|
1015
|
+
line-height: 1.4;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.passive-finding-evidence {
|
|
1019
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1020
|
+
font-size: 11px;
|
|
1021
|
+
color: var(--text-dim);
|
|
1022
|
+
margin-top: 4px;
|
|
1023
|
+
padding: 4px 8px;
|
|
1024
|
+
background: rgba(255,255,255,0.02);
|
|
1025
|
+
border-radius: 4px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.passive-sev-tag {
|
|
1029
|
+
font-size: 10px;
|
|
1030
|
+
font-weight: 700;
|
|
1031
|
+
letter-spacing: 0.04em;
|
|
1032
|
+
flex-shrink: 0;
|
|
1033
|
+
margin-top: 2px;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.passive-remedy {
|
|
1037
|
+
padding: 12px 20px;
|
|
1038
|
+
background: rgba(66, 165, 245, 0.04);
|
|
1039
|
+
border-top: 1px solid rgba(66, 165, 245, 0.08);
|
|
1040
|
+
font-size: 12px;
|
|
1041
|
+
color: var(--text-muted);
|
|
1042
|
+
line-height: 1.5;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.passive-remedy-label {
|
|
1046
|
+
font-weight: 600;
|
|
1047
|
+
margin-right: 6px;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.passive-checks-passed {
|
|
1051
|
+
padding: 12px 20px;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.passive-check-item {
|
|
1055
|
+
display: flex;
|
|
1056
|
+
align-items: center;
|
|
1057
|
+
gap: 8px;
|
|
1058
|
+
padding: 4px 0;
|
|
1059
|
+
font-size: 12px;
|
|
1060
|
+
color: var(--text-muted);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.passive-check-icon {
|
|
1064
|
+
color: ${COLORS.success};
|
|
1065
|
+
font-weight: 700;
|
|
1066
|
+
font-size: 11px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.passive-clear {
|
|
1070
|
+
text-align: center;
|
|
1071
|
+
padding: 40px 24px;
|
|
1072
|
+
background: var(--surface);
|
|
1073
|
+
border: 1px solid var(--border);
|
|
1074
|
+
border-radius: var(--radius);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.passive-clear .icon { font-size: 36px; margin-bottom: 12px; }
|
|
1078
|
+
.passive-clear h4 { font-size: 16px; font-weight: 600; color: ${COLORS.success}; margin-bottom: 6px; }
|
|
1079
|
+
.passive-clear p { font-size: 13px; color: var(--text-muted); }
|
|
1080
|
+
|
|
608
1081
|
/* Errors section */
|
|
609
1082
|
.errors-section {
|
|
610
1083
|
margin-bottom: 32px;
|
|
@@ -669,6 +1142,7 @@ function generateHtml(data) {
|
|
|
669
1142
|
body::before { display: none; }
|
|
670
1143
|
.finding-details { display: block !important; padding-top: 12px !important; }
|
|
671
1144
|
.finding-card { page-break-inside: avoid; }
|
|
1145
|
+
.passive-category-card { page-break-inside: avoid; }
|
|
672
1146
|
}
|
|
673
1147
|
</style>
|
|
674
1148
|
</head>
|
|
@@ -684,8 +1158,8 @@ function generateHtml(data) {
|
|
|
684
1158
|
</div>
|
|
685
1159
|
</div>
|
|
686
1160
|
<div class="header-meta">
|
|
687
|
-
<div>${formatDate(generatedAt)}</div>
|
|
688
|
-
<div>Engine v${escapeHtml(engineVersion)}</div>
|
|
1161
|
+
<div>${formatDate(report.generatedAt)}</div>
|
|
1162
|
+
<div>Engine v${escapeHtml(report.engineVersion)}</div>
|
|
689
1163
|
</div>
|
|
690
1164
|
</div>
|
|
691
1165
|
|
|
@@ -700,11 +1174,11 @@ function generateHtml(data) {
|
|
|
700
1174
|
${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>` : ""}
|
|
701
1175
|
<div class="meta-item">
|
|
702
1176
|
<span class="meta-label">Duration</span>
|
|
703
|
-
<span class="meta-value">${formatDuration(
|
|
1177
|
+
<span class="meta-value">${formatDuration(durationMs)}</span>
|
|
704
1178
|
</div>
|
|
705
1179
|
<div class="meta-item">
|
|
706
1180
|
<span class="meta-label">Generated</span>
|
|
707
|
-
<span class="meta-value">${formatDate(generatedAt)}</span>
|
|
1181
|
+
<span class="meta-value">${formatDate(report.generatedAt)}</span>
|
|
708
1182
|
</div>
|
|
709
1183
|
</div>
|
|
710
1184
|
</div>
|
|
@@ -717,13 +1191,13 @@ function generateHtml(data) {
|
|
|
717
1191
|
<svg viewBox="0 0 160 160" width="160" height="160">
|
|
718
1192
|
<circle cx="80" cy="80" r="68" fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="10"/>
|
|
719
1193
|
<circle cx="80" cy="80" r="68" fill="none" stroke="${riskColor}" stroke-width="10"
|
|
720
|
-
stroke-dasharray="${
|
|
1194
|
+
stroke-dasharray="${risk.percent / 100 * 427} 427"
|
|
721
1195
|
stroke-linecap="round"
|
|
722
1196
|
style="filter: drop-shadow(0 0 6px ${riskColor});"/>
|
|
723
1197
|
</svg>
|
|
724
1198
|
<div class="risk-gauge-label">
|
|
725
|
-
<div class="score" style="color: ${riskColor}">${hasFindings ?
|
|
726
|
-
<div class="label">${
|
|
1199
|
+
<div class="score" style="color: ${riskColor}">${hasFindings ? risk.percent : 0}</div>
|
|
1200
|
+
<div class="label">${risk.label}</div>
|
|
727
1201
|
</div>
|
|
728
1202
|
</div>
|
|
729
1203
|
</div>
|
|
@@ -736,11 +1210,11 @@ function generateHtml(data) {
|
|
|
736
1210
|
<div class="stat-label">Findings</div>
|
|
737
1211
|
</div>
|
|
738
1212
|
<div class="stat-box">
|
|
739
|
-
<div class="stat-number">${
|
|
1213
|
+
<div class="stat-number">${payloadsTested}</div>
|
|
740
1214
|
<div class="stat-label">Payloads Tested</div>
|
|
741
1215
|
</div>
|
|
742
1216
|
<div class="stat-box">
|
|
743
|
-
<div class="stat-number">${
|
|
1217
|
+
<div class="stat-number">${stepsExecuted}</div>
|
|
744
1218
|
<div class="stat-label">Steps Executed</div>
|
|
745
1219
|
</div>
|
|
746
1220
|
<div class="stat-box">
|
|
@@ -768,14 +1242,17 @@ function generateHtml(data) {
|
|
|
768
1242
|
</div>
|
|
769
1243
|
</div>
|
|
770
1244
|
|
|
771
|
-
<!--
|
|
1245
|
+
<!-- Passive Security Analysis -->
|
|
1246
|
+
${renderPassiveSection(passiveAnalysis)}
|
|
1247
|
+
|
|
1248
|
+
<!-- Active Findings -->
|
|
772
1249
|
<div class="findings-section animate-in-delay-3">
|
|
773
1250
|
<div class="findings-header">
|
|
774
|
-
<h3
|
|
775
|
-
<span class="findings-count">${
|
|
1251
|
+
<h3>\u{1F3AF} Active Scan Findings</h3>
|
|
1252
|
+
<span class="findings-count">${activeFindings.length} finding${activeFindings.length !== 1 ? "s" : ""}</span>
|
|
776
1253
|
</div>
|
|
777
1254
|
|
|
778
|
-
${
|
|
1255
|
+
${activeFindings.length > 0 ? activeFindings.map(
|
|
779
1256
|
(f, i) => `
|
|
780
1257
|
<div class="finding-card" onclick="this.classList.toggle('open')">
|
|
781
1258
|
<div class="finding-header">
|
|
@@ -821,16 +1298,16 @@ function generateHtml(data) {
|
|
|
821
1298
|
).join("") : `
|
|
822
1299
|
<div class="no-findings">
|
|
823
1300
|
<div class="icon">\u{1F6E1}\uFE0F</div>
|
|
824
|
-
<h3>No Vulnerabilities Detected</h3>
|
|
825
|
-
<p>${
|
|
1301
|
+
<h3>No Active Vulnerabilities Detected</h3>
|
|
1302
|
+
<p>${payloadsTested} payloads were tested across ${stepsExecuted} steps with no findings.</p>
|
|
826
1303
|
</div>
|
|
827
1304
|
`}
|
|
828
1305
|
</div>
|
|
829
1306
|
|
|
830
|
-
${
|
|
1307
|
+
${errors.length > 0 ? `
|
|
831
1308
|
<div class="errors-section">
|
|
832
|
-
<h3>\u26A0\uFE0F Errors During Execution (${
|
|
833
|
-
${
|
|
1309
|
+
<h3>\u26A0\uFE0F Errors During Execution (${errors.length})</h3>
|
|
1310
|
+
${errors.map((e) => `<div class="error-item">${escapeHtml(e)}</div>`).join("")}
|
|
834
1311
|
</div>
|
|
835
1312
|
` : ""}
|
|
836
1313
|
|
|
@@ -865,110 +1342,63 @@ function generateDonut(counts, total) {
|
|
|
865
1342
|
}
|
|
866
1343
|
|
|
867
1344
|
// src/json.ts
|
|
868
|
-
function
|
|
869
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
870
|
-
return `${(ms / 1e3).toFixed(1)}s`;
|
|
871
|
-
}
|
|
872
|
-
function generateJson(session, result, generatedAt, engineVersion) {
|
|
873
|
-
const counts = {
|
|
874
|
-
critical: 0,
|
|
875
|
-
high: 0,
|
|
876
|
-
medium: 0,
|
|
877
|
-
low: 0,
|
|
878
|
-
info: 0
|
|
879
|
-
};
|
|
880
|
-
for (const f of result.findings) {
|
|
881
|
-
counts[f.severity] = (counts[f.severity] || 0) + 1;
|
|
882
|
-
}
|
|
883
|
-
const riskScore = counts.critical * 10 + counts.high * 7 + counts.medium * 4 + counts.low * 1;
|
|
1345
|
+
function generateJson(report) {
|
|
884
1346
|
return {
|
|
885
1347
|
vulcn: {
|
|
886
|
-
version: engineVersion,
|
|
887
|
-
reportVersion:
|
|
888
|
-
generatedAt
|
|
889
|
-
},
|
|
890
|
-
session: {
|
|
891
|
-
name: session.name,
|
|
892
|
-
driver: session.driver,
|
|
893
|
-
driverConfig: session.driverConfig,
|
|
894
|
-
stepsCount: session.steps.length,
|
|
895
|
-
metadata: session.metadata
|
|
1348
|
+
version: report.engineVersion,
|
|
1349
|
+
reportVersion: report.reportVersion,
|
|
1350
|
+
generatedAt: report.generatedAt
|
|
896
1351
|
},
|
|
1352
|
+
session: report.session,
|
|
897
1353
|
execution: {
|
|
898
|
-
stepsExecuted:
|
|
899
|
-
payloadsTested:
|
|
900
|
-
durationMs:
|
|
901
|
-
durationFormatted:
|
|
902
|
-
errors:
|
|
1354
|
+
stepsExecuted: report.stats.stepsExecuted,
|
|
1355
|
+
payloadsTested: report.stats.payloadsTested,
|
|
1356
|
+
durationMs: report.stats.durationMs,
|
|
1357
|
+
durationFormatted: formatDuration(report.stats.durationMs),
|
|
1358
|
+
errors: report.stats.errors
|
|
903
1359
|
},
|
|
904
1360
|
summary: {
|
|
905
|
-
totalFindings:
|
|
906
|
-
riskScore,
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1361
|
+
totalFindings: report.summary.totalFindings,
|
|
1362
|
+
riskScore: report.summary.risk.score,
|
|
1363
|
+
riskLabel: report.summary.risk.label,
|
|
1364
|
+
severityCounts: report.summary.severityCounts,
|
|
1365
|
+
vulnerabilityTypes: report.summary.vulnerabilityTypes,
|
|
1366
|
+
affectedUrls: report.summary.affectedUrls
|
|
1367
|
+
},
|
|
1368
|
+
findings: report.findings,
|
|
1369
|
+
passiveAnalysis: {
|
|
1370
|
+
totalIssues: report.passiveAnalysis.totalIssues,
|
|
1371
|
+
categories: report.passiveAnalysis.categories.map((c) => ({
|
|
1372
|
+
id: c.definition.id,
|
|
1373
|
+
label: c.definition.label,
|
|
1374
|
+
status: c.status,
|
|
1375
|
+
issueCount: c.issueCount,
|
|
1376
|
+
passedChecks: c.passedChecks,
|
|
1377
|
+
totalChecks: c.totalChecks,
|
|
1378
|
+
remedy: c.definition.remedy
|
|
1379
|
+
}))
|
|
910
1380
|
},
|
|
911
|
-
|
|
1381
|
+
rules: report.rules
|
|
912
1382
|
};
|
|
913
1383
|
}
|
|
914
1384
|
|
|
915
1385
|
// src/yaml.ts
|
|
916
1386
|
import { stringify } from "yaml";
|
|
917
|
-
function generateYaml(
|
|
918
|
-
const
|
|
1387
|
+
function generateYaml(report) {
|
|
1388
|
+
const jsonReport = generateJson(report);
|
|
919
1389
|
const header = [
|
|
920
1390
|
"# \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",
|
|
921
1391
|
"# Vulcn Security Report",
|
|
922
|
-
`# Generated: ${generatedAt}`,
|
|
923
|
-
`# Session: ${session.name}`,
|
|
924
|
-
`# Findings: ${
|
|
1392
|
+
`# Generated: ${report.generatedAt}`,
|
|
1393
|
+
`# Session: ${report.session.name}`,
|
|
1394
|
+
`# Findings: ${report.summary.totalFindings}`,
|
|
925
1395
|
"# \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",
|
|
926
1396
|
""
|
|
927
1397
|
].join("\n");
|
|
928
|
-
return header + stringify(
|
|
1398
|
+
return header + stringify(jsonReport, { indent: 2 });
|
|
929
1399
|
}
|
|
930
1400
|
|
|
931
1401
|
// src/sarif.ts
|
|
932
|
-
var CWE_MAP = {
|
|
933
|
-
xss: {
|
|
934
|
-
id: 79,
|
|
935
|
-
name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')"
|
|
936
|
-
},
|
|
937
|
-
sqli: {
|
|
938
|
-
id: 89,
|
|
939
|
-
name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"
|
|
940
|
-
},
|
|
941
|
-
ssrf: { id: 918, name: "Server-Side Request Forgery (SSRF)" },
|
|
942
|
-
xxe: {
|
|
943
|
-
id: 611,
|
|
944
|
-
name: "Improper Restriction of XML External Entity Reference"
|
|
945
|
-
},
|
|
946
|
-
"command-injection": {
|
|
947
|
-
id: 78,
|
|
948
|
-
name: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
|
|
949
|
-
},
|
|
950
|
-
"path-traversal": {
|
|
951
|
-
id: 22,
|
|
952
|
-
name: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
|
|
953
|
-
},
|
|
954
|
-
"open-redirect": {
|
|
955
|
-
id: 601,
|
|
956
|
-
name: "URL Redirection to Untrusted Site ('Open Redirect')"
|
|
957
|
-
},
|
|
958
|
-
reflection: {
|
|
959
|
-
id: 200,
|
|
960
|
-
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
961
|
-
},
|
|
962
|
-
"security-misconfiguration": {
|
|
963
|
-
id: 16,
|
|
964
|
-
name: "Configuration"
|
|
965
|
-
},
|
|
966
|
-
"information-disclosure": {
|
|
967
|
-
id: 200,
|
|
968
|
-
name: "Exposure of Sensitive Information to an Unauthorized Actor"
|
|
969
|
-
},
|
|
970
|
-
custom: { id: 20, name: "Improper Input Validation" }
|
|
971
|
-
};
|
|
972
1402
|
function toSarifLevel(severity) {
|
|
973
1403
|
switch (severity) {
|
|
974
1404
|
case "critical":
|
|
@@ -983,22 +1413,6 @@ function toSarifLevel(severity) {
|
|
|
983
1413
|
return "warning";
|
|
984
1414
|
}
|
|
985
1415
|
}
|
|
986
|
-
function toSecuritySeverity(severity) {
|
|
987
|
-
switch (severity) {
|
|
988
|
-
case "critical":
|
|
989
|
-
return "9.0";
|
|
990
|
-
case "high":
|
|
991
|
-
return "7.0";
|
|
992
|
-
case "medium":
|
|
993
|
-
return "4.0";
|
|
994
|
-
case "low":
|
|
995
|
-
return "2.0";
|
|
996
|
-
case "info":
|
|
997
|
-
return "0.0";
|
|
998
|
-
default:
|
|
999
|
-
return "4.0";
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
1416
|
function toPrecision(severity) {
|
|
1003
1417
|
switch (severity) {
|
|
1004
1418
|
case "critical":
|
|
@@ -1014,63 +1428,53 @@ function toPrecision(severity) {
|
|
|
1014
1428
|
return "medium";
|
|
1015
1429
|
}
|
|
1016
1430
|
}
|
|
1017
|
-
function
|
|
1018
|
-
return
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
text: `${cwe.name} (CWE-${cwe.id})`
|
|
1035
|
-
},
|
|
1036
|
-
fullDescription: {
|
|
1037
|
-
text: `Vulcn detected a potential ${type} vulnerability. ${cwe.name}. See CWE-${cwe.id} for details.`
|
|
1038
|
-
},
|
|
1039
|
-
helpUri: `https://cwe.mitre.org/data/definitions/${cwe.id}.html`,
|
|
1040
|
-
help: {
|
|
1041
|
-
text: `## ${cwe.name}
|
|
1042
|
-
|
|
1043
|
-
CWE-${cwe.id}: ${cwe.name}
|
|
1044
|
-
|
|
1045
|
-
This rule detects ${type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1431
|
+
function toSarifRule(rule) {
|
|
1432
|
+
return {
|
|
1433
|
+
id: rule.id,
|
|
1434
|
+
name: rule.type,
|
|
1435
|
+
shortDescription: {
|
|
1436
|
+
text: `${rule.cwe.name} (CWE-${rule.cwe.id})`
|
|
1437
|
+
},
|
|
1438
|
+
fullDescription: {
|
|
1439
|
+
text: rule.description
|
|
1440
|
+
},
|
|
1441
|
+
helpUri: `https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html`,
|
|
1442
|
+
help: {
|
|
1443
|
+
text: `## ${rule.cwe.name}
|
|
1444
|
+
|
|
1445
|
+
CWE-${rule.cwe.id}: ${rule.cwe.name}
|
|
1446
|
+
|
|
1447
|
+
This rule detects ${rule.type} vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1046
1448
|
|
|
1047
1449
|
### Remediation
|
|
1048
1450
|
|
|
1049
|
-
See https://cwe.mitre.org/data/definitions/${cwe.id}.html for detailed remediation guidance.`,
|
|
1050
|
-
|
|
1451
|
+
See https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html for detailed remediation guidance.`,
|
|
1452
|
+
markdown: `## ${rule.cwe.name}
|
|
1051
1453
|
|
|
1052
|
-
**CWE-${cwe.id}**: ${cwe.name}
|
|
1454
|
+
**CWE-${rule.cwe.id}**: ${rule.cwe.name}
|
|
1053
1455
|
|
|
1054
|
-
This rule detects \`${type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1456
|
+
This rule detects \`${rule.type}\` vulnerabilities by injecting security payloads into form inputs and analyzing the application's response for signs of exploitation.
|
|
1055
1457
|
|
|
1056
1458
|
### Remediation
|
|
1057
1459
|
|
|
1058
|
-
See [CWE-${cwe.id}](https://cwe.mitre.org/data/definitions/${cwe.id}.html) for detailed remediation guidance.`
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1460
|
+
See [CWE-${rule.cwe.id}](https://cwe.mitre.org/data/definitions/${rule.cwe.id}.html) for detailed remediation guidance.`
|
|
1461
|
+
},
|
|
1462
|
+
properties: {
|
|
1463
|
+
tags: [
|
|
1464
|
+
"security",
|
|
1465
|
+
`CWE-${rule.cwe.id}`,
|
|
1466
|
+
`external/cwe/cwe-${rule.cwe.id}`
|
|
1467
|
+
],
|
|
1468
|
+
precision: toPrecision(rule.severity),
|
|
1469
|
+
"security-severity": rule.securitySeverity
|
|
1470
|
+
},
|
|
1471
|
+
defaultConfiguration: {
|
|
1472
|
+
level: toSarifLevel(rule.severity)
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1070
1475
|
}
|
|
1071
|
-
function toSarifResult(finding,
|
|
1072
|
-
const
|
|
1073
|
-
const ruleIndex = rules.findIndex((r) => r.id === ruleId);
|
|
1476
|
+
function toSarifResult(finding, sarifRules) {
|
|
1477
|
+
const ruleIndex = sarifRules.findIndex((r) => r.id === finding.ruleId);
|
|
1074
1478
|
let messageText = `${finding.title}
|
|
1075
1479
|
|
|
1076
1480
|
${finding.description}`;
|
|
@@ -1082,10 +1486,8 @@ Evidence: ${finding.evidence}`;
|
|
|
1082
1486
|
messageText += `
|
|
1083
1487
|
|
|
1084
1488
|
Payload: ${finding.payload}`;
|
|
1085
|
-
const uri = finding.url || "unknown";
|
|
1086
|
-
const fingerprint = `${finding.type}:${finding.stepId}:${finding.payload.slice(0, 50)}`;
|
|
1087
1489
|
return {
|
|
1088
|
-
ruleId,
|
|
1490
|
+
ruleId: finding.ruleId,
|
|
1089
1491
|
ruleIndex: Math.max(ruleIndex, 0),
|
|
1090
1492
|
level: toSarifLevel(finding.severity),
|
|
1091
1493
|
message: { text: messageText },
|
|
@@ -1093,7 +1495,7 @@ Payload: ${finding.payload}`;
|
|
|
1093
1495
|
{
|
|
1094
1496
|
physicalLocation: {
|
|
1095
1497
|
artifactLocation: {
|
|
1096
|
-
uri
|
|
1498
|
+
uri: finding.url || "unknown"
|
|
1097
1499
|
},
|
|
1098
1500
|
region: {
|
|
1099
1501
|
startLine: 1
|
|
@@ -1108,7 +1510,7 @@ Payload: ${finding.payload}`;
|
|
|
1108
1510
|
}
|
|
1109
1511
|
],
|
|
1110
1512
|
fingerprints: {
|
|
1111
|
-
vulcnFindingV1: fingerprint
|
|
1513
|
+
vulcnFindingV1: finding.fingerprint
|
|
1112
1514
|
},
|
|
1113
1515
|
partialFingerprints: {
|
|
1114
1516
|
vulcnType: finding.type,
|
|
@@ -1118,23 +1520,21 @@ Payload: ${finding.payload}`;
|
|
|
1118
1520
|
severity: finding.severity,
|
|
1119
1521
|
payload: finding.payload,
|
|
1120
1522
|
stepId: finding.stepId,
|
|
1523
|
+
detectionMethod: finding.detectionMethod,
|
|
1121
1524
|
...finding.evidence ? { evidence: finding.evidence } : {},
|
|
1122
|
-
...finding.
|
|
1525
|
+
...finding.passiveCategory ? { passiveCategory: finding.passiveCategory } : {}
|
|
1123
1526
|
}
|
|
1124
1527
|
};
|
|
1125
1528
|
}
|
|
1126
|
-
function generateSarif(
|
|
1127
|
-
const
|
|
1128
|
-
const results =
|
|
1129
|
-
const
|
|
1130
|
-
...new Set(result.findings.map((f) => f.url).filter(Boolean))
|
|
1131
|
-
];
|
|
1132
|
-
const artifacts = uniqueUrls.map((url) => ({
|
|
1529
|
+
function generateSarif(report) {
|
|
1530
|
+
const sarifRules = report.rules.map(toSarifRule);
|
|
1531
|
+
const results = report.findings.map((f) => toSarifResult(f, sarifRules));
|
|
1532
|
+
const artifacts = report.summary.affectedUrls.map((url) => ({
|
|
1133
1533
|
location: { uri: url }
|
|
1134
1534
|
}));
|
|
1135
|
-
const startDate = new Date(generatedAt);
|
|
1136
|
-
const endDate = new Date(startDate.getTime() +
|
|
1137
|
-
|
|
1535
|
+
const startDate = new Date(report.generatedAt);
|
|
1536
|
+
const endDate = new Date(startDate.getTime() + report.stats.durationMs);
|
|
1537
|
+
return {
|
|
1138
1538
|
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1139
1539
|
version: "2.1.0",
|
|
1140
1540
|
runs: [
|
|
@@ -1142,24 +1542,24 @@ function generateSarif(session, result, generatedAt, engineVersion) {
|
|
|
1142
1542
|
tool: {
|
|
1143
1543
|
driver: {
|
|
1144
1544
|
name: "Vulcn",
|
|
1145
|
-
version: engineVersion,
|
|
1146
|
-
semanticVersion: engineVersion,
|
|
1545
|
+
version: report.engineVersion,
|
|
1546
|
+
semanticVersion: report.engineVersion,
|
|
1147
1547
|
informationUri: "https://vulcn.dev",
|
|
1148
|
-
rules
|
|
1548
|
+
rules: sarifRules
|
|
1149
1549
|
}
|
|
1150
1550
|
},
|
|
1151
1551
|
results,
|
|
1152
1552
|
invocations: [
|
|
1153
1553
|
{
|
|
1154
|
-
executionSuccessful:
|
|
1155
|
-
startTimeUtc: generatedAt,
|
|
1554
|
+
executionSuccessful: report.stats.errors.length === 0,
|
|
1555
|
+
startTimeUtc: report.generatedAt,
|
|
1156
1556
|
endTimeUtc: endDate.toISOString(),
|
|
1157
1557
|
properties: {
|
|
1158
|
-
sessionName: session.name,
|
|
1159
|
-
stepsExecuted:
|
|
1160
|
-
payloadsTested:
|
|
1161
|
-
durationMs:
|
|
1162
|
-
...
|
|
1558
|
+
sessionName: report.session.name,
|
|
1559
|
+
stepsExecuted: report.stats.stepsExecuted,
|
|
1560
|
+
payloadsTested: report.stats.payloadsTested,
|
|
1561
|
+
durationMs: report.stats.durationMs,
|
|
1562
|
+
...report.stats.errors.length > 0 ? { errors: report.stats.errors } : {}
|
|
1163
1563
|
}
|
|
1164
1564
|
}
|
|
1165
1565
|
],
|
|
@@ -1167,7 +1567,6 @@ function generateSarif(session, result, generatedAt, engineVersion) {
|
|
|
1167
1567
|
}
|
|
1168
1568
|
]
|
|
1169
1569
|
};
|
|
1170
|
-
return sarifLog;
|
|
1171
1570
|
}
|
|
1172
1571
|
|
|
1173
1572
|
// src/index.ts
|
|
@@ -1216,13 +1615,20 @@ var plugin = {
|
|
|
1216
1615
|
);
|
|
1217
1616
|
},
|
|
1218
1617
|
/**
|
|
1219
|
-
* Generate report(s) after run completes
|
|
1618
|
+
* Generate report(s) after run completes.
|
|
1619
|
+
*
|
|
1620
|
+
* Architecture: RunResult + Session → buildReport() → VulcnReport
|
|
1621
|
+
* Each output format is a pure projection of the canonical model.
|
|
1220
1622
|
*/
|
|
1221
1623
|
onRunEnd: async (result, ctx) => {
|
|
1222
1624
|
const config = configSchema.parse(ctx.config);
|
|
1223
1625
|
const formats = getFormats(config.format);
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1626
|
+
const report = buildReport(
|
|
1627
|
+
ctx.session,
|
|
1628
|
+
result,
|
|
1629
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1630
|
+
ctx.engine.version
|
|
1631
|
+
);
|
|
1226
1632
|
const outDir = resolve(config.outputDir);
|
|
1227
1633
|
await mkdir(outDir, { recursive: true });
|
|
1228
1634
|
const basePath = resolve(outDir, config.filename);
|
|
@@ -1231,13 +1637,7 @@ var plugin = {
|
|
|
1231
1637
|
try {
|
|
1232
1638
|
switch (fmt) {
|
|
1233
1639
|
case "html": {
|
|
1234
|
-
const
|
|
1235
|
-
session: ctx.session,
|
|
1236
|
-
result,
|
|
1237
|
-
generatedAt,
|
|
1238
|
-
engineVersion
|
|
1239
|
-
};
|
|
1240
|
-
const html = generateHtml(htmlData);
|
|
1640
|
+
const html = generateHtml(report);
|
|
1241
1641
|
const htmlPath = `${basePath}.html`;
|
|
1242
1642
|
await writeFile(htmlPath, html, "utf-8");
|
|
1243
1643
|
writtenFiles.push(htmlPath);
|
|
@@ -1245,12 +1645,7 @@ var plugin = {
|
|
|
1245
1645
|
break;
|
|
1246
1646
|
}
|
|
1247
1647
|
case "json": {
|
|
1248
|
-
const jsonReport = generateJson(
|
|
1249
|
-
ctx.session,
|
|
1250
|
-
result,
|
|
1251
|
-
generatedAt,
|
|
1252
|
-
engineVersion
|
|
1253
|
-
);
|
|
1648
|
+
const jsonReport = generateJson(report);
|
|
1254
1649
|
const jsonPath = `${basePath}.json`;
|
|
1255
1650
|
await writeFile(
|
|
1256
1651
|
jsonPath,
|
|
@@ -1262,12 +1657,7 @@ var plugin = {
|
|
|
1262
1657
|
break;
|
|
1263
1658
|
}
|
|
1264
1659
|
case "yaml": {
|
|
1265
|
-
const yamlContent = generateYaml(
|
|
1266
|
-
ctx.session,
|
|
1267
|
-
result,
|
|
1268
|
-
generatedAt,
|
|
1269
|
-
engineVersion
|
|
1270
|
-
);
|
|
1660
|
+
const yamlContent = generateYaml(report);
|
|
1271
1661
|
const yamlPath = `${basePath}.yml`;
|
|
1272
1662
|
await writeFile(yamlPath, yamlContent, "utf-8");
|
|
1273
1663
|
writtenFiles.push(yamlPath);
|
|
@@ -1275,12 +1665,7 @@ var plugin = {
|
|
|
1275
1665
|
break;
|
|
1276
1666
|
}
|
|
1277
1667
|
case "sarif": {
|
|
1278
|
-
const sarifReport = generateSarif(
|
|
1279
|
-
ctx.session,
|
|
1280
|
-
result,
|
|
1281
|
-
generatedAt,
|
|
1282
|
-
engineVersion
|
|
1283
|
-
);
|
|
1668
|
+
const sarifReport = generateSarif(report);
|
|
1284
1669
|
const sarifPath = `${basePath}.sarif`;
|
|
1285
1670
|
await writeFile(
|
|
1286
1671
|
sarifPath,
|
|
@@ -1313,6 +1698,7 @@ var plugin = {
|
|
|
1313
1698
|
};
|
|
1314
1699
|
var index_default = plugin;
|
|
1315
1700
|
export {
|
|
1701
|
+
buildReport,
|
|
1316
1702
|
configSchema,
|
|
1317
1703
|
index_default as default,
|
|
1318
1704
|
generateHtml,
|