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 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 prefixMap = new Map();
15
- // Regex to detect router mounts, e.g. app.use('/api/v1/auth', authRouter) or router.use('/auth', authRouter)
16
- // Group 3 matches the path prefix, Group 4 matches the router variable name
17
- const mountPattern = /\b([a-zA-Z0-9_]+)\.(use|route|group)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*([a-zA-Z0-9_]+)/gi;
18
- // Read files first to construct prefixMap
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 prefix = match[3];
26
- const routerName = match[4];
27
- if (prefix && routerName && routerName !== "express" && routerName !== "cors" && routerName !== "helmet") {
28
- prefixMap.set(routerName, prefix);
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
- // Regex patterns
35
- // 1. Generic Router paths: authRouter.post('/login', ...)
36
- // Group 1 matches the router variable, Group 2 matches method, Group 3 matches endpoint path
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
- let rPath = subPath;
62
- // If the instanceName has a registered mount prefix, prepend it!
63
- if (prefixMap.has(instanceName)) {
64
- const prefix = prefixMap.get(instanceName).replace(/\/$/, "");
65
- rPath = `${prefix}/${subPath.replace(/^\//, "")}`;
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
- defaultValue: selectedRoute.routePath,
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
  }
@@ -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
  }