bandit-cli 1.0.3 → 1.0.6
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/README.md +3 -0
- package/dist/cli/api.js +267 -32
- package/dist/cli/bench.js +12 -0
- package/dist/cli/doctor.js +175 -0
- package/dist/cli/output.js +41 -58
- package/dist/cli/studio.js +54 -0
- package/dist/index.js +25 -3
- package/dist/studio/db.js +96 -0
- package/dist/studio/scanner.js +66 -0
- package/dist/studio/server.js +284 -0
- package/dist/utils/monorepo.js +56 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
|
|
15
15
|
- **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
|
|
16
16
|
- **Static Project Auditor**: Evaluates package setup, file organization, safety configs, and dependency scopes.
|
|
17
|
+
- **Monorepo & Microservices Auto-Discovery**: Automatically scans your workspace for nested packages containing `package.json` and interactively prompts you to target specific services.
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -38,11 +39,13 @@ npx bandit-cli <command>
|
|
|
38
39
|
### 1. Interactive API Playfield
|
|
39
40
|
|
|
40
41
|
Find and test routes in your codebase interactively:
|
|
42
|
+
|
|
41
43
|
```bash
|
|
42
44
|
bandit api
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Or execute a direct API request immediately (acts like a local curl/HTTPie):
|
|
48
|
+
|
|
46
49
|
```bash
|
|
47
50
|
bandit api GET /api/v1/products --url http://localhost:8080 --token "jwt-token"
|
|
48
51
|
bandit api POST /api/v1/users --body '{"name":"Raj"}'
|
package/dist/cli/api.js
CHANGED
|
@@ -3,41 +3,278 @@ import path from "node:path";
|
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import * as clack from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
+
function normalizePath(filePath) {
|
|
7
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
8
|
+
const winDriveRegex = /^([A-Z]):\//;
|
|
9
|
+
const match = normalized.match(winDriveRegex);
|
|
10
|
+
if (match) {
|
|
11
|
+
normalized = normalized.replace(winDriveRegex, `${match[1].toLowerCase()}:/`);
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
function resolveImport(sourceFile, importPath, projectPath) {
|
|
16
|
+
let absolutePath = "";
|
|
17
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
|
|
18
|
+
const relativePart = importPath.slice(2);
|
|
19
|
+
absolutePath = path.resolve(projectPath, "src", relativePart);
|
|
20
|
+
}
|
|
21
|
+
else if (importPath.startsWith("src/")) {
|
|
22
|
+
absolutePath = path.resolve(projectPath, importPath);
|
|
23
|
+
}
|
|
24
|
+
else if (importPath.startsWith(".")) {
|
|
25
|
+
const sourceDir = path.dirname(sourceFile);
|
|
26
|
+
absolutePath = path.resolve(sourceDir, importPath);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const extensions = [".ts", ".js", ".tsx", ".jsx"];
|
|
32
|
+
for (const ext of extensions) {
|
|
33
|
+
const fileWithExt = absolutePath + ext;
|
|
34
|
+
if (fs.existsSync(fileWithExt)) {
|
|
35
|
+
return normalizePath(fileWithExt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
39
|
+
for (const ext of extensions) {
|
|
40
|
+
const indexFile = path.join(absolutePath, "index" + ext);
|
|
41
|
+
if (fs.existsSync(indexFile)) {
|
|
42
|
+
return normalizePath(indexFile);
|
|
43
|
+
}
|
|
44
|
+
const routerFile = path.join(absolutePath, "router" + ext);
|
|
45
|
+
if (fs.existsSync(routerFile)) {
|
|
46
|
+
return normalizePath(routerFile);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function parseImportNames(importClause) {
|
|
53
|
+
const names = [];
|
|
54
|
+
importClause = importClause.trim();
|
|
55
|
+
const nsMatch = importClause.match(/\*\s+as\s+([a-zA-Z0-9_$]+)/);
|
|
56
|
+
if (nsMatch) {
|
|
57
|
+
names.push({ localName: nsMatch[1] });
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
const destructureMatch = importClause.match(/\{([\s\S]*?)\}/);
|
|
61
|
+
if (destructureMatch) {
|
|
62
|
+
const content = destructureMatch[1];
|
|
63
|
+
const parts = content.split(",");
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
const trimmed = part.trim();
|
|
66
|
+
if (!trimmed)
|
|
67
|
+
continue;
|
|
68
|
+
const aliasMatch = trimmed.match(/([a-zA-Z0-9_$]+)\s+as\s+([a-zA-Z0-9_$]+)/);
|
|
69
|
+
if (aliasMatch) {
|
|
70
|
+
names.push({ localName: aliasMatch[2] });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const nameMatch = trimmed.match(/^([a-zA-Z0-9_$]+)$/);
|
|
74
|
+
if (nameMatch) {
|
|
75
|
+
names.push({ localName: nameMatch[1] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const beforeDestruct = importClause.split("{")[0].trim().replace(/,$/, "").trim();
|
|
80
|
+
if (beforeDestruct && !beforeDestruct.startsWith("import")) {
|
|
81
|
+
names.push({ localName: beforeDestruct });
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
if (importClause && !importClause.includes("{") && !importClause.includes("*")) {
|
|
86
|
+
names.push({ localName: importClause });
|
|
87
|
+
}
|
|
88
|
+
return names;
|
|
89
|
+
}
|
|
90
|
+
function resolvePaths(pref, p) {
|
|
91
|
+
let combined = `${pref}/${p}`.replace(/\/+/g, "/");
|
|
92
|
+
if (!combined.startsWith("/"))
|
|
93
|
+
combined = "/" + combined;
|
|
94
|
+
if (combined.length > 1 && combined.endsWith("/")) {
|
|
95
|
+
combined = combined.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
return combined;
|
|
98
|
+
}
|
|
6
99
|
// Scans files for common route definitions and mounts
|
|
7
|
-
async function scanForRoutes(projectPath) {
|
|
100
|
+
export async function scanForRoutes(projectPath) {
|
|
101
|
+
projectPath = normalizePath(projectPath);
|
|
8
102
|
const routes = [];
|
|
9
|
-
const files = await fg(["src/**/*.{ts,js}", "*.{ts,js}", "routes/**/*.{ts,js}", "controllers/**/*.{ts,js}"], {
|
|
103
|
+
const files = (await fg(["src/**/*.{ts,js}", "*.{ts,js}", "routes/**/*.{ts,js}", "controllers/**/*.{ts,js}"], {
|
|
10
104
|
cwd: projectPath,
|
|
11
105
|
absolute: true,
|
|
12
106
|
ignore: ["**/node_modules/**", "**/dist/**", "**/*.test.{ts,js}", "**/*.spec.{ts,js}"],
|
|
13
|
-
});
|
|
14
|
-
const
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
107
|
+
})).map(normalizePath);
|
|
108
|
+
const importsByFile = new Map();
|
|
109
|
+
// Step 1: Scan imports & requires for all files
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const localToTarget = new Map();
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
114
|
+
const esImportRegex = /import\s+([\s\S]*?)\s+from\s+['"`]([^'"`]+)['"`]/g;
|
|
115
|
+
let match;
|
|
116
|
+
while ((match = esImportRegex.exec(content)) !== null) {
|
|
117
|
+
const importClause = match[1];
|
|
118
|
+
const importPath = match[2];
|
|
119
|
+
const targetFile = resolveImport(file, importPath, projectPath);
|
|
120
|
+
if (targetFile) {
|
|
121
|
+
const names = parseImportNames(importClause);
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
localToTarget.set(name.localName, targetFile);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const requireRegex = /(?:const|let|var)\s+([\s\S]*?)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
128
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
129
|
+
const varClause = match[1];
|
|
130
|
+
const importPath = match[2];
|
|
131
|
+
const targetFile = resolveImport(file, importPath, projectPath);
|
|
132
|
+
if (targetFile) {
|
|
133
|
+
const names = parseImportNames(varClause);
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
localToTarget.set(name.localName, targetFile);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
importsByFile.set(file, localToTarget);
|
|
142
|
+
}
|
|
143
|
+
// Step 2: Build Dependency Graph Nodes and Edges
|
|
144
|
+
const nodes = new Map();
|
|
145
|
+
const edges = [];
|
|
146
|
+
const ensureNode = (id, type, filePath, varName) => {
|
|
147
|
+
if (!nodes.has(id)) {
|
|
148
|
+
nodes.set(id, { id, type, filePath, varName, prefixes: new Set() });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Create file nodes
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
ensureNode(`file:${file}`, "file", file);
|
|
154
|
+
}
|
|
155
|
+
// Pre-populate route variable nodes from route declarations
|
|
156
|
+
const routeVarPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(/gi;
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
try {
|
|
159
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
160
|
+
let match;
|
|
161
|
+
routeVarPattern.lastIndex = 0;
|
|
162
|
+
while ((match = routeVarPattern.exec(content)) !== null) {
|
|
163
|
+
const instanceName = match[1];
|
|
164
|
+
ensureNode(`var:${file}:${instanceName}`, "var", file, instanceName);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
}
|
|
169
|
+
const mountPattern = /\b([a-zA-Z0-9_]+)\.(use|route|group)\s*\(\s*(?:['"`]([^'"`]+)['"`]\s*,\s*)?(?:require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|([a-zA-Z0-9_]+))/gi;
|
|
19
170
|
for (const file of files) {
|
|
20
171
|
try {
|
|
21
172
|
const content = fs.readFileSync(file, "utf-8");
|
|
22
173
|
let match;
|
|
23
174
|
mountPattern.lastIndex = 0;
|
|
24
175
|
while ((match = mountPattern.exec(content)) !== null) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
176
|
+
const parentVar = match[1];
|
|
177
|
+
const mountPath = match[3] || "/";
|
|
178
|
+
const inlineRequire = match[4];
|
|
179
|
+
const childVar = match[5];
|
|
180
|
+
const parentId = `var:${file}:${parentVar}`;
|
|
181
|
+
ensureNode(parentId, "var", file, parentVar);
|
|
182
|
+
if (inlineRequire) {
|
|
183
|
+
const targetFile = resolveImport(file, inlineRequire, projectPath);
|
|
184
|
+
if (targetFile) {
|
|
185
|
+
const targetFileId = `file:${targetFile}`;
|
|
186
|
+
ensureNode(targetFileId, "file", targetFile);
|
|
187
|
+
edges.push({ from: parentId, to: targetFileId, path: mountPath });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (childVar) {
|
|
191
|
+
const childId = `var:${file}:${childVar}`;
|
|
192
|
+
ensureNode(childId, "var", file, childVar);
|
|
193
|
+
edges.push({ from: parentId, to: childId, path: mountPath });
|
|
194
|
+
// Check if childVar is imported
|
|
195
|
+
const targetFile = importsByFile.get(file)?.get(childVar);
|
|
196
|
+
if (targetFile) {
|
|
197
|
+
const targetFileId = `file:${targetFile}`;
|
|
198
|
+
ensureNode(targetFileId, "file", targetFile);
|
|
199
|
+
edges.push({ from: childId, to: targetFileId, path: "/" });
|
|
200
|
+
}
|
|
29
201
|
}
|
|
30
202
|
}
|
|
31
203
|
}
|
|
32
204
|
catch { }
|
|
33
205
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
206
|
+
// Step 3: Link file nodes to their root variables within that file
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const fileId = `file:${file}`;
|
|
209
|
+
// Find all variable nodes in this file
|
|
210
|
+
const varNodesInFile = Array.from(nodes.values()).filter((n) => n.type === "var" && n.filePath === file);
|
|
211
|
+
for (const vNode of varNodesInFile) {
|
|
212
|
+
// Is there an incoming edge to this variable from another variable/file in the same file?
|
|
213
|
+
const hasIncomingLocal = edges.some((e) => e.to === vNode.id && e.from.startsWith(`var:${file}:`));
|
|
214
|
+
if (!hasIncomingLocal) {
|
|
215
|
+
// It is a root variable in this file, link the file node to it
|
|
216
|
+
edges.push({ from: fileId, to: vNode.id, path: "/" });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Step 4: Identify entry points and initialize their prefixes
|
|
221
|
+
// Entry points are file nodes that have no incoming edges from OTHER files
|
|
222
|
+
const fileIncomingFromOthers = new Set();
|
|
223
|
+
for (const edge of edges) {
|
|
224
|
+
if (edge.to.startsWith("file:")) {
|
|
225
|
+
const fromFile = nodes.get(edge.from)?.filePath;
|
|
226
|
+
const toFile = nodes.get(edge.to)?.filePath;
|
|
227
|
+
if (fromFile && toFile && fromFile !== toFile) {
|
|
228
|
+
fileIncomingFromOthers.add(edge.to);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const fileId = `file:${file}`;
|
|
234
|
+
if (!fileIncomingFromOthers.has(fileId)) {
|
|
235
|
+
const fNode = nodes.get(fileId);
|
|
236
|
+
if (fNode) {
|
|
237
|
+
fNode.prefixes.add("/");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Step 5: Propagate prefixes using BFS
|
|
242
|
+
const queue = [];
|
|
243
|
+
for (const node of nodes.values()) {
|
|
244
|
+
if (node.prefixes.size > 0) {
|
|
245
|
+
queue.push(node.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const inQueue = new Set(queue);
|
|
249
|
+
while (queue.length > 0) {
|
|
250
|
+
const currId = queue.shift();
|
|
251
|
+
inQueue.delete(currId);
|
|
252
|
+
const currNode = nodes.get(currId);
|
|
253
|
+
if (!currNode)
|
|
254
|
+
continue;
|
|
255
|
+
// Find outgoing edges
|
|
256
|
+
const outgoing = edges.filter((e) => e.from === currId);
|
|
257
|
+
for (const edge of outgoing) {
|
|
258
|
+
const targetNode = nodes.get(edge.to);
|
|
259
|
+
if (!targetNode)
|
|
260
|
+
continue;
|
|
261
|
+
let changed = false;
|
|
262
|
+
for (const pref of currNode.prefixes) {
|
|
263
|
+
const combined = resolvePaths(pref, edge.path);
|
|
264
|
+
if (!targetNode.prefixes.has(combined)) {
|
|
265
|
+
targetNode.prefixes.add(combined);
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed && !inQueue.has(edge.to)) {
|
|
270
|
+
queue.push(edge.to);
|
|
271
|
+
inQueue.add(edge.to);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Step 6: Scan files for actual routes and use propagated prefixes
|
|
37
276
|
const expressPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
38
|
-
// 2. NestJS decorators: @Get('/path')
|
|
39
277
|
const nestPattern = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
40
|
-
// 3. NestJS controllers: @Controller('/prefix')
|
|
41
278
|
const controllerPattern = /@Controller\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
42
279
|
for (const file of files) {
|
|
43
280
|
try {
|
|
@@ -58,21 +295,19 @@ async function scanForRoutes(projectPath) {
|
|
|
58
295
|
const instanceName = match[1];
|
|
59
296
|
const method = match[2].toUpperCase();
|
|
60
297
|
const subPath = match[3];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
298
|
+
const varNodeId = `var:${file}:${instanceName}`;
|
|
299
|
+
const varNode = nodes.get(varNodeId);
|
|
300
|
+
const targetPrefixes = (varNode && varNode.prefixes.size > 0)
|
|
301
|
+
? Array.from(varNode.prefixes)
|
|
302
|
+
: ["/"];
|
|
303
|
+
for (const prefix of targetPrefixes) {
|
|
304
|
+
const rPath = resolvePaths(prefix, subPath);
|
|
305
|
+
routes.push({
|
|
306
|
+
method,
|
|
307
|
+
routePath: rPath,
|
|
308
|
+
sourceFile: relativePath,
|
|
309
|
+
});
|
|
66
310
|
}
|
|
67
|
-
// Clean up double slashes
|
|
68
|
-
rPath = rPath.replace(/\/+/g, "/");
|
|
69
|
-
if (!rPath.startsWith("/"))
|
|
70
|
-
rPath = `/${rPath}`;
|
|
71
|
-
routes.push({
|
|
72
|
-
method,
|
|
73
|
-
routePath: rPath,
|
|
74
|
-
sourceFile: relativePath,
|
|
75
|
-
});
|
|
76
311
|
}
|
|
77
312
|
// 2. NestJS
|
|
78
313
|
nestPattern.lastIndex = 0;
|
|
@@ -304,7 +539,7 @@ export async function runApiCommand(methodOrPath = ".", routePath, opts = {}) {
|
|
|
304
539
|
// Let the developer confirm and edit the path (highly convenient for prefix additions!)
|
|
305
540
|
const editPath = await clack.text({
|
|
306
541
|
message: "Confirm or edit the route path:",
|
|
307
|
-
|
|
542
|
+
initialValue: selectedRoute.routePath,
|
|
308
543
|
placeholder: selectedRoute.routePath,
|
|
309
544
|
validate: (v) => (!v || !v.startsWith("/") ? "Route path must start with a slash '/'" : undefined),
|
|
310
545
|
});
|
package/dist/cli/bench.js
CHANGED
|
@@ -131,5 +131,17 @@ export async function runBenchCommand(targetUrl, opts) {
|
|
|
131
131
|
console.log(` ${chalk.gray(label)} : [${chalk.green(bar)}] ${count.toString().padStart(4)} (${percentage.toFixed(1)}%)`);
|
|
132
132
|
}
|
|
133
133
|
console.log(chalk.gray("──────────────────────────────────────────────────"));
|
|
134
|
+
try {
|
|
135
|
+
const { StudioDB } = await import("../studio/db.js");
|
|
136
|
+
const db = new StudioDB();
|
|
137
|
+
db.saveBenchmark({
|
|
138
|
+
targetUrl,
|
|
139
|
+
connections: opts.connections,
|
|
140
|
+
requests: opts.requests,
|
|
141
|
+
rps,
|
|
142
|
+
latency: { min, avg, max, p50, p90, p95, p99 },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
134
146
|
clack.outro(chalk.bold.green("Benchmark complete!"));
|
|
135
147
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -16,6 +16,26 @@ async function fetchWithTimeout(url, options = {}, timeoutMs = 4000) {
|
|
|
16
16
|
throw err;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
function findSensitiveKeys(obj) {
|
|
20
|
+
const sensitiveKeywords = [
|
|
21
|
+
"password", "hashed_password", "salt", "secret", "ssn",
|
|
22
|
+
"credit_card", "creditcard", "passwd", "token", "auth_token"
|
|
23
|
+
];
|
|
24
|
+
const found = [];
|
|
25
|
+
function search(current) {
|
|
26
|
+
if (!current || typeof current !== "object")
|
|
27
|
+
return;
|
|
28
|
+
for (const key of Object.keys(current)) {
|
|
29
|
+
const lowerKey = key.toLowerCase();
|
|
30
|
+
if (sensitiveKeywords.includes(lowerKey)) {
|
|
31
|
+
found.push(key);
|
|
32
|
+
}
|
|
33
|
+
search(current[key]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
search(obj);
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
19
39
|
export async function runDoctorCommand(targetUrl = "http://localhost:3000") {
|
|
20
40
|
clack.intro(chalk.bold.bgBlue.white(" Bandit Live Server Doctor "));
|
|
21
41
|
clack.log.info(`Target URL: ${chalk.bold.cyan(targetUrl)}`);
|
|
@@ -205,6 +225,155 @@ export async function runDoctorCommand(targetUrl = "http://localhost:3000") {
|
|
|
205
225
|
}
|
|
206
226
|
}
|
|
207
227
|
s.stop("Oversized payload probe complete.");
|
|
228
|
+
// 4. Sensitive Data Leak Scanner
|
|
229
|
+
s.start("Scanning response for sensitive data leakage...");
|
|
230
|
+
try {
|
|
231
|
+
const rootText = await rootResponse.text();
|
|
232
|
+
let rootJson = null;
|
|
233
|
+
try {
|
|
234
|
+
rootJson = JSON.parse(rootText);
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
if (rootJson) {
|
|
238
|
+
const leaked = findSensitiveKeys(rootJson);
|
|
239
|
+
if (leaked.length > 0) {
|
|
240
|
+
reports.push({
|
|
241
|
+
title: "Sensitive Data Leak Check",
|
|
242
|
+
status: "fail",
|
|
243
|
+
details: `Root JSON response exposed sensitive field(s): [${leaked.join(", ")}]`,
|
|
244
|
+
suggestion: "Remove database columns like password/salt/secret from response serializers/DTOs.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
reports.push({
|
|
249
|
+
title: "Sensitive Data Leak Check",
|
|
250
|
+
status: "pass",
|
|
251
|
+
details: "No sensitive fields leaked in root JSON response.",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
reports.push({
|
|
257
|
+
title: "Sensitive Data Leak Check",
|
|
258
|
+
status: "pass",
|
|
259
|
+
details: "Root response did not return JSON (no leaks scanned).",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
reports.push({
|
|
265
|
+
title: "Sensitive Data Leak Check",
|
|
266
|
+
status: "warn",
|
|
267
|
+
details: `Failed to audit sensitive data leaks: ${err.message}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
s.stop("Sensitive data scan complete.");
|
|
271
|
+
// 5. Active Security Penetration Probes (SQLi, NoSQLi, XSS)
|
|
272
|
+
s.start("Executing active security penetration probes...");
|
|
273
|
+
// SQLi Probe
|
|
274
|
+
try {
|
|
275
|
+
const sqliUrl = `${targetUrl.replace(/\/$/, "")}/?id=%27%20OR%20%271%27%3D%271`;
|
|
276
|
+
const sqliRes = await fetchWithTimeout(sqliUrl, {}, 3000);
|
|
277
|
+
const sqliText = await sqliRes.text();
|
|
278
|
+
const sqlErrors = [
|
|
279
|
+
"SQLITE_ERROR",
|
|
280
|
+
"syntax error near",
|
|
281
|
+
"PostgreSQL query failed",
|
|
282
|
+
"mysql server version",
|
|
283
|
+
"MariaDB server version",
|
|
284
|
+
"You have an error in your SQL syntax",
|
|
285
|
+
"pg_query",
|
|
286
|
+
];
|
|
287
|
+
const leakedSqlError = sqlErrors.find(err => sqliText.toLowerCase().includes(err.toLowerCase()));
|
|
288
|
+
if (leakedSqlError) {
|
|
289
|
+
reports.push({
|
|
290
|
+
title: "SQL Injection (SQLi) Vulnerability Probe",
|
|
291
|
+
status: "fail",
|
|
292
|
+
details: `Server exposed SQL error token [${leakedSqlError}] when queried with SQL injection payload.`,
|
|
293
|
+
suggestion: "Use parameterized queries or ORMs (Prisma/Drizzle) and never concatenate raw inputs into SQL.",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
reports.push({
|
|
298
|
+
title: "SQL Injection (SQLi) Vulnerability Probe",
|
|
299
|
+
status: "pass",
|
|
300
|
+
details: "No raw SQL exceptions leaked in response to injection payload.",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
reports.push({
|
|
306
|
+
title: "SQL Injection (SQLi) Vulnerability Probe",
|
|
307
|
+
status: "warn",
|
|
308
|
+
details: `Failed to execute SQLi probe: ${err.message}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// XSS Probe
|
|
312
|
+
try {
|
|
313
|
+
const xssPayload = "<script>alert(1)</script>";
|
|
314
|
+
const xssUrl = `${targetUrl.replace(/\/$/, "")}/?search=${encodeURIComponent(xssPayload)}`;
|
|
315
|
+
const xssRes = await fetchWithTimeout(xssUrl, {}, 3000);
|
|
316
|
+
const xssText = await xssRes.text();
|
|
317
|
+
if (xssRes.ok && xssText.includes(xssPayload)) {
|
|
318
|
+
reports.push({
|
|
319
|
+
title: "Cross-Site Scripting (XSS) Vulnerability Probe",
|
|
320
|
+
status: "fail",
|
|
321
|
+
details: "Server reflected unescaped HTML script tags in the response body.",
|
|
322
|
+
suggestion: "Sanitize user inputs and HTML-encode reflected outputs to prevent script execution.",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
reports.push({
|
|
327
|
+
title: "Cross-Site Scripting (XSS) Vulnerability Probe",
|
|
328
|
+
status: "pass",
|
|
329
|
+
details: "Input reflected HTML script tags are properly escaped or omitted by the server.",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
reports.push({
|
|
335
|
+
title: "Cross-Site Scripting (XSS) Vulnerability Probe",
|
|
336
|
+
status: "warn",
|
|
337
|
+
details: `Failed to execute XSS probe: ${err.message}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
// NoSQLi Probe
|
|
341
|
+
try {
|
|
342
|
+
const nosqliUrl = `${targetUrl.replace(/\/$/, "")}/?username[%24ne]=admin`;
|
|
343
|
+
const nosqliRes = await fetchWithTimeout(nosqliUrl, {}, 3000);
|
|
344
|
+
const nosqliText = await nosqliRes.text();
|
|
345
|
+
const nosqlErrors = [
|
|
346
|
+
"MongoError",
|
|
347
|
+
"MongoServerError",
|
|
348
|
+
"CastError",
|
|
349
|
+
"ObjectParameterError",
|
|
350
|
+
"MongooseError",
|
|
351
|
+
];
|
|
352
|
+
const leakedNoSqlError = nosqlErrors.find(err => nosqliText.toLowerCase().includes(err.toLowerCase()));
|
|
353
|
+
if (leakedNoSqlError) {
|
|
354
|
+
reports.push({
|
|
355
|
+
title: "NoSQL Injection Vulnerability Probe",
|
|
356
|
+
status: "fail",
|
|
357
|
+
details: `Server exposed NoSQL error token [${leakedNoSqlError}] in response to query parameters.`,
|
|
358
|
+
suggestion: "Sanitize input parameters and sanitize mongo operator prefixes ($) using tools like 'mongo-sanitize'.",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
reports.push({
|
|
363
|
+
title: "NoSQL Injection Vulnerability Probe",
|
|
364
|
+
status: "pass",
|
|
365
|
+
details: "No NoSQL database exceptions leaked in response to query payload.",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
reports.push({
|
|
371
|
+
title: "NoSQL Injection Vulnerability Probe",
|
|
372
|
+
status: "warn",
|
|
373
|
+
details: `Failed to execute NoSQLi probe: ${err.message}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
s.stop("Active penetration probes complete.");
|
|
208
377
|
// Print results
|
|
209
378
|
console.log("\n" + chalk.bold("Diagnostic Results:"));
|
|
210
379
|
for (const report of reports) {
|
|
@@ -227,5 +396,11 @@ export async function runDoctorCommand(targetUrl = "http://localhost:3000") {
|
|
|
227
396
|
}
|
|
228
397
|
console.log("");
|
|
229
398
|
}
|
|
399
|
+
try {
|
|
400
|
+
const { StudioDB } = await import("../studio/db.js");
|
|
401
|
+
const db = new StudioDB();
|
|
402
|
+
db.saveAudit(reports);
|
|
403
|
+
}
|
|
404
|
+
catch { }
|
|
230
405
|
clack.outro(chalk.bold.green("Doctor diagnostic scan completed."));
|
|
231
406
|
}
|