secure-review-extension 1.0.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +304 -0
- package/bin/secure-review.js +269 -0
- package/extension.js +368 -0
- package/media/shield.png +0 -0
- package/media/shield.svg +6 -0
- package/package.json +323 -0
- package/scripts/bootstrap-review-tools.js +54 -0
- package/src/code-actions.js +47 -0
- package/src/constants.js +20 -0
- package/src/diagnostics.js +41 -0
- package/src/findings-provider.js +78 -0
- package/src/report.js +837 -0
- package/src/scanners/bootstrap-tools.js +303 -0
- package/src/scanners/dynamic-scan.js +224 -0
- package/src/scanners/static-rules.js +497 -0
- package/src/scanners/static-scan.js +341 -0
- package/src/scanners/tool-integrations.js +666 -0
- package/src/scanners/workspace-profile.js +316 -0
- package/src/store.js +49 -0
- package/src/utils.js +24 -0
package/src/report.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
let vscode;
|
|
4
|
+
try {
|
|
5
|
+
vscode = require("vscode");
|
|
6
|
+
} catch {
|
|
7
|
+
vscode = null;
|
|
8
|
+
}
|
|
9
|
+
const { shorten } = require("./utils");
|
|
10
|
+
|
|
11
|
+
const SEVERITY_ORDER = ["critical", "high", "medium", "low"];
|
|
12
|
+
|
|
13
|
+
function buildReportModel(findings, metadata = {}) {
|
|
14
|
+
const normalized = [...findings]
|
|
15
|
+
.map((finding) => normalizeFinding(finding))
|
|
16
|
+
.sort(compareFindings);
|
|
17
|
+
const hasDynamicFindings = normalized.some((finding) => finding.source === "dynamic");
|
|
18
|
+
const hasStaticFindings = normalized.some((finding) => finding.source !== "dynamic");
|
|
19
|
+
const effectiveScanType = metadata.scanType
|
|
20
|
+
|| (hasStaticFindings && hasDynamicFindings
|
|
21
|
+
? "Static + Dynamic Analysis"
|
|
22
|
+
: hasDynamicFindings
|
|
23
|
+
? "Dynamic Analysis"
|
|
24
|
+
: "Static Analysis");
|
|
25
|
+
|
|
26
|
+
const severitySummary = SEVERITY_ORDER.map((severity) => ({
|
|
27
|
+
severity,
|
|
28
|
+
label: severity.toUpperCase(),
|
|
29
|
+
count: normalized.filter((finding) => finding.groupKey === severity).length
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const categoryMap = new Map();
|
|
33
|
+
for (const finding of normalized) {
|
|
34
|
+
categoryMap.set(finding.category, (categoryMap.get(finding.category) || 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const categorySummary = [...categoryMap.entries()]
|
|
38
|
+
.sort((left, right) => left[0].localeCompare(right[0]))
|
|
39
|
+
.map(([category, count]) => ({ category, count }));
|
|
40
|
+
|
|
41
|
+
const reviewDomainMap = new Map();
|
|
42
|
+
for (const finding of normalized) {
|
|
43
|
+
reviewDomainMap.set(finding.reviewDomain, (reviewDomainMap.get(finding.reviewDomain) || 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const reviewDomainSummary = [...reviewDomainMap.entries()]
|
|
47
|
+
.sort((left, right) => left[0].localeCompare(right[0]))
|
|
48
|
+
.map(([domain, count]) => ({ domain, count }));
|
|
49
|
+
|
|
50
|
+
const groupedOverview = SEVERITY_ORDER.map((severity) => ({
|
|
51
|
+
severity,
|
|
52
|
+
label: severity === "critical"
|
|
53
|
+
? "Critical Findings"
|
|
54
|
+
: severity === "high"
|
|
55
|
+
? "High Severity Findings"
|
|
56
|
+
: severity === "medium"
|
|
57
|
+
? "Medium Severity Findings"
|
|
58
|
+
: "Low Severity Findings",
|
|
59
|
+
findings: normalized.filter((finding) => finding.groupKey === severity)
|
|
60
|
+
})).filter((group) => group.findings.length > 0);
|
|
61
|
+
|
|
62
|
+
const totalFindings = normalized.length;
|
|
63
|
+
const categoriesCount = categorySummary.length;
|
|
64
|
+
const criticalCount = severitySummary.find((item) => item.severity === "critical")?.count || 0;
|
|
65
|
+
const topRecommendations = buildRecommendations(normalized);
|
|
66
|
+
const executiveSummary = totalFindings === 0
|
|
67
|
+
? `This report presents the results of the requested secure review. No findings were identified in the current visible findings set.`
|
|
68
|
+
: `This report presents the results of a ${effectiveScanType} on the ${metadata.projectName || "workspace"} workspace. A total of ${totalFindings} finding${totalFindings === 1 ? "" : "s"} were identified across ${categoriesCount} categor${categoriesCount === 1 ? "y" : "ies"} and ${reviewDomainSummary.length} review domain${reviewDomainSummary.length === 1 ? "" : "s"}.${criticalCount > 0 ? ` Immediate attention is required for the ${criticalCount} critical-severity finding${criticalCount === 1 ? "" : "s"}.` : ""}`;
|
|
69
|
+
|
|
70
|
+
const methodologyNotes = metadata.notes
|
|
71
|
+
|| (hasStaticFindings && hasDynamicFindings
|
|
72
|
+
? "Static analysis was performed using Secure Review built-in rule packs and any locally available external static analysis tools enabled in the extension settings. Dynamic analysis was performed using OWASP ZAP against the configured local or test target."
|
|
73
|
+
: hasDynamicFindings
|
|
74
|
+
? "Dynamic analysis was performed using OWASP ZAP against the configured local or test target URL from Secure Review."
|
|
75
|
+
: "Static analysis was performed using Secure Review built-in rule packs and any locally available external static analysis tools enabled in the extension settings.");
|
|
76
|
+
|
|
77
|
+
const methodologyLimitations = hasDynamicFindings && !hasStaticFindings
|
|
78
|
+
? "Results reflect only the current visible dynamic findings set in Secure Review. Ignored findings and findings below the configured minimum severity are excluded. Dynamic coverage depends on crawler reachability and the selected ZAP scan mode."
|
|
79
|
+
: hasStaticFindings && hasDynamicFindings
|
|
80
|
+
? "Results reflect only the current visible findings set in Secure Review. Ignored findings and findings below the configured minimum severity are excluded. Static coverage depends on which local analysis tools are installed, and dynamic coverage depends on crawler reachability and the selected ZAP scan mode."
|
|
81
|
+
: "Results reflect only the current visible findings set in Secure Review. Ignored findings and findings below the configured minimum severity are excluded. External tool coverage depends on which tools are installed locally.";
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
header: {
|
|
85
|
+
title: "Security Code Review Report",
|
|
86
|
+
projectName: metadata.projectName || "Workspace",
|
|
87
|
+
reportDate: formatDate(metadata.reportDate || new Date()),
|
|
88
|
+
scanType: effectiveScanType,
|
|
89
|
+
totalFindings
|
|
90
|
+
},
|
|
91
|
+
executiveSummary,
|
|
92
|
+
severitySummary,
|
|
93
|
+
categorySummary,
|
|
94
|
+
reviewDomainSummary,
|
|
95
|
+
groupedOverview,
|
|
96
|
+
detailedFindings: groupedOverview,
|
|
97
|
+
recommendationsSummary: topRecommendations,
|
|
98
|
+
methodology: {
|
|
99
|
+
notes: methodologyNotes,
|
|
100
|
+
limitations: methodologyLimitations,
|
|
101
|
+
configuration: metadata.scanConfiguration || `Scan Type: ${effectiveScanType}`
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeFinding(finding) {
|
|
107
|
+
const isDynamic = finding.source === "dynamic";
|
|
108
|
+
const locationDisplay = isDynamic
|
|
109
|
+
? (finding.targetUrl || "Unknown URL")
|
|
110
|
+
: finding.relativePath && finding.line
|
|
111
|
+
? `${finding.relativePath} (line ${finding.line})`
|
|
112
|
+
: finding.relativePath || finding.filePath || "Unknown file";
|
|
113
|
+
|
|
114
|
+
const overviewLocationDisplay = isDynamic
|
|
115
|
+
? (finding.targetUrl || "Unknown URL")
|
|
116
|
+
: finding.relativePath && finding.line
|
|
117
|
+
? `${finding.relativePath}:${finding.line}`
|
|
118
|
+
: finding.relativePath || finding.filePath || "Unknown file";
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...finding,
|
|
122
|
+
severityDisplay: severityLabel(finding.severity).toUpperCase(),
|
|
123
|
+
groupKey: finding.severity,
|
|
124
|
+
category: finding.category || "General",
|
|
125
|
+
subcategory: finding.subcategory || "General",
|
|
126
|
+
reviewDomain: finding.reviewDomain || "security",
|
|
127
|
+
confidence: finding.confidence || "medium",
|
|
128
|
+
locationDisplay,
|
|
129
|
+
overviewLocationDisplay,
|
|
130
|
+
remediationSummary: shorten(stripHtml(finding.remediation || "No remediation provided."), 100),
|
|
131
|
+
evidenceDisplay: stripHtml(finding.evidence || "n/a"),
|
|
132
|
+
whyItMatters: stripHtml(finding.whyItMatters || "This issue may increase security, correctness, reliability, or maintainability risk."),
|
|
133
|
+
remediation: stripHtml(finding.remediation || "No remediation provided."),
|
|
134
|
+
suggestion: stripHtml(finding.suggestion || finding.remediation || "Review and update the affected code path."),
|
|
135
|
+
standardsDisplay: Array.isArray(finding.standards) ? finding.standards.join(", ") : (finding.standards || "n/a")
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildRecommendations(findings) {
|
|
140
|
+
const grouped = new Map();
|
|
141
|
+
for (const finding of findings) {
|
|
142
|
+
const key = `${finding.category}|${finding.suggestion}`;
|
|
143
|
+
if (!grouped.has(key)) {
|
|
144
|
+
grouped.set(key, {
|
|
145
|
+
category: finding.category,
|
|
146
|
+
suggestion: finding.suggestion,
|
|
147
|
+
count: 0
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
grouped.get(key).count += 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...grouped.values()]
|
|
154
|
+
.sort((left, right) => right.count - left.count || left.category.localeCompare(right.category))
|
|
155
|
+
.slice(0, 5);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function severityLabel(severity) {
|
|
159
|
+
return severity.charAt(0).toUpperCase() + severity.slice(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function compareFindings(left, right) {
|
|
163
|
+
const severityDiff = SEVERITY_ORDER.indexOf(left.severity) - SEVERITY_ORDER.indexOf(right.severity);
|
|
164
|
+
if (severityDiff !== 0) {
|
|
165
|
+
return severityDiff;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const locationDiff = (left.overviewLocationDisplay || "").localeCompare(right.overviewLocationDisplay || "");
|
|
169
|
+
if (locationDiff !== 0) {
|
|
170
|
+
return locationDiff;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return left.title.localeCompare(right.title);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatDate(value) {
|
|
177
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
178
|
+
return date.toLocaleDateString("en-US", {
|
|
179
|
+
year: "numeric",
|
|
180
|
+
month: "long",
|
|
181
|
+
day: "numeric"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderReportHtml(reportModel) {
|
|
186
|
+
const severityCards = reportModel.severitySummary
|
|
187
|
+
.map((item) => `<div class="card"><strong>${escapeHtml(item.label)}</strong><div>${item.count}</div></div>`)
|
|
188
|
+
.join("");
|
|
189
|
+
|
|
190
|
+
const categoryRows = reportModel.categorySummary
|
|
191
|
+
.map((item) => `<tr><td>${escapeHtml(item.category)}</td><td>${item.count}</td></tr>`)
|
|
192
|
+
.join("") || '<tr><td colspan="2">No findings</td></tr>';
|
|
193
|
+
|
|
194
|
+
const domainRows = reportModel.reviewDomainSummary
|
|
195
|
+
.map((item) => `<tr><td>${escapeHtml(item.domain)}</td><td>${item.count}</td></tr>`)
|
|
196
|
+
.join("") || '<tr><td colspan="2">No findings</td></tr>';
|
|
197
|
+
|
|
198
|
+
const overviewSections = reportModel.groupedOverview
|
|
199
|
+
.map((group) => `
|
|
200
|
+
<section class="block">
|
|
201
|
+
<h3>${escapeHtml(group.label)}</h3>
|
|
202
|
+
${renderFindingsTable(group.findings)}
|
|
203
|
+
</section>
|
|
204
|
+
`)
|
|
205
|
+
.join("") || '<p>No findings are present in the current visible set.</p>';
|
|
206
|
+
|
|
207
|
+
const detailSections = reportModel.detailedFindings
|
|
208
|
+
.map((group, groupIndex) => `
|
|
209
|
+
<section class="block">
|
|
210
|
+
<h3>${groupIndex + 1}. ${escapeHtml(group.label)}</h3>
|
|
211
|
+
${group.findings.map((finding, index) => renderFindingDetail(finding, index + 1)).join("")}
|
|
212
|
+
</section>
|
|
213
|
+
`)
|
|
214
|
+
.join("") || "<p>No detailed findings to display.</p>";
|
|
215
|
+
|
|
216
|
+
return `<!DOCTYPE html>
|
|
217
|
+
<html lang="en">
|
|
218
|
+
<head>
|
|
219
|
+
<meta charset="UTF-8" />
|
|
220
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
221
|
+
<title>Secure Review Report</title>
|
|
222
|
+
<style>
|
|
223
|
+
body { font-family: Arial, sans-serif; font-size: 11pt; padding: 28px; color: #222222; background: #ffffff; line-height: 1.45; }
|
|
224
|
+
h1, h2, h3, h4 { margin-bottom: 8px; }
|
|
225
|
+
h1 { font-size: 28pt; font-weight: 400; color: #111111; }
|
|
226
|
+
h2 { color: #1F4E79; font-size: 18pt; font-weight: 700; border-bottom: 2px solid #1F4E79; padding-bottom: 4px; margin-top: 28px; }
|
|
227
|
+
h3 { color: #2E75B6; font-size: 13pt; font-weight: 700; margin-top: 18px; }
|
|
228
|
+
h4 { color: #333333; font-size: 11pt; font-weight: 700; margin-top: 14px; }
|
|
229
|
+
p { margin: 6px 0 12px; }
|
|
230
|
+
.meta p { margin: 3px 0; }
|
|
231
|
+
.cards { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 14px 0 20px; }
|
|
232
|
+
.card { background: white; border: 1px solid #b7c9dd; padding: 12px; }
|
|
233
|
+
.block, .finding { background: white; margin-bottom: 16px; }
|
|
234
|
+
table { width: 100%; border-collapse: collapse; background: white; margin-top: 8px; }
|
|
235
|
+
th, td { text-align: left; padding: 8px 10px; border: 1px solid #9aa9b8; vertical-align: top; font-size: 10.5pt; }
|
|
236
|
+
th { background: #ffffff; font-weight: 700; }
|
|
237
|
+
.finding-grid { display: grid; grid-template-columns: 180px 1fr; gap: 8px 16px; margin-top: 10px; }
|
|
238
|
+
.finding-grid strong { color: #222222; }
|
|
239
|
+
</style>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
<h1>${escapeHtml(reportModel.header.title)}</h1>
|
|
243
|
+
<div class="meta">
|
|
244
|
+
<p><strong>Project:</strong> ${escapeHtml(reportModel.header.projectName)}</p>
|
|
245
|
+
<p><strong>Date:</strong> ${escapeHtml(reportModel.header.reportDate)}</p>
|
|
246
|
+
<p><strong>Scan Type:</strong> ${escapeHtml(reportModel.header.scanType)}</p>
|
|
247
|
+
<p><strong>Total Findings:</strong> ${reportModel.header.totalFindings}</p>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<h2>1. Executive Summary</h2>
|
|
251
|
+
<p>${escapeHtml(reportModel.executiveSummary)}</p>
|
|
252
|
+
|
|
253
|
+
<h3>Finding Severity Summary</h3>
|
|
254
|
+
<div class="cards">${severityCards}</div>
|
|
255
|
+
|
|
256
|
+
<h3>Findings by Category</h3>
|
|
257
|
+
<table>
|
|
258
|
+
<thead><tr><th>Category</th><th>Finding Count</th></tr></thead>
|
|
259
|
+
<tbody>${categoryRows}</tbody>
|
|
260
|
+
</table>
|
|
261
|
+
|
|
262
|
+
<h3>Findings by Review Domain</h3>
|
|
263
|
+
<table>
|
|
264
|
+
<thead><tr><th>Review Domain</th><th>Finding Count</th></tr></thead>
|
|
265
|
+
<tbody>${domainRows}</tbody>
|
|
266
|
+
</table>
|
|
267
|
+
|
|
268
|
+
<h2>2. Findings Overview</h2>
|
|
269
|
+
${overviewSections}
|
|
270
|
+
|
|
271
|
+
<h2>3. Detailed Findings</h2>
|
|
272
|
+
<p>The following section provides full detail for each finding, including evidence and specific remediation guidance.</p>
|
|
273
|
+
${detailSections}
|
|
274
|
+
|
|
275
|
+
<h2>4. Recommendations Summary</h2>
|
|
276
|
+
<table>
|
|
277
|
+
<thead><tr><th>Category</th><th>Recommendation</th><th>Finding Count</th></tr></thead>
|
|
278
|
+
<tbody>
|
|
279
|
+
${reportModel.recommendationsSummary.map((item) => `<tr><td>${escapeHtml(item.category)}</td><td>${escapeHtml(item.suggestion)}</td><td>${item.count}</td></tr>`).join("") || '<tr><td colspan="3">No recommendations</td></tr>'}
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
|
|
283
|
+
<h2>5. Methodology, Limitations, and Scan Configuration</h2>
|
|
284
|
+
<div class="block">
|
|
285
|
+
<div class="finding-grid">
|
|
286
|
+
<strong>Methodology</strong><span>${escapeHtml(reportModel.methodology.notes)}</span>
|
|
287
|
+
<strong>Limitations</strong><span>${escapeHtml(reportModel.methodology.limitations)}</span>
|
|
288
|
+
<strong>Scan Configuration</strong><span>${escapeHtml(reportModel.methodology.configuration)}</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</body>
|
|
292
|
+
</html>`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderFindingsTable(findings) {
|
|
296
|
+
const rows = findings
|
|
297
|
+
.map((finding) => `
|
|
298
|
+
<tr>
|
|
299
|
+
<td>${escapeHtml(finding.severityDisplay)}</td>
|
|
300
|
+
<td>${escapeHtml(finding.category)}</td>
|
|
301
|
+
<td>${escapeHtml(finding.title)}</td>
|
|
302
|
+
<td>${escapeHtml(finding.overviewLocationDisplay)}</td>
|
|
303
|
+
<td>${escapeHtml(finding.remediationSummary)}</td>
|
|
304
|
+
</tr>
|
|
305
|
+
`)
|
|
306
|
+
.join("");
|
|
307
|
+
|
|
308
|
+
return `
|
|
309
|
+
<table>
|
|
310
|
+
<thead>
|
|
311
|
+
<tr>
|
|
312
|
+
<th>Severity</th>
|
|
313
|
+
<th>Category</th>
|
|
314
|
+
<th>Title</th>
|
|
315
|
+
<th>File / Line</th>
|
|
316
|
+
<th>Remediation Summary</th>
|
|
317
|
+
</tr>
|
|
318
|
+
</thead>
|
|
319
|
+
<tbody>${rows || '<tr><td colspan="5">No findings</td></tr>'}</tbody>
|
|
320
|
+
</table>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderFindingDetail(finding, index) {
|
|
325
|
+
return `
|
|
326
|
+
<div class="finding">
|
|
327
|
+
<h4>${index}. ${escapeHtml(finding.title)}</h4>
|
|
328
|
+
<div class="finding-grid">
|
|
329
|
+
<strong>Severity</strong><span>${escapeHtml(finding.severityDisplay)}</span>
|
|
330
|
+
<strong>Category</strong><span>${escapeHtml(finding.category)}</span>
|
|
331
|
+
<strong>Subcategory</strong><span>${escapeHtml(finding.subcategory)}</span>
|
|
332
|
+
<strong>Review Domain</strong><span>${escapeHtml(finding.reviewDomain)}</span>
|
|
333
|
+
<strong>Confidence</strong><span>${escapeHtml(finding.confidence)}</span>
|
|
334
|
+
<strong>Finding ID</strong><span>${escapeHtml(finding.id)}</span>
|
|
335
|
+
<strong>File / Location</strong><span>${escapeHtml(finding.locationDisplay)}</span>
|
|
336
|
+
<strong>Source</strong><span>${escapeHtml(finding.source)}</span>
|
|
337
|
+
<strong>Evidence</strong><span>${escapeHtml(finding.evidenceDisplay)}</span>
|
|
338
|
+
<strong>Why It Matters</strong><span>${escapeHtml(finding.whyItMatters)}</span>
|
|
339
|
+
<strong>Remediation</strong><span>${escapeHtml(finding.remediation || "No remediation provided.")}</span>
|
|
340
|
+
<strong>Suggestion</strong><span>${escapeHtml(finding.suggestion)}</span>
|
|
341
|
+
<strong>Standards</strong><span>${escapeHtml(finding.standardsDisplay)}</span>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function promptReportMetadata(defaultScanType = "Static Analysis") {
|
|
348
|
+
if (!vscode) {
|
|
349
|
+
throw new Error("Interactive report metadata prompts are only available inside VS Code.");
|
|
350
|
+
}
|
|
351
|
+
const workspaceName = vscode.workspace.workspaceFolders?.[0]?.name || "Workspace";
|
|
352
|
+
const projectName = await vscode.window.showInputBox({
|
|
353
|
+
title: "Secure Review Report",
|
|
354
|
+
prompt: "Project name",
|
|
355
|
+
value: workspaceName,
|
|
356
|
+
ignoreFocusOut: true
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (projectName === undefined) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const reportDate = await vscode.window.showInputBox({
|
|
364
|
+
title: "Secure Review Report",
|
|
365
|
+
prompt: "Report date",
|
|
366
|
+
value: formatDate(new Date()),
|
|
367
|
+
ignoreFocusOut: true
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (reportDate === undefined) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const scanType = await vscode.window.showInputBox({
|
|
375
|
+
title: "Secure Review Report",
|
|
376
|
+
prompt: "Scan type",
|
|
377
|
+
value: defaultScanType,
|
|
378
|
+
ignoreFocusOut: true
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (scanType === undefined) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const notes = await vscode.window.showInputBox({
|
|
386
|
+
title: "Secure Review Report",
|
|
387
|
+
prompt: "Optional methodology or limitation notes",
|
|
388
|
+
value: "",
|
|
389
|
+
ignoreFocusOut: true
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (notes === undefined) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
projectName: projectName.trim() || workspaceName,
|
|
398
|
+
reportDate: reportDate.trim() || formatDate(new Date()),
|
|
399
|
+
scanType: scanType.trim() || defaultScanType,
|
|
400
|
+
notes: notes.trim()
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function stripHtml(value) {
|
|
405
|
+
return String(value || "")
|
|
406
|
+
.replace(/<[^>]+>/g, " ")
|
|
407
|
+
.replace(/ /gi, " ")
|
|
408
|
+
.replace(/&/gi, "&")
|
|
409
|
+
.replace(/</gi, "<")
|
|
410
|
+
.replace(/>/gi, ">")
|
|
411
|
+
.replace(/\s+/g, " ")
|
|
412
|
+
.trim();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function exportDocx(reportModel, targetUri) {
|
|
416
|
+
const contentTypes = xml(
|
|
417
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
418
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
419
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
420
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
421
|
+
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
|
|
422
|
+
'<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>' +
|
|
423
|
+
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
|
|
424
|
+
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
|
|
425
|
+
"</Types>"
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const rootRels = xml(
|
|
429
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
430
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
431
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' +
|
|
432
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' +
|
|
433
|
+
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' +
|
|
434
|
+
"</Relationships>"
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const documentRels = xml(
|
|
438
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
439
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
440
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' +
|
|
441
|
+
"</Relationships>"
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const styles = xml(
|
|
445
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
446
|
+
'<w:styles mc:Ignorable="w14 w15" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">' +
|
|
447
|
+
'<w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:sz w:val="22"/><w:szCs w:val="22"/><w:color w:val="222222"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="120" w:line="240" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults>' +
|
|
448
|
+
'<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:color w:val="222222"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr></w:style>' +
|
|
449
|
+
'<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="160"/></w:pPr><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:color w:val="111111"/><w:sz w:val="56"/><w:szCs w:val="56"/></w:rPr></w:style>' +
|
|
450
|
+
'<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="Heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:pBdr><w:bottom w:val="single" w:color="1F4E79" w:sz="6" w:space="1"/></w:pBdr><w:spacing w:before="360" w:after="200"/><w:outlineLvl w:val="0"/></w:pPr><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:b/><w:bCs/><w:color w:val="1F4E79"/><w:sz w:val="36"/><w:szCs w:val="36"/></w:rPr></w:style>' +
|
|
451
|
+
'<w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="Heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="280" w:after="120"/><w:outlineLvl w:val="1"/></w:pPr><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:b/><w:bCs/><w:color w:val="2E75B6"/><w:sz w:val="26"/><w:szCs w:val="26"/></w:rPr></w:style>' +
|
|
452
|
+
'<w:style w:type="paragraph" w:styleId="Heading3"><w:name w:val="Heading 3"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="200" w:after="80"/><w:outlineLvl w:val="2"/></w:pPr><w:rPr><w:rFonts w:ascii="Arial" w:cs="Arial" w:eastAsia="Arial" w:hAnsi="Arial"/><w:b/><w:bCs/><w:color w:val="333333"/><w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr></w:style>' +
|
|
453
|
+
'<w:style w:type="paragraph" w:styleId="Strong"><w:name w:val="Strong"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:rPr><w:b/><w:bCs/></w:rPr></w:style>' +
|
|
454
|
+
'<w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="80"/></w:pPr></w:style>' +
|
|
455
|
+
"</w:styles>"
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const createdIso = new Date().toISOString();
|
|
459
|
+
const core = xml(
|
|
460
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
461
|
+
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
|
|
462
|
+
`<dc:title>${escapeXml(reportModel.header.title)}</dc:title>` +
|
|
463
|
+
"<dc:creator>Secure Review</dc:creator>" +
|
|
464
|
+
"<cp:lastModifiedBy>Secure Review</cp:lastModifiedBy>" +
|
|
465
|
+
`<dcterms:created xsi:type="dcterms:W3CDTF">${createdIso}</dcterms:created>` +
|
|
466
|
+
`<dcterms:modified xsi:type="dcterms:W3CDTF">${createdIso}</dcterms:modified>` +
|
|
467
|
+
"</cp:coreProperties>"
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const app = xml(
|
|
471
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
472
|
+
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
|
|
473
|
+
"<Application>Secure Review</Application>" +
|
|
474
|
+
"</Properties>"
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const document = xml(buildDocumentXml(reportModel));
|
|
478
|
+
|
|
479
|
+
const zipBuffer = buildZip([
|
|
480
|
+
{ name: "[Content_Types].xml", data: Buffer.from(contentTypes, "utf8") },
|
|
481
|
+
{ name: "_rels/.rels", data: Buffer.from(rootRels, "utf8") },
|
|
482
|
+
{ name: "docProps/core.xml", data: Buffer.from(core, "utf8") },
|
|
483
|
+
{ name: "docProps/app.xml", data: Buffer.from(app, "utf8") },
|
|
484
|
+
{ name: "word/document.xml", data: Buffer.from(document, "utf8") },
|
|
485
|
+
{ name: "word/styles.xml", data: Buffer.from(styles, "utf8") },
|
|
486
|
+
{ name: "word/_rels/document.xml.rels", data: Buffer.from(documentRels, "utf8") }
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
if (typeof targetUri === "string") {
|
|
490
|
+
await fs.writeFile(targetUri, zipBuffer);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!vscode) {
|
|
495
|
+
throw new Error("A filesystem path string is required when exporting DOCX outside VS Code.");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await vscode.workspace.fs.writeFile(targetUri, zipBuffer);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildDocumentXml(reportModel) {
|
|
502
|
+
const body = [];
|
|
503
|
+
|
|
504
|
+
body.push(paragraph(reportModel.header.title, "Title"));
|
|
505
|
+
body.push(paragraph(`Project: ${reportModel.header.projectName}`));
|
|
506
|
+
body.push(paragraph(`Date: ${reportModel.header.reportDate}`));
|
|
507
|
+
body.push(paragraph(`Scan Type: ${reportModel.header.scanType}`));
|
|
508
|
+
body.push(paragraph(`Total Findings: ${reportModel.header.totalFindings}`));
|
|
509
|
+
|
|
510
|
+
body.push(paragraph("1. Executive Summary", "Heading2"));
|
|
511
|
+
body.push(paragraph(reportModel.executiveSummary));
|
|
512
|
+
body.push(paragraph("Finding Severity Summary", "Heading3"));
|
|
513
|
+
body.push(simpleTable(
|
|
514
|
+
[["Severity", "Count"]],
|
|
515
|
+
reportModel.severitySummary.map((item) => [item.label, String(item.count)])
|
|
516
|
+
));
|
|
517
|
+
body.push(paragraph("Findings by Category", "Heading3"));
|
|
518
|
+
body.push(simpleTable(
|
|
519
|
+
[["Category", "Finding Count"]],
|
|
520
|
+
reportModel.categorySummary.length
|
|
521
|
+
? reportModel.categorySummary.map((item) => [item.category, String(item.count)])
|
|
522
|
+
: [["No findings", "0"]]
|
|
523
|
+
));
|
|
524
|
+
body.push(paragraph("Findings by Review Domain", "Heading3"));
|
|
525
|
+
body.push(simpleTable(
|
|
526
|
+
[["Review Domain", "Finding Count"]],
|
|
527
|
+
reportModel.reviewDomainSummary.length
|
|
528
|
+
? reportModel.reviewDomainSummary.map((item) => [item.domain, String(item.count)])
|
|
529
|
+
: [["No findings", "0"]]
|
|
530
|
+
));
|
|
531
|
+
|
|
532
|
+
body.push(paragraph("2. Findings Overview", "Heading2"));
|
|
533
|
+
if (reportModel.groupedOverview.length === 0) {
|
|
534
|
+
body.push(paragraph("No findings are present in the current visible set."));
|
|
535
|
+
} else {
|
|
536
|
+
for (const group of reportModel.groupedOverview) {
|
|
537
|
+
body.push(paragraph(group.label, "Heading3"));
|
|
538
|
+
body.push(simpleTable(
|
|
539
|
+
[["Severity", "Category", "Title", "File / Line", "Remediation Summary"]],
|
|
540
|
+
group.findings.map((finding) => [
|
|
541
|
+
finding.severityDisplay,
|
|
542
|
+
finding.category,
|
|
543
|
+
finding.title,
|
|
544
|
+
finding.overviewLocationDisplay,
|
|
545
|
+
finding.remediationSummary
|
|
546
|
+
])
|
|
547
|
+
));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
body.push(paragraph("3. Detailed Findings", "Heading2"));
|
|
552
|
+
body.push(paragraph("The following section provides full detail for each finding, including evidence and specific remediation guidance."));
|
|
553
|
+
if (reportModel.detailedFindings.length === 0) {
|
|
554
|
+
body.push(paragraph("No detailed findings to display."));
|
|
555
|
+
} else {
|
|
556
|
+
reportModel.detailedFindings.forEach((group, groupIndex) => {
|
|
557
|
+
body.push(paragraph(`3.${groupIndex + 1} ${group.label}`, "Heading3"));
|
|
558
|
+
group.findings.forEach((finding, index) => {
|
|
559
|
+
body.push(paragraph(`${index + 1}. ${finding.title}`));
|
|
560
|
+
body.push(keyValueParagraph("Severity", finding.severityDisplay));
|
|
561
|
+
body.push(keyValueParagraph("Category", finding.category));
|
|
562
|
+
body.push(keyValueParagraph("Subcategory", finding.subcategory));
|
|
563
|
+
body.push(keyValueParagraph("Review Domain", finding.reviewDomain));
|
|
564
|
+
body.push(keyValueParagraph("Confidence", finding.confidence));
|
|
565
|
+
body.push(keyValueParagraph("Finding ID", finding.id));
|
|
566
|
+
body.push(keyValueParagraph("File / Location", finding.locationDisplay));
|
|
567
|
+
body.push(keyValueParagraph("Source", finding.source));
|
|
568
|
+
body.push(keyValueParagraph("Evidence", finding.evidenceDisplay));
|
|
569
|
+
body.push(keyValueParagraph("Why It Matters", finding.whyItMatters));
|
|
570
|
+
body.push(keyValueParagraph("Remediation", finding.remediation || "No remediation provided."));
|
|
571
|
+
body.push(keyValueParagraph("Suggestion", finding.suggestion));
|
|
572
|
+
body.push(keyValueParagraph("Standards", finding.standardsDisplay));
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
body.push(paragraph("4. Recommendations Summary", "Heading2"));
|
|
578
|
+
body.push(simpleTable(
|
|
579
|
+
[["Category", "Recommendation", "Finding Count"]],
|
|
580
|
+
reportModel.recommendationsSummary.length
|
|
581
|
+
? reportModel.recommendationsSummary.map((item) => [item.category, item.suggestion, String(item.count)])
|
|
582
|
+
: [["No recommendations", "n/a", "0"]]
|
|
583
|
+
));
|
|
584
|
+
|
|
585
|
+
body.push(paragraph("5. Methodology, Limitations, and Scan Configuration", "Heading2"));
|
|
586
|
+
body.push(keyValueParagraph("Methodology", reportModel.methodology.notes));
|
|
587
|
+
body.push(keyValueParagraph("Limitations", reportModel.methodology.limitations));
|
|
588
|
+
body.push(keyValueParagraph("Scan Configuration", reportModel.methodology.configuration));
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
592
|
+
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' +
|
|
593
|
+
`<w:body>${body.join("")}<w:sectPr/></w:body>` +
|
|
594
|
+
"</w:document>"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function paragraph(text, styleId) {
|
|
599
|
+
const style = styleId ? `<w:pPr><w:pStyle w:val="${styleId}"/></w:pPr>` : "";
|
|
600
|
+
return `<w:p>${style}<w:r><w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r></w:p>`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function keyValueParagraph(label, value) {
|
|
604
|
+
return (
|
|
605
|
+
"<w:p>" +
|
|
606
|
+
'<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">' + escapeXml(`${label}: `) + "</w:t></w:r>" +
|
|
607
|
+
`<w:r><w:t xml:space="preserve">${escapeXml(value)}</w:t></w:r>` +
|
|
608
|
+
"</w:p>"
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function simpleTable(headerRows, dataRows) {
|
|
613
|
+
const rows = [...headerRows, ...dataRows].map((row, index) => (
|
|
614
|
+
`<w:tr>${row.map((cell) => tableCell(cell, index === 0)).join("")}</w:tr>`
|
|
615
|
+
)).join("");
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
'<w:tbl>' +
|
|
619
|
+
'<w:tblPr><w:tblBorders><w:top w:val="single" w:color="9AA9B8" w:sz="6"/><w:left w:val="single" w:color="9AA9B8" w:sz="6"/><w:bottom w:val="single" w:color="9AA9B8" w:sz="6"/><w:right w:val="single" w:color="9AA9B8" w:sz="6"/><w:insideH w:val="single" w:color="9AA9B8" w:sz="4"/><w:insideV w:val="single" w:color="9AA9B8" w:sz="4"/></w:tblBorders><w:tblCellMar><w:top w:w="60" w:type="dxa"/><w:left w:w="80" w:type="dxa"/><w:bottom w:w="60" w:type="dxa"/><w:right w:w="80" w:type="dxa"/></w:tblCellMar></w:tblPr>' +
|
|
620
|
+
rows +
|
|
621
|
+
"</w:tbl>"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function tableCell(text, isHeader) {
|
|
626
|
+
return (
|
|
627
|
+
"<w:tc><w:p><w:pPr><w:spacing w:after=\"40\"/></w:pPr><w:r>" +
|
|
628
|
+
(isHeader ? "<w:rPr><w:b/><w:bCs/><w:rFonts w:ascii=\"Arial\" w:cs=\"Arial\" w:eastAsia=\"Arial\" w:hAnsi=\"Arial\"/></w:rPr>" : "<w:rPr><w:rFonts w:ascii=\"Arial\" w:cs=\"Arial\" w:eastAsia=\"Arial\" w:hAnsi=\"Arial\"/></w:rPr>") +
|
|
629
|
+
`<w:t xml:space="preserve">${escapeXml(text)}</w:t>` +
|
|
630
|
+
"</w:r></w:p></w:tc>"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function escapeHtml(value) {
|
|
635
|
+
return String(value)
|
|
636
|
+
.replaceAll("&", "&")
|
|
637
|
+
.replaceAll("<", "<")
|
|
638
|
+
.replaceAll(">", ">")
|
|
639
|
+
.replaceAll('"', """);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function escapeXml(value) {
|
|
643
|
+
return escapeHtml(value).replaceAll("'", "'");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function xml(value) {
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function exportReport(reportModel) {
|
|
651
|
+
if (!vscode) {
|
|
652
|
+
throw new Error("Interactive report export is only available inside VS Code.");
|
|
653
|
+
}
|
|
654
|
+
const defaultName = slugify(reportModel.header.projectName || "secure-review-report");
|
|
655
|
+
const target = await vscode.window.showSaveDialog({
|
|
656
|
+
saveLabel: "Export Secure Review DOCX Report",
|
|
657
|
+
defaultUri: vscode.Uri.file(path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || "", `${defaultName}-security-report.docx`)),
|
|
658
|
+
filters: {
|
|
659
|
+
DOCX: ["docx"]
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (!target) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
await exportDocx(reportModel, target);
|
|
669
|
+
return true;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
const fallbackTarget = await vscode.window.showSaveDialog({
|
|
672
|
+
saveLabel: "DOCX export failed, save Markdown fallback",
|
|
673
|
+
defaultUri: vscode.Uri.file(path.join(vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || "", `${defaultName}-security-report.md`)),
|
|
674
|
+
filters: {
|
|
675
|
+
Markdown: ["md"]
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (!fallbackTarget) {
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
await vscode.workspace.fs.writeFile(fallbackTarget, Buffer.from(renderMarkdown(reportModel), "utf8"));
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderMarkdown(reportModel) {
|
|
689
|
+
const lines = [
|
|
690
|
+
`# ${reportModel.header.title}`,
|
|
691
|
+
"",
|
|
692
|
+
`Project: ${reportModel.header.projectName}`,
|
|
693
|
+
`Date: ${reportModel.header.reportDate}`,
|
|
694
|
+
`Scan Type: ${reportModel.header.scanType}`,
|
|
695
|
+
`Total Findings: ${reportModel.header.totalFindings}`,
|
|
696
|
+
"",
|
|
697
|
+
"## 1. Executive Summary",
|
|
698
|
+
"",
|
|
699
|
+
reportModel.executiveSummary,
|
|
700
|
+
"",
|
|
701
|
+
"## 2. Findings Overview",
|
|
702
|
+
""
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
for (const group of reportModel.groupedOverview) {
|
|
706
|
+
lines.push(`### ${group.label}`);
|
|
707
|
+
lines.push("");
|
|
708
|
+
for (const finding of group.findings) {
|
|
709
|
+
lines.push(`- [${finding.severityDisplay}] ${finding.title} | ${finding.overviewLocationDisplay} | ${finding.remediationSummary}`);
|
|
710
|
+
}
|
|
711
|
+
lines.push("");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
lines.push("## 3. Detailed Findings", "");
|
|
715
|
+
|
|
716
|
+
for (const group of reportModel.detailedFindings) {
|
|
717
|
+
lines.push(`### ${group.label}`, "");
|
|
718
|
+
for (const finding of group.findings) {
|
|
719
|
+
lines.push(`#### ${finding.title}`);
|
|
720
|
+
lines.push(`- Severity: ${finding.severityDisplay}`);
|
|
721
|
+
lines.push(`- Category: ${finding.category}`);
|
|
722
|
+
lines.push(`- Finding ID: ${finding.id}`);
|
|
723
|
+
lines.push(`- File / Location: ${finding.locationDisplay}`);
|
|
724
|
+
lines.push(`- Source: ${finding.source}`);
|
|
725
|
+
lines.push(`- Evidence: ${finding.evidenceDisplay}`);
|
|
726
|
+
lines.push(`- Remediation: ${finding.remediation || "No remediation provided."}`);
|
|
727
|
+
lines.push("");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
lines.push("## 4. Methodology, Limitations, and Scan Configuration", "");
|
|
732
|
+
lines.push(`- Methodology: ${reportModel.methodology.notes}`);
|
|
733
|
+
lines.push(`- Limitations: ${reportModel.methodology.limitations}`);
|
|
734
|
+
lines.push(`- Scan Configuration: ${reportModel.methodology.configuration}`);
|
|
735
|
+
|
|
736
|
+
return lines.join("\n");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function slugify(value) {
|
|
740
|
+
return String(value)
|
|
741
|
+
.toLowerCase()
|
|
742
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
743
|
+
.replace(/^-+|-+$/g, "") || "secure-review";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function buildZip(entries) {
|
|
747
|
+
const localFiles = [];
|
|
748
|
+
const centralRecords = [];
|
|
749
|
+
let offset = 0;
|
|
750
|
+
|
|
751
|
+
for (const entry of entries) {
|
|
752
|
+
const nameBuffer = Buffer.from(entry.name, "utf8");
|
|
753
|
+
const dataBuffer = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data);
|
|
754
|
+
const crc = crc32(dataBuffer);
|
|
755
|
+
|
|
756
|
+
const localHeader = Buffer.alloc(30);
|
|
757
|
+
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
758
|
+
localHeader.writeUInt16LE(20, 4);
|
|
759
|
+
localHeader.writeUInt16LE(0, 6);
|
|
760
|
+
localHeader.writeUInt16LE(0, 8);
|
|
761
|
+
localHeader.writeUInt16LE(0, 10);
|
|
762
|
+
localHeader.writeUInt16LE(0, 12);
|
|
763
|
+
localHeader.writeUInt32LE(crc >>> 0, 14);
|
|
764
|
+
localHeader.writeUInt32LE(dataBuffer.length, 18);
|
|
765
|
+
localHeader.writeUInt32LE(dataBuffer.length, 22);
|
|
766
|
+
localHeader.writeUInt16LE(nameBuffer.length, 26);
|
|
767
|
+
localHeader.writeUInt16LE(0, 28);
|
|
768
|
+
|
|
769
|
+
localFiles.push(localHeader, nameBuffer, dataBuffer);
|
|
770
|
+
|
|
771
|
+
const centralHeader = Buffer.alloc(46);
|
|
772
|
+
centralHeader.writeUInt32LE(0x02014b50, 0);
|
|
773
|
+
centralHeader.writeUInt16LE(20, 4);
|
|
774
|
+
centralHeader.writeUInt16LE(20, 6);
|
|
775
|
+
centralHeader.writeUInt16LE(0, 8);
|
|
776
|
+
centralHeader.writeUInt16LE(0, 10);
|
|
777
|
+
centralHeader.writeUInt16LE(0, 12);
|
|
778
|
+
centralHeader.writeUInt16LE(0, 14);
|
|
779
|
+
centralHeader.writeUInt32LE(crc >>> 0, 16);
|
|
780
|
+
centralHeader.writeUInt32LE(dataBuffer.length, 20);
|
|
781
|
+
centralHeader.writeUInt32LE(dataBuffer.length, 24);
|
|
782
|
+
centralHeader.writeUInt16LE(nameBuffer.length, 28);
|
|
783
|
+
centralHeader.writeUInt16LE(0, 30);
|
|
784
|
+
centralHeader.writeUInt16LE(0, 32);
|
|
785
|
+
centralHeader.writeUInt16LE(0, 34);
|
|
786
|
+
centralHeader.writeUInt16LE(0, 36);
|
|
787
|
+
centralHeader.writeUInt32LE(0, 38);
|
|
788
|
+
centralHeader.writeUInt32LE(offset, 42);
|
|
789
|
+
|
|
790
|
+
centralRecords.push(centralHeader, nameBuffer);
|
|
791
|
+
offset += localHeader.length + nameBuffer.length + dataBuffer.length;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const centralDirectory = Buffer.concat(centralRecords);
|
|
795
|
+
const end = Buffer.alloc(22);
|
|
796
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
797
|
+
end.writeUInt16LE(0, 4);
|
|
798
|
+
end.writeUInt16LE(0, 6);
|
|
799
|
+
end.writeUInt16LE(entries.length, 8);
|
|
800
|
+
end.writeUInt16LE(entries.length, 10);
|
|
801
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
802
|
+
end.writeUInt32LE(offset, 16);
|
|
803
|
+
end.writeUInt16LE(0, 20);
|
|
804
|
+
|
|
805
|
+
return Buffer.concat([...localFiles, centralDirectory, end]);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const CRC_TABLE = buildCrcTable();
|
|
809
|
+
|
|
810
|
+
function buildCrcTable() {
|
|
811
|
+
const table = [];
|
|
812
|
+
for (let i = 0; i < 256; i += 1) {
|
|
813
|
+
let c = i;
|
|
814
|
+
for (let j = 0; j < 8; j += 1) {
|
|
815
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
816
|
+
}
|
|
817
|
+
table.push(c >>> 0);
|
|
818
|
+
}
|
|
819
|
+
return table;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function crc32(buffer) {
|
|
823
|
+
let crc = 0 ^ (-1);
|
|
824
|
+
for (const byte of buffer) {
|
|
825
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 0xff];
|
|
826
|
+
}
|
|
827
|
+
return (crc ^ (-1)) >>> 0;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
module.exports = {
|
|
831
|
+
buildReportModel,
|
|
832
|
+
renderReportHtml,
|
|
833
|
+
renderMarkdown,
|
|
834
|
+
exportDocx,
|
|
835
|
+
exportReport,
|
|
836
|
+
promptReportMetadata
|
|
837
|
+
};
|