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/extension.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
const vscode = require("vscode");
|
|
2
|
+
const { FindingsStore } = require("./src/store");
|
|
3
|
+
const { FindingsProvider } = require("./src/findings-provider");
|
|
4
|
+
const { scanWorkspace, scanCurrentFile } = require("./src/scanners/static-scan");
|
|
5
|
+
const { runDockerZapScan } = require("./src/scanners/dynamic-scan");
|
|
6
|
+
const { detectWorkspace, buildInstallPlan, formatInstallPlan } = require("./src/scanners/bootstrap-tools");
|
|
7
|
+
const { applyDiagnostics } = require("./src/diagnostics");
|
|
8
|
+
const { buildReportModel, renderReportHtml, exportReport, promptReportMetadata } = require("./src/report");
|
|
9
|
+
const { SecureReviewCodeActionProvider } = require("./src/code-actions");
|
|
10
|
+
const { severityOrder } = require("./src/constants");
|
|
11
|
+
|
|
12
|
+
let reportPanel;
|
|
13
|
+
let lastReportMetadata;
|
|
14
|
+
|
|
15
|
+
function activate(context) {
|
|
16
|
+
const store = new FindingsStore();
|
|
17
|
+
const diagnostics = vscode.languages.createDiagnosticCollection("secureReview");
|
|
18
|
+
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
|
19
|
+
statusBar.text = "$(shield) Secure Review: Ready";
|
|
20
|
+
statusBar.command = "secureReview.scanWorkspace";
|
|
21
|
+
statusBar.show();
|
|
22
|
+
|
|
23
|
+
const configAccessor = () => vscode.workspace.getConfiguration("secureReview");
|
|
24
|
+
const provider = new FindingsProvider(store, configAccessor);
|
|
25
|
+
|
|
26
|
+
context.subscriptions.push(
|
|
27
|
+
diagnostics,
|
|
28
|
+
statusBar,
|
|
29
|
+
vscode.window.registerTreeDataProvider("secureReview.findingsView", provider),
|
|
30
|
+
vscode.languages.registerCodeActionsProvider(
|
|
31
|
+
{ scheme: "file" },
|
|
32
|
+
new SecureReviewCodeActionProvider(store)
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function refreshUi() {
|
|
37
|
+
const minSeverity = configAccessor().get("minimumSeverity", "low");
|
|
38
|
+
const visibleFindings = store.getVisibleFindings(minSeverity, severityOrder);
|
|
39
|
+
applyDiagnostics(diagnostics, visibleFindings);
|
|
40
|
+
statusBar.text = `$(shield) Secure Review: ${visibleFindings.length} findings`;
|
|
41
|
+
|
|
42
|
+
if (reportPanel) {
|
|
43
|
+
const reportModel = buildReportModel(visibleFindings, getReportMetadata(store.lastRun));
|
|
44
|
+
reportPanel.webview.html = renderReportHtml(reportModel);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
store.onDidChange(refreshUi);
|
|
49
|
+
|
|
50
|
+
async function runWorkspaceScan() {
|
|
51
|
+
if (!vscode.workspace.workspaceFolders?.length) {
|
|
52
|
+
vscode.window.showWarningMessage("Open a workspace folder before running Secure Review.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await vscode.window.withProgress(
|
|
57
|
+
{
|
|
58
|
+
location: vscode.ProgressLocation.Notification,
|
|
59
|
+
title: "Secure Review: scanning workspace",
|
|
60
|
+
cancellable: false
|
|
61
|
+
},
|
|
62
|
+
async () => {
|
|
63
|
+
const findings = await scanWorkspace(configAccessor());
|
|
64
|
+
const dynamicFindings = store.findings.filter((item) => item.source === "dynamic");
|
|
65
|
+
store.setFindings(sortFindings([...findings, ...dynamicFindings]));
|
|
66
|
+
vscode.window.showInformationMessage(`Secure Review found ${store.findings.length} total findings.`);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runCurrentFileScan() {
|
|
72
|
+
const editor = vscode.window.activeTextEditor;
|
|
73
|
+
if (!editor) {
|
|
74
|
+
vscode.window.showWarningMessage("Open a file before running a current-file scan.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await vscode.window.withProgress(
|
|
79
|
+
{
|
|
80
|
+
location: vscode.ProgressLocation.Notification,
|
|
81
|
+
title: "Secure Review: scanning current file",
|
|
82
|
+
cancellable: false
|
|
83
|
+
},
|
|
84
|
+
async () => {
|
|
85
|
+
const currentFileFindings = await scanCurrentFile();
|
|
86
|
+
const otherStaticFindings = store.findings.filter(
|
|
87
|
+
(item) => item.source === "static" && item.filePath !== editor.document.uri.fsPath
|
|
88
|
+
);
|
|
89
|
+
const dynamicFindings = store.findings.filter((item) => item.source === "dynamic");
|
|
90
|
+
|
|
91
|
+
store.setFindings(sortFindings([...otherStaticFindings, ...currentFileFindings, ...dynamicFindings]));
|
|
92
|
+
vscode.window.showInformationMessage(`Secure Review updated findings for ${editor.document.fileName}.`);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runDynamicScan() {
|
|
98
|
+
const mode = configAccessor().get("dynamicScanMode", "baseline");
|
|
99
|
+
if (mode === "full") {
|
|
100
|
+
const choice = await vscode.window.showWarningMessage(
|
|
101
|
+
"ZAP full scan performs active attacks and should only be used against developer-controlled local or test applications.",
|
|
102
|
+
{ modal: true },
|
|
103
|
+
"Run Full Scan"
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (choice !== "Run Full Scan") {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await vscode.window.withProgress(
|
|
112
|
+
{
|
|
113
|
+
location: vscode.ProgressLocation.Notification,
|
|
114
|
+
title: `Secure Review: running Docker-based ZAP ${mode} scan`,
|
|
115
|
+
cancellable: false
|
|
116
|
+
},
|
|
117
|
+
async () => {
|
|
118
|
+
try {
|
|
119
|
+
const dynamicFindings = await runDockerZapScan(configAccessor());
|
|
120
|
+
const staticFindings = store.findings.filter((item) => item.source === "static");
|
|
121
|
+
store.setFindings(sortFindings([...staticFindings, ...dynamicFindings]));
|
|
122
|
+
vscode.window.showInformationMessage(`Secure Review dynamic scan completed with ${dynamicFindings.length} findings.`);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
vscode.window.showErrorMessage(`Dynamic scan failed: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function openReport() {
|
|
131
|
+
if (!reportPanel) {
|
|
132
|
+
reportPanel = vscode.window.createWebviewPanel(
|
|
133
|
+
"secureReview.report",
|
|
134
|
+
"Secure Review Report",
|
|
135
|
+
vscode.ViewColumn.Beside,
|
|
136
|
+
{ enableFindWidget: true }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
reportPanel.onDidDispose(() => {
|
|
140
|
+
reportPanel = undefined;
|
|
141
|
+
}, null, context.subscriptions);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const minSeverity = configAccessor().get("minimumSeverity", "low");
|
|
145
|
+
const visibleFindings = store.getVisibleFindings(minSeverity, severityOrder);
|
|
146
|
+
const reportModel = buildReportModel(visibleFindings, getReportMetadata(store.lastRun));
|
|
147
|
+
reportPanel.webview.html = renderReportHtml(reportModel);
|
|
148
|
+
reportPanel.reveal();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function exportFindingsReport() {
|
|
152
|
+
const minSeverity = configAccessor().get("minimumSeverity", "low");
|
|
153
|
+
const visibleFindings = store.getVisibleFindings(minSeverity, severityOrder);
|
|
154
|
+
const selection = await promptReportScope(visibleFindings);
|
|
155
|
+
if (!selection) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const scopedFindings = filterFindingsForReport(visibleFindings, selection.scope);
|
|
160
|
+
const metadata = await promptReportMetadata(selection.defaultScanType);
|
|
161
|
+
if (!metadata) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lastReportMetadata = metadata;
|
|
166
|
+
const reportModel = buildReportModel(
|
|
167
|
+
scopedFindings,
|
|
168
|
+
getReportMetadata(store.lastRun, selection.defaultScanType)
|
|
169
|
+
);
|
|
170
|
+
const saved = await exportReport(reportModel);
|
|
171
|
+
|
|
172
|
+
if (saved) {
|
|
173
|
+
vscode.window.showInformationMessage("Secure Review report exported.");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function clearFindings() {
|
|
178
|
+
store.clear();
|
|
179
|
+
diagnostics.clear();
|
|
180
|
+
vscode.window.showInformationMessage("Secure Review findings cleared.");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function inspectWorkspaceRequirements() {
|
|
184
|
+
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
185
|
+
if (!workspaceRoot) {
|
|
186
|
+
vscode.window.showWarningMessage("Open a workspace folder before checking Secure Review requirements.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const profile = detectWorkspace(workspaceRoot);
|
|
191
|
+
const plan = buildInstallPlan(profile);
|
|
192
|
+
const formatted = formatInstallPlan(profile, plan);
|
|
193
|
+
|
|
194
|
+
const document = await vscode.workspace.openTextDocument({
|
|
195
|
+
language: "markdown",
|
|
196
|
+
content: `# Secure Review Workspace Requirements\n\n\`\`\`text\n${formatted}\n\`\`\`\n`
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await vscode.window.showTextDocument(document, {
|
|
200
|
+
preview: false,
|
|
201
|
+
viewColumn: vscode.ViewColumn.Beside
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!plan.length) {
|
|
205
|
+
vscode.window.showInformationMessage("Secure Review did not find any additional language-specific scanner requirements for this workspace.");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
vscode.window.showInformationMessage(
|
|
210
|
+
`Secure Review identified ${plan.length} scanner tool requirement${plan.length === 1 ? "" : "s"} for this workspace.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function openFinding(finding) {
|
|
215
|
+
const targetFinding = finding?.id ? finding : store.getById(finding);
|
|
216
|
+
if (!targetFinding) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (targetFinding.filePath) {
|
|
221
|
+
const document = await vscode.workspace.openTextDocument(targetFinding.filePath);
|
|
222
|
+
const editor = await vscode.window.showTextDocument(document, { preview: false });
|
|
223
|
+
const line = Math.max(0, (targetFinding.line || 1) - 1);
|
|
224
|
+
const column = Math.max(0, (targetFinding.column || 1) - 1);
|
|
225
|
+
const position = new vscode.Position(line, column);
|
|
226
|
+
editor.selection = new vscode.Selection(position, position);
|
|
227
|
+
editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
vscode.window.showInformationMessage(
|
|
231
|
+
`${targetFinding.title}: ${targetFinding.remediation}`,
|
|
232
|
+
"Open Report"
|
|
233
|
+
).then((action) => {
|
|
234
|
+
if (action === "Open Report") {
|
|
235
|
+
openReport();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ignoreFinding(finding) {
|
|
241
|
+
const targetFinding = finding?.id ? finding : store.getById(finding);
|
|
242
|
+
if (!targetFinding) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
store.ignore(targetFinding.id);
|
|
247
|
+
vscode.window.showInformationMessage(`Ignored finding ${targetFinding.code}.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
context.subscriptions.push(
|
|
251
|
+
vscode.commands.registerCommand("secureReview.scanWorkspace", runWorkspaceScan),
|
|
252
|
+
vscode.commands.registerCommand("secureReview.inspectRequirements", inspectWorkspaceRequirements),
|
|
253
|
+
vscode.commands.registerCommand("secureReview.scanCurrentFile", runCurrentFileScan),
|
|
254
|
+
vscode.commands.registerCommand("secureReview.dynamicScan", runDynamicScan),
|
|
255
|
+
vscode.commands.registerCommand("secureReview.openReport", openReport),
|
|
256
|
+
vscode.commands.registerCommand("secureReview.exportReport", exportFindingsReport),
|
|
257
|
+
vscode.commands.registerCommand("secureReview.clearFindings", clearFindings),
|
|
258
|
+
vscode.commands.registerCommand("secureReview.openFinding", openFinding),
|
|
259
|
+
vscode.commands.registerCommand("secureReview.ignoreFinding", ignoreFinding)
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
context.subscriptions.push(
|
|
263
|
+
vscode.workspace.onDidSaveTextDocument(async () => {
|
|
264
|
+
if (configAccessor().get("runOnSave", false)) {
|
|
265
|
+
await runCurrentFileScan();
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
refreshUi();
|
|
271
|
+
|
|
272
|
+
function getReportMetadata(lastRun, defaultScanType) {
|
|
273
|
+
return {
|
|
274
|
+
projectName: lastReportMetadata?.projectName || vscode.workspace.workspaceFolders?.[0]?.name || "Workspace",
|
|
275
|
+
reportDate: lastReportMetadata?.reportDate || (lastRun || new Date()),
|
|
276
|
+
scanType: lastReportMetadata?.scanType || defaultScanType || (store.findings.some((item) => item.source === "dynamic") ? "Static + Dynamic Analysis" : "Static Analysis"),
|
|
277
|
+
notes: lastReportMetadata?.notes || "",
|
|
278
|
+
scanConfiguration: `Minimum Severity: ${configAccessor().get("minimumSeverity", "low")}; Run On Save: ${configAccessor().get("runOnSave", false) ? "Enabled" : "Disabled"}; Built-In Rules: ${configAccessor().get("enableBuiltInRules", true) ? "Enabled" : "Disabled"}; Semgrep: ${configAccessor().get("enableSemgrep", true) ? "Enabled" : "Disabled"}; ESLint: ${configAccessor().get("enableEslint", true) ? "Enabled" : "Disabled"}; Bandit: ${configAccessor().get("enableBandit", true) ? "Enabled" : "Disabled"}; npm audit: ${configAccessor().get("enableNpmAudit", true) ? "Enabled" : "Disabled"}; pip-audit: ${configAccessor().get("enablePipAudit", true) ? "Enabled" : "Disabled"}; SpotBugs: ${configAccessor().get("enableSpotBugs", true) ? "Enabled" : "Disabled"}; gosec: ${configAccessor().get("enableGosec", true) ? "Enabled" : "Disabled"}; govulncheck: ${configAccessor().get("enableGovulncheck", true) ? "Enabled" : "Disabled"}; cargo-audit: ${configAccessor().get("enableCargoAudit", true) ? "Enabled" : "Disabled"}; Clippy: ${configAccessor().get("enableClippy", true) ? "Enabled" : "Disabled"}; cppcheck: ${configAccessor().get("enableCppcheck", true) ? "Enabled" : "Disabled"}; Dynamic URL: ${configAccessor().get("dynamicBaseUrl", "http://127.0.0.1:3000")}; Dynamic Mode: ${configAccessor().get("dynamicScanMode", "baseline")}; Ajax Spider: ${configAccessor().get("dynamicUseAjaxSpider", false) ? "Enabled" : "Disabled"}`
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function promptReportScope(findings) {
|
|
284
|
+
const staticCount = findings.filter((finding) => finding.source !== "dynamic").length;
|
|
285
|
+
const dynamicCount = findings.filter((finding) => finding.source === "dynamic").length;
|
|
286
|
+
const options = [];
|
|
287
|
+
|
|
288
|
+
if (staticCount > 0 && dynamicCount > 0) {
|
|
289
|
+
options.push(
|
|
290
|
+
{
|
|
291
|
+
label: "Combined Report",
|
|
292
|
+
description: `${staticCount} static + ${dynamicCount} dynamic findings`,
|
|
293
|
+
scope: "combined",
|
|
294
|
+
defaultScanType: "Static + Dynamic Analysis"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
label: "Static-Only Report",
|
|
298
|
+
description: `${staticCount} static findings`,
|
|
299
|
+
scope: "static",
|
|
300
|
+
defaultScanType: "Static Analysis"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
label: "Dynamic-Only Report",
|
|
304
|
+
description: `${dynamicCount} dynamic findings`,
|
|
305
|
+
scope: "dynamic",
|
|
306
|
+
defaultScanType: "Dynamic Analysis"
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
} else if (dynamicCount > 0) {
|
|
310
|
+
options.push({
|
|
311
|
+
label: "Dynamic-Only Report",
|
|
312
|
+
description: `${dynamicCount} dynamic findings`,
|
|
313
|
+
scope: "dynamic",
|
|
314
|
+
defaultScanType: "Dynamic Analysis"
|
|
315
|
+
});
|
|
316
|
+
} else if (staticCount > 0) {
|
|
317
|
+
options.push({
|
|
318
|
+
label: "Static-Only Report",
|
|
319
|
+
description: `${staticCount} static findings`,
|
|
320
|
+
scope: "static",
|
|
321
|
+
defaultScanType: "Static Analysis"
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
vscode.window.showWarningMessage("Secure Review has no findings to export.");
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.length === 1) {
|
|
329
|
+
return options[0];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return vscode.window.showQuickPick(options, {
|
|
333
|
+
placeHolder: "Choose which findings to include in the exported report",
|
|
334
|
+
ignoreFocusOut: true
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function filterFindingsForReport(findings, scope) {
|
|
339
|
+
if (scope === "dynamic") {
|
|
340
|
+
return findings.filter((finding) => finding.source === "dynamic");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (scope === "static") {
|
|
344
|
+
return findings.filter((finding) => finding.source !== "dynamic");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return findings;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function sortFindings(findings) {
|
|
351
|
+
return [...findings].sort((left, right) => {
|
|
352
|
+
const severityDiff = severityOrder[right.severity] - severityOrder[left.severity];
|
|
353
|
+
if (severityDiff !== 0) {
|
|
354
|
+
return severityDiff;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const leftLocation = left.relativePath || "";
|
|
358
|
+
const rightLocation = right.relativePath || "";
|
|
359
|
+
return leftLocation.localeCompare(rightLocation);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function deactivate() {}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
activate,
|
|
367
|
+
deactivate
|
|
368
|
+
};
|
package/media/shield.png
ADDED
|
Binary file
|
package/media/shield.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="128" height="128" rx="28" fill="#12355B"/>
|
|
3
|
+
<path d="M64 18L96 30V54C96 77.5 82.4 99.2 64 110C45.6 99.2 32 77.5 32 54V30L64 18Z" fill="#F76707"/>
|
|
4
|
+
<path d="M64 29L87 37.5V53.6C87 71.3 77.1 87.9 64 96.2C50.9 87.9 41 71.3 41 53.6V37.5L64 29Z" fill="#FFF6E8"/>
|
|
5
|
+
<path d="M58 69.5L48 59.5L52.7 54.8L58 60.1L76.8 41.2L81.5 45.9L58 69.5Z" fill="#0B7B72"/>
|
|
6
|
+
</svg>
|