bandit-cli 1.0.6 → 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/env.js +7 -2
- package/dist/index.js +0 -11
- package/dist/rules/index.js +2 -0
- package/dist/rules/mvp.rules.js +103 -0
- package/dist/studio/scanner.js +22 -0
- package/dist/studio/server.js +354 -122
- package/dist/utils/monorepo.js +21 -1
- package/package.json +1 -1
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 === "" ||
|
|
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 &&
|
|
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 {
|
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")
|
package/dist/rules/index.js
CHANGED
|
@@ -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
|
package/dist/rules/mvp.rules.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/studio/scanner.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { scanForRoutes } from "../cli/api.js";
|
|
2
4
|
import { StudioDB } from "./db.js";
|
|
3
5
|
export async function generateApiBlueprint(projectPath) {
|
|
@@ -24,11 +26,31 @@ export async function generateApiBlueprint(projectPath) {
|
|
|
24
26
|
// Find matching telemetry if benchmarked
|
|
25
27
|
const matchBench = benchmarks.find(b => b.targetUrl.endsWith(r.routePath));
|
|
26
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 { }
|
|
27
48
|
nodes.push({
|
|
28
49
|
id: routeId,
|
|
29
50
|
label: routeLabel,
|
|
30
51
|
type: "route",
|
|
31
52
|
details: r.sourceFile,
|
|
53
|
+
authStatus,
|
|
32
54
|
metrics,
|
|
33
55
|
});
|
|
34
56
|
const controllerName = r.sourceFile.split(/[/\\]/).pop() || "Controller";
|
package/dist/studio/server.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
2
4
|
import { StudioDB } from "./db.js";
|
|
3
5
|
import { generateApiBlueprint } from "./scanner.js";
|
|
4
6
|
export async function startStudioServer(projectPath, port = 4000) {
|
|
@@ -11,109 +13,234 @@ export async function startStudioServer(projectPath, port = 4000) {
|
|
|
11
13
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
14
|
<title>Bandit Studio - Backend Developer Platform</title>
|
|
13
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
|
+
|
|
14
18
|
:root {
|
|
15
|
-
--bg: #
|
|
16
|
-
--card-bg: #
|
|
17
|
-
--card-hover: #
|
|
18
|
-
--border: #
|
|
19
|
-
--border-bright: #
|
|
19
|
+
--bg: #050507;
|
|
20
|
+
--card-bg: #0c0c0e;
|
|
21
|
+
--card-hover: #121215;
|
|
22
|
+
--border: #1f1f23;
|
|
23
|
+
--border-bright: #2f2f37;
|
|
20
24
|
--text: #f4f4f5;
|
|
21
|
-
--text-dim: #
|
|
25
|
+
--text-dim: #71717a;
|
|
22
26
|
--primary: #eab308;
|
|
23
27
|
--primary-hover: #facc15;
|
|
24
|
-
--primary-dim: rgba(234, 179, 8, 0.
|
|
28
|
+
--primary-dim: rgba(234, 179, 8, 0.08);
|
|
25
29
|
--success: #22c55e;
|
|
30
|
+
--success-dim: rgba(34, 197, 94, 0.08);
|
|
26
31
|
--warning: #f59e0b;
|
|
32
|
+
--warning-dim: rgba(245, 158, 11, 0.08);
|
|
27
33
|
--danger: #ef4444;
|
|
28
|
-
--
|
|
29
|
-
--accent
|
|
34
|
+
--danger-dim: rgba(239, 68, 68, 0.08);
|
|
35
|
+
--accent: #a855f7;
|
|
30
36
|
}
|
|
31
37
|
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Inter', -apple-system, sans-serif; }
|
|
32
38
|
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:
|
|
34
|
-
|
|
35
|
-
.logo-
|
|
36
|
-
|
|
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); }
|
|
37
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; }
|
|
38
|
-
nav button.active { color: #
|
|
48
|
+
nav button.active { color: #000000; background: var(--primary); font-weight: 700; }
|
|
39
49
|
nav button:hover:not(.active) { color: var(--text); background: var(--card-hover); }
|
|
50
|
+
|
|
40
51
|
main { flex: 1; padding: 2rem; max-width: 1280px; margin: 0 auto; width: 100%; }
|
|
41
52
|
.tab-content { display: none; }
|
|
42
53
|
.tab-content.active { display: block; }
|
|
43
|
-
|
|
44
|
-
.section-header
|
|
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; }
|
|
45
57
|
.section-header p { color: var(--text-dim); font-size: 0.9rem; margin-top: 0.25rem; }
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.card
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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; }
|
|
52
95
|
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
96
|
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
code
|
|
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); }
|
|
70
109
|
</style>
|
|
71
110
|
</head>
|
|
72
111
|
<body>
|
|
73
112
|
<header>
|
|
74
|
-
<div class="logo
|
|
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>
|
|
75
118
|
<nav>
|
|
76
|
-
<button
|
|
77
|
-
|
|
78
|
-
|
|
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>
|
|
79
131
|
</nav>
|
|
80
132
|
</header>
|
|
81
133
|
<main>
|
|
82
|
-
|
|
134
|
+
<!-- API Health Matrix (Tab 1) -->
|
|
135
|
+
<div id="blueprint" class="tab-content active">
|
|
83
136
|
<div class="section-header">
|
|
84
|
-
<h2>
|
|
85
|
-
<p>
|
|
137
|
+
<h2>API Health & Performance Matrix</h2>
|
|
138
|
+
<p>Real-time endpoint registry tracking authentication scopes, load test latencies, and active vulnerability states.</p>
|
|
86
139
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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>
|
|
90
212
|
</div>
|
|
91
213
|
</div>
|
|
92
214
|
|
|
93
|
-
|
|
215
|
+
<!-- Performance History (Tab 2) -->
|
|
216
|
+
<div id="performance" class="tab-content">
|
|
94
217
|
<div class="section-header">
|
|
95
|
-
<h2>
|
|
96
|
-
<p>
|
|
218
|
+
<h2>Performance & Regression History</h2>
|
|
219
|
+
<p>Tracks benchmark latency and throughput trends linked with Git commits.</p>
|
|
97
220
|
</div>
|
|
98
221
|
<div class="card">
|
|
99
|
-
<h3>
|
|
100
|
-
<div id="
|
|
222
|
+
<h3>Recorded Load Test Runs</h3>
|
|
223
|
+
<div id="benchmarks-table-container">Loading metrics...</div>
|
|
101
224
|
</div>
|
|
102
225
|
</div>
|
|
103
226
|
|
|
227
|
+
<!-- Security Inspector (Tab 3) -->
|
|
104
228
|
<div id="audits" class="tab-content">
|
|
105
229
|
<div class="section-header">
|
|
106
230
|
<h2>Security & Codebase Inspector</h2>
|
|
107
|
-
<p>Active penetration test findings (SQLi, XSS, Header Security) and
|
|
231
|
+
<p>Active penetration test findings (SQLi, XSS, Header Security) and diagnostic logs.</p>
|
|
108
232
|
</div>
|
|
109
233
|
<div class="card">
|
|
110
234
|
<h3>Active Penetration Test Results</h3>
|
|
111
|
-
<div id="audits-container">Loading
|
|
235
|
+
<div id="audits-container">Loading diagnostics...</div>
|
|
112
236
|
</div>
|
|
113
237
|
</div>
|
|
114
238
|
</main>
|
|
115
239
|
|
|
116
240
|
<script>
|
|
241
|
+
let globalRoutes = [];
|
|
242
|
+
let globalAudits = [];
|
|
243
|
+
|
|
117
244
|
function showTab(tabId, btn) {
|
|
118
245
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
119
246
|
document.querySelectorAll('nav button').forEach(el => el.classList.remove('active'));
|
|
@@ -121,25 +248,107 @@ export async function startStudioServer(projectPath, port = 4000) {
|
|
|
121
248
|
btn.classList.add('active');
|
|
122
249
|
}
|
|
123
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
|
+
|
|
124
335
|
async function loadData() {
|
|
125
|
-
// Load
|
|
336
|
+
// Load Audits
|
|
126
337
|
try {
|
|
127
|
-
const res = await fetch('/api/
|
|
128
|
-
|
|
129
|
-
const container = document.getElementById('
|
|
130
|
-
if (!
|
|
131
|
-
container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No
|
|
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>';
|
|
132
343
|
} else {
|
|
133
|
-
let html = '<table><thead><tr><th>
|
|
134
|
-
|
|
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';
|
|
135
347
|
html += \`<tr>
|
|
136
|
-
<td>\${
|
|
137
|
-
<td><
|
|
138
|
-
<td
|
|
139
|
-
<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>
|
|
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>
|
|
143
352
|
</tr>\`;
|
|
144
353
|
});
|
|
145
354
|
html += '</tbody></table>';
|
|
@@ -151,70 +360,80 @@ export async function startStudioServer(projectPath, port = 4000) {
|
|
|
151
360
|
try {
|
|
152
361
|
const res = await fetch('/api/blueprint');
|
|
153
362
|
const graph = await res.json();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
363
|
+
globalRoutes = graph.nodes.filter(n => n.type === 'route');
|
|
364
|
+
|
|
365
|
+
// Renders Table
|
|
366
|
+
renderMatrixTable(globalRoutes);
|
|
169
367
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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>
|
|
188
408
|
</div>
|
|
189
|
-
<div class="
|
|
190
|
-
|
|
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>
|
|
409
|
+
<div class="radar-bar-bg">
|
|
410
|
+
<div class="radar-bar-val" style="width: \${widthPct}%;"></div>
|
|
194
411
|
</div>
|
|
195
412
|
</div>\`;
|
|
196
413
|
});
|
|
414
|
+
radarList.innerHTML = radarHtml;
|
|
197
415
|
}
|
|
198
|
-
container.innerHTML = html;
|
|
199
416
|
}
|
|
200
417
|
} catch (err) { console.error(err); }
|
|
201
418
|
|
|
202
|
-
// Load
|
|
419
|
+
// Load Benchmarks
|
|
203
420
|
try {
|
|
204
|
-
const res = await fetch('/api/
|
|
205
|
-
const
|
|
206
|
-
const container = document.getElementById('
|
|
207
|
-
if (!
|
|
208
|
-
container.innerHTML = '<p style="color: var(--text-dim); padding: 1rem 0;">No
|
|
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 <url></code> in your terminal.</p>';
|
|
209
426
|
} else {
|
|
210
|
-
let html = '<table><thead><tr><th>
|
|
211
|
-
|
|
212
|
-
const badgeClass = item.status === 'pass' ? 'badge-success' : item.status === 'warn' ? 'badge-warning' : 'badge-danger';
|
|
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 => {
|
|
213
429
|
html += \`<tr>
|
|
214
|
-
<td
|
|
215
|
-
<td><
|
|
216
|
-
<td>\${
|
|
217
|
-
<td
|
|
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>
|
|
218
437
|
</tr>\`;
|
|
219
438
|
});
|
|
220
439
|
html += '</tbody></table>';
|
|
@@ -230,6 +449,19 @@ export async function startStudioServer(projectPath, port = 4000) {
|
|
|
230
449
|
};
|
|
231
450
|
const server = http.createServer(async (req, res) => {
|
|
232
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
|
+
}
|
|
233
465
|
if (url === "/api/benchmarks" && req.method === "GET") {
|
|
234
466
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
235
467
|
res.end(JSON.stringify(db.getBenchmarks()));
|
package/dist/utils/monorepo.js
CHANGED
|
@@ -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({
|