bandit-cli 1.0.3 → 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 +3 -0
- package/dist/cli/api.js +266 -31
- package/dist/cli/doctor.js +169 -0
- package/dist/cli/output.js +41 -58
- package/dist/index.js +14 -3
- package/dist/utils/monorepo.js +56 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- **Live Server Doctor**: Probes your running server for security headers, CORS wildcards, stack trace/error leakage, and payload crash handling.
|
|
15
15
|
- **Environment Variable Auditor**: Compares active `.env` files against `.env.example` to flag missing keys, empty values, and placeholder defaults before you boot.
|
|
16
16
|
- **Static Project Auditor**: Evaluates package setup, file organization, safety configs, and dependency scopes.
|
|
17
|
+
- **Monorepo & Microservices Auto-Discovery**: Automatically scans your workspace for nested packages containing `package.json` and interactively prompts you to target specific services.
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -38,11 +39,13 @@ npx bandit-cli <command>
|
|
|
38
39
|
### 1. Interactive API Playfield
|
|
39
40
|
|
|
40
41
|
Find and test routes in your codebase interactively:
|
|
42
|
+
|
|
41
43
|
```bash
|
|
42
44
|
bandit api
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Or execute a direct API request immediately (acts like a local curl/HTTPie):
|
|
48
|
+
|
|
46
49
|
```bash
|
|
47
50
|
bandit api GET /api/v1/products --url http://localhost:8080 --token "jwt-token"
|
|
48
51
|
bandit api POST /api/v1/users --body '{"name":"Raj"}'
|
package/dist/cli/api.js
CHANGED
|
@@ -3,41 +3,278 @@ import path from "node:path";
|
|
|
3
3
|
import fg from "fast-glob";
|
|
4
4
|
import * as clack from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
|
+
function normalizePath(filePath) {
|
|
7
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
8
|
+
const winDriveRegex = /^([A-Z]):\//;
|
|
9
|
+
const match = normalized.match(winDriveRegex);
|
|
10
|
+
if (match) {
|
|
11
|
+
normalized = normalized.replace(winDriveRegex, `${match[1].toLowerCase()}:/`);
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
function resolveImport(sourceFile, importPath, projectPath) {
|
|
16
|
+
let absolutePath = "";
|
|
17
|
+
if (importPath.startsWith("@/") || importPath.startsWith("~/")) {
|
|
18
|
+
const relativePart = importPath.slice(2);
|
|
19
|
+
absolutePath = path.resolve(projectPath, "src", relativePart);
|
|
20
|
+
}
|
|
21
|
+
else if (importPath.startsWith("src/")) {
|
|
22
|
+
absolutePath = path.resolve(projectPath, importPath);
|
|
23
|
+
}
|
|
24
|
+
else if (importPath.startsWith(".")) {
|
|
25
|
+
const sourceDir = path.dirname(sourceFile);
|
|
26
|
+
absolutePath = path.resolve(sourceDir, importPath);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const extensions = [".ts", ".js", ".tsx", ".jsx"];
|
|
32
|
+
for (const ext of extensions) {
|
|
33
|
+
const fileWithExt = absolutePath + ext;
|
|
34
|
+
if (fs.existsSync(fileWithExt)) {
|
|
35
|
+
return normalizePath(fileWithExt);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
39
|
+
for (const ext of extensions) {
|
|
40
|
+
const indexFile = path.join(absolutePath, "index" + ext);
|
|
41
|
+
if (fs.existsSync(indexFile)) {
|
|
42
|
+
return normalizePath(indexFile);
|
|
43
|
+
}
|
|
44
|
+
const routerFile = path.join(absolutePath, "router" + ext);
|
|
45
|
+
if (fs.existsSync(routerFile)) {
|
|
46
|
+
return normalizePath(routerFile);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function parseImportNames(importClause) {
|
|
53
|
+
const names = [];
|
|
54
|
+
importClause = importClause.trim();
|
|
55
|
+
const nsMatch = importClause.match(/\*\s+as\s+([a-zA-Z0-9_$]+)/);
|
|
56
|
+
if (nsMatch) {
|
|
57
|
+
names.push({ localName: nsMatch[1] });
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
const destructureMatch = importClause.match(/\{([\s\S]*?)\}/);
|
|
61
|
+
if (destructureMatch) {
|
|
62
|
+
const content = destructureMatch[1];
|
|
63
|
+
const parts = content.split(",");
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
const trimmed = part.trim();
|
|
66
|
+
if (!trimmed)
|
|
67
|
+
continue;
|
|
68
|
+
const aliasMatch = trimmed.match(/([a-zA-Z0-9_$]+)\s+as\s+([a-zA-Z0-9_$]+)/);
|
|
69
|
+
if (aliasMatch) {
|
|
70
|
+
names.push({ localName: aliasMatch[2] });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const nameMatch = trimmed.match(/^([a-zA-Z0-9_$]+)$/);
|
|
74
|
+
if (nameMatch) {
|
|
75
|
+
names.push({ localName: nameMatch[1] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const beforeDestruct = importClause.split("{")[0].trim().replace(/,$/, "").trim();
|
|
80
|
+
if (beforeDestruct && !beforeDestruct.startsWith("import")) {
|
|
81
|
+
names.push({ localName: beforeDestruct });
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
if (importClause && !importClause.includes("{") && !importClause.includes("*")) {
|
|
86
|
+
names.push({ localName: importClause });
|
|
87
|
+
}
|
|
88
|
+
return names;
|
|
89
|
+
}
|
|
90
|
+
function resolvePaths(pref, p) {
|
|
91
|
+
let combined = `${pref}/${p}`.replace(/\/+/g, "/");
|
|
92
|
+
if (!combined.startsWith("/"))
|
|
93
|
+
combined = "/" + combined;
|
|
94
|
+
if (combined.length > 1 && combined.endsWith("/")) {
|
|
95
|
+
combined = combined.slice(0, -1);
|
|
96
|
+
}
|
|
97
|
+
return combined;
|
|
98
|
+
}
|
|
6
99
|
// Scans files for common route definitions and mounts
|
|
7
|
-
async function scanForRoutes(projectPath) {
|
|
100
|
+
export async function scanForRoutes(projectPath) {
|
|
101
|
+
projectPath = normalizePath(projectPath);
|
|
8
102
|
const routes = [];
|
|
9
|
-
const files = await fg(["src/**/*.{ts,js}", "*.{ts,js}", "routes/**/*.{ts,js}", "controllers/**/*.{ts,js}"], {
|
|
103
|
+
const files = (await fg(["src/**/*.{ts,js}", "*.{ts,js}", "routes/**/*.{ts,js}", "controllers/**/*.{ts,js}"], {
|
|
10
104
|
cwd: projectPath,
|
|
11
105
|
absolute: true,
|
|
12
106
|
ignore: ["**/node_modules/**", "**/dist/**", "**/*.test.{ts,js}", "**/*.spec.{ts,js}"],
|
|
13
|
-
});
|
|
14
|
-
const
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
107
|
+
})).map(normalizePath);
|
|
108
|
+
const importsByFile = new Map();
|
|
109
|
+
// Step 1: Scan imports & requires for all files
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const localToTarget = new Map();
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
114
|
+
const esImportRegex = /import\s+([\s\S]*?)\s+from\s+['"`]([^'"`]+)['"`]/g;
|
|
115
|
+
let match;
|
|
116
|
+
while ((match = esImportRegex.exec(content)) !== null) {
|
|
117
|
+
const importClause = match[1];
|
|
118
|
+
const importPath = match[2];
|
|
119
|
+
const targetFile = resolveImport(file, importPath, projectPath);
|
|
120
|
+
if (targetFile) {
|
|
121
|
+
const names = parseImportNames(importClause);
|
|
122
|
+
for (const name of names) {
|
|
123
|
+
localToTarget.set(name.localName, targetFile);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const requireRegex = /(?:const|let|var)\s+([\s\S]*?)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
128
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
129
|
+
const varClause = match[1];
|
|
130
|
+
const importPath = match[2];
|
|
131
|
+
const targetFile = resolveImport(file, importPath, projectPath);
|
|
132
|
+
if (targetFile) {
|
|
133
|
+
const names = parseImportNames(varClause);
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
localToTarget.set(name.localName, targetFile);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { }
|
|
141
|
+
importsByFile.set(file, localToTarget);
|
|
142
|
+
}
|
|
143
|
+
// Step 2: Build Dependency Graph Nodes and Edges
|
|
144
|
+
const nodes = new Map();
|
|
145
|
+
const edges = [];
|
|
146
|
+
const ensureNode = (id, type, filePath, varName) => {
|
|
147
|
+
if (!nodes.has(id)) {
|
|
148
|
+
nodes.set(id, { id, type, filePath, varName, prefixes: new Set() });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// Create file nodes
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
ensureNode(`file:${file}`, "file", file);
|
|
154
|
+
}
|
|
155
|
+
// Pre-populate route variable nodes from route declarations
|
|
156
|
+
const routeVarPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(/gi;
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
try {
|
|
159
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
160
|
+
let match;
|
|
161
|
+
routeVarPattern.lastIndex = 0;
|
|
162
|
+
while ((match = routeVarPattern.exec(content)) !== null) {
|
|
163
|
+
const instanceName = match[1];
|
|
164
|
+
ensureNode(`var:${file}:${instanceName}`, "var", file, instanceName);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
}
|
|
169
|
+
const mountPattern = /\b([a-zA-Z0-9_]+)\.(use|route|group)\s*\(\s*(?:['"`]([^'"`]+)['"`]\s*,\s*)?(?:require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|([a-zA-Z0-9_]+))/gi;
|
|
19
170
|
for (const file of files) {
|
|
20
171
|
try {
|
|
21
172
|
const content = fs.readFileSync(file, "utf-8");
|
|
22
173
|
let match;
|
|
23
174
|
mountPattern.lastIndex = 0;
|
|
24
175
|
while ((match = mountPattern.exec(content)) !== null) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
176
|
+
const parentVar = match[1];
|
|
177
|
+
const mountPath = match[3] || "/";
|
|
178
|
+
const inlineRequire = match[4];
|
|
179
|
+
const childVar = match[5];
|
|
180
|
+
const parentId = `var:${file}:${parentVar}`;
|
|
181
|
+
ensureNode(parentId, "var", file, parentVar);
|
|
182
|
+
if (inlineRequire) {
|
|
183
|
+
const targetFile = resolveImport(file, inlineRequire, projectPath);
|
|
184
|
+
if (targetFile) {
|
|
185
|
+
const targetFileId = `file:${targetFile}`;
|
|
186
|
+
ensureNode(targetFileId, "file", targetFile);
|
|
187
|
+
edges.push({ from: parentId, to: targetFileId, path: mountPath });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (childVar) {
|
|
191
|
+
const childId = `var:${file}:${childVar}`;
|
|
192
|
+
ensureNode(childId, "var", file, childVar);
|
|
193
|
+
edges.push({ from: parentId, to: childId, path: mountPath });
|
|
194
|
+
// Check if childVar is imported
|
|
195
|
+
const targetFile = importsByFile.get(file)?.get(childVar);
|
|
196
|
+
if (targetFile) {
|
|
197
|
+
const targetFileId = `file:${targetFile}`;
|
|
198
|
+
ensureNode(targetFileId, "file", targetFile);
|
|
199
|
+
edges.push({ from: childId, to: targetFileId, path: "/" });
|
|
200
|
+
}
|
|
29
201
|
}
|
|
30
202
|
}
|
|
31
203
|
}
|
|
32
204
|
catch { }
|
|
33
205
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
206
|
+
// Step 3: Link file nodes to their root variables within that file
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
const fileId = `file:${file}`;
|
|
209
|
+
// Find all variable nodes in this file
|
|
210
|
+
const varNodesInFile = Array.from(nodes.values()).filter((n) => n.type === "var" && n.filePath === file);
|
|
211
|
+
for (const vNode of varNodesInFile) {
|
|
212
|
+
// Is there an incoming edge to this variable from another variable/file in the same file?
|
|
213
|
+
const hasIncomingLocal = edges.some((e) => e.to === vNode.id && e.from.startsWith(`var:${file}:`));
|
|
214
|
+
if (!hasIncomingLocal) {
|
|
215
|
+
// It is a root variable in this file, link the file node to it
|
|
216
|
+
edges.push({ from: fileId, to: vNode.id, path: "/" });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Step 4: Identify entry points and initialize their prefixes
|
|
221
|
+
// Entry points are file nodes that have no incoming edges from OTHER files
|
|
222
|
+
const fileIncomingFromOthers = new Set();
|
|
223
|
+
for (const edge of edges) {
|
|
224
|
+
if (edge.to.startsWith("file:")) {
|
|
225
|
+
const fromFile = nodes.get(edge.from)?.filePath;
|
|
226
|
+
const toFile = nodes.get(edge.to)?.filePath;
|
|
227
|
+
if (fromFile && toFile && fromFile !== toFile) {
|
|
228
|
+
fileIncomingFromOthers.add(edge.to);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const file of files) {
|
|
233
|
+
const fileId = `file:${file}`;
|
|
234
|
+
if (!fileIncomingFromOthers.has(fileId)) {
|
|
235
|
+
const fNode = nodes.get(fileId);
|
|
236
|
+
if (fNode) {
|
|
237
|
+
fNode.prefixes.add("/");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Step 5: Propagate prefixes using BFS
|
|
242
|
+
const queue = [];
|
|
243
|
+
for (const node of nodes.values()) {
|
|
244
|
+
if (node.prefixes.size > 0) {
|
|
245
|
+
queue.push(node.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const inQueue = new Set(queue);
|
|
249
|
+
while (queue.length > 0) {
|
|
250
|
+
const currId = queue.shift();
|
|
251
|
+
inQueue.delete(currId);
|
|
252
|
+
const currNode = nodes.get(currId);
|
|
253
|
+
if (!currNode)
|
|
254
|
+
continue;
|
|
255
|
+
// Find outgoing edges
|
|
256
|
+
const outgoing = edges.filter((e) => e.from === currId);
|
|
257
|
+
for (const edge of outgoing) {
|
|
258
|
+
const targetNode = nodes.get(edge.to);
|
|
259
|
+
if (!targetNode)
|
|
260
|
+
continue;
|
|
261
|
+
let changed = false;
|
|
262
|
+
for (const pref of currNode.prefixes) {
|
|
263
|
+
const combined = resolvePaths(pref, edge.path);
|
|
264
|
+
if (!targetNode.prefixes.has(combined)) {
|
|
265
|
+
targetNode.prefixes.add(combined);
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (changed && !inQueue.has(edge.to)) {
|
|
270
|
+
queue.push(edge.to);
|
|
271
|
+
inQueue.add(edge.to);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Step 6: Scan files for actual routes and use propagated prefixes
|
|
37
276
|
const expressPattern = /\b([a-zA-Z0-9_]+)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
38
|
-
// 2. NestJS decorators: @Get('/path')
|
|
39
277
|
const nestPattern = /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
40
|
-
// 3. NestJS controllers: @Controller('/prefix')
|
|
41
278
|
const controllerPattern = /@Controller\s*\(\s*['"`]([^'"`\s]+)['"`]/gi;
|
|
42
279
|
for (const file of files) {
|
|
43
280
|
try {
|
|
@@ -58,21 +295,19 @@ async function scanForRoutes(projectPath) {
|
|
|
58
295
|
const instanceName = match[1];
|
|
59
296
|
const method = match[2].toUpperCase();
|
|
60
297
|
const subPath = match[3];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
298
|
+
const varNodeId = `var:${file}:${instanceName}`;
|
|
299
|
+
const varNode = nodes.get(varNodeId);
|
|
300
|
+
const targetPrefixes = (varNode && varNode.prefixes.size > 0)
|
|
301
|
+
? Array.from(varNode.prefixes)
|
|
302
|
+
: ["/"];
|
|
303
|
+
for (const prefix of targetPrefixes) {
|
|
304
|
+
const rPath = resolvePaths(prefix, subPath);
|
|
305
|
+
routes.push({
|
|
306
|
+
method,
|
|
307
|
+
routePath: rPath,
|
|
308
|
+
sourceFile: relativePath,
|
|
309
|
+
});
|
|
66
310
|
}
|
|
67
|
-
// Clean up double slashes
|
|
68
|
-
rPath = rPath.replace(/\/+/g, "/");
|
|
69
|
-
if (!rPath.startsWith("/"))
|
|
70
|
-
rPath = `/${rPath}`;
|
|
71
|
-
routes.push({
|
|
72
|
-
method,
|
|
73
|
-
routePath: rPath,
|
|
74
|
-
sourceFile: relativePath,
|
|
75
|
-
});
|
|
76
311
|
}
|
|
77
312
|
// 2. NestJS
|
|
78
313
|
nestPattern.lastIndex = 0;
|
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
|
|
@@ -65,8 +69,15 @@ program
|
|
|
65
69
|
.option("-t, --token <token>", "Bearer Auth token")
|
|
66
70
|
.option("-H, --header <headers...>", "Custom headers in Key:Value format")
|
|
67
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
|
+
}
|
|
68
79
|
const { runApiCommand } = await import("./cli/api.js");
|
|
69
|
-
await runApiCommand(
|
|
80
|
+
await runApiCommand(targetPath, routePath, opts);
|
|
70
81
|
});
|
|
71
82
|
// Subcommand: bench
|
|
72
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",
|