brakit 0.8.4 → 0.8.5
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/api.d.ts +26 -9
- package/dist/api.js +209 -99
- package/dist/bin/brakit.js +509 -218
- package/dist/dashboard.html +2652 -0
- package/dist/mcp/server.js +195 -90
- package/dist/runtime/index.js +854 -388
- package/package.json +3 -2
package/dist/bin/brakit.js
CHANGED
|
@@ -10,29 +10,49 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/constants/routes.ts
|
|
13
|
-
var DASHBOARD_API_REQUESTS, DASHBOARD_API_CLEAR, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_FINDINGS;
|
|
13
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS;
|
|
14
14
|
var init_routes = __esm({
|
|
15
15
|
"src/constants/routes.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
DASHBOARD_PREFIX = "/__brakit";
|
|
18
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
19
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
20
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
21
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
22
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
23
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
24
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
25
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
26
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
27
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
28
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
29
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
30
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
31
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
32
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
33
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
34
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
35
|
+
VALID_TABS_TUPLE = [
|
|
36
|
+
"overview",
|
|
37
|
+
"actions",
|
|
38
|
+
"requests",
|
|
39
|
+
"fetches",
|
|
40
|
+
"queries",
|
|
41
|
+
"errors",
|
|
42
|
+
"logs",
|
|
43
|
+
"performance",
|
|
44
|
+
"security"
|
|
45
|
+
];
|
|
46
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
27
47
|
}
|
|
28
48
|
});
|
|
29
49
|
|
|
30
50
|
// src/constants/limits.ts
|
|
31
|
-
var
|
|
51
|
+
var FINDING_ID_HASH_LENGTH;
|
|
32
52
|
var init_limits = __esm({
|
|
33
53
|
"src/constants/limits.ts"() {
|
|
34
54
|
"use strict";
|
|
35
|
-
|
|
55
|
+
FINDING_ID_HASH_LENGTH = 16;
|
|
36
56
|
}
|
|
37
57
|
});
|
|
38
58
|
|
|
@@ -70,19 +90,22 @@ var init_headers = __esm({
|
|
|
70
90
|
});
|
|
71
91
|
|
|
72
92
|
// src/constants/network.ts
|
|
93
|
+
var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
|
|
73
94
|
var init_network = __esm({
|
|
74
95
|
"src/constants/network.ts"() {
|
|
75
96
|
"use strict";
|
|
97
|
+
RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
98
|
+
PORT_MIN = 1;
|
|
99
|
+
PORT_MAX = 65535;
|
|
76
100
|
}
|
|
77
101
|
});
|
|
78
102
|
|
|
79
103
|
// src/constants/mcp.ts
|
|
80
|
-
var MCP_SERVER_NAME,
|
|
104
|
+
var MCP_SERVER_NAME, 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, MCP_SERVER_VERSION;
|
|
81
105
|
var init_mcp = __esm({
|
|
82
106
|
"src/constants/mcp.ts"() {
|
|
83
107
|
"use strict";
|
|
84
108
|
MCP_SERVER_NAME = "brakit";
|
|
85
|
-
MCP_SERVER_VERSION = "0.8.4";
|
|
86
109
|
INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
87
110
|
LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
88
111
|
CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -92,6 +115,7 @@ var init_mcp = __esm({
|
|
|
92
115
|
MAX_TIMELINE_EVENTS = 20;
|
|
93
116
|
MAX_RESOLVED_DISPLAY = 5;
|
|
94
117
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
118
|
+
MCP_SERVER_VERSION = "0.8.5";
|
|
95
119
|
}
|
|
96
120
|
});
|
|
97
121
|
|
|
@@ -103,18 +127,9 @@ var init_encoding = __esm({
|
|
|
103
127
|
});
|
|
104
128
|
|
|
105
129
|
// src/constants/severity.ts
|
|
106
|
-
var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
|
|
107
130
|
var init_severity = __esm({
|
|
108
131
|
"src/constants/severity.ts"() {
|
|
109
132
|
"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
133
|
}
|
|
119
134
|
});
|
|
120
135
|
|
|
@@ -125,6 +140,17 @@ var init_telemetry = __esm({
|
|
|
125
140
|
}
|
|
126
141
|
});
|
|
127
142
|
|
|
143
|
+
// src/constants/lifecycle.ts
|
|
144
|
+
var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
|
|
145
|
+
var init_lifecycle = __esm({
|
|
146
|
+
"src/constants/lifecycle.ts"() {
|
|
147
|
+
"use strict";
|
|
148
|
+
VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
149
|
+
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
150
|
+
VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
128
154
|
// src/constants/index.ts
|
|
129
155
|
var init_constants = __esm({
|
|
130
156
|
"src/constants/index.ts"() {
|
|
@@ -140,18 +166,52 @@ var init_constants = __esm({
|
|
|
140
166
|
init_encoding();
|
|
141
167
|
init_severity();
|
|
142
168
|
init_telemetry();
|
|
169
|
+
init_lifecycle();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/utils/log.ts
|
|
174
|
+
function brakitDebug(message) {
|
|
175
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
176
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var PREFIX;
|
|
181
|
+
var init_log = __esm({
|
|
182
|
+
"src/utils/log.ts"() {
|
|
183
|
+
"use strict";
|
|
184
|
+
PREFIX = "[brakit]";
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// src/utils/type-guards.ts
|
|
189
|
+
function isNonEmptyString(val) {
|
|
190
|
+
return typeof val === "string" && val.trim().length > 0;
|
|
191
|
+
}
|
|
192
|
+
function isValidFindingState(val) {
|
|
193
|
+
return typeof val === "string" && VALID_FINDING_STATES.has(val);
|
|
194
|
+
}
|
|
195
|
+
function isValidAiFixStatus(val) {
|
|
196
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
197
|
+
}
|
|
198
|
+
var init_type_guards = __esm({
|
|
199
|
+
"src/utils/type-guards.ts"() {
|
|
200
|
+
"use strict";
|
|
201
|
+
init_lifecycle();
|
|
143
202
|
}
|
|
144
203
|
});
|
|
145
204
|
|
|
146
205
|
// src/store/finding-id.ts
|
|
147
206
|
import { createHash } from "crypto";
|
|
148
|
-
function
|
|
149
|
-
const key = `${
|
|
150
|
-
return createHash("sha256").update(key).digest("hex").slice(0,
|
|
207
|
+
function computeInsightId(type, endpoint, desc) {
|
|
208
|
+
const key = `${type}:${endpoint}:${desc}`;
|
|
209
|
+
return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
|
|
151
210
|
}
|
|
152
211
|
var init_finding_id = __esm({
|
|
153
212
|
"src/store/finding-id.ts"() {
|
|
154
213
|
"use strict";
|
|
214
|
+
init_limits();
|
|
155
215
|
}
|
|
156
216
|
});
|
|
157
217
|
|
|
@@ -221,6 +281,19 @@ var init_client = __esm({
|
|
|
221
281
|
if (state) url.searchParams.set("state", state);
|
|
222
282
|
return this.fetchJson(url);
|
|
223
283
|
}
|
|
284
|
+
async reportFix(findingId, status, notes) {
|
|
285
|
+
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ findingId, status, notes }),
|
|
289
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
290
|
+
});
|
|
291
|
+
if (!res.ok) return false;
|
|
292
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
293
|
+
if (!contentType.includes("application/json")) return false;
|
|
294
|
+
const body = await res.json();
|
|
295
|
+
return body.ok === true;
|
|
296
|
+
}
|
|
224
297
|
async clearAll() {
|
|
225
298
|
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
|
|
226
299
|
method: "POST",
|
|
@@ -252,50 +325,56 @@ var init_client = __esm({
|
|
|
252
325
|
});
|
|
253
326
|
|
|
254
327
|
// src/mcp/discovery.ts
|
|
255
|
-
import {
|
|
256
|
-
import { resolve as resolve5, dirname } from "path";
|
|
257
|
-
function readPort(portPath) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
328
|
+
import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
|
|
329
|
+
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
330
|
+
async function readPort(portPath) {
|
|
331
|
+
try {
|
|
332
|
+
const raw = (await readFile6(portPath, "utf-8")).trim();
|
|
333
|
+
const port = parseInt(raw, 10);
|
|
334
|
+
return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
262
338
|
}
|
|
263
|
-
function portInDir(dir) {
|
|
339
|
+
async function portInDir(dir) {
|
|
264
340
|
return readPort(resolve5(dir, PORT_FILE));
|
|
265
341
|
}
|
|
266
|
-
function portInChildren(dir) {
|
|
342
|
+
async function portInChildren(dir) {
|
|
267
343
|
try {
|
|
268
|
-
|
|
344
|
+
const entries = await readdir2(dir);
|
|
345
|
+
for (const entry of entries) {
|
|
269
346
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
270
347
|
const child = resolve5(dir, entry);
|
|
271
348
|
try {
|
|
272
|
-
if (!
|
|
273
|
-
} catch {
|
|
349
|
+
if (!(await stat(child)).isDirectory()) continue;
|
|
350
|
+
} catch (err) {
|
|
351
|
+
brakitDebug(`discovery: stat failed for ${child}: ${err}`);
|
|
274
352
|
continue;
|
|
275
353
|
}
|
|
276
|
-
const port = portInDir(child);
|
|
354
|
+
const port = await portInDir(child);
|
|
277
355
|
if (port) return port;
|
|
278
356
|
}
|
|
279
|
-
} catch {
|
|
357
|
+
} catch (err) {
|
|
358
|
+
brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
|
|
280
359
|
}
|
|
281
360
|
return null;
|
|
282
361
|
}
|
|
283
|
-
function searchForPort(startDir) {
|
|
362
|
+
async function searchForPort(startDir) {
|
|
284
363
|
const start = resolve5(startDir);
|
|
285
|
-
const initial = portInDir(start) ?? portInChildren(start);
|
|
364
|
+
const initial = await portInDir(start) ?? await portInChildren(start);
|
|
286
365
|
if (initial) return initial;
|
|
287
|
-
let dir =
|
|
366
|
+
let dir = dirname2(start);
|
|
288
367
|
for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
|
|
289
|
-
const port = portInDir(dir);
|
|
368
|
+
const port = await portInDir(dir) ?? await portInChildren(dir);
|
|
290
369
|
if (port) return port;
|
|
291
|
-
const parent =
|
|
370
|
+
const parent = dirname2(dir);
|
|
292
371
|
if (parent === dir) break;
|
|
293
372
|
dir = parent;
|
|
294
373
|
}
|
|
295
374
|
return null;
|
|
296
375
|
}
|
|
297
|
-
function discoverBrakitPort(cwd) {
|
|
298
|
-
const port = searchForPort(cwd ?? process.cwd());
|
|
376
|
+
async function discoverBrakitPort(cwd) {
|
|
377
|
+
const port = await searchForPort(cwd ?? process.cwd());
|
|
299
378
|
if (!port) {
|
|
300
379
|
throw new Error(
|
|
301
380
|
"Brakit is not running. Start your app with brakit enabled first."
|
|
@@ -307,7 +386,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
|
|
|
307
386
|
const deadline = Date.now() + timeoutMs;
|
|
308
387
|
while (Date.now() < deadline) {
|
|
309
388
|
try {
|
|
310
|
-
const result = discoverBrakitPort(cwd);
|
|
389
|
+
const result = await discoverBrakitPort(cwd);
|
|
311
390
|
const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
|
|
312
391
|
if (res.ok) return result;
|
|
313
392
|
} catch {
|
|
@@ -322,50 +401,52 @@ var init_discovery = __esm({
|
|
|
322
401
|
"src/mcp/discovery.ts"() {
|
|
323
402
|
"use strict";
|
|
324
403
|
init_constants();
|
|
404
|
+
init_log();
|
|
325
405
|
init_mcp();
|
|
326
406
|
}
|
|
327
407
|
});
|
|
328
408
|
|
|
329
409
|
// src/mcp/enrichment.ts
|
|
330
|
-
import { createHash as createHash2 } from "crypto";
|
|
331
|
-
function computeInsightId(type, endpoint, desc) {
|
|
332
|
-
const key = `${type}:${endpoint}:${desc}`;
|
|
333
|
-
return createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
334
|
-
}
|
|
335
410
|
async function enrichFindings(client) {
|
|
336
411
|
const [securityData, insightsData] = await Promise.all([
|
|
337
412
|
client.getSecurityFindings(),
|
|
338
413
|
client.getInsights()
|
|
339
414
|
]);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
415
|
+
const contexts = await Promise.all(
|
|
416
|
+
securityData.findings.map(async (sf) => {
|
|
417
|
+
try {
|
|
418
|
+
const { path } = parseEndpointKey(sf.finding.endpoint);
|
|
419
|
+
const reqData = await client.getRequests({ search: path, limit: 1 });
|
|
420
|
+
if (reqData.requests.length > 0) {
|
|
421
|
+
const req = reqData.requests[0];
|
|
422
|
+
if (req.id) {
|
|
423
|
+
const activity = await client.getActivity(req.id);
|
|
424
|
+
const queryCount = activity.counts?.queries ?? 0;
|
|
425
|
+
const fetchCount = activity.counts?.fetches ?? 0;
|
|
426
|
+
return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
|
|
427
|
+
}
|
|
353
428
|
}
|
|
429
|
+
} catch {
|
|
430
|
+
return "(context unavailable)";
|
|
354
431
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
432
|
+
return "";
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
const enriched = securityData.findings.map((sf, i) => {
|
|
436
|
+
const f = sf.finding;
|
|
437
|
+
return {
|
|
438
|
+
findingId: sf.findingId,
|
|
360
439
|
severity: f.severity,
|
|
361
440
|
title: f.title,
|
|
362
441
|
endpoint: f.endpoint,
|
|
363
442
|
description: f.desc,
|
|
364
443
|
hint: f.hint,
|
|
365
444
|
occurrences: f.count,
|
|
366
|
-
context
|
|
367
|
-
|
|
368
|
-
|
|
445
|
+
context: contexts[i],
|
|
446
|
+
aiStatus: sf.aiStatus,
|
|
447
|
+
aiNotes: sf.aiNotes
|
|
448
|
+
};
|
|
449
|
+
});
|
|
369
450
|
for (const si of insightsData.insights) {
|
|
370
451
|
if (si.state === "resolved") continue;
|
|
371
452
|
const i = si.insight;
|
|
@@ -379,7 +460,9 @@ async function enrichFindings(client) {
|
|
|
379
460
|
description: i.desc,
|
|
380
461
|
hint: i.hint,
|
|
381
462
|
occurrences: 1,
|
|
382
|
-
context: i.detail ?? ""
|
|
463
|
+
context: i.detail ?? "",
|
|
464
|
+
aiStatus: si.aiStatus,
|
|
465
|
+
aiNotes: si.aiNotes
|
|
383
466
|
});
|
|
384
467
|
}
|
|
385
468
|
return enriched;
|
|
@@ -441,13 +524,13 @@ var init_enrichment = __esm({
|
|
|
441
524
|
});
|
|
442
525
|
|
|
443
526
|
// src/mcp/tools/get-findings.ts
|
|
444
|
-
var
|
|
527
|
+
var getFindings;
|
|
445
528
|
var init_get_findings = __esm({
|
|
446
529
|
"src/mcp/tools/get-findings.ts"() {
|
|
447
530
|
"use strict";
|
|
448
531
|
init_enrichment();
|
|
449
|
-
|
|
450
|
-
|
|
532
|
+
init_lifecycle();
|
|
533
|
+
init_type_guards();
|
|
451
534
|
getFindings = {
|
|
452
535
|
name: "get_findings",
|
|
453
536
|
description: "Get all security findings and performance insights from the running app. Returns enriched findings with actionable fix hints, endpoint context, and evidence. Use this to understand what issues exist in the running application.",
|
|
@@ -469,10 +552,10 @@ var init_get_findings = __esm({
|
|
|
469
552
|
async handler(client, args) {
|
|
470
553
|
const severity = args.severity;
|
|
471
554
|
const state = args.state;
|
|
472
|
-
if (severity && !
|
|
555
|
+
if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
|
|
473
556
|
return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
|
|
474
557
|
}
|
|
475
|
-
if (state && !
|
|
558
|
+
if (state && !isValidFindingState(state)) {
|
|
476
559
|
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
|
|
477
560
|
}
|
|
478
561
|
let findings = await enrichFindings(client);
|
|
@@ -491,10 +574,18 @@ var init_get_findings = __esm({
|
|
|
491
574
|
`];
|
|
492
575
|
for (const f of findings) {
|
|
493
576
|
lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
|
|
577
|
+
lines.push(` ID: ${f.findingId}`);
|
|
494
578
|
lines.push(` Endpoint: ${f.endpoint}`);
|
|
495
579
|
lines.push(` Issue: ${f.description}`);
|
|
496
580
|
if (f.context) lines.push(` Context: ${f.context}`);
|
|
497
581
|
lines.push(` Fix: ${f.hint}`);
|
|
582
|
+
if (f.aiStatus === "fixed") {
|
|
583
|
+
lines.push(` AI Status: fixed (awaiting verification)`);
|
|
584
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
585
|
+
} else if (f.aiStatus === "wont_fix") {
|
|
586
|
+
lines.push(` AI Status: won't fix`);
|
|
587
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
588
|
+
}
|
|
498
589
|
lines.push("");
|
|
499
590
|
}
|
|
500
591
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -823,6 +914,61 @@ var init_clear_findings = __esm({
|
|
|
823
914
|
}
|
|
824
915
|
});
|
|
825
916
|
|
|
917
|
+
// src/mcp/tools/report-fix.ts
|
|
918
|
+
var reportFix;
|
|
919
|
+
var init_report_fix = __esm({
|
|
920
|
+
"src/mcp/tools/report-fix.ts"() {
|
|
921
|
+
"use strict";
|
|
922
|
+
init_type_guards();
|
|
923
|
+
reportFix = {
|
|
924
|
+
name: "report_fix",
|
|
925
|
+
description: "Report the result of fixing a brakit finding. Call this after attempting to fix each finding to update the dashboard with the outcome. Use status 'fixed' when you've applied a fix, or 'wont_fix' when the issue can't be resolved (e.g. third-party library, by design).",
|
|
926
|
+
inputSchema: {
|
|
927
|
+
type: "object",
|
|
928
|
+
properties: {
|
|
929
|
+
finding_id: {
|
|
930
|
+
type: "string",
|
|
931
|
+
description: "The finding ID to report on"
|
|
932
|
+
},
|
|
933
|
+
status: {
|
|
934
|
+
type: "string",
|
|
935
|
+
description: "Whether the fix was applied or can't be fixed",
|
|
936
|
+
enum: ["fixed", "wont_fix"]
|
|
937
|
+
},
|
|
938
|
+
summary: {
|
|
939
|
+
type: "string",
|
|
940
|
+
description: "Brief description of what was done or why it can't be fixed"
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
required: ["finding_id", "status", "summary"]
|
|
944
|
+
},
|
|
945
|
+
async handler(client, args) {
|
|
946
|
+
const { finding_id, status, summary } = args;
|
|
947
|
+
if (!isNonEmptyString(finding_id)) {
|
|
948
|
+
return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
|
|
949
|
+
}
|
|
950
|
+
if (!isValidAiFixStatus(status)) {
|
|
951
|
+
return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
|
|
952
|
+
}
|
|
953
|
+
if (!isNonEmptyString(summary)) {
|
|
954
|
+
return { content: [{ type: "text", text: "summary is required." }], isError: true };
|
|
955
|
+
}
|
|
956
|
+
const ok = await client.reportFix(finding_id, status, summary);
|
|
957
|
+
if (!ok) {
|
|
958
|
+
return {
|
|
959
|
+
content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
|
|
960
|
+
isError: true
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
|
|
964
|
+
return {
|
|
965
|
+
content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
826
972
|
// src/mcp/tools/index.ts
|
|
827
973
|
function getToolDefinitions() {
|
|
828
974
|
return [...TOOL_MAP.values()].map((t) => ({
|
|
@@ -851,17 +997,19 @@ var init_tools = __esm({
|
|
|
851
997
|
init_verify_fix();
|
|
852
998
|
init_get_report();
|
|
853
999
|
init_clear_findings();
|
|
1000
|
+
init_report_fix();
|
|
854
1001
|
TOOL_MAP = new Map(
|
|
855
|
-
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
|
|
1002
|
+
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
|
|
856
1003
|
);
|
|
857
1004
|
}
|
|
858
1005
|
});
|
|
859
1006
|
|
|
860
1007
|
// src/mcp/prompts.ts
|
|
861
|
-
var PROMPTS, PROMPT_MESSAGES;
|
|
1008
|
+
var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
|
|
862
1009
|
var init_prompts = __esm({
|
|
863
1010
|
"src/mcp/prompts.ts"() {
|
|
864
1011
|
"use strict";
|
|
1012
|
+
SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing any brakit finding, always call the report_fix tool with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did. This updates the dashboard in real-time so the developer can track AI-driven fixes.";
|
|
865
1013
|
PROMPTS = [
|
|
866
1014
|
{
|
|
867
1015
|
name: "check-app",
|
|
@@ -873,18 +1021,8 @@ var init_prompts = __esm({
|
|
|
873
1021
|
}
|
|
874
1022
|
];
|
|
875
1023
|
PROMPT_MESSAGES = {
|
|
876
|
-
"check-app":
|
|
877
|
-
|
|
878
|
-
"First get all findings, then get the endpoint summary.",
|
|
879
|
-
"For any critical or warning findings, get the request detail to understand the root cause.",
|
|
880
|
-
"Give me a clear report of what's wrong and offer to fix each issue."
|
|
881
|
-
].join(" "),
|
|
882
|
-
"fix-findings": [
|
|
883
|
-
"Get all open brakit findings.",
|
|
884
|
-
"For each finding, get the request detail to understand the exact issue.",
|
|
885
|
-
"Then find the source code responsible and fix it.",
|
|
886
|
-
"After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
|
|
887
|
-
].join(" ")
|
|
1024
|
+
"check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing any issue, call report_fix with the finding_id, status, and summary.",
|
|
1025
|
+
"fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Call report_fix with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did\n4. Move to the next finding\n\nAfter all findings are processed, ask me to re-trigger the endpoints so brakit can verify the fixes."
|
|
888
1026
|
};
|
|
889
1027
|
}
|
|
890
1028
|
});
|
|
@@ -912,7 +1050,7 @@ async function startMcpServer() {
|
|
|
912
1050
|
let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
|
|
913
1051
|
const server = new Server(
|
|
914
1052
|
{ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
|
|
915
|
-
{ capabilities: { tools: {}, prompts: {} } }
|
|
1053
|
+
{ capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
916
1054
|
);
|
|
917
1055
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
918
1056
|
prompts: [...PROMPTS]
|
|
@@ -990,27 +1128,19 @@ import { runMain } from "citty";
|
|
|
990
1128
|
|
|
991
1129
|
// src/cli/commands/install.ts
|
|
992
1130
|
import { defineCommand } from "citty";
|
|
993
|
-
import { resolve as resolve3, join as join2 } from "path";
|
|
994
|
-
import { readFile as
|
|
1131
|
+
import { resolve as resolve3, join as join2, dirname } from "path";
|
|
1132
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
995
1133
|
import { execSync } from "child_process";
|
|
1134
|
+
import { existsSync as existsSync5 } from "fs";
|
|
996
1135
|
import pc from "picocolors";
|
|
997
1136
|
|
|
998
1137
|
// src/store/finding-store.ts
|
|
999
|
-
|
|
1138
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1000
1139
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
1001
1140
|
import { resolve as resolve2 } from "path";
|
|
1002
1141
|
|
|
1003
|
-
// src/utils/atomic-writer.ts
|
|
1004
|
-
import {
|
|
1005
|
-
writeFileSync as writeFileSync2,
|
|
1006
|
-
existsSync as existsSync2,
|
|
1007
|
-
mkdirSync as mkdirSync2,
|
|
1008
|
-
renameSync
|
|
1009
|
-
} from "fs";
|
|
1010
|
-
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1011
|
-
|
|
1012
1142
|
// src/utils/fs.ts
|
|
1013
|
-
import { access } from "fs/promises";
|
|
1143
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
1014
1144
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1015
1145
|
import { resolve } from "path";
|
|
1016
1146
|
async function fileExists(path) {
|
|
@@ -1023,12 +1153,28 @@ async function fileExists(path) {
|
|
|
1023
1153
|
}
|
|
1024
1154
|
|
|
1025
1155
|
// src/store/finding-store.ts
|
|
1156
|
+
init_constants();
|
|
1157
|
+
init_limits();
|
|
1158
|
+
|
|
1159
|
+
// src/utils/atomic-writer.ts
|
|
1160
|
+
import {
|
|
1161
|
+
writeFileSync as writeFileSync2,
|
|
1162
|
+
existsSync as existsSync2,
|
|
1163
|
+
mkdirSync as mkdirSync2,
|
|
1164
|
+
renameSync
|
|
1165
|
+
} from "fs";
|
|
1166
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1167
|
+
init_log();
|
|
1168
|
+
init_type_guards();
|
|
1169
|
+
|
|
1170
|
+
// src/store/finding-store.ts
|
|
1171
|
+
init_log();
|
|
1026
1172
|
init_finding_id();
|
|
1027
1173
|
|
|
1028
1174
|
// src/detect/project.ts
|
|
1029
|
-
import { readFile as
|
|
1175
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1030
1176
|
import { existsSync as existsSync4 } from "fs";
|
|
1031
|
-
import { join } from "path";
|
|
1177
|
+
import { join, relative } from "path";
|
|
1032
1178
|
var FRAMEWORKS = [
|
|
1033
1179
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
1034
1180
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -1038,7 +1184,7 @@ var FRAMEWORKS = [
|
|
|
1038
1184
|
];
|
|
1039
1185
|
async function detectProject(rootDir) {
|
|
1040
1186
|
const pkgPath = join(rootDir, "package.json");
|
|
1041
|
-
const raw = await
|
|
1187
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
1042
1188
|
const pkg = JSON.parse(raw);
|
|
1043
1189
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1044
1190
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
@@ -1063,16 +1209,143 @@ function detectFrameworkFromDeps(allDeps) {
|
|
|
1063
1209
|
}
|
|
1064
1210
|
return "unknown";
|
|
1065
1211
|
}
|
|
1212
|
+
var PYTHON_ENTRY_CANDIDATES = [
|
|
1213
|
+
"app.py",
|
|
1214
|
+
"main.py",
|
|
1215
|
+
"wsgi.py",
|
|
1216
|
+
"asgi.py",
|
|
1217
|
+
"server.py",
|
|
1218
|
+
"run.py",
|
|
1219
|
+
"manage.py",
|
|
1220
|
+
"app/__init__.py"
|
|
1221
|
+
];
|
|
1222
|
+
var PYTHON_FRAMEWORK_MAP = {
|
|
1223
|
+
flask: "flask",
|
|
1224
|
+
fastapi: "fastapi",
|
|
1225
|
+
django: "django"
|
|
1226
|
+
};
|
|
1227
|
+
var PYTHON_DEFAULT_PORTS = {
|
|
1228
|
+
flask: 5e3,
|
|
1229
|
+
fastapi: 8e3,
|
|
1230
|
+
django: 8e3,
|
|
1231
|
+
unknown: 8e3
|
|
1232
|
+
};
|
|
1233
|
+
async function detectPythonProject(rootDir) {
|
|
1234
|
+
const hasPyproject = await fileExists(join(rootDir, "pyproject.toml"));
|
|
1235
|
+
const hasRequirements = await fileExists(join(rootDir, "requirements.txt"));
|
|
1236
|
+
const hasSetupPy = await fileExists(join(rootDir, "setup.py"));
|
|
1237
|
+
if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
|
|
1238
|
+
const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
|
|
1239
|
+
const packageManager = await detectPythonPackageManager(rootDir);
|
|
1240
|
+
const entryFile = await detectPythonEntry(rootDir);
|
|
1241
|
+
return {
|
|
1242
|
+
framework,
|
|
1243
|
+
packageManager,
|
|
1244
|
+
entryFile,
|
|
1245
|
+
defaultPort: PYTHON_DEFAULT_PORTS[framework] ?? 8e3
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
1249
|
+
if (hasPyproject) {
|
|
1250
|
+
try {
|
|
1251
|
+
const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
|
|
1252
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1253
|
+
if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
|
|
1254
|
+
return fw;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (hasRequirements) {
|
|
1261
|
+
try {
|
|
1262
|
+
const content = await readFile3(join(rootDir, "requirements.txt"), "utf-8");
|
|
1263
|
+
const lines = content.toLowerCase().split("\n");
|
|
1264
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1265
|
+
if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
|
|
1266
|
+
return fw;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return "unknown";
|
|
1273
|
+
}
|
|
1274
|
+
async function detectPythonPackageManager(rootDir) {
|
|
1275
|
+
if (await fileExists(join(rootDir, "uv.lock"))) return "uv";
|
|
1276
|
+
if (await fileExists(join(rootDir, "poetry.lock"))) return "poetry";
|
|
1277
|
+
if (await fileExists(join(rootDir, "Pipfile.lock"))) return "pipenv";
|
|
1278
|
+
if (await fileExists(join(rootDir, "Pipfile"))) return "pipenv";
|
|
1279
|
+
if (await fileExists(join(rootDir, "requirements.txt"))) return "pip";
|
|
1280
|
+
try {
|
|
1281
|
+
const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
|
|
1282
|
+
if (content.includes("[tool.poetry]")) return "poetry";
|
|
1283
|
+
if (content.includes("[tool.uv]")) return "uv";
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
return "unknown";
|
|
1287
|
+
}
|
|
1288
|
+
async function detectPythonEntry(rootDir) {
|
|
1289
|
+
for (const candidate of PYTHON_ENTRY_CANDIDATES) {
|
|
1290
|
+
if (await fileExists(join(rootDir, candidate))) {
|
|
1291
|
+
return candidate;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1297
|
+
"node_modules",
|
|
1298
|
+
".git",
|
|
1299
|
+
".brakit",
|
|
1300
|
+
"dist",
|
|
1301
|
+
"build",
|
|
1302
|
+
"__pycache__",
|
|
1303
|
+
".venv",
|
|
1304
|
+
"venv",
|
|
1305
|
+
".next",
|
|
1306
|
+
".nuxt",
|
|
1307
|
+
".output",
|
|
1308
|
+
".cache",
|
|
1309
|
+
"coverage"
|
|
1310
|
+
]);
|
|
1311
|
+
async function scanForProjects(rootDir) {
|
|
1312
|
+
const projects = [];
|
|
1313
|
+
await detectInDir(rootDir, rootDir, projects);
|
|
1314
|
+
try {
|
|
1315
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
1316
|
+
for (const entry of entries) {
|
|
1317
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1318
|
+
const childDir = join(rootDir, entry.name);
|
|
1319
|
+
await detectInDir(childDir, rootDir, projects);
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
return projects;
|
|
1324
|
+
}
|
|
1325
|
+
async function detectInDir(dir, rootDir, projects) {
|
|
1326
|
+
const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
|
|
1327
|
+
if (await fileExists(join(dir, "package.json"))) {
|
|
1328
|
+
try {
|
|
1329
|
+
const node = await detectProject(dir);
|
|
1330
|
+
projects.push({ dir, relDir: rel, type: "node", node });
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const python = await detectPythonProject(dir);
|
|
1335
|
+
if (python) {
|
|
1336
|
+
projects.push({ dir, relDir: rel, type: "python", python });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1066
1339
|
|
|
1067
1340
|
// src/analysis/rules/patterns.ts
|
|
1068
1341
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
1069
1342
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
1070
1343
|
var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
1071
|
-
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections
|
|
1344
|
+
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
|
|
1072
1345
|
var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
1073
1346
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
1074
|
-
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1075
|
-
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1347
|
+
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
1348
|
+
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
1076
1349
|
var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
1077
1350
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1078
1351
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -1082,9 +1355,9 @@ var RULE_HINTS = {
|
|
|
1082
1355
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
1083
1356
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
1084
1357
|
"error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
|
|
1358
|
+
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1085
1359
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
1086
1360
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
1087
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1088
1361
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
1089
1362
|
};
|
|
1090
1363
|
|
|
@@ -1455,48 +1728,47 @@ function hasInternalIds(obj) {
|
|
|
1455
1728
|
}
|
|
1456
1729
|
return false;
|
|
1457
1730
|
}
|
|
1458
|
-
function
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1731
|
+
function detectEchoPII(method, reqBody, target) {
|
|
1732
|
+
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1733
|
+
const reqEmails = findEmails(reqBody);
|
|
1734
|
+
if (reqEmails.length === 0) return null;
|
|
1735
|
+
const resEmails = findEmails(target);
|
|
1736
|
+
const echoed = reqEmails.filter((e) => resEmails.includes(e));
|
|
1737
|
+
if (echoed.length === 0) return null;
|
|
1738
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1739
|
+
if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
|
|
1740
|
+
return { reason: "echo", emailCount: echoed.length };
|
|
1472
1741
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
function detectFullRecordPII(target) {
|
|
1745
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
1746
|
+
const fields = topLevelFieldCount(target);
|
|
1747
|
+
if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
|
|
1748
|
+
const emails = findEmails(target);
|
|
1749
|
+
if (emails.length === 0) return null;
|
|
1750
|
+
return { reason: "full-record", emailCount: emails.length };
|
|
1751
|
+
}
|
|
1752
|
+
function detectListPII(target) {
|
|
1753
|
+
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1754
|
+
let itemsWithEmail = 0;
|
|
1755
|
+
for (let i = 0; i < Math.min(target.length, 10); i++) {
|
|
1756
|
+
const item = target[i];
|
|
1757
|
+
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
1758
|
+
itemsWithEmail++;
|
|
1480
1759
|
}
|
|
1481
1760
|
}
|
|
1482
|
-
if (
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (item && typeof item === "object") {
|
|
1487
|
-
const emails = findEmails(item);
|
|
1488
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
1492
|
-
const first = target[0];
|
|
1493
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1494
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1761
|
+
if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
|
|
1762
|
+
const first = target[0];
|
|
1763
|
+
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1764
|
+
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1497
1765
|
}
|
|
1498
1766
|
return null;
|
|
1499
1767
|
}
|
|
1768
|
+
function detectPII(method, reqBody, resBody) {
|
|
1769
|
+
const target = unwrapResponse(resBody);
|
|
1770
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1771
|
+
}
|
|
1500
1772
|
var REASON_LABELS = {
|
|
1501
1773
|
echo: "echoes back PII from the request body",
|
|
1502
1774
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -1540,6 +1812,9 @@ var responsePiiLeakRule = {
|
|
|
1540
1812
|
}
|
|
1541
1813
|
};
|
|
1542
1814
|
|
|
1815
|
+
// src/analysis/engine.ts
|
|
1816
|
+
init_limits();
|
|
1817
|
+
|
|
1543
1818
|
// src/analysis/group.ts
|
|
1544
1819
|
init_constants();
|
|
1545
1820
|
import { randomUUID } from "crypto";
|
|
@@ -1597,10 +1872,11 @@ init_constants();
|
|
|
1597
1872
|
|
|
1598
1873
|
// src/analysis/insight-tracker.ts
|
|
1599
1874
|
init_endpoint();
|
|
1875
|
+
init_finding_id();
|
|
1600
1876
|
init_thresholds();
|
|
1601
1877
|
|
|
1602
1878
|
// src/index.ts
|
|
1603
|
-
var VERSION = "0.8.
|
|
1879
|
+
var VERSION = "0.8.5";
|
|
1604
1880
|
|
|
1605
1881
|
// src/cli/commands/install.ts
|
|
1606
1882
|
init_constants();
|
|
@@ -1628,7 +1904,6 @@ var ENTRY_CANDIDATES = [
|
|
|
1628
1904
|
"index.js"
|
|
1629
1905
|
];
|
|
1630
1906
|
var BRAKIT_TEMPLATES = {
|
|
1631
|
-
/** Next.js instrumentation.ts — standalone file created by install */
|
|
1632
1907
|
nextjs: [
|
|
1633
1908
|
`export async function register() {`,
|
|
1634
1909
|
` if (process.env.NODE_ENV !== "production") {`,
|
|
@@ -1636,7 +1911,6 @@ var BRAKIT_TEMPLATES = {
|
|
|
1636
1911
|
` }`,
|
|
1637
1912
|
`}`
|
|
1638
1913
|
].join("\n"),
|
|
1639
|
-
/** Nuxt server/plugins/brakit.ts — standalone file created by install */
|
|
1640
1914
|
nuxt: `import "brakit";`
|
|
1641
1915
|
};
|
|
1642
1916
|
var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
|
|
@@ -1668,53 +1942,45 @@ var install_default = defineCommand({
|
|
|
1668
1942
|
},
|
|
1669
1943
|
async run({ args }) {
|
|
1670
1944
|
const rootDir = resolve3(args.dir);
|
|
1671
|
-
const pkgPath = join2(rootDir, "package.json");
|
|
1672
|
-
if (!await fileExists(pkgPath)) {
|
|
1673
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1674
|
-
process.exit(1);
|
|
1675
|
-
}
|
|
1676
|
-
let pkg;
|
|
1677
|
-
try {
|
|
1678
|
-
pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
1679
|
-
} catch {
|
|
1680
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1681
|
-
process.exit(1);
|
|
1682
|
-
}
|
|
1683
|
-
if (!pkg.name || typeof pkg.name !== "string") {
|
|
1684
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1685
|
-
process.exit(1);
|
|
1686
|
-
}
|
|
1687
|
-
let project;
|
|
1688
|
-
try {
|
|
1689
|
-
project = await detectProject(rootDir);
|
|
1690
|
-
} catch {
|
|
1691
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1692
|
-
process.exit(1);
|
|
1693
|
-
}
|
|
1694
1945
|
console.log();
|
|
1695
1946
|
console.log(pc.bold(" \u25C6 brakit install"));
|
|
1696
1947
|
console.log();
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
const result = await setupInstrumentation(rootDir, project.framework);
|
|
1704
|
-
if (result.action === "created") {
|
|
1705
|
-
console.log(pc.green(` \u2713 Created ${result.file}`));
|
|
1706
|
-
if (result.content) {
|
|
1948
|
+
const projects = await scanForProjects(rootDir);
|
|
1949
|
+
const nodeProjects = projects.filter((p) => p.type === "node");
|
|
1950
|
+
const pythonProjects = projects.filter((p) => p.type === "python");
|
|
1951
|
+
if (nodeProjects.length === 0) {
|
|
1952
|
+
if (pythonProjects.length > 0) {
|
|
1953
|
+
console.log(pc.dim(" Python project detected. To add brakit:"));
|
|
1707
1954
|
console.log();
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1955
|
+
console.log(pc.bold(" pip install brakit"));
|
|
1956
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
1957
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
1958
|
+
console.log();
|
|
1959
|
+
} else {
|
|
1960
|
+
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1961
|
+
}
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
for (const p of nodeProjects) {
|
|
1965
|
+
const node = p.node;
|
|
1966
|
+
const suffix = p.relDir === "." ? "" : ` in ${p.relDir}`;
|
|
1967
|
+
const installed = await installPackage(p.dir, node.packageManager);
|
|
1968
|
+
if (installed) {
|
|
1969
|
+
console.log(pc.green(` \u2713 Added brakit to devDependencies${suffix}`));
|
|
1970
|
+
} else {
|
|
1971
|
+
console.log(pc.dim(` \u2713 brakit already in dependencies${suffix}`));
|
|
1972
|
+
}
|
|
1973
|
+
const result = await setupInstrumentation(p.dir, node.framework);
|
|
1974
|
+
const prefix = p.relDir === "." ? "" : `${p.relDir}/`;
|
|
1975
|
+
if (result.action === "created") {
|
|
1976
|
+
console.log(pc.green(` \u2713 Created ${prefix}${result.file}`));
|
|
1977
|
+
} else if (result.action === "prepended") {
|
|
1978
|
+
console.log(pc.green(` \u2713 Added import to ${prefix}${result.file}`));
|
|
1979
|
+
} else if (result.action === "exists") {
|
|
1980
|
+
console.log(pc.dim(` \u2713 ${prefix}${result.file} already has brakit import`));
|
|
1981
|
+
} else {
|
|
1982
|
+
printManualInstructions(node.framework);
|
|
1711
1983
|
}
|
|
1712
|
-
} else if (result.action === "prepended") {
|
|
1713
|
-
console.log(pc.green(` \u2713 Added import to ${result.file}`));
|
|
1714
|
-
} else if (result.action === "exists") {
|
|
1715
|
-
console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
|
|
1716
|
-
} else {
|
|
1717
|
-
printManualInstructions(project.framework);
|
|
1718
1984
|
}
|
|
1719
1985
|
await ensureGitignoreEntry(rootDir, METRICS_DIR);
|
|
1720
1986
|
const mcpResult = await setupMcp(rootDir);
|
|
@@ -1723,14 +1989,30 @@ var install_default = defineCommand({
|
|
|
1723
1989
|
} else if (mcpResult === "exists") {
|
|
1724
1990
|
console.log(pc.dim(" \u2713 MCP already configured"));
|
|
1725
1991
|
}
|
|
1992
|
+
const gitRoot = findGitRoot(rootDir);
|
|
1993
|
+
if (gitRoot && gitRoot !== rootDir) {
|
|
1994
|
+
const parentMcpResult = await setupMcp(gitRoot);
|
|
1995
|
+
if (parentMcpResult === "created" || parentMcpResult === "updated") {
|
|
1996
|
+
console.log(pc.green(" \u2713 Configured MCP at project root"));
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1726
1999
|
console.log();
|
|
2000
|
+
const port = nodeProjects[0].node?.defaultPort ?? 3e3;
|
|
1727
2001
|
console.log(pc.dim(" Start your app and visit:"));
|
|
1728
|
-
console.log(pc.bold(
|
|
2002
|
+
console.log(pc.bold(` http://localhost:${port}/__brakit`));
|
|
2003
|
+
if (pythonProjects.length > 0) {
|
|
2004
|
+
const pyLabel = pythonProjects.map((p) => p.relDir).join(", ");
|
|
2005
|
+
console.log();
|
|
2006
|
+
console.log(pc.dim(` Python backend detected (${pyLabel}). To capture telemetry:`));
|
|
2007
|
+
console.log(pc.bold(" pip install brakit"));
|
|
2008
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
2009
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
2010
|
+
}
|
|
1729
2011
|
console.log();
|
|
1730
2012
|
}
|
|
1731
2013
|
});
|
|
1732
2014
|
async function installPackage(rootDir, pm) {
|
|
1733
|
-
const pkgRaw = await
|
|
2015
|
+
const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
|
|
1734
2016
|
const pkg = JSON.parse(pkgRaw);
|
|
1735
2017
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1736
2018
|
if (allDeps["brakit"]) return false;
|
|
@@ -1766,7 +2048,7 @@ async function setupNextjs(rootDir) {
|
|
|
1766
2048
|
const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
|
|
1767
2049
|
const absPath = join2(rootDir, relPath);
|
|
1768
2050
|
if (await fileExists(absPath)) {
|
|
1769
|
-
const content2 = await
|
|
2051
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1770
2052
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1771
2053
|
return { action: "exists", file: relPath };
|
|
1772
2054
|
}
|
|
@@ -1780,7 +2062,7 @@ async function setupNuxt(rootDir) {
|
|
|
1780
2062
|
const relPath = "server/plugins/brakit.ts";
|
|
1781
2063
|
const absPath = join2(rootDir, relPath);
|
|
1782
2064
|
if (await fileExists(absPath)) {
|
|
1783
|
-
const content2 = await
|
|
2065
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1784
2066
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1785
2067
|
return { action: "exists", file: relPath };
|
|
1786
2068
|
}
|
|
@@ -1797,7 +2079,7 @@ async function setupPrepend(rootDir, ...candidates) {
|
|
|
1797
2079
|
for (const relPath of candidates) {
|
|
1798
2080
|
const absPath = join2(rootDir, relPath);
|
|
1799
2081
|
if (!await fileExists(absPath)) continue;
|
|
1800
|
-
const content = await
|
|
2082
|
+
const content = await readFile4(absPath, "utf-8");
|
|
1801
2083
|
if (content.includes(IMPORT_MARKER)) {
|
|
1802
2084
|
return { action: "exists", file: relPath };
|
|
1803
2085
|
}
|
|
@@ -1809,7 +2091,7 @@ ${content}`);
|
|
|
1809
2091
|
}
|
|
1810
2092
|
async function setupGeneric(rootDir) {
|
|
1811
2093
|
try {
|
|
1812
|
-
const pkgRaw = await
|
|
2094
|
+
const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
|
|
1813
2095
|
const pkg = JSON.parse(pkgRaw);
|
|
1814
2096
|
if (pkg.main && typeof pkg.main === "string") {
|
|
1815
2097
|
const result2 = await setupPrepend(rootDir, pkg.main);
|
|
@@ -1829,21 +2111,21 @@ var MCP_CONFIG = {
|
|
|
1829
2111
|
}
|
|
1830
2112
|
}
|
|
1831
2113
|
};
|
|
1832
|
-
async function setupMcp(rootDir) {
|
|
2114
|
+
async function setupMcp(rootDir, config = MCP_CONFIG) {
|
|
1833
2115
|
const mcpPath = join2(rootDir, ".mcp.json");
|
|
1834
2116
|
if (await fileExists(mcpPath)) {
|
|
1835
|
-
const raw = await
|
|
2117
|
+
const raw = await readFile4(mcpPath, "utf-8");
|
|
1836
2118
|
try {
|
|
1837
|
-
const
|
|
1838
|
-
if (
|
|
1839
|
-
|
|
1840
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
2119
|
+
const existing = JSON.parse(raw);
|
|
2120
|
+
if (existing?.mcpServers?.brakit) return "exists";
|
|
2121
|
+
existing.mcpServers = { ...existing.mcpServers, ...config.mcpServers };
|
|
2122
|
+
await writeFile3(mcpPath, JSON.stringify(existing, null, 2) + "\n");
|
|
1841
2123
|
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1842
2124
|
return "updated";
|
|
1843
2125
|
} catch {
|
|
1844
2126
|
}
|
|
1845
2127
|
}
|
|
1846
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
2128
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1847
2129
|
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1848
2130
|
return "created";
|
|
1849
2131
|
}
|
|
@@ -1851,7 +2133,7 @@ async function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1851
2133
|
const gitignorePath = join2(rootDir, ".gitignore");
|
|
1852
2134
|
try {
|
|
1853
2135
|
if (await fileExists(gitignorePath)) {
|
|
1854
|
-
const content = await
|
|
2136
|
+
const content = await readFile4(gitignorePath, "utf-8");
|
|
1855
2137
|
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
1856
2138
|
await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
1857
2139
|
} else {
|
|
@@ -1860,6 +2142,15 @@ async function ensureGitignoreEntry(rootDir, entry) {
|
|
|
1860
2142
|
} catch {
|
|
1861
2143
|
}
|
|
1862
2144
|
}
|
|
2145
|
+
function findGitRoot(startDir) {
|
|
2146
|
+
let dir = resolve3(startDir);
|
|
2147
|
+
while (true) {
|
|
2148
|
+
if (existsSync5(join2(dir, ".git"))) return dir;
|
|
2149
|
+
const parent = dirname(dir);
|
|
2150
|
+
if (parent === dir) return null;
|
|
2151
|
+
dir = parent;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1863
2154
|
function printManualInstructions(framework) {
|
|
1864
2155
|
console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
|
|
1865
2156
|
console.log();
|
|
@@ -1880,7 +2171,7 @@ function printManualInstructions(framework) {
|
|
|
1880
2171
|
// src/cli/commands/uninstall.ts
|
|
1881
2172
|
import { defineCommand as defineCommand2 } from "citty";
|
|
1882
2173
|
import { resolve as resolve4, join as join3 } from "path";
|
|
1883
|
-
import { readFile as
|
|
2174
|
+
import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
|
|
1884
2175
|
import { execSync as execSync2 } from "child_process";
|
|
1885
2176
|
import pc2 from "picocolors";
|
|
1886
2177
|
init_constants();
|
|
@@ -1917,7 +2208,7 @@ var uninstall_default = defineCommand2({
|
|
|
1917
2208
|
for (const relPath of CREATED_FILES) {
|
|
1918
2209
|
const absPath = join3(rootDir, relPath);
|
|
1919
2210
|
if (!await fileExists(absPath)) continue;
|
|
1920
|
-
const content = await
|
|
2211
|
+
const content = await readFile5(absPath, "utf-8");
|
|
1921
2212
|
if (!content.includes("brakit")) continue;
|
|
1922
2213
|
if (isExactBrakitTemplate(content)) {
|
|
1923
2214
|
await unlink(absPath);
|
|
@@ -1939,7 +2230,7 @@ var uninstall_default = defineCommand2({
|
|
|
1939
2230
|
if (!removed) {
|
|
1940
2231
|
const candidates = [...PREPENDED_FILES];
|
|
1941
2232
|
try {
|
|
1942
|
-
const pkgRaw = await
|
|
2233
|
+
const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
|
|
1943
2234
|
const pkg = JSON.parse(pkgRaw);
|
|
1944
2235
|
if (pkg.main) candidates.unshift(pkg.main);
|
|
1945
2236
|
} catch {
|
|
@@ -1947,7 +2238,7 @@ var uninstall_default = defineCommand2({
|
|
|
1947
2238
|
for (const relPath of candidates) {
|
|
1948
2239
|
const absPath = join3(rootDir, relPath);
|
|
1949
2240
|
if (!await fileExists(absPath)) continue;
|
|
1950
|
-
const content = await
|
|
2241
|
+
const content = await readFile5(absPath, "utf-8");
|
|
1951
2242
|
if (!content.includes(IMPORT_LINE)) continue;
|
|
1952
2243
|
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
1953
2244
|
await writeFile4(absPath, updated);
|
|
@@ -1983,7 +2274,7 @@ async function removeMcpConfig(rootDir) {
|
|
|
1983
2274
|
const mcpPath = join3(rootDir, ".mcp.json");
|
|
1984
2275
|
if (!await fileExists(mcpPath)) return false;
|
|
1985
2276
|
try {
|
|
1986
|
-
const raw = await
|
|
2277
|
+
const raw = await readFile5(mcpPath, "utf-8");
|
|
1987
2278
|
const config = JSON.parse(raw);
|
|
1988
2279
|
if (!config?.mcpServers?.brakit) return false;
|
|
1989
2280
|
delete config.mcpServers.brakit;
|
|
@@ -1999,7 +2290,7 @@ async function removeMcpConfig(rootDir) {
|
|
|
1999
2290
|
}
|
|
2000
2291
|
async function uninstallPackage(rootDir, pm) {
|
|
2001
2292
|
try {
|
|
2002
|
-
const pkgRaw = await
|
|
2293
|
+
const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
|
|
2003
2294
|
const pkg = JSON.parse(pkgRaw);
|
|
2004
2295
|
if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
|
|
2005
2296
|
} catch {
|
|
@@ -2033,7 +2324,7 @@ async function cleanGitignore(rootDir) {
|
|
|
2033
2324
|
const gitignorePath = join3(rootDir, ".gitignore");
|
|
2034
2325
|
if (!await fileExists(gitignorePath)) return false;
|
|
2035
2326
|
try {
|
|
2036
|
-
const content = await
|
|
2327
|
+
const content = await readFile5(gitignorePath, "utf-8");
|
|
2037
2328
|
const lines = content.split("\n");
|
|
2038
2329
|
const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
|
|
2039
2330
|
if (filtered.length === lines.length) return false;
|