bandit-cli 1.0.4 → 1.0.7

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/dist/cli/api.js CHANGED
@@ -539,7 +539,7 @@ export async function runApiCommand(methodOrPath = ".", routePath, opts = {}) {
539
539
  // Let the developer confirm and edit the path (highly convenient for prefix additions!)
540
540
  const editPath = await clack.text({
541
541
  message: "Confirm or edit the route path:",
542
- defaultValue: selectedRoute.routePath,
542
+ initialValue: selectedRoute.routePath,
543
543
  placeholder: selectedRoute.routePath,
544
544
  validate: (v) => (!v || !v.startsWith("/") ? "Route path must start with a slash '/'" : undefined),
545
545
  });
package/dist/cli/bench.js CHANGED
@@ -131,5 +131,17 @@ export async function runBenchCommand(targetUrl, opts) {
131
131
  console.log(` ${chalk.gray(label)} : [${chalk.green(bar)}] ${count.toString().padStart(4)} (${percentage.toFixed(1)}%)`);
132
132
  }
133
133
  console.log(chalk.gray("──────────────────────────────────────────────────"));
134
+ try {
135
+ const { StudioDB } = await import("../studio/db.js");
136
+ const db = new StudioDB();
137
+ db.saveBenchmark({
138
+ targetUrl,
139
+ connections: opts.connections,
140
+ requests: opts.requests,
141
+ rps,
142
+ latency: { min, avg, max, p50, p90, p95, p99 },
143
+ });
144
+ }
145
+ catch { }
134
146
  clack.outro(chalk.bold.green("Benchmark complete!"));
135
147
  }
@@ -396,5 +396,11 @@ export async function runDoctorCommand(targetUrl = "http://localhost:3000") {
396
396
  }
397
397
  console.log("");
398
398
  }
399
+ try {
400
+ const { StudioDB } = await import("../studio/db.js");
401
+ const db = new StudioDB();
402
+ db.saveAudit(reports);
403
+ }
404
+ catch { }
399
405
  clack.outro(chalk.bold.green("Doctor diagnostic scan completed."));
400
406
  }
package/dist/cli/env.js CHANGED
@@ -69,7 +69,10 @@ export async function runEnvCommand(projectPath = ".") {
69
69
  }
70
70
  else {
71
71
  const val = currentKeysMap.get(expectedKey);
72
- if (val === "" || val === "your_key_here" || val === "placeholder" || val.includes("TODO")) {
72
+ if (val === "" ||
73
+ val === "your_key_here" ||
74
+ val === "placeholder" ||
75
+ val.includes("TODO")) {
73
76
  emptyKeys.push(expectedKey);
74
77
  }
75
78
  }
@@ -81,7 +84,9 @@ export async function runEnvCommand(projectPath = ".") {
81
84
  }
82
85
  }
83
86
  // Output report for this file
84
- if (missingKeys.length === 0 && emptyKeys.length === 0 && extraKeys.length === 0) {
87
+ if (missingKeys.length === 0 &&
88
+ emptyKeys.length === 0 &&
89
+ extraKeys.length === 0) {
85
90
  clack.log.success(chalk.green(` ✔ ${envFile} is perfectly in sync with .env.example!`));
86
91
  }
87
92
  else {
@@ -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
@@ -38,17 +38,6 @@ program
38
38
  const { runPortsCommand } = await import("./cli/ports.js");
39
39
  await runPortsCommand();
40
40
  });
41
- // Subcommand: env
42
- program
43
- .command("env")
44
- .description("Audit local .env configurations against .env.example")
45
- .argument("[path]", "Path to the project", ".")
46
- .action(async (projectPath) => {
47
- const { resolveProjectPath } = await import("./utils/monorepo.js");
48
- const resolvedPath = await resolveProjectPath(projectPath || ".");
49
- const { runEnvCommand } = await import("./cli/env.js");
50
- await runEnvCommand(resolvedPath);
51
- });
52
41
  // Subcommand: doctor
53
42
  program
54
43
  .command("doctor")
@@ -100,6 +89,17 @@ program
100
89
  const { runBenchCommand } = await import("./cli/bench.js");
101
90
  await runBenchCommand(url, { connections, requests });
102
91
  });
92
+ // Subcommand: studio
93
+ program
94
+ .command("studio")
95
+ .description("Launch Bandit Studio local web dashboard")
96
+ .argument("[path]", "Path to the project", ".")
97
+ .action(async (projectPath) => {
98
+ const { resolveProjectPath } = await import("./utils/monorepo.js");
99
+ const resolvedPath = await resolveProjectPath(projectPath || ".");
100
+ const { runStudioCommand } = await import("./cli/studio.js");
101
+ await runStudioCommand(resolvedPath);
102
+ });
103
103
  // Default to help if no command specified
104
104
  if (!process.argv.slice(2).length) {
105
105
  program.outputHelp();
@@ -4,6 +4,7 @@ import { ruleEnvExampleExists } from "./mvp.rules.js";
4
4
  import { ruleEnvInGitignore } from "./mvp.rules.js";
5
5
  import { ruleDockerfileExists } from "./mvp.rules.js";
6
6
  import { ruleHasTestScript } from "./mvp.rules.js";
7
+ import { ruleEnvKeysMatch } from "./mvp.rules.js";
7
8
  import { ruleSrcFolderExists, ruleDetectFramework, ruleSecurityDeps, ruleGlobalErrorHandler, } from "./phase2.rules.js";
8
9
  import { ruleLoggingSetup, ruleEnvValidation, ruleDatabaseSetup, ruleTypeScriptStrict, ruleProductionDeps, } from "./phase3.rules.js";
9
10
  export const rules = [
@@ -12,6 +13,7 @@ export const rules = [
12
13
  ruleEnvExists,
13
14
  ruleEnvExampleExists,
14
15
  ruleEnvInGitignore,
16
+ ruleEnvKeysMatch,
15
17
  ruleDockerfileExists,
16
18
  ruleHasTestScript,
17
19
  // Phase 2 - Structure + detection + security
@@ -136,3 +136,106 @@ export const ruleHasTestScript = {
136
136
  };
137
137
  },
138
138
  };
139
+ function parseEnvContent(content) {
140
+ const map = new Map();
141
+ const lines = content.split("\n");
142
+ for (const line of lines) {
143
+ const trimmed = line.trim();
144
+ if (!trimmed || trimmed.startsWith("#"))
145
+ continue;
146
+ const firstEqual = trimmed.indexOf("=");
147
+ if (firstEqual === -1)
148
+ continue;
149
+ const key = trimmed.slice(0, firstEqual).trim();
150
+ let val = trimmed.slice(firstEqual + 1).trim();
151
+ if ((val.startsWith('"') && val.endsWith('"')) ||
152
+ (val.startsWith("'") && val.endsWith("'"))) {
153
+ val = val.slice(1, -1);
154
+ }
155
+ map.set(key, val);
156
+ }
157
+ return map;
158
+ }
159
+ export const ruleEnvKeysMatch = {
160
+ id: "env-keys-match",
161
+ title: ".env aligns with .env.example",
162
+ severity: "warn",
163
+ async run(ctx) {
164
+ const envPath = path.join(ctx.projectPath, ".env");
165
+ const envExamplePath = path.join(ctx.projectPath, ".env.example");
166
+ if (!exists(envPath) || !exists(envExamplePath)) {
167
+ return {
168
+ id: this.id,
169
+ title: this.title,
170
+ severity: this.severity,
171
+ status: "skip",
172
+ details: "Requires both .env and .env.example files to run comparison.",
173
+ };
174
+ }
175
+ try {
176
+ const envContent = readText(envPath) || "";
177
+ const exampleContent = readText(envExamplePath) || "";
178
+ const envKeys = parseEnvContent(envContent);
179
+ const exampleKeys = parseEnvContent(exampleContent);
180
+ const missing = [];
181
+ const extra = [];
182
+ const placeholders = [];
183
+ for (const key of exampleKeys.keys()) {
184
+ if (!envKeys.has(key)) {
185
+ missing.push(key);
186
+ }
187
+ else {
188
+ const val = envKeys.get(key);
189
+ if (val === "" ||
190
+ val === "your_key_here" ||
191
+ val === "placeholder" ||
192
+ val.includes("TODO")) {
193
+ placeholders.push(key);
194
+ }
195
+ }
196
+ }
197
+ for (const key of envKeys.keys()) {
198
+ if (!exampleKeys.has(key)) {
199
+ extra.push(key);
200
+ }
201
+ }
202
+ if (missing.length === 0 && extra.length === 0 && placeholders.length === 0) {
203
+ return {
204
+ id: this.id,
205
+ title: this.title,
206
+ severity: this.severity,
207
+ status: "pass",
208
+ details: ".env has all keys defined in .env.example with active values.",
209
+ };
210
+ }
211
+ const issues = [];
212
+ if (missing.length > 0) {
213
+ issues.push(`Missing keys: ${missing.join(", ")}`);
214
+ }
215
+ if (placeholders.length > 0) {
216
+ issues.push(`Placeholder/Empty values: ${placeholders.join(", ")}`);
217
+ }
218
+ if (extra.length > 0) {
219
+ issues.push(`Extra keys: ${extra.join(", ")}`);
220
+ }
221
+ return {
222
+ id: this.id,
223
+ title: this.title,
224
+ severity: this.severity,
225
+ status: "fail",
226
+ details: issues.join(" | "),
227
+ suggestion: "Update your .env or .env.example to keep them in sync and replace any placeholders.",
228
+ };
229
+ }
230
+ catch (err) {
231
+ return {
232
+ id: this.id,
233
+ title: this.title,
234
+ severity: this.severity,
235
+ status: "fail",
236
+ details: `Failed to compare env files: ${err.message}`,
237
+ suggestion: "Ensure env files are valid text files.",
238
+ };
239
+ }
240
+ },
241
+ };
@@ -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,88 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { scanForRoutes } from "../cli/api.js";
4
+ import { StudioDB } from "./db.js";
5
+ export async function generateApiBlueprint(projectPath) {
6
+ const rawRoutes = await scanForRoutes(projectPath);
7
+ const db = new StudioDB(projectPath);
8
+ const benchmarks = db.getBenchmarks();
9
+ // Filter out non-route scans (like test files or CLI internal strings)
10
+ const routes = rawRoutes.filter(r => !r.sourceFile.includes("cli/") &&
11
+ !r.sourceFile.includes("doctor.") &&
12
+ !r.sourceFile.includes("api.") &&
13
+ r.routePath.startsWith("/") &&
14
+ !r.routePath.includes("-") // exclude header test strings like /access-control-allow-origin
15
+ );
16
+ const nodes = [];
17
+ const edges = [];
18
+ // Infrastructure Nodes
19
+ nodes.push({ id: "db:pg", label: "PostgreSQL Database", type: "database", details: "Primary Data Store & Connection Pool" });
20
+ nodes.push({ id: "cache:redis", label: "Redis Cache Layer", type: "cache", details: "In-Memory Response & Session Cache" });
21
+ const addedControllers = new Set();
22
+ const addedServices = new Set();
23
+ routes.forEach((r, idx) => {
24
+ const routeId = `route:${idx}`;
25
+ const routeLabel = `[${r.method}] ${r.routePath}`;
26
+ // Find matching telemetry if benchmarked
27
+ const matchBench = benchmarks.find(b => b.targetUrl.endsWith(r.routePath));
28
+ const metrics = matchBench ? { rps: matchBench.rps, p99: matchBench.latency.p99, avg: matchBench.latency.avg } : undefined;
29
+ // Static Auth Status Detection
30
+ let authStatus = "Public";
31
+ try {
32
+ const fullPath = path.resolve(projectPath, r.sourceFile);
33
+ if (fs.existsSync(fullPath)) {
34
+ const fileContent = fs.readFileSync(fullPath, "utf-8");
35
+ const lines = fileContent.split("\n");
36
+ // Check if any line defining this path contains auth keywords
37
+ const matchingLine = lines.find(line => line.includes(r.routePath) ||
38
+ (line.includes(r.method.toLowerCase()) && line.includes(r.routePath.split("/").pop() || "___")));
39
+ if (matchingLine) {
40
+ const authKeywords = ["auth", "protect", "require", "jwt", "session", "admin", "vendor", "guard", "cookie"];
41
+ const hasAuth = authKeywords.some(kw => matchingLine.toLowerCase().includes(kw));
42
+ if (hasAuth)
43
+ authStatus = "Protected";
44
+ }
45
+ }
46
+ }
47
+ catch { }
48
+ nodes.push({
49
+ id: routeId,
50
+ label: routeLabel,
51
+ type: "route",
52
+ details: r.sourceFile,
53
+ authStatus,
54
+ metrics,
55
+ });
56
+ const controllerName = r.sourceFile.split(/[/\\]/).pop() || "Controller";
57
+ const controllerId = `controller:${controllerName}`;
58
+ if (!addedControllers.has(controllerId)) {
59
+ addedControllers.add(controllerId);
60
+ nodes.push({
61
+ id: controllerId,
62
+ label: controllerName,
63
+ type: "controller",
64
+ details: r.sourceFile,
65
+ });
66
+ // Map controller to corresponding service layer
67
+ const serviceName = controllerName.replace(".controller.", ".service.").replace("controller", "service");
68
+ const serviceId = `service:${serviceName}`;
69
+ if (!addedServices.has(serviceId)) {
70
+ addedServices.add(serviceId);
71
+ nodes.push({
72
+ id: serviceId,
73
+ label: serviceName,
74
+ type: "service",
75
+ details: `Business logic & DB ORM mapping`,
76
+ });
77
+ // Link Service to Infrastructure
78
+ edges.push({ from: serviceId, to: "db:pg", label: "queries" });
79
+ if (serviceName.toLowerCase().includes("product") || serviceName.toLowerCase().includes("catalog") || serviceName.toLowerCase().includes("cache")) {
80
+ edges.push({ from: serviceId, to: "cache:redis", label: "hits cache" });
81
+ }
82
+ }
83
+ edges.push({ from: controllerId, to: serviceId, label: "delegates" });
84
+ }
85
+ edges.push({ from: routeId, to: controllerId, label: "handles" });
86
+ });
87
+ return { nodes, edges };
88
+ }
@@ -0,0 +1,516 @@
1
+ import http from "node:http";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { StudioDB } from "./db.js";
5
+ import { generateApiBlueprint } from "./scanner.js";
6
+ export async function startStudioServer(projectPath, port = 4000) {
7
+ const db = new StudioDB(projectPath);
8
+ const getClientHtml = () => {
9
+ return `<!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>Bandit Studio - Backend Developer Platform</title>
15
+ <style>
16
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
17
+
18
+ :root {
19
+ --bg: #050507;
20
+ --card-bg: #0c0c0e;
21
+ --card-hover: #121215;
22
+ --border: #1f1f23;
23
+ --border-bright: #2f2f37;
24
+ --text: #f4f4f5;
25
+ --text-dim: #71717a;
26
+ --primary: #eab308;
27
+ --primary-hover: #facc15;
28
+ --primary-dim: rgba(234, 179, 8, 0.08);
29
+ --success: #22c55e;
30
+ --success-dim: rgba(34, 197, 94, 0.08);
31
+ --warning: #f59e0b;
32
+ --warning-dim: rgba(245, 158, 11, 0.08);
33
+ --danger: #ef4444;
34
+ --danger-dim: rgba(239, 68, 68, 0.08);
35
+ --accent: #a855f7;
36
+ }
37
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Inter', -apple-system, sans-serif; }
38
+ body { background-color: var(--bg); color: var(--text); display: flex; flex-direction: column; min-height: 100vh; }
39
+ header { background: #000000; border-bottom: 1px solid var(--border); padding: 0.8rem 2rem; display: flex; justify-content: space-between; align-items: center; }
40
+
41
+ .logo-container { display: flex; align-items: center; gap: 0.65rem; }
42
+ .logo-container img { height: 32px; width: auto; object-fit: contain; }
43
+ .logo-text { font-size: 1.45rem; font-weight: 800; color: #ffffff; letter-spacing: -0.03em; }
44
+ .logo-beta { font-size: 0.65rem; font-weight: 700; background: #131316; border: 1px solid var(--border); color: var(--text-dim); padding: 0.15rem 0.5rem; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.05em; margin-left: 0.1rem; }
45
+
46
+ nav { display: flex; gap: 0.5rem; background: #0c0c0e; padding: 0.3rem; border-radius: 0.5rem; border: 1px solid var(--border); }
47
+ nav button { background: none; border: none; color: var(--text-dim); font-size: 0.875rem; font-weight: 600; padding: 0.5rem 1rem; cursor: pointer; border-radius: 0.375rem; transition: all 0.15s ease; }
48
+ nav button.active { color: #000000; background: var(--primary); font-weight: 700; }
49
+ nav button:hover:not(.active) { color: var(--text); background: var(--card-hover); }
50
+
51
+ main { flex: 1; padding: 2rem; max-width: 1280px; margin: 0 auto; width: 100%; }
52
+ .tab-content { display: none; }
53
+ .tab-content.active { display: block; }
54
+
55
+ .section-header { margin-bottom: 2rem; }
56
+ .section-header h2 { font-size: 1.6rem; font-weight: 800; color: var(--text); letter-spacing: -0.02em; }
57
+ .section-header p { color: var(--text-dim); font-size: 0.9rem; margin-top: 0.25rem; }
58
+
59
+ .analytics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
60
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: border-color 0.2s; }
61
+ .card:hover { border-color: var(--border-bright); }
62
+ .card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 1.25rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
63
+
64
+ /* Progress Rings style */
65
+ .rings-row { display: flex; justify-content: space-around; align-items: center; height: 100px; }
66
+ .ring-container { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
67
+ .ring-svg { transform: rotate(-90deg); }
68
+ .ring-bg { fill: none; stroke: var(--border); stroke-width: 8; }
69
+ .ring-bar { fill: none; stroke: var(--primary); stroke-width: 8; stroke-dasharray: 220; stroke-dashoffset: 220; transition: stroke-dashoffset 1s ease-out; stroke-linecap: round; }
70
+ .ring-text { font-size: 0.85rem; font-weight: 800; color: var(--text); font-family: 'JetBrains Mono', monospace; }
71
+
72
+ /* Exposure Bar */
73
+ .exposure-box { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 0.5rem; }
74
+ .progress-bar-bg { height: 10px; background: var(--border); border-radius: 999px; overflow: hidden; display: flex; }
75
+ .progress-bar-val { height: 100%; background: var(--primary); transition: width 0.8s ease-out; }
76
+
77
+ /* Latency Radar (Slowest Endpoints Chart) */
78
+ .radar-list { display: flex; flex-direction: column; gap: 0.75rem; }
79
+ .radar-item { display: flex; flex-direction: column; gap: 0.25rem; }
80
+ .radar-labels { display: flex; justify-content: space-between; font-size: 0.8rem; }
81
+ .radar-labels span { font-family: 'JetBrains Mono', monospace; }
82
+ .radar-bar-bg { height: 8px; background: var(--border); border-radius: 999px; overflow: hidden; }
83
+ .radar-bar-val { height: 100%; background: var(--danger); border-radius: 999px; transition: width 0.8s ease-out; }
84
+
85
+ /* Search Bar & Matrix Layout */
86
+ .controls-row { display: flex; gap: 1rem; align-items: center; margin-bottom: 1.25rem; }
87
+ .search-input { flex: 1; background: #000; border: 1px solid var(--border); padding: 0.75rem 1rem; border-radius: 0.5rem; color: var(--text); font-size: 0.9rem; transition: border-color 0.15s; }
88
+ .search-input:focus { outline: none; border-color: var(--primary); }
89
+
90
+ .matrix-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; }
91
+
92
+ table { width: 100%; border-collapse: collapse; text-align: left; }
93
+ th, td { padding: 1rem; border-bottom: 1px solid var(--border); font-size: 0.875rem; }
94
+ th { color: var(--text-dim); font-weight: 600; background: #000; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
95
+ tr:hover td { background: var(--card-hover); }
96
+
97
+ .badge { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; display: inline-block; border: 1px solid transparent; }
98
+ .badge-success { background: var(--success-dim); color: var(--success); border-color: rgba(34, 197, 94, 0.2); }
99
+ .badge-warning { background: var(--warning-dim); color: var(--warning); border-color: rgba(245, 158, 11, 0.2); }
100
+ .badge-danger { background: var(--danger-dim); color: var(--danger); border-color: rgba(239, 68, 68, 0.2); }
101
+ .badge-yellow { background: var(--primary-dim); color: var(--primary); border-color: rgba(234, 179, 8, 0.2); }
102
+
103
+ .actions-cell { display: flex; gap: 0.5rem; }
104
+ .btn-action { background: var(--border); border: 1px solid var(--border); color: var(--text-dim); padding: 0.4rem 0.75rem; border-radius: 0.375rem; cursor: pointer; font-size: 0.75rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.35rem; transition: all 0.15s; }
105
+ .btn-action:hover { color: var(--text); background: var(--border-bright); }
106
+
107
+ code, td strong, .radar-labels span, .ring-text, td code { font-family: 'JetBrains Mono', monospace; }
108
+ code { background: #000; padding: 0.15rem 0.35rem; border-radius: 0.2rem; font-size: 0.8rem; color: var(--primary); }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <header>
113
+ <div class="logo-container">
114
+ <img src="https://res.cloudinary.com/dv7iah7yv/image/upload/v1783707247/Gemini_Generated_Image_6xf1oi6xf1oi6xf1-removebg-preview_znn27s.png" alt="Bandit Logo" />
115
+ <span class="logo-text">Bandit CLI <span style="color: var(--primary);">Studio</span></span>
116
+ <span class="logo-beta">BETA</span>
117
+ </div>
118
+ <nav>
119
+ <button onclick="showTab('blueprint', this)" class="active" style="display: inline-flex; align-items: center; gap: 0.45rem;">
120
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg>
121
+ API Health Matrix
122
+ </button>
123
+ <button onclick="showTab('performance', this)" style="display: inline-flex; align-items: center; gap: 0.45rem;">
124
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
125
+ Performance History
126
+ </button>
127
+ <button onclick="showTab('audits', this)" style="display: inline-flex; align-items: center; gap: 0.45rem;">
128
+ <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
129
+ Security Inspector
130
+ </button>
131
+ </nav>
132
+ </header>
133
+ <main>
134
+ <!-- API Health Matrix (Tab 1) -->
135
+ <div id="blueprint" class="tab-content active">
136
+ <div class="section-header">
137
+ <h2>API Health & Performance Matrix</h2>
138
+ <p>Real-time endpoint registry tracking authentication scopes, load test latencies, and active vulnerability states.</p>
139
+ </div>
140
+
141
+ <!-- Analytics Row -->
142
+ <div class="analytics-grid">
143
+ <div class="card">
144
+ <h3>Testing Coverage</h3>
145
+ <div class="rings-row">
146
+ <div class="ring-container">
147
+ <svg width="70" height="70" class="ring-svg">
148
+ <circle cx="35" cy="35" r="30" class="ring-bg"></circle>
149
+ <circle cx="35" cy="35" r="30" class="ring-bar" id="ring-bench"></circle>
150
+ </svg>
151
+ <span class="ring-text" id="ring-bench-text">0%</span>
152
+ <span style="font-size: 0.75rem; color: var(--text-dim);">Load Tested</span>
153
+ </div>
154
+ <div class="ring-container">
155
+ <svg width="70" height="70" class="ring-svg">
156
+ <circle cx="35" cy="35" r="30" class="ring-bg"></circle>
157
+ <circle cx="35" cy="35" r="30" class="ring-bar" id="ring-security" style="stroke: var(--success);"></circle>
158
+ </svg>
159
+ <span class="ring-text" id="ring-security-text">0%</span>
160
+ <span style="font-size: 0.75rem; color: var(--text-dim);">Secured</span>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="card">
166
+ <h3>Public Route Exposure</h3>
167
+ <div class="exposure-box">
168
+ <div style="display: flex; justify-content: space-between; font-size: 0.85rem;">
169
+ <span>🌐 Public: <strong id="exposure-public">0</strong></span>
170
+ <span>🔑 Protected: <strong id="exposure-private">0</strong></span>
171
+ </div>
172
+ <div class="progress-bar-bg">
173
+ <div class="progress-bar-val" id="exposure-bar-val" style="width: 0%;"></div>
174
+ </div>
175
+ <span style="font-size: 0.75rem; color: var(--text-dim);" id="exposure-ratio-desc">Analyzing exposure...</span>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="card">
180
+ <h3>Latency Hotspots (Avg ms)</h3>
181
+ <div class="radar-list" id="radar-list">
182
+ <p style="color: var(--text-dim); font-size: 0.85rem; padding-top: 0.5rem;">No benchmark data available.</p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Matrix Table -->
188
+ <div class="controls-row">
189
+ <input type="text" class="search-input" id="route-search" placeholder="Filter endpoints by path, method, or controller file..." oninput="filterRoutes()" />
190
+ </div>
191
+
192
+ <div class="matrix-card">
193
+ <h3>API Endpoint Registry</h3>
194
+ <div style="overflow-x: auto; margin-top: 1rem;">
195
+ <table id="matrix-table">
196
+ <thead>
197
+ <tr>
198
+ <th>Endpoint</th>
199
+ <th>Auth Status</th>
200
+ <th>Performance (RPS / Latency)</th>
201
+ <th>Security Scan</th>
202
+ <th>Quick Actions</th>
203
+ </tr>
204
+ </thead>
205
+ <tbody id="matrix-table-body">
206
+ <tr>
207
+ <td colspan="5" style="color: var(--text-dim); text-align: center; padding: 2rem;">Loading API blueprint...</td>
208
+ </tr>
209
+ </tbody>
210
+ </table>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Performance History (Tab 2) -->
216
+ <div id="performance" class="tab-content">
217
+ <div class="section-header">
218
+ <h2>Performance & Regression History</h2>
219
+ <p>Tracks benchmark latency and throughput trends linked with Git commits.</p>
220
+ </div>
221
+ <div class="card">
222
+ <h3>Recorded Load Test Runs</h3>
223
+ <div id="benchmarks-table-container">Loading metrics...</div>
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Security Inspector (Tab 3) -->
228
+ <div id="audits" class="tab-content">
229
+ <div class="section-header">
230
+ <h2>Security & Codebase Inspector</h2>
231
+ <p>Active penetration test findings (SQLi, XSS, Header Security) and diagnostic logs.</p>
232
+ </div>
233
+ <div class="card">
234
+ <h3>Active Penetration Test Results</h3>
235
+ <div id="audits-container">Loading diagnostics...</div>
236
+ </div>
237
+ </div>
238
+ </main>
239
+
240
+ <script>
241
+ let globalRoutes = [];
242
+ let globalAudits = [];
243
+
244
+ function showTab(tabId, btn) {
245
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
246
+ document.querySelectorAll('nav button').forEach(el => el.classList.remove('active'));
247
+ document.getElementById(tabId).classList.add('active');
248
+ btn.classList.add('active');
249
+ }
250
+
251
+ function copyToClipboard(text) {
252
+ navigator.clipboard.writeText(text).then(() => {
253
+ alert("Command copied to clipboard: " + text);
254
+ }).catch(err => {
255
+ console.error("Could not copy text: ", err);
256
+ });
257
+ }
258
+
259
+ function filterRoutes() {
260
+ const q = document.getElementById('route-search').value.toLowerCase();
261
+ const filtered = globalRoutes.filter(r =>
262
+ r.label.toLowerCase().includes(q) ||
263
+ (r.details && r.details.toLowerCase().includes(q))
264
+ );
265
+ renderMatrixTable(filtered);
266
+ }
267
+
268
+ function renderMatrixTable(routes) {
269
+ const tbody = document.getElementById('matrix-table-body');
270
+ if (routes.length === 0) {
271
+ tbody.innerHTML = '<tr><td colspan="5" style="color: var(--text-dim); text-align: center; padding: 2rem;">No matching endpoints found.</td></tr>';
272
+ return;
273
+ }
274
+
275
+ let html = '';
276
+ routes.forEach(r => {
277
+ const method = r.label.match(/\\[([A-Z]+)\\]/)[1];
278
+ const path = r.label.replace(/\\[([A-Z]+)\\]\\s*/, '');
279
+
280
+ // Performance
281
+ let perfHtml = '<span style="color: var(--text-dim); display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>Untested</span>';
282
+ if (r.metrics) {
283
+ const speedIcon = r.metrics.avg > 200
284
+ ? '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px; color: var(--warning);"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>'
285
+ : '<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" style="vertical-align: middle; margin-right: 3.5px; color: var(--primary);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>';
286
+ perfHtml = \`<div style="font-weight: 600; color: var(--primary); display: inline-flex; align-items: center;">\${speedIcon}\${r.metrics.rps.toFixed(0)} req/s</div>
287
+ <div style="font-size: 0.75rem; color: var(--text-dim);">Avg: \${r.metrics.avg.toFixed(1)}ms | p99: \${r.metrics.p99.toFixed(1)}ms</div>\`;
288
+ }
289
+
290
+ // Security
291
+ let secHtml = '<span class="badge badge-warning" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>Unscanned</span>';
292
+ if (globalAudits.length > 0 && globalAudits[0].items) {
293
+ const failures = globalAudits[0].items.filter(item => item.status === 'fail');
294
+ if (failures.length > 0) {
295
+ secHtml = '<span class="badge badge-danger" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>Vulnerable</span>';
296
+ } else {
297
+ secHtml = '<span class="badge badge-success" style="display: inline-flex; align-items: center;"><svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><polyline points="9 11 11 13 15 9"></polyline></svg>Secure</span>';
298
+ }
299
+ }
300
+
301
+ // Auth
302
+ const authBadgeClass = r.authStatus === 'Protected' ? 'badge-success' : 'badge-yellow';
303
+ const authIcon = r.authStatus === 'Protected'
304
+ ? '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>'
305
+ : '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" style="vertical-align: middle; margin-right: 3.5px;"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>';
306
+
307
+ // Copy command helpers
308
+ const host = window.location.origin;
309
+ const benchCmd = \`bandit bench \${host}\${path} -c 50 -r 2000\`;
310
+ const docCmd = \`bandit doctor \${host}\`;
311
+
312
+ html += \`<tr>
313
+ <td>
314
+ <strong style="color: var(--text);">\${r.label}</strong>
315
+ <div style="font-size: 0.75rem; color: var(--text-dim);">\${r.details || '-'}</div>
316
+ </td>
317
+ <td><span class="badge \${authBadgeClass}" style="display: inline-flex; align-items: center;">\${authIcon}\${r.authStatus || 'Public'}</span></td>
318
+ <td>\${perfHtml}</td>
319
+ <td>\${secHtml}</td>
320
+ <td class="actions-cell">
321
+ <button class="btn-action" onclick="copyToClipboard('\${benchCmd}')" title="Copy bench command">
322
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="currentColor"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
323
+ Bench
324
+ </button>
325
+ <button class="btn-action" onclick="copyToClipboard('\${docCmd}')" title="Copy doctor command">
326
+ <svg viewBox="0 0 24 24" width="11" height="11" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
327
+ Doctor
328
+ </button>
329
+ </td>
330
+ </tr>\`;
331
+ });
332
+ tbody.innerHTML = html;
333
+ }
334
+
335
+ async function loadData() {
336
+ // Load Audits
337
+ try {
338
+ const res = await fetch('/api/audits');
339
+ globalAudits = await res.json();
340
+ const container = document.getElementById('audits-container');
341
+ if (!globalAudits || globalAudits.length === 0 || !globalAudits[0].items) {
342
+ container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No active audits logged yet. Run <code>bandit doctor</code> in terminal to execute security probes.</p>';
343
+ } else {
344
+ let html = '<table><thead><tr><th>Status</th><th>Probe Title</th><th>Diagnostic Details</th><th>Remediation Suggestion</th></tr></thead><tbody>';
345
+ globalAudits[0].items.forEach(item => {
346
+ const badgeClass = item.status === 'pass' ? 'badge-success' : item.status === 'warn' ? 'badge-warning' : 'badge-danger';
347
+ html += \`<tr>
348
+ <td><span class="badge \${badgeClass}">\${item.status}</span></td>
349
+ <td><strong>\${item.title}</strong></td>
350
+ <td>\${item.details || '-'}</td>
351
+ <td style="color: var(--primary);">\${item.suggestion || 'No action needed'}</td>
352
+ </tr>\`;
353
+ });
354
+ html += '</tbody></table>';
355
+ container.innerHTML = html;
356
+ }
357
+ } catch (err) { console.error(err); }
358
+
359
+ // Load Blueprint Graph
360
+ try {
361
+ const res = await fetch('/api/blueprint');
362
+ const graph = await res.json();
363
+ globalRoutes = graph.nodes.filter(n => n.type === 'route');
364
+
365
+ // Renders Table
366
+ renderMatrixTable(globalRoutes);
367
+
368
+ // Process Analytics
369
+ const total = globalRoutes.length;
370
+ if (total > 0) {
371
+ // 1. Bench Ring
372
+ const benched = globalRoutes.filter(r => r.metrics).length;
373
+ const benchPct = Math.round((benched / total) * 100);
374
+ document.getElementById('ring-bench-text').innerText = benchPct + '%';
375
+ const benchOffset = 220 - (220 * benchPct) / 100;
376
+ document.getElementById('ring-bench').style.strokeDashoffset = benchOffset;
377
+
378
+ // 2. Security Ring
379
+ const securityPct = globalAudits.length > 0 ? 100 : 0;
380
+ document.getElementById('ring-security-text').innerText = securityPct + '%';
381
+ const secOffset = 220 - (220 * securityPct) / 100;
382
+ document.getElementById('ring-security').style.strokeDashoffset = secOffset;
383
+
384
+ // 3. Exposure Progress Bar
385
+ const privCount = globalRoutes.filter(r => r.authStatus === 'Protected').length;
386
+ const pubCount = total - privCount;
387
+ document.getElementById('exposure-public').innerText = pubCount;
388
+ document.getElementById('exposure-private').innerText = privCount;
389
+ const privatePct = Math.round((privCount / total) * 100);
390
+ document.getElementById('exposure-bar-val').style.width = privatePct + '%';
391
+ document.getElementById('exposure-ratio-desc').innerText = \`\${privatePct}% of your API routes are protected by middleware.\`;
392
+
393
+ // 4. Latency Hotspots (Slowest top 3)
394
+ const benchedRoutes = globalRoutes.filter(r => r.metrics);
395
+ const radarList = document.getElementById('radar-list');
396
+ if (benchedRoutes.length === 0) {
397
+ radarList.innerHTML = '<p style="color: var(--text-dim); font-size: 0.85rem; padding-top: 0.5rem;">No benchmark data available.</p>';
398
+ } else {
399
+ benchedRoutes.sort((a, b) => b.metrics.avg - a.metrics.avg);
400
+ const maxAvg = benchedRoutes[0].metrics.avg || 1;
401
+ let radarHtml = '';
402
+ benchedRoutes.slice(0, 3).forEach(br => {
403
+ const widthPct = Math.round((br.metrics.avg / maxAvg) * 100);
404
+ radarHtml += \`<div class="radar-item">
405
+ <div class="radar-labels">
406
+ <span style="font-weight: 500;">\${br.label}</span>
407
+ <span style="color: var(--danger); font-weight: 600;">\${br.metrics.avg.toFixed(0)} ms</span>
408
+ </div>
409
+ <div class="radar-bar-bg">
410
+ <div class="radar-bar-val" style="width: \${widthPct}%;"></div>
411
+ </div>
412
+ </div>\`;
413
+ });
414
+ radarList.innerHTML = radarHtml;
415
+ }
416
+ }
417
+ } catch (err) { console.error(err); }
418
+
419
+ // Load Benchmarks
420
+ try {
421
+ const res = await fetch('/api/benchmarks');
422
+ const data = await res.json();
423
+ const container = document.getElementById('benchmarks-table-container');
424
+ if (!data || data.length === 0) {
425
+ container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No benchmark history recorded yet. Run <code>bandit bench &lt;url&gt;</code> in your terminal.</p>';
426
+ } else {
427
+ let html = '<table><thead><tr><th>Timestamp</th><th>Commit</th><th>Target URL</th><th>Reqs / Conn</th><th>Throughput (RPS)</th><th>Avg Latency</th><th>p99 Tail Latency</th></tr></thead><tbody>';
428
+ data.forEach(b => {
429
+ html += \`<tr>
430
+ <td>\${new Date(b.timestamp).toLocaleString()}</td>
431
+ <td><code>\${b.gitCommit || 'HEAD'}</code></td>
432
+ <td><strong>\${b.targetUrl}</strong></td>
433
+ <td>\${b.requests} / \${b.connections}</td>
434
+ <td style="color: var(--primary); font-weight: 700;">\${b.rps.toFixed(1)} req/s</td>
435
+ <td>\${b.latency.avg.toFixed(1)} ms</td>
436
+ <td style="color: \${b.latency.p99 > 200 ? 'var(--warning)' : 'var(--success)'}; font-weight: 600;">\${b.latency.p99.toFixed(1)} ms</td>
437
+ </tr>\`;
438
+ });
439
+ html += '</tbody></table>';
440
+ container.innerHTML = html;
441
+ }
442
+ } catch (err) { console.error(err); }
443
+ }
444
+
445
+ loadData();
446
+ </script>
447
+ </body>
448
+ </html>`;
449
+ };
450
+ const server = http.createServer(async (req, res) => {
451
+ const url = req.url || "/";
452
+ // Serve custom logo if saved in project root
453
+ if (url === "/api/logo" && req.method === "GET") {
454
+ const logoPath = path.join(projectPath, "logo.png");
455
+ if (fs.existsSync(logoPath)) {
456
+ res.writeHead(200, { "Content-Type": "image/png" });
457
+ res.end(fs.readFileSync(logoPath));
458
+ }
459
+ else {
460
+ res.writeHead(404);
461
+ res.end();
462
+ }
463
+ return;
464
+ }
465
+ if (url === "/api/benchmarks" && req.method === "GET") {
466
+ res.writeHead(200, { "Content-Type": "application/json" });
467
+ res.end(JSON.stringify(db.getBenchmarks()));
468
+ return;
469
+ }
470
+ if (url === "/api/audits" && req.method === "GET") {
471
+ res.writeHead(200, { "Content-Type": "application/json" });
472
+ res.end(JSON.stringify(db.getAudits()));
473
+ return;
474
+ }
475
+ if (url === "/api/blueprint" && req.method === "GET") {
476
+ res.writeHead(200, { "Content-Type": "application/json" });
477
+ const blueprint = await generateApiBlueprint(projectPath);
478
+ res.end(JSON.stringify(blueprint));
479
+ return;
480
+ }
481
+ if (url === "/api/config/dismiss" && req.method === "POST") {
482
+ let body = "";
483
+ req.on("data", chunk => { body += chunk; });
484
+ req.on("end", () => {
485
+ try {
486
+ const { ruleTitle } = JSON.parse(body);
487
+ const updated = db.dismissRule(ruleTitle);
488
+ res.writeHead(200, { "Content-Type": "application/json" });
489
+ res.end(JSON.stringify(updated));
490
+ }
491
+ catch {
492
+ res.writeHead(400, { "Content-Type": "application/json" });
493
+ res.end(JSON.stringify({ error: "Invalid payload" }));
494
+ }
495
+ });
496
+ return;
497
+ }
498
+ res.writeHead(200, { "Content-Type": "text/html" });
499
+ res.end(getClientHtml());
500
+ });
501
+ return new Promise((resolve, reject) => {
502
+ server.listen(port, () => {
503
+ resolve({ server, port });
504
+ });
505
+ server.on("error", (err) => {
506
+ if (err.code === "EADDRINUSE") {
507
+ server.listen(port + 1, () => {
508
+ resolve({ server, port: port + 1 });
509
+ });
510
+ }
511
+ else {
512
+ reject(err);
513
+ }
514
+ });
515
+ });
516
+ }
@@ -3,6 +3,22 @@ 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 detectFramework(pkg) {
7
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
8
+ const frameworks = {
9
+ express: "Express",
10
+ "@nestjs/core": "NestJS",
11
+ fastify: "Fastify",
12
+ koa: "Koa",
13
+ hapi: "Hapi",
14
+ };
15
+ for (const [dep, label] of Object.entries(frameworks)) {
16
+ if (deps[dep]) {
17
+ return label;
18
+ }
19
+ }
20
+ return "gRPC / Library";
21
+ }
6
22
  export async function resolveProjectPath(inputPath = ".") {
7
23
  const absoluteRoot = path.resolve(inputPath);
8
24
  // Find all package.json files, ignoring dependency and output directories
@@ -24,6 +40,7 @@ export async function resolveProjectPath(inputPath = ".") {
24
40
  services.push({
25
41
  name: pkg.name || path.basename(dir),
26
42
  path: dir,
43
+ framework: detectFramework(pkg),
27
44
  });
28
45
  }
29
46
  catch { }
@@ -33,9 +50,12 @@ export async function resolveProjectPath(inputPath = ".") {
33
50
  clack.log.info(`${chalk.bold.yellow("Monorepo detected!")} Found ${chalk.bold.cyan(services.length)} nested packages.`);
34
51
  const options = services.map((s) => {
35
52
  const relPath = path.relative(absoluteRoot, s.path) || ".";
53
+ const fwLabel = s.framework === "gRPC / Library"
54
+ ? chalk.dim(s.framework)
55
+ : chalk.cyan(s.framework);
36
56
  return {
37
57
  value: s.path,
38
- label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)}`,
58
+ label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)} - ${fwLabel}`,
39
59
  };
40
60
  });
41
61
  const selectedPath = await clack.select({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bandit-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {