brakit 0.8.6 → 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,19 +9,26 @@ 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;
19
19
  PII_SCAN_ARRAY_LIMIT = 10;
20
20
  MIN_SECRET_VALUE_LENGTH = 8;
21
- FULL_RECORD_MIN_FIELDS = 5;
21
+ FULL_RECORD_MIN_FIELDS = 8;
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();
68
+ init_config();
74
69
  }
75
70
  });
76
71
 
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
-
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.6";
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,6 +122,20 @@ var init_cli = __esm({
212
122
  ]);
213
123
  BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
214
124
  FALLBACK_SCAN_DIRS = ["src", "."];
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;
215
139
  }
216
140
  });
217
141
 
@@ -219,19 +143,9 @@ var init_cli = __esm({
219
143
  var init_constants = __esm({
220
144
  "src/constants/index.ts"() {
221
145
  "use strict";
222
- init_routes();
223
- init_limits();
224
- init_thresholds();
225
- init_transport();
226
- init_metrics();
227
- init_headers();
228
- init_network();
229
- init_mcp();
230
- init_encoding();
231
- init_severity();
232
- init_telemetry();
233
- init_lifecycle();
234
- init_cli();
146
+ init_config();
147
+ init_labels();
148
+ init_features();
235
149
  }
236
150
  });
237
151
 
@@ -254,8 +168,8 @@ var BrakitClient;
254
168
  var init_client = __esm({
255
169
  "src/mcp/client.ts"() {
256
170
  "use strict";
257
- init_routes();
258
- init_mcp();
171
+ init_labels();
172
+ init_features();
259
173
  BrakitClient = class {
260
174
  constructor(baseUrl) {
261
175
  this.baseUrl = baseUrl;
@@ -422,7 +336,7 @@ var init_discovery = __esm({
422
336
  "use strict";
423
337
  init_constants();
424
338
  init_log();
425
- init_mcp();
339
+ init_features();
426
340
  }
427
341
  });
428
342
 
@@ -523,7 +437,7 @@ async function buildRequestDetail(client, req) {
523
437
  var init_enrichment = __esm({
524
438
  "src/mcp/enrichment.ts"() {
525
439
  "use strict";
526
- init_mcp();
440
+ init_features();
527
441
  init_endpoint();
528
442
  }
529
443
  });
@@ -534,7 +448,7 @@ var init_get_findings = __esm({
534
448
  "src/mcp/tools/get-findings.ts"() {
535
449
  "use strict";
536
450
  init_enrichment();
537
- init_lifecycle();
451
+ init_config();
538
452
  init_type_guards();
539
453
  getFindings = {
540
454
  name: "get_findings",
@@ -649,7 +563,7 @@ var getRequestDetail;
649
563
  var init_get_request_detail = __esm({
650
564
  "src/mcp/tools/get-request-detail.ts"() {
651
565
  "use strict";
652
- init_mcp();
566
+ init_features();
653
567
  init_enrichment();
654
568
  getRequestDetail = {
655
569
  name: "get_request_detail",
@@ -829,7 +743,7 @@ var getReport;
829
743
  var init_get_report = __esm({
830
744
  "src/mcp/tools/get-report.ts"() {
831
745
  "use strict";
832
- init_mcp();
746
+ init_features();
833
747
  getReport = {
834
748
  name: "get_report",
835
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.",
@@ -1126,7 +1040,7 @@ var init_server = __esm({
1126
1040
  init_client();
1127
1041
  init_discovery();
1128
1042
  init_tools();
1129
- init_mcp();
1043
+ init_features();
1130
1044
  init_prompts();
1131
1045
  }
1132
1046
  });
@@ -1148,7 +1062,7 @@ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync }
1148
1062
  import { resolve as resolve2 } from "path";
1149
1063
 
1150
1064
  // src/utils/fs.ts
1151
- init_limits();
1065
+ init_config();
1152
1066
  init_log();
1153
1067
  init_type_guards();
1154
1068
  import { access, readFile, writeFile } from "fs/promises";
@@ -1171,10 +1085,7 @@ async function fileExists(path) {
1171
1085
  }
1172
1086
 
1173
1087
  // src/store/issue-store.ts
1174
- init_metrics();
1175
- init_limits();
1176
- init_thresholds();
1177
- init_limits();
1088
+ init_config();
1178
1089
 
1179
1090
  // src/utils/atomic-writer.ts
1180
1091
  import {
@@ -1192,7 +1103,7 @@ init_log();
1192
1103
  init_type_guards();
1193
1104
 
1194
1105
  // src/utils/issue-id.ts
1195
- init_limits();
1106
+ init_config();
1196
1107
  import { createHash as createHash2 } from "crypto";
1197
1108
 
1198
1109
  // src/detect/project.ts
@@ -1362,12 +1273,13 @@ async function detectInDir(dir, rootDir, projects) {
1362
1273
  }
1363
1274
 
1364
1275
  // src/utils/response.ts
1365
- init_thresholds();
1276
+ init_config();
1277
+ var MAX_WRAPPER_KEYS = 3;
1366
1278
  function unwrapResponse(parsed) {
1367
1279
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1368
1280
  const obj = parsed;
1369
1281
  const keys = Object.keys(obj);
1370
- if (keys.length > 3) return parsed;
1282
+ if (keys.length > MAX_WRAPPER_KEYS) return parsed;
1371
1283
  let best = null;
1372
1284
  let bestSize = 0;
1373
1285
  for (const key of keys) {
@@ -1386,6 +1298,10 @@ function unwrapResponse(parsed) {
1386
1298
  return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1387
1299
  }
1388
1300
 
1301
+ // src/analysis/rules/scanner.ts
1302
+ init_log();
1303
+ init_type_guards();
1304
+
1389
1305
  // src/analysis/rules/patterns.ts
1390
1306
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1391
1307
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -1399,6 +1315,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1399
1315
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1400
1316
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
1401
1317
  var INTERNAL_ID_SUFFIX = /Id$|_id$/;
1318
+ var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
1319
+ 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;
1402
1320
  var RULE_HINTS = {
1403
1321
  "exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
1404
1322
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
@@ -1410,8 +1328,8 @@ var RULE_HINTS = {
1410
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."
1411
1329
  };
1412
1330
 
1413
- // src/analysis/rules/exposed-secret.ts
1414
- init_limits();
1331
+ // src/analysis/rules/auth-rules.ts
1332
+ init_config();
1415
1333
 
1416
1334
  // src/utils/http-status.ts
1417
1335
  function isErrorStatus(code) {
@@ -1421,27 +1339,66 @@ function isRedirect(code) {
1421
1339
  return code >= 300 && code < 400;
1422
1340
  }
1423
1341
 
1424
- // src/analysis/rules/exposed-secret.ts
1425
- function findSecretKeys(obj, prefix, depth = 0) {
1426
- const found = [];
1427
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1428
- if (!obj || typeof obj !== "object") return found;
1429
- if (Array.isArray(obj)) {
1430
- for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
1431
- 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;
1432
1353
  }
1433
- return found;
1354
+ seen.set(result.key, result.finding);
1355
+ findings.push(result.finding);
1434
1356
  }
1435
- for (const k of Object.keys(obj)) {
1436
- const val = obj[k];
1437
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
1438
- 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);
1439
1376
  }
1377
+ return;
1378
+ }
1379
+ for (const key of Object.keys(obj)) {
1380
+ const val = obj[key];
1381
+ visitor(key, val, depth);
1440
1382
  if (typeof val === "object" && val !== null) {
1441
- found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1383
+ walk(val, visitor, opts, depth + 1);
1442
1384
  }
1443
1385
  }
1444
- 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
+ );
1445
1402
  }
1446
1403
  var exposedSecretRule = {
1447
1404
  id: "exposed-secret",
@@ -1449,50 +1406,38 @@ var exposedSecretRule = {
1449
1406
  name: "Exposed Secret in Response",
1450
1407
  hint: RULE_HINTS["exposed-secret"],
1451
1408
  check(ctx) {
1452
- const findings = [];
1453
- const seen = /* @__PURE__ */ new Map();
1454
- for (const r of ctx.requests) {
1455
- if (isErrorStatus(r.statusCode)) continue;
1456
- const parsed = ctx.parsedBodies.response.get(r.id);
1457
- if (!parsed) continue;
1458
- const keys = findSecretKeys(parsed, "");
1459
- if (keys.length === 0) continue;
1460
- const ep = `${r.method} ${r.path}`;
1461
- const dedupKey = `${ep}:${keys.sort().join(",")}`;
1462
- const existing = seen.get(dedupKey);
1463
- if (existing) {
1464
- existing.count++;
1465
- continue;
1466
- }
1467
- const finding = {
1468
- severity: "critical",
1469
- rule: "exposed-secret",
1470
- title: "Exposed Secret in Response",
1471
- desc: `${ep} \u2014 response contains ${keys.join(", ")} field${keys.length > 1 ? "s" : ""}`,
1472
- hint: this.hint,
1473
- endpoint: ep,
1474
- 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
+ }
1475
1427
  };
1476
- seen.set(dedupKey, finding);
1477
- findings.push(finding);
1478
- }
1479
- return findings;
1428
+ });
1480
1429
  }
1481
1430
  };
1482
-
1483
- // src/analysis/rules/token-in-url.ts
1484
1431
  var tokenInUrlRule = {
1485
1432
  id: "token-in-url",
1486
1433
  severity: "critical",
1487
1434
  name: "Auth Token in URL",
1488
1435
  hint: RULE_HINTS["token-in-url"],
1489
1436
  check(ctx) {
1490
- const findings = [];
1491
- const seen = /* @__PURE__ */ new Map();
1492
- for (const r of ctx.requests) {
1493
- const qIdx = r.url.indexOf("?");
1494
- if (qIdx === -1) continue;
1495
- 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("&");
1496
1441
  const flagged = [];
1497
1442
  for (const param of params) {
1498
1443
  const [name, ...rest] = param.split("=");
@@ -1502,65 +1447,125 @@ var tokenInUrlRule = {
1502
1447
  flagged.push(name);
1503
1448
  }
1504
1449
  }
1505
- if (flagged.length === 0) continue;
1506
- const ep = `${r.method} ${r.path}`;
1507
- const dedupKey = `${ep}:${flagged.sort().join(",")}`;
1508
- const existing = seen.get(dedupKey);
1509
- if (existing) {
1510
- existing.count++;
1511
- 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 });
1512
1488
  }
1513
- const finding = {
1514
- severity: "critical",
1515
- rule: "token-in-url",
1516
- title: "Auth Token in URL",
1517
- 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)`,
1518
1533
  hint: this.hint,
1519
1534
  endpoint: ep,
1520
1535
  count: 1
1521
- };
1522
- seen.set(dedupKey, finding);
1523
- findings.push(finding);
1536
+ });
1524
1537
  }
1525
1538
  return findings;
1526
1539
  }
1527
1540
  };
1528
1541
 
1529
- // src/analysis/rules/stack-trace-leak.ts
1542
+ // src/analysis/rules/data-rules.ts
1543
+ init_config();
1530
1544
  var stackTraceLeakRule = {
1531
1545
  id: "stack-trace-leak",
1532
1546
  severity: "critical",
1533
1547
  name: "Stack Trace Leaked to Client",
1534
1548
  hint: RULE_HINTS["stack-trace-leak"],
1535
1549
  check(ctx) {
1536
- const findings = [];
1537
- const seen = /* @__PURE__ */ new Map();
1538
- for (const r of ctx.requests) {
1539
- if (!r.responseBody) continue;
1540
- if (!STACK_TRACE_RE.test(r.responseBody)) continue;
1541
- const ep = `${r.method} ${r.path}`;
1542
- const existing = seen.get(ep);
1543
- if (existing) {
1544
- existing.count++;
1545
- continue;
1546
- }
1547
- const finding = {
1548
- severity: "critical",
1549
- rule: "stack-trace-leak",
1550
- title: "Stack Trace Leaked to Client",
1551
- desc: `${ep} \u2014 response exposes internal stack trace`,
1552
- hint: this.hint,
1553
- endpoint: ep,
1554
- 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
+ }
1555
1565
  };
1556
- seen.set(ep, finding);
1557
- findings.push(finding);
1558
- }
1559
- return findings;
1566
+ });
1560
1567
  }
1561
1568
  };
1562
-
1563
- // src/analysis/rules/error-info-leak.ts
1564
1569
  var CRITICAL_PATTERNS = [
1565
1570
  { re: DB_CONN_RE, label: "database connection string" },
1566
1571
  { re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
@@ -1572,90 +1577,34 @@ var errorInfoLeakRule = {
1572
1577
  name: "Sensitive Data in Error Response",
1573
1578
  hint: RULE_HINTS["error-info-leak"],
1574
1579
  check(ctx) {
1575
- const findings = [];
1576
- const seen = /* @__PURE__ */ new Map();
1577
- for (const r of ctx.requests) {
1578
- if (r.statusCode < 400) continue;
1579
- if (!r.responseBody) continue;
1580
- if (r.responseHeaders["x-nextjs-error"] || r.responseHeaders["x-nextjs-matched-path"]) continue;
1581
- const ep = `${r.method} ${r.path}`;
1582
- for (const p of CRITICAL_PATTERNS) {
1583
- if (!p.re.test(r.responseBody)) continue;
1584
- const dedupKey = `${ep}:${p.label}`;
1585
- const existing = seen.get(dedupKey);
1586
- if (existing) {
1587
- existing.count++;
1588
- 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 });
1589
1589
  }
1590
- const finding = {
1590
+ }
1591
+ }
1592
+ return deduplicateFindings(entries, ({ ep, pattern }) => {
1593
+ return {
1594
+ key: `${ep}:${pattern.label}`,
1595
+ finding: {
1591
1596
  severity: "critical",
1592
1597
  rule: "error-info-leak",
1593
1598
  title: "Sensitive Data in Error Response",
1594
- desc: `${ep} \u2014 error response exposes ${p.label}`,
1599
+ desc: `${ep} \u2014 error response exposes ${pattern.label}`,
1595
1600
  hint: this.hint,
1596
1601
  endpoint: ep,
1597
1602
  count: 1
1598
- };
1599
- seen.set(dedupKey, finding);
1600
- findings.push(finding);
1601
- }
1602
- }
1603
- return findings;
1604
- }
1605
- };
1606
-
1607
- // src/analysis/rules/insecure-cookie.ts
1608
- function isFrameworkResponse(r) {
1609
- if (isRedirect(r.statusCode)) return true;
1610
- if (r.path?.startsWith("/__")) return true;
1611
- if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1612
- return false;
1613
- }
1614
- var insecureCookieRule = {
1615
- id: "insecure-cookie",
1616
- severity: "warning",
1617
- name: "Insecure Cookie",
1618
- hint: RULE_HINTS["insecure-cookie"],
1619
- check(ctx) {
1620
- const findings = [];
1621
- const seen = /* @__PURE__ */ new Map();
1622
- for (const r of ctx.requests) {
1623
- if (!r.responseHeaders) continue;
1624
- if (isFrameworkResponse(r)) continue;
1625
- const setCookie = r.responseHeaders["set-cookie"];
1626
- if (!setCookie) continue;
1627
- const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
1628
- for (const cookie of cookies) {
1629
- const cookieName = cookie.trim().split("=")[0].trim();
1630
- const lower = cookie.toLowerCase();
1631
- const issues = [];
1632
- if (!lower.includes("httponly")) issues.push("HttpOnly");
1633
- if (!lower.includes("samesite")) issues.push("SameSite");
1634
- if (issues.length === 0) continue;
1635
- const dedupKey = `${cookieName}:${issues.join(",")}`;
1636
- const existing = seen.get(dedupKey);
1637
- if (existing) {
1638
- existing.count++;
1639
- continue;
1640
1603
  }
1641
- const finding = {
1642
- severity: "warning",
1643
- rule: "insecure-cookie",
1644
- title: "Insecure Cookie",
1645
- desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
1646
- hint: this.hint,
1647
- endpoint: cookieName,
1648
- count: 1
1649
- };
1650
- seen.set(dedupKey, finding);
1651
- findings.push(finding);
1652
- }
1653
- }
1654
- return findings;
1604
+ };
1605
+ });
1655
1606
  }
1656
1607
  };
1657
-
1658
- // src/analysis/rules/sensitive-logs.ts
1659
1608
  var sensitiveLogsRule = {
1660
1609
  id: "sensitive-logs",
1661
1610
  severity: "warning",
@@ -1680,59 +1629,13 @@ var sensitiveLogsRule = {
1680
1629
  }];
1681
1630
  }
1682
1631
  };
1683
-
1684
- // src/analysis/rules/cors-credentials.ts
1685
- var corsCredentialsRule = {
1686
- id: "cors-credentials",
1687
- severity: "warning",
1688
- name: "CORS Credentials with Wildcard",
1689
- hint: RULE_HINTS["cors-credentials"],
1690
- check(ctx) {
1691
- const findings = [];
1692
- const seen = /* @__PURE__ */ new Set();
1693
- for (const r of ctx.requests) {
1694
- if (!r.responseHeaders) continue;
1695
- const origin = r.responseHeaders["access-control-allow-origin"];
1696
- const creds = r.responseHeaders["access-control-allow-credentials"];
1697
- if (origin !== "*" || creds !== "true") continue;
1698
- const ep = `${r.method} ${r.path}`;
1699
- if (seen.has(ep)) continue;
1700
- seen.add(ep);
1701
- findings.push({
1702
- severity: "warning",
1703
- rule: "cors-credentials",
1704
- title: "CORS Credentials with Wildcard",
1705
- desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
1706
- hint: this.hint,
1707
- endpoint: ep,
1708
- count: 1
1709
- });
1710
- }
1711
- return findings;
1712
- }
1713
- };
1714
-
1715
- // src/analysis/rules/response-pii-leak.ts
1716
- init_limits();
1717
1632
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1718
- function findEmails(obj, depth = 0) {
1719
- const emails = [];
1720
- if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1721
- if (!obj || typeof obj !== "object") return emails;
1722
- if (Array.isArray(obj)) {
1723
- for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
1724
- emails.push(...findEmails(obj[i], depth + 1));
1725
- }
1726
- return emails;
1727
- }
1728
- for (const v of Object.values(obj)) {
1729
- if (typeof v === "string" && EMAIL_RE.test(v)) {
1730
- emails.push(v);
1731
- } else if (typeof v === "object" && v !== null) {
1732
- emails.push(...findEmails(v, depth + 1));
1733
- }
1734
- }
1735
- 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
+ );
1736
1639
  }
1737
1640
  function topLevelFieldCount(obj) {
1738
1641
  if (Array.isArray(obj)) {
@@ -1748,6 +1651,15 @@ function hasInternalIds(obj) {
1748
1651
  }
1749
1652
  return false;
1750
1653
  }
1654
+ function hasSensitiveFieldNames(obj, depth = 0) {
1655
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
1656
+ if (!obj || typeof obj !== "object") return false;
1657
+ if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
1658
+ for (const key of Object.keys(obj)) {
1659
+ if (SENSITIVE_FIELD_NAMES.test(key)) return true;
1660
+ }
1661
+ return false;
1662
+ }
1751
1663
  function detectEchoPII(method, reqBody, target) {
1752
1664
  if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
1753
1665
  const reqEmails = findEmails(reqBody);
@@ -1769,6 +1681,13 @@ function detectFullRecordPII(target) {
1769
1681
  if (emails.length === 0) return null;
1770
1682
  return { reason: "full-record", emailCount: emails.length };
1771
1683
  }
1684
+ function detectSensitiveFieldPII(target) {
1685
+ const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
1686
+ if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
1687
+ if (!hasSensitiveFieldNames(inspect)) return null;
1688
+ if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
1689
+ return { reason: "sensitive-fields", emailCount: 0 };
1690
+ }
1772
1691
  function detectListPII(target) {
1773
1692
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1774
1693
  let itemsWithEmail = 0;
@@ -1787,12 +1706,13 @@ function detectListPII(target) {
1787
1706
  }
1788
1707
  function detectPII(method, reqBody, resBody) {
1789
1708
  const target = unwrapResponse(resBody);
1790
- return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1709
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
1791
1710
  }
1792
1711
  var REASON_LABELS = {
1793
1712
  echo: "echoes back PII from the request body",
1794
1713
  "full-record": "returns a full record with email and internal IDs",
1795
- "list-pii": "returns a list of records containing email addresses"
1714
+ "list-pii": "returns a list of records containing email addresses",
1715
+ "sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
1796
1716
  };
1797
1717
  var responsePiiLeakRule = {
1798
1718
  id: "response-pii-leak",
@@ -1800,101 +1720,78 @@ var responsePiiLeakRule = {
1800
1720
  name: "PII Leak in Response",
1801
1721
  hint: RULE_HINTS["response-pii-leak"],
1802
1722
  check(ctx) {
1803
- const findings = [];
1804
- const seen = /* @__PURE__ */ new Map();
1805
- for (const r of ctx.requests) {
1806
- if (isErrorStatus(r.statusCode)) continue;
1807
- const resJson = ctx.parsedBodies.response.get(r.id);
1808
- if (!resJson) continue;
1809
- const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1810
- const detection = detectPII(r.method, reqJson, resJson);
1811
- if (!detection) continue;
1812
- const ep = `${r.method} ${r.path}`;
1813
- const dedupKey = `${ep}:${detection.reason}`;
1814
- const existing = seen.get(dedupKey);
1815
- if (existing) {
1816
- existing.count++;
1817
- continue;
1818
- }
1819
- const finding = {
1820
- severity: "warning",
1821
- rule: "response-pii-leak",
1822
- title: "PII Leak in Response",
1823
- desc: `${ep} \u2014 ${REASON_LABELS[detection.reason]}`,
1824
- hint: this.hint,
1825
- endpoint: ep,
1826
- 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
+ }
1827
1743
  };
1828
- seen.set(dedupKey, finding);
1829
- findings.push(finding);
1830
- }
1831
- return findings;
1744
+ });
1832
1745
  }
1833
1746
  };
1834
1747
 
1835
1748
  // src/analysis/engine.ts
1836
- init_limits();
1749
+ init_config();
1837
1750
 
1838
1751
  // src/analysis/group.ts
1839
1752
  init_constants();
1840
1753
  import { randomUUID } from "crypto";
1754
+ init_endpoint();
1841
1755
 
1842
1756
  // src/analysis/label.ts
1843
1757
  init_constants();
1844
1758
 
1845
1759
  // src/analysis/transforms.ts
1846
1760
  init_constants();
1761
+ init_config();
1762
+ init_endpoint();
1847
1763
 
1848
1764
  // src/analysis/insights/prepare.ts
1849
1765
  init_endpoint();
1850
1766
  init_constants();
1851
- init_thresholds();
1767
+ init_config();
1852
1768
 
1853
- // src/analysis/insights/rules/n1.ts
1854
- init_endpoint();
1855
- init_constants();
1769
+ // src/analysis/insights/runner.ts
1770
+ init_log();
1771
+ init_type_guards();
1856
1772
 
1857
- // src/analysis/insights/rules/cross-endpoint.ts
1773
+ // src/analysis/insights/rules/query-rules.ts
1858
1774
  init_endpoint();
1859
1775
  init_constants();
1860
1776
 
1861
- // src/analysis/insights/rules/redundant-query.ts
1777
+ // src/analysis/insights/rules/response-rules.ts
1862
1778
  init_endpoint();
1779
+ init_log();
1780
+ init_type_guards();
1863
1781
  init_constants();
1864
1782
 
1865
- // src/analysis/insights/rules/error-hotspot.ts
1866
- init_constants();
1867
-
1868
- // src/analysis/insights/rules/duplicate.ts
1869
- init_constants();
1870
-
1871
- // src/analysis/insights/rules/slow.ts
1872
- init_constants();
1873
-
1874
- // src/analysis/insights/rules/query-heavy.ts
1875
- init_constants();
1876
-
1877
- // src/analysis/insights/rules/select-star.ts
1878
- init_constants();
1879
-
1880
- // src/analysis/insights/rules/high-rows.ts
1783
+ // src/analysis/insights/rules/reliability-rules.ts
1881
1784
  init_constants();
1882
1785
 
1883
- // src/analysis/insights/rules/response-overfetch.ts
1786
+ // src/analysis/insights/rules/pattern-rules.ts
1884
1787
  init_endpoint();
1885
1788
  init_constants();
1886
1789
 
1887
- // src/analysis/insights/rules/large-response.ts
1888
- init_constants();
1889
-
1890
- // src/analysis/insights/rules/regression.ts
1891
- init_constants();
1892
-
1893
1790
  // src/analysis/issue-mappers.ts
1894
1791
  init_endpoint();
1895
1792
 
1896
1793
  // src/index.ts
1897
- var VERSION = "0.8.6";
1794
+ var VERSION = "9.0.0";
1898
1795
 
1899
1796
  // src/cli/commands/install.ts
1900
1797
  init_constants();