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,303 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const { execFileSync } = require("node:child_process");
|
|
5
|
+
|
|
6
|
+
function detectWorkspace(workspaceRoot) {
|
|
7
|
+
const manifestNames = new Set(walkFiles(workspaceRoot, 4).map((file) => path.relative(workspaceRoot, file)));
|
|
8
|
+
const languages = new Set();
|
|
9
|
+
const frameworks = new Set();
|
|
10
|
+
|
|
11
|
+
if (exists("package.json")) {
|
|
12
|
+
languages.add("javascript");
|
|
13
|
+
const packageJson = readJson(path.join(workspaceRoot, "package.json"));
|
|
14
|
+
const dependencies = {
|
|
15
|
+
...(packageJson.dependencies || {}),
|
|
16
|
+
...(packageJson.devDependencies || {}),
|
|
17
|
+
...(packageJson.peerDependencies || {})
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (dependencies.react) frameworks.add("react");
|
|
21
|
+
if (dependencies.next) frameworks.add("nextjs");
|
|
22
|
+
if (dependencies.vue) frameworks.add("vue");
|
|
23
|
+
if (dependencies["@angular/core"]) frameworks.add("angular");
|
|
24
|
+
if (dependencies.express) frameworks.add("express");
|
|
25
|
+
if (dependencies["@nestjs/core"]) frameworks.add("nestjs");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (exists("requirements.txt") || exists("pyproject.toml") || exists("Pipfile")) {
|
|
29
|
+
languages.add("python");
|
|
30
|
+
const pythonText = readTextIfExists("requirements.txt") || readTextIfExists("pyproject.toml") || "";
|
|
31
|
+
if (/django/i.test(pythonText)) frameworks.add("django");
|
|
32
|
+
if (/flask/i.test(pythonText)) frameworks.add("flask");
|
|
33
|
+
if (/fastapi/i.test(pythonText)) frameworks.add("fastapi");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (exists("go.mod")) {
|
|
37
|
+
languages.add("go");
|
|
38
|
+
const goMod = readTextIfExists("go.mod");
|
|
39
|
+
if (/gin-gonic\/gin/i.test(goMod)) frameworks.add("gin");
|
|
40
|
+
if (/labstack\/echo/i.test(goMod)) frameworks.add("echo");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (exists("Cargo.toml")) {
|
|
44
|
+
languages.add("rust");
|
|
45
|
+
const cargoToml = readTextIfExists("Cargo.toml");
|
|
46
|
+
if (/actix-web/i.test(cargoToml)) frameworks.add("actix-web");
|
|
47
|
+
if (/\baxum\b/i.test(cargoToml)) frameworks.add("axum");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (exists("pom.xml") || exists("build.gradle") || exists("build.gradle.kts")) {
|
|
51
|
+
languages.add("java");
|
|
52
|
+
const javaText = readTextIfExists("pom.xml") || readTextIfExists("build.gradle") || readTextIfExists("build.gradle.kts") || "";
|
|
53
|
+
if (/spring/i.test(javaText)) frameworks.add("spring");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (exists("CMakeLists.txt") || exists("Makefile")) {
|
|
57
|
+
languages.add("c");
|
|
58
|
+
languages.add("cpp");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const file of manifestNames) {
|
|
62
|
+
const ext = path.extname(file).toLowerCase();
|
|
63
|
+
if ([".c", ".h"].includes(ext)) languages.add("c");
|
|
64
|
+
if ([".cpp", ".cc", ".cxx", ".hpp", ".hh"].includes(ext)) languages.add("cpp");
|
|
65
|
+
if (ext === ".java") languages.add("java");
|
|
66
|
+
if (ext === ".py") languages.add("python");
|
|
67
|
+
if (ext === ".go") languages.add("go");
|
|
68
|
+
if (ext === ".rs") languages.add("rust");
|
|
69
|
+
if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(ext)) languages.add("javascript");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
languages: [...languages].sort(),
|
|
75
|
+
frameworks: [...frameworks].sort()
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function exists(relativePath) {
|
|
79
|
+
return fs.existsSync(path.join(workspaceRoot, relativePath));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readTextIfExists(relativePath) {
|
|
83
|
+
const fullPath = path.join(workspaceRoot, relativePath);
|
|
84
|
+
return fs.existsSync(fullPath) ? fs.readFileSync(fullPath, "utf8") : "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildInstallPlan(profile) {
|
|
89
|
+
const plan = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
|
|
92
|
+
addTool(plan, seen, {
|
|
93
|
+
tool: "Semgrep",
|
|
94
|
+
requiredFor: ["common multi-language scanning"],
|
|
95
|
+
install: resolvePythonInstall("semgrep"),
|
|
96
|
+
verify: ["semgrep", "--version"]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (hasAny(profile.languages, ["javascript"])) {
|
|
100
|
+
addTool(plan, seen, {
|
|
101
|
+
tool: "ESLint",
|
|
102
|
+
requiredFor: ["JavaScript / TypeScript / React"],
|
|
103
|
+
install: resolveNpmGlobalInstall(["eslint", "@typescript-eslint/parser", "@typescript-eslint/eslint-plugin", "eslint-plugin-react", "eslint-plugin-jsx-a11y", "eslint-plugin-security"]),
|
|
104
|
+
verify: ["eslint", "-v"]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (hasAny(profile.languages, ["python"])) {
|
|
109
|
+
addTool(plan, seen, {
|
|
110
|
+
tool: "Bandit",
|
|
111
|
+
requiredFor: ["Python"],
|
|
112
|
+
install: resolvePythonInstall("bandit"),
|
|
113
|
+
verify: ["bandit", "--version"]
|
|
114
|
+
});
|
|
115
|
+
addTool(plan, seen, {
|
|
116
|
+
tool: "pip-audit",
|
|
117
|
+
requiredFor: ["Python"],
|
|
118
|
+
install: resolvePythonInstall("pip-audit"),
|
|
119
|
+
verify: ["pip-audit", "--version"]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hasAny(profile.languages, ["java"])) {
|
|
124
|
+
addTool(plan, seen, {
|
|
125
|
+
tool: "SpotBugs",
|
|
126
|
+
requiredFor: ["Java"],
|
|
127
|
+
install: resolveSpotBugsInstall(),
|
|
128
|
+
verify: ["spotbugs", "-version"]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (hasAny(profile.languages, ["go"])) {
|
|
133
|
+
addTool(plan, seen, {
|
|
134
|
+
tool: "gosec",
|
|
135
|
+
requiredFor: ["Go"],
|
|
136
|
+
install: resolveGoInstall("github.com/securego/gosec/v2/cmd/gosec@latest"),
|
|
137
|
+
verify: ["gosec", "-version"]
|
|
138
|
+
});
|
|
139
|
+
addTool(plan, seen, {
|
|
140
|
+
tool: "govulncheck",
|
|
141
|
+
requiredFor: ["Go"],
|
|
142
|
+
install: resolveGoInstall("golang.org/x/vuln/cmd/govulncheck@latest"),
|
|
143
|
+
verify: ["govulncheck", "-version"]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (hasAny(profile.languages, ["rust"])) {
|
|
148
|
+
addTool(plan, seen, {
|
|
149
|
+
tool: "cargo-audit",
|
|
150
|
+
requiredFor: ["Rust"],
|
|
151
|
+
install: resolveCargoInstall("cargo-audit"),
|
|
152
|
+
verify: ["cargo", "audit", "--version"]
|
|
153
|
+
});
|
|
154
|
+
addTool(plan, seen, {
|
|
155
|
+
tool: "Clippy",
|
|
156
|
+
requiredFor: ["Rust"],
|
|
157
|
+
install: resolveRustupComponent("clippy"),
|
|
158
|
+
verify: ["cargo", "clippy", "--version"]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (hasAny(profile.languages, ["c", "cpp"])) {
|
|
163
|
+
addTool(plan, seen, {
|
|
164
|
+
tool: "cppcheck",
|
|
165
|
+
requiredFor: ["C / C++"],
|
|
166
|
+
install: resolveCppcheckInstall(),
|
|
167
|
+
verify: ["cppcheck", "--version"]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatInstallPlan(profile, plan) {
|
|
175
|
+
const lines = [
|
|
176
|
+
"Secure Review tool bootstrap",
|
|
177
|
+
`Workspace: ${profile.workspaceRoot}`,
|
|
178
|
+
`Languages: ${profile.languages.length ? profile.languages.join(", ") : "none detected"}`,
|
|
179
|
+
`Frameworks: ${profile.frameworks.length ? profile.frameworks.join(", ") : "none detected"}`,
|
|
180
|
+
""
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
if (!plan.length) {
|
|
184
|
+
lines.push("No language-specific scanner setup was required for this workspace.");
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push("Planned scanner tool setup:");
|
|
189
|
+
for (const item of plan) {
|
|
190
|
+
lines.push(`- ${item.tool} (${item.requiredFor.join(", ")})`);
|
|
191
|
+
if (item.install.kind === "command") {
|
|
192
|
+
lines.push(` Install: ${item.install.command.join(" ")}`);
|
|
193
|
+
} else if (item.install.kind === "already-installed") {
|
|
194
|
+
lines.push(" Install: already available");
|
|
195
|
+
} else {
|
|
196
|
+
lines.push(` Install: ${item.install.note}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(` Verify: ${item.verify.join(" ")}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function addTool(plan, seen, entry) {
|
|
205
|
+
const key = entry.tool.toLowerCase();
|
|
206
|
+
if (seen.has(key)) return;
|
|
207
|
+
seen.add(key);
|
|
208
|
+
plan.push(entry);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolvePythonInstall(packageName) {
|
|
212
|
+
if (hasCommand("pipx")) return { kind: "command", command: ["pipx", "install", packageName] };
|
|
213
|
+
if (hasCommand("python3")) return { kind: "command", command: ["python3", "-m", "pip", "install", "--user", packageName] };
|
|
214
|
+
return { kind: "manual", note: `Install ${packageName} with pipx or python3 -m pip.` };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveNpmGlobalInstall(packages) {
|
|
218
|
+
if (hasCommand("npm")) return { kind: "command", command: ["npm", "install", "-g", ...packages] };
|
|
219
|
+
return { kind: "manual", note: `Install ${packages.join(", ")} with npm.` };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveGoInstall(moduleRef) {
|
|
223
|
+
if (hasCommand("go")) return { kind: "command", command: ["go", "install", moduleRef] };
|
|
224
|
+
return { kind: "manual", note: `Install ${moduleRef} with Go installed and GOPATH/bin on PATH.` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveCargoInstall(crateName) {
|
|
228
|
+
if (hasCommand("cargo")) return { kind: "command", command: ["cargo", "install", crateName] };
|
|
229
|
+
return { kind: "manual", note: `Install ${crateName} with Cargo.` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveRustupComponent(component) {
|
|
233
|
+
if (hasCommand("rustup")) return { kind: "command", command: ["rustup", "component", "add", component] };
|
|
234
|
+
return { kind: "manual", note: `Install Rustup and add the ${component} component.` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveCppcheckInstall() {
|
|
238
|
+
if (hasCommand("cppcheck")) return { kind: "already-installed" };
|
|
239
|
+
if (os.platform() === "linux") return { kind: "command", command: ["sudo", "apt-get", "install", "-y", "cppcheck"] };
|
|
240
|
+
if (hasCommand("brew")) return { kind: "command", command: ["brew", "install", "cppcheck"] };
|
|
241
|
+
return { kind: "manual", note: "Install cppcheck with your system package manager." };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveSpotBugsInstall() {
|
|
245
|
+
if (hasCommand("spotbugs")) return { kind: "already-installed" };
|
|
246
|
+
if (hasCommand("brew")) return { kind: "command", command: ["brew", "install", "spotbugs"] };
|
|
247
|
+
return { kind: "manual", note: "Install SpotBugs manually and ensure `spotbugs` is on PATH." };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hasAny(values, expected) {
|
|
251
|
+
return expected.some((value) => values.includes(value));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function hasCommand(command) {
|
|
255
|
+
try {
|
|
256
|
+
execFileSync("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" });
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function walkFiles(root, maxDepth) {
|
|
264
|
+
const results = [];
|
|
265
|
+
visit(root, 0);
|
|
266
|
+
return results;
|
|
267
|
+
|
|
268
|
+
function visit(currentPath, depth) {
|
|
269
|
+
if (depth > maxDepth) return;
|
|
270
|
+
let entries = [];
|
|
271
|
+
try {
|
|
272
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
273
|
+
} catch {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
279
|
+
if (["node_modules", ".git", "dist", "build", ".next", "coverage"].includes(entry.name)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
visit(fullPath, depth + 1);
|
|
284
|
+
} else {
|
|
285
|
+
results.push(fullPath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readJson(filePath) {
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
294
|
+
} catch {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
detectWorkspace,
|
|
301
|
+
buildInstallPlan,
|
|
302
|
+
formatInstallPlan
|
|
303
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { execFile } = require("node:child_process");
|
|
4
|
+
const { URL } = require("node:url");
|
|
5
|
+
let vscode;
|
|
6
|
+
try {
|
|
7
|
+
vscode = require("vscode");
|
|
8
|
+
} catch {
|
|
9
|
+
vscode = null;
|
|
10
|
+
}
|
|
11
|
+
const { hashFinding } = require("../utils");
|
|
12
|
+
|
|
13
|
+
function execFileAsync(command, args, options = {}) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
execFile(command, args, { maxBuffer: 20 * 1024 * 1024, ...options }, (error, stdout, stderr) => {
|
|
16
|
+
resolve({
|
|
17
|
+
error,
|
|
18
|
+
stdout,
|
|
19
|
+
stderr,
|
|
20
|
+
code: error && typeof error.code === "number" ? error.code : 0
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function runDockerZapScan(config) {
|
|
27
|
+
return runDockerZapScanWithOptions({
|
|
28
|
+
workspaceRoot: vscode?.workspace?.workspaceFolders?.[0]?.uri.fsPath,
|
|
29
|
+
target: config.get("dynamicBaseUrl", "http://127.0.0.1:3000"),
|
|
30
|
+
scanMode: config.get("dynamicScanMode", "baseline"),
|
|
31
|
+
allowHosts: config.get("dynamicAllowHosts", ["127.0.0.1", "localhost"]),
|
|
32
|
+
spiderMinutes: config.get("dynamicSpiderMinutes", 1),
|
|
33
|
+
useAjaxSpider: config.get("dynamicUseAjaxSpider", false),
|
|
34
|
+
dockerImage: config.get("zapDockerImage", "ghcr.io/zaproxy/zaproxy:stable")
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function runDockerZapScanWithOptions(options) {
|
|
39
|
+
const target = options.target || "http://127.0.0.1:3000";
|
|
40
|
+
const scanMode = options.scanMode || "baseline";
|
|
41
|
+
const allowHosts = options.allowHosts || ["127.0.0.1", "localhost"];
|
|
42
|
+
const spiderMinutes = String(options.spiderMinutes || 1);
|
|
43
|
+
const useAjaxSpider = Boolean(options.useAjaxSpider);
|
|
44
|
+
const dockerImage = options.dockerImage || "ghcr.io/zaproxy/zaproxy:stable";
|
|
45
|
+
|
|
46
|
+
const parsed = validateTarget(target, allowHosts);
|
|
47
|
+
const dockerTarget = rewriteTargetForDocker(parsed);
|
|
48
|
+
|
|
49
|
+
const tempDir = await createWorkspaceZapDirectory(options.workspaceRoot);
|
|
50
|
+
const reportPath = path.join(tempDir, "zap-report.json");
|
|
51
|
+
const htmlReportPath = path.join(tempDir, "zap-report.html");
|
|
52
|
+
const script = scanMode === "full" ? "zap-full-scan.py" : "zap-baseline.py";
|
|
53
|
+
const args = [
|
|
54
|
+
"run",
|
|
55
|
+
"--rm",
|
|
56
|
+
"-v",
|
|
57
|
+
`${tempDir}:/zap/wrk/:rw`,
|
|
58
|
+
"--add-host",
|
|
59
|
+
"host.docker.internal:host-gateway",
|
|
60
|
+
dockerImage,
|
|
61
|
+
script,
|
|
62
|
+
"-t",
|
|
63
|
+
dockerTarget,
|
|
64
|
+
"-J",
|
|
65
|
+
"zap-report.json",
|
|
66
|
+
"-r",
|
|
67
|
+
"zap-report.html",
|
|
68
|
+
"-m",
|
|
69
|
+
spiderMinutes,
|
|
70
|
+
"-I"
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (useAjaxSpider) {
|
|
74
|
+
args.push("-j");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dockerCheck = await execFileAsync("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
78
|
+
if (dockerCheck.error) {
|
|
79
|
+
throw new Error("Docker does not appear to be installed or running.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await execFileAsync("docker", args);
|
|
83
|
+
if (result.code !== 0 && result.code !== 1 && result.code !== 2) {
|
|
84
|
+
throw new Error(result.stderr || result.stdout || "ZAP Docker scan failed.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let rawReport;
|
|
88
|
+
try {
|
|
89
|
+
rawReport = await fs.readFile(reportPath, "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error("ZAP completed but no JSON report was generated.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsedReport = JSON.parse(rawReport);
|
|
95
|
+
const findings = normalizeZapReport(parsedReport);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await fs.unlink(reportPath);
|
|
99
|
+
await fs.unlink(htmlReportPath);
|
|
100
|
+
await fs.rmdir(tempDir);
|
|
101
|
+
} catch {}
|
|
102
|
+
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function createWorkspaceZapDirectory(workspaceRoot) {
|
|
107
|
+
const resolvedRoot = workspaceRoot || vscode?.workspace?.workspaceFolders?.[0]?.uri.fsPath;
|
|
108
|
+
if (!resolvedRoot) {
|
|
109
|
+
throw new Error("Open a workspace folder before running a dynamic scan.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const baseDir = path.join(resolvedRoot, ".secure-review", "zap-output");
|
|
113
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
return fs.mkdtemp(path.join(baseDir, "run-"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateTarget(target, allowHosts) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = new URL(target);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error("Dynamic Base URL is not a valid URL.");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const allowed = new Set((allowHosts || []).map((host) => String(host).toLowerCase()));
|
|
127
|
+
if (!allowed.has(parsed.hostname.toLowerCase())) {
|
|
128
|
+
throw new Error(`Dynamic scan blocked: host ${parsed.hostname} is not in Secure Review > Dynamic Allow Hosts.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rewriteTargetForDocker(url) {
|
|
135
|
+
const cloned = new URL(url.toString());
|
|
136
|
+
if (cloned.hostname === "127.0.0.1" || cloned.hostname === "localhost") {
|
|
137
|
+
cloned.hostname = "host.docker.internal";
|
|
138
|
+
}
|
|
139
|
+
return cloned.toString();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeZapReport(report) {
|
|
143
|
+
const sites = Array.isArray(report.site) ? report.site : [];
|
|
144
|
+
const findings = [];
|
|
145
|
+
|
|
146
|
+
for (const site of sites) {
|
|
147
|
+
const alerts = Array.isArray(site.alerts) ? site.alerts : [];
|
|
148
|
+
for (const alert of alerts) {
|
|
149
|
+
const instances = Array.isArray(alert.instances) && alert.instances.length
|
|
150
|
+
? alert.instances
|
|
151
|
+
: [{}];
|
|
152
|
+
|
|
153
|
+
for (const instance of instances) {
|
|
154
|
+
const targetUrl = instance.uri || site["@name"] || "Unknown URL";
|
|
155
|
+
const evidence = [instance.method, instance.param, instance.evidence, alert.otherinfo]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(" | ");
|
|
158
|
+
|
|
159
|
+
findings.push({
|
|
160
|
+
id: hashFinding(["zap", alert.pluginid || alert.alertRef || alert.name || "alert", targetUrl]),
|
|
161
|
+
source: "dynamic",
|
|
162
|
+
title: alert.name || "ZAP finding",
|
|
163
|
+
severity: mapZapRisk(alert.riskcode, alert.riskdesc),
|
|
164
|
+
confidence: mapZapConfidence(alert.confidence),
|
|
165
|
+
category: "Dynamic",
|
|
166
|
+
subcategory: alert.name || "ZAP Alert",
|
|
167
|
+
reviewDomain: "security",
|
|
168
|
+
code: String(alert.pluginid || alert.alertRef || "zap"),
|
|
169
|
+
message: alert.desc || alert.name || "OWASP ZAP detected a dynamic issue.",
|
|
170
|
+
targetUrl,
|
|
171
|
+
evidence: evidence || "n/a",
|
|
172
|
+
remediation: alert.solution || "Review the affected endpoint and apply the recommended security control.",
|
|
173
|
+
suggestion: alert.solution || "Inspect the endpoint and validate the alert in the target application.",
|
|
174
|
+
whyItMatters: alert.desc || "Dynamic testing identified a web application behavior that may be exploitable at runtime.",
|
|
175
|
+
standards: [alert.cweid ? `CWE-${alert.cweid}` : null, alert.wascid ? `WASC-${alert.wascid}` : null].filter(Boolean)
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return dedupeZapFindings(findings);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function dedupeZapFindings(findings) {
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
return findings.filter((finding) => {
|
|
187
|
+
const key = `${finding.code}|${finding.targetUrl}|${finding.title}`;
|
|
188
|
+
if (seen.has(key)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
seen.add(key);
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mapZapRisk(riskCode, riskDesc) {
|
|
197
|
+
const raw = `${riskCode || ""} ${riskDesc || ""}`.toLowerCase();
|
|
198
|
+
if (raw.includes("3") || raw.includes("high")) {
|
|
199
|
+
return "high";
|
|
200
|
+
}
|
|
201
|
+
if (raw.includes("2") || raw.includes("medium")) {
|
|
202
|
+
return "medium";
|
|
203
|
+
}
|
|
204
|
+
if (raw.includes("1") || raw.includes("low")) {
|
|
205
|
+
return "low";
|
|
206
|
+
}
|
|
207
|
+
return "low";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mapZapConfidence(value) {
|
|
211
|
+
const raw = String(value || "").toLowerCase();
|
|
212
|
+
if (raw.includes("high")) {
|
|
213
|
+
return "high";
|
|
214
|
+
}
|
|
215
|
+
if (raw.includes("medium")) {
|
|
216
|
+
return "medium";
|
|
217
|
+
}
|
|
218
|
+
return "low";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
runDockerZapScan,
|
|
223
|
+
runDockerZapScanWithOptions
|
|
224
|
+
};
|