brakit 0.8.0 → 0.8.2
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/README.md +17 -6
- package/dist/api.d.ts +165 -82
- package/dist/api.js +245 -262
- package/dist/bin/brakit.js +132 -210
- package/dist/mcp/server.js +65 -15
- package/dist/runtime/index.js +2192 -1858
- package/package.json +1 -1
package/dist/bin/brakit.js
CHANGED
|
@@ -28,12 +28,11 @@ var init_routes = __esm({
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// src/constants/limits.ts
|
|
31
|
-
var
|
|
31
|
+
var MAX_INGEST_BYTES;
|
|
32
32
|
var init_limits = __esm({
|
|
33
33
|
"src/constants/limits.ts"() {
|
|
34
34
|
"use strict";
|
|
35
|
-
|
|
36
|
-
MAX_TELEMETRY_ENTRIES = 1e3;
|
|
35
|
+
MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
37
36
|
}
|
|
38
37
|
});
|
|
39
38
|
|
|
@@ -78,23 +77,47 @@ var init_network = __esm({
|
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
// src/constants/mcp.ts
|
|
81
|
-
var MCP_SERVER_NAME, MCP_SERVER_VERSION, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
|
|
80
|
+
var MCP_SERVER_NAME, MCP_SERVER_VERSION, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
|
|
82
81
|
var init_mcp = __esm({
|
|
83
82
|
"src/constants/mcp.ts"() {
|
|
84
83
|
"use strict";
|
|
85
84
|
MCP_SERVER_NAME = "brakit";
|
|
86
|
-
MCP_SERVER_VERSION = "0.8.
|
|
85
|
+
MCP_SERVER_VERSION = "0.8.2";
|
|
87
86
|
INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
88
87
|
LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
89
88
|
CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
90
89
|
HEALTH_CHECK_TIMEOUT_MS = 3e3;
|
|
91
90
|
DISCOVERY_POLL_INTERVAL_MS = 500;
|
|
91
|
+
MAX_DISCOVERY_DEPTH = 5;
|
|
92
92
|
MAX_TIMELINE_EVENTS = 20;
|
|
93
93
|
MAX_RESOLVED_DISPLAY = 5;
|
|
94
94
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
// src/constants/encoding.ts
|
|
99
|
+
var init_encoding = __esm({
|
|
100
|
+
"src/constants/encoding.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// src/constants/severity.ts
|
|
106
|
+
var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
|
|
107
|
+
var init_severity = __esm({
|
|
108
|
+
"src/constants/severity.ts"() {
|
|
109
|
+
"use strict";
|
|
110
|
+
SEVERITY_CRITICAL = "critical";
|
|
111
|
+
SEVERITY_WARNING = "warning";
|
|
112
|
+
SEVERITY_INFO = "info";
|
|
113
|
+
SEVERITY_ICON_MAP = {
|
|
114
|
+
[SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
|
|
115
|
+
[SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
|
|
116
|
+
[SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
98
121
|
// src/constants/index.ts
|
|
99
122
|
var init_constants = __esm({
|
|
100
123
|
"src/constants/index.ts"() {
|
|
@@ -107,6 +130,8 @@ var init_constants = __esm({
|
|
|
107
130
|
init_headers();
|
|
108
131
|
init_network();
|
|
109
132
|
init_mcp();
|
|
133
|
+
init_encoding();
|
|
134
|
+
init_severity();
|
|
110
135
|
}
|
|
111
136
|
});
|
|
112
137
|
|
|
@@ -219,22 +244,55 @@ var init_client = __esm({
|
|
|
219
244
|
});
|
|
220
245
|
|
|
221
246
|
// src/mcp/discovery.ts
|
|
222
|
-
import { readFileSync as
|
|
223
|
-
import { resolve as
|
|
247
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync, statSync } from "fs";
|
|
248
|
+
import { resolve as resolve5, dirname } from "path";
|
|
249
|
+
function readPort(portPath) {
|
|
250
|
+
if (!existsSync4(portPath)) return null;
|
|
251
|
+
const raw = readFileSync3(portPath, "utf-8").trim();
|
|
252
|
+
const port = parseInt(raw, 10);
|
|
253
|
+
return isNaN(port) || port < 1 || port > 65535 ? null : port;
|
|
254
|
+
}
|
|
255
|
+
function portInDir(dir) {
|
|
256
|
+
return readPort(resolve5(dir, PORT_FILE));
|
|
257
|
+
}
|
|
258
|
+
function portInChildren(dir) {
|
|
259
|
+
try {
|
|
260
|
+
for (const entry of readdirSync(dir)) {
|
|
261
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
262
|
+
const child = resolve5(dir, entry);
|
|
263
|
+
try {
|
|
264
|
+
if (!statSync(child).isDirectory()) continue;
|
|
265
|
+
} catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const port = portInDir(child);
|
|
269
|
+
if (port) return port;
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
function searchForPort(startDir) {
|
|
276
|
+
const start = resolve5(startDir);
|
|
277
|
+
const initial = portInDir(start) ?? portInChildren(start);
|
|
278
|
+
if (initial) return initial;
|
|
279
|
+
let dir = dirname(start);
|
|
280
|
+
for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
|
|
281
|
+
const port = portInDir(dir);
|
|
282
|
+
if (port) return port;
|
|
283
|
+
const parent = dirname(dir);
|
|
284
|
+
if (parent === dir) break;
|
|
285
|
+
dir = parent;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
224
289
|
function discoverBrakitPort(cwd) {
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
if (!existsSync4(portPath)) {
|
|
290
|
+
const port = searchForPort(cwd ?? process.cwd());
|
|
291
|
+
if (!port) {
|
|
228
292
|
throw new Error(
|
|
229
|
-
|
|
230
|
-
Start your app with brakit enabled first.`
|
|
293
|
+
"Brakit is not running. Start your app with brakit enabled first."
|
|
231
294
|
);
|
|
232
295
|
}
|
|
233
|
-
const raw = readFileSync4(portPath, "utf-8").trim();
|
|
234
|
-
const port = parseInt(raw, 10);
|
|
235
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
236
|
-
throw new Error(`Invalid port in ${portPath}: "${raw}"`);
|
|
237
|
-
}
|
|
238
296
|
return { port, baseUrl: `http://localhost:${port}` };
|
|
239
297
|
}
|
|
240
298
|
async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
|
|
@@ -300,7 +358,9 @@ async function enrichFindings(client) {
|
|
|
300
358
|
context
|
|
301
359
|
});
|
|
302
360
|
}
|
|
303
|
-
for (const
|
|
361
|
+
for (const si of insightsData.insights) {
|
|
362
|
+
if (si.state === "resolved") continue;
|
|
363
|
+
const i = si.insight;
|
|
304
364
|
if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
|
|
305
365
|
const endpoint = i.nav ?? "global";
|
|
306
366
|
enriched.push({
|
|
@@ -690,13 +750,14 @@ var init_get_report = __esm({
|
|
|
690
750
|
(s, ep) => s + ep.summary.totalRequests,
|
|
691
751
|
0
|
|
692
752
|
);
|
|
753
|
+
const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
|
|
693
754
|
const lines = [
|
|
694
755
|
"=== Brakit Report ===",
|
|
695
756
|
"",
|
|
696
757
|
`Endpoints observed: ${metricsData.endpoints.length}`,
|
|
697
758
|
`Total requests captured: ${totalRequests}`,
|
|
698
759
|
`Active security rules: ${securityData.findings.length} finding(s)`,
|
|
699
|
-
`Performance insights: ${insightsData.insights.length}
|
|
760
|
+
`Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
|
|
700
761
|
"",
|
|
701
762
|
"--- Finding Summary ---",
|
|
702
763
|
`Total: ${findings.length}`,
|
|
@@ -923,22 +984,24 @@ import { runMain } from "citty";
|
|
|
923
984
|
|
|
924
985
|
// src/cli/commands/install.ts
|
|
925
986
|
import { defineCommand } from "citty";
|
|
926
|
-
import { resolve as
|
|
927
|
-
import { readFile as readFile3, writeFile as
|
|
987
|
+
import { resolve as resolve3, join as join2 } from "path";
|
|
988
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
928
989
|
import { execSync } from "child_process";
|
|
929
990
|
import pc from "picocolors";
|
|
930
991
|
|
|
931
992
|
// src/store/finding-store.ts
|
|
932
993
|
init_constants();
|
|
994
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
995
|
+
import { resolve as resolve2 } from "path";
|
|
996
|
+
|
|
997
|
+
// src/utils/atomic-writer.ts
|
|
933
998
|
import {
|
|
934
|
-
readFileSync as readFileSync2,
|
|
935
999
|
writeFileSync as writeFileSync2,
|
|
936
1000
|
existsSync as existsSync2,
|
|
937
1001
|
mkdirSync as mkdirSync2,
|
|
938
1002
|
renameSync
|
|
939
1003
|
} from "fs";
|
|
940
1004
|
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
941
|
-
import { resolve as resolve2 } from "path";
|
|
942
1005
|
|
|
943
1006
|
// src/utils/fs.ts
|
|
944
1007
|
import { access } from "fs/promises";
|
|
@@ -1472,166 +1535,9 @@ var responsePiiLeakRule = {
|
|
|
1472
1535
|
}
|
|
1473
1536
|
};
|
|
1474
1537
|
|
|
1475
|
-
// src/store/request-store.ts
|
|
1476
|
-
init_constants();
|
|
1477
|
-
|
|
1478
|
-
// src/utils/static-patterns.ts
|
|
1479
|
-
var STATIC_PATTERNS = [
|
|
1480
|
-
/^\/_next\//,
|
|
1481
|
-
/\.(?:js|css|map|ico|png|jpg|jpeg|gif|svg|webp|woff2?|ttf|eot)$/,
|
|
1482
|
-
/^\/favicon/,
|
|
1483
|
-
/^\/__nextjs/
|
|
1484
|
-
];
|
|
1485
|
-
function isStaticPath(urlPath) {
|
|
1486
|
-
return STATIC_PATTERNS.some((p) => p.test(urlPath));
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
// src/store/request-store.ts
|
|
1490
|
-
function flattenHeaders(headers) {
|
|
1491
|
-
const flat = {};
|
|
1492
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1493
|
-
if (value === void 0) continue;
|
|
1494
|
-
flat[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
1495
|
-
}
|
|
1496
|
-
return flat;
|
|
1497
|
-
}
|
|
1498
|
-
var RequestStore = class {
|
|
1499
|
-
constructor(maxEntries = MAX_REQUEST_ENTRIES) {
|
|
1500
|
-
this.maxEntries = maxEntries;
|
|
1501
|
-
}
|
|
1502
|
-
requests = [];
|
|
1503
|
-
listeners = [];
|
|
1504
|
-
capture(input) {
|
|
1505
|
-
const url = input.url;
|
|
1506
|
-
const path = url.split("?")[0];
|
|
1507
|
-
let requestBodyStr = null;
|
|
1508
|
-
if (input.requestBody && input.requestBody.length > 0) {
|
|
1509
|
-
requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
1510
|
-
}
|
|
1511
|
-
let responseBodyStr = null;
|
|
1512
|
-
if (input.responseBody && input.responseBody.length > 0) {
|
|
1513
|
-
const ct = input.responseContentType;
|
|
1514
|
-
if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
|
|
1515
|
-
responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
const entry = {
|
|
1519
|
-
id: input.requestId,
|
|
1520
|
-
method: input.method,
|
|
1521
|
-
url,
|
|
1522
|
-
path,
|
|
1523
|
-
headers: flattenHeaders(input.requestHeaders),
|
|
1524
|
-
requestBody: requestBodyStr,
|
|
1525
|
-
statusCode: input.statusCode,
|
|
1526
|
-
responseHeaders: flattenHeaders(input.responseHeaders),
|
|
1527
|
-
responseBody: responseBodyStr,
|
|
1528
|
-
startedAt: input.startTime,
|
|
1529
|
-
durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
|
|
1530
|
-
responseSize: input.responseBody?.length ?? 0,
|
|
1531
|
-
isStatic: isStaticPath(path)
|
|
1532
|
-
};
|
|
1533
|
-
this.requests.push(entry);
|
|
1534
|
-
if (this.requests.length > this.maxEntries) {
|
|
1535
|
-
this.requests.shift();
|
|
1536
|
-
}
|
|
1537
|
-
for (const fn of this.listeners) {
|
|
1538
|
-
fn(entry);
|
|
1539
|
-
}
|
|
1540
|
-
return entry;
|
|
1541
|
-
}
|
|
1542
|
-
getAll() {
|
|
1543
|
-
return this.requests;
|
|
1544
|
-
}
|
|
1545
|
-
clear() {
|
|
1546
|
-
this.requests.length = 0;
|
|
1547
|
-
}
|
|
1548
|
-
onRequest(fn) {
|
|
1549
|
-
this.listeners.push(fn);
|
|
1550
|
-
}
|
|
1551
|
-
offRequest(fn) {
|
|
1552
|
-
const idx = this.listeners.indexOf(fn);
|
|
1553
|
-
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
1554
|
-
}
|
|
1555
|
-
};
|
|
1556
|
-
|
|
1557
|
-
// src/store/request-log.ts
|
|
1558
|
-
var defaultStore = new RequestStore();
|
|
1559
|
-
|
|
1560
|
-
// src/store/telemetry-store.ts
|
|
1561
|
-
init_constants();
|
|
1562
|
-
import { randomUUID } from "crypto";
|
|
1563
|
-
var TelemetryStore = class {
|
|
1564
|
-
constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
|
|
1565
|
-
this.maxEntries = maxEntries;
|
|
1566
|
-
}
|
|
1567
|
-
entries = [];
|
|
1568
|
-
listeners = [];
|
|
1569
|
-
add(data) {
|
|
1570
|
-
const entry = { id: randomUUID(), ...data };
|
|
1571
|
-
this.entries.push(entry);
|
|
1572
|
-
if (this.entries.length > this.maxEntries) this.entries.shift();
|
|
1573
|
-
for (const fn of this.listeners) fn(entry);
|
|
1574
|
-
return entry;
|
|
1575
|
-
}
|
|
1576
|
-
getAll() {
|
|
1577
|
-
return this.entries;
|
|
1578
|
-
}
|
|
1579
|
-
getByRequest(requestId) {
|
|
1580
|
-
return this.entries.filter((e) => e.parentRequestId === requestId);
|
|
1581
|
-
}
|
|
1582
|
-
clear() {
|
|
1583
|
-
this.entries.length = 0;
|
|
1584
|
-
}
|
|
1585
|
-
onEntry(fn) {
|
|
1586
|
-
this.listeners.push(fn);
|
|
1587
|
-
}
|
|
1588
|
-
offEntry(fn) {
|
|
1589
|
-
const idx = this.listeners.indexOf(fn);
|
|
1590
|
-
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
1591
|
-
}
|
|
1592
|
-
};
|
|
1593
|
-
|
|
1594
|
-
// src/store/fetch-store.ts
|
|
1595
|
-
var FetchStore = class extends TelemetryStore {
|
|
1596
|
-
};
|
|
1597
|
-
var defaultFetchStore = new FetchStore();
|
|
1598
|
-
|
|
1599
|
-
// src/store/log-store.ts
|
|
1600
|
-
var LogStore = class extends TelemetryStore {
|
|
1601
|
-
};
|
|
1602
|
-
var defaultLogStore = new LogStore();
|
|
1603
|
-
|
|
1604
|
-
// src/store/error-store.ts
|
|
1605
|
-
var ErrorStore = class extends TelemetryStore {
|
|
1606
|
-
};
|
|
1607
|
-
var defaultErrorStore = new ErrorStore();
|
|
1608
|
-
|
|
1609
|
-
// src/store/query-store.ts
|
|
1610
|
-
var QueryStore = class extends TelemetryStore {
|
|
1611
|
-
};
|
|
1612
|
-
var defaultQueryStore = new QueryStore();
|
|
1613
|
-
|
|
1614
|
-
// src/store/metrics/metrics-store.ts
|
|
1615
|
-
init_constants();
|
|
1616
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1617
|
-
init_endpoint();
|
|
1618
|
-
|
|
1619
|
-
// src/store/metrics/persistence.ts
|
|
1620
|
-
init_constants();
|
|
1621
|
-
import {
|
|
1622
|
-
readFileSync as readFileSync3,
|
|
1623
|
-
writeFileSync as writeFileSync3,
|
|
1624
|
-
mkdirSync as mkdirSync3,
|
|
1625
|
-
existsSync as existsSync3,
|
|
1626
|
-
unlinkSync,
|
|
1627
|
-
renameSync as renameSync2
|
|
1628
|
-
} from "fs";
|
|
1629
|
-
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
1630
|
-
import { resolve as resolve3 } from "path";
|
|
1631
|
-
|
|
1632
1538
|
// src/analysis/group.ts
|
|
1633
1539
|
init_constants();
|
|
1634
|
-
import { randomUUID
|
|
1540
|
+
import { randomUUID } from "crypto";
|
|
1635
1541
|
|
|
1636
1542
|
// src/analysis/label.ts
|
|
1637
1543
|
init_constants();
|
|
@@ -1642,49 +1548,54 @@ init_constants();
|
|
|
1642
1548
|
// src/analysis/insights/prepare.ts
|
|
1643
1549
|
init_endpoint();
|
|
1644
1550
|
init_constants();
|
|
1551
|
+
init_thresholds();
|
|
1645
1552
|
|
|
1646
1553
|
// src/analysis/insights/rules/n1.ts
|
|
1647
1554
|
init_endpoint();
|
|
1648
|
-
|
|
1555
|
+
init_constants();
|
|
1649
1556
|
|
|
1650
1557
|
// src/analysis/insights/rules/cross-endpoint.ts
|
|
1651
1558
|
init_endpoint();
|
|
1652
|
-
|
|
1559
|
+
init_constants();
|
|
1653
1560
|
|
|
1654
1561
|
// src/analysis/insights/rules/redundant-query.ts
|
|
1655
1562
|
init_endpoint();
|
|
1656
|
-
|
|
1563
|
+
init_constants();
|
|
1657
1564
|
|
|
1658
1565
|
// src/analysis/insights/rules/error-hotspot.ts
|
|
1659
|
-
|
|
1566
|
+
init_constants();
|
|
1660
1567
|
|
|
1661
1568
|
// src/analysis/insights/rules/duplicate.ts
|
|
1662
|
-
|
|
1569
|
+
init_constants();
|
|
1663
1570
|
|
|
1664
1571
|
// src/analysis/insights/rules/slow.ts
|
|
1665
|
-
|
|
1572
|
+
init_constants();
|
|
1666
1573
|
|
|
1667
1574
|
// src/analysis/insights/rules/query-heavy.ts
|
|
1668
|
-
|
|
1575
|
+
init_constants();
|
|
1669
1576
|
|
|
1670
1577
|
// src/analysis/insights/rules/select-star.ts
|
|
1671
|
-
|
|
1578
|
+
init_constants();
|
|
1672
1579
|
|
|
1673
1580
|
// src/analysis/insights/rules/high-rows.ts
|
|
1674
|
-
|
|
1581
|
+
init_constants();
|
|
1675
1582
|
|
|
1676
1583
|
// src/analysis/insights/rules/response-overfetch.ts
|
|
1677
1584
|
init_endpoint();
|
|
1678
|
-
|
|
1585
|
+
init_constants();
|
|
1679
1586
|
|
|
1680
1587
|
// src/analysis/insights/rules/large-response.ts
|
|
1681
|
-
|
|
1588
|
+
init_constants();
|
|
1682
1589
|
|
|
1683
1590
|
// src/analysis/insights/rules/regression.ts
|
|
1591
|
+
init_constants();
|
|
1592
|
+
|
|
1593
|
+
// src/analysis/insight-tracker.ts
|
|
1594
|
+
init_endpoint();
|
|
1684
1595
|
init_thresholds();
|
|
1685
1596
|
|
|
1686
1597
|
// src/index.ts
|
|
1687
|
-
var VERSION = "0.8.
|
|
1598
|
+
var VERSION = "0.8.2";
|
|
1688
1599
|
|
|
1689
1600
|
// src/cli/commands/install.ts
|
|
1690
1601
|
var IMPORT_LINE = `import "brakit";`;
|
|
@@ -1704,10 +1615,21 @@ var install_default = defineCommand({
|
|
|
1704
1615
|
}
|
|
1705
1616
|
},
|
|
1706
1617
|
async run({ args }) {
|
|
1707
|
-
const rootDir =
|
|
1618
|
+
const rootDir = resolve3(args.dir);
|
|
1708
1619
|
const pkgPath = join2(rootDir, "package.json");
|
|
1709
1620
|
if (!await fileExists(pkgPath)) {
|
|
1710
|
-
console.error(pc.red(" No
|
|
1621
|
+
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1622
|
+
process.exit(1);
|
|
1623
|
+
}
|
|
1624
|
+
let pkg;
|
|
1625
|
+
try {
|
|
1626
|
+
pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
1627
|
+
} catch {
|
|
1628
|
+
console.error(pc.red(" Failed to read package.json."));
|
|
1629
|
+
process.exit(1);
|
|
1630
|
+
}
|
|
1631
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
1632
|
+
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1711
1633
|
process.exit(1);
|
|
1712
1634
|
}
|
|
1713
1635
|
let project;
|
|
@@ -1804,7 +1726,7 @@ async function setupNextjs(rootDir) {
|
|
|
1804
1726
|
`}`,
|
|
1805
1727
|
``
|
|
1806
1728
|
].join("\n");
|
|
1807
|
-
await
|
|
1729
|
+
await writeFile3(absPath, content);
|
|
1808
1730
|
return { action: "created", file: relPath, content };
|
|
1809
1731
|
}
|
|
1810
1732
|
async function setupNuxt(rootDir) {
|
|
@@ -1820,9 +1742,9 @@ async function setupNuxt(rootDir) {
|
|
|
1820
1742
|
const content = `${IMPORT_LINE}
|
|
1821
1743
|
`;
|
|
1822
1744
|
const dir = join2(rootDir, "server/plugins");
|
|
1823
|
-
const { mkdirSync:
|
|
1824
|
-
|
|
1825
|
-
await
|
|
1745
|
+
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
1746
|
+
mkdirSync3(dir, { recursive: true });
|
|
1747
|
+
await writeFile3(absPath, content);
|
|
1826
1748
|
return { action: "created", file: relPath, content };
|
|
1827
1749
|
}
|
|
1828
1750
|
async function setupPrepend(rootDir, ...candidates) {
|
|
@@ -1833,7 +1755,7 @@ async function setupPrepend(rootDir, ...candidates) {
|
|
|
1833
1755
|
if (content.includes(IMPORT_MARKER)) {
|
|
1834
1756
|
return { action: "exists", file: relPath };
|
|
1835
1757
|
}
|
|
1836
|
-
await
|
|
1758
|
+
await writeFile3(absPath, `${IMPORT_LINE}
|
|
1837
1759
|
${content}`);
|
|
1838
1760
|
return { action: "prepended", file: relPath };
|
|
1839
1761
|
}
|
|
@@ -1883,13 +1805,13 @@ async function setupMcp(rootDir) {
|
|
|
1883
1805
|
const config = JSON.parse(raw);
|
|
1884
1806
|
if (config?.mcpServers?.brakit) return "exists";
|
|
1885
1807
|
config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
|
|
1886
|
-
await
|
|
1808
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1887
1809
|
await ensureGitignoreMcp(rootDir);
|
|
1888
1810
|
return "updated";
|
|
1889
1811
|
} catch {
|
|
1890
1812
|
}
|
|
1891
1813
|
}
|
|
1892
|
-
await
|
|
1814
|
+
await writeFile3(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
|
|
1893
1815
|
await ensureGitignoreMcp(rootDir);
|
|
1894
1816
|
return "created";
|
|
1895
1817
|
}
|
|
@@ -1899,9 +1821,9 @@ async function ensureGitignoreMcp(rootDir) {
|
|
|
1899
1821
|
if (await fileExists(gitignorePath)) {
|
|
1900
1822
|
const content = await readFile3(gitignorePath, "utf-8");
|
|
1901
1823
|
if (content.split("\n").some((l) => l.trim() === ".mcp.json")) return;
|
|
1902
|
-
await
|
|
1824
|
+
await writeFile3(gitignorePath, content.trimEnd() + "\n.mcp.json\n");
|
|
1903
1825
|
} else {
|
|
1904
|
-
await
|
|
1826
|
+
await writeFile3(gitignorePath, ".mcp.json\n");
|
|
1905
1827
|
}
|
|
1906
1828
|
} catch {
|
|
1907
1829
|
}
|
|
@@ -1925,8 +1847,8 @@ function printManualInstructions(framework) {
|
|
|
1925
1847
|
|
|
1926
1848
|
// src/cli/commands/uninstall.ts
|
|
1927
1849
|
import { defineCommand as defineCommand2 } from "citty";
|
|
1928
|
-
import { resolve as
|
|
1929
|
-
import { readFile as readFile4, writeFile as
|
|
1850
|
+
import { resolve as resolve4, join as join3 } from "path";
|
|
1851
|
+
import { readFile as readFile4, writeFile as writeFile4, unlink, rm } from "fs/promises";
|
|
1930
1852
|
import { execSync as execSync2 } from "child_process";
|
|
1931
1853
|
import pc2 from "picocolors";
|
|
1932
1854
|
init_constants();
|
|
@@ -1967,7 +1889,7 @@ var uninstall_default = defineCommand2({
|
|
|
1967
1889
|
}
|
|
1968
1890
|
},
|
|
1969
1891
|
async run({ args }) {
|
|
1970
|
-
const rootDir =
|
|
1892
|
+
const rootDir = resolve4(args.dir);
|
|
1971
1893
|
let project = null;
|
|
1972
1894
|
try {
|
|
1973
1895
|
project = await detectProject(rootDir);
|
|
@@ -2005,7 +1927,7 @@ var uninstall_default = defineCommand2({
|
|
|
2005
1927
|
const content = await readFile4(absPath, "utf-8");
|
|
2006
1928
|
if (!content.includes(IMPORT_LINE2)) continue;
|
|
2007
1929
|
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE2.trim()).join("\n");
|
|
2008
|
-
await
|
|
1930
|
+
await writeFile4(absPath, updated);
|
|
2009
1931
|
console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
|
|
2010
1932
|
removed = true;
|
|
2011
1933
|
break;
|
|
@@ -2045,7 +1967,7 @@ async function removeMcpConfig(rootDir) {
|
|
|
2045
1967
|
if (Object.keys(config.mcpServers).length === 0) {
|
|
2046
1968
|
await unlink(mcpPath);
|
|
2047
1969
|
} else {
|
|
2048
|
-
await
|
|
1970
|
+
await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
2049
1971
|
}
|
|
2050
1972
|
return true;
|
|
2051
1973
|
} catch {
|
|
@@ -2092,7 +2014,7 @@ async function cleanGitignore(rootDir) {
|
|
|
2092
2014
|
const lines = content.split("\n");
|
|
2093
2015
|
const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
|
|
2094
2016
|
if (filtered.length === lines.length) return false;
|
|
2095
|
-
await
|
|
2017
|
+
await writeFile4(gitignorePath, filtered.join("\n"));
|
|
2096
2018
|
return true;
|
|
2097
2019
|
} catch {
|
|
2098
2020
|
return false;
|
package/dist/mcp/server.js
CHANGED
|
@@ -22,12 +22,13 @@ var DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
|
|
|
22
22
|
|
|
23
23
|
// src/constants/mcp.ts
|
|
24
24
|
var MCP_SERVER_NAME = "brakit";
|
|
25
|
-
var MCP_SERVER_VERSION = "0.8.
|
|
25
|
+
var MCP_SERVER_VERSION = "0.8.2";
|
|
26
26
|
var INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
27
27
|
var LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
28
28
|
var CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
29
29
|
var HEALTH_CHECK_TIMEOUT_MS = 3e3;
|
|
30
30
|
var DISCOVERY_POLL_INTERVAL_MS = 500;
|
|
31
|
+
var MAX_DISCOVERY_DEPTH = 5;
|
|
31
32
|
var MAX_TIMELINE_EVENTS = 20;
|
|
32
33
|
var MAX_RESOLVED_DISPLAY = 5;
|
|
33
34
|
var ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
@@ -107,27 +108,73 @@ var BrakitClient = class {
|
|
|
107
108
|
};
|
|
108
109
|
|
|
109
110
|
// src/mcp/discovery.ts
|
|
110
|
-
import { readFileSync, existsSync } from "fs";
|
|
111
|
-
import { resolve } from "path";
|
|
111
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
112
|
+
import { resolve, dirname } from "path";
|
|
113
|
+
|
|
114
|
+
// src/constants/limits.ts
|
|
115
|
+
var MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
112
116
|
|
|
113
117
|
// src/constants/metrics.ts
|
|
114
118
|
var PORT_FILE = ".brakit/port";
|
|
115
119
|
|
|
120
|
+
// src/constants/severity.ts
|
|
121
|
+
var SEVERITY_CRITICAL = "critical";
|
|
122
|
+
var SEVERITY_WARNING = "warning";
|
|
123
|
+
var SEVERITY_INFO = "info";
|
|
124
|
+
var SEVERITY_ICON_MAP = {
|
|
125
|
+
[SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
|
|
126
|
+
[SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
|
|
127
|
+
[SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
|
|
128
|
+
};
|
|
129
|
+
|
|
116
130
|
// src/mcp/discovery.ts
|
|
131
|
+
function readPort(portPath) {
|
|
132
|
+
if (!existsSync(portPath)) return null;
|
|
133
|
+
const raw = readFileSync(portPath, "utf-8").trim();
|
|
134
|
+
const port = parseInt(raw, 10);
|
|
135
|
+
return isNaN(port) || port < 1 || port > 65535 ? null : port;
|
|
136
|
+
}
|
|
137
|
+
function portInDir(dir) {
|
|
138
|
+
return readPort(resolve(dir, PORT_FILE));
|
|
139
|
+
}
|
|
140
|
+
function portInChildren(dir) {
|
|
141
|
+
try {
|
|
142
|
+
for (const entry of readdirSync(dir)) {
|
|
143
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
144
|
+
const child = resolve(dir, entry);
|
|
145
|
+
try {
|
|
146
|
+
if (!statSync(child).isDirectory()) continue;
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const port = portInDir(child);
|
|
151
|
+
if (port) return port;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function searchForPort(startDir) {
|
|
158
|
+
const start = resolve(startDir);
|
|
159
|
+
const initial = portInDir(start) ?? portInChildren(start);
|
|
160
|
+
if (initial) return initial;
|
|
161
|
+
let dir = dirname(start);
|
|
162
|
+
for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
|
|
163
|
+
const port = portInDir(dir);
|
|
164
|
+
if (port) return port;
|
|
165
|
+
const parent = dirname(dir);
|
|
166
|
+
if (parent === dir) break;
|
|
167
|
+
dir = parent;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
117
171
|
function discoverBrakitPort(cwd) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (!existsSync(portPath)) {
|
|
172
|
+
const port = searchForPort(cwd ?? process.cwd());
|
|
173
|
+
if (!port) {
|
|
121
174
|
throw new Error(
|
|
122
|
-
|
|
123
|
-
Start your app with brakit enabled first.`
|
|
175
|
+
"Brakit is not running. Start your app with brakit enabled first."
|
|
124
176
|
);
|
|
125
177
|
}
|
|
126
|
-
const raw = readFileSync(portPath, "utf-8").trim();
|
|
127
|
-
const port = parseInt(raw, 10);
|
|
128
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
129
|
-
throw new Error(`Invalid port in ${portPath}: "${raw}"`);
|
|
130
|
-
}
|
|
131
178
|
return { port, baseUrl: `http://localhost:${port}` };
|
|
132
179
|
}
|
|
133
180
|
async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
|
|
@@ -204,7 +251,9 @@ async function enrichFindings(client) {
|
|
|
204
251
|
context
|
|
205
252
|
});
|
|
206
253
|
}
|
|
207
|
-
for (const
|
|
254
|
+
for (const si of insightsData.insights) {
|
|
255
|
+
if (si.state === "resolved") continue;
|
|
256
|
+
const i = si.insight;
|
|
208
257
|
if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
|
|
209
258
|
const endpoint = i.nav ?? "global";
|
|
210
259
|
enriched.push({
|
|
@@ -553,13 +602,14 @@ var getReport = {
|
|
|
553
602
|
(s, ep) => s + ep.summary.totalRequests,
|
|
554
603
|
0
|
|
555
604
|
);
|
|
605
|
+
const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
|
|
556
606
|
const lines = [
|
|
557
607
|
"=== Brakit Report ===",
|
|
558
608
|
"",
|
|
559
609
|
`Endpoints observed: ${metricsData.endpoints.length}`,
|
|
560
610
|
`Total requests captured: ${totalRequests}`,
|
|
561
611
|
`Active security rules: ${securityData.findings.length} finding(s)`,
|
|
562
|
-
`Performance insights: ${insightsData.insights.length}
|
|
612
|
+
`Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
|
|
563
613
|
"",
|
|
564
614
|
"--- Finding Summary ---",
|
|
565
615
|
`Total: ${findings.length}`,
|