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
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { execFile } = require("node:child_process");
|
|
4
|
+
const { hashFinding } = require("../utils");
|
|
5
|
+
|
|
6
|
+
function createScannerRegistry(config, workspaceProfile) {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
id: "semgrep",
|
|
10
|
+
enabled: config.get("enableSemgrep", true),
|
|
11
|
+
applies: () => workspaceProfile.languages.length > 0,
|
|
12
|
+
run: () => runSemgrep(workspaceProfile)
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "eslint",
|
|
16
|
+
enabled: config.get("enableEslint", true),
|
|
17
|
+
applies: () => hasLanguage(workspaceProfile, ["javascript", "typescript"]) || hasFramework(workspaceProfile, ["react", "nextjs", "vue", "angular", "svelte"]),
|
|
18
|
+
run: () => runEslint(workspaceProfile)
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "npm-audit",
|
|
22
|
+
enabled: config.get("enableNpmAudit", true),
|
|
23
|
+
applies: () => workspaceProfile.manifests["package.json"],
|
|
24
|
+
run: () => runNpmAudit(workspaceProfile)
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "bandit",
|
|
28
|
+
enabled: config.get("enableBandit", true),
|
|
29
|
+
applies: () => hasLanguage(workspaceProfile, ["python"]),
|
|
30
|
+
run: () => runBandit(workspaceProfile)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "pip-audit",
|
|
34
|
+
enabled: config.get("enablePipAudit", true),
|
|
35
|
+
applies: () => workspaceProfile.manifests["requirements.txt"] || workspaceProfile.manifests["pyproject.toml"] || workspaceProfile.manifests["Pipfile"],
|
|
36
|
+
run: () => runPipAudit(workspaceProfile)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "spotbugs",
|
|
40
|
+
enabled: config.get("enableSpotBugs", true),
|
|
41
|
+
applies: () => hasLanguage(workspaceProfile, ["java"]),
|
|
42
|
+
run: () => runSpotBugs(workspaceProfile)
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "gosec",
|
|
46
|
+
enabled: config.get("enableGosec", true),
|
|
47
|
+
applies: () => hasLanguage(workspaceProfile, ["go"]),
|
|
48
|
+
run: () => runGosec(workspaceProfile)
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "govulncheck",
|
|
52
|
+
enabled: config.get("enableGovulncheck", true),
|
|
53
|
+
applies: () => hasLanguage(workspaceProfile, ["go"]),
|
|
54
|
+
run: () => runGovulncheck(workspaceProfile)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "cargo-audit",
|
|
58
|
+
enabled: config.get("enableCargoAudit", true),
|
|
59
|
+
applies: () => hasLanguage(workspaceProfile, ["rust"]),
|
|
60
|
+
run: () => runCargoAudit(workspaceProfile)
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "clippy",
|
|
64
|
+
enabled: config.get("enableClippy", true),
|
|
65
|
+
applies: () => hasLanguage(workspaceProfile, ["rust"]),
|
|
66
|
+
run: () => runClippy(workspaceProfile)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "cppcheck",
|
|
70
|
+
enabled: config.get("enableCppcheck", true),
|
|
71
|
+
applies: () => hasLanguage(workspaceProfile, ["c", "cpp"]),
|
|
72
|
+
run: () => runCppcheck(workspaceProfile)
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runRegisteredScanners(config, workspaceProfile) {
|
|
78
|
+
if (!workspaceProfile.workspaceRoot) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const findings = [];
|
|
83
|
+
const registry = createScannerRegistry(config, workspaceProfile);
|
|
84
|
+
|
|
85
|
+
for (const scanner of registry) {
|
|
86
|
+
if (!scanner.enabled || !scanner.applies()) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
findings.push(...await safeRun(scanner.run));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function safeRun(run) {
|
|
97
|
+
return run().catch(() => []);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function execFileAsync(command, args, options = {}) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
execFile(command, args, { maxBuffer: 25 * 1024 * 1024, ...options }, (error, stdout, stderr) => {
|
|
103
|
+
resolve({
|
|
104
|
+
error,
|
|
105
|
+
stdout,
|
|
106
|
+
stderr,
|
|
107
|
+
code: typeof error?.code === "number" ? error.code : 0
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasLanguage(workspaceProfile, languages) {
|
|
114
|
+
return languages.some((language) => workspaceProfile.languages.includes(language));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasFramework(workspaceProfile, frameworks) {
|
|
118
|
+
return frameworks.some((framework) => workspaceProfile.frameworks.includes(framework));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runSemgrep(workspaceProfile) {
|
|
122
|
+
const result = await execFileAsync("semgrep", ["scan", "--config", "auto", "--json", workspaceProfile.workspaceRoot], {
|
|
123
|
+
cwd: workspaceProfile.workspaceRoot
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (result.error) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const payload = parseJson(result.stdout);
|
|
131
|
+
return (payload.results || []).map((item) => normalizeStaticFinding({
|
|
132
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
133
|
+
tool: "semgrep",
|
|
134
|
+
title: item.extra?.message || item.check_id || "Semgrep finding",
|
|
135
|
+
severity: mapSeverity(item.extra?.severity),
|
|
136
|
+
confidence: "high",
|
|
137
|
+
category: "Security",
|
|
138
|
+
subcategory: "Semgrep",
|
|
139
|
+
reviewDomain: "security",
|
|
140
|
+
relativePath: item.path || "",
|
|
141
|
+
line: item.start?.line || 1,
|
|
142
|
+
column: item.start?.col || 1,
|
|
143
|
+
code: item.check_id || "semgrep",
|
|
144
|
+
message: item.extra?.message || "Semgrep finding",
|
|
145
|
+
evidence: item.extra?.lines || "",
|
|
146
|
+
remediation: "Review the flagged code path and apply the guidance behind the triggered Semgrep rule.",
|
|
147
|
+
suggestion: "Validate exploitability, then remediate the underlying pattern and add regression coverage if needed.",
|
|
148
|
+
whyItMatters: "Semgrep detected a code pattern associated with known security or correctness risks.",
|
|
149
|
+
standards: ensureArray(item.extra?.metadata?.cwe || item.extra?.metadata?.owasp || ["Semgrep"])
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runEslint(workspaceProfile) {
|
|
154
|
+
const result = await execFileAsync("eslint", ["-f", "json", "."], {
|
|
155
|
+
cwd: workspaceProfile.workspaceRoot
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (result.error && !result.stdout) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const payload = parseJson(result.stdout);
|
|
163
|
+
if (!Array.isArray(payload)) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return payload.flatMap((file) => (file.messages || []).map((message) => {
|
|
168
|
+
const ruleId = message.ruleId || "eslint";
|
|
169
|
+
const lowerRule = ruleId.toLowerCase();
|
|
170
|
+
const reviewDomain = /(security|xss|no-eval|detect|react|jsx-a11y)/.test(lowerRule)
|
|
171
|
+
? "security"
|
|
172
|
+
: /(complexity|maintainability|style)/.test(lowerRule)
|
|
173
|
+
? "maintainability"
|
|
174
|
+
: "code-quality";
|
|
175
|
+
|
|
176
|
+
return normalizeStaticFinding({
|
|
177
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
178
|
+
tool: "eslint",
|
|
179
|
+
title: message.message || "ESLint finding",
|
|
180
|
+
severity: mapEslintSeverity(message.severity, lowerRule),
|
|
181
|
+
confidence: "medium",
|
|
182
|
+
category: reviewDomain === "security" ? "Security" : reviewDomain === "maintainability" ? "Maintainability" : "Code Quality",
|
|
183
|
+
subcategory: hasFramework(workspaceProfile, ["react", "nextjs"]) ? "ESLint / Frontend" : "ESLint",
|
|
184
|
+
reviewDomain,
|
|
185
|
+
relativePath: relativize(workspaceProfile.workspaceRoot, file.filePath),
|
|
186
|
+
line: message.line || 1,
|
|
187
|
+
column: message.column || 1,
|
|
188
|
+
code: ruleId,
|
|
189
|
+
message: message.message || "ESLint finding",
|
|
190
|
+
evidence: message.source || "",
|
|
191
|
+
remediation: "Update the flagged code so it complies with the rule and project standards.",
|
|
192
|
+
suggestion: "Use the ESLint rule details to align the implementation with safer and more maintainable patterns.",
|
|
193
|
+
whyItMatters: "Lint findings often point to correctness, maintainability, accessibility, or security issues before they become defects.",
|
|
194
|
+
standards: ["ESLint"]
|
|
195
|
+
});
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function runNpmAudit(workspaceProfile) {
|
|
200
|
+
if (!fs.existsSync(path.join(workspaceProfile.workspaceRoot, "package.json"))) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = await execFileAsync("npm", ["audit", "--json"], { cwd: workspaceProfile.workspaceRoot });
|
|
205
|
+
const payload = parseJson(result.stdout || result.stderr);
|
|
206
|
+
const vulnerabilities = payload.vulnerabilities || {};
|
|
207
|
+
|
|
208
|
+
return Object.entries(vulnerabilities).map(([name, vulnerability]) => normalizeStaticFinding({
|
|
209
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
210
|
+
tool: "npm-audit",
|
|
211
|
+
title: `Vulnerable dependency: ${name}`,
|
|
212
|
+
severity: mapSeverity(vulnerability.severity),
|
|
213
|
+
confidence: "high",
|
|
214
|
+
category: "Dependency Risk",
|
|
215
|
+
subcategory: "npm audit",
|
|
216
|
+
reviewDomain: "dependency-risk",
|
|
217
|
+
relativePath: "package.json",
|
|
218
|
+
line: 1,
|
|
219
|
+
column: 1,
|
|
220
|
+
code: "npm-audit",
|
|
221
|
+
message: vulnerability.title || `npm audit reported a vulnerability in ${name}.`,
|
|
222
|
+
evidence: `${name} via ${Array.isArray(vulnerability.via) ? vulnerability.via.join(", ") : vulnerability.via || "transitive path unknown"}`,
|
|
223
|
+
remediation: vulnerability.fixAvailable ? "Apply the recommended package update or remediation path." : "Review the dependency manually and upgrade or replace it with a patched alternative.",
|
|
224
|
+
suggestion: "Pin to a safe version and re-run dependency review after remediation.",
|
|
225
|
+
whyItMatters: "Known vulnerable dependencies can expose exploitable flaws even when application code looks safe.",
|
|
226
|
+
standards: ["Dependency Audit"]
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runBandit(workspaceProfile) {
|
|
231
|
+
const result = await execFileAsync("bandit", ["-r", ".", "-f", "json"], {
|
|
232
|
+
cwd: workspaceProfile.workspaceRoot
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (result.error && !result.stdout) {
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const payload = parseJson(result.stdout);
|
|
240
|
+
return (payload.results || []).map((item) => normalizeStaticFinding({
|
|
241
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
242
|
+
tool: "bandit",
|
|
243
|
+
title: item.test_name || item.issue_text || "Bandit finding",
|
|
244
|
+
severity: mapSeverity(item.issue_severity),
|
|
245
|
+
confidence: mapSeverity(item.issue_confidence),
|
|
246
|
+
category: "Security",
|
|
247
|
+
subcategory: "Bandit",
|
|
248
|
+
reviewDomain: "security",
|
|
249
|
+
relativePath: relativize(workspaceProfile.workspaceRoot, item.filename),
|
|
250
|
+
line: item.line_number || 1,
|
|
251
|
+
column: 1,
|
|
252
|
+
code: item.test_id || "bandit",
|
|
253
|
+
message: item.issue_text || item.test_name || "Bandit finding",
|
|
254
|
+
evidence: item.code || "",
|
|
255
|
+
remediation: "Review the Python code path and replace the unsafe pattern with a safer equivalent.",
|
|
256
|
+
suggestion: "Use the Bandit test guidance to harden the implementation and add regression tests where appropriate.",
|
|
257
|
+
whyItMatters: "Bandit identified a Python pattern commonly associated with exploitable security issues.",
|
|
258
|
+
standards: ensureArray(item.more_info ? [item.more_info] : ["Bandit"])
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function runPipAudit(workspaceProfile) {
|
|
263
|
+
const result = await execFileAsync("pip-audit", ["-f", "json"], {
|
|
264
|
+
cwd: workspaceProfile.workspaceRoot
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (result.error && !result.stdout) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const payload = parseJson(result.stdout);
|
|
272
|
+
return (payload.dependencies || []).flatMap((dependency) => (dependency.vulns || []).map((vulnerability) => normalizeStaticFinding({
|
|
273
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
274
|
+
tool: "pip-audit",
|
|
275
|
+
title: `Vulnerable Python dependency: ${dependency.name}`,
|
|
276
|
+
severity: "high",
|
|
277
|
+
confidence: "high",
|
|
278
|
+
category: "Dependency Risk",
|
|
279
|
+
subcategory: "pip-audit",
|
|
280
|
+
reviewDomain: "dependency-risk",
|
|
281
|
+
relativePath: "requirements.txt",
|
|
282
|
+
line: 1,
|
|
283
|
+
column: 1,
|
|
284
|
+
code: "pip-audit",
|
|
285
|
+
message: vulnerability.description || vulnerability.id || "pip-audit finding",
|
|
286
|
+
evidence: `${dependency.name} ${dependency.version || ""}`.trim(),
|
|
287
|
+
remediation: "Upgrade the affected package to a patched version and retest the environment.",
|
|
288
|
+
suggestion: "Pin Python dependencies and add routine vulnerability review to dependency management workflows.",
|
|
289
|
+
whyItMatters: "Known vulnerable Python packages can introduce exploitable behavior through direct or transitive use.",
|
|
290
|
+
standards: [vulnerability.id || "pip-audit"]
|
|
291
|
+
})));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function runSpotBugs(workspaceProfile) {
|
|
295
|
+
const targetPath = findExistingPath(workspaceProfile.workspaceRoot, [
|
|
296
|
+
"target/classes",
|
|
297
|
+
"build/classes/java/main",
|
|
298
|
+
"build/classes",
|
|
299
|
+
"out/production"
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
if (!targetPath) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const result = await execFileAsync("spotbugs", ["-textui", "-xml:withMessages", targetPath], {
|
|
307
|
+
cwd: workspaceProfile.workspaceRoot
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (result.error && !result.stdout) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const xml = result.stdout || "";
|
|
315
|
+
const bugBlocks = [...xml.matchAll(/<BugInstance[\s\S]*?<\/BugInstance>/g)];
|
|
316
|
+
|
|
317
|
+
return bugBlocks.map((match) => {
|
|
318
|
+
const block = match[0];
|
|
319
|
+
const sourcePath = capture(block, /sourcepath="([^"]+)"/);
|
|
320
|
+
const message = capture(block, /<ShortMessage>([\s\S]*?)<\/ShortMessage>/) || "SpotBugs finding";
|
|
321
|
+
const longMessage = capture(block, /<LongMessage>([\s\S]*?)<\/LongMessage>/) || message;
|
|
322
|
+
const type = capture(block, /type="([^"]+)"/) || "spotbugs";
|
|
323
|
+
const priority = capture(block, /priority="([^"]+)"/);
|
|
324
|
+
const line = Number(capture(block, /start="(\d+)"/) || 1);
|
|
325
|
+
|
|
326
|
+
return normalizeStaticFinding({
|
|
327
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
328
|
+
tool: "spotbugs",
|
|
329
|
+
title: message,
|
|
330
|
+
severity: mapSpotbugsPriority(priority),
|
|
331
|
+
confidence: "medium",
|
|
332
|
+
category: "Security",
|
|
333
|
+
subcategory: "SpotBugs",
|
|
334
|
+
reviewDomain: "security",
|
|
335
|
+
relativePath: sourcePath || "Java bytecode analysis",
|
|
336
|
+
line,
|
|
337
|
+
column: 1,
|
|
338
|
+
code: type,
|
|
339
|
+
message: longMessage,
|
|
340
|
+
evidence: longMessage,
|
|
341
|
+
remediation: "Inspect the affected Java class and address the unsafe pattern identified by SpotBugs.",
|
|
342
|
+
suggestion: "Use this bytecode-level finding to verify whether the issue is real in the compiled code path.",
|
|
343
|
+
whyItMatters: "SpotBugs detected a pattern in compiled Java classes that may not be obvious from simple text inspection.",
|
|
344
|
+
standards: ["SpotBugs"]
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function runGosec(workspaceProfile) {
|
|
350
|
+
const result = await execFileAsync("gosec", ["-fmt=json", "./..."], {
|
|
351
|
+
cwd: workspaceProfile.workspaceRoot
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (result.error && !result.stdout) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const payload = parseJson(result.stdout);
|
|
359
|
+
return (payload.Issues || []).map((item) => normalizeStaticFinding({
|
|
360
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
361
|
+
tool: "gosec",
|
|
362
|
+
title: item.rule_id || item.details || "gosec finding",
|
|
363
|
+
severity: mapSeverity(item.severity),
|
|
364
|
+
confidence: mapSeverity(item.confidence),
|
|
365
|
+
category: "Security",
|
|
366
|
+
subcategory: "gosec",
|
|
367
|
+
reviewDomain: "security",
|
|
368
|
+
relativePath: relativize(workspaceProfile.workspaceRoot, item.file),
|
|
369
|
+
line: item.line || 1,
|
|
370
|
+
column: item.column || 1,
|
|
371
|
+
code: item.rule_id || "gosec",
|
|
372
|
+
message: item.details || item.rule_id || "gosec finding",
|
|
373
|
+
evidence: item.code || "",
|
|
374
|
+
remediation: "Refactor the Go code path to remove the unsafe pattern identified by gosec.",
|
|
375
|
+
suggestion: "Validate the issue in context, then harden the implementation and add tests where feasible.",
|
|
376
|
+
whyItMatters: "gosec identified a Go security pattern that commonly leads to exploitable vulnerabilities.",
|
|
377
|
+
standards: ["gosec"]
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function runGovulncheck(workspaceProfile) {
|
|
382
|
+
const result = await execFileAsync("govulncheck", ["-json", "./..."], {
|
|
383
|
+
cwd: workspaceProfile.workspaceRoot
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (result.error && !result.stdout && !result.stderr) {
|
|
387
|
+
return [];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const lines = `${result.stdout}\n${result.stderr}`.split(/\r?\n/).filter(Boolean);
|
|
391
|
+
const findings = [];
|
|
392
|
+
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
const entry = parseJson(line);
|
|
395
|
+
const vulnerability = entry?.finding || entry?.vulnerability;
|
|
396
|
+
if (!vulnerability) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const osv = vulnerability.osv || vulnerability;
|
|
401
|
+
const id = osv.id || vulnerability.osv || "govulncheck";
|
|
402
|
+
const summary = osv.summary || vulnerability.trace?.[0]?.module || "Go vulnerability finding";
|
|
403
|
+
findings.push(normalizeStaticFinding({
|
|
404
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
405
|
+
tool: "govulncheck",
|
|
406
|
+
title: `Go vulnerability: ${id}`,
|
|
407
|
+
severity: "high",
|
|
408
|
+
confidence: "high",
|
|
409
|
+
category: "Dependency Risk",
|
|
410
|
+
subcategory: "govulncheck",
|
|
411
|
+
reviewDomain: "dependency-risk",
|
|
412
|
+
relativePath: "go.mod",
|
|
413
|
+
line: 1,
|
|
414
|
+
column: 1,
|
|
415
|
+
code: "govulncheck",
|
|
416
|
+
message: summary,
|
|
417
|
+
evidence: id,
|
|
418
|
+
remediation: "Upgrade the affected module to a non-vulnerable version and validate transitive exposure.",
|
|
419
|
+
suggestion: "Use the vulnerability identifier to review impacted call paths and prioritize patching.",
|
|
420
|
+
whyItMatters: "Known vulnerable Go modules can be exploitable depending on how the affected packages are used.",
|
|
421
|
+
standards: [id]
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return findings;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function runCargoAudit(workspaceProfile) {
|
|
429
|
+
const result = await execFileAsync("cargo", ["audit", "--json"], {
|
|
430
|
+
cwd: workspaceProfile.workspaceRoot
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (result.error && !result.stdout) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const payload = parseJson(result.stdout);
|
|
438
|
+
const advisories = payload.vulnerabilities?.list || [];
|
|
439
|
+
|
|
440
|
+
return advisories.map((item) => normalizeStaticFinding({
|
|
441
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
442
|
+
tool: "cargo-audit",
|
|
443
|
+
title: `Rust advisory: ${item.advisory?.id || item.package?.name || "cargo-audit"}`,
|
|
444
|
+
severity: "high",
|
|
445
|
+
confidence: "high",
|
|
446
|
+
category: "Dependency Risk",
|
|
447
|
+
subcategory: "cargo audit",
|
|
448
|
+
reviewDomain: "dependency-risk",
|
|
449
|
+
relativePath: "Cargo.lock",
|
|
450
|
+
line: 1,
|
|
451
|
+
column: 1,
|
|
452
|
+
code: "cargo-audit",
|
|
453
|
+
message: item.advisory?.title || item.advisory?.description || "cargo-audit finding",
|
|
454
|
+
evidence: `${item.package?.name || "package"} ${item.package?.version || ""}`.trim(),
|
|
455
|
+
remediation: "Update the affected Rust crate to a patched version and re-run dependency review.",
|
|
456
|
+
suggestion: "Review the advisory details, impacted crate versions, and lockfile drift before release.",
|
|
457
|
+
whyItMatters: "Known vulnerable Rust crates can undermine otherwise memory-safe application code.",
|
|
458
|
+
standards: ensureArray(item.advisory?.id ? [item.advisory.id] : ["cargo-audit"])
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function runClippy(workspaceProfile) {
|
|
463
|
+
const result = await execFileAsync("cargo", ["clippy", "--message-format=json", "--all-targets", "--all-features"], {
|
|
464
|
+
cwd: workspaceProfile.workspaceRoot
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (result.error && !result.stdout && !result.stderr) {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const lines = `${result.stdout}\n${result.stderr}`.split(/\r?\n/).filter(Boolean);
|
|
472
|
+
const findings = [];
|
|
473
|
+
|
|
474
|
+
for (const line of lines) {
|
|
475
|
+
const entry = parseJson(line);
|
|
476
|
+
if (entry.reason !== "compiler-message" || !entry.message) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const level = entry.message.level || "warning";
|
|
481
|
+
if (level !== "warning" && level !== "error") {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const span = entry.message.spans?.find((item) => item.is_primary) || entry.message.spans?.[0];
|
|
486
|
+
findings.push(normalizeStaticFinding({
|
|
487
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
488
|
+
tool: "clippy",
|
|
489
|
+
title: entry.message.code?.code || entry.message.message || "clippy finding",
|
|
490
|
+
severity: level === "error" ? "high" : "medium",
|
|
491
|
+
confidence: "medium",
|
|
492
|
+
category: "Code Quality",
|
|
493
|
+
subcategory: "clippy",
|
|
494
|
+
reviewDomain: "code-quality",
|
|
495
|
+
relativePath: span?.file_name || "Rust workspace",
|
|
496
|
+
line: span?.line_start || 1,
|
|
497
|
+
column: span?.column_start || 1,
|
|
498
|
+
code: entry.message.code?.code || "clippy",
|
|
499
|
+
message: entry.message.message || "clippy finding",
|
|
500
|
+
evidence: span?.text?.map((item) => item.text).join(" ") || "",
|
|
501
|
+
remediation: "Refactor the Rust code path according to the clippy guidance and project conventions.",
|
|
502
|
+
suggestion: "Treat clippy warnings in critical modules as opportunities to reduce correctness and maintainability risk.",
|
|
503
|
+
whyItMatters: "Rust lints can expose subtle correctness and maintainability issues before they turn into bugs.",
|
|
504
|
+
standards: ["clippy"]
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return findings;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function runCppcheck(workspaceProfile) {
|
|
512
|
+
const result = await execFileAsync("cppcheck", ["--xml", "--xml-version=2", "--enable=warning,style,performance,portability,information,unusedFunction", "."], {
|
|
513
|
+
cwd: workspaceProfile.workspaceRoot
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const xml = result.stderr || result.stdout;
|
|
517
|
+
if (!xml) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const errors = [...xml.matchAll(/<error\b([^>]*)>([\s\S]*?)<\/error>/g)];
|
|
522
|
+
return errors.map((match) => {
|
|
523
|
+
const attrs = match[1];
|
|
524
|
+
const block = match[2];
|
|
525
|
+
const filePath = capture(block, /<location[^>]*file="([^"]+)"/);
|
|
526
|
+
const line = Number(capture(block, /<location[^>]*line="([^"]+)"/) || 1);
|
|
527
|
+
const column = Number(capture(block, /<location[^>]*column="([^"]+)"/) || 1);
|
|
528
|
+
const id = capture(attrs, /id="([^"]+)"/) || "cppcheck";
|
|
529
|
+
const severity = capture(attrs, /severity="([^"]+)"/) || "warning";
|
|
530
|
+
const message = capture(attrs, /msg="([^"]+)"/) || capture(attrs, /verbose="([^"]+)"/) || "cppcheck finding";
|
|
531
|
+
|
|
532
|
+
return normalizeStaticFinding({
|
|
533
|
+
workspaceRoot: workspaceProfile.workspaceRoot,
|
|
534
|
+
tool: "cppcheck",
|
|
535
|
+
title: message,
|
|
536
|
+
severity: mapCppcheckSeverity(severity),
|
|
537
|
+
confidence: "medium",
|
|
538
|
+
category: "Code Quality",
|
|
539
|
+
subcategory: "cppcheck",
|
|
540
|
+
reviewDomain: /(performance)/i.test(severity) ? "performance" : "code-quality",
|
|
541
|
+
relativePath: relativize(workspaceProfile.workspaceRoot, filePath || ""),
|
|
542
|
+
line,
|
|
543
|
+
column,
|
|
544
|
+
code: id,
|
|
545
|
+
message,
|
|
546
|
+
evidence: message,
|
|
547
|
+
remediation: "Refactor the affected C/C++ code path to address the static analysis warning.",
|
|
548
|
+
suggestion: "Validate whether the finding affects runtime safety, performance, or portability before release.",
|
|
549
|
+
whyItMatters: "C and C++ static analysis can surface memory-safety, portability, and correctness risks that are hard to catch manually.",
|
|
550
|
+
standards: ["cppcheck"]
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function normalizeStaticFinding(input) {
|
|
556
|
+
const relativePath = input.relativePath || "";
|
|
557
|
+
const absolutePath = input.filePath
|
|
558
|
+
|| (relativePath ? path.join(input.workspaceRoot, relativePath) : input.workspaceRoot);
|
|
559
|
+
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
id: hashFinding([input.tool, normalizedPath, String(input.line || 1), input.code || input.title]),
|
|
563
|
+
source: "static",
|
|
564
|
+
title: input.title,
|
|
565
|
+
severity: input.severity,
|
|
566
|
+
confidence: input.confidence || "medium",
|
|
567
|
+
category: input.category || "Security",
|
|
568
|
+
subcategory: input.subcategory || input.tool,
|
|
569
|
+
reviewDomain: input.reviewDomain || "security",
|
|
570
|
+
filePath: absolutePath,
|
|
571
|
+
relativePath: normalizedPath,
|
|
572
|
+
line: input.line || 1,
|
|
573
|
+
column: input.column || 1,
|
|
574
|
+
code: input.code || input.tool,
|
|
575
|
+
message: input.message || input.title,
|
|
576
|
+
evidence: input.evidence || "",
|
|
577
|
+
remediation: input.remediation || "Review and remediate the flagged issue.",
|
|
578
|
+
suggestion: input.suggestion || input.remediation || "Review and remediate the flagged issue.",
|
|
579
|
+
whyItMatters: input.whyItMatters || "This issue may increase security, correctness, reliability, or maintainability risk.",
|
|
580
|
+
standards: ensureArray(input.standards || [input.tool])
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function parseJson(raw) {
|
|
585
|
+
try {
|
|
586
|
+
return JSON.parse(raw || "");
|
|
587
|
+
} catch {
|
|
588
|
+
return {};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function ensureArray(value) {
|
|
593
|
+
return Array.isArray(value) ? value : [value];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function relativize(workspaceRoot, candidate) {
|
|
597
|
+
if (!candidate) {
|
|
598
|
+
return "";
|
|
599
|
+
}
|
|
600
|
+
return path.isAbsolute(candidate) ? path.relative(workspaceRoot, candidate) : candidate;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function findExistingPath(workspaceRoot, candidates) {
|
|
604
|
+
for (const candidate of candidates) {
|
|
605
|
+
const fullPath = path.join(workspaceRoot, candidate);
|
|
606
|
+
if (fs.existsSync(fullPath)) {
|
|
607
|
+
return fullPath;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function capture(text, pattern) {
|
|
614
|
+
return text.match(pattern)?.[1] || "";
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function mapSeverity(severity) {
|
|
618
|
+
const normalized = String(severity || "").toLowerCase();
|
|
619
|
+
if (normalized.includes("critical")) {
|
|
620
|
+
return "critical";
|
|
621
|
+
}
|
|
622
|
+
if (normalized.includes("high") || normalized === "error") {
|
|
623
|
+
return "high";
|
|
624
|
+
}
|
|
625
|
+
if (normalized.includes("moderate") || normalized.includes("medium")) {
|
|
626
|
+
return "medium";
|
|
627
|
+
}
|
|
628
|
+
return "low";
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function mapEslintSeverity(value, ruleId) {
|
|
632
|
+
if ((ruleId || "").includes("security") || (ruleId || "").includes("xss")) {
|
|
633
|
+
return "high";
|
|
634
|
+
}
|
|
635
|
+
if (Number(value) >= 2) {
|
|
636
|
+
return "medium";
|
|
637
|
+
}
|
|
638
|
+
return "low";
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function mapSpotbugsPriority(priority) {
|
|
642
|
+
const numeric = Number(priority || 0);
|
|
643
|
+
if (numeric === 1) {
|
|
644
|
+
return "high";
|
|
645
|
+
}
|
|
646
|
+
if (numeric === 2) {
|
|
647
|
+
return "medium";
|
|
648
|
+
}
|
|
649
|
+
return "low";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function mapCppcheckSeverity(value) {
|
|
653
|
+
const normalized = String(value || "").toLowerCase();
|
|
654
|
+
if (normalized.includes("error")) {
|
|
655
|
+
return "high";
|
|
656
|
+
}
|
|
657
|
+
if (normalized.includes("warning") || normalized.includes("performance")) {
|
|
658
|
+
return "medium";
|
|
659
|
+
}
|
|
660
|
+
return "low";
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
module.exports = {
|
|
664
|
+
runRegisteredScanners,
|
|
665
|
+
createScannerRegistry
|
|
666
|
+
};
|