bandit-cli 1.0.4 → 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.
- package/dist/cli/api.js +1 -1
- package/dist/cli/bench.js +12 -0
- package/dist/cli/doctor.js +6 -0
- package/dist/cli/studio.js +54 -0
- package/dist/index.js +11 -0
- package/dist/studio/db.js +96 -0
- package/dist/studio/scanner.js +66 -0
- package/dist/studio/server.js +284 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
@@ -100,6 +100,17 @@ program
|
|
|
100
100
|
const { runBenchCommand } = await import("./cli/bench.js");
|
|
101
101
|
await runBenchCommand(url, { connections, requests });
|
|
102
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
|
+
});
|
|
103
114
|
// Default to help if no command specified
|
|
104
115
|
if (!process.argv.slice(2).length) {
|
|
105
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
|
+
}
|
|
@@ -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 <url></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
|
+
}
|