brakit 0.8.7 → 9.0.0

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.
@@ -9,10 +9,10 @@ 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"() {
12
+ // src/constants/config.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, OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS, METRICS_DIR, PORT_FILE, VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
14
+ var init_config = __esm({
15
+ "src/constants/config.ts"() {
16
16
  "use strict";
17
17
  PROJECT_HASH_LENGTH = 8;
18
18
  SECRET_SCAN_ARRAY_LIMIT = 5;
@@ -22,6 +22,13 @@ var init_limits = __esm({
22
22
  LIST_PII_MIN_ITEMS = 2;
23
23
  MAX_OBJECT_SCAN_DEPTH = 5;
24
24
  ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
25
+ OVERFETCH_UNWRAP_MIN_SIZE = 3;
26
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
27
+ METRICS_DIR = ".brakit";
28
+ PORT_FILE = ".brakit/port";
29
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
30
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
31
+ VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
25
32
  }
26
33
  });
27
34
 
@@ -40,17 +47,6 @@ var init_log = __esm({
40
47
  }
41
48
  });
42
49
 
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
50
  // src/utils/type-guards.ts
55
51
  function isNonEmptyString(val) {
56
52
  return typeof val === "string" && val.trim().length > 0;
@@ -69,35 +65,14 @@ function isValidAiFixStatus(val) {
69
65
  var init_type_guards = __esm({
70
66
  "src/utils/type-guards.ts"() {
71
67
  "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;
68
+ init_config();
94
69
  }
95
70
  });
96
71
 
97
- // src/constants/routes.ts
72
+ // src/constants/labels.ts
98
73
  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;
99
- var init_routes = __esm({
100
- "src/constants/routes.ts"() {
74
+ var init_labels = __esm({
75
+ "src/constants/labels.ts"() {
101
76
  "use strict";
102
77
  DASHBOARD_PREFIX = "/__brakit";
103
78
  DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
@@ -132,75 +107,10 @@ var init_routes = __esm({
132
107
  }
133
108
  });
134
109
 
135
- // src/constants/transport.ts
136
- var init_transport = __esm({
137
- "src/constants/transport.ts"() {
138
- "use strict";
139
- }
140
- });
141
-
142
- // src/constants/headers.ts
143
- var init_headers = __esm({
144
- "src/constants/headers.ts"() {
145
- "use strict";
146
- }
147
- });
148
-
149
- // src/constants/network.ts
150
- var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
151
- var init_network = __esm({
152
- "src/constants/network.ts"() {
153
- "use strict";
154
- RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
155
- PORT_MIN = 1;
156
- PORT_MAX = 65535;
157
- }
158
- });
159
-
160
- // src/constants/mcp.ts
161
- 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;
162
- var init_mcp = __esm({
163
- "src/constants/mcp.ts"() {
164
- "use strict";
165
- MCP_SERVER_NAME = "brakit";
166
- INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
167
- LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
168
- CLIENT_FETCH_TIMEOUT_MS = 1e4;
169
- HEALTH_CHECK_TIMEOUT_MS = 3e3;
170
- DISCOVERY_POLL_INTERVAL_MS = 500;
171
- MAX_DISCOVERY_DEPTH = 5;
172
- MAX_TIMELINE_EVENTS = 20;
173
- MAX_RESOLVED_DISPLAY = 5;
174
- ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
175
- MCP_SERVER_VERSION = "0.8.7";
176
- }
177
- });
178
-
179
- // src/constants/encoding.ts
180
- var init_encoding = __esm({
181
- "src/constants/encoding.ts"() {
182
- "use strict";
183
- }
184
- });
185
-
186
- // src/constants/severity.ts
187
- var init_severity = __esm({
188
- "src/constants/severity.ts"() {
189
- "use strict";
190
- }
191
- });
192
-
193
- // src/constants/telemetry.ts
194
- var init_telemetry = __esm({
195
- "src/constants/telemetry.ts"() {
196
- "use strict";
197
- }
198
- });
199
-
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"() {
110
+ // src/constants/features.ts
111
+ var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS, 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, RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
112
+ var init_features = __esm({
113
+ "src/constants/features.ts"() {
204
114
  "use strict";
205
115
  SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
206
116
  ".ts",
@@ -212,20 +122,20 @@ var init_cli = __esm({
212
122
  ]);
213
123
  BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
214
124
  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"() {
228
- "use strict";
125
+ MCP_SERVER_NAME = "brakit";
126
+ INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
127
+ LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
128
+ CLIENT_FETCH_TIMEOUT_MS = 1e4;
129
+ HEALTH_CHECK_TIMEOUT_MS = 3e3;
130
+ DISCOVERY_POLL_INTERVAL_MS = 500;
131
+ MAX_DISCOVERY_DEPTH = 5;
132
+ MAX_TIMELINE_EVENTS = 20;
133
+ MAX_RESOLVED_DISPLAY = 5;
134
+ ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
135
+ MCP_SERVER_VERSION = "9.0.0";
136
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
137
+ PORT_MIN = 1;
138
+ PORT_MAX = 65535;
229
139
  }
230
140
  });
231
141
 
@@ -233,21 +143,9 @@ var init_sdk_events = __esm({
233
143
  var init_constants = __esm({
234
144
  "src/constants/index.ts"() {
235
145
  "use strict";
236
- init_routes();
237
- init_limits();
238
- init_thresholds();
239
- init_transport();
240
- init_metrics();
241
- init_headers();
242
- init_network();
243
- init_mcp();
244
- init_encoding();
245
- init_severity();
246
- init_telemetry();
247
- init_lifecycle();
248
- init_cli();
249
- init_timeline();
250
- init_sdk_events();
146
+ init_config();
147
+ init_labels();
148
+ init_features();
251
149
  }
252
150
  });
253
151
 
@@ -270,8 +168,8 @@ var BrakitClient;
270
168
  var init_client = __esm({
271
169
  "src/mcp/client.ts"() {
272
170
  "use strict";
273
- init_routes();
274
- init_mcp();
171
+ init_labels();
172
+ init_features();
275
173
  BrakitClient = class {
276
174
  constructor(baseUrl) {
277
175
  this.baseUrl = baseUrl;
@@ -438,7 +336,7 @@ var init_discovery = __esm({
438
336
  "use strict";
439
337
  init_constants();
440
338
  init_log();
441
- init_mcp();
339
+ init_features();
442
340
  }
443
341
  });
444
342
 
@@ -539,7 +437,7 @@ async function buildRequestDetail(client, req) {
539
437
  var init_enrichment = __esm({
540
438
  "src/mcp/enrichment.ts"() {
541
439
  "use strict";
542
- init_mcp();
440
+ init_features();
543
441
  init_endpoint();
544
442
  }
545
443
  });
@@ -550,7 +448,7 @@ var init_get_findings = __esm({
550
448
  "src/mcp/tools/get-findings.ts"() {
551
449
  "use strict";
552
450
  init_enrichment();
553
- init_lifecycle();
451
+ init_config();
554
452
  init_type_guards();
555
453
  getFindings = {
556
454
  name: "get_findings",
@@ -665,7 +563,7 @@ var getRequestDetail;
665
563
  var init_get_request_detail = __esm({
666
564
  "src/mcp/tools/get-request-detail.ts"() {
667
565
  "use strict";
668
- init_mcp();
566
+ init_features();
669
567
  init_enrichment();
670
568
  getRequestDetail = {
671
569
  name: "get_request_detail",
@@ -845,7 +743,7 @@ var getReport;
845
743
  var init_get_report = __esm({
846
744
  "src/mcp/tools/get-report.ts"() {
847
745
  "use strict";
848
- init_mcp();
746
+ init_features();
849
747
  getReport = {
850
748
  name: "get_report",
851
749
  description: "Generate a summary report of all findings: total found, open, resolved. Use this to get a high-level overview of the application's health.",
@@ -1142,7 +1040,7 @@ var init_server = __esm({
1142
1040
  init_client();
1143
1041
  init_discovery();
1144
1042
  init_tools();
1145
- init_mcp();
1043
+ init_features();
1146
1044
  init_prompts();
1147
1045
  }
1148
1046
  });
@@ -1164,7 +1062,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync }
1164
1062
  import { resolve as resolve2 } from "path";
1165
1063
 
1166
1064
  // src/utils/fs.ts
1167
- init_limits();
1065
+ init_config();
1168
1066
  init_log();
1169
1067
  init_type_guards();
1170
1068
  import { access, readFile, writeFile } from "fs/promises";
@@ -1187,10 +1085,7 @@ async function fileExists(path) {
1187
1085
  }
1188
1086
 
1189
1087
  // src/store/issue-store.ts
1190
- init_metrics();
1191
- init_limits();
1192
- init_thresholds();
1193
- init_limits();
1088
+ init_config();
1194
1089
 
1195
1090
  // src/utils/atomic-writer.ts
1196
1091
  import {
@@ -1208,7 +1103,7 @@ init_log();
1208
1103
  init_type_guards();
1209
1104
 
1210
1105
  // src/utils/issue-id.ts
1211
- init_limits();
1106
+ init_config();
1212
1107
  import { createHash as createHash2 } from "crypto";
1213
1108
 
1214
1109
  // src/detect/project.ts
@@ -1378,12 +1273,13 @@ async function detectInDir(dir, rootDir, projects) {
1378
1273
  }
1379
1274
 
1380
1275
  // src/utils/response.ts
1381
- init_thresholds();
1276
+ init_config();
1277
+ var MAX_WRAPPER_KEYS = 3;
1382
1278
  function unwrapResponse(parsed) {
1383
1279
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1384
1280
  const obj = parsed;
1385
1281
  const keys = Object.keys(obj);
1386
- if (keys.length > 3) return parsed;
1282
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
1387
1283
  let best = null;
1388
1284
  let bestSize = 0;
1389
1285
  for (const key of keys) {
@@ -1402,6 +1298,10 @@ function unwrapResponse(parsed) {
1402
1298
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1403
1299
  }
1404
1300
 
1301
+ // src/analysis/rules/scanner.ts
1302
+ init_log();
1303
+ init_type_guards();
1304
+
1405
1305
  // src/analysis/rules/patterns.ts
1406
1306
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1407
1307
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -1428,8 +1328,8 @@ var RULE_HINTS = {
1428
1328
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
1429
1329
  };
1430
1330
 
1431
- // src/analysis/rules/exposed-secret.ts
1432
- init_limits();
1331
+ // src/analysis/rules/auth-rules.ts
1332
+ init_config();
1433
1333
 
1434
1334
  // src/utils/http-status.ts
1435
1335
  function isErrorStatus(code) {
@@ -1439,27 +1339,66 @@ function isRedirect(code) {
1439
1339
  return code >= 300 && code < 400;
1440
1340
  }
1441
1341
 
1442
- // src/analysis/rules/exposed-secret.ts
1443
- function findSecretKeys(obj, prefix, depth = 0) {
1444
- const found = [];
1445
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1446
- if (!obj || typeof obj !== "object") return found;
1447
- if (Array.isArray(obj)) {
1448
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
1449
- found.push(...findSecretKeys(obj[i], prefix, depth + 1));
1342
+ // src/utils/collections.ts
1343
+ function deduplicateFindings(items, extract) {
1344
+ const seen = /* @__PURE__ */ new Map();
1345
+ const findings = [];
1346
+ for (const item of items) {
1347
+ const result = extract(item);
1348
+ if (!result) continue;
1349
+ const existing = seen.get(result.key);
1350
+ if (existing) {
1351
+ existing.count++;
1352
+ continue;
1450
1353
  }
1451
- return found;
1354
+ seen.set(result.key, result.finding);
1355
+ findings.push(result.finding);
1452
1356
  }
1453
- for (const k of Object.keys(obj)) {
1454
- const val = obj[k];
1455
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
1456
- found.push(k);
1357
+ return findings;
1358
+ }
1359
+
1360
+ // src/utils/object-scan.ts
1361
+ init_config();
1362
+ var DEFAULTS = {
1363
+ maxDepth: MAX_OBJECT_SCAN_DEPTH,
1364
+ arrayLimit: SECRET_SCAN_ARRAY_LIMIT
1365
+ };
1366
+ function walkObject(obj, visitor, options) {
1367
+ const opts = { ...DEFAULTS, ...options };
1368
+ walk(obj, visitor, opts, 0);
1369
+ }
1370
+ function walk(obj, visitor, opts, depth) {
1371
+ if (depth >= opts.maxDepth) return;
1372
+ if (!obj || typeof obj !== "object") return;
1373
+ if (Array.isArray(obj)) {
1374
+ for (let i = 0; i < Math.min(obj.length, opts.arrayLimit); i++) {
1375
+ walk(obj[i], visitor, opts, depth + 1);
1457
1376
  }
1377
+ return;
1378
+ }
1379
+ for (const key of Object.keys(obj)) {
1380
+ const val = obj[key];
1381
+ visitor(key, val, depth);
1458
1382
  if (typeof val === "object" && val !== null) {
1459
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1383
+ walk(val, visitor, opts, depth + 1);
1460
1384
  }
1461
1385
  }
1462
- return found;
1386
+ }
1387
+ function collectFromObject(obj, match, options) {
1388
+ const results = [];
1389
+ walkObject(obj, (key, value) => {
1390
+ const result = match(key, value);
1391
+ if (result !== null) results.push(result);
1392
+ }, options);
1393
+ return results;
1394
+ }
1395
+
1396
+ // src/analysis/rules/auth-rules.ts
1397
+ function findSecretKeys(obj) {
1398
+ return collectFromObject(
1399
+ obj,
1400
+ (key, val) => SECRET_KEYS.test(key) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val) ? key : null
1401
+ );
1463
1402
  }
1464
1403
  var exposedSecretRule = {
1465
1404
  id: "exposed-secret",
@@ -1467,50 +1406,38 @@ var exposedSecretRule = {
1467
1406
  name: "Exposed Secret in Response",
1468
1407
  hint: RULE_HINTS["exposed-secret"],
1469
1408
  check(ctx) {
1470
- const findings = [];
1471
- const seen = /* @__PURE__ */ new Map();
1472
- for (const r of ctx.requests) {
1473
- if (isErrorStatus(r.statusCode)) continue;
1474
- const parsed = ctx.parsedBodies.response.get(r.id);
1475
- if (!parsed) continue;
1476
- const keys = findSecretKeys(parsed, "");
1477
- if (keys.length === 0) continue;
1478
- const ep = `${r.method} ${r.path}`;
1479
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
1480
- const existing = seen.get(dedupKey);
1481
- if (existing) {
1482
- existing.count++;
1483
- continue;
1484
- }
1485
- const finding = {
1486
- severity: "critical",
1487
- rule: "exposed-secret",
1488
- title: "Exposed Secret in Response",
1489
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
1490
- hint: this.hint,
1491
- endpoint: ep,
1492
- count: 1
1409
+ return deduplicateFindings(ctx.requests, (request) => {
1410
+ if (isErrorStatus(request.statusCode)) return null;
1411
+ const parsed = ctx.parsedBodies.response.get(request.id);
1412
+ if (!parsed) return null;
1413
+ const keys = findSecretKeys(parsed);
1414
+ if (keys.length === 0) return null;
1415
+ const ep = `${request.method} ${request.path}`;
1416
+ return {
1417
+ key: `${ep}:${keys.sort().join(",")}`,
1418
+ finding: {
1419
+ severity: "critical",
1420
+ rule: "exposed-secret",
1421
+ title: "Exposed Secret in Response",
1422
+ desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
1423
+ hint: this.hint,
1424
+ endpoint: ep,
1425
+ count: 1
1426
+ }
1493
1427
  };
1494
- seen.set(dedupKey, finding);
1495
- findings.push(finding);
1496
- }
1497
- return findings;
1428
+ });
1498
1429
  }
1499
1430
  };
1500
-
1501
- // src/analysis/rules/token-in-url.ts
1502
1431
  var tokenInUrlRule = {
1503
1432
  id: "token-in-url",
1504
1433
  severity: "critical",
1505
1434
  name: "Auth Token in URL",
1506
1435
  hint: RULE_HINTS["token-in-url"],
1507
1436
  check(ctx) {
1508
- const findings = [];
1509
- const seen = /* @__PURE__ */ new Map();
1510
- for (const r of ctx.requests) {
1511
- const qIdx = r.url.indexOf("?");
1512
- if (qIdx === -1) continue;
1513
- const params = r.url.substring(qIdx + 1).split("&");
1437
+ return deduplicateFindings(ctx.requests, (request) => {
1438
+ const qIdx = request.url.indexOf("?");
1439
+ if (qIdx === -1) return null;
1440
+ const params = request.url.substring(qIdx + 1).split("&");
1514
1441
  const flagged = [];
1515
1442
  for (const param of params) {
1516
1443
  const [name, ...rest] = param.split("=");
@@ -1520,65 +1447,125 @@ var tokenInUrlRule = {
1520
1447
  flagged.push(name);
1521
1448
  }
1522
1449
  }
1523
- if (flagged.length === 0) continue;
1524
- const ep = `${r.method} ${r.path}`;
1525
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
1526
- const existing = seen.get(dedupKey);
1527
- if (existing) {
1528
- existing.count++;
1529
- continue;
1450
+ if (flagged.length === 0) return null;
1451
+ const ep = `${request.method} ${request.path}`;
1452
+ return {
1453
+ key: `${ep}:${flagged.sort().join(",")}`,
1454
+ finding: {
1455
+ severity: "critical",
1456
+ rule: "token-in-url",
1457
+ title: "Auth Token in URL",
1458
+ desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
1459
+ hint: this.hint,
1460
+ endpoint: ep,
1461
+ count: 1
1462
+ }
1463
+ };
1464
+ });
1465
+ }
1466
+ };
1467
+ function isFrameworkResponse(request) {
1468
+ if (isRedirect(request.statusCode)) return true;
1469
+ if (request.path?.startsWith("/__")) return true;
1470
+ if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
1471
+ return false;
1472
+ }
1473
+ var insecureCookieRule = {
1474
+ id: "insecure-cookie",
1475
+ severity: "warning",
1476
+ name: "Insecure Cookie",
1477
+ hint: RULE_HINTS["insecure-cookie"],
1478
+ check(ctx) {
1479
+ const cookieEntries = [];
1480
+ for (const request of ctx.requests) {
1481
+ if (!request.responseHeaders) continue;
1482
+ if (isFrameworkResponse(request)) continue;
1483
+ const setCookie = request.responseHeaders["set-cookie"];
1484
+ if (!setCookie) continue;
1485
+ const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1486
+ for (const cookie of cookies) {
1487
+ cookieEntries.push({ cookie });
1530
1488
  }
1531
- const finding = {
1532
- severity: "critical",
1533
- rule: "token-in-url",
1534
- title: "Auth Token in URL",
1535
- desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
1489
+ }
1490
+ return deduplicateFindings(cookieEntries, ({ cookie }) => {
1491
+ const cookieName = cookie.trim().split("=")[0].trim();
1492
+ const lower = cookie.toLowerCase();
1493
+ const issues = [];
1494
+ if (!lower.includes("httponly")) issues.push("HttpOnly");
1495
+ if (!lower.includes("samesite")) issues.push("SameSite");
1496
+ if (issues.length === 0) return null;
1497
+ return {
1498
+ key: `${cookieName}:${issues.join(",")}`,
1499
+ finding: {
1500
+ severity: "warning",
1501
+ rule: "insecure-cookie",
1502
+ title: "Insecure Cookie",
1503
+ desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1504
+ hint: this.hint,
1505
+ endpoint: cookieName,
1506
+ count: 1
1507
+ }
1508
+ };
1509
+ });
1510
+ }
1511
+ };
1512
+ var corsCredentialsRule = {
1513
+ id: "cors-credentials",
1514
+ severity: "warning",
1515
+ name: "CORS Credentials with Wildcard",
1516
+ hint: RULE_HINTS["cors-credentials"],
1517
+ check(ctx) {
1518
+ const findings = [];
1519
+ const seen = /* @__PURE__ */ new Set();
1520
+ for (const request of ctx.requests) {
1521
+ if (!request.responseHeaders) continue;
1522
+ const origin = request.responseHeaders["access-control-allow-origin"];
1523
+ const creds = request.responseHeaders["access-control-allow-credentials"];
1524
+ if (origin !== "*" || creds !== "true") continue;
1525
+ const ep = `${request.method} ${request.path}`;
1526
+ if (seen.has(ep)) continue;
1527
+ seen.add(ep);
1528
+ findings.push({
1529
+ severity: "warning",
1530
+ rule: "cors-credentials",
1531
+ title: "CORS Credentials with Wildcard",
1532
+ desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1536
1533
  hint: this.hint,
1537
1534
  endpoint: ep,
1538
1535
  count: 1
1539
- };
1540
- seen.set(dedupKey, finding);
1541
- findings.push(finding);
1536
+ });
1542
1537
  }
1543
1538
  return findings;
1544
1539
  }
1545
1540
  };
1546
1541
 
1547
- // src/analysis/rules/stack-trace-leak.ts
1542
+ // src/analysis/rules/data-rules.ts
1543
+ init_config();
1548
1544
  var stackTraceLeakRule = {
1549
1545
  id: "stack-trace-leak",
1550
1546
  severity: "critical",
1551
1547
  name: "Stack Trace Leaked to Client",
1552
1548
  hint: RULE_HINTS["stack-trace-leak"],
1553
1549
  check(ctx) {
1554
- const findings = [];
1555
- const seen = /* @__PURE__ */ new Map();
1556
- for (const r of ctx.requests) {
1557
- if (!r.responseBody) continue;
1558
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
1559
- const ep = `${r.method} ${r.path}`;
1560
- const existing = seen.get(ep);
1561
- if (existing) {
1562
- existing.count++;
1563
- continue;
1564
- }
1565
- const finding = {
1566
- severity: "critical",
1567
- rule: "stack-trace-leak",
1568
- title: "Stack Trace Leaked to Client",
1569
- desc: `${ep} \u2014 response exposes internal stack trace`,
1570
- hint: this.hint,
1571
- endpoint: ep,
1572
- count: 1
1550
+ return deduplicateFindings(ctx.requests, (request) => {
1551
+ if (!request.responseBody) return null;
1552
+ if (!STACK_TRACE_RE.test(request.responseBody)) return null;
1553
+ const ep = `${request.method} ${request.path}`;
1554
+ return {
1555
+ key: ep,
1556
+ finding: {
1557
+ severity: "critical",
1558
+ rule: "stack-trace-leak",
1559
+ title: "Stack Trace Leaked to Client",
1560
+ desc: `${ep} \u2014 response exposes internal stack trace`,
1561
+ hint: this.hint,
1562
+ endpoint: ep,
1563
+ count: 1
1564
+ }
1573
1565
  };
1574
- seen.set(ep, finding);
1575
- findings.push(finding);
1576
- }
1577
- return findings;
1566
+ });
1578
1567
  }
1579
1568
  };
1580
-
1581
- // src/analysis/rules/error-info-leak.ts
1582
1569
  var CRITICAL_PATTERNS = [
1583
1570
  { re: DB_CONN_RE, label: "database connection string" },
1584
1571
  { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
@@ -1590,90 +1577,34 @@ var errorInfoLeakRule = {
1590
1577
  name: "Sensitive Data in Error Response",
1591
1578
  hint: RULE_HINTS["error-info-leak"],
1592
1579
  check(ctx) {
1593
- const findings = [];
1594
- const seen = /* @__PURE__ */ new Map();
1595
- for (const r of ctx.requests) {
1596
- if (r.statusCode < 400) continue;
1597
- if (!r.responseBody) continue;
1598
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
1599
- const ep = `${r.method} ${r.path}`;
1600
- for (const p of CRITICAL_PATTERNS) {
1601
- if (!p.re.test(r.responseBody)) continue;
1602
- const dedupKey = `${ep}:${p.label}`;
1603
- const existing = seen.get(dedupKey);
1604
- if (existing) {
1605
- existing.count++;
1606
- continue;
1580
+ const entries = [];
1581
+ for (const request of ctx.requests) {
1582
+ if (request.statusCode < 400) continue;
1583
+ if (!request.responseBody) continue;
1584
+ if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
1585
+ const ep = `${request.method} ${request.path}`;
1586
+ for (const pattern of CRITICAL_PATTERNS) {
1587
+ if (pattern.re.test(request.responseBody)) {
1588
+ entries.push({ ep, pattern, body: request.responseBody });
1607
1589
  }
1608
- const finding = {
1590
+ }
1591
+ }
1592
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
1593
+ return {
1594
+ key: `${ep}:${pattern.label}`,
1595
+ finding: {
1609
1596
  severity: "critical",
1610
1597
  rule: "error-info-leak",
1611
1598
  title: "Sensitive Data in Error Response",
1612
- desc: `${ep} \u2014 error response exposes ${p.label}`,
1599
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
1613
1600
  hint: this.hint,
1614
1601
  endpoint: ep,
1615
1602
  count: 1
1616
- };
1617
- seen.set(dedupKey, finding);
1618
- findings.push(finding);
1619
- }
1620
- }
1621
- return findings;
1622
- }
1623
- };
1624
-
1625
- // src/analysis/rules/insecure-cookie.ts
1626
- function isFrameworkResponse(r) {
1627
- if (isRedirect(r.statusCode)) return true;
1628
- if (r.path?.startsWith("/__")) return true;
1629
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1630
- return false;
1631
- }
1632
- var insecureCookieRule = {
1633
- id: "insecure-cookie",
1634
- severity: "warning",
1635
- name: "Insecure Cookie",
1636
- hint: RULE_HINTS["insecure-cookie"],
1637
- check(ctx) {
1638
- const findings = [];
1639
- const seen = /* @__PURE__ */ new Map();
1640
- for (const r of ctx.requests) {
1641
- if (!r.responseHeaders) continue;
1642
- if (isFrameworkResponse(r)) continue;
1643
- const setCookie = r.responseHeaders["set-cookie"];
1644
- if (!setCookie) continue;
1645
- const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1646
- for (const cookie of cookies) {
1647
- const cookieName = cookie.trim().split("=")[0].trim();
1648
- const lower = cookie.toLowerCase();
1649
- const issues = [];
1650
- if (!lower.includes("httponly")) issues.push("HttpOnly");
1651
- if (!lower.includes("samesite")) issues.push("SameSite");
1652
- if (issues.length === 0) continue;
1653
- const dedupKey = `${cookieName}:${issues.join(",")}`;
1654
- const existing = seen.get(dedupKey);
1655
- if (existing) {
1656
- existing.count++;
1657
- continue;
1658
1603
  }
1659
- const finding = {
1660
- severity: "warning",
1661
- rule: "insecure-cookie",
1662
- title: "Insecure Cookie",
1663
- desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1664
- hint: this.hint,
1665
- endpoint: cookieName,
1666
- count: 1
1667
- };
1668
- seen.set(dedupKey, finding);
1669
- findings.push(finding);
1670
- }
1671
- }
1672
- return findings;
1604
+ };
1605
+ });
1673
1606
  }
1674
1607
  };
1675
-
1676
- // src/analysis/rules/sensitive-logs.ts
1677
1608
  var sensitiveLogsRule = {
1678
1609
  id: "sensitive-logs",
1679
1610
  severity: "warning",
@@ -1698,59 +1629,13 @@ var sensitiveLogsRule = {
1698
1629
  }];
1699
1630
  }
1700
1631
  };
1701
-
1702
- // src/analysis/rules/cors-credentials.ts
1703
- var corsCredentialsRule = {
1704
- id: "cors-credentials",
1705
- severity: "warning",
1706
- name: "CORS Credentials with Wildcard",
1707
- hint: RULE_HINTS["cors-credentials"],
1708
- check(ctx) {
1709
- const findings = [];
1710
- const seen = /* @__PURE__ */ new Set();
1711
- for (const r of ctx.requests) {
1712
- if (!r.responseHeaders) continue;
1713
- const origin = r.responseHeaders["access-control-allow-origin"];
1714
- const creds = r.responseHeaders["access-control-allow-credentials"];
1715
- if (origin !== "*" || creds !== "true") continue;
1716
- const ep = `${r.method} ${r.path}`;
1717
- if (seen.has(ep)) continue;
1718
- seen.add(ep);
1719
- findings.push({
1720
- severity: "warning",
1721
- rule: "cors-credentials",
1722
- title: "CORS Credentials with Wildcard",
1723
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1724
- hint: this.hint,
1725
- endpoint: ep,
1726
- count: 1
1727
- });
1728
- }
1729
- return findings;
1730
- }
1731
- };
1732
-
1733
- // src/analysis/rules/response-pii-leak.ts
1734
- init_limits();
1735
1632
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1736
- function findEmails(obj, depth = 0) {
1737
- const emails = [];
1738
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1739
- if (!obj || typeof obj !== "object") return emails;
1740
- if (Array.isArray(obj)) {
1741
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
1742
- emails.push(...findEmails(obj[i], depth + 1));
1743
- }
1744
- return emails;
1745
- }
1746
- for (const v of Object.values(obj)) {
1747
- if (typeof v === "string" && EMAIL_RE.test(v)) {
1748
- emails.push(v);
1749
- } else if (typeof v === "object" && v !== null) {
1750
- emails.push(...findEmails(v, depth + 1));
1751
- }
1752
- }
1753
- return emails;
1633
+ function findEmails(obj) {
1634
+ return collectFromObject(
1635
+ obj,
1636
+ (_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
1637
+ { arrayLimit: PII_SCAN_ARRAY_LIMIT }
1638
+ );
1754
1639
  }
1755
1640
  function topLevelFieldCount(obj) {
1756
1641
  if (Array.isArray(obj)) {
@@ -1835,101 +1720,78 @@ var responsePiiLeakRule = {
1835
1720
  name: "PII Leak in Response",
1836
1721
  hint: RULE_HINTS["response-pii-leak"],
1837
1722
  check(ctx) {
1838
- const findings = [];
1839
- const seen = /* @__PURE__ */ new Map();
1840
- for (const r of ctx.requests) {
1841
- if (isErrorStatus(r.statusCode)) continue;
1842
- if (SELF_SERVICE_PATH.test(r.path)) continue;
1843
- const resJson = ctx.parsedBodies.response.get(r.id);
1844
- if (!resJson) continue;
1845
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1846
- const detection = detectPII(r.method, reqJson, resJson);
1847
- if (!detection) continue;
1848
- const ep = `${r.method} ${r.path}`;
1849
- const existing = seen.get(ep);
1850
- if (existing) {
1851
- existing.count++;
1852
- continue;
1853
- }
1854
- const finding = {
1855
- severity: "warning",
1856
- rule: "response-pii-leak",
1857
- title: "PII Leak in Response",
1858
- desc: `${ep} \u2014 exposes PII in response`,
1859
- hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
1860
- endpoint: ep,
1861
- count: 1
1723
+ return deduplicateFindings(ctx.requests, (request) => {
1724
+ if (isErrorStatus(request.statusCode)) return null;
1725
+ if (SELF_SERVICE_PATH.test(request.path)) return null;
1726
+ const resJson = ctx.parsedBodies.response.get(request.id);
1727
+ if (!resJson) return null;
1728
+ const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
1729
+ const detection = detectPII(request.method, reqJson, resJson);
1730
+ if (!detection) return null;
1731
+ const ep = `${request.method} ${request.path}`;
1732
+ return {
1733
+ key: ep,
1734
+ finding: {
1735
+ severity: "warning",
1736
+ rule: "response-pii-leak",
1737
+ title: "PII Leak in Response",
1738
+ desc: `${ep} \u2014 exposes PII in response`,
1739
+ hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
1740
+ endpoint: ep,
1741
+ count: 1
1742
+ }
1862
1743
  };
1863
- seen.set(ep, finding);
1864
- findings.push(finding);
1865
- }
1866
- return findings;
1744
+ });
1867
1745
  }
1868
1746
  };
1869
1747
 
1870
1748
  // src/analysis/engine.ts
1871
- init_limits();
1749
+ init_config();
1872
1750
 
1873
1751
  // src/analysis/group.ts
1874
1752
  init_constants();
1875
1753
  import { randomUUID } from "crypto";
1754
+ init_endpoint();
1876
1755
 
1877
1756
  // src/analysis/label.ts
1878
1757
  init_constants();
1879
1758
 
1880
1759
  // src/analysis/transforms.ts
1881
1760
  init_constants();
1761
+ init_config();
1762
+ init_endpoint();
1882
1763
 
1883
1764
  // src/analysis/insights/prepare.ts
1884
1765
  init_endpoint();
1885
1766
  init_constants();
1886
- init_thresholds();
1767
+ init_config();
1887
1768
 
1888
- // src/analysis/insights/rules/n1.ts
1889
- init_endpoint();
1890
- init_constants();
1769
+ // src/analysis/insights/runner.ts
1770
+ init_log();
1771
+ init_type_guards();
1891
1772
 
1892
- // src/analysis/insights/rules/cross-endpoint.ts
1773
+ // src/analysis/insights/rules/query-rules.ts
1893
1774
  init_endpoint();
1894
1775
  init_constants();
1895
1776
 
1896
- // src/analysis/insights/rules/redundant-query.ts
1777
+ // src/analysis/insights/rules/response-rules.ts
1897
1778
  init_endpoint();
1779
+ init_log();
1780
+ init_type_guards();
1898
1781
  init_constants();
1899
1782
 
1900
- // src/analysis/insights/rules/error-hotspot.ts
1901
- init_constants();
1902
-
1903
- // src/analysis/insights/rules/duplicate.ts
1904
- init_constants();
1905
-
1906
- // src/analysis/insights/rules/slow.ts
1907
- init_constants();
1908
-
1909
- // src/analysis/insights/rules/query-heavy.ts
1910
- init_constants();
1911
-
1912
- // src/analysis/insights/rules/select-star.ts
1913
- init_constants();
1914
-
1915
- // src/analysis/insights/rules/high-rows.ts
1783
+ // src/analysis/insights/rules/reliability-rules.ts
1916
1784
  init_constants();
1917
1785
 
1918
- // src/analysis/insights/rules/response-overfetch.ts
1786
+ // src/analysis/insights/rules/pattern-rules.ts
1919
1787
  init_endpoint();
1920
1788
  init_constants();
1921
1789
 
1922
- // src/analysis/insights/rules/large-response.ts
1923
- init_constants();
1924
-
1925
- // src/analysis/insights/rules/regression.ts
1926
- init_constants();
1927
-
1928
1790
  // src/analysis/issue-mappers.ts
1929
1791
  init_endpoint();
1930
1792
 
1931
1793
  // src/index.ts
1932
- var VERSION = "0.8.7";
1794
+ var VERSION = "9.0.0";
1933
1795
 
1934
1796
  // src/cli/commands/install.ts
1935
1797
  init_constants();