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.
- package/README.md +1 -0
- package/dist/cli/api.js +374 -0
- package/dist/cli/bench.js +135 -0
- package/dist/cli/doctor.js +231 -0
- package/dist/cli/env.js +108 -0
- package/dist/cli/output.js +138 -0
- package/dist/cli/ports.js +177 -0
- package/dist/core/auditor.js +32 -0
- package/dist/core/context.js +7 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +92 -0
- package/dist/rules/index.js +28 -0
- package/dist/rules/mvp.rules.js +138 -0
- package/dist/rules/phase2.rules.js +233 -0
- package/dist/rules/phase3.rules.js +261 -0
- package/dist/utils/codeScanner.js +66 -0
- package/dist/utils/fs.js +13 -0
- package/dist/utils/packageJson.js +54 -0
- package/package.json +38 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = 4000) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const id = setTimeout(() => controller.abort(), timeoutMs);
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(url, {
|
|
8
|
+
...options,
|
|
9
|
+
signal: controller.signal,
|
|
10
|
+
});
|
|
11
|
+
clearTimeout(id);
|
|
12
|
+
return res;
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
clearTimeout(id);
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function runDoctorCommand(targetUrl = "http://localhost:3000") {
|
|
20
|
+
clack.intro(chalk.bold.bgBlue.white(" Bandit Live Server Doctor "));
|
|
21
|
+
clack.log.info(`Target URL: ${chalk.bold.cyan(targetUrl)}`);
|
|
22
|
+
const s = clack.spinner();
|
|
23
|
+
s.start("Connecting to target server...");
|
|
24
|
+
let rootResponse;
|
|
25
|
+
try {
|
|
26
|
+
rootResponse = await fetchWithTimeout(targetUrl);
|
|
27
|
+
s.stop("Connection established.");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
s.stop("Connection failed.");
|
|
31
|
+
clack.log.error(`Could not connect to ${targetUrl}: ${err.message}`);
|
|
32
|
+
clack.log.info("Make sure your backend server is running locally and listening on the specified port.");
|
|
33
|
+
clack.outro(chalk.red("Execution stopped."));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const reports = [];
|
|
37
|
+
// 1. Audit Security Headers
|
|
38
|
+
s.start("Auditing response headers...");
|
|
39
|
+
const headers = rootResponse.headers;
|
|
40
|
+
// X-Powered-By
|
|
41
|
+
const xPoweredBy = headers.get("x-powered-by");
|
|
42
|
+
if (xPoweredBy) {
|
|
43
|
+
reports.push({
|
|
44
|
+
title: "Information Disclosure (X-Powered-By)",
|
|
45
|
+
status: "warn",
|
|
46
|
+
details: `Leaks software stack info: ${xPoweredBy}`,
|
|
47
|
+
suggestion: "Disable X-Powered-By header (e.g. `app.disable('x-powered-by')` in Express or use Helmet).",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
reports.push({
|
|
52
|
+
title: "Information Disclosure (X-Powered-By)",
|
|
53
|
+
status: "pass",
|
|
54
|
+
details: "X-Powered-By header is hidden.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// X-Frame-Options
|
|
58
|
+
const xFrameOptions = headers.get("x-frame-options");
|
|
59
|
+
if (!xFrameOptions || (!xFrameOptions.toLowerCase().includes("deny") && !xFrameOptions.toLowerCase().includes("sameorigin"))) {
|
|
60
|
+
reports.push({
|
|
61
|
+
title: "Clickjacking Protection (X-Frame-Options)",
|
|
62
|
+
status: "warn",
|
|
63
|
+
details: xFrameOptions ? `Weak policy: ${xFrameOptions}` : "Header missing",
|
|
64
|
+
suggestion: "Set X-Frame-Options to DENY or SAMEORIGIN.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
reports.push({
|
|
69
|
+
title: "Clickjacking Protection (X-Frame-Options)",
|
|
70
|
+
status: "pass",
|
|
71
|
+
details: `Configured: ${xFrameOptions}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// X-Content-Type-Options
|
|
75
|
+
const xContentTypeOptions = headers.get("x-content-type-options");
|
|
76
|
+
if (!xContentTypeOptions || xContentTypeOptions.toLowerCase() !== "nosniff") {
|
|
77
|
+
reports.push({
|
|
78
|
+
title: "MIME-Sniffing Protection (X-Content-Type-Options)",
|
|
79
|
+
status: "warn",
|
|
80
|
+
details: xContentTypeOptions ? `Weak policy: ${xContentTypeOptions}` : "Header missing",
|
|
81
|
+
suggestion: "Set X-Content-Type-Options to 'nosniff'.",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
reports.push({
|
|
86
|
+
title: "MIME-Sniffing Protection (X-Content-Type-Options)",
|
|
87
|
+
status: "pass",
|
|
88
|
+
details: "Configured: nosniff",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// CORS Access-Control-Allow-Origin
|
|
92
|
+
const corsOrigin = headers.get("access-control-allow-origin");
|
|
93
|
+
if (corsOrigin === "*") {
|
|
94
|
+
reports.push({
|
|
95
|
+
title: "CORS Wildcard Check",
|
|
96
|
+
status: "warn",
|
|
97
|
+
details: "Access-Control-Allow-Origin is set to wildcard '*'",
|
|
98
|
+
suggestion: "For APIs processing cookies or credentials, restrict origin to specific domains.",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
reports.push({
|
|
103
|
+
title: "CORS Wildcard Check",
|
|
104
|
+
status: "pass",
|
|
105
|
+
details: corsOrigin ? `Restricted to: ${corsOrigin}` : "CORS header not present on root (standard behavior for non-CORS requests).",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
s.stop("Response headers audited.");
|
|
109
|
+
// 2. Audit Error Stack Leakage
|
|
110
|
+
s.start("Checking for error stack trace leakage...");
|
|
111
|
+
const dummyUrl = `${targetUrl.replace(/\/$/, "")}/non-existent-bandit-route-${Math.floor(Math.random() * 100000)}`;
|
|
112
|
+
try {
|
|
113
|
+
const errorResponse = await fetchWithTimeout(dummyUrl);
|
|
114
|
+
const bodyText = await errorResponse.text();
|
|
115
|
+
const stackTraceIndicators = [
|
|
116
|
+
"at ",
|
|
117
|
+
"node_modules",
|
|
118
|
+
"Stack Trace",
|
|
119
|
+
"TypeError",
|
|
120
|
+
"ReferenceError",
|
|
121
|
+
"SyntaxError",
|
|
122
|
+
"InternalServerError",
|
|
123
|
+
"sqlite3",
|
|
124
|
+
"mongodb",
|
|
125
|
+
"postgresql",
|
|
126
|
+
"sequelize",
|
|
127
|
+
"prisma",
|
|
128
|
+
];
|
|
129
|
+
const foundIndicators = stackTraceIndicators.filter((indicator) => bodyText.includes(indicator));
|
|
130
|
+
if (foundIndicators.length > 0) {
|
|
131
|
+
reports.push({
|
|
132
|
+
title: "Stack Trace Leakage Check",
|
|
133
|
+
status: "fail",
|
|
134
|
+
details: `Server leaked stack trace/debug indicators: [${foundIndicators.join(", ")}]`,
|
|
135
|
+
suggestion: "Ensure env is set to production or capture errors globally and hide stack traces from clients.",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
reports.push({
|
|
140
|
+
title: "Stack Trace Leakage Check",
|
|
141
|
+
status: "pass",
|
|
142
|
+
details: "No stack traces or internal details leaked in error response.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
reports.push({
|
|
148
|
+
title: "Stack Trace Leakage Check",
|
|
149
|
+
status: "warn",
|
|
150
|
+
details: `Failed to probe error response: ${err.message}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
s.stop("Error leakage probe complete.");
|
|
154
|
+
// 3. Oversized Payload Handling
|
|
155
|
+
s.start("Probing oversized payload response...");
|
|
156
|
+
try {
|
|
157
|
+
// 5MB payload
|
|
158
|
+
const largePayload = "A".repeat(5 * 1024 * 1024);
|
|
159
|
+
const postOptions = {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": "application/json",
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({ data: largePayload }),
|
|
165
|
+
};
|
|
166
|
+
const payloadResponse = await fetchWithTimeout(targetUrl, postOptions, 5000);
|
|
167
|
+
if (payloadResponse.status === 413) {
|
|
168
|
+
reports.push({
|
|
169
|
+
title: "Oversized Payload Protection",
|
|
170
|
+
status: "pass",
|
|
171
|
+
details: "Server rejected oversized payload with 413 Payload Too Large.",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if (payloadResponse.status >= 200 && payloadResponse.status < 300) {
|
|
175
|
+
reports.push({
|
|
176
|
+
title: "Oversized Payload Protection",
|
|
177
|
+
status: "warn",
|
|
178
|
+
details: `Server accepted a 5MB JSON payload (Status: ${payloadResponse.status})`,
|
|
179
|
+
suggestion: "Set a body limit on your parser (e.g. limit json parser to '1mb' or '100kb').",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
reports.push({
|
|
184
|
+
title: "Oversized Payload Protection",
|
|
185
|
+
status: "pass",
|
|
186
|
+
details: `Server responded with ${payloadResponse.status} ${payloadResponse.statusText}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err.name === "AbortError") {
|
|
192
|
+
reports.push({
|
|
193
|
+
title: "Oversized Payload Protection",
|
|
194
|
+
status: "fail",
|
|
195
|
+
details: "Server timed out (>5s) or hung when processing a 5MB payload.",
|
|
196
|
+
suggestion: "Add payload size limits to parser middleware (e.g., body-parser) to prevent DoS.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
reports.push({
|
|
201
|
+
title: "Oversized Payload Protection",
|
|
202
|
+
status: "warn",
|
|
203
|
+
details: `Connection closed / error during payload transfer: ${err.message}`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
s.stop("Oversized payload probe complete.");
|
|
208
|
+
// Print results
|
|
209
|
+
console.log("\n" + chalk.bold("Diagnostic Results:"));
|
|
210
|
+
for (const report of reports) {
|
|
211
|
+
let statusPrefix = "";
|
|
212
|
+
if (report.status === "pass") {
|
|
213
|
+
statusPrefix = chalk.green("✔ [PASS]");
|
|
214
|
+
}
|
|
215
|
+
else if (report.status === "warn") {
|
|
216
|
+
statusPrefix = chalk.yellow("⚠ [WARN]");
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
statusPrefix = chalk.red("✖ [FAIL]");
|
|
220
|
+
}
|
|
221
|
+
console.log(`${statusPrefix} ${chalk.bold(report.title)}`);
|
|
222
|
+
if (report.details) {
|
|
223
|
+
console.log(chalk.gray(` ↳ ${report.details}`));
|
|
224
|
+
}
|
|
225
|
+
if (report.status !== "pass" && report.suggestion) {
|
|
226
|
+
console.log(chalk.cyan(` ↳ Suggestion: ${report.suggestion}`));
|
|
227
|
+
}
|
|
228
|
+
console.log("");
|
|
229
|
+
}
|
|
230
|
+
clack.outro(chalk.bold.green("Doctor diagnostic scan completed."));
|
|
231
|
+
}
|
package/dist/cli/env.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
function parseEnvContent(content) {
|
|
6
|
+
const map = new Map();
|
|
7
|
+
const lines = content.split("\n");
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
11
|
+
continue;
|
|
12
|
+
const firstEqual = trimmed.indexOf("=");
|
|
13
|
+
if (firstEqual === -1)
|
|
14
|
+
continue;
|
|
15
|
+
const key = trimmed.slice(0, firstEqual).trim();
|
|
16
|
+
let val = trimmed.slice(firstEqual + 1).trim();
|
|
17
|
+
// Strip quotes if any
|
|
18
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
19
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
20
|
+
val = val.slice(1, -1);
|
|
21
|
+
}
|
|
22
|
+
map.set(key, val);
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
export async function runEnvCommand(projectPath = ".") {
|
|
27
|
+
clack.intro(chalk.bold.bgBlue.white(" Bandit Environment Auditor "));
|
|
28
|
+
const absoluteProjectPath = path.resolve(projectPath);
|
|
29
|
+
const examplePath = path.join(absoluteProjectPath, ".env.example");
|
|
30
|
+
if (!fs.existsSync(examplePath)) {
|
|
31
|
+
clack.log.warn(`No ${chalk.cyan(".env.example")} file found in the root of the project.`);
|
|
32
|
+
clack.log.info("Create a .env.example containing your environment variable keys as placeholders.");
|
|
33
|
+
clack.outro(chalk.red("Failed to audit environment variables."));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let exampleKeysMap;
|
|
37
|
+
try {
|
|
38
|
+
const exampleContent = fs.readFileSync(examplePath, "utf-8");
|
|
39
|
+
exampleKeysMap = parseEnvContent(exampleContent);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
clack.log.error(`Failed to read .env.example: ${err.message}`);
|
|
43
|
+
clack.outro(chalk.red("Failed to audit environment variables."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const exampleKeys = Array.from(exampleKeysMap.keys());
|
|
47
|
+
clack.log.info(`Found ${chalk.bold.cyan(exampleKeys.length)} keys in .env.example`);
|
|
48
|
+
// Scan for active .env files
|
|
49
|
+
const files = fs.readdirSync(absoluteProjectPath);
|
|
50
|
+
const envFiles = files.filter((f) => f === ".env" || (f.startsWith(".env.") && !f.endsWith(".example")));
|
|
51
|
+
if (envFiles.length === 0) {
|
|
52
|
+
clack.log.warn("No active environment configuration files (like `.env`, `.env.local`, etc.) detected.");
|
|
53
|
+
clack.outro(chalk.yellow("Done."));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const envFile of envFiles) {
|
|
57
|
+
const envFilePath = path.join(absoluteProjectPath, envFile);
|
|
58
|
+
clack.log.step(`Auditing ${chalk.bold.magenta(envFile)}...`);
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(envFilePath, "utf-8");
|
|
61
|
+
const currentKeysMap = parseEnvContent(content);
|
|
62
|
+
const missingKeys = [];
|
|
63
|
+
const emptyKeys = [];
|
|
64
|
+
const extraKeys = [];
|
|
65
|
+
// Check for missing or empty keys
|
|
66
|
+
for (const expectedKey of exampleKeys) {
|
|
67
|
+
if (!currentKeysMap.has(expectedKey)) {
|
|
68
|
+
missingKeys.push(expectedKey);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const val = currentKeysMap.get(expectedKey);
|
|
72
|
+
if (val === "" || val === "your_key_here" || val === "placeholder" || val.includes("TODO")) {
|
|
73
|
+
emptyKeys.push(expectedKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Check for extra keys (not defined in .env.example)
|
|
78
|
+
for (const currentKey of currentKeysMap.keys()) {
|
|
79
|
+
if (!exampleKeysMap.has(currentKey)) {
|
|
80
|
+
extraKeys.push(currentKey);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Output report for this file
|
|
84
|
+
if (missingKeys.length === 0 && emptyKeys.length === 0 && extraKeys.length === 0) {
|
|
85
|
+
clack.log.success(chalk.green(` ✔ ${envFile} is perfectly in sync with .env.example!`));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (missingKeys.length > 0) {
|
|
89
|
+
clack.log.error(` ❌ Missing keys (required by .env.example):\n` +
|
|
90
|
+
missingKeys.map((k) => ` - ${chalk.red(k)}`).join("\n"));
|
|
91
|
+
}
|
|
92
|
+
if (emptyKeys.length > 0) {
|
|
93
|
+
clack.log.warn(` ⚠ Placeholder/Empty values detected:\n` +
|
|
94
|
+
emptyKeys.map((k) => ` - ${chalk.yellow(k)}`).join("\n"));
|
|
95
|
+
}
|
|
96
|
+
if (extraKeys.length > 0) {
|
|
97
|
+
clack.log.info(` ℹ Extra keys (not declared in .env.example):\n` +
|
|
98
|
+
extraKeys.map((k) => ` - ${chalk.cyan(k)}`).join("\n"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
clack.log.error(` ❌ Failed to read ${envFile}: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
console.log("");
|
|
106
|
+
}
|
|
107
|
+
clack.outro(chalk.bold.green("Environment audit finished."));
|
|
108
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
function icon(status) {
|
|
3
|
+
if (status === "pass")
|
|
4
|
+
return chalk.green("✔");
|
|
5
|
+
if (status === "fail")
|
|
6
|
+
return chalk.red("✖");
|
|
7
|
+
return chalk.gray("-");
|
|
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
|
+
function printSeparator() {
|
|
24
|
+
console.log(chalk.gray("─".repeat(80)));
|
|
25
|
+
}
|
|
26
|
+
function printItem(item) {
|
|
27
|
+
const statusIcon = icon(item.status);
|
|
28
|
+
const line = `${statusIcon} ${chalk.bold(item.title)}`;
|
|
29
|
+
console.log(line);
|
|
30
|
+
// Show details for pass/fail/skip
|
|
31
|
+
if (item.details) {
|
|
32
|
+
console.log(chalk.gray(` ↳ ${item.details}`));
|
|
33
|
+
}
|
|
34
|
+
// Show suggestion only for failures
|
|
35
|
+
if (item.status === "fail" && item.suggestion) {
|
|
36
|
+
console.log(chalk.gray(` ↳ Suggestion: ${item.suggestion}`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function printSection(title, items, color) {
|
|
40
|
+
if (items.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log(color(chalk.bold(`● ${title}`)));
|
|
44
|
+
printSeparator();
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
printItem(item);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function printHuman(report) {
|
|
50
|
+
// Header
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
53
|
+
console.log(chalk.bold.white(` Backend Audit Report`));
|
|
54
|
+
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
55
|
+
console.log(chalk.gray(`Project: ${report.projectPath}`));
|
|
56
|
+
console.log(chalk.gray(`Summary: ${chalk.green(`pass=${report.summary.pass}`)} ${chalk.red(`fail=${report.summary.fail}`)} ${chalk.gray(`skip=${report.summary.skip}`)}`));
|
|
57
|
+
// Severity Legend
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(chalk.bold("Severity Legend:"));
|
|
60
|
+
console.log(chalk.red(" ● ERROR ") +
|
|
61
|
+
chalk.gray("- Critical issues that must be fixed"));
|
|
62
|
+
console.log(chalk.yellow(" ● WARN ") +
|
|
63
|
+
chalk.gray("- Important issues that should be addressed"));
|
|
64
|
+
console.log(chalk.blue(" ● INFO ") +
|
|
65
|
+
chalk.gray("- Informational items or best practices"));
|
|
66
|
+
// Group items by status first
|
|
67
|
+
const passedItems = report.items.filter((item) => item.status === "pass");
|
|
68
|
+
const failedItems = report.items.filter((item) => item.status === "fail");
|
|
69
|
+
const skippedItems = report.items.filter((item) => item.status === "skip");
|
|
70
|
+
// Print PASSED section
|
|
71
|
+
if (passedItems.length > 0) {
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(chalk.bold.green(`✓ PASSED CHECKS (${passedItems.length})`));
|
|
74
|
+
printSeparator();
|
|
75
|
+
for (const item of passedItems) {
|
|
76
|
+
printItem(item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Print FAILED sections grouped by severity
|
|
80
|
+
if (failedItems.length > 0) {
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(chalk.bold.red(`✖ FAILED CHECKS (${failedItems.length})`));
|
|
83
|
+
printSeparator();
|
|
84
|
+
// Group failed items by severity
|
|
85
|
+
const failedErrors = failedItems.filter((item) => item.severity === "error");
|
|
86
|
+
const failedWarns = failedItems.filter((item) => item.severity === "warn");
|
|
87
|
+
const failedInfos = failedItems.filter((item) => item.severity === "info");
|
|
88
|
+
// Print failed ERRORS
|
|
89
|
+
if (failedErrors.length > 0) {
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
|
|
92
|
+
for (const item of failedErrors) {
|
|
93
|
+
console.log("");
|
|
94
|
+
printItem(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Print failed WARNINGS
|
|
98
|
+
if (failedWarns.length > 0) {
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
|
|
101
|
+
for (const item of failedWarns) {
|
|
102
|
+
console.log("");
|
|
103
|
+
printItem(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Print failed INFO
|
|
107
|
+
if (failedInfos.length > 0) {
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
|
|
110
|
+
for (const item of failedInfos) {
|
|
111
|
+
console.log("");
|
|
112
|
+
printItem(item);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Print SKIPPED section (if any)
|
|
117
|
+
if (skippedItems.length > 0) {
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(chalk.bold.gray(`- SKIPPED CHECKS (${skippedItems.length})`));
|
|
120
|
+
printSeparator();
|
|
121
|
+
for (const item of skippedItems) {
|
|
122
|
+
printItem(item);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Footer
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
128
|
+
// Summary message
|
|
129
|
+
const failCount = report.summary.fail;
|
|
130
|
+
if (failCount === 0) {
|
|
131
|
+
console.log(chalk.green.bold(`✓ All checks passed!`));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(chalk.yellow(` ⚠ ${failCount} check${failCount > 1 ? "s" : ""} need${failCount === 1 ? "s" : ""} attention.`));
|
|
135
|
+
}
|
|
136
|
+
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
137
|
+
console.log("");
|
|
138
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
// Windows helper: map PIDs to process names
|
|
7
|
+
async function getWindowsProcessMap() {
|
|
8
|
+
const map = new Map();
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execAsync("tasklist /FO CSV /NH");
|
|
11
|
+
const lines = stdout.split("\n");
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (!line.trim())
|
|
14
|
+
continue;
|
|
15
|
+
// CSV format: "Image Name","PID","Session Name","Session#","Mem Usage"
|
|
16
|
+
// e.g. "node.exe","22064","Console","1","53,284 K"
|
|
17
|
+
const parts = line.split(`","`).map((p) => p.replace(/"/g, "").trim());
|
|
18
|
+
if (parts.length >= 2) {
|
|
19
|
+
const name = parts[0];
|
|
20
|
+
const pid = parseInt(parts[1], 10);
|
|
21
|
+
if (!isNaN(pid)) {
|
|
22
|
+
map.set(pid, name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
// Ignore and return empty map
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
// Windows port scanner
|
|
33
|
+
async function scanWindowsPorts() {
|
|
34
|
+
const processMap = await getWindowsProcessMap();
|
|
35
|
+
const list = [];
|
|
36
|
+
const seenKeys = new Set();
|
|
37
|
+
try {
|
|
38
|
+
// netstat -ano lists all connections and listening ports with PIDs
|
|
39
|
+
const { stdout } = await execAsync("netstat -ano");
|
|
40
|
+
const lines = stdout.split("\n");
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed.startsWith("TCP") && !trimmed.startsWith("UDP"))
|
|
44
|
+
continue;
|
|
45
|
+
// TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 12345
|
|
46
|
+
// Split by whitespace
|
|
47
|
+
const tokens = trimmed.split(/\s+/);
|
|
48
|
+
if (tokens.length < 5)
|
|
49
|
+
continue;
|
|
50
|
+
const state = tokens[3];
|
|
51
|
+
if (state !== "LISTENING")
|
|
52
|
+
continue;
|
|
53
|
+
const localAddress = tokens[1];
|
|
54
|
+
const pidStr = tokens[4];
|
|
55
|
+
const pid = parseInt(pidStr, 10);
|
|
56
|
+
if (isNaN(pid))
|
|
57
|
+
continue;
|
|
58
|
+
// Extract port from localAddress (e.g. 0.0.0.0:3000 or [::]:3000)
|
|
59
|
+
const lastColon = localAddress.lastIndexOf(":");
|
|
60
|
+
if (lastColon === -1)
|
|
61
|
+
continue;
|
|
62
|
+
const portStr = localAddress.slice(lastColon + 1);
|
|
63
|
+
const port = parseInt(portStr, 10);
|
|
64
|
+
if (isNaN(port))
|
|
65
|
+
continue;
|
|
66
|
+
const key = `${port}-${pid}`;
|
|
67
|
+
if (seenKeys.has(key))
|
|
68
|
+
continue;
|
|
69
|
+
seenKeys.add(key);
|
|
70
|
+
const name = processMap.get(pid) || "Unknown";
|
|
71
|
+
list.push({ port, pid, name });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
throw new Error(`Failed to scan Windows ports: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
return list.sort((a, b) => a.port - b.port);
|
|
78
|
+
}
|
|
79
|
+
// Unix port scanner (Linux / macOS)
|
|
80
|
+
async function scanUnixPorts() {
|
|
81
|
+
const list = [];
|
|
82
|
+
const seenKeys = new Set();
|
|
83
|
+
try {
|
|
84
|
+
// lsof -i -P -n -sTCP:LISTEN
|
|
85
|
+
const { stdout } = await execAsync("lsof -i -P -n -sTCP:LISTEN");
|
|
86
|
+
const lines = stdout.split("\n");
|
|
87
|
+
// Command headers: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
|
88
|
+
// e.g. node 22064 user 22u IPv6 0x... 0t0 TCP *:3000 (LISTEN)
|
|
89
|
+
for (let i = 1; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i].trim();
|
|
91
|
+
if (!line)
|
|
92
|
+
continue;
|
|
93
|
+
const tokens = line.split(/\s+/);
|
|
94
|
+
if (tokens.length < 9)
|
|
95
|
+
continue;
|
|
96
|
+
const name = tokens[0];
|
|
97
|
+
const pid = parseInt(tokens[1], 10);
|
|
98
|
+
const nameCol = tokens[8]; // e.g. *:3000 or 127.0.0.1:3000 or [::1]:3000
|
|
99
|
+
const lastColon = nameCol.lastIndexOf(":");
|
|
100
|
+
if (lastColon === -1)
|
|
101
|
+
continue;
|
|
102
|
+
const portStr = nameCol.slice(lastColon + 1);
|
|
103
|
+
const port = parseInt(portStr, 10);
|
|
104
|
+
if (isNaN(pid) || isNaN(port))
|
|
105
|
+
continue;
|
|
106
|
+
const key = `${port}-${pid}`;
|
|
107
|
+
if (seenKeys.has(key))
|
|
108
|
+
continue;
|
|
109
|
+
seenKeys.add(key);
|
|
110
|
+
list.push({ port, pid, name });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
throw new Error(`Failed to scan Unix ports (make sure lsof is installed): ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
return list.sort((a, b) => a.port - b.port);
|
|
117
|
+
}
|
|
118
|
+
export async function runPortsCommand() {
|
|
119
|
+
clack.intro(chalk.bold.bgBlue.white(" Bandit Port Inspector "));
|
|
120
|
+
const isWin = process.platform === "win32";
|
|
121
|
+
const s = clack.spinner();
|
|
122
|
+
s.start("Scanning for active listening ports...");
|
|
123
|
+
let ports = [];
|
|
124
|
+
try {
|
|
125
|
+
ports = isWin ? await scanWindowsPorts() : await scanUnixPorts();
|
|
126
|
+
s.stop(`Scan complete. Found ${ports.length} listening process(es).`);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
s.stop("Scan failed.");
|
|
130
|
+
clack.log.error(err.message);
|
|
131
|
+
clack.outro(chalk.red("Execution stopped."));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (ports.length === 0) {
|
|
135
|
+
clack.log.info("No active local listening processes detected on common backend ports.");
|
|
136
|
+
clack.outro(chalk.green("Done."));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const options = ports.map((p) => ({
|
|
140
|
+
value: p,
|
|
141
|
+
label: `Port ${chalk.bold.green(p.port)} ➜ PID ${p.pid} (${chalk.cyan(p.name)})`,
|
|
142
|
+
}));
|
|
143
|
+
const selected = (await clack.select({
|
|
144
|
+
message: "Select a process to inspect or terminate:",
|
|
145
|
+
options: [
|
|
146
|
+
...options,
|
|
147
|
+
{ value: "cancel", label: chalk.gray("Cancel / Exit") },
|
|
148
|
+
],
|
|
149
|
+
}));
|
|
150
|
+
if (clack.isCancel(selected) || selected === "cancel") {
|
|
151
|
+
clack.outro("No action taken.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const confirmKill = await clack.confirm({
|
|
155
|
+
message: `Are you sure you want to terminate PID ${selected.pid} (${selected.name}) listening on Port ${selected.port}?`,
|
|
156
|
+
});
|
|
157
|
+
if (clack.isCancel(confirmKill) || !confirmKill) {
|
|
158
|
+
clack.outro("Aborted.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
s.start(`Terminating process ${selected.name} (PID ${selected.pid})...`);
|
|
162
|
+
try {
|
|
163
|
+
if (isWin) {
|
|
164
|
+
await execAsync(`taskkill /F /PID ${selected.pid}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
await execAsync(`kill -9 ${selected.pid}`);
|
|
168
|
+
}
|
|
169
|
+
s.stop(`Process terminated successfully.`);
|
|
170
|
+
clack.outro(chalk.bold.green(`✔ Port ${selected.port} has been freed!`));
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
s.stop(`Failed to terminate process.`);
|
|
174
|
+
clack.log.error(`Error details: ${err.message}`);
|
|
175
|
+
clack.outro(chalk.bold.red("❌ Process termination failed. You might need administrative/root privileges."));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// File: src/core/auditor.ts
|
|
2
|
+
import { makeContext } from "./context.js";
|
|
3
|
+
import { rules } from "../rules/index.js";
|
|
4
|
+
export async function runAudit(projectPath) {
|
|
5
|
+
const ctx = makeContext(projectPath);
|
|
6
|
+
const items = [];
|
|
7
|
+
for (const rule of rules) {
|
|
8
|
+
try {
|
|
9
|
+
const result = await rule.run(ctx);
|
|
10
|
+
items.push(result);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
items.push({
|
|
14
|
+
id: rule.id,
|
|
15
|
+
title: rule.title,
|
|
16
|
+
severity: rule.severity,
|
|
17
|
+
status: "fail",
|
|
18
|
+
details: `Rule crashed: ${err?.message ?? String(err)}`,
|
|
19
|
+
suggestion: "Fix the rule implementation.",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const summary = items.reduce((acc, item) => {
|
|
24
|
+
acc[item.status] += 1;
|
|
25
|
+
return acc;
|
|
26
|
+
}, { pass: 0, fail: 0, skip: 0 });
|
|
27
|
+
return {
|
|
28
|
+
projectPath,
|
|
29
|
+
summary,
|
|
30
|
+
items,
|
|
31
|
+
};
|
|
32
|
+
}
|