bandit-cli 1.0.3 → 1.0.6

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.
@@ -6,71 +6,54 @@ function icon(status) {
6
6
  return chalk.red("✖");
7
7
  return chalk.gray("-");
8
8
  }
9
- function sevColor(sev, text) {
10
- if (sev === "error")
11
- return chalk.red(text);
12
- if (sev === "warn")
13
- return chalk.yellow(text);
14
- return chalk.blue(text);
15
- }
16
- function sevLabel(sev) {
17
- if (sev === "error")
18
- return "ERROR";
19
- if (sev === "warn")
20
- return "WARN";
21
- return "INFO";
22
- }
23
9
  function printSeparator() {
24
- console.log(chalk.gray("─".repeat(80)));
10
+ console.log(chalk.gray(" " + "─".repeat(70)));
25
11
  }
26
12
  function printItem(item) {
27
13
  const statusIcon = icon(item.status);
28
- const line = `${statusIcon} ${chalk.bold(item.title)}`;
29
- console.log(line);
30
- // Show details for pass/fail/skip
14
+ let titleText = chalk.bold(item.title);
15
+ if (item.status === "fail") {
16
+ if (item.severity === "error")
17
+ titleText += chalk.red.bold(" (ERROR)");
18
+ else if (item.severity === "warn")
19
+ titleText += chalk.yellow.bold(" (WARN)");
20
+ else
21
+ titleText += chalk.blue.bold(" (INFO)");
22
+ }
23
+ console.log(` ${statusIcon} ${titleText}`);
24
+ // Show details
31
25
  if (item.details) {
32
- console.log(chalk.gray(` ↳ ${item.details}`));
26
+ console.log(chalk.gray(` ↳ ${item.details}`));
33
27
  }
34
28
  // Show suggestion only for failures
35
29
  if (item.status === "fail" && item.suggestion) {
36
- console.log(chalk.gray(` ↳ Suggestion: ${item.suggestion}`));
37
- }
38
- }
39
- function printSection(title, items, color) {
40
- if (items.length === 0)
41
- return;
42
- console.log("");
43
- console.log(color(chalk.bold(`● ${title}`)));
44
- printSeparator();
45
- for (const item of items) {
46
- printItem(item);
30
+ console.log(chalk.cyan(` ↳ Suggestion: ${item.suggestion}`));
47
31
  }
48
32
  }
49
33
  export function printHuman(report) {
50
- // Header
34
+ // Header Box
51
35
  console.log("");
52
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
53
- console.log(chalk.bold.white(` Backend Audit Report`));
54
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
55
- console.log(chalk.gray(`Project: ${report.projectPath}`));
56
- console.log(chalk.gray(`Summary: ${chalk.green(`pass=${report.summary.pass}`)} ${chalk.red(`fail=${report.summary.fail}`)} ${chalk.gray(`skip=${report.summary.skip}`)}`));
36
+ console.log(chalk.cyan(` ┌${"─".repeat(70)}┐`));
37
+ console.log(chalk.cyan(` │`) + chalk.bold.white(" BACKEND PROJECT AUDIT SUMMARY ") + chalk.cyan(`│`));
38
+ console.log(chalk.cyan(` └${"─".repeat(70)}┘`));
39
+ console.log(` ${chalk.gray("Project:")} ${chalk.bold(report.projectPath)}`);
40
+ console.log(` ${chalk.gray("Results:")} ${chalk.green(`${report.summary.pass} Passed`)}, ` +
41
+ `${chalk.red(`${report.summary.fail} Failed`)}, ` +
42
+ `${chalk.gray(`${report.summary.skip} Skipped`)}`);
57
43
  // Severity Legend
58
44
  console.log("");
59
- console.log(chalk.bold("Severity Legend:"));
60
- console.log(chalk.red(" ● ERROR ") +
61
- chalk.gray("- Critical issues that must be fixed"));
62
- console.log(chalk.yellow(" WARN ") +
63
- chalk.gray("- Important issues that should be addressed"));
64
- console.log(chalk.blue(" ● INFO ") +
65
- chalk.gray("- Informational items or best practices"));
66
- // Group items by status first
45
+ console.log(` ${chalk.bold.gray("Severity Legend:")} ` +
46
+ `${chalk.red("● ERROR")} (Critical) ` +
47
+ `${chalk.yellow(" WARN")} (Important) ` +
48
+ `${chalk.blue("● INFO")} (Best Practice)`);
49
+ // Group items
67
50
  const passedItems = report.items.filter((item) => item.status === "pass");
68
51
  const failedItems = report.items.filter((item) => item.status === "fail");
69
52
  const skippedItems = report.items.filter((item) => item.status === "skip");
70
53
  // Print PASSED section
71
54
  if (passedItems.length > 0) {
72
55
  console.log("");
73
- console.log(chalk.bold.green(`✓ PASSED CHECKS (${passedItems.length})`));
56
+ console.log(chalk.bold.green(` ✔ PASSED CHECKS (${passedItems.length})`));
74
57
  printSeparator();
75
58
  for (const item of passedItems) {
76
59
  printItem(item);
@@ -79,16 +62,15 @@ export function printHuman(report) {
79
62
  // Print FAILED sections grouped by severity
80
63
  if (failedItems.length > 0) {
81
64
  console.log("");
82
- console.log(chalk.bold.red(`✖ FAILED CHECKS (${failedItems.length})`));
65
+ console.log(chalk.bold.red(` ✖ FAILED CHECKS (${failedItems.length})`));
83
66
  printSeparator();
84
- // Group failed items by severity
85
67
  const failedErrors = failedItems.filter((item) => item.severity === "error");
86
68
  const failedWarns = failedItems.filter((item) => item.severity === "warn");
87
69
  const failedInfos = failedItems.filter((item) => item.severity === "info");
88
70
  // Print failed ERRORS
89
71
  if (failedErrors.length > 0) {
90
72
  console.log("");
91
- console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
73
+ console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
92
74
  for (const item of failedErrors) {
93
75
  console.log("");
94
76
  printItem(item);
@@ -97,7 +79,7 @@ export function printHuman(report) {
97
79
  // Print failed WARNINGS
98
80
  if (failedWarns.length > 0) {
99
81
  console.log("");
100
- console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
82
+ console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
101
83
  for (const item of failedWarns) {
102
84
  console.log("");
103
85
  printItem(item);
@@ -106,7 +88,7 @@ export function printHuman(report) {
106
88
  // Print failed INFO
107
89
  if (failedInfos.length > 0) {
108
90
  console.log("");
109
- console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
91
+ console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
110
92
  for (const item of failedInfos) {
111
93
  console.log("");
112
94
  printItem(item);
@@ -116,23 +98,24 @@ export function printHuman(report) {
116
98
  // Print SKIPPED section (if any)
117
99
  if (skippedItems.length > 0) {
118
100
  console.log("");
119
- console.log(chalk.bold.gray(`- SKIPPED CHECKS (${skippedItems.length})`));
101
+ console.log(chalk.bold.gray(` - SKIPPED CHECKS (${skippedItems.length})`));
120
102
  printSeparator();
121
103
  for (const item of skippedItems) {
122
104
  printItem(item);
123
105
  }
124
106
  }
125
- // Footer
126
- console.log("");
127
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
128
- // Summary message
107
+ // Footer Box
129
108
  const failCount = report.summary.fail;
109
+ console.log("");
130
110
  if (failCount === 0) {
131
- console.log(chalk.green.bold(`✓ All checks passed!`));
111
+ console.log(chalk.green(` ┌${"─".repeat(70)}┐`));
112
+ console.log(chalk.green(` │`) + chalk.bold.green(" ✔ All audits passed successfully! ") + chalk.green(`│`));
113
+ console.log(chalk.green(` └${"─".repeat(70)}┘`));
132
114
  }
133
115
  else {
134
- console.log(chalk.yellow(` ⚠ ${failCount} check${failCount > 1 ? "s" : ""} need${failCount === 1 ? "s" : ""} attention.`));
116
+ console.log(chalk.yellow(` ┌${"".repeat(70)}┐`));
117
+ console.log(chalk.yellow(` │`) + chalk.bold.yellow(" ⚠ Audit completed with warnings/errors. Check issues above. ") + chalk.yellow(`│`));
118
+ console.log(chalk.yellow(` └${"─".repeat(70)}┘`));
135
119
  }
136
- console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
137
120
  console.log("");
138
121
  }
@@ -0,0 +1,54 @@
1
+ import * as clack from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { exec } from "node:child_process";
4
+ import { startStudioServer } from "../studio/server.js";
5
+ export async function runStudioCommand(projectPath = ".") {
6
+ clack.intro(chalk.bold.bgBlue.white(" Bandit Studio "));
7
+ const s = clack.spinner();
8
+ s.start("Starting Bandit Studio local server...");
9
+ try {
10
+ const { server, port } = await startStudioServer(projectPath, 4000);
11
+ const url = `http://localhost:${port}`;
12
+ s.stop(`Bandit Studio server running at ${chalk.bold.cyan(url)}`);
13
+ clack.log.info("Press Ctrl+C to stop the studio server.");
14
+ let isShuttingDown = false;
15
+ const shutdown = () => {
16
+ if (isShuttingDown)
17
+ return;
18
+ isShuttingDown = true;
19
+ console.log(chalk.bold.yellow("\nStopping Bandit Studio server... Goodbye!"));
20
+ try {
21
+ server.close();
22
+ }
23
+ catch { }
24
+ process.exit(0);
25
+ };
26
+ process.on("SIGINT", shutdown);
27
+ process.on("SIGTERM", shutdown);
28
+ // Windows PowerShell / Terminal raw input listener for Ctrl+C (\u0003)
29
+ if (process.stdin.isTTY) {
30
+ try {
31
+ process.stdin.setRawMode(true);
32
+ process.stdin.resume();
33
+ process.stdin.on("data", (chunk) => {
34
+ const str = chunk.toString();
35
+ if (str === "\u0003" || str === "\u001b" || str.includes("\u0003")) {
36
+ shutdown();
37
+ }
38
+ });
39
+ }
40
+ catch { }
41
+ }
42
+ // Attempt auto-opening browser
43
+ const startCmd = process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`;
44
+ exec(startCmd, (err) => {
45
+ if (err) {
46
+ clack.log.warn(`Could not open browser automatically. Please open ${url} in your browser.`);
47
+ }
48
+ });
49
+ }
50
+ catch (err) {
51
+ s.stop("Failed to start Bandit Studio server.");
52
+ clack.log.error(`Server startup error: ${err.message}`);
53
+ }
54
+ }
package/dist/index.js CHANGED
@@ -14,7 +14,9 @@ program
14
14
  .option("--json", "Print results as JSON")
15
15
  .option("--fail-on-warn", "Exit with code 1 if warnings exist")
16
16
  .action(async (projectPath, opts) => {
17
- const results = await runAudit(projectPath);
17
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
18
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
19
+ const results = await runAudit(resolvedPath);
18
20
  if (opts.json) {
19
21
  process.stdout.write(JSON.stringify(results, null, 2) + "\n");
20
22
  }
@@ -42,8 +44,10 @@ program
42
44
  .description("Audit local .env configurations against .env.example")
43
45
  .argument("[path]", "Path to the project", ".")
44
46
  .action(async (projectPath) => {
47
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
48
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
45
49
  const { runEnvCommand } = await import("./cli/env.js");
46
- await runEnvCommand(projectPath);
50
+ await runEnvCommand(resolvedPath);
47
51
  });
48
52
  // Subcommand: doctor
49
53
  program
@@ -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(methodOrPath, routePath, opts);
80
+ await runApiCommand(targetPath, routePath, opts);
70
81
  });
71
82
  // Subcommand: bench
72
83
  program
@@ -89,6 +100,17 @@ program
89
100
  const { runBenchCommand } = await import("./cli/bench.js");
90
101
  await runBenchCommand(url, { connections, requests });
91
102
  });
103
+ // Subcommand: studio
104
+ program
105
+ .command("studio")
106
+ .description("Launch Bandit Studio local web dashboard")
107
+ .argument("[path]", "Path to the project", ".")
108
+ .action(async (projectPath) => {
109
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
110
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
111
+ const { runStudioCommand } = await import("./cli/studio.js");
112
+ await runStudioCommand(resolvedPath);
113
+ });
92
114
  // Default to help if no command specified
93
115
  if (!process.argv.slice(2).length) {
94
116
  program.outputHelp();
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ export class StudioDB {
5
+ baseDir;
6
+ constructor(projectPath = ".") {
7
+ this.baseDir = path.join(path.resolve(projectPath), ".bandit");
8
+ this.ensureDir();
9
+ }
10
+ ensureDir() {
11
+ if (!fs.existsSync(this.baseDir)) {
12
+ fs.mkdirSync(this.baseDir, { recursive: true });
13
+ }
14
+ }
15
+ getFilePath(filename) {
16
+ return path.join(this.baseDir, filename);
17
+ }
18
+ readJson(filename, defaultValue) {
19
+ const file = this.getFilePath(filename);
20
+ if (!fs.existsSync(file))
21
+ return defaultValue;
22
+ try {
23
+ const content = fs.readFileSync(file, "utf-8");
24
+ return JSON.parse(content);
25
+ }
26
+ catch {
27
+ return defaultValue;
28
+ }
29
+ }
30
+ writeJson(filename, data) {
31
+ const file = this.getFilePath(filename);
32
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
33
+ }
34
+ getGitMetadata() {
35
+ try {
36
+ const commit = execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "ignore"] })
37
+ .toString()
38
+ .trim();
39
+ const branch = execSync("git branch --show-current", { stdio: ["ignore", "pipe", "ignore"] })
40
+ .toString()
41
+ .trim();
42
+ const author = execSync('git log -1 --pretty=format:"%an"', { stdio: ["ignore", "pipe", "ignore"] })
43
+ .toString()
44
+ .trim();
45
+ return { commit, branch, author };
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ saveBenchmark(data) {
52
+ const records = this.readJson("benchmarks.json", []);
53
+ const git = this.getGitMetadata();
54
+ const newRecord = {
55
+ id: Math.random().toString(36).substring(2, 10),
56
+ timestamp: new Date().toISOString(),
57
+ gitCommit: git.commit,
58
+ gitBranch: git.branch,
59
+ gitAuthor: git.author,
60
+ ...data,
61
+ };
62
+ records.unshift(newRecord);
63
+ this.writeJson("benchmarks.json", records.slice(0, 200));
64
+ return newRecord;
65
+ }
66
+ getBenchmarks() {
67
+ return this.readJson("benchmarks.json", []);
68
+ }
69
+ saveAudit(items) {
70
+ const records = this.readJson("audits.json", []);
71
+ const git = this.getGitMetadata();
72
+ const newRecord = {
73
+ id: Math.random().toString(36).substring(2, 10),
74
+ timestamp: new Date().toISOString(),
75
+ gitCommit: git.commit,
76
+ items,
77
+ };
78
+ records.unshift(newRecord);
79
+ this.writeJson("audits.json", records.slice(0, 50));
80
+ return newRecord;
81
+ }
82
+ getAudits() {
83
+ return this.readJson("audits.json", []);
84
+ }
85
+ getConfig() {
86
+ return this.readJson("config.json", { dismissedRules: [] });
87
+ }
88
+ dismissRule(ruleTitle) {
89
+ const config = this.getConfig();
90
+ if (!config.dismissedRules.includes(ruleTitle)) {
91
+ config.dismissedRules.push(ruleTitle);
92
+ this.writeJson("config.json", config);
93
+ }
94
+ return config;
95
+ }
96
+ }
@@ -0,0 +1,66 @@
1
+ import { scanForRoutes } from "../cli/api.js";
2
+ import { StudioDB } from "./db.js";
3
+ export async function generateApiBlueprint(projectPath) {
4
+ const rawRoutes = await scanForRoutes(projectPath);
5
+ const db = new StudioDB(projectPath);
6
+ const benchmarks = db.getBenchmarks();
7
+ // Filter out non-route scans (like test files or CLI internal strings)
8
+ const routes = rawRoutes.filter(r => !r.sourceFile.includes("cli/") &&
9
+ !r.sourceFile.includes("doctor.") &&
10
+ !r.sourceFile.includes("api.") &&
11
+ r.routePath.startsWith("/") &&
12
+ !r.routePath.includes("-") // exclude header test strings like /access-control-allow-origin
13
+ );
14
+ const nodes = [];
15
+ const edges = [];
16
+ // Infrastructure Nodes
17
+ nodes.push({ id: "db:pg", label: "PostgreSQL Database", type: "database", details: "Primary Data Store & Connection Pool" });
18
+ nodes.push({ id: "cache:redis", label: "Redis Cache Layer", type: "cache", details: "In-Memory Response & Session Cache" });
19
+ const addedControllers = new Set();
20
+ const addedServices = new Set();
21
+ routes.forEach((r, idx) => {
22
+ const routeId = `route:${idx}`;
23
+ const routeLabel = `[${r.method}] ${r.routePath}`;
24
+ // Find matching telemetry if benchmarked
25
+ const matchBench = benchmarks.find(b => b.targetUrl.endsWith(r.routePath));
26
+ const metrics = matchBench ? { rps: matchBench.rps, p99: matchBench.latency.p99, avg: matchBench.latency.avg } : undefined;
27
+ nodes.push({
28
+ id: routeId,
29
+ label: routeLabel,
30
+ type: "route",
31
+ details: r.sourceFile,
32
+ metrics,
33
+ });
34
+ const controllerName = r.sourceFile.split(/[/\\]/).pop() || "Controller";
35
+ const controllerId = `controller:${controllerName}`;
36
+ if (!addedControllers.has(controllerId)) {
37
+ addedControllers.add(controllerId);
38
+ nodes.push({
39
+ id: controllerId,
40
+ label: controllerName,
41
+ type: "controller",
42
+ details: r.sourceFile,
43
+ });
44
+ // Map controller to corresponding service layer
45
+ const serviceName = controllerName.replace(".controller.", ".service.").replace("controller", "service");
46
+ const serviceId = `service:${serviceName}`;
47
+ if (!addedServices.has(serviceId)) {
48
+ addedServices.add(serviceId);
49
+ nodes.push({
50
+ id: serviceId,
51
+ label: serviceName,
52
+ type: "service",
53
+ details: `Business logic & DB ORM mapping`,
54
+ });
55
+ // Link Service to Infrastructure
56
+ edges.push({ from: serviceId, to: "db:pg", label: "queries" });
57
+ if (serviceName.toLowerCase().includes("product") || serviceName.toLowerCase().includes("catalog") || serviceName.toLowerCase().includes("cache")) {
58
+ edges.push({ from: serviceId, to: "cache:redis", label: "hits cache" });
59
+ }
60
+ }
61
+ edges.push({ from: controllerId, to: serviceId, label: "delegates" });
62
+ }
63
+ edges.push({ from: routeId, to: controllerId, label: "handles" });
64
+ });
65
+ return { nodes, edges };
66
+ }