bandit-cli 1.0.1 → 1.0.4

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
@@ -1,4 +1,5 @@
1
- # Bandit CLI 🤠
1
+ # Bandit CLI
2
+
2
3
  > **The Swiss Army Knife for Backend Developers**
3
4
 
4
5
  `bandit` is an interactive, high-utility terminal workspace helper built for backend developers. Instead of relying on heavy GUI clients or complex shell scripts, `bandit` lets you test APIs, benchmark endpoints, terminate locked ports, validate environment setups, and audit server health directly from your command line.
@@ -7,23 +8,26 @@
7
8
 
8
9
  ## 🚀 Key Features
9
10
 
10
- * **Interactive API Client**: Auto-scans your codebase (Express, Fastify, Hono, NestJS) to find route definitions, maps parameters, takes JSON payloads, prompts for Bearer Auth tokens, and displays pretty-printed HTTP responses.
11
- * **High-Speed Load Benchmarker**: Benchmark any local or remote HTTP endpoint with configurable concurrency, calculating throughput (RPS), p50-p99 percentiles, and rendering a beautiful ASCII distribution chart.
12
- * **Port Inspector & Process Killer**: Scans active ports, identifies the PID and process names (e.g. `node.exe`, `postgres.exe`, `ollama.exe`), and lets you terminate locked processes in one click.
13
- * **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
14
- * **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
15
- * **Static Project Auditor**: Evaluates package setup, file organization, safety configs, and dependency scopes.
11
+ - **Interactive API Client**: Auto-scans your codebase (Express, Fastify, Hono, NestJS) to find route definitions, maps parameters, takes JSON payloads, prompts for Bearer Auth tokens, and displays pretty-printed HTTP responses.
12
+ - **High-Speed Load Benchmarker**: Benchmark any local or remote HTTP endpoint with configurable concurrency, calculating throughput (RPS), p50-p99 percentiles, and rendering a beautiful ASCII distribution chart.
13
+ - **Port Inspector & Process Killer**: Scans active ports, identifies the PID and process names (e.g. `node.exe`, `postgres.exe`, `ollama.exe`), and lets you terminate locked processes in one click.
14
+ - **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
15
+ - **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
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.
16
18
 
17
19
  ---
18
20
 
19
21
  ## 📦 Installation
20
22
 
21
23
  Install globally via npm:
24
+
22
25
  ```bash
23
26
  npm install -g bandit-cli
24
27
  ```
25
28
 
26
29
  Or run instantly using `npx`:
30
+
27
31
  ```bash
28
32
  npx bandit-cli <command>
29
33
  ```
@@ -33,37 +37,56 @@ npx bandit-cli <command>
33
37
  ## 🛠️ Usage & Commands
34
38
 
35
39
  ### 1. Interactive API Playfield
36
- Find and test routes in your codebase:
40
+
41
+ Find and test routes in your codebase interactively:
42
+
37
43
  ```bash
38
44
  bandit api
39
45
  ```
40
46
 
47
+ Or execute a direct API request immediately (acts like a local curl/HTTPie):
48
+
49
+ ```bash
50
+ bandit api GET /api/v1/products --url http://localhost:8080 --token "jwt-token"
51
+ bandit api POST /api/v1/users --body '{"name":"Raj"}'
52
+ ```
53
+
41
54
  ### 2. Endpoint Benchmarking
55
+
42
56
  Run a concurrent load test on an endpoint:
57
+
43
58
  ```bash
44
59
  bandit bench http://localhost:3000/api/users --connections 10 --requests 200
45
60
  ```
46
61
 
47
62
  ### 3. Ports Inspector
63
+
48
64
  List processes listening on local ports and terminate them:
65
+
49
66
  ```bash
50
67
  bandit ports
51
68
  ```
52
69
 
53
70
  ### 4. Active Server Diagnostics
71
+
54
72
  Audit a running server's security headers and vulnerabilities:
73
+
55
74
  ```bash
56
75
  bandit doctor http://localhost:3000
57
76
  ```
58
77
 
59
78
  ### 5. Environment variable check
79
+
60
80
  Validate local environment configurations:
81
+
61
82
  ```bash
62
83
  bandit env
63
84
  ```
64
85
 
65
86
  ### 6. Static Project Audit
87
+
66
88
  Audit files, directory structure, and dependencies:
89
+
67
90
  ```bash
68
91
  bandit audit
69
92
  ```
@@ -71,4 +94,5 @@ bandit audit
71
94
  ---
72
95
 
73
96
  ## 🎨 Under the Hood
97
+
74
98
  Built with TypeScript, [Commander](https://github.com/tj/commander.js), and [@clack/prompts](https://github.com/natemoo-re/clack) for a modern, fluid interactive terminal interface.
package/dist/cli/api.js CHANGED
@@ -3,26 +3,284 @@ 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
- // Scans files for common route definitions
7
- async function scanForRoutes(projectPath) {
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
+ }
99
+ // Scans files for common route definitions and mounts
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
- // Regex patterns
15
- // 1. Express/Fastify/Hono: router.get('/path', ...) or app.post('/path', ...)
16
- const expressPattern = /\b(app|router|route|fastify|hono|api)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
17
- // 2. NestJS decorators: @Get('/path') or @Post('/path')
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;
170
+ for (const file of files) {
171
+ try {
172
+ const content = fs.readFileSync(file, "utf-8");
173
+ let match;
174
+ mountPattern.lastIndex = 0;
175
+ while ((match = mountPattern.exec(content)) !== null) {
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
+ }
201
+ }
202
+ }
203
+ }
204
+ catch { }
205
+ }
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
276
+ const expressPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
18
277
  const nestPattern = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
19
- // 3. NestJS controllers to prefix: @Controller('/prefix')
20
278
  const controllerPattern = /@Controller\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
21
279
  for (const file of files) {
22
280
  try {
23
281
  const content = fs.readFileSync(file, "utf-8");
24
282
  const relativePath = path.relative(projectPath, file);
25
- // Check NestJS Controller prefix if any
283
+ // NestJS controller prefix
26
284
  let controllerPrefix = "";
27
285
  const cMatch = [...content.matchAll(controllerPattern)];
28
286
  if (cMatch.length > 0) {
@@ -30,20 +288,28 @@ async function scanForRoutes(projectPath) {
30
288
  if (controllerPrefix === "/")
31
289
  controllerPrefix = "";
32
290
  }
33
- // 1. Check Express-style matches
291
+ // 1. Express-style
34
292
  let match;
35
293
  expressPattern.lastIndex = 0;
36
294
  while ((match = expressPattern.exec(content)) !== null) {
295
+ const instanceName = match[1];
37
296
  const method = match[2].toUpperCase();
38
- let rPath = match[3];
39
- // Sanitize path variable names if necessary
40
- routes.push({
41
- method,
42
- routePath: rPath,
43
- sourceFile: relativePath,
44
- });
297
+ const subPath = match[3];
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
+ });
310
+ }
45
311
  }
46
- // 2. Check NestJS matches
312
+ // 2. NestJS
47
313
  nestPattern.lastIndex = 0;
48
314
  while ((match = nestPattern.exec(content)) !== null) {
49
315
  const method = match[1].toUpperCase();
@@ -51,17 +317,16 @@ async function scanForRoutes(projectPath) {
51
317
  if (subPath === "/")
52
318
  subPath = "";
53
319
  const fullPath = `${controllerPrefix.replace(/\/$/, "")}/${subPath.replace(/^\//, "")}`;
54
- const sanitizedFullPath = fullPath.startsWith("/") ? fullPath : `/${fullPath}`;
320
+ let rPath = fullPath.startsWith("/") ? fullPath : `/${fullPath}`;
321
+ rPath = rPath.replace(/\/+/g, "/");
55
322
  routes.push({
56
323
  method: method === "DELETE" ? "DELETE" : method,
57
- routePath: sanitizedFullPath,
324
+ routePath: rPath,
58
325
  sourceFile: relativePath,
59
326
  });
60
327
  }
61
328
  }
62
- catch (err) {
63
- // Ignore reading errors
64
- }
329
+ catch { }
65
330
  }
66
331
  // Deduplicate routes by method + routePath
67
332
  const seen = new Set();
@@ -90,9 +355,90 @@ function detectLocalPort(projectPath) {
90
355
  catch { }
91
356
  return 3000;
92
357
  }
93
- export async function runApiCommand(projectPath = ".") {
358
+ export async function runApiCommand(methodOrPath = ".", routePath, opts = {}) {
359
+ const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
360
+ const isDirectMode = routePath !== undefined && httpMethods.includes(methodOrPath.toUpperCase());
361
+ if (isDirectMode) {
362
+ const finalMethod = methodOrPath.toUpperCase();
363
+ const finalPath = routePath;
364
+ // Resolve base URL
365
+ const absoluteProjectPath = path.resolve(".");
366
+ const port = detectLocalPort(absoluteProjectPath);
367
+ let baseUrl = opts.url || `http://localhost:${port}`;
368
+ baseUrl = baseUrl.replace(/\/$/, "");
369
+ // Resolve Headers
370
+ const headers = {
371
+ Accept: "application/json, text/plain, */*",
372
+ };
373
+ if (opts.token) {
374
+ headers["Authorization"] = `Bearer ${opts.token}`;
375
+ }
376
+ if (opts.body) {
377
+ headers["Content-Type"] = "application/json";
378
+ }
379
+ // Parse custom headers -H "Key: Val"
380
+ if (opts.header) {
381
+ const hList = Array.isArray(opts.header) ? opts.header : [opts.header];
382
+ for (const h of hList) {
383
+ const colonIdx = h.indexOf(":");
384
+ if (colonIdx !== -1) {
385
+ const key = h.slice(0, colonIdx).trim();
386
+ const val = h.slice(colonIdx + 1).trim();
387
+ headers[key] = val;
388
+ }
389
+ }
390
+ }
391
+ const finalUrl = `${baseUrl}${finalPath}`;
392
+ clack.intro(chalk.bold.bgBlue.white(" Bandit Direct API Request "));
393
+ clack.log.info(`Sending ${chalk.bold.green(finalMethod)} request to ${chalk.cyan(finalUrl)}...`);
394
+ const s = clack.spinner();
395
+ s.start("Executing request...");
396
+ try {
397
+ const start = performance.now();
398
+ const res = await fetch(finalUrl, {
399
+ method: finalMethod,
400
+ headers,
401
+ ...(opts.body ? { body: opts.body } : {}),
402
+ });
403
+ const end = performance.now();
404
+ const duration = end - start;
405
+ s.stop(`Request completed in ${duration.toFixed(1)}ms.`);
406
+ const contentType = res.headers.get("content-type") || "";
407
+ const resText = await res.text();
408
+ console.log("\n" + chalk.bold.cyan("📬 RESPONSE"));
409
+ console.log(chalk.gray("──────────────────────────────────────────────────"));
410
+ console.log(`Status: ${res.status >= 200 && res.status < 300 ? chalk.bold.green(res.status) : chalk.bold.red(res.status)} ${res.statusText}`);
411
+ console.log(`Time: ${duration.toFixed(1)} ms`);
412
+ console.log(chalk.gray("──────────────────────────────────────────────────"));
413
+ console.log(chalk.bold.cyan("Headers:"));
414
+ res.headers.forEach((val, key) => {
415
+ console.log(` ${chalk.gray(key)}: ${val}`);
416
+ });
417
+ console.log(chalk.gray("──────────────────────────────────────────────────"));
418
+ console.log(chalk.bold.cyan("Body:"));
419
+ if (contentType.includes("application/json")) {
420
+ try {
421
+ const parsed = JSON.parse(resText);
422
+ console.log(chalk.green(JSON.stringify(parsed, null, 2)));
423
+ }
424
+ catch {
425
+ console.log(resText);
426
+ }
427
+ }
428
+ else {
429
+ console.log(resText || chalk.italic.gray("<empty body>"));
430
+ }
431
+ console.log(chalk.gray("──────────────────────────────────────────────────"));
432
+ }
433
+ catch (err) {
434
+ s.stop("Request failed.");
435
+ clack.log.error(`Fetch execution error: ${err.message}`);
436
+ }
437
+ clack.outro(chalk.bold.green("Direct API call complete."));
438
+ return;
439
+ }
94
440
  clack.intro(chalk.bold.bgBlue.white(" Bandit Interactive API Client "));
95
- const absoluteProjectPath = path.resolve(projectPath);
441
+ const absoluteProjectPath = path.resolve(methodOrPath);
96
442
  const s = clack.spinner();
97
443
  s.start("Scanning codebase for API endpoints...");
98
444
  const discoveredRoutes = await scanForRoutes(absoluteProjectPath);
@@ -190,10 +536,20 @@ export async function runApiCommand(projectPath = ".") {
190
536
  }
191
537
  else {
192
538
  finalMethod = selectedRoute.method;
193
- finalPath = selectedRoute.routePath;
539
+ // Let the developer confirm and edit the path (highly convenient for prefix additions!)
540
+ const editPath = await clack.text({
541
+ message: "Confirm or edit the route path:",
542
+ defaultValue: selectedRoute.routePath,
543
+ placeholder: selectedRoute.routePath,
544
+ validate: (v) => (!v || !v.startsWith("/") ? "Route path must start with a slash '/'" : undefined),
545
+ });
546
+ if (clack.isCancel(editPath)) {
547
+ clack.outro("Execution stopped.");
548
+ return;
549
+ }
550
+ finalPath = editPath;
194
551
  }
195
552
  // 1. Process Path Parameters if any (e.g. /users/:id or /users/{id})
196
- // We look for :name or {name}
197
553
  const pathParamPattern = /:([a-zA-Z0-9_]+)|\{([a-zA-Z0-9_]+)\}/g;
198
554
  let matches;
199
555
  const pathParams = [];
@@ -213,7 +569,6 @@ export async function runApiCommand(projectPath = ".") {
213
569
  clack.outro("Execution stopped.");
214
570
  return;
215
571
  }
216
- // Replace :param or {param}
217
572
  resolvedPath = resolvedPath
218
573
  .replace(`:${param}`, val)
219
574
  .replace(`{${param}}`, val);
@@ -242,7 +597,7 @@ export async function runApiCommand(projectPath = ".") {
242
597
  queryStr = trimmed.startsWith("?") ? trimmed : `?${trimmed}`;
243
598
  }
244
599
  }
245
- // 3. Process Request Body if POST/PUT/PATCH
600
+ // 3. Process Request Body if POST/PUT/PATCH (Interactive Key-Value Editor!)
246
601
  let requestBody;
247
602
  if (["POST", "PUT", "PATCH"].includes(finalMethod)) {
248
603
  const enterBody = await clack.confirm({
@@ -254,32 +609,119 @@ export async function runApiCommand(projectPath = ".") {
254
609
  return;
255
610
  }
256
611
  if (enterBody) {
257
- const bodyInput = await clack.text({
258
- message: "Enter JSON payload:",
259
- placeholder: '{"name": "Alice", "email": "alice@gmail.com"}',
260
- validate: (v) => {
261
- if (!v || !v.trim())
262
- return undefined;
263
- try {
264
- JSON.parse(v);
265
- return undefined;
266
- }
267
- catch {
268
- return "Invalid JSON syntax. Please enter valid JSON.";
269
- }
270
- },
612
+ const bodyInputType = await clack.select({
613
+ message: "How would you like to enter the JSON body?",
614
+ options: [
615
+ { value: "kv", label: "Interactive Key-Value builder (Easy/Convenient)" },
616
+ { value: "raw", label: "Paste raw JSON string" },
617
+ ],
271
618
  });
272
- if (clack.isCancel(bodyInput)) {
619
+ if (clack.isCancel(bodyInputType)) {
273
620
  clack.outro("Execution stopped.");
274
621
  return;
275
622
  }
276
- const val = bodyInput.trim();
277
- if (val) {
278
- requestBody = val;
623
+ if (bodyInputType === "kv") {
624
+ const bodyObj = {};
625
+ let addingFields = true;
626
+ while (addingFields) {
627
+ const fieldKey = await clack.text({
628
+ message: "Enter field key:",
629
+ placeholder: "e.g. email",
630
+ });
631
+ if (clack.isCancel(fieldKey) || !fieldKey || !fieldKey.trim())
632
+ break;
633
+ const fieldVal = await clack.text({
634
+ message: `Enter value for '${fieldKey.trim()}':`,
635
+ placeholder: "e.g. raj@example.com",
636
+ });
637
+ if (clack.isCancel(fieldVal))
638
+ break;
639
+ const trimmedVal = fieldVal.trim();
640
+ let parsedVal = trimmedVal;
641
+ // Smart casting
642
+ if (trimmedVal.toLowerCase() === "true")
643
+ parsedVal = true;
644
+ else if (trimmedVal.toLowerCase() === "false")
645
+ parsedVal = false;
646
+ else if (trimmedVal.toLowerCase() === "null")
647
+ parsedVal = null;
648
+ else if (!isNaN(Number(trimmedVal)) && trimmedVal !== "")
649
+ parsedVal = Number(trimmedVal);
650
+ bodyObj[fieldKey.trim()] = parsedVal;
651
+ const addMore = await clack.confirm({
652
+ message: "Add another field to the request body?",
653
+ initialValue: false,
654
+ });
655
+ if (clack.isCancel(addMore) || !addMore) {
656
+ addingFields = false;
657
+ }
658
+ }
659
+ requestBody = JSON.stringify(bodyObj);
660
+ }
661
+ else {
662
+ // Raw JSON input
663
+ const bodyInput = await clack.text({
664
+ message: "Enter JSON payload:",
665
+ placeholder: '{"name": "Alice", "email": "alice@gmail.com"}',
666
+ validate: (v) => {
667
+ if (!v || !v.trim())
668
+ return undefined;
669
+ try {
670
+ JSON.parse(v);
671
+ return undefined;
672
+ }
673
+ catch {
674
+ return "Invalid JSON syntax. Please enter valid JSON.";
675
+ }
676
+ },
677
+ });
678
+ if (clack.isCancel(bodyInput)) {
679
+ clack.outro("Execution stopped.");
680
+ return;
681
+ }
682
+ const val = bodyInput.trim();
683
+ if (val) {
684
+ requestBody = val;
685
+ }
686
+ }
687
+ }
688
+ }
689
+ // 4. Custom Headers Editor
690
+ const customHeaders = {};
691
+ const addHeaders = await clack.confirm({
692
+ message: "Do you want to add custom request headers?",
693
+ initialValue: false,
694
+ });
695
+ if (clack.isCancel(addHeaders)) {
696
+ clack.outro("Execution stopped.");
697
+ return;
698
+ }
699
+ if (addHeaders) {
700
+ let addingHeaders = true;
701
+ while (addingHeaders) {
702
+ const headerKey = await clack.text({
703
+ message: "Enter Header Name:",
704
+ placeholder: "e.g. X-Custom-Key",
705
+ });
706
+ if (clack.isCancel(headerKey) || !headerKey || !headerKey.trim())
707
+ break;
708
+ const headerVal = await clack.text({
709
+ message: `Enter value for '${headerKey.trim()}':`,
710
+ placeholder: "e.g. test-value",
711
+ });
712
+ if (clack.isCancel(headerVal))
713
+ break;
714
+ customHeaders[headerKey.trim()] = headerVal.trim();
715
+ const addMoreHeaders = await clack.confirm({
716
+ message: "Add another custom header?",
717
+ initialValue: false,
718
+ });
719
+ if (clack.isCancel(addMoreHeaders) || !addMoreHeaders) {
720
+ addingHeaders = false;
279
721
  }
280
722
  }
281
723
  }
282
- // 4. Prompt for Authorization Header (Bearer token)
724
+ // 5. Prompt for Authorization Header (Bearer token)
283
725
  const addAuth = await clack.confirm({
284
726
  message: "Do you want to add an Authorization Bearer token?",
285
727
  initialValue: false,
@@ -304,6 +746,9 @@ export async function runApiCommand(projectPath = ".") {
304
746
  // Confirm and Execute Request
305
747
  const finalUrl = `${baseUrl}${resolvedPath}${queryStr}`;
306
748
  clack.log.info(`Ready to request: ${chalk.bold.green(finalMethod)} ${chalk.cyan(finalUrl)}`);
749
+ if (Object.keys(customHeaders).length > 0) {
750
+ clack.log.info(`Headers:\n${chalk.gray(JSON.stringify(customHeaders, null, 2))}`);
751
+ }
307
752
  if (bearerToken) {
308
753
  const preview = bearerToken.length > 12 ? `${bearerToken.slice(0, 8)}...` : bearerToken;
309
754
  clack.log.info(`Auth: Bearer ${preview} (masked)`);
@@ -325,6 +770,7 @@ export async function runApiCommand(projectPath = ".") {
325
770
  const headers = {
326
771
  Accept: "application/json, text/plain, */*",
327
772
  ...(requestBody ? { "Content-Type": "application/json" } : {}),
773
+ ...customHeaders,
328
774
  };
329
775
  if (bearerToken) {
330
776
  headers["Authorization"] = `Bearer ${bearerToken}`;
@@ -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) {
@@ -6,71 +6,54 @@ function icon(status) {
6
6
  return chalk.red("✖");
7
7
  return chalk.gray("-");
8
8
  }
9
- function sevColor(sev, text) {
10
- if (sev === "error")
11
- return chalk.red(text);
12
- if (sev === "warn")
13
- return chalk.yellow(text);
14
- return chalk.blue(text);
15
- }
16
- function sevLabel(sev) {
17
- if (sev === "error")
18
- return "ERROR";
19
- if (sev === "warn")
20
- return "WARN";
21
- return "INFO";
22
- }
23
9
  function printSeparator() {
24
- console.log(chalk.gray("─".repeat(80)));
10
+ console.log(chalk.gray(" " + "─".repeat(70)));
25
11
  }
26
12
  function printItem(item) {
27
13
  const statusIcon = icon(item.status);
28
- const line = `${statusIcon} ${chalk.bold(item.title)}`;
29
- console.log(line);
30
- // Show details for pass/fail/skip
14
+ let titleText = chalk.bold(item.title);
15
+ if (item.status === "fail") {
16
+ if (item.severity === "error")
17
+ titleText += chalk.red.bold(" (ERROR)");
18
+ else if (item.severity === "warn")
19
+ titleText += chalk.yellow.bold(" (WARN)");
20
+ else
21
+ titleText += chalk.blue.bold(" (INFO)");
22
+ }
23
+ console.log(` ${statusIcon} ${titleText}`);
24
+ // Show details
31
25
  if (item.details) {
32
- console.log(chalk.gray(` ↳ ${item.details}`));
26
+ console.log(chalk.gray(` ↳ ${item.details}`));
33
27
  }
34
28
  // Show suggestion only for failures
35
29
  if (item.status === "fail" && item.suggestion) {
36
- console.log(chalk.gray(` ↳ Suggestion: ${item.suggestion}`));
37
- }
38
- }
39
- function printSection(title, items, color) {
40
- if (items.length === 0)
41
- return;
42
- console.log("");
43
- console.log(color(chalk.bold(`● ${title}`)));
44
- printSeparator();
45
- for (const item of items) {
46
- printItem(item);
30
+ console.log(chalk.cyan(` ↳ Suggestion: ${item.suggestion}`));
47
31
  }
48
32
  }
49
33
  export function printHuman(report) {
50
- // Header
34
+ // Header Box
51
35
  console.log("");
52
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
53
- console.log(chalk.bold.white(` Backend Audit Report`));
54
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
55
- console.log(chalk.gray(`Project: ${report.projectPath}`));
56
- console.log(chalk.gray(`Summary: ${chalk.green(`pass=${report.summary.pass}`)} ${chalk.red(`fail=${report.summary.fail}`)} ${chalk.gray(`skip=${report.summary.skip}`)}`));
36
+ console.log(chalk.cyan(` ┌${"─".repeat(70)}┐`));
37
+ console.log(chalk.cyan(` │`) + chalk.bold.white(" BACKEND PROJECT AUDIT SUMMARY ") + chalk.cyan(`│`));
38
+ console.log(chalk.cyan(` └${"─".repeat(70)}┘`));
39
+ console.log(` ${chalk.gray("Project:")} ${chalk.bold(report.projectPath)}`);
40
+ console.log(` ${chalk.gray("Results:")} ${chalk.green(`${report.summary.pass} Passed`)}, ` +
41
+ `${chalk.red(`${report.summary.fail} Failed`)}, ` +
42
+ `${chalk.gray(`${report.summary.skip} Skipped`)}`);
57
43
  // Severity Legend
58
44
  console.log("");
59
- console.log(chalk.bold("Severity Legend:"));
60
- console.log(chalk.red(" ● ERROR ") +
61
- chalk.gray("- Critical issues that must be fixed"));
62
- console.log(chalk.yellow(" WARN ") +
63
- chalk.gray("- Important issues that should be addressed"));
64
- console.log(chalk.blue(" ● INFO ") +
65
- chalk.gray("- Informational items or best practices"));
66
- // Group items by status first
45
+ console.log(` ${chalk.bold.gray("Severity Legend:")} ` +
46
+ `${chalk.red("● ERROR")} (Critical) ` +
47
+ `${chalk.yellow(" WARN")} (Important) ` +
48
+ `${chalk.blue("● INFO")} (Best Practice)`);
49
+ // Group items
67
50
  const passedItems = report.items.filter((item) => item.status === "pass");
68
51
  const failedItems = report.items.filter((item) => item.status === "fail");
69
52
  const skippedItems = report.items.filter((item) => item.status === "skip");
70
53
  // Print PASSED section
71
54
  if (passedItems.length > 0) {
72
55
  console.log("");
73
- console.log(chalk.bold.green(`✓ PASSED CHECKS (${passedItems.length})`));
56
+ console.log(chalk.bold.green(` ✔ PASSED CHECKS (${passedItems.length})`));
74
57
  printSeparator();
75
58
  for (const item of passedItems) {
76
59
  printItem(item);
@@ -79,16 +62,15 @@ export function printHuman(report) {
79
62
  // Print FAILED sections grouped by severity
80
63
  if (failedItems.length > 0) {
81
64
  console.log("");
82
- console.log(chalk.bold.red(`✖ FAILED CHECKS (${failedItems.length})`));
65
+ console.log(chalk.bold.red(` ✖ FAILED CHECKS (${failedItems.length})`));
83
66
  printSeparator();
84
- // Group failed items by severity
85
67
  const failedErrors = failedItems.filter((item) => item.severity === "error");
86
68
  const failedWarns = failedItems.filter((item) => item.severity === "warn");
87
69
  const failedInfos = failedItems.filter((item) => item.severity === "info");
88
70
  // Print failed ERRORS
89
71
  if (failedErrors.length > 0) {
90
72
  console.log("");
91
- console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
73
+ console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
92
74
  for (const item of failedErrors) {
93
75
  console.log("");
94
76
  printItem(item);
@@ -97,7 +79,7 @@ export function printHuman(report) {
97
79
  // Print failed WARNINGS
98
80
  if (failedWarns.length > 0) {
99
81
  console.log("");
100
- console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
82
+ console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
101
83
  for (const item of failedWarns) {
102
84
  console.log("");
103
85
  printItem(item);
@@ -106,7 +88,7 @@ export function printHuman(report) {
106
88
  // Print failed INFO
107
89
  if (failedInfos.length > 0) {
108
90
  console.log("");
109
- console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
91
+ console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
110
92
  for (const item of failedInfos) {
111
93
  console.log("");
112
94
  printItem(item);
@@ -116,23 +98,24 @@ export function printHuman(report) {
116
98
  // Print SKIPPED section (if any)
117
99
  if (skippedItems.length > 0) {
118
100
  console.log("");
119
- console.log(chalk.bold.gray(`- SKIPPED CHECKS (${skippedItems.length})`));
101
+ console.log(chalk.bold.gray(` - SKIPPED CHECKS (${skippedItems.length})`));
120
102
  printSeparator();
121
103
  for (const item of skippedItems) {
122
104
  printItem(item);
123
105
  }
124
106
  }
125
- // Footer
126
- console.log("");
127
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
128
- // Summary message
107
+ // Footer Box
129
108
  const failCount = report.summary.fail;
109
+ console.log("");
130
110
  if (failCount === 0) {
131
- console.log(chalk.green.bold(`✓ All checks passed!`));
111
+ console.log(chalk.green(` ┌${"─".repeat(70)}┐`));
112
+ console.log(chalk.green(` │`) + chalk.bold.green(" ✔ All audits passed successfully! ") + chalk.green(`│`));
113
+ console.log(chalk.green(` └${"─".repeat(70)}┘`));
132
114
  }
133
115
  else {
134
- console.log(chalk.yellow(` ⚠ ${failCount} check${failCount > 1 ? "s" : ""} need${failCount === 1 ? "s" : ""} attention.`));
116
+ console.log(chalk.yellow(` ┌${"".repeat(70)}┐`));
117
+ console.log(chalk.yellow(` │`) + chalk.bold.yellow(" ⚠ Audit completed with warnings/errors. Check issues above. ") + chalk.yellow(`│`));
118
+ console.log(chalk.yellow(` └${"─".repeat(70)}┘`));
135
119
  }
136
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
137
120
  console.log("");
138
121
  }
package/dist/index.js CHANGED
@@ -14,7 +14,9 @@ program
14
14
  .option("--json", "Print results as JSON")
15
15
  .option("--fail-on-warn", "Exit with code 1 if warnings exist")
16
16
  .action(async (projectPath, opts) => {
17
- const results = await runAudit(projectPath);
17
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
18
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
19
+ const results = await runAudit(resolvedPath);
18
20
  if (opts.json) {
19
21
  process.stdout.write(JSON.stringify(results, null, 2) + "\n");
20
22
  }
@@ -42,8 +44,10 @@ program
42
44
  .description("Audit local .env configurations against .env.example")
43
45
  .argument("[path]", "Path to the project", ".")
44
46
  .action(async (projectPath) => {
47
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
48
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
45
49
  const { runEnvCommand } = await import("./cli/env.js");
46
- await runEnvCommand(projectPath);
50
+ await runEnvCommand(resolvedPath);
47
51
  });
48
52
  // Subcommand: doctor
49
53
  program
@@ -57,11 +61,23 @@ program
57
61
  // Subcommand: api
58
62
  program
59
63
  .command("api")
60
- .description("Interactive API playfield: scan codebase routes and send test requests")
61
- .argument("[path]", "Path to the project", ".")
62
- .action(async (projectPath) => {
64
+ .description("API playfield: scan codebase routes (interactive) or send direct requests")
65
+ .argument("[method_or_path]", "HTTP Method or project path", ".")
66
+ .argument("[route_path]", "Route path (only when method is provided)")
67
+ .option("-u, --url <url>", "Server base URL")
68
+ .option("-b, --body <body>", "JSON request body")
69
+ .option("-t, --token <token>", "Bearer Auth token")
70
+ .option("-H, --header <headers...>", "Custom headers in Key:Value format")
71
+ .action(async (methodOrPath, routePath, opts) => {
72
+ const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
73
+ const isDirectMode = routePath !== undefined && httpMethods.includes(methodOrPath.toUpperCase());
74
+ let targetPath = methodOrPath;
75
+ if (!isDirectMode) {
76
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
77
+ targetPath = await resolveProjectPath(methodOrPath || ".");
78
+ }
63
79
  const { runApiCommand } = await import("./cli/api.js");
64
- await runApiCommand(projectPath);
80
+ await runApiCommand(targetPath, routePath, opts);
65
81
  });
66
82
  // Subcommand: bench
67
83
  program
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import * as clack from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ export async function resolveProjectPath(inputPath = ".") {
7
+ const absoluteRoot = path.resolve(inputPath);
8
+ // Find all package.json files, ignoring dependency and output directories
9
+ const packageFiles = await fg(["**/package.json"], {
10
+ cwd: absoluteRoot,
11
+ absolute: true,
12
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/coverage/**"],
13
+ });
14
+ if (packageFiles.length <= 1) {
15
+ // If only one package.json (or none) is found, bypass selection
16
+ return inputPath;
17
+ }
18
+ const services = [];
19
+ for (const file of packageFiles) {
20
+ try {
21
+ const content = fs.readFileSync(file, "utf-8");
22
+ const pkg = JSON.parse(content);
23
+ const dir = path.dirname(file);
24
+ services.push({
25
+ name: pkg.name || path.basename(dir),
26
+ path: dir,
27
+ });
28
+ }
29
+ catch { }
30
+ }
31
+ // Sort services by name alphabetically
32
+ services.sort((a, b) => a.name.localeCompare(b.name));
33
+ clack.log.info(`${chalk.bold.yellow("Monorepo detected!")} Found ${chalk.bold.cyan(services.length)} nested packages.`);
34
+ const options = services.map((s) => {
35
+ const relPath = path.relative(absoluteRoot, s.path) || ".";
36
+ return {
37
+ value: s.path,
38
+ label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)}`,
39
+ };
40
+ });
41
+ const selectedPath = await clack.select({
42
+ message: "Select a microservice to target:",
43
+ options: [
44
+ ...options,
45
+ { value: "cancel", label: chalk.gray("Cancel / Exit") },
46
+ ],
47
+ });
48
+ if (clack.isCancel(selectedPath) || selectedPath === "cancel") {
49
+ clack.outro("Execution stopped.");
50
+ process.exit(0);
51
+ }
52
+ const relSelected = path.relative(absoluteRoot, selectedPath) || ".";
53
+ clack.log.step(`Target set to: ${chalk.bold.magenta(relSelected)}`);
54
+ console.log(""); // Empty line for spacing
55
+ return selectedPath;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bandit-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,7 +18,7 @@
18
18
  "license": "ISC",
19
19
  "type": "module",
20
20
  "bin": {
21
- "bandit": "./dist/index.js"
21
+ "bandit": "dist/index.js"
22
22
  },
23
23
  "dependencies": {
24
24
  "@clack/prompts": "^1.5.1",