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 +32 -8
- package/dist/cli/api.js +495 -49
- package/dist/cli/doctor.js +169 -0
- package/dist/cli/output.js +41 -58
- package/dist/index.js +22 -6
- package/dist/utils/monorepo.js +56 -0
- package/package.json +2 -2
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
// 1
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
324
|
+
routePath: rPath,
|
|
58
325
|
sourceFile: relativePath,
|
|
59
326
|
});
|
|
60
327
|
}
|
|
61
328
|
}
|
|
62
|
-
catch
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
258
|
-
message: "
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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(
|
|
619
|
+
if (clack.isCancel(bodyInputType)) {
|
|
273
620
|
clack.outro("Execution stopped.");
|
|
274
621
|
return;
|
|
275
622
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
//
|
|
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}`;
|
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) {
|
package/dist/cli/output.js
CHANGED
|
@@ -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(
|
|
10
|
+
console.log(chalk.gray(" " + "─".repeat(70)));
|
|
25
11
|
}
|
|
26
12
|
function printItem(item) {
|
|
27
13
|
const statusIcon = icon(item.status);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(`
|
|
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.
|
|
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.
|
|
53
|
-
console.log(chalk.bold.white(
|
|
54
|
-
console.log(chalk.
|
|
55
|
-
console.log(chalk.gray(
|
|
56
|
-
console.log(chalk.gray(
|
|
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
|
-
|
|
61
|
-
chalk.
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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(
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(
|
|
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.
|
|
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(`
|
|
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
|
|
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(
|
|
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("
|
|
61
|
-
.argument("[
|
|
62
|
-
.
|
|
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(
|
|
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.
|
|
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": "
|
|
21
|
+
"bandit": "dist/index.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@clack/prompts": "^1.5.1",
|