brakit 0.8.6 → 0.9.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.
- package/README.md +22 -4
- package/dist/api.d.ts +87 -48
- package/dist/api.js +820 -788
- package/dist/bin/brakit.js +327 -419
- package/dist/dashboard-client.global.js +901 -0
- package/dist/dashboard.html +1215 -2215
- package/dist/mcp/server.js +7 -15
- package/dist/runtime/index.js +3364 -5700
- package/package.json +4 -2
package/dist/bin/brakit.js
CHANGED
|
@@ -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/
|
|
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
|
|
15
|
-
"src/constants/
|
|
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 =
|
|
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
|
-
|
|
73
|
-
init_limits();
|
|
68
|
+
init_config();
|
|
74
69
|
}
|
|
75
70
|
});
|
|
76
71
|
|
|
77
|
-
// src/constants/
|
|
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
|
|
100
|
-
"src/constants/
|
|
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/
|
|
136
|
-
var
|
|
137
|
-
|
|
138
|
-
|
|
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 = "0.9.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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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/
|
|
1414
|
-
|
|
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/
|
|
1425
|
-
function
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1354
|
+
seen.set(result.key, result.finding);
|
|
1355
|
+
findings.push(result.finding);
|
|
1434
1356
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1383
|
+
walk(val, visitor, opts, depth + 1);
|
|
1442
1384
|
}
|
|
1443
1385
|
}
|
|
1444
|
-
|
|
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,39 @@ var exposedSecretRule = {
|
|
|
1449
1406
|
name: "Exposed Secret in Response",
|
|
1450
1407
|
hint: RULE_HINTS["exposed-secret"],
|
|
1451
1408
|
check(ctx) {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
if (
|
|
1456
|
-
const
|
|
1457
|
-
if (
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
+
detail: `Exposed fields: ${keys.join(", ")}. ${keys.length} unmasked secret value${keys.length !== 1 ? "s" : ""} in response body.`,
|
|
1425
|
+
endpoint: ep,
|
|
1426
|
+
count: 1
|
|
1427
|
+
}
|
|
1475
1428
|
};
|
|
1476
|
-
|
|
1477
|
-
findings.push(finding);
|
|
1478
|
-
}
|
|
1479
|
-
return findings;
|
|
1429
|
+
});
|
|
1480
1430
|
}
|
|
1481
1431
|
};
|
|
1482
|
-
|
|
1483
|
-
// src/analysis/rules/token-in-url.ts
|
|
1484
1432
|
var tokenInUrlRule = {
|
|
1485
1433
|
id: "token-in-url",
|
|
1486
1434
|
severity: "critical",
|
|
1487
1435
|
name: "Auth Token in URL",
|
|
1488
1436
|
hint: RULE_HINTS["token-in-url"],
|
|
1489
1437
|
check(ctx) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
if (qIdx === -1) continue;
|
|
1495
|
-
const params = r.url.substring(qIdx + 1).split("&");
|
|
1438
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
1439
|
+
const qIdx = request.url.indexOf("?");
|
|
1440
|
+
if (qIdx === -1) return null;
|
|
1441
|
+
const params = request.url.substring(qIdx + 1).split("&");
|
|
1496
1442
|
const flagged = [];
|
|
1497
1443
|
for (const param of params) {
|
|
1498
1444
|
const [name, ...rest] = param.split("=");
|
|
@@ -1502,65 +1448,129 @@ var tokenInUrlRule = {
|
|
|
1502
1448
|
flagged.push(name);
|
|
1503
1449
|
}
|
|
1504
1450
|
}
|
|
1505
|
-
if (flagged.length === 0)
|
|
1506
|
-
const ep = `${
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1451
|
+
if (flagged.length === 0) return null;
|
|
1452
|
+
const ep = `${request.method} ${request.path}`;
|
|
1453
|
+
return {
|
|
1454
|
+
key: `${ep}:${flagged.sort().join(",")}`,
|
|
1455
|
+
finding: {
|
|
1456
|
+
severity: "critical",
|
|
1457
|
+
rule: "token-in-url",
|
|
1458
|
+
title: "Auth Token in URL",
|
|
1459
|
+
desc: `${ep} \u2014 ${flagged.join(", ")} exposed in query string`,
|
|
1460
|
+
hint: this.hint,
|
|
1461
|
+
detail: `Parameters in URL: ${flagged.join(", ")}. Auth tokens in URLs are logged by proxies, browsers, and CDNs.`,
|
|
1462
|
+
endpoint: ep,
|
|
1463
|
+
count: 1
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
function isFrameworkResponse(request) {
|
|
1470
|
+
if (isRedirect(request.statusCode)) return true;
|
|
1471
|
+
if (request.path?.startsWith("/__")) return true;
|
|
1472
|
+
if (request.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
var insecureCookieRule = {
|
|
1476
|
+
id: "insecure-cookie",
|
|
1477
|
+
severity: "warning",
|
|
1478
|
+
name: "Insecure Cookie",
|
|
1479
|
+
hint: RULE_HINTS["insecure-cookie"],
|
|
1480
|
+
check(ctx) {
|
|
1481
|
+
const cookieEntries = [];
|
|
1482
|
+
for (const request of ctx.requests) {
|
|
1483
|
+
if (!request.responseHeaders) continue;
|
|
1484
|
+
if (isFrameworkResponse(request)) continue;
|
|
1485
|
+
const setCookie = request.responseHeaders["set-cookie"];
|
|
1486
|
+
if (!setCookie) continue;
|
|
1487
|
+
const cookies = setCookie.split(/,(?=\s*[A-Za-z0-9_\-]+=)/);
|
|
1488
|
+
for (const cookie of cookies) {
|
|
1489
|
+
cookieEntries.push({ cookie });
|
|
1512
1490
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1491
|
+
}
|
|
1492
|
+
return deduplicateFindings(cookieEntries, ({ cookie }) => {
|
|
1493
|
+
const cookieName = cookie.trim().split("=")[0].trim();
|
|
1494
|
+
const lower = cookie.toLowerCase();
|
|
1495
|
+
const issues = [];
|
|
1496
|
+
if (!lower.includes("httponly")) issues.push("HttpOnly");
|
|
1497
|
+
if (!lower.includes("samesite")) issues.push("SameSite");
|
|
1498
|
+
if (issues.length === 0) return null;
|
|
1499
|
+
return {
|
|
1500
|
+
key: `${cookieName}:${issues.join(",")}`,
|
|
1501
|
+
finding: {
|
|
1502
|
+
severity: "warning",
|
|
1503
|
+
rule: "insecure-cookie",
|
|
1504
|
+
title: "Insecure Cookie",
|
|
1505
|
+
desc: `${cookieName} \u2014 missing ${issues.join(", ")} flag${issues.length > 1 ? "s" : ""}`,
|
|
1506
|
+
hint: this.hint,
|
|
1507
|
+
detail: `Missing: ${issues.join(", ")}. ${issues.includes("HttpOnly") ? "Cookie accessible via JavaScript (XSS risk). " : ""}${issues.includes("SameSite") ? "Cookie sent on cross-site requests (CSRF risk)." : ""}`,
|
|
1508
|
+
endpoint: cookieName,
|
|
1509
|
+
count: 1
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var corsCredentialsRule = {
|
|
1516
|
+
id: "cors-credentials",
|
|
1517
|
+
severity: "warning",
|
|
1518
|
+
name: "CORS Credentials with Wildcard",
|
|
1519
|
+
hint: RULE_HINTS["cors-credentials"],
|
|
1520
|
+
check(ctx) {
|
|
1521
|
+
const findings = [];
|
|
1522
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1523
|
+
for (const request of ctx.requests) {
|
|
1524
|
+
if (!request.responseHeaders) continue;
|
|
1525
|
+
const origin = request.responseHeaders["access-control-allow-origin"];
|
|
1526
|
+
const creds = request.responseHeaders["access-control-allow-credentials"];
|
|
1527
|
+
if (origin !== "*" || creds !== "true") continue;
|
|
1528
|
+
const ep = `${request.method} ${request.path}`;
|
|
1529
|
+
if (seen.has(ep)) continue;
|
|
1530
|
+
seen.add(ep);
|
|
1531
|
+
findings.push({
|
|
1532
|
+
severity: "warning",
|
|
1533
|
+
rule: "cors-credentials",
|
|
1534
|
+
title: "CORS Credentials with Wildcard",
|
|
1535
|
+
desc: `${ep} \u2014 credentials:true with origin:* (browser will reject)`,
|
|
1518
1536
|
hint: this.hint,
|
|
1519
1537
|
endpoint: ep,
|
|
1520
1538
|
count: 1
|
|
1521
|
-
};
|
|
1522
|
-
seen.set(dedupKey, finding);
|
|
1523
|
-
findings.push(finding);
|
|
1539
|
+
});
|
|
1524
1540
|
}
|
|
1525
1541
|
return findings;
|
|
1526
1542
|
}
|
|
1527
1543
|
};
|
|
1528
1544
|
|
|
1529
|
-
// src/analysis/rules/
|
|
1545
|
+
// src/analysis/rules/data-rules.ts
|
|
1546
|
+
init_config();
|
|
1530
1547
|
var stackTraceLeakRule = {
|
|
1531
1548
|
id: "stack-trace-leak",
|
|
1532
1549
|
severity: "critical",
|
|
1533
1550
|
name: "Stack Trace Leaked to Client",
|
|
1534
1551
|
hint: RULE_HINTS["stack-trace-leak"],
|
|
1535
1552
|
check(ctx) {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
endpoint: ep,
|
|
1554
|
-
count: 1
|
|
1553
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
1554
|
+
if (!request.responseBody) return null;
|
|
1555
|
+
if (!STACK_TRACE_RE.test(request.responseBody)) return null;
|
|
1556
|
+
const ep = `${request.method} ${request.path}`;
|
|
1557
|
+
const firstLine = request.responseBody.split("\n").find((l) => STACK_TRACE_RE.test(l))?.trim() ?? "";
|
|
1558
|
+
return {
|
|
1559
|
+
key: ep,
|
|
1560
|
+
finding: {
|
|
1561
|
+
severity: "critical",
|
|
1562
|
+
rule: "stack-trace-leak",
|
|
1563
|
+
title: "Stack Trace Leaked to Client",
|
|
1564
|
+
desc: `${ep} \u2014 response exposes internal stack trace`,
|
|
1565
|
+
hint: this.hint,
|
|
1566
|
+
detail: firstLine ? `Stack trace: ${firstLine.slice(0, 120)}` : void 0,
|
|
1567
|
+
endpoint: ep,
|
|
1568
|
+
count: 1
|
|
1569
|
+
}
|
|
1555
1570
|
};
|
|
1556
|
-
|
|
1557
|
-
findings.push(finding);
|
|
1558
|
-
}
|
|
1559
|
-
return findings;
|
|
1571
|
+
});
|
|
1560
1572
|
}
|
|
1561
1573
|
};
|
|
1562
|
-
|
|
1563
|
-
// src/analysis/rules/error-info-leak.ts
|
|
1564
1574
|
var CRITICAL_PATTERNS = [
|
|
1565
1575
|
{ re: DB_CONN_RE, label: "database connection string" },
|
|
1566
1576
|
{ re: SQL_FRAGMENT_RE, label: "SQL query fragment" },
|
|
@@ -1572,90 +1582,35 @@ var errorInfoLeakRule = {
|
|
|
1572
1582
|
name: "Sensitive Data in Error Response",
|
|
1573
1583
|
hint: RULE_HINTS["error-info-leak"],
|
|
1574
1584
|
check(ctx) {
|
|
1575
|
-
const
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
if (
|
|
1579
|
-
if (
|
|
1580
|
-
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const dedupKey = `${ep}:${p.label}`;
|
|
1585
|
-
const existing = seen.get(dedupKey);
|
|
1586
|
-
if (existing) {
|
|
1587
|
-
existing.count++;
|
|
1588
|
-
continue;
|
|
1585
|
+
const entries = [];
|
|
1586
|
+
for (const request of ctx.requests) {
|
|
1587
|
+
if (request.statusCode < 400) continue;
|
|
1588
|
+
if (!request.responseBody) continue;
|
|
1589
|
+
if (request.responseHeaders["x-nextjs-error"] || request.responseHeaders["x-nextjs-matched-path"]) continue;
|
|
1590
|
+
const ep = `${request.method} ${request.path}`;
|
|
1591
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
1592
|
+
if (pattern.re.test(request.responseBody)) {
|
|
1593
|
+
entries.push({ ep, pattern, body: request.responseBody });
|
|
1589
1594
|
}
|
|
1590
|
-
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return deduplicateFindings(entries, ({ ep, pattern }) => {
|
|
1598
|
+
return {
|
|
1599
|
+
key: `${ep}:${pattern.label}`,
|
|
1600
|
+
finding: {
|
|
1591
1601
|
severity: "critical",
|
|
1592
1602
|
rule: "error-info-leak",
|
|
1593
1603
|
title: "Sensitive Data in Error Response",
|
|
1594
|
-
desc: `${ep} \u2014 error response exposes ${
|
|
1604
|
+
desc: `${ep} \u2014 error response exposes ${pattern.label}`,
|
|
1595
1605
|
hint: this.hint,
|
|
1606
|
+
detail: `Detected: ${pattern.label} in error response body`,
|
|
1596
1607
|
endpoint: ep,
|
|
1597
1608
|
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
1609
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
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;
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1655
1612
|
}
|
|
1656
1613
|
};
|
|
1657
|
-
|
|
1658
|
-
// src/analysis/rules/sensitive-logs.ts
|
|
1659
1614
|
var sensitiveLogsRule = {
|
|
1660
1615
|
id: "sensitive-logs",
|
|
1661
1616
|
severity: "warning",
|
|
@@ -1680,59 +1635,13 @@ var sensitiveLogsRule = {
|
|
|
1680
1635
|
}];
|
|
1681
1636
|
}
|
|
1682
1637
|
};
|
|
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
1638
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
1718
|
-
function findEmails(obj
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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;
|
|
1639
|
+
function findEmails(obj) {
|
|
1640
|
+
return collectFromObject(
|
|
1641
|
+
obj,
|
|
1642
|
+
(_key, val) => typeof val === "string" && EMAIL_RE.test(val) ? val : null,
|
|
1643
|
+
{ arrayLimit: PII_SCAN_ARRAY_LIMIT }
|
|
1644
|
+
);
|
|
1736
1645
|
}
|
|
1737
1646
|
function topLevelFieldCount(obj) {
|
|
1738
1647
|
if (Array.isArray(obj)) {
|
|
@@ -1748,6 +1657,15 @@ function hasInternalIds(obj) {
|
|
|
1748
1657
|
}
|
|
1749
1658
|
return false;
|
|
1750
1659
|
}
|
|
1660
|
+
function hasSensitiveFieldNames(obj, depth = 0) {
|
|
1661
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
|
|
1662
|
+
if (!obj || typeof obj !== "object") return false;
|
|
1663
|
+
if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
|
|
1664
|
+
for (const key of Object.keys(obj)) {
|
|
1665
|
+
if (SENSITIVE_FIELD_NAMES.test(key)) return true;
|
|
1666
|
+
}
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1751
1669
|
function detectEchoPII(method, reqBody, target) {
|
|
1752
1670
|
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1753
1671
|
const reqEmails = findEmails(reqBody);
|
|
@@ -1769,6 +1687,13 @@ function detectFullRecordPII(target) {
|
|
|
1769
1687
|
if (emails.length === 0) return null;
|
|
1770
1688
|
return { reason: "full-record", emailCount: emails.length };
|
|
1771
1689
|
}
|
|
1690
|
+
function detectSensitiveFieldPII(target) {
|
|
1691
|
+
const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1692
|
+
if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
|
|
1693
|
+
if (!hasSensitiveFieldNames(inspect)) return null;
|
|
1694
|
+
if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
|
|
1695
|
+
return { reason: "sensitive-fields", emailCount: 0 };
|
|
1696
|
+
}
|
|
1772
1697
|
function detectListPII(target) {
|
|
1773
1698
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1774
1699
|
let itemsWithEmail = 0;
|
|
@@ -1787,12 +1712,13 @@ function detectListPII(target) {
|
|
|
1787
1712
|
}
|
|
1788
1713
|
function detectPII(method, reqBody, resBody) {
|
|
1789
1714
|
const target = unwrapResponse(resBody);
|
|
1790
|
-
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1715
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
1791
1716
|
}
|
|
1792
1717
|
var REASON_LABELS = {
|
|
1793
1718
|
echo: "echoes back PII from the request body",
|
|
1794
1719
|
"full-record": "returns a full record with email and internal IDs",
|
|
1795
|
-
"list-pii": "returns a list of records containing email addresses"
|
|
1720
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
1721
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
1796
1722
|
};
|
|
1797
1723
|
var responsePiiLeakRule = {
|
|
1798
1724
|
id: "response-pii-leak",
|
|
@@ -1800,101 +1726,83 @@ var responsePiiLeakRule = {
|
|
|
1800
1726
|
name: "PII Leak in Response",
|
|
1801
1727
|
hint: RULE_HINTS["response-pii-leak"],
|
|
1802
1728
|
check(ctx) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
const
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
if (
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1729
|
+
return deduplicateFindings(ctx.requests, (request) => {
|
|
1730
|
+
if (isErrorStatus(request.statusCode)) return null;
|
|
1731
|
+
if (SELF_SERVICE_PATH.test(request.path)) return null;
|
|
1732
|
+
const resJson = ctx.parsedBodies.response.get(request.id);
|
|
1733
|
+
if (!resJson) return null;
|
|
1734
|
+
const reqJson = ctx.parsedBodies.request.get(request.id) ?? null;
|
|
1735
|
+
const detection = detectPII(request.method, reqJson, resJson);
|
|
1736
|
+
if (!detection) return null;
|
|
1737
|
+
const ep = `${request.method} ${request.path}`;
|
|
1738
|
+
const fieldCount = topLevelFieldCount(resJson);
|
|
1739
|
+
const detailParts = [`Pattern: ${REASON_LABELS[detection.reason]}`];
|
|
1740
|
+
if (detection.emailCount > 0) detailParts.push(`${detection.emailCount} email${detection.emailCount !== 1 ? "s" : ""} detected`);
|
|
1741
|
+
if (fieldCount > 0) detailParts.push(`${fieldCount} fields per record`);
|
|
1742
|
+
return {
|
|
1743
|
+
key: ep,
|
|
1744
|
+
finding: {
|
|
1745
|
+
severity: "warning",
|
|
1746
|
+
rule: "response-pii-leak",
|
|
1747
|
+
title: "PII Leak in Response",
|
|
1748
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
1749
|
+
hint: this.hint,
|
|
1750
|
+
detail: detailParts.join(". "),
|
|
1751
|
+
endpoint: ep,
|
|
1752
|
+
count: 1
|
|
1753
|
+
}
|
|
1827
1754
|
};
|
|
1828
|
-
|
|
1829
|
-
findings.push(finding);
|
|
1830
|
-
}
|
|
1831
|
-
return findings;
|
|
1755
|
+
});
|
|
1832
1756
|
}
|
|
1833
1757
|
};
|
|
1834
1758
|
|
|
1835
1759
|
// src/analysis/engine.ts
|
|
1836
|
-
|
|
1760
|
+
init_config();
|
|
1837
1761
|
|
|
1838
1762
|
// src/analysis/group.ts
|
|
1839
1763
|
init_constants();
|
|
1840
1764
|
import { randomUUID } from "crypto";
|
|
1765
|
+
init_endpoint();
|
|
1841
1766
|
|
|
1842
1767
|
// src/analysis/label.ts
|
|
1843
1768
|
init_constants();
|
|
1844
1769
|
|
|
1845
1770
|
// src/analysis/transforms.ts
|
|
1846
1771
|
init_constants();
|
|
1772
|
+
init_config();
|
|
1773
|
+
init_endpoint();
|
|
1847
1774
|
|
|
1848
1775
|
// src/analysis/insights/prepare.ts
|
|
1849
1776
|
init_endpoint();
|
|
1850
1777
|
init_constants();
|
|
1851
|
-
|
|
1778
|
+
init_config();
|
|
1852
1779
|
|
|
1853
|
-
// src/analysis/insights/
|
|
1854
|
-
|
|
1855
|
-
|
|
1780
|
+
// src/analysis/insights/runner.ts
|
|
1781
|
+
init_log();
|
|
1782
|
+
init_type_guards();
|
|
1856
1783
|
|
|
1857
|
-
// src/analysis/insights/rules/
|
|
1784
|
+
// src/analysis/insights/rules/query-rules.ts
|
|
1858
1785
|
init_endpoint();
|
|
1859
1786
|
init_constants();
|
|
1860
1787
|
|
|
1861
|
-
// src/analysis/insights/rules/
|
|
1788
|
+
// src/analysis/insights/rules/response-rules.ts
|
|
1862
1789
|
init_endpoint();
|
|
1790
|
+
init_log();
|
|
1791
|
+
init_type_guards();
|
|
1863
1792
|
init_constants();
|
|
1864
1793
|
|
|
1865
|
-
// src/analysis/insights/rules/
|
|
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
|
|
1794
|
+
// src/analysis/insights/rules/reliability-rules.ts
|
|
1881
1795
|
init_constants();
|
|
1882
1796
|
|
|
1883
|
-
// src/analysis/insights/rules/
|
|
1797
|
+
// src/analysis/insights/rules/pattern-rules.ts
|
|
1884
1798
|
init_endpoint();
|
|
1885
1799
|
init_constants();
|
|
1886
1800
|
|
|
1887
|
-
// src/analysis/insights/rules/large-response.ts
|
|
1888
|
-
init_constants();
|
|
1889
|
-
|
|
1890
|
-
// src/analysis/insights/rules/regression.ts
|
|
1891
|
-
init_constants();
|
|
1892
|
-
|
|
1893
1801
|
// src/analysis/issue-mappers.ts
|
|
1894
1802
|
init_endpoint();
|
|
1895
1803
|
|
|
1896
1804
|
// src/index.ts
|
|
1897
|
-
var VERSION = "0.
|
|
1805
|
+
var VERSION = "0.9.0";
|
|
1898
1806
|
|
|
1899
1807
|
// src/cli/commands/install.ts
|
|
1900
1808
|
init_constants();
|