brakit 0.8.5 → 0.8.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/README.md +10 -4
- package/dist/api.d.ts +124 -117
- package/dist/api.js +417 -363
- package/dist/bin/brakit.js +499 -339
- package/dist/dashboard-client.global.js +703 -0
- package/dist/dashboard.html +895 -2168
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +2934 -5028
- package/package.json +4 -2
package/dist/bin/brakit.js
CHANGED
|
@@ -9,6 +9,91 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/constants/limits.ts
|
|
13
|
+
var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS;
|
|
14
|
+
var init_limits = __esm({
|
|
15
|
+
"src/constants/limits.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
PROJECT_HASH_LENGTH = 8;
|
|
18
|
+
SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
19
|
+
PII_SCAN_ARRAY_LIMIT = 10;
|
|
20
|
+
MIN_SECRET_VALUE_LENGTH = 8;
|
|
21
|
+
FULL_RECORD_MIN_FIELDS = 8;
|
|
22
|
+
LIST_PII_MIN_ITEMS = 2;
|
|
23
|
+
MAX_OBJECT_SCAN_DEPTH = 5;
|
|
24
|
+
ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// src/utils/log.ts
|
|
29
|
+
function brakitDebug(message) {
|
|
30
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
31
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var PREFIX;
|
|
36
|
+
var init_log = __esm({
|
|
37
|
+
"src/utils/log.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
PREFIX = "[brakit]";
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/constants/lifecycle.ts
|
|
44
|
+
var VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
|
|
45
|
+
var init_lifecycle = __esm({
|
|
46
|
+
"src/constants/lifecycle.ts"() {
|
|
47
|
+
"use strict";
|
|
48
|
+
VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
|
|
49
|
+
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
50
|
+
VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/utils/type-guards.ts
|
|
55
|
+
function isNonEmptyString(val) {
|
|
56
|
+
return typeof val === "string" && val.trim().length > 0;
|
|
57
|
+
}
|
|
58
|
+
function getErrorMessage(err) {
|
|
59
|
+
if (err instanceof Error) return err.message;
|
|
60
|
+
if (typeof err === "string") return err;
|
|
61
|
+
return String(err);
|
|
62
|
+
}
|
|
63
|
+
function isValidIssueState(val) {
|
|
64
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
65
|
+
}
|
|
66
|
+
function isValidAiFixStatus(val) {
|
|
67
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
68
|
+
}
|
|
69
|
+
var init_type_guards = __esm({
|
|
70
|
+
"src/utils/type-guards.ts"() {
|
|
71
|
+
"use strict";
|
|
72
|
+
init_lifecycle();
|
|
73
|
+
init_limits();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// src/constants/metrics.ts
|
|
78
|
+
var METRICS_DIR, PORT_FILE;
|
|
79
|
+
var init_metrics = __esm({
|
|
80
|
+
"src/constants/metrics.ts"() {
|
|
81
|
+
"use strict";
|
|
82
|
+
METRICS_DIR = ".brakit";
|
|
83
|
+
PORT_FILE = ".brakit/port";
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// src/constants/thresholds.ts
|
|
88
|
+
var OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS;
|
|
89
|
+
var init_thresholds = __esm({
|
|
90
|
+
"src/constants/thresholds.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
93
|
+
STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
12
97
|
// src/constants/routes.ts
|
|
13
98
|
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
99
|
var init_routes = __esm({
|
|
@@ -47,24 +132,6 @@ var init_routes = __esm({
|
|
|
47
132
|
}
|
|
48
133
|
});
|
|
49
134
|
|
|
50
|
-
// src/constants/limits.ts
|
|
51
|
-
var FINDING_ID_HASH_LENGTH;
|
|
52
|
-
var init_limits = __esm({
|
|
53
|
-
"src/constants/limits.ts"() {
|
|
54
|
-
"use strict";
|
|
55
|
-
FINDING_ID_HASH_LENGTH = 16;
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// src/constants/thresholds.ts
|
|
60
|
-
var OVERFETCH_UNWRAP_MIN_SIZE;
|
|
61
|
-
var init_thresholds = __esm({
|
|
62
|
-
"src/constants/thresholds.ts"() {
|
|
63
|
-
"use strict";
|
|
64
|
-
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
135
|
// src/constants/transport.ts
|
|
69
136
|
var init_transport = __esm({
|
|
70
137
|
"src/constants/transport.ts"() {
|
|
@@ -72,16 +139,6 @@ var init_transport = __esm({
|
|
|
72
139
|
}
|
|
73
140
|
});
|
|
74
141
|
|
|
75
|
-
// src/constants/metrics.ts
|
|
76
|
-
var METRICS_DIR, PORT_FILE;
|
|
77
|
-
var init_metrics = __esm({
|
|
78
|
-
"src/constants/metrics.ts"() {
|
|
79
|
-
"use strict";
|
|
80
|
-
METRICS_DIR = ".brakit";
|
|
81
|
-
PORT_FILE = ".brakit/port";
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
142
|
// src/constants/headers.ts
|
|
86
143
|
var init_headers = __esm({
|
|
87
144
|
"src/constants/headers.ts"() {
|
|
@@ -115,7 +172,7 @@ var init_mcp = __esm({
|
|
|
115
172
|
MAX_TIMELINE_EVENTS = 20;
|
|
116
173
|
MAX_RESOLVED_DISPLAY = 5;
|
|
117
174
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
118
|
-
MCP_SERVER_VERSION = "0.8.
|
|
175
|
+
MCP_SERVER_VERSION = "0.8.7";
|
|
119
176
|
}
|
|
120
177
|
});
|
|
121
178
|
|
|
@@ -140,14 +197,35 @@ var init_telemetry = __esm({
|
|
|
140
197
|
}
|
|
141
198
|
});
|
|
142
199
|
|
|
143
|
-
// src/constants/
|
|
144
|
-
var
|
|
145
|
-
var
|
|
146
|
-
"src/constants/
|
|
200
|
+
// src/constants/cli.ts
|
|
201
|
+
var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS;
|
|
202
|
+
var init_cli = __esm({
|
|
203
|
+
"src/constants/cli.ts"() {
|
|
204
|
+
"use strict";
|
|
205
|
+
SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
206
|
+
".ts",
|
|
207
|
+
".tsx",
|
|
208
|
+
".js",
|
|
209
|
+
".jsx",
|
|
210
|
+
".mjs",
|
|
211
|
+
".mts"
|
|
212
|
+
]);
|
|
213
|
+
BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
|
|
214
|
+
FALLBACK_SCAN_DIRS = ["src", "."];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// src/constants/timeline.ts
|
|
219
|
+
var init_timeline = __esm({
|
|
220
|
+
"src/constants/timeline.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// src/constants/sdk-events.ts
|
|
226
|
+
var init_sdk_events = __esm({
|
|
227
|
+
"src/constants/sdk-events.ts"() {
|
|
147
228
|
"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
229
|
}
|
|
152
230
|
});
|
|
153
231
|
|
|
@@ -167,51 +245,9 @@ var init_constants = __esm({
|
|
|
167
245
|
init_severity();
|
|
168
246
|
init_telemetry();
|
|
169
247
|
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();
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// src/store/finding-id.ts
|
|
206
|
-
import { createHash } from "crypto";
|
|
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);
|
|
210
|
-
}
|
|
211
|
-
var init_finding_id = __esm({
|
|
212
|
-
"src/store/finding-id.ts"() {
|
|
213
|
-
"use strict";
|
|
214
|
-
init_limits();
|
|
248
|
+
init_cli();
|
|
249
|
+
init_timeline();
|
|
250
|
+
init_sdk_events();
|
|
215
251
|
}
|
|
216
252
|
});
|
|
217
253
|
|
|
@@ -249,11 +285,11 @@ var init_client = __esm({
|
|
|
249
285
|
if (params?.offset) url.searchParams.set("offset", String(params.offset));
|
|
250
286
|
return this.fetchJson(url);
|
|
251
287
|
}
|
|
252
|
-
async
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return this.fetchJson(
|
|
288
|
+
async getIssues(params) {
|
|
289
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
|
|
290
|
+
if (params?.state) url.searchParams.set("state", params.state);
|
|
291
|
+
if (params?.category) url.searchParams.set("category", params.category);
|
|
292
|
+
return this.fetchJson(url);
|
|
257
293
|
}
|
|
258
294
|
async getQueries(requestId) {
|
|
259
295
|
const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
|
|
@@ -325,7 +361,7 @@ var init_client = __esm({
|
|
|
325
361
|
});
|
|
326
362
|
|
|
327
363
|
// src/mcp/discovery.ts
|
|
328
|
-
import { readFile as readFile6, readdir as
|
|
364
|
+
import { readFile as readFile6, readdir as readdir3, stat } from "fs/promises";
|
|
329
365
|
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
330
366
|
async function readPort(portPath) {
|
|
331
367
|
try {
|
|
@@ -341,7 +377,7 @@ async function portInDir(dir) {
|
|
|
341
377
|
}
|
|
342
378
|
async function portInChildren(dir) {
|
|
343
379
|
try {
|
|
344
|
-
const entries = await
|
|
380
|
+
const entries = await readdir3(dir);
|
|
345
381
|
for (const entry of entries) {
|
|
346
382
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
347
383
|
const child = resolve5(dir, entry);
|
|
@@ -408,14 +444,16 @@ var init_discovery = __esm({
|
|
|
408
444
|
|
|
409
445
|
// src/mcp/enrichment.ts
|
|
410
446
|
async function enrichFindings(client) {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
447
|
+
const issuesData = await client.getIssues();
|
|
448
|
+
const issues = issuesData.issues.filter(
|
|
449
|
+
(si) => si.state !== "resolved" && si.state !== "stale"
|
|
450
|
+
);
|
|
415
451
|
const contexts = await Promise.all(
|
|
416
|
-
|
|
452
|
+
issues.map(async (si) => {
|
|
453
|
+
const endpoint = si.issue.endpoint;
|
|
454
|
+
if (!endpoint) return si.issue.detail ?? "";
|
|
417
455
|
try {
|
|
418
|
-
const { path } = parseEndpointKey(
|
|
456
|
+
const { path } = parseEndpointKey(endpoint);
|
|
419
457
|
const reqData = await client.getRequests({ search: path, limit: 1 });
|
|
420
458
|
if (reqData.requests.length > 0) {
|
|
421
459
|
const req = reqData.requests[0];
|
|
@@ -429,38 +467,22 @@ async function enrichFindings(client) {
|
|
|
429
467
|
} catch {
|
|
430
468
|
return "(context unavailable)";
|
|
431
469
|
}
|
|
432
|
-
return "";
|
|
470
|
+
return si.issue.detail ?? "";
|
|
433
471
|
})
|
|
434
472
|
);
|
|
435
|
-
const enriched =
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
severity: f.severity,
|
|
440
|
-
title: f.title,
|
|
441
|
-
endpoint: f.endpoint,
|
|
442
|
-
description: f.desc,
|
|
443
|
-
hint: f.hint,
|
|
444
|
-
occurrences: f.count,
|
|
445
|
-
context: contexts[i],
|
|
446
|
-
aiStatus: sf.aiStatus,
|
|
447
|
-
aiNotes: sf.aiNotes
|
|
448
|
-
};
|
|
449
|
-
});
|
|
450
|
-
for (const si of insightsData.insights) {
|
|
451
|
-
if (si.state === "resolved") continue;
|
|
452
|
-
const i = si.insight;
|
|
453
|
-
if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
|
|
454
|
-
const endpoint = i.nav ?? "global";
|
|
473
|
+
const enriched = [];
|
|
474
|
+
for (let i = 0; i < issues.length; i++) {
|
|
475
|
+
const si = issues[i];
|
|
476
|
+
if (!ENRICHMENT_SEVERITY_FILTER.includes(si.issue.severity)) continue;
|
|
455
477
|
enriched.push({
|
|
456
|
-
findingId:
|
|
457
|
-
severity:
|
|
458
|
-
title:
|
|
459
|
-
endpoint,
|
|
460
|
-
description:
|
|
461
|
-
hint:
|
|
462
|
-
occurrences:
|
|
463
|
-
context: i
|
|
478
|
+
findingId: si.issueId,
|
|
479
|
+
severity: si.issue.severity,
|
|
480
|
+
title: si.issue.title,
|
|
481
|
+
endpoint: si.issue.endpoint ?? "global",
|
|
482
|
+
description: si.issue.desc,
|
|
483
|
+
hint: si.issue.hint,
|
|
484
|
+
occurrences: si.occurrences,
|
|
485
|
+
context: contexts[i],
|
|
464
486
|
aiStatus: si.aiStatus,
|
|
465
487
|
aiNotes: si.aiNotes
|
|
466
488
|
});
|
|
@@ -518,7 +540,6 @@ var init_enrichment = __esm({
|
|
|
518
540
|
"src/mcp/enrichment.ts"() {
|
|
519
541
|
"use strict";
|
|
520
542
|
init_mcp();
|
|
521
|
-
init_finding_id();
|
|
522
543
|
init_endpoint();
|
|
523
544
|
}
|
|
524
545
|
});
|
|
@@ -544,8 +565,8 @@ var init_get_findings = __esm({
|
|
|
544
565
|
},
|
|
545
566
|
state: {
|
|
546
567
|
type: "string",
|
|
547
|
-
enum: ["open", "fixing", "resolved"],
|
|
548
|
-
description: "Filter by
|
|
568
|
+
enum: ["open", "fixing", "resolved", "stale", "regressed"],
|
|
569
|
+
description: "Filter by issue state"
|
|
549
570
|
}
|
|
550
571
|
}
|
|
551
572
|
},
|
|
@@ -555,17 +576,17 @@ var init_get_findings = __esm({
|
|
|
555
576
|
if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
|
|
556
577
|
return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
|
|
557
578
|
}
|
|
558
|
-
if (state && !
|
|
559
|
-
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
|
|
579
|
+
if (state && !isValidIssueState(state)) {
|
|
580
|
+
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
|
|
560
581
|
}
|
|
561
582
|
let findings = await enrichFindings(client);
|
|
562
583
|
if (severity) {
|
|
563
584
|
findings = findings.filter((f) => f.severity === severity);
|
|
564
585
|
}
|
|
565
586
|
if (state) {
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
findings = findings.filter((f) =>
|
|
587
|
+
const issuesData = await client.getIssues({ state });
|
|
588
|
+
const issueIds = new Set(issuesData.issues.map((i) => i.issueId));
|
|
589
|
+
findings = findings.filter((f) => issueIds.has(f.findingId));
|
|
569
590
|
}
|
|
570
591
|
if (findings.length === 0) {
|
|
571
592
|
return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
|
|
@@ -744,20 +765,21 @@ var init_verify_fix = __esm({
|
|
|
744
765
|
}
|
|
745
766
|
if (findingId) {
|
|
746
767
|
const data = await client.getFindings();
|
|
747
|
-
const finding = data.findings.find((f) => f.
|
|
768
|
+
const finding = data.findings.find((f) => f.issueId === findingId);
|
|
748
769
|
if (!finding) {
|
|
749
770
|
return {
|
|
750
771
|
content: [{
|
|
751
772
|
type: "text",
|
|
752
773
|
text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
|
|
753
|
-
}]
|
|
774
|
+
}],
|
|
775
|
+
isError: true
|
|
754
776
|
};
|
|
755
777
|
}
|
|
756
778
|
if (finding.state === "resolved") {
|
|
757
779
|
return {
|
|
758
780
|
content: [{
|
|
759
781
|
type: "text",
|
|
760
|
-
text: `RESOLVED: "${finding.
|
|
782
|
+
text: `RESOLVED: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"} is no longer detected. The fix worked.`
|
|
761
783
|
}]
|
|
762
784
|
};
|
|
763
785
|
}
|
|
@@ -765,12 +787,12 @@ var init_verify_fix = __esm({
|
|
|
765
787
|
content: [{
|
|
766
788
|
type: "text",
|
|
767
789
|
text: [
|
|
768
|
-
`STILL PRESENT: "${finding.
|
|
790
|
+
`STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
|
|
769
791
|
` State: ${finding.state}`,
|
|
770
792
|
` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
|
|
771
793
|
` Occurrences: ${finding.occurrences}`,
|
|
772
|
-
` Issue: ${finding.
|
|
773
|
-
` Hint: ${finding.
|
|
794
|
+
` Issue: ${finding.issue.desc}`,
|
|
795
|
+
` Hint: ${finding.issue.hint}`,
|
|
774
796
|
"",
|
|
775
797
|
"Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
|
|
776
798
|
].join("\n")
|
|
@@ -780,7 +802,7 @@ var init_verify_fix = __esm({
|
|
|
780
802
|
if (endpoint) {
|
|
781
803
|
const data = await client.getFindings();
|
|
782
804
|
const endpointFindings = data.findings.filter(
|
|
783
|
-
(f) => f.
|
|
805
|
+
(f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
|
|
784
806
|
);
|
|
785
807
|
if (endpointFindings.length === 0) {
|
|
786
808
|
return {
|
|
@@ -790,7 +812,7 @@ var init_verify_fix = __esm({
|
|
|
790
812
|
}]
|
|
791
813
|
};
|
|
792
814
|
}
|
|
793
|
-
const open = endpointFindings.filter((f) => f.state === "open");
|
|
815
|
+
const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
|
|
794
816
|
const resolved = endpointFindings.filter((f) => f.state === "resolved");
|
|
795
817
|
const lines = [
|
|
796
818
|
`Endpoint: ${endpoint}`,
|
|
@@ -799,10 +821,10 @@ var init_verify_fix = __esm({
|
|
|
799
821
|
""
|
|
800
822
|
];
|
|
801
823
|
for (const f of open) {
|
|
802
|
-
lines.push(` [${f.
|
|
824
|
+
lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
|
|
803
825
|
}
|
|
804
826
|
for (const f of resolved) {
|
|
805
|
-
lines.push(` [resolved] ${f.
|
|
827
|
+
lines.push(` [resolved] ${f.issue.title}`);
|
|
806
828
|
}
|
|
807
829
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
808
830
|
}
|
|
@@ -810,7 +832,8 @@ var init_verify_fix = __esm({
|
|
|
810
832
|
content: [{
|
|
811
833
|
type: "text",
|
|
812
834
|
text: "Please provide either a finding_id or an endpoint to verify."
|
|
813
|
-
}]
|
|
835
|
+
}],
|
|
836
|
+
isError: true
|
|
814
837
|
};
|
|
815
838
|
}
|
|
816
839
|
};
|
|
@@ -831,51 +854,52 @@ var init_get_report = __esm({
|
|
|
831
854
|
properties: {}
|
|
832
855
|
},
|
|
833
856
|
async handler(client, _args) {
|
|
834
|
-
const [
|
|
835
|
-
client.
|
|
836
|
-
client.getSecurityFindings(),
|
|
837
|
-
client.getInsights(),
|
|
857
|
+
const [issuesData, metricsData] = await Promise.all([
|
|
858
|
+
client.getIssues(),
|
|
838
859
|
client.getLiveMetrics()
|
|
839
860
|
]);
|
|
840
|
-
const
|
|
841
|
-
const open =
|
|
842
|
-
const resolved =
|
|
843
|
-
const fixing =
|
|
844
|
-
const
|
|
845
|
-
const
|
|
861
|
+
const issues = issuesData.issues;
|
|
862
|
+
const open = issues.filter((f) => f.state === "open" || f.state === "regressed");
|
|
863
|
+
const resolved = issues.filter((f) => f.state === "resolved");
|
|
864
|
+
const fixing = issues.filter((f) => f.state === "fixing");
|
|
865
|
+
const stale = issues.filter((f) => f.state === "stale");
|
|
866
|
+
const criticalOpen = open.filter((f) => f.issue.severity === "critical");
|
|
867
|
+
const warningOpen = open.filter((f) => f.issue.severity === "warning");
|
|
868
|
+
const securityIssues = issues.filter((f) => f.category === "security");
|
|
869
|
+
const perfIssues = issues.filter((f) => f.category === "performance");
|
|
846
870
|
const totalRequests = metricsData.endpoints.reduce(
|
|
847
871
|
(s, ep) => s + ep.summary.totalRequests,
|
|
848
872
|
0
|
|
849
873
|
);
|
|
850
|
-
const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
|
|
851
874
|
const lines = [
|
|
852
875
|
"=== Brakit Report ===",
|
|
853
876
|
"",
|
|
854
877
|
`Endpoints observed: ${metricsData.endpoints.length}`,
|
|
855
878
|
`Total requests captured: ${totalRequests}`,
|
|
856
|
-
`
|
|
857
|
-
`Performance
|
|
879
|
+
`Security issues: ${securityIssues.length}`,
|
|
880
|
+
`Performance issues: ${perfIssues.length}`,
|
|
858
881
|
"",
|
|
859
|
-
"---
|
|
860
|
-
`Total: ${
|
|
882
|
+
"--- Issue Summary ---",
|
|
883
|
+
`Total: ${issues.length}`,
|
|
861
884
|
` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
|
|
862
885
|
` In progress: ${fixing.length}`,
|
|
863
|
-
` Resolved: ${resolved.length}
|
|
886
|
+
` Resolved: ${resolved.length}`,
|
|
887
|
+
` Stale: ${stale.length}`
|
|
864
888
|
];
|
|
865
889
|
if (criticalOpen.length > 0) {
|
|
866
890
|
lines.push("");
|
|
867
891
|
lines.push("--- Critical Issues (fix first) ---");
|
|
868
892
|
for (const f of criticalOpen) {
|
|
869
|
-
lines.push(` [CRITICAL] ${f.
|
|
870
|
-
lines.push(` ${f.
|
|
871
|
-
lines.push(` Fix: ${f.
|
|
893
|
+
lines.push(` [CRITICAL] ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
|
|
894
|
+
lines.push(` ${f.issue.desc}`);
|
|
895
|
+
lines.push(` Fix: ${f.issue.hint}`);
|
|
872
896
|
}
|
|
873
897
|
}
|
|
874
898
|
if (resolved.length > 0) {
|
|
875
899
|
lines.push("");
|
|
876
900
|
lines.push("--- Recently Resolved ---");
|
|
877
901
|
for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
|
|
878
|
-
lines.push(` \u2713 ${f.
|
|
902
|
+
lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
|
|
879
903
|
}
|
|
880
904
|
if (resolved.length > MAX_RESOLVED_DISPLAY) {
|
|
881
905
|
lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
|
|
@@ -1128,21 +1152,31 @@ import { runMain } from "citty";
|
|
|
1128
1152
|
|
|
1129
1153
|
// src/cli/commands/install.ts
|
|
1130
1154
|
import { defineCommand } from "citty";
|
|
1131
|
-
import { resolve as resolve3, join as
|
|
1155
|
+
import { resolve as resolve3, join as join3, dirname } from "path";
|
|
1132
1156
|
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1133
1157
|
import { execSync } from "child_process";
|
|
1134
1158
|
import { existsSync as existsSync5 } from "fs";
|
|
1135
1159
|
import pc from "picocolors";
|
|
1136
1160
|
|
|
1137
|
-
// src/store/
|
|
1161
|
+
// src/store/issue-store.ts
|
|
1138
1162
|
import { readFile as readFile2 } from "fs/promises";
|
|
1139
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
1163
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
|
|
1140
1164
|
import { resolve as resolve2 } from "path";
|
|
1141
1165
|
|
|
1142
1166
|
// src/utils/fs.ts
|
|
1167
|
+
init_limits();
|
|
1168
|
+
init_log();
|
|
1169
|
+
init_type_guards();
|
|
1143
1170
|
import { access, readFile, writeFile } from "fs/promises";
|
|
1144
1171
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1145
|
-
import {
|
|
1172
|
+
import { createHash } from "crypto";
|
|
1173
|
+
import { homedir } from "os";
|
|
1174
|
+
import { resolve, join } from "path";
|
|
1175
|
+
function getProjectDataDir(projectRoot) {
|
|
1176
|
+
const absolute = resolve(projectRoot);
|
|
1177
|
+
const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
|
|
1178
|
+
return join(homedir(), ".brakit", "projects", hash);
|
|
1179
|
+
}
|
|
1146
1180
|
async function fileExists(path) {
|
|
1147
1181
|
try {
|
|
1148
1182
|
await access(path);
|
|
@@ -1152,8 +1186,10 @@ async function fileExists(path) {
|
|
|
1152
1186
|
}
|
|
1153
1187
|
}
|
|
1154
1188
|
|
|
1155
|
-
// src/store/
|
|
1156
|
-
|
|
1189
|
+
// src/store/issue-store.ts
|
|
1190
|
+
init_metrics();
|
|
1191
|
+
init_limits();
|
|
1192
|
+
init_thresholds();
|
|
1157
1193
|
init_limits();
|
|
1158
1194
|
|
|
1159
1195
|
// src/utils/atomic-writer.ts
|
|
@@ -1167,14 +1203,18 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
|
1167
1203
|
init_log();
|
|
1168
1204
|
init_type_guards();
|
|
1169
1205
|
|
|
1170
|
-
// src/store/
|
|
1206
|
+
// src/store/issue-store.ts
|
|
1171
1207
|
init_log();
|
|
1172
|
-
|
|
1208
|
+
init_type_guards();
|
|
1209
|
+
|
|
1210
|
+
// src/utils/issue-id.ts
|
|
1211
|
+
init_limits();
|
|
1212
|
+
import { createHash as createHash2 } from "crypto";
|
|
1173
1213
|
|
|
1174
1214
|
// src/detect/project.ts
|
|
1175
1215
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1176
1216
|
import { existsSync as existsSync4 } from "fs";
|
|
1177
|
-
import { join, relative } from "path";
|
|
1217
|
+
import { join as join2, relative } from "path";
|
|
1178
1218
|
var FRAMEWORKS = [
|
|
1179
1219
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
1180
1220
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -1183,24 +1223,24 @@ var FRAMEWORKS = [
|
|
|
1183
1223
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
1184
1224
|
];
|
|
1185
1225
|
async function detectProject(rootDir) {
|
|
1186
|
-
const pkgPath =
|
|
1226
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
1187
1227
|
const raw = await readFile3(pkgPath, "utf-8");
|
|
1188
1228
|
const pkg = JSON.parse(raw);
|
|
1189
1229
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1190
1230
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
1191
1231
|
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
1192
1232
|
const devCommand = matched?.devCmd ?? "";
|
|
1193
|
-
const devBin = matched ?
|
|
1233
|
+
const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
1194
1234
|
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
1195
1235
|
const packageManager = await detectPackageManager(rootDir);
|
|
1196
1236
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
1197
1237
|
}
|
|
1198
1238
|
async function detectPackageManager(rootDir) {
|
|
1199
|
-
if (await fileExists(
|
|
1200
|
-
if (await fileExists(
|
|
1201
|
-
if (await fileExists(
|
|
1202
|
-
if (await fileExists(
|
|
1203
|
-
if (await fileExists(
|
|
1239
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
1240
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
1241
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
1242
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
1243
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
1204
1244
|
return "unknown";
|
|
1205
1245
|
}
|
|
1206
1246
|
function detectFrameworkFromDeps(allDeps) {
|
|
@@ -1231,9 +1271,9 @@ var PYTHON_DEFAULT_PORTS = {
|
|
|
1231
1271
|
unknown: 8e3
|
|
1232
1272
|
};
|
|
1233
1273
|
async function detectPythonProject(rootDir) {
|
|
1234
|
-
const hasPyproject = await fileExists(
|
|
1235
|
-
const hasRequirements = await fileExists(
|
|
1236
|
-
const hasSetupPy = await fileExists(
|
|
1274
|
+
const hasPyproject = await fileExists(join2(rootDir, "pyproject.toml"));
|
|
1275
|
+
const hasRequirements = await fileExists(join2(rootDir, "requirements.txt"));
|
|
1276
|
+
const hasSetupPy = await fileExists(join2(rootDir, "setup.py"));
|
|
1237
1277
|
if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
|
|
1238
1278
|
const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
|
|
1239
1279
|
const packageManager = await detectPythonPackageManager(rootDir);
|
|
@@ -1248,7 +1288,7 @@ async function detectPythonProject(rootDir) {
|
|
|
1248
1288
|
async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
1249
1289
|
if (hasPyproject) {
|
|
1250
1290
|
try {
|
|
1251
|
-
const content = await readFile3(
|
|
1291
|
+
const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
|
|
1252
1292
|
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1253
1293
|
if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
|
|
1254
1294
|
return fw;
|
|
@@ -1259,7 +1299,7 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
|
1259
1299
|
}
|
|
1260
1300
|
if (hasRequirements) {
|
|
1261
1301
|
try {
|
|
1262
|
-
const content = await readFile3(
|
|
1302
|
+
const content = await readFile3(join2(rootDir, "requirements.txt"), "utf-8");
|
|
1263
1303
|
const lines = content.toLowerCase().split("\n");
|
|
1264
1304
|
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1265
1305
|
if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
|
|
@@ -1272,13 +1312,13 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
|
1272
1312
|
return "unknown";
|
|
1273
1313
|
}
|
|
1274
1314
|
async function detectPythonPackageManager(rootDir) {
|
|
1275
|
-
if (await fileExists(
|
|
1276
|
-
if (await fileExists(
|
|
1277
|
-
if (await fileExists(
|
|
1278
|
-
if (await fileExists(
|
|
1279
|
-
if (await fileExists(
|
|
1315
|
+
if (await fileExists(join2(rootDir, "uv.lock"))) return "uv";
|
|
1316
|
+
if (await fileExists(join2(rootDir, "poetry.lock"))) return "poetry";
|
|
1317
|
+
if (await fileExists(join2(rootDir, "Pipfile.lock"))) return "pipenv";
|
|
1318
|
+
if (await fileExists(join2(rootDir, "Pipfile"))) return "pipenv";
|
|
1319
|
+
if (await fileExists(join2(rootDir, "requirements.txt"))) return "pip";
|
|
1280
1320
|
try {
|
|
1281
|
-
const content = await readFile3(
|
|
1321
|
+
const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
|
|
1282
1322
|
if (content.includes("[tool.poetry]")) return "poetry";
|
|
1283
1323
|
if (content.includes("[tool.uv]")) return "uv";
|
|
1284
1324
|
} catch {
|
|
@@ -1287,7 +1327,7 @@ async function detectPythonPackageManager(rootDir) {
|
|
|
1287
1327
|
}
|
|
1288
1328
|
async function detectPythonEntry(rootDir) {
|
|
1289
1329
|
for (const candidate of PYTHON_ENTRY_CANDIDATES) {
|
|
1290
|
-
if (await fileExists(
|
|
1330
|
+
if (await fileExists(join2(rootDir, candidate))) {
|
|
1291
1331
|
return candidate;
|
|
1292
1332
|
}
|
|
1293
1333
|
}
|
|
@@ -1315,7 +1355,7 @@ async function scanForProjects(rootDir) {
|
|
|
1315
1355
|
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
1316
1356
|
for (const entry of entries) {
|
|
1317
1357
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1318
|
-
const childDir =
|
|
1358
|
+
const childDir = join2(rootDir, entry.name);
|
|
1319
1359
|
await detectInDir(childDir, rootDir, projects);
|
|
1320
1360
|
}
|
|
1321
1361
|
} catch {
|
|
@@ -1324,7 +1364,7 @@ async function scanForProjects(rootDir) {
|
|
|
1324
1364
|
}
|
|
1325
1365
|
async function detectInDir(dir, rootDir, projects) {
|
|
1326
1366
|
const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
|
|
1327
|
-
if (await fileExists(
|
|
1367
|
+
if (await fileExists(join2(dir, "package.json"))) {
|
|
1328
1368
|
try {
|
|
1329
1369
|
const node = await detectProject(dir);
|
|
1330
1370
|
projects.push({ dir, relDir: rel, type: "node", node });
|
|
@@ -1337,6 +1377,31 @@ async function detectInDir(dir, rootDir, projects) {
|
|
|
1337
1377
|
}
|
|
1338
1378
|
}
|
|
1339
1379
|
|
|
1380
|
+
// src/utils/response.ts
|
|
1381
|
+
init_thresholds();
|
|
1382
|
+
function unwrapResponse(parsed) {
|
|
1383
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
1384
|
+
const obj = parsed;
|
|
1385
|
+
const keys = Object.keys(obj);
|
|
1386
|
+
if (keys.length > 3) return parsed;
|
|
1387
|
+
let best = null;
|
|
1388
|
+
let bestSize = 0;
|
|
1389
|
+
for (const key of keys) {
|
|
1390
|
+
const val = obj[key];
|
|
1391
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
1392
|
+
best = val;
|
|
1393
|
+
bestSize = val.length;
|
|
1394
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1395
|
+
const size = Object.keys(val).length;
|
|
1396
|
+
if (size > bestSize) {
|
|
1397
|
+
best = val;
|
|
1398
|
+
bestSize = size;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1340
1405
|
// src/analysis/rules/patterns.ts
|
|
1341
1406
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
1342
1407
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
@@ -1350,6 +1415,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
|
1350
1415
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1351
1416
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
1352
1417
|
var INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
1418
|
+
var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
|
|
1419
|
+
var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
|
|
1353
1420
|
var RULE_HINTS = {
|
|
1354
1421
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
1355
1422
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -1362,30 +1429,34 @@ var RULE_HINTS = {
|
|
|
1362
1429
|
};
|
|
1363
1430
|
|
|
1364
1431
|
// src/analysis/rules/exposed-secret.ts
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1432
|
+
init_limits();
|
|
1433
|
+
|
|
1434
|
+
// src/utils/http-status.ts
|
|
1435
|
+
function isErrorStatus(code) {
|
|
1436
|
+
return code >= 400;
|
|
1437
|
+
}
|
|
1438
|
+
function isRedirect(code) {
|
|
1439
|
+
return code >= 300 && code < 400;
|
|
1372
1440
|
}
|
|
1373
|
-
|
|
1441
|
+
|
|
1442
|
+
// src/analysis/rules/exposed-secret.ts
|
|
1443
|
+
function findSecretKeys(obj, prefix, depth = 0) {
|
|
1374
1444
|
const found = [];
|
|
1445
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
|
|
1375
1446
|
if (!obj || typeof obj !== "object") return found;
|
|
1376
1447
|
if (Array.isArray(obj)) {
|
|
1377
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
1378
|
-
found.push(...findSecretKeys(obj[i], prefix));
|
|
1448
|
+
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
1449
|
+
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
1379
1450
|
}
|
|
1380
1451
|
return found;
|
|
1381
1452
|
}
|
|
1382
1453
|
for (const k of Object.keys(obj)) {
|
|
1383
1454
|
const val = obj[k];
|
|
1384
|
-
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >=
|
|
1455
|
+
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
|
|
1385
1456
|
found.push(k);
|
|
1386
1457
|
}
|
|
1387
1458
|
if (typeof val === "object" && val !== null) {
|
|
1388
|
-
found.push(...findSecretKeys(val, prefix + k + "."));
|
|
1459
|
+
found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
|
|
1389
1460
|
}
|
|
1390
1461
|
}
|
|
1391
1462
|
return found;
|
|
@@ -1399,8 +1470,8 @@ var exposedSecretRule = {
|
|
|
1399
1470
|
const findings = [];
|
|
1400
1471
|
const seen = /* @__PURE__ */ new Map();
|
|
1401
1472
|
for (const r of ctx.requests) {
|
|
1402
|
-
if (r.statusCode
|
|
1403
|
-
const parsed =
|
|
1473
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
1474
|
+
const parsed = ctx.parsedBodies.response.get(r.id);
|
|
1404
1475
|
if (!parsed) continue;
|
|
1405
1476
|
const keys = findSecretKeys(parsed, "");
|
|
1406
1477
|
if (keys.length === 0) continue;
|
|
@@ -1553,7 +1624,7 @@ var errorInfoLeakRule = {
|
|
|
1553
1624
|
|
|
1554
1625
|
// src/analysis/rules/insecure-cookie.ts
|
|
1555
1626
|
function isFrameworkResponse(r) {
|
|
1556
|
-
if (r.statusCode
|
|
1627
|
+
if (isRedirect(r.statusCode)) return true;
|
|
1557
1628
|
if (r.path?.startsWith("/__")) return true;
|
|
1558
1629
|
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
1559
1630
|
return false;
|
|
@@ -1659,49 +1730,16 @@ var corsCredentialsRule = {
|
|
|
1659
1730
|
}
|
|
1660
1731
|
};
|
|
1661
1732
|
|
|
1662
|
-
// src/utils/response.ts
|
|
1663
|
-
init_thresholds();
|
|
1664
|
-
function unwrapResponse(parsed) {
|
|
1665
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
1666
|
-
const obj = parsed;
|
|
1667
|
-
const keys = Object.keys(obj);
|
|
1668
|
-
if (keys.length > 3) return parsed;
|
|
1669
|
-
let best = null;
|
|
1670
|
-
let bestSize = 0;
|
|
1671
|
-
for (const key of keys) {
|
|
1672
|
-
const val = obj[key];
|
|
1673
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
1674
|
-
best = val;
|
|
1675
|
-
bestSize = val.length;
|
|
1676
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1677
|
-
const size = Object.keys(val).length;
|
|
1678
|
-
if (size > bestSize) {
|
|
1679
|
-
best = val;
|
|
1680
|
-
bestSize = size;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
1733
|
// src/analysis/rules/response-pii-leak.ts
|
|
1734
|
+
init_limits();
|
|
1688
1735
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
1689
|
-
|
|
1690
|
-
var LIST_PII_MIN_ITEMS = 2;
|
|
1691
|
-
function tryParseJson2(body) {
|
|
1692
|
-
if (!body) return null;
|
|
1693
|
-
try {
|
|
1694
|
-
return JSON.parse(body);
|
|
1695
|
-
} catch {
|
|
1696
|
-
return null;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
function findEmails(obj) {
|
|
1736
|
+
function findEmails(obj, depth = 0) {
|
|
1700
1737
|
const emails = [];
|
|
1738
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
|
|
1701
1739
|
if (!obj || typeof obj !== "object") return emails;
|
|
1702
1740
|
if (Array.isArray(obj)) {
|
|
1703
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
1704
|
-
emails.push(...findEmails(obj[i]));
|
|
1741
|
+
for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
1742
|
+
emails.push(...findEmails(obj[i], depth + 1));
|
|
1705
1743
|
}
|
|
1706
1744
|
return emails;
|
|
1707
1745
|
}
|
|
@@ -1709,7 +1747,7 @@ function findEmails(obj) {
|
|
|
1709
1747
|
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
1710
1748
|
emails.push(v);
|
|
1711
1749
|
} else if (typeof v === "object" && v !== null) {
|
|
1712
|
-
emails.push(...findEmails(v));
|
|
1750
|
+
emails.push(...findEmails(v, depth + 1));
|
|
1713
1751
|
}
|
|
1714
1752
|
}
|
|
1715
1753
|
return emails;
|
|
@@ -1728,6 +1766,15 @@ function hasInternalIds(obj) {
|
|
|
1728
1766
|
}
|
|
1729
1767
|
return false;
|
|
1730
1768
|
}
|
|
1769
|
+
function hasSensitiveFieldNames(obj, depth = 0) {
|
|
1770
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
|
|
1771
|
+
if (!obj || typeof obj !== "object") return false;
|
|
1772
|
+
if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
|
|
1773
|
+
for (const key of Object.keys(obj)) {
|
|
1774
|
+
if (SENSITIVE_FIELD_NAMES.test(key)) return true;
|
|
1775
|
+
}
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1731
1778
|
function detectEchoPII(method, reqBody, target) {
|
|
1732
1779
|
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1733
1780
|
const reqEmails = findEmails(reqBody);
|
|
@@ -1749,10 +1796,17 @@ function detectFullRecordPII(target) {
|
|
|
1749
1796
|
if (emails.length === 0) return null;
|
|
1750
1797
|
return { reason: "full-record", emailCount: emails.length };
|
|
1751
1798
|
}
|
|
1799
|
+
function detectSensitiveFieldPII(target) {
|
|
1800
|
+
const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1801
|
+
if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
|
|
1802
|
+
if (!hasSensitiveFieldNames(inspect)) return null;
|
|
1803
|
+
if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
|
|
1804
|
+
return { reason: "sensitive-fields", emailCount: 0 };
|
|
1805
|
+
}
|
|
1752
1806
|
function detectListPII(target) {
|
|
1753
1807
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1754
1808
|
let itemsWithEmail = 0;
|
|
1755
|
-
for (let i = 0; i < Math.min(target.length,
|
|
1809
|
+
for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
1756
1810
|
const item = target[i];
|
|
1757
1811
|
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
1758
1812
|
itemsWithEmail++;
|
|
@@ -1767,12 +1821,13 @@ function detectListPII(target) {
|
|
|
1767
1821
|
}
|
|
1768
1822
|
function detectPII(method, reqBody, resBody) {
|
|
1769
1823
|
const target = unwrapResponse(resBody);
|
|
1770
|
-
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1824
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
1771
1825
|
}
|
|
1772
1826
|
var REASON_LABELS = {
|
|
1773
1827
|
echo: "echoes back PII from the request body",
|
|
1774
1828
|
"full-record": "returns a full record with email and internal IDs",
|
|
1775
|
-
"list-pii": "returns a list of records containing email addresses"
|
|
1829
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
1830
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
1776
1831
|
};
|
|
1777
1832
|
var responsePiiLeakRule = {
|
|
1778
1833
|
id: "response-pii-leak",
|
|
@@ -1783,15 +1838,15 @@ var responsePiiLeakRule = {
|
|
|
1783
1838
|
const findings = [];
|
|
1784
1839
|
const seen = /* @__PURE__ */ new Map();
|
|
1785
1840
|
for (const r of ctx.requests) {
|
|
1786
|
-
if (r.statusCode
|
|
1787
|
-
|
|
1841
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
1842
|
+
if (SELF_SERVICE_PATH.test(r.path)) continue;
|
|
1843
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
1788
1844
|
if (!resJson) continue;
|
|
1789
|
-
const reqJson =
|
|
1845
|
+
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
1790
1846
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
1791
1847
|
if (!detection) continue;
|
|
1792
1848
|
const ep = `${r.method} ${r.path}`;
|
|
1793
|
-
const
|
|
1794
|
-
const existing = seen.get(dedupKey);
|
|
1849
|
+
const existing = seen.get(ep);
|
|
1795
1850
|
if (existing) {
|
|
1796
1851
|
existing.count++;
|
|
1797
1852
|
continue;
|
|
@@ -1800,12 +1855,12 @@ var responsePiiLeakRule = {
|
|
|
1800
1855
|
severity: "warning",
|
|
1801
1856
|
rule: "response-pii-leak",
|
|
1802
1857
|
title: "PII Leak in Response",
|
|
1803
|
-
desc: `${ep} \u2014
|
|
1804
|
-
hint: this.hint
|
|
1858
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
1859
|
+
hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
|
|
1805
1860
|
endpoint: ep,
|
|
1806
1861
|
count: 1
|
|
1807
1862
|
};
|
|
1808
|
-
seen.set(
|
|
1863
|
+
seen.set(ep, finding);
|
|
1809
1864
|
findings.push(finding);
|
|
1810
1865
|
}
|
|
1811
1866
|
return findings;
|
|
@@ -1870,13 +1925,11 @@ init_constants();
|
|
|
1870
1925
|
// src/analysis/insights/rules/regression.ts
|
|
1871
1926
|
init_constants();
|
|
1872
1927
|
|
|
1873
|
-
// src/analysis/
|
|
1928
|
+
// src/analysis/issue-mappers.ts
|
|
1874
1929
|
init_endpoint();
|
|
1875
|
-
init_finding_id();
|
|
1876
|
-
init_thresholds();
|
|
1877
1930
|
|
|
1878
1931
|
// src/index.ts
|
|
1879
|
-
var VERSION = "0.8.
|
|
1932
|
+
var VERSION = "0.8.7";
|
|
1880
1933
|
|
|
1881
1934
|
// src/cli/commands/install.ts
|
|
1882
1935
|
init_constants();
|
|
@@ -1884,10 +1937,28 @@ init_constants();
|
|
|
1884
1937
|
// src/cli/templates.ts
|
|
1885
1938
|
var IMPORT_LINE = `import "brakit";`;
|
|
1886
1939
|
var IMPORT_MARKER = "brakit";
|
|
1940
|
+
var BRAKIT_IMPORT_PATTERNS = [
|
|
1941
|
+
'import("brakit")',
|
|
1942
|
+
'import "brakit"',
|
|
1943
|
+
"import 'brakit'",
|
|
1944
|
+
'require("brakit")',
|
|
1945
|
+
"require('brakit')"
|
|
1946
|
+
];
|
|
1947
|
+
function containsBrakitImport(content) {
|
|
1948
|
+
return BRAKIT_IMPORT_PATTERNS.some((p) => content.includes(p));
|
|
1949
|
+
}
|
|
1950
|
+
function removeBrakitImportLines(lines) {
|
|
1951
|
+
return lines.filter(
|
|
1952
|
+
(line) => !BRAKIT_IMPORT_PATTERNS.some((p) => line.includes(p))
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1887
1955
|
var CREATED_FILES = [
|
|
1888
1956
|
"src/instrumentation.ts",
|
|
1957
|
+
"src/instrumentation.js",
|
|
1889
1958
|
"instrumentation.ts",
|
|
1890
|
-
"
|
|
1959
|
+
"instrumentation.js",
|
|
1960
|
+
"server/plugins/brakit.ts",
|
|
1961
|
+
"server/plugins/brakit.js"
|
|
1891
1962
|
];
|
|
1892
1963
|
var ENTRY_CANDIDATES = [
|
|
1893
1964
|
"src/index.ts",
|
|
@@ -2012,7 +2083,7 @@ var install_default = defineCommand({
|
|
|
2012
2083
|
}
|
|
2013
2084
|
});
|
|
2014
2085
|
async function installPackage(rootDir, pm) {
|
|
2015
|
-
const pkgRaw = await readFile4(
|
|
2086
|
+
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
2016
2087
|
const pkg = JSON.parse(pkgRaw);
|
|
2017
2088
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2018
2089
|
if (allDeps["brakit"]) return false;
|
|
@@ -2044,9 +2115,9 @@ async function setupInstrumentation(rootDir, framework) {
|
|
|
2044
2115
|
}
|
|
2045
2116
|
}
|
|
2046
2117
|
async function setupNextjs(rootDir) {
|
|
2047
|
-
const hasSrc = await fileExists(
|
|
2118
|
+
const hasSrc = await fileExists(join3(rootDir, "src"));
|
|
2048
2119
|
const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
|
|
2049
|
-
const absPath =
|
|
2120
|
+
const absPath = join3(rootDir, relPath);
|
|
2050
2121
|
if (await fileExists(absPath)) {
|
|
2051
2122
|
const content2 = await readFile4(absPath, "utf-8");
|
|
2052
2123
|
if (content2.includes(IMPORT_MARKER)) {
|
|
@@ -2060,7 +2131,7 @@ async function setupNextjs(rootDir) {
|
|
|
2060
2131
|
}
|
|
2061
2132
|
async function setupNuxt(rootDir) {
|
|
2062
2133
|
const relPath = "server/plugins/brakit.ts";
|
|
2063
|
-
const absPath =
|
|
2134
|
+
const absPath = join3(rootDir, relPath);
|
|
2064
2135
|
if (await fileExists(absPath)) {
|
|
2065
2136
|
const content2 = await readFile4(absPath, "utf-8");
|
|
2066
2137
|
if (content2.includes(IMPORT_MARKER)) {
|
|
@@ -2069,7 +2140,7 @@ async function setupNuxt(rootDir) {
|
|
|
2069
2140
|
return { action: "manual", file: relPath };
|
|
2070
2141
|
}
|
|
2071
2142
|
const content = BRAKIT_TEMPLATES.nuxt + "\n";
|
|
2072
|
-
const dir =
|
|
2143
|
+
const dir = join3(rootDir, "server/plugins");
|
|
2073
2144
|
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
2074
2145
|
mkdirSync3(dir, { recursive: true });
|
|
2075
2146
|
await writeFile3(absPath, content);
|
|
@@ -2077,7 +2148,7 @@ async function setupNuxt(rootDir) {
|
|
|
2077
2148
|
}
|
|
2078
2149
|
async function setupPrepend(rootDir, ...candidates) {
|
|
2079
2150
|
for (const relPath of candidates) {
|
|
2080
|
-
const absPath =
|
|
2151
|
+
const absPath = join3(rootDir, relPath);
|
|
2081
2152
|
if (!await fileExists(absPath)) continue;
|
|
2082
2153
|
const content = await readFile4(absPath, "utf-8");
|
|
2083
2154
|
if (content.includes(IMPORT_MARKER)) {
|
|
@@ -2091,7 +2162,7 @@ ${content}`);
|
|
|
2091
2162
|
}
|
|
2092
2163
|
async function setupGeneric(rootDir) {
|
|
2093
2164
|
try {
|
|
2094
|
-
const pkgRaw = await readFile4(
|
|
2165
|
+
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
2095
2166
|
const pkg = JSON.parse(pkgRaw);
|
|
2096
2167
|
if (pkg.main && typeof pkg.main === "string") {
|
|
2097
2168
|
const result2 = await setupPrepend(rootDir, pkg.main);
|
|
@@ -2112,7 +2183,7 @@ var MCP_CONFIG = {
|
|
|
2112
2183
|
}
|
|
2113
2184
|
};
|
|
2114
2185
|
async function setupMcp(rootDir, config = MCP_CONFIG) {
|
|
2115
|
-
const mcpPath =
|
|
2186
|
+
const mcpPath = join3(rootDir, ".mcp.json");
|
|
2116
2187
|
if (await fileExists(mcpPath)) {
|
|
2117
2188
|
const raw = await readFile4(mcpPath, "utf-8");
|
|
2118
2189
|
try {
|
|
@@ -2130,7 +2201,7 @@ async function setupMcp(rootDir, config = MCP_CONFIG) {
|
|
|
2130
2201
|
return "created";
|
|
2131
2202
|
}
|
|
2132
2203
|
async function ensureGitignoreEntry(rootDir, entry) {
|
|
2133
|
-
const gitignorePath =
|
|
2204
|
+
const gitignorePath = join3(rootDir, ".gitignore");
|
|
2134
2205
|
try {
|
|
2135
2206
|
if (await fileExists(gitignorePath)) {
|
|
2136
2207
|
const content = await readFile4(gitignorePath, "utf-8");
|
|
@@ -2145,7 +2216,7 @@ async function ensureGitignoreEntry(rootDir, entry) {
|
|
|
2145
2216
|
function findGitRoot(startDir) {
|
|
2146
2217
|
let dir = resolve3(startDir);
|
|
2147
2218
|
while (true) {
|
|
2148
|
-
if (existsSync5(
|
|
2219
|
+
if (existsSync5(join3(dir, ".git"))) return dir;
|
|
2149
2220
|
const parent = dirname(dir);
|
|
2150
2221
|
if (parent === dir) return null;
|
|
2151
2222
|
dir = parent;
|
|
@@ -2170,11 +2241,13 @@ function printManualInstructions(framework) {
|
|
|
2170
2241
|
|
|
2171
2242
|
// src/cli/commands/uninstall.ts
|
|
2172
2243
|
import { defineCommand as defineCommand2 } from "citty";
|
|
2173
|
-
import { resolve as resolve4, join as
|
|
2174
|
-
import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
|
|
2244
|
+
import { resolve as resolve4, join as join4, relative as relative2 } from "path";
|
|
2245
|
+
import { readFile as readFile5, writeFile as writeFile4, unlink, rm, readdir as readdir2 } from "fs/promises";
|
|
2175
2246
|
import { execSync as execSync2 } from "child_process";
|
|
2176
2247
|
import pc2 from "picocolors";
|
|
2177
2248
|
init_constants();
|
|
2249
|
+
init_log();
|
|
2250
|
+
init_type_guards();
|
|
2178
2251
|
var PREPENDED_FILES = [
|
|
2179
2252
|
"app/entry.server.tsx",
|
|
2180
2253
|
"app/entry.server.ts",
|
|
@@ -2196,82 +2269,139 @@ var uninstall_default = defineCommand2({
|
|
|
2196
2269
|
},
|
|
2197
2270
|
async run({ args }) {
|
|
2198
2271
|
const rootDir = resolve4(args.dir);
|
|
2199
|
-
let
|
|
2272
|
+
let projects = [];
|
|
2200
2273
|
try {
|
|
2201
|
-
|
|
2202
|
-
|
|
2274
|
+
const scanned = await scanForProjects(rootDir);
|
|
2275
|
+
projects = scanned.filter((p) => p.type === "node" && p.node).map((p) => ({ dir: p.dir, pm: p.node.packageManager }));
|
|
2276
|
+
} catch (err) {
|
|
2277
|
+
brakitDebug(`uninstall: project scan failed: ${getErrorMessage(err)}`);
|
|
2278
|
+
}
|
|
2279
|
+
if (projects.length === 0) {
|
|
2280
|
+
projects = [{ dir: rootDir, pm: "npm" }];
|
|
2203
2281
|
}
|
|
2204
2282
|
console.log();
|
|
2205
2283
|
console.log(pc2.bold(" \u25C6 brakit uninstall"));
|
|
2206
2284
|
console.log();
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
await unlink(absPath);
|
|
2215
|
-
console.log(pc2.green(` \u2713 Removed ${relPath}`));
|
|
2216
|
-
removed = true;
|
|
2217
|
-
break;
|
|
2218
|
-
}
|
|
2219
|
-
const lines = content.split("\n");
|
|
2220
|
-
const cleaned = lines.filter(
|
|
2221
|
-
(line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
|
|
2222
|
-
);
|
|
2223
|
-
if (cleaned.length < lines.length) {
|
|
2224
|
-
await writeFile4(absPath, cleaned.join("\n"));
|
|
2225
|
-
console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
|
|
2226
|
-
removed = true;
|
|
2227
|
-
break;
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
if (!removed) {
|
|
2231
|
-
const candidates = [...PREPENDED_FILES];
|
|
2232
|
-
try {
|
|
2233
|
-
const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
|
|
2234
|
-
const pkg = JSON.parse(pkgRaw);
|
|
2235
|
-
if (pkg.main) candidates.unshift(pkg.main);
|
|
2236
|
-
} catch {
|
|
2285
|
+
for (const project of projects) {
|
|
2286
|
+
const suffix = projects.length > 1 ? ` in ${relative2(rootDir, project.dir) || "."}` : "";
|
|
2287
|
+
const removed = await removeInstrumentation(project.dir);
|
|
2288
|
+
if (removed) {
|
|
2289
|
+
console.log(pc2.green(` \u2713 ${removed}${suffix}`));
|
|
2290
|
+
} else {
|
|
2291
|
+
console.log(pc2.dim(` No brakit instrumentation files found${suffix}.`));
|
|
2237
2292
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
if (!content.includes(IMPORT_LINE)) continue;
|
|
2243
|
-
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
2244
|
-
await writeFile4(absPath, updated);
|
|
2245
|
-
console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
|
|
2246
|
-
removed = true;
|
|
2247
|
-
break;
|
|
2293
|
+
const uninstalled = await uninstallPackage(project.dir, project.pm);
|
|
2294
|
+
if (uninstalled === true) {
|
|
2295
|
+
console.log(pc2.green(` \u2713 Removed brakit from devDependencies${suffix}`));
|
|
2296
|
+
} else if (uninstalled === "failed") {
|
|
2248
2297
|
}
|
|
2249
2298
|
}
|
|
2250
|
-
if (!removed) {
|
|
2251
|
-
console.log(pc2.dim(" No brakit instrumentation files found."));
|
|
2252
|
-
}
|
|
2253
2299
|
const mcpRemoved = await removeMcpConfig(rootDir);
|
|
2254
2300
|
if (mcpRemoved) {
|
|
2255
2301
|
console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
|
|
2256
2302
|
}
|
|
2257
2303
|
const dataRemoved = await removeBrakitData(rootDir);
|
|
2258
2304
|
if (dataRemoved) {
|
|
2259
|
-
console.log(pc2.green(" \u2713 Removed .brakit
|
|
2305
|
+
console.log(pc2.green(" \u2713 Removed .brakit data"));
|
|
2260
2306
|
}
|
|
2261
2307
|
const gitignoreCleaned = await cleanGitignore(rootDir);
|
|
2262
2308
|
if (gitignoreCleaned) {
|
|
2263
2309
|
console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
|
|
2264
2310
|
}
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
console.log(pc2.green(" \u2713 Removed brakit from devDependencies"));
|
|
2311
|
+
const cacheCleared = await clearBuildCaches(rootDir);
|
|
2312
|
+
if (cacheCleared) {
|
|
2313
|
+
console.log(pc2.green(" \u2713 Cleared build cache"));
|
|
2269
2314
|
}
|
|
2270
2315
|
console.log();
|
|
2271
2316
|
}
|
|
2272
2317
|
});
|
|
2318
|
+
async function removeInstrumentation(projectDir) {
|
|
2319
|
+
for (const relPath of CREATED_FILES) {
|
|
2320
|
+
const result2 = await tryRemoveBrakitFromFile(projectDir, relPath);
|
|
2321
|
+
if (result2) return result2;
|
|
2322
|
+
}
|
|
2323
|
+
const candidates = [...PREPENDED_FILES];
|
|
2324
|
+
try {
|
|
2325
|
+
const pkgRaw = await readFile5(join4(projectDir, "package.json"), "utf-8");
|
|
2326
|
+
const pkg = JSON.parse(pkgRaw);
|
|
2327
|
+
if (pkg.main) candidates.unshift(pkg.main);
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
brakitDebug(`uninstall: no package.json main: ${getErrorMessage(err)}`);
|
|
2330
|
+
}
|
|
2331
|
+
for (const relPath of candidates) {
|
|
2332
|
+
const result2 = await tryRemoveImportLine(projectDir, relPath);
|
|
2333
|
+
if (result2) return result2;
|
|
2334
|
+
}
|
|
2335
|
+
const result = await fallbackSearchAndRemove(projectDir);
|
|
2336
|
+
if (result) return result;
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
async function tryRemoveBrakitFromFile(projectDir, relPath) {
|
|
2340
|
+
const absPath = join4(projectDir, relPath);
|
|
2341
|
+
if (!await fileExists(absPath)) return null;
|
|
2342
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2343
|
+
if (!content.includes("brakit")) return null;
|
|
2344
|
+
if (isExactBrakitTemplate(content)) {
|
|
2345
|
+
await unlink(absPath);
|
|
2346
|
+
return `Removed ${relPath}`;
|
|
2347
|
+
}
|
|
2348
|
+
const lines = content.split("\n");
|
|
2349
|
+
const cleaned = removeBrakitImportLines(lines);
|
|
2350
|
+
if (cleaned.length < lines.length) {
|
|
2351
|
+
await writeFile4(absPath, cleaned.join("\n"));
|
|
2352
|
+
return `Removed brakit lines from ${relPath}`;
|
|
2353
|
+
}
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
async function tryRemoveImportLine(projectDir, relPath) {
|
|
2357
|
+
const absPath = join4(projectDir, relPath);
|
|
2358
|
+
if (!await fileExists(absPath)) return null;
|
|
2359
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2360
|
+
if (!content.includes(IMPORT_LINE)) return null;
|
|
2361
|
+
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
2362
|
+
await writeFile4(absPath, updated);
|
|
2363
|
+
return `Removed brakit import from ${relPath}`;
|
|
2364
|
+
}
|
|
2365
|
+
async function fallbackSearchAndRemove(projectDir) {
|
|
2366
|
+
const dirsToScan = FALLBACK_SCAN_DIRS;
|
|
2367
|
+
for (const dir of dirsToScan) {
|
|
2368
|
+
const absDir = join4(projectDir, dir);
|
|
2369
|
+
if (!await fileExists(absDir)) continue;
|
|
2370
|
+
let entries;
|
|
2371
|
+
try {
|
|
2372
|
+
entries = await readdir2(absDir);
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
brakitDebug(`uninstall: could not read ${absDir}: ${getErrorMessage(err)}`);
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
for (const entry of entries) {
|
|
2378
|
+
const ext = entry.slice(entry.lastIndexOf("."));
|
|
2379
|
+
if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext)) continue;
|
|
2380
|
+
const relPath = dir === "." ? entry : `${dir}/${entry}`;
|
|
2381
|
+
const absPath = join4(projectDir, relPath);
|
|
2382
|
+
try {
|
|
2383
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2384
|
+
if (!containsBrakitImport(content)) continue;
|
|
2385
|
+
if (isExactBrakitTemplate(content)) {
|
|
2386
|
+
await unlink(absPath);
|
|
2387
|
+
return `Removed ${relPath}`;
|
|
2388
|
+
}
|
|
2389
|
+
const lines = content.split("\n");
|
|
2390
|
+
const cleaned = removeBrakitImportLines(lines);
|
|
2391
|
+
if (cleaned.length < lines.length) {
|
|
2392
|
+
await writeFile4(absPath, cleaned.join("\n"));
|
|
2393
|
+
return `Removed brakit import from ${relPath}`;
|
|
2394
|
+
}
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
brakitDebug(`uninstall: fallback scan failed for ${relPath}: ${getErrorMessage(err)}`);
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2273
2403
|
async function removeMcpConfig(rootDir) {
|
|
2274
|
-
const mcpPath =
|
|
2404
|
+
const mcpPath = join4(rootDir, ".mcp.json");
|
|
2275
2405
|
if (!await fileExists(mcpPath)) return false;
|
|
2276
2406
|
try {
|
|
2277
2407
|
const raw = await readFile5(mcpPath, "utf-8");
|
|
@@ -2284,16 +2414,18 @@ async function removeMcpConfig(rootDir) {
|
|
|
2284
2414
|
await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
2285
2415
|
}
|
|
2286
2416
|
return true;
|
|
2287
|
-
} catch {
|
|
2417
|
+
} catch (err) {
|
|
2418
|
+
brakitDebug(`uninstall: MCP config cleanup failed: ${getErrorMessage(err)}`);
|
|
2288
2419
|
return false;
|
|
2289
2420
|
}
|
|
2290
2421
|
}
|
|
2291
2422
|
async function uninstallPackage(rootDir, pm) {
|
|
2292
2423
|
try {
|
|
2293
|
-
const pkgRaw = await readFile5(
|
|
2424
|
+
const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
|
|
2294
2425
|
const pkg = JSON.parse(pkgRaw);
|
|
2295
2426
|
if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
|
|
2296
|
-
} catch {
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
brakitDebug(`uninstall: could not read package.json: ${getErrorMessage(err)}`);
|
|
2297
2429
|
return false;
|
|
2298
2430
|
}
|
|
2299
2431
|
const cmds = {
|
|
@@ -2305,23 +2437,36 @@ async function uninstallPackage(rootDir, pm) {
|
|
|
2305
2437
|
const cmd = cmds[pm] ?? cmds.npm;
|
|
2306
2438
|
try {
|
|
2307
2439
|
execSync2(cmd, { cwd: rootDir, stdio: "pipe" });
|
|
2440
|
+
return true;
|
|
2308
2441
|
} catch {
|
|
2309
2442
|
console.warn(pc2.yellow(` \u26A0 Failed to run "${cmd}". Remove brakit manually.`));
|
|
2443
|
+
return "failed";
|
|
2310
2444
|
}
|
|
2311
|
-
return true;
|
|
2312
2445
|
}
|
|
2313
2446
|
async function removeBrakitData(rootDir) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2447
|
+
let removed = false;
|
|
2448
|
+
const projectDir = join4(rootDir, METRICS_DIR);
|
|
2449
|
+
if (await fileExists(projectDir)) {
|
|
2450
|
+
try {
|
|
2451
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
2452
|
+
removed = true;
|
|
2453
|
+
} catch (err) {
|
|
2454
|
+
brakitDebug(`uninstall: could not remove ${projectDir}: ${getErrorMessage(err)}`);
|
|
2455
|
+
}
|
|
2321
2456
|
}
|
|
2457
|
+
const homeDataDir = getProjectDataDir(rootDir);
|
|
2458
|
+
if (await fileExists(homeDataDir)) {
|
|
2459
|
+
try {
|
|
2460
|
+
await rm(homeDataDir, { recursive: true, force: true });
|
|
2461
|
+
removed = true;
|
|
2462
|
+
} catch (err) {
|
|
2463
|
+
brakitDebug(`uninstall: could not remove ${homeDataDir}: ${getErrorMessage(err)}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return removed;
|
|
2322
2467
|
}
|
|
2323
2468
|
async function cleanGitignore(rootDir) {
|
|
2324
|
-
const gitignorePath =
|
|
2469
|
+
const gitignorePath = join4(rootDir, ".gitignore");
|
|
2325
2470
|
if (!await fileExists(gitignorePath)) return false;
|
|
2326
2471
|
try {
|
|
2327
2472
|
const content = await readFile5(gitignorePath, "utf-8");
|
|
@@ -2330,10 +2475,25 @@ async function cleanGitignore(rootDir) {
|
|
|
2330
2475
|
if (filtered.length === lines.length) return false;
|
|
2331
2476
|
await writeFile4(gitignorePath, filtered.join("\n"));
|
|
2332
2477
|
return true;
|
|
2333
|
-
} catch {
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
brakitDebug(`uninstall: gitignore cleanup failed: ${getErrorMessage(err)}`);
|
|
2334
2480
|
return false;
|
|
2335
2481
|
}
|
|
2336
2482
|
}
|
|
2483
|
+
async function clearBuildCaches(rootDir) {
|
|
2484
|
+
let cleared = false;
|
|
2485
|
+
for (const dir of BUILD_CACHE_DIRS) {
|
|
2486
|
+
const absDir = join4(rootDir, dir);
|
|
2487
|
+
if (!await fileExists(absDir)) continue;
|
|
2488
|
+
try {
|
|
2489
|
+
await rm(absDir, { recursive: true, force: true });
|
|
2490
|
+
cleared = true;
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
brakitDebug(`uninstall: could not clear cache ${absDir}: ${getErrorMessage(err)}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return cleared;
|
|
2496
|
+
}
|
|
2337
2497
|
|
|
2338
2498
|
// bin/brakit.ts
|
|
2339
2499
|
var sub = process.argv[2];
|