bandit-cli 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ import path from "node:path";
2
+ export function makeContext(projectPath) {
3
+ return {
4
+ projectPath,
5
+ packageJsonPath: path.join(projectPath, "package.json"),
6
+ };
7
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runAudit } from "./core/auditor.js";
4
+ import { printHuman } from "./cli/output.js";
5
+ const program = new Command();
6
+ program
7
+ .name("bandit")
8
+ .description("The Swiss Army Knife for Backend Developers");
9
+ // Subcommand: audit (original behavior)
10
+ program
11
+ .command("audit")
12
+ .description("Audit a backend project directory for basic best-practices")
13
+ .argument("[path]", "Path to the project", ".")
14
+ .option("--json", "Print results as JSON")
15
+ .option("--fail-on-warn", "Exit with code 1 if warnings exist")
16
+ .action(async (projectPath, opts) => {
17
+ const results = await runAudit(projectPath);
18
+ if (opts.json) {
19
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
20
+ }
21
+ else {
22
+ printHuman(results);
23
+ }
24
+ const hasErrors = results.items.some((r) => r.status === "fail" && r.severity === "error");
25
+ const hasWarnings = results.items.some((r) => r.status === "fail" && r.severity === "warn");
26
+ if (hasErrors)
27
+ process.exit(1);
28
+ if (opts.failOnWarn && hasWarnings)
29
+ process.exit(1);
30
+ });
31
+ // Subcommand: ports
32
+ program
33
+ .command("ports")
34
+ .description("Scan active listening ports and interactively terminate locked processes")
35
+ .action(async () => {
36
+ const { runPortsCommand } = await import("./cli/ports.js");
37
+ await runPortsCommand();
38
+ });
39
+ // Subcommand: env
40
+ program
41
+ .command("env")
42
+ .description("Audit local .env configurations against .env.example")
43
+ .argument("[path]", "Path to the project", ".")
44
+ .action(async (projectPath) => {
45
+ const { runEnvCommand } = await import("./cli/env.js");
46
+ await runEnvCommand(projectPath);
47
+ });
48
+ // Subcommand: doctor
49
+ program
50
+ .command("doctor")
51
+ .description("Actively audit a running local server for header security, error leakage, and payload crashes")
52
+ .argument("[url]", "URL of the running server", "http://localhost:3000")
53
+ .action(async (url) => {
54
+ const { runDoctorCommand } = await import("./cli/doctor.js");
55
+ await runDoctorCommand(url);
56
+ });
57
+ // Subcommand: api
58
+ program
59
+ .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) => {
63
+ const { runApiCommand } = await import("./cli/api.js");
64
+ await runApiCommand(projectPath);
65
+ });
66
+ // Subcommand: bench
67
+ program
68
+ .command("bench")
69
+ .description("Benchmark an HTTP endpoint with concurrent connections load testing")
70
+ .argument("<url>", "Target URL to load test")
71
+ .option("-c, --connections <number>", "Number of concurrent connections", "10")
72
+ .option("-r, --requests <number>", "Total number of requests to execute", "200")
73
+ .action(async (url, opts) => {
74
+ const connections = parseInt(opts.connections, 10);
75
+ const requests = parseInt(opts.requests, 10);
76
+ if (isNaN(connections) || connections <= 0) {
77
+ console.error("Error: connections must be a positive integer.");
78
+ process.exit(1);
79
+ }
80
+ if (isNaN(requests) || requests <= 0) {
81
+ console.error("Error: requests must be a positive integer.");
82
+ process.exit(1);
83
+ }
84
+ const { runBenchCommand } = await import("./cli/bench.js");
85
+ await runBenchCommand(url, { connections, requests });
86
+ });
87
+ // Default to help if no command specified
88
+ if (!process.argv.slice(2).length) {
89
+ program.outputHelp();
90
+ process.exit(0);
91
+ }
92
+ program.parse(process.argv);
@@ -0,0 +1,28 @@
1
+ import { rulePackageJsonExists } from "./mvp.rules.js";
2
+ import { ruleEnvExists } from "./mvp.rules.js";
3
+ import { ruleEnvExampleExists } from "./mvp.rules.js";
4
+ import { ruleEnvInGitignore } from "./mvp.rules.js";
5
+ import { ruleDockerfileExists } from "./mvp.rules.js";
6
+ import { ruleHasTestScript } from "./mvp.rules.js";
7
+ import { ruleSrcFolderExists, ruleDetectFramework, ruleSecurityDeps, ruleGlobalErrorHandler, } from "./phase2.rules.js";
8
+ import { ruleLoggingSetup, ruleEnvValidation, ruleDatabaseSetup, ruleTypeScriptStrict, ruleProductionDeps, } from "./phase3.rules.js";
9
+ export const rules = [
10
+ // Phase 1 - MVP checks
11
+ rulePackageJsonExists,
12
+ ruleEnvExists,
13
+ ruleEnvExampleExists,
14
+ ruleEnvInGitignore,
15
+ ruleDockerfileExists,
16
+ ruleHasTestScript,
17
+ // Phase 2 - Structure + detection + security
18
+ ruleSrcFolderExists,
19
+ ruleDetectFramework,
20
+ ruleSecurityDeps,
21
+ ruleGlobalErrorHandler,
22
+ // Phase 3 - Production readiness
23
+ ruleLoggingSetup,
24
+ ruleEnvValidation,
25
+ ruleDatabaseSetup,
26
+ ruleTypeScriptStrict,
27
+ ruleProductionDeps,
28
+ ];
@@ -0,0 +1,138 @@
1
+ // File: src/rules/mvp.rules.ts
2
+ import path from "node:path";
3
+ import { exists, readText } from "../utils/fs.js";
4
+ import { readPackageJson, hasScript } from "../utils/packageJson.js";
5
+ export const rulePackageJsonExists = {
6
+ id: "pkg-json-exists",
7
+ title: "package.json exists",
8
+ severity: "error",
9
+ async run(ctx) {
10
+ const ok = exists(ctx.packageJsonPath);
11
+ return {
12
+ id: this.id,
13
+ title: this.title,
14
+ severity: this.severity,
15
+ status: ok ? "pass" : "fail",
16
+ details: ok ? undefined : "package.json not found.",
17
+ suggestion: ok ? undefined : "Run `npm init -y` in the project root.",
18
+ };
19
+ },
20
+ };
21
+ export const ruleEnvExists = {
22
+ id: "env-exists",
23
+ title: ".env exists",
24
+ severity: "warn",
25
+ async run(ctx) {
26
+ const envPath = path.join(ctx.projectPath, ".env");
27
+ const ok = exists(envPath);
28
+ return {
29
+ id: this.id,
30
+ title: this.title,
31
+ severity: this.severity,
32
+ status: ok ? "pass" : "fail",
33
+ details: ok ? undefined : ".env not found.",
34
+ suggestion: ok ? undefined : "Create a local .env file for development.",
35
+ };
36
+ },
37
+ };
38
+ export const ruleEnvExampleExists = {
39
+ id: "env-example-exists",
40
+ title: ".env.example exists",
41
+ severity: "info",
42
+ async run(ctx) {
43
+ const envExamplePath = path.join(ctx.projectPath, ".env.example");
44
+ const ok = exists(envExamplePath);
45
+ return {
46
+ id: this.id,
47
+ title: this.title,
48
+ severity: this.severity,
49
+ status: ok ? "pass" : "fail",
50
+ details: ok ? undefined : ".env.example not found.",
51
+ suggestion: ok
52
+ ? undefined
53
+ : "Create .env.example listing all required env keys (without real secrets).",
54
+ };
55
+ },
56
+ };
57
+ export const ruleEnvInGitignore = {
58
+ id: "env-in-gitignore",
59
+ title: ".env is ignored in .gitignore",
60
+ severity: "error",
61
+ async run(ctx) {
62
+ const gitignorePath = path.join(ctx.projectPath, ".gitignore");
63
+ const gitignore = readText(gitignorePath);
64
+ if (!gitignore) {
65
+ return {
66
+ id: this.id,
67
+ title: this.title,
68
+ severity: this.severity,
69
+ status: "fail",
70
+ details: ".gitignore not found.",
71
+ suggestion: "Create a .gitignore and add `.env` to it.",
72
+ };
73
+ }
74
+ const lines = gitignore
75
+ .split("\n")
76
+ .map((l) => l.trim())
77
+ .filter((l) => l && !l.startsWith("#"));
78
+ const ok = lines.includes(".env") || lines.includes(".env*");
79
+ return {
80
+ id: this.id,
81
+ title: this.title,
82
+ severity: this.severity,
83
+ status: ok ? "pass" : "fail",
84
+ details: ok ? undefined : "`.env` is not ignored.",
85
+ suggestion: ok
86
+ ? undefined
87
+ : "Add `.env` to .gitignore to avoid leaking secrets.",
88
+ };
89
+ },
90
+ };
91
+ export const ruleDockerfileExists = {
92
+ id: "dockerfile-exists",
93
+ title: "Dockerfile exists",
94
+ severity: "info",
95
+ async run(ctx) {
96
+ const dockerfilePath = path.join(ctx.projectPath, "Dockerfile");
97
+ const ok = exists(dockerfilePath);
98
+ return {
99
+ id: this.id,
100
+ title: this.title,
101
+ severity: this.severity,
102
+ status: ok ? "pass" : "fail",
103
+ details: ok ? undefined : "Dockerfile not found.",
104
+ suggestion: ok
105
+ ? undefined
106
+ : "If you plan to containerize this backend, add a Dockerfile.",
107
+ };
108
+ },
109
+ };
110
+ export const ruleHasTestScript = {
111
+ id: "test-script-exists",
112
+ title: "package.json contains a test script",
113
+ severity: "warn",
114
+ async run(ctx) {
115
+ const pkg = readPackageJson(ctx.packageJsonPath);
116
+ if (!pkg) {
117
+ return {
118
+ id: this.id,
119
+ title: this.title,
120
+ severity: this.severity,
121
+ status: "skip",
122
+ details: "Could not read/parse package.json.",
123
+ suggestion: "Fix package.json JSON syntax.",
124
+ };
125
+ }
126
+ const ok = hasScript(pkg, "test");
127
+ return {
128
+ id: this.id,
129
+ title: this.title,
130
+ severity: this.severity,
131
+ status: ok ? "pass" : "fail",
132
+ details: ok ? undefined : "No `test` script found.",
133
+ suggestion: ok
134
+ ? undefined
135
+ : "Add a test runner (jest/vitest) and a `test` script.",
136
+ };
137
+ },
138
+ };
@@ -0,0 +1,233 @@
1
+ // File: src/rules/phase2.rules.ts
2
+ import path from "node:path";
3
+ import { exists } from "../utils/fs.js";
4
+ import { readPackageJson, hasDependency, detectFramework, } from "../utils/packageJson.js";
5
+ import { scanForErrorHandler } from "../utils/codeScanner.js";
6
+ // Rule 7: src/ folder exists
7
+ export const ruleSrcFolderExists = {
8
+ id: "src-folder-exists",
9
+ title: "src/ folder exists",
10
+ severity: "warn",
11
+ async run(ctx) {
12
+ const srcPath = path.join(ctx.projectPath, "src");
13
+ const ok = exists(srcPath);
14
+ return {
15
+ id: this.id,
16
+ title: this.title,
17
+ severity: this.severity,
18
+ status: ok ? "pass" : "fail",
19
+ details: ok ? undefined : "src/ folder not found.",
20
+ suggestion: ok
21
+ ? undefined
22
+ : "Create a src/ folder to organize your backend code.",
23
+ };
24
+ },
25
+ };
26
+ // Rule 8: Detect backend framework
27
+ export const ruleDetectFramework = {
28
+ id: "detect-framework",
29
+ title: "Detect backend framework",
30
+ severity: "info",
31
+ async run(ctx) {
32
+ const pkg = readPackageJson(ctx.packageJsonPath);
33
+ if (!pkg) {
34
+ return {
35
+ id: this.id,
36
+ title: this.title,
37
+ severity: this.severity,
38
+ status: "skip",
39
+ details: "Could not read package.json.",
40
+ suggestion: "Fix package.json JSON syntax.",
41
+ };
42
+ }
43
+ const framework = detectFramework(pkg);
44
+ // Update context with detected framework for subsequent rules
45
+ ctx.framework = framework;
46
+ if (framework === "unknown") {
47
+ return {
48
+ id: this.id,
49
+ title: this.title,
50
+ severity: this.severity,
51
+ status: "fail",
52
+ details: "No known backend framework detected.",
53
+ suggestion: "Consider using a popular framework like Express, Fastify, NestJS, or Hono.",
54
+ };
55
+ }
56
+ return {
57
+ id: this.id,
58
+ title: this.title,
59
+ severity: this.severity,
60
+ status: "pass",
61
+ details: `Detected framework: ${framework}`,
62
+ };
63
+ },
64
+ };
65
+ // Rule 9: Security dependencies check (framework-dependent)
66
+ export const ruleSecurityDeps = {
67
+ id: "security-deps",
68
+ title: "Security dependencies check",
69
+ severity: "warn",
70
+ async run(ctx) {
71
+ const pkg = readPackageJson(ctx.packageJsonPath);
72
+ if (!pkg) {
73
+ return {
74
+ id: this.id,
75
+ title: this.title,
76
+ severity: this.severity,
77
+ status: "skip",
78
+ details: "Could not read package.json.",
79
+ };
80
+ }
81
+ const framework = ctx.framework || detectFramework(pkg);
82
+ // For Express, check helmet and cors
83
+ if (framework === "express") {
84
+ const hasHelmet = hasDependency(pkg, "helmet");
85
+ const hasCors = hasDependency(pkg, "cors");
86
+ if (!hasHelmet && !hasCors) {
87
+ return {
88
+ id: this.id,
89
+ title: this.title,
90
+ severity: this.severity,
91
+ status: "fail",
92
+ details: "Missing security dependencies: helmet and cors not found.",
93
+ suggestion: "Install: `npm install helmet cors`",
94
+ };
95
+ }
96
+ if (!hasHelmet) {
97
+ return {
98
+ id: this.id,
99
+ title: this.title,
100
+ severity: this.severity,
101
+ status: "fail",
102
+ details: "helmet is not installed.",
103
+ suggestion: "Install: `npm install helmet`",
104
+ };
105
+ }
106
+ if (!hasCors) {
107
+ return {
108
+ id: this.id,
109
+ title: this.title,
110
+ severity: this.severity,
111
+ status: "fail",
112
+ details: "cors is not installed.",
113
+ suggestion: "Install: `npm install cors`",
114
+ };
115
+ }
116
+ return {
117
+ id: this.id,
118
+ title: this.title,
119
+ severity: this.severity,
120
+ status: "pass",
121
+ details: "helmet and cors are installed.",
122
+ };
123
+ }
124
+ // For Fastify
125
+ if (framework === "fastify") {
126
+ const hasHelmet = hasDependency(pkg, "@fastify/helmet");
127
+ const hasCors = hasDependency(pkg, "@fastify/cors");
128
+ if (!hasHelmet && !hasCors) {
129
+ return {
130
+ id: this.id,
131
+ title: this.title,
132
+ severity: this.severity,
133
+ status: "fail",
134
+ details: "Missing security dependencies: @fastify/helmet and @fastify/cors not found.",
135
+ suggestion: "Install: `npm install @fastify/helmet @fastify/cors`",
136
+ };
137
+ }
138
+ if (!hasHelmet) {
139
+ return {
140
+ id: this.id,
141
+ title: this.title,
142
+ severity: this.severity,
143
+ status: "fail",
144
+ details: "@fastify/helmet is not installed.",
145
+ suggestion: "Install: `npm install @fastify/helmet`",
146
+ };
147
+ }
148
+ if (!hasCors) {
149
+ return {
150
+ id: this.id,
151
+ title: this.title,
152
+ severity: this.severity,
153
+ status: "fail",
154
+ details: "@fastify/cors is not installed.",
155
+ suggestion: "Install: `npm install @fastify/cors`",
156
+ };
157
+ }
158
+ return {
159
+ id: this.id,
160
+ title: this.title,
161
+ severity: this.severity,
162
+ status: "pass",
163
+ details: "@fastify/helmet and @fastify/cors are installed.",
164
+ };
165
+ }
166
+ // For NestJS, security is built-in but can check helmet
167
+ if (framework === "nest") {
168
+ return {
169
+ id: this.id,
170
+ title: this.title,
171
+ severity: this.severity,
172
+ status: "pass",
173
+ details: "NestJS has built-in security features. Consider adding helmet.",
174
+ };
175
+ }
176
+ // For other frameworks or unknown
177
+ return {
178
+ id: this.id,
179
+ title: this.title,
180
+ severity: this.severity,
181
+ status: "skip",
182
+ details: `Security checks not defined for framework: ${framework}`,
183
+ };
184
+ },
185
+ };
186
+ // Rule 10: Global error handler present
187
+ export const ruleGlobalErrorHandler = {
188
+ id: "global-error-handler",
189
+ title: "Global error handler present",
190
+ severity: "warn",
191
+ async run(ctx) {
192
+ const srcPath = path.join(ctx.projectPath, "src");
193
+ if (!exists(srcPath)) {
194
+ return {
195
+ id: this.id,
196
+ title: this.title,
197
+ severity: this.severity,
198
+ status: "skip",
199
+ details: "src/ folder not found, skipping code scan.",
200
+ };
201
+ }
202
+ const framework = ctx.framework || "unknown";
203
+ // For Express, look for error handler middleware pattern
204
+ if (framework === "express") {
205
+ const found = await scanForErrorHandler(srcPath);
206
+ if (!found) {
207
+ return {
208
+ id: this.id,
209
+ title: this.title,
210
+ severity: this.severity,
211
+ status: "fail",
212
+ details: "No global error handler middleware found.",
213
+ suggestion: "Add app.use((err, req, res, next) => {...}) after all routes.",
214
+ };
215
+ }
216
+ return {
217
+ id: this.id,
218
+ title: this.title,
219
+ severity: this.severity,
220
+ status: "pass",
221
+ details: "Global error handler detected.",
222
+ };
223
+ }
224
+ // For other frameworks, skip for now
225
+ return {
226
+ id: this.id,
227
+ title: this.title,
228
+ severity: this.severity,
229
+ status: "skip",
230
+ details: `Error handler check not implemented for framework: ${framework}`,
231
+ };
232
+ },
233
+ };