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.
@@ -0,0 +1,284 @@
1
+ import http from "node:http";
2
+ import { StudioDB } from "./db.js";
3
+ import { generateApiBlueprint } from "./scanner.js";
4
+ export async function startStudioServer(projectPath, port = 4000) {
5
+ const db = new StudioDB(projectPath);
6
+ const getClientHtml = () => {
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
+ <title>Bandit Studio - Backend Developer Platform</title>
13
+ <style>
14
+ :root {
15
+ --bg: #09090b;
16
+ --card-bg: #141417;
17
+ --card-hover: #1c1c21;
18
+ --border: #27272a;
19
+ --border-bright: #3f3f46;
20
+ --text: #f4f4f5;
21
+ --text-dim: #a1a1aa;
22
+ --primary: #eab308;
23
+ --primary-hover: #facc15;
24
+ --primary-dim: rgba(234, 179, 8, 0.1);
25
+ --success: #22c55e;
26
+ --warning: #f59e0b;
27
+ --danger: #ef4444;
28
+ --accent-blue: #38bdf8;
29
+ --accent-purple: #c084fc;
30
+ }
31
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Inter', -apple-system, sans-serif; }
32
+ body { background-color: var(--bg); color: var(--text); display: flex; flex-direction: column; min-height: 100vh; }
33
+ header { background: #000000; border-bottom: 1px solid var(--border); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
34
+ .logo { font-size: 1.25rem; font-weight: 800; color: var(--primary); display: flex; align-items: center; gap: 0.6rem; letter-spacing: -0.02em; }
35
+ .logo-badge { background: var(--primary); color: #000; font-size: 0.7rem; font-weight: 900; padding: 0.15rem 0.4rem; border-radius: 0.2rem; text-transform: uppercase; }
36
+ nav { display: flex; gap: 0.5rem; background: #141417; padding: 0.3rem; border-radius: 0.5rem; border: 1px solid var(--border); }
37
+ 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; }
38
+ nav button.active { color: #000; background: var(--primary); font-weight: 700; }
39
+ nav button:hover:not(.active) { color: var(--text); background: var(--card-hover); }
40
+ main { flex: 1; padding: 2rem; max-width: 1280px; margin: 0 auto; width: 100%; }
41
+ .tab-content { display: none; }
42
+ .tab-content.active { display: block; }
43
+ .section-header { margin-bottom: 1.5rem; }
44
+ .section-header h2 { font-size: 1.5rem; font-weight: 700; color: var(--text); letter-spacing: -0.02em; }
45
+ .section-header p { color: var(--text-dim); font-size: 0.9rem; margin-top: 0.25rem; }
46
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; }
47
+ .card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; position: relative; overflow: hidden; }
48
+ .card h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 1rem; color: var(--primary); display: flex; align-items: center; justify-content: space-between; }
49
+ table { width: 100%; border-collapse: collapse; margin-top: 0.5rem; text-align: left; }
50
+ th, td { padding: 0.85rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.875rem; }
51
+ th { color: var(--text-dim); font-weight: 600; background: #09090b; }
52
+ tr:hover td { background: var(--card-hover); }
53
+ .badge { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; display: inline-block; }
54
+ .badge-success { background: rgba(34, 197, 94, 0.15); color: var(--success); border: 1px solid rgba(34, 197, 94, 0.3); }
55
+ .badge-warning { background: rgba(245, 158, 11, 0.15); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
56
+ .badge-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.3); }
57
+ .badge-yellow { background: var(--primary-dim); color: var(--primary); border: 1px solid rgba(234, 179, 8, 0.3); }
58
+
59
+ /* Architecture Blueprint Flow Layout */
60
+ .flow-container { display: flex; flex-direction: column; gap: 1.5rem; margin-top: 1rem; }
61
+ .flow-row { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; background: #09090b; padding: 1.25rem; border-radius: 0.75rem; border: 1px solid var(--border); }
62
+ .flow-step { background: var(--card-bg); border: 1px solid var(--border); padding: 0.85rem 1.1rem; border-radius: 0.5rem; font-size: 0.875rem; display: flex; flex-direction: column; gap: 0.25rem; min-width: 200px; }
63
+ .flow-step.route { border-color: var(--primary); }
64
+ .flow-step.controller { border-color: var(--accent-blue); }
65
+ .flow-step.service { border-color: var(--accent-purple); }
66
+ .flow-step.infra { border-color: var(--success); }
67
+ .flow-arrow { color: var(--primary); font-weight: bold; font-size: 1.2rem; }
68
+ .metric-tag { font-size: 0.75rem; background: var(--primary-dim); color: var(--primary); padding: 0.15rem 0.4rem; border-radius: 0.2rem; font-weight: 700; width: fit-content; margin-top: 0.2rem; }
69
+ code { font-family: monospace; background: #000; padding: 0.15rem 0.35rem; border-radius: 0.2rem; font-size: 0.8rem; color: var(--primary); }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <header>
74
+ <div class="logo">⚑ BANDIT <span class="logo-badge">STUDIO</span></div>
75
+ <nav>
76
+ <button class="active" onclick="showTab('performance', this)">πŸ“ˆ Performance Regressions</button>
77
+ <button onclick="showTab('blueprint', this)">πŸ—ΊοΈ Architecture Blueprint</button>
78
+ <button onclick="showTab('audits', this)">πŸ›‘οΈ Security Inspector</button>
79
+ </nav>
80
+ </header>
81
+ <main>
82
+ <div id="performance" class="tab-content active">
83
+ <div class="section-header">
84
+ <h2>Performance & Regression History</h2>
85
+ <p>Tracks benchmark latency and throughput linked with Git commits.</p>
86
+ </div>
87
+ <div class="card">
88
+ <h3>Recorded Load Test Runs</h3>
89
+ <div id="benchmarks-table-container">Loading metrics...</div>
90
+ </div>
91
+ </div>
92
+
93
+ <div id="blueprint" class="tab-content">
94
+ <div class="section-header">
95
+ <h2>Backend Architecture Blueprint</h2>
96
+ <p>Visual component dependency map tracing HTTP requests down to Controllers, Services, and Database layers.</p>
97
+ </div>
98
+ <div class="card">
99
+ <h3>Request Execution Flow</h3>
100
+ <div id="blueprint-container" class="flow-container">Loading architecture graph...</div>
101
+ </div>
102
+ </div>
103
+
104
+ <div id="audits" class="tab-content">
105
+ <div class="section-header">
106
+ <h2>Security & Codebase Inspector</h2>
107
+ <p>Active penetration test findings (SQLi, XSS, Header Security) and framework diagnostic reports.</p>
108
+ </div>
109
+ <div class="card">
110
+ <h3>Active Penetration Test Results</h3>
111
+ <div id="audits-container">Loading diagnostic findings...</div>
112
+ </div>
113
+ </div>
114
+ </main>
115
+
116
+ <script>
117
+ function showTab(tabId, btn) {
118
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
119
+ document.querySelectorAll('nav button').forEach(el => el.classList.remove('active'));
120
+ document.getElementById(tabId).classList.add('active');
121
+ btn.classList.add('active');
122
+ }
123
+
124
+ async function loadData() {
125
+ // Load Benchmarks
126
+ try {
127
+ const res = await fetch('/api/benchmarks');
128
+ const data = await res.json();
129
+ const container = document.getElementById('benchmarks-table-container');
130
+ if (!data || data.length === 0) {
131
+ 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 to log performance metrics.</p>';
132
+ } else {
133
+ 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>';
134
+ data.forEach(b => {
135
+ html += \`<tr>
136
+ <td>\${new Date(b.timestamp).toLocaleString()}</td>
137
+ <td><code>\${b.gitCommit || 'HEAD'}</code></td>
138
+ <td><strong>\${b.targetUrl}</strong></td>
139
+ <td>\${b.requests} / \${b.connections}</td>
140
+ <td style="color: var(--primary); font-weight: 700;">\${b.rps.toFixed(1)} req/s</td>
141
+ <td>\${b.latency.avg.toFixed(1)} ms</td>
142
+ <td style="color: \${b.latency.p99 > 200 ? 'var(--warning)' : 'var(--success)'}; font-weight: 600;">\${b.latency.p99.toFixed(1)} ms</td>
143
+ </tr>\`;
144
+ });
145
+ html += '</tbody></table>';
146
+ container.innerHTML = html;
147
+ }
148
+ } catch (err) { console.error(err); }
149
+
150
+ // Load Blueprint Graph
151
+ try {
152
+ const res = await fetch('/api/blueprint');
153
+ const graph = await res.json();
154
+ const container = document.getElementById('blueprint-container');
155
+ if (!graph.nodes || graph.nodes.length === 0) {
156
+ container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No routes discovered in project.</p>';
157
+ } else {
158
+ const routeNodes = graph.nodes.filter(n => n.type === 'route');
159
+ let html = '';
160
+
161
+ if (routeNodes.length === 0) {
162
+ html = '<p style="color: var(--text-dim);">No API endpoints found in source directory.</p>';
163
+ } else {
164
+ routeNodes.forEach(r => {
165
+ const controllerEdge = graph.edges.find(e => e.from === r.id);
166
+ const controllerNode = controllerEdge ? graph.nodes.find(n => n.id === controllerEdge.to) : null;
167
+ const serviceEdge = controllerNode ? graph.edges.find(e => e.from === controllerNode.id) : null;
168
+ const serviceNode = serviceEdge ? graph.nodes.find(n => n.id === serviceEdge.to) : null;
169
+
170
+ html += \`<div class="flow-row">
171
+ <div class="flow-step route">
172
+ <span class="badge badge-yellow">HTTP ENDPOINT</span>
173
+ <strong>\${r.label}</strong>
174
+ <span style="font-size:0.75rem; color:var(--text-dim)">\${r.details || ''}</span>
175
+ \${r.metrics ? \`<div class="metric-tag">\${r.metrics.rps.toFixed(0)} RPS | p99: \${r.metrics.p99.toFixed(0)}ms</div>\` : ''}
176
+ </div>
177
+ <div class="flow-arrow">βž”</div>
178
+ <div class="flow-step controller">
179
+ <span class="badge badge-yellow" style="color:var(--accent-blue); background:rgba(56,189,248,0.1); border-color:rgba(56,189,248,0.3)">CONTROLLER LAYER</span>
180
+ <strong>\${controllerNode ? controllerNode.label : 'Route Handler'}</strong>
181
+ <span style="font-size:0.75rem; color:var(--text-dim)">Delegates request validation</span>
182
+ </div>
183
+ <div class="flow-arrow">βž”</div>
184
+ <div class="flow-step service">
185
+ <span class="badge badge-yellow" style="color:var(--accent-purple); background:rgba(192,132,252,0.1); border-color:rgba(192,132,252,0.3)">SERVICE LAYER</span>
186
+ <strong>\${serviceNode ? serviceNode.label : 'Business Service'}</strong>
187
+ <span style="font-size:0.75rem; color:var(--text-dim)">ORM & Business logic</span>
188
+ </div>
189
+ <div class="flow-arrow">βž”</div>
190
+ <div class="flow-step infra">
191
+ <span class="badge badge-success">PERSISTENCE</span>
192
+ <strong>PostgreSQL / Redis</strong>
193
+ <span style="font-size:0.75rem; color:var(--text-dim)">Connection Pool</span>
194
+ </div>
195
+ </div>\`;
196
+ });
197
+ }
198
+ container.innerHTML = html;
199
+ }
200
+ } catch (err) { console.error(err); }
201
+
202
+ // Load Audits
203
+ try {
204
+ const res = await fetch('/api/audits');
205
+ const audits = await res.json();
206
+ const container = document.getElementById('audits-container');
207
+ if (!audits || audits.length === 0 || !audits[0].items) {
208
+ 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>';
209
+ } else {
210
+ let html = '<table><thead><tr><th>Status</th><th>Probe Title</th><th>Diagnostic Details</th><th>Remediation Suggestion</th></tr></thead><tbody>';
211
+ audits[0].items.forEach(item => {
212
+ const badgeClass = item.status === 'pass' ? 'badge-success' : item.status === 'warn' ? 'badge-warning' : 'badge-danger';
213
+ html += \`<tr>
214
+ <td><span class="badge \${badgeClass}">\${item.status}</span></td>
215
+ <td><strong>\${item.title}</strong></td>
216
+ <td>\${item.details || '-'}</td>
217
+ <td style="color: var(--primary);">\${item.suggestion || 'No action needed'}</td>
218
+ </tr>\`;
219
+ });
220
+ html += '</tbody></table>';
221
+ container.innerHTML = html;
222
+ }
223
+ } catch (err) { console.error(err); }
224
+ }
225
+
226
+ loadData();
227
+ </script>
228
+ </body>
229
+ </html>`;
230
+ };
231
+ const server = http.createServer(async (req, res) => {
232
+ const url = req.url || "/";
233
+ if (url === "/api/benchmarks" && req.method === "GET") {
234
+ res.writeHead(200, { "Content-Type": "application/json" });
235
+ res.end(JSON.stringify(db.getBenchmarks()));
236
+ return;
237
+ }
238
+ if (url === "/api/audits" && req.method === "GET") {
239
+ res.writeHead(200, { "Content-Type": "application/json" });
240
+ res.end(JSON.stringify(db.getAudits()));
241
+ return;
242
+ }
243
+ if (url === "/api/blueprint" && req.method === "GET") {
244
+ res.writeHead(200, { "Content-Type": "application/json" });
245
+ const blueprint = await generateApiBlueprint(projectPath);
246
+ res.end(JSON.stringify(blueprint));
247
+ return;
248
+ }
249
+ if (url === "/api/config/dismiss" && req.method === "POST") {
250
+ let body = "";
251
+ req.on("data", chunk => { body += chunk; });
252
+ req.on("end", () => {
253
+ try {
254
+ const { ruleTitle } = JSON.parse(body);
255
+ const updated = db.dismissRule(ruleTitle);
256
+ res.writeHead(200, { "Content-Type": "application/json" });
257
+ res.end(JSON.stringify(updated));
258
+ }
259
+ catch {
260
+ res.writeHead(400, { "Content-Type": "application/json" });
261
+ res.end(JSON.stringify({ error: "Invalid payload" }));
262
+ }
263
+ });
264
+ return;
265
+ }
266
+ res.writeHead(200, { "Content-Type": "text/html" });
267
+ res.end(getClientHtml());
268
+ });
269
+ return new Promise((resolve, reject) => {
270
+ server.listen(port, () => {
271
+ resolve({ server, port });
272
+ });
273
+ server.on("error", (err) => {
274
+ if (err.code === "EADDRINUSE") {
275
+ server.listen(port + 1, () => {
276
+ resolve({ server, port: port + 1 });
277
+ });
278
+ }
279
+ else {
280
+ reject(err);
281
+ }
282
+ });
283
+ });
284
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import * as clack from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ export async function resolveProjectPath(inputPath = ".") {
7
+ const absoluteRoot = path.resolve(inputPath);
8
+ // Find all package.json files, ignoring dependency and output directories
9
+ const packageFiles = await fg(["**/package.json"], {
10
+ cwd: absoluteRoot,
11
+ absolute: true,
12
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/coverage/**"],
13
+ });
14
+ if (packageFiles.length <= 1) {
15
+ // If only one package.json (or none) is found, bypass selection
16
+ return inputPath;
17
+ }
18
+ const services = [];
19
+ for (const file of packageFiles) {
20
+ try {
21
+ const content = fs.readFileSync(file, "utf-8");
22
+ const pkg = JSON.parse(content);
23
+ const dir = path.dirname(file);
24
+ services.push({
25
+ name: pkg.name || path.basename(dir),
26
+ path: dir,
27
+ });
28
+ }
29
+ catch { }
30
+ }
31
+ // Sort services by name alphabetically
32
+ services.sort((a, b) => a.name.localeCompare(b.name));
33
+ clack.log.info(`${chalk.bold.yellow("Monorepo detected!")} Found ${chalk.bold.cyan(services.length)} nested packages.`);
34
+ const options = services.map((s) => {
35
+ const relPath = path.relative(absoluteRoot, s.path) || ".";
36
+ return {
37
+ value: s.path,
38
+ label: `${chalk.bold.green(s.name)} ${chalk.gray(`(${relPath})`)}`,
39
+ };
40
+ });
41
+ const selectedPath = await clack.select({
42
+ message: "Select a microservice to target:",
43
+ options: [
44
+ ...options,
45
+ { value: "cancel", label: chalk.gray("Cancel / Exit") },
46
+ ],
47
+ });
48
+ if (clack.isCancel(selectedPath) || selectedPath === "cancel") {
49
+ clack.outro("Execution stopped.");
50
+ process.exit(0);
51
+ }
52
+ const relSelected = path.relative(absoluteRoot, selectedPath) || ".";
53
+ clack.log.step(`Target set to: ${chalk.bold.magenta(relSelected)}`);
54
+ console.log(""); // Empty line for spacing
55
+ return selectedPath;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bandit-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,7 +18,7 @@
18
18
  "license": "ISC",
19
19
  "type": "module",
20
20
  "bin": {
21
- "bandit": "./dist/index.js"
21
+ "bandit": "dist/index.js"
22
22
  },
23
23
  "dependencies": {
24
24
  "@clack/prompts": "^1.5.1",