brakit 0.8.5 → 0.8.6
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 +3 -3
- package/dist/api.d.ts +120 -115
- package/dist/api.js +371 -340
- package/dist/bin/brakit.js +457 -332
- package/dist/dashboard.html +60 -59
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +637 -529
- package/package.json +1 -1
package/dist/runtime/index.js
CHANGED
|
@@ -47,7 +47,7 @@ var init_routes = __esm({
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
// src/constants/limits.ts
|
|
50
|
-
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS,
|
|
50
|
+
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS, MAX_JSON_BODY_BYTES, ANALYSIS_DEBOUNCE_MS, ISSUE_ID_HASH_LENGTH, ISSUES_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER, PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_API_LIMIT, MAX_OBJECT_SCAN_DEPTH, MAX_UNIQUE_ENDPOINTS, MAX_ACCUMULATOR_ENTRIES, ISSUE_PRUNE_TTL_MS;
|
|
51
51
|
var init_limits = __esm({
|
|
52
52
|
"src/constants/limits.ts"() {
|
|
53
53
|
"use strict";
|
|
@@ -62,14 +62,25 @@ var init_limits = __esm({
|
|
|
62
62
|
SENSITIVE_MASK_VISIBLE_CHARS = 4;
|
|
63
63
|
MAX_JSON_BODY_BYTES = 65536;
|
|
64
64
|
ANALYSIS_DEBOUNCE_MS = 300;
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
ISSUE_ID_HASH_LENGTH = 16;
|
|
66
|
+
ISSUES_DATA_VERSION = 2;
|
|
67
67
|
SENSITIVE_MASK_PLACEHOLDER = "****";
|
|
68
|
+
PROJECT_HASH_LENGTH = 8;
|
|
69
|
+
SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
70
|
+
PII_SCAN_ARRAY_LIMIT = 10;
|
|
71
|
+
MIN_SECRET_VALUE_LENGTH = 8;
|
|
72
|
+
FULL_RECORD_MIN_FIELDS = 5;
|
|
73
|
+
LIST_PII_MIN_ITEMS = 2;
|
|
74
|
+
MAX_API_LIMIT = 500;
|
|
75
|
+
MAX_OBJECT_SCAN_DEPTH = 5;
|
|
76
|
+
MAX_UNIQUE_ENDPOINTS = 500;
|
|
77
|
+
MAX_ACCUMULATOR_ENTRIES = 1e3;
|
|
78
|
+
ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
68
79
|
}
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
// src/constants/thresholds.ts
|
|
72
|
-
var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT,
|
|
83
|
+
var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, CLEAN_HITS_FOR_RESOLUTION, STALE_ISSUE_TTL_MS;
|
|
73
84
|
var init_thresholds = __esm({
|
|
74
85
|
"src/constants/thresholds.ts"() {
|
|
75
86
|
"use strict";
|
|
@@ -99,9 +110,9 @@ var init_thresholds = __esm({
|
|
|
99
110
|
OVERFETCH_MANY_FIELDS = 12;
|
|
100
111
|
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
101
112
|
MAX_DUPLICATE_INSIGHTS = 3;
|
|
102
|
-
INSIGHT_WINDOW_PER_ENDPOINT =
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
INSIGHT_WINDOW_PER_ENDPOINT = 20;
|
|
114
|
+
CLEAN_HITS_FOR_RESOLUTION = 5;
|
|
115
|
+
STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
|
|
105
116
|
}
|
|
106
117
|
});
|
|
107
118
|
|
|
@@ -117,18 +128,18 @@ var init_transport = __esm({
|
|
|
117
128
|
});
|
|
118
129
|
|
|
119
130
|
// src/constants/metrics.ts
|
|
120
|
-
var METRICS_DIR, METRICS_FILE, PORT_FILE,
|
|
131
|
+
var METRICS_DIR, METRICS_FILE, PORT_FILE, ISSUES_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, ISSUES_FLUSH_INTERVAL_MS;
|
|
121
132
|
var init_metrics = __esm({
|
|
122
133
|
"src/constants/metrics.ts"() {
|
|
123
134
|
"use strict";
|
|
124
135
|
METRICS_DIR = ".brakit";
|
|
125
|
-
METRICS_FILE = "
|
|
136
|
+
METRICS_FILE = "metrics.json";
|
|
126
137
|
PORT_FILE = ".brakit/port";
|
|
127
|
-
|
|
138
|
+
ISSUES_FILE = "issues.json";
|
|
128
139
|
METRICS_FLUSH_INTERVAL_MS = 3e4;
|
|
129
140
|
METRICS_MAX_SESSIONS = 50;
|
|
130
141
|
METRICS_MAX_DATA_POINTS = 200;
|
|
131
|
-
|
|
142
|
+
ISSUES_FLUSH_INTERVAL_MS = 1e4;
|
|
132
143
|
}
|
|
133
144
|
});
|
|
134
145
|
|
|
@@ -149,7 +160,7 @@ var init_headers = __esm({
|
|
|
149
160
|
});
|
|
150
161
|
|
|
151
162
|
// src/constants/network.ts
|
|
152
|
-
var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE;
|
|
163
|
+
var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE, DIR_MODE_OWNER_ONLY, FILE_MODE_OWNER_ONLY;
|
|
153
164
|
var init_network = __esm({
|
|
154
165
|
"src/constants/network.ts"() {
|
|
155
166
|
"use strict";
|
|
@@ -179,6 +190,8 @@ var init_network = __esm({
|
|
|
179
190
|
LOCALHOST_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
180
191
|
LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
181
192
|
URL_PARSE_BASE = "http://localhost";
|
|
193
|
+
DIR_MODE_OWNER_ONLY = 448;
|
|
194
|
+
FILE_MODE_OWNER_ONLY = 384;
|
|
182
195
|
}
|
|
183
196
|
});
|
|
184
197
|
|
|
@@ -214,26 +227,35 @@ var init_severity = __esm({
|
|
|
214
227
|
});
|
|
215
228
|
|
|
216
229
|
// src/constants/telemetry.ts
|
|
217
|
-
var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
|
|
230
|
+
var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, SPEED_BUCKET_THRESHOLDS;
|
|
218
231
|
var init_telemetry = __esm({
|
|
219
232
|
"src/constants/telemetry.ts"() {
|
|
220
233
|
"use strict";
|
|
221
234
|
POSTHOG_HOST = "https://us.i.posthog.com";
|
|
222
235
|
POSTHOG_CAPTURE_PATH = "/i/v0/e/";
|
|
223
236
|
POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
|
|
237
|
+
SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
|
|
224
238
|
}
|
|
225
239
|
});
|
|
226
240
|
|
|
227
241
|
// src/constants/lifecycle.ts
|
|
228
|
-
var
|
|
242
|
+
var VALID_ISSUE_STATES, VALID_ISSUE_CATEGORIES, VALID_AI_FIX_STATUSES;
|
|
229
243
|
var init_lifecycle = __esm({
|
|
230
244
|
"src/constants/lifecycle.ts"() {
|
|
231
245
|
"use strict";
|
|
232
|
-
|
|
246
|
+
VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
|
|
247
|
+
VALID_ISSUE_CATEGORIES = /* @__PURE__ */ new Set(["security", "performance", "reliability"]);
|
|
233
248
|
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
234
249
|
}
|
|
235
250
|
});
|
|
236
251
|
|
|
252
|
+
// src/constants/cli.ts
|
|
253
|
+
var init_cli = __esm({
|
|
254
|
+
"src/constants/cli.ts"() {
|
|
255
|
+
"use strict";
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
237
259
|
// src/constants/index.ts
|
|
238
260
|
var init_constants = __esm({
|
|
239
261
|
"src/constants/index.ts"() {
|
|
@@ -250,6 +272,7 @@ var init_constants = __esm({
|
|
|
250
272
|
init_severity();
|
|
251
273
|
init_telemetry();
|
|
252
274
|
init_lifecycle();
|
|
275
|
+
init_cli();
|
|
253
276
|
}
|
|
254
277
|
});
|
|
255
278
|
|
|
@@ -828,6 +851,22 @@ var init_http = __esm({
|
|
|
828
851
|
}
|
|
829
852
|
});
|
|
830
853
|
|
|
854
|
+
// src/utils/http-status.ts
|
|
855
|
+
function isErrorStatus(code) {
|
|
856
|
+
return code >= 400;
|
|
857
|
+
}
|
|
858
|
+
function isServerError(code) {
|
|
859
|
+
return code >= 500;
|
|
860
|
+
}
|
|
861
|
+
function isRedirect(code) {
|
|
862
|
+
return code >= 300 && code < 400;
|
|
863
|
+
}
|
|
864
|
+
var init_http_status = __esm({
|
|
865
|
+
"src/utils/http-status.ts"() {
|
|
866
|
+
"use strict";
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
831
870
|
// src/analysis/categorize.ts
|
|
832
871
|
function detectCategory(req) {
|
|
833
872
|
const { method, url, statusCode, responseHeaders } = req;
|
|
@@ -896,7 +935,7 @@ function labelRequest(req) {
|
|
|
896
935
|
function generateHumanLabel(req, category) {
|
|
897
936
|
const effectivePath = getEffectivePath(req);
|
|
898
937
|
const endpointName = getEndpointName(effectivePath);
|
|
899
|
-
const failed = req.statusCode
|
|
938
|
+
const failed = isErrorStatus(req.statusCode);
|
|
900
939
|
switch (category) {
|
|
901
940
|
case "auth-handshake":
|
|
902
941
|
return "Auth handshake";
|
|
@@ -991,6 +1030,7 @@ var init_label = __esm({
|
|
|
991
1030
|
"use strict";
|
|
992
1031
|
init_constants();
|
|
993
1032
|
init_categorize();
|
|
1033
|
+
init_http_status();
|
|
994
1034
|
}
|
|
995
1035
|
});
|
|
996
1036
|
|
|
@@ -1083,7 +1123,7 @@ function detectWarnings(requests) {
|
|
|
1083
1123
|
for (const req of slowRequests) {
|
|
1084
1124
|
warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
|
|
1085
1125
|
}
|
|
1086
|
-
const errors = requests.filter((r) => r.statusCode
|
|
1126
|
+
const errors = requests.filter((r) => isServerError(r.statusCode));
|
|
1087
1127
|
for (const req of errors) {
|
|
1088
1128
|
warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
|
|
1089
1129
|
}
|
|
@@ -1095,6 +1135,7 @@ var init_transforms = __esm({
|
|
|
1095
1135
|
init_constants();
|
|
1096
1136
|
init_categorize();
|
|
1097
1137
|
init_label();
|
|
1138
|
+
init_http_status();
|
|
1098
1139
|
}
|
|
1099
1140
|
});
|
|
1100
1141
|
|
|
@@ -1148,7 +1189,7 @@ function buildFlow(rawRequests) {
|
|
|
1148
1189
|
requests,
|
|
1149
1190
|
startTime,
|
|
1150
1191
|
totalDurationMs: Math.round(endTime - startTime),
|
|
1151
|
-
hasErrors: requests.some((r) => r.statusCode
|
|
1192
|
+
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
|
|
1152
1193
|
warnings: detectWarnings(rawRequests),
|
|
1153
1194
|
sourcePage,
|
|
1154
1195
|
redundancyPct
|
|
@@ -1202,6 +1243,7 @@ var init_group = __esm({
|
|
|
1202
1243
|
"src/analysis/group.ts"() {
|
|
1203
1244
|
"use strict";
|
|
1204
1245
|
init_constants();
|
|
1246
|
+
init_http_status();
|
|
1205
1247
|
init_label();
|
|
1206
1248
|
init_categorize();
|
|
1207
1249
|
init_transforms();
|
|
@@ -1319,11 +1361,9 @@ function createRequestsHandler(registry) {
|
|
|
1319
1361
|
const method = url.searchParams.get("method");
|
|
1320
1362
|
const status = url.searchParams.get("status");
|
|
1321
1363
|
const search = url.searchParams.get("search");
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
);
|
|
1326
|
-
const offset = parseInt(url.searchParams.get("offset") ?? "0", 10);
|
|
1364
|
+
const rawLimit = parseInt(url.searchParams.get("limit") ?? String(DEFAULT_API_LIMIT), 10);
|
|
1365
|
+
const limit = Math.min(Math.max(rawLimit || DEFAULT_API_LIMIT, 1), MAX_API_LIMIT);
|
|
1366
|
+
const offset = Math.max(parseInt(url.searchParams.get("offset") ?? "0", 10) || 0, 0);
|
|
1327
1367
|
let results = [...registry.get("request-store").getAll()].reverse();
|
|
1328
1368
|
if (method) {
|
|
1329
1369
|
results = results.filter((r) => r.method === method.toUpperCase());
|
|
@@ -1373,7 +1413,7 @@ function createClearHandler(registry) {
|
|
|
1373
1413
|
registry.get("error-store").clear();
|
|
1374
1414
|
registry.get("query-store").clear();
|
|
1375
1415
|
registry.get("metrics-store").reset();
|
|
1376
|
-
if (registry.has("
|
|
1416
|
+
if (registry.has("issue-store")) registry.get("issue-store").clear();
|
|
1377
1417
|
registry.get("event-bus").emit("store:cleared", void 0);
|
|
1378
1418
|
sendJson(req, res, HTTP_OK, { cleared: true });
|
|
1379
1419
|
};
|
|
@@ -1415,16 +1455,32 @@ function getErrorMessage(err) {
|
|
|
1415
1455
|
if (typeof err === "string") return err;
|
|
1416
1456
|
return String(err);
|
|
1417
1457
|
}
|
|
1418
|
-
function
|
|
1419
|
-
return typeof val === "string" &&
|
|
1458
|
+
function isValidIssueState(val) {
|
|
1459
|
+
return typeof val === "string" && VALID_ISSUE_STATES.has(val);
|
|
1460
|
+
}
|
|
1461
|
+
function isValidIssueCategory(val) {
|
|
1462
|
+
return typeof val === "string" && VALID_ISSUE_CATEGORIES.has(val);
|
|
1420
1463
|
}
|
|
1421
1464
|
function isValidAiFixStatus(val) {
|
|
1422
1465
|
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
1423
1466
|
}
|
|
1467
|
+
function validateIssuesData(parsed) {
|
|
1468
|
+
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
|
|
1469
|
+
return parsed;
|
|
1470
|
+
}
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
function validateMetricsData(parsed) {
|
|
1474
|
+
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === 1 && Array.isArray(parsed.endpoints)) {
|
|
1475
|
+
return parsed;
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1424
1479
|
var init_type_guards = __esm({
|
|
1425
1480
|
"src/utils/type-guards.ts"() {
|
|
1426
1481
|
"use strict";
|
|
1427
1482
|
init_lifecycle();
|
|
1483
|
+
init_limits();
|
|
1428
1484
|
}
|
|
1429
1485
|
});
|
|
1430
1486
|
|
|
@@ -1762,46 +1818,42 @@ var init_api = __esm({
|
|
|
1762
1818
|
}
|
|
1763
1819
|
});
|
|
1764
1820
|
|
|
1765
|
-
// src/dashboard/api/
|
|
1766
|
-
function
|
|
1767
|
-
return (req, res) => {
|
|
1768
|
-
if (!requireGet(req, res)) return;
|
|
1769
|
-
sendJson(req, res, HTTP_OK, { insights: engine.getStatefulInsights() });
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
function createSecurityHandler(engine) {
|
|
1821
|
+
// src/dashboard/api/issues.ts
|
|
1822
|
+
function createIssuesHandler(issueStore) {
|
|
1773
1823
|
return (req, res) => {
|
|
1774
1824
|
if (!requireGet(req, res)) return;
|
|
1775
|
-
|
|
1825
|
+
const url = parseRequestUrl(req);
|
|
1826
|
+
const stateParam = url.searchParams.get("state");
|
|
1827
|
+
const categoryParam = url.searchParams.get("category");
|
|
1828
|
+
let issues;
|
|
1829
|
+
if (stateParam && isValidIssueState(stateParam)) {
|
|
1830
|
+
issues = issueStore.getByState(stateParam);
|
|
1831
|
+
} else if (categoryParam && isValidIssueCategory(categoryParam)) {
|
|
1832
|
+
issues = issueStore.getByCategory(categoryParam);
|
|
1833
|
+
} else {
|
|
1834
|
+
issues = issueStore.getAll();
|
|
1835
|
+
}
|
|
1836
|
+
sendJson(req, res, HTTP_OK, { issues });
|
|
1776
1837
|
};
|
|
1777
1838
|
}
|
|
1778
|
-
|
|
1779
|
-
"src/dashboard/api/insights.ts"() {
|
|
1780
|
-
"use strict";
|
|
1781
|
-
init_shared2();
|
|
1782
|
-
init_http();
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
|
|
1786
|
-
// src/dashboard/api/findings.ts
|
|
1787
|
-
function createFindingsHandler(findingStore) {
|
|
1839
|
+
function createFindingsHandler(issueStore) {
|
|
1788
1840
|
return (req, res) => {
|
|
1789
1841
|
if (!requireGet(req, res)) return;
|
|
1790
1842
|
const url = parseRequestUrl(req);
|
|
1791
1843
|
const stateParam = url.searchParams.get("state");
|
|
1792
|
-
let
|
|
1793
|
-
if (stateParam &&
|
|
1794
|
-
|
|
1844
|
+
let issues;
|
|
1845
|
+
if (stateParam && isValidIssueState(stateParam)) {
|
|
1846
|
+
issues = issueStore.getByState(stateParam);
|
|
1795
1847
|
} else {
|
|
1796
|
-
|
|
1848
|
+
issues = issueStore.getAll();
|
|
1797
1849
|
}
|
|
1798
1850
|
sendJson(req, res, HTTP_OK, {
|
|
1799
|
-
total:
|
|
1800
|
-
findings
|
|
1851
|
+
total: issues.length,
|
|
1852
|
+
findings: issues
|
|
1801
1853
|
});
|
|
1802
1854
|
};
|
|
1803
1855
|
}
|
|
1804
|
-
function
|
|
1856
|
+
function createIssuesReportHandler(issueStore, eventBus) {
|
|
1805
1857
|
return async (req, res) => {
|
|
1806
1858
|
if (req.method !== "POST") {
|
|
1807
1859
|
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
@@ -1822,27 +1874,16 @@ function createFindingsReportHandler(findingStore, eventBus, analysisEngine) {
|
|
|
1822
1874
|
sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
|
|
1823
1875
|
return;
|
|
1824
1876
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
eventBus.emit("findings:changed", findingStore.getAll());
|
|
1828
|
-
sendJson(req, res, HTTP_OK, { ok: true });
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
if (analysisEngine?.reportInsightFix(findingId, status, notes)) {
|
|
1832
|
-
eventBus.emit("analysis:updated", {
|
|
1833
|
-
insights: analysisEngine.getInsights(),
|
|
1834
|
-
findings: analysisEngine.getFindings(),
|
|
1835
|
-
statefulFindings: analysisEngine.getStatefulFindings(),
|
|
1836
|
-
statefulInsights: analysisEngine.getStatefulInsights()
|
|
1837
|
-
});
|
|
1877
|
+
if (issueStore.reportFix(findingId, status, notes)) {
|
|
1878
|
+
eventBus.emit("issues:changed", issueStore.getAll());
|
|
1838
1879
|
sendJson(req, res, HTTP_OK, { ok: true });
|
|
1839
1880
|
return;
|
|
1840
1881
|
}
|
|
1841
1882
|
sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
|
|
1842
1883
|
};
|
|
1843
1884
|
}
|
|
1844
|
-
var
|
|
1845
|
-
"src/dashboard/api/
|
|
1885
|
+
var init_issues = __esm({
|
|
1886
|
+
"src/dashboard/api/issues.ts"() {
|
|
1846
1887
|
"use strict";
|
|
1847
1888
|
init_shared2();
|
|
1848
1889
|
init_type_guards();
|
|
@@ -1851,7 +1892,7 @@ var init_findings = __esm({
|
|
|
1851
1892
|
});
|
|
1852
1893
|
|
|
1853
1894
|
// src/constants/events.ts
|
|
1854
|
-
var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY,
|
|
1895
|
+
var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_ISSUES;
|
|
1855
1896
|
var init_events = __esm({
|
|
1856
1897
|
"src/constants/events.ts"() {
|
|
1857
1898
|
"use strict";
|
|
@@ -1859,8 +1900,7 @@ var init_events = __esm({
|
|
|
1859
1900
|
SSE_EVENT_LOG = "log";
|
|
1860
1901
|
SSE_EVENT_ERROR = "error_event";
|
|
1861
1902
|
SSE_EVENT_QUERY = "query";
|
|
1862
|
-
|
|
1863
|
-
SSE_EVENT_SECURITY = "security";
|
|
1903
|
+
SSE_EVENT_ISSUES = "issues";
|
|
1864
1904
|
}
|
|
1865
1905
|
});
|
|
1866
1906
|
|
|
@@ -1893,12 +1933,11 @@ data: ${data}
|
|
|
1893
1933
|
bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
|
|
1894
1934
|
bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
|
|
1895
1935
|
bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
|
|
1896
|
-
bus.on("analysis:updated", ({
|
|
1897
|
-
broadcast(
|
|
1898
|
-
broadcast(SSE_EVENT_SECURITY, JSON.stringify(statefulFindings));
|
|
1936
|
+
bus.on("analysis:updated", ({ issues }) => {
|
|
1937
|
+
broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
|
|
1899
1938
|
});
|
|
1900
|
-
bus.on("
|
|
1901
|
-
broadcast(
|
|
1939
|
+
bus.on("issues:changed", (issues) => {
|
|
1940
|
+
broadcast(SSE_EVENT_ISSUES, JSON.stringify(issues));
|
|
1902
1941
|
});
|
|
1903
1942
|
return (req, res) => {
|
|
1904
1943
|
const headers2 = {
|
|
@@ -2531,7 +2570,14 @@ var init_styles = __esm({
|
|
|
2531
2570
|
// src/utils/fs.ts
|
|
2532
2571
|
import { access, readFile, writeFile } from "fs/promises";
|
|
2533
2572
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2534
|
-
import {
|
|
2573
|
+
import { createHash } from "crypto";
|
|
2574
|
+
import { homedir } from "os";
|
|
2575
|
+
import { resolve, join } from "path";
|
|
2576
|
+
function getProjectDataDir(projectRoot) {
|
|
2577
|
+
const absolute = resolve(projectRoot);
|
|
2578
|
+
const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
|
|
2579
|
+
return join(homedir(), ".brakit", "projects", hash);
|
|
2580
|
+
}
|
|
2535
2581
|
async function fileExists(path) {
|
|
2536
2582
|
try {
|
|
2537
2583
|
await access(path);
|
|
@@ -2550,7 +2596,8 @@ function ensureGitignore(dir, entry) {
|
|
|
2550
2596
|
} else {
|
|
2551
2597
|
writeFileSync(gitignorePath, entry + "\n");
|
|
2552
2598
|
}
|
|
2553
|
-
} catch {
|
|
2599
|
+
} catch (err) {
|
|
2600
|
+
brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
|
|
2554
2601
|
}
|
|
2555
2602
|
}
|
|
2556
2603
|
async function ensureGitignoreAsync(dir, entry) {
|
|
@@ -2563,12 +2610,16 @@ async function ensureGitignoreAsync(dir, entry) {
|
|
|
2563
2610
|
} else {
|
|
2564
2611
|
await writeFile(gitignorePath, entry + "\n");
|
|
2565
2612
|
}
|
|
2566
|
-
} catch {
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
|
|
2567
2615
|
}
|
|
2568
2616
|
}
|
|
2569
2617
|
var init_fs = __esm({
|
|
2570
2618
|
"src/utils/fs.ts"() {
|
|
2571
2619
|
"use strict";
|
|
2620
|
+
init_limits();
|
|
2621
|
+
init_log();
|
|
2622
|
+
init_type_guards();
|
|
2572
2623
|
}
|
|
2573
2624
|
});
|
|
2574
2625
|
|
|
@@ -2646,60 +2697,57 @@ var init_atomic_writer = __esm({
|
|
|
2646
2697
|
}
|
|
2647
2698
|
});
|
|
2648
2699
|
|
|
2649
|
-
// src/
|
|
2650
|
-
import { createHash } from "crypto";
|
|
2651
|
-
function
|
|
2652
|
-
const
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
function computeInsightId(type, endpoint, desc) {
|
|
2656
|
-
const key = `${type}:${endpoint}:${desc}`;
|
|
2657
|
-
return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
|
|
2700
|
+
// src/utils/issue-id.ts
|
|
2701
|
+
import { createHash as createHash2 } from "crypto";
|
|
2702
|
+
function computeIssueId(issue) {
|
|
2703
|
+
const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
|
|
2704
|
+
const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
|
|
2705
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
|
|
2658
2706
|
}
|
|
2659
|
-
var
|
|
2660
|
-
"src/
|
|
2707
|
+
var init_issue_id = __esm({
|
|
2708
|
+
"src/utils/issue-id.ts"() {
|
|
2661
2709
|
"use strict";
|
|
2662
2710
|
init_limits();
|
|
2663
2711
|
}
|
|
2664
2712
|
});
|
|
2665
2713
|
|
|
2666
|
-
// src/store/
|
|
2714
|
+
// src/store/issue-store.ts
|
|
2667
2715
|
import { readFile as readFile2 } from "fs/promises";
|
|
2668
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
2716
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
|
|
2669
2717
|
import { resolve as resolve2 } from "path";
|
|
2670
|
-
var
|
|
2671
|
-
var
|
|
2672
|
-
"src/store/
|
|
2718
|
+
var IssueStore;
|
|
2719
|
+
var init_issue_store = __esm({
|
|
2720
|
+
"src/store/issue-store.ts"() {
|
|
2673
2721
|
"use strict";
|
|
2674
2722
|
init_fs();
|
|
2675
|
-
|
|
2723
|
+
init_metrics();
|
|
2724
|
+
init_limits();
|
|
2725
|
+
init_thresholds();
|
|
2676
2726
|
init_limits();
|
|
2677
2727
|
init_atomic_writer();
|
|
2678
2728
|
init_log();
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
this.
|
|
2729
|
+
init_type_guards();
|
|
2730
|
+
init_issue_id();
|
|
2731
|
+
IssueStore = class {
|
|
2732
|
+
constructor(dataDir) {
|
|
2733
|
+
this.dataDir = dataDir;
|
|
2734
|
+
this.issuesPath = resolve2(dataDir, ISSUES_FILE);
|
|
2685
2735
|
this.writer = new AtomicWriter({
|
|
2686
|
-
dir:
|
|
2687
|
-
filePath: this.
|
|
2688
|
-
|
|
2689
|
-
label: "findings"
|
|
2736
|
+
dir: dataDir,
|
|
2737
|
+
filePath: this.issuesPath,
|
|
2738
|
+
label: "issues"
|
|
2690
2739
|
});
|
|
2691
2740
|
}
|
|
2692
|
-
|
|
2741
|
+
issues = /* @__PURE__ */ new Map();
|
|
2693
2742
|
flushTimer = null;
|
|
2694
2743
|
dirty = false;
|
|
2695
2744
|
writer;
|
|
2696
|
-
|
|
2745
|
+
issuesPath;
|
|
2697
2746
|
start() {
|
|
2698
|
-
this.loadAsync().catch(() => {
|
|
2699
|
-
});
|
|
2747
|
+
this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
|
|
2700
2748
|
this.flushTimer = setInterval(
|
|
2701
2749
|
() => this.flush(),
|
|
2702
|
-
|
|
2750
|
+
ISSUES_FLUSH_INTERVAL_MS
|
|
2703
2751
|
);
|
|
2704
2752
|
this.flushTimer.unref();
|
|
2705
2753
|
}
|
|
@@ -2710,119 +2758,148 @@ var init_finding_store = __esm({
|
|
|
2710
2758
|
}
|
|
2711
2759
|
this.flushSync();
|
|
2712
2760
|
}
|
|
2713
|
-
upsert(
|
|
2714
|
-
const id =
|
|
2715
|
-
const existing = this.
|
|
2761
|
+
upsert(issue, source) {
|
|
2762
|
+
const id = computeIssueId(issue);
|
|
2763
|
+
const existing = this.issues.get(id);
|
|
2716
2764
|
const now = Date.now();
|
|
2717
2765
|
if (existing) {
|
|
2718
2766
|
existing.lastSeenAt = now;
|
|
2719
2767
|
existing.occurrences++;
|
|
2720
|
-
existing.
|
|
2721
|
-
|
|
2722
|
-
|
|
2768
|
+
existing.issue = issue;
|
|
2769
|
+
existing.cleanHitsSinceLastSeen = 0;
|
|
2770
|
+
if (existing.state === "resolved" || existing.state === "stale") {
|
|
2771
|
+
existing.state = "regressed";
|
|
2723
2772
|
existing.resolvedAt = null;
|
|
2724
2773
|
}
|
|
2725
2774
|
this.dirty = true;
|
|
2726
2775
|
return existing;
|
|
2727
2776
|
}
|
|
2728
2777
|
const stateful = {
|
|
2729
|
-
|
|
2778
|
+
issueId: id,
|
|
2730
2779
|
state: "open",
|
|
2731
2780
|
source,
|
|
2732
|
-
|
|
2781
|
+
category: issue.category,
|
|
2782
|
+
issue,
|
|
2733
2783
|
firstSeenAt: now,
|
|
2734
2784
|
lastSeenAt: now,
|
|
2735
2785
|
resolvedAt: null,
|
|
2736
2786
|
occurrences: 1,
|
|
2787
|
+
cleanHitsSinceLastSeen: 0,
|
|
2737
2788
|
aiStatus: null,
|
|
2738
2789
|
aiNotes: null
|
|
2739
2790
|
};
|
|
2740
|
-
this.
|
|
2791
|
+
this.issues.set(id, stateful);
|
|
2741
2792
|
this.dirty = true;
|
|
2742
2793
|
return stateful;
|
|
2743
2794
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2795
|
+
/**
|
|
2796
|
+
* Reconcile issues against the current analysis results using evidence-based resolution.
|
|
2797
|
+
*
|
|
2798
|
+
* @param currentIssueIds - IDs of issues detected in the current analysis cycle
|
|
2799
|
+
* @param activeEndpoints - Endpoints that had requests in the current cycle
|
|
2800
|
+
*/
|
|
2801
|
+
reconcile(currentIssueIds, activeEndpoints) {
|
|
2802
|
+
const now = Date.now();
|
|
2803
|
+
for (const [, stateful] of this.issues) {
|
|
2804
|
+
const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
|
|
2805
|
+
if (!isActive) continue;
|
|
2806
|
+
if (currentIssueIds.has(stateful.issueId)) continue;
|
|
2807
|
+
const endpoint = stateful.issue.endpoint;
|
|
2808
|
+
if (endpoint && activeEndpoints.has(endpoint)) {
|
|
2809
|
+
stateful.cleanHitsSinceLastSeen++;
|
|
2810
|
+
if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
|
|
2811
|
+
stateful.state = "resolved";
|
|
2812
|
+
stateful.resolvedAt = now;
|
|
2813
|
+
}
|
|
2814
|
+
this.dirty = true;
|
|
2815
|
+
} else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
|
|
2816
|
+
stateful.state = "stale";
|
|
2817
|
+
this.dirty = true;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
for (const [id, stateful] of this.issues) {
|
|
2821
|
+
if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
|
|
2822
|
+
this.issues.delete(id);
|
|
2823
|
+
this.dirty = true;
|
|
2824
|
+
} else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
|
|
2825
|
+
this.issues.delete(id);
|
|
2826
|
+
this.dirty = true;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
transition(issueId, state) {
|
|
2831
|
+
const issue = this.issues.get(issueId);
|
|
2832
|
+
if (!issue) return false;
|
|
2833
|
+
issue.state = state;
|
|
2748
2834
|
if (state === "resolved") {
|
|
2749
|
-
|
|
2835
|
+
issue.resolvedAt = Date.now();
|
|
2750
2836
|
}
|
|
2751
2837
|
this.dirty = true;
|
|
2752
2838
|
return true;
|
|
2753
2839
|
}
|
|
2754
|
-
reportFix(
|
|
2755
|
-
const
|
|
2756
|
-
if (!
|
|
2757
|
-
|
|
2758
|
-
|
|
2840
|
+
reportFix(issueId, status, notes) {
|
|
2841
|
+
const issue = this.issues.get(issueId);
|
|
2842
|
+
if (!issue) return false;
|
|
2843
|
+
issue.aiStatus = status;
|
|
2844
|
+
issue.aiNotes = notes;
|
|
2759
2845
|
if (status === "fixed") {
|
|
2760
|
-
|
|
2846
|
+
issue.state = "fixing";
|
|
2761
2847
|
}
|
|
2762
2848
|
this.dirty = true;
|
|
2763
2849
|
return true;
|
|
2764
2850
|
}
|
|
2765
|
-
/**
|
|
2766
|
-
* Reconcile passive findings against the current analysis results.
|
|
2767
|
-
*
|
|
2768
|
-
* Passive findings are detected by continuous scanning (not user-triggered).
|
|
2769
|
-
* When a previously-seen finding is absent from the current results, it means
|
|
2770
|
-
* the issue has been fixed — transition it to "resolved" automatically.
|
|
2771
|
-
* Active findings (from MCP verify-fix) are not auto-resolved because they
|
|
2772
|
-
* require explicit verification.
|
|
2773
|
-
*/
|
|
2774
|
-
reconcilePassive(currentFindings) {
|
|
2775
|
-
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
2776
|
-
for (const [id, stateful] of this.findings) {
|
|
2777
|
-
if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
|
|
2778
|
-
stateful.state = "resolved";
|
|
2779
|
-
stateful.resolvedAt = Date.now();
|
|
2780
|
-
this.dirty = true;
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
2851
|
getAll() {
|
|
2785
|
-
return [...this.
|
|
2852
|
+
return [...this.issues.values()];
|
|
2786
2853
|
}
|
|
2787
2854
|
getByState(state) {
|
|
2788
|
-
return [...this.
|
|
2855
|
+
return [...this.issues.values()].filter((i) => i.state === state);
|
|
2856
|
+
}
|
|
2857
|
+
getByCategory(category) {
|
|
2858
|
+
return [...this.issues.values()].filter((i) => i.category === category);
|
|
2789
2859
|
}
|
|
2790
|
-
get(
|
|
2791
|
-
return this.
|
|
2860
|
+
get(issueId) {
|
|
2861
|
+
return this.issues.get(issueId);
|
|
2792
2862
|
}
|
|
2793
2863
|
clear() {
|
|
2794
|
-
this.
|
|
2795
|
-
this.dirty =
|
|
2864
|
+
this.issues.clear();
|
|
2865
|
+
this.dirty = false;
|
|
2866
|
+
try {
|
|
2867
|
+
if (existsSync3(this.issuesPath)) {
|
|
2868
|
+
unlinkSync(this.issuesPath);
|
|
2869
|
+
}
|
|
2870
|
+
} catch {
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
isDirty() {
|
|
2874
|
+
return this.dirty;
|
|
2796
2875
|
}
|
|
2797
2876
|
async loadAsync() {
|
|
2798
2877
|
try {
|
|
2799
|
-
if (await fileExists(this.
|
|
2800
|
-
const raw = await readFile2(this.
|
|
2801
|
-
|
|
2802
|
-
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
2803
|
-
for (const f of parsed.findings) {
|
|
2804
|
-
this.findings.set(f.findingId, f);
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2878
|
+
if (await fileExists(this.issuesPath)) {
|
|
2879
|
+
const raw = await readFile2(this.issuesPath, "utf-8");
|
|
2880
|
+
this.hydrate(raw);
|
|
2807
2881
|
}
|
|
2808
2882
|
} catch (err) {
|
|
2809
|
-
brakitDebug(`
|
|
2883
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
2810
2884
|
}
|
|
2811
2885
|
}
|
|
2812
2886
|
/** Sync load for tests only — not used in production paths. */
|
|
2813
2887
|
loadSync() {
|
|
2814
2888
|
try {
|
|
2815
|
-
if (existsSync3(this.
|
|
2816
|
-
const raw = readFileSync2(this.
|
|
2817
|
-
|
|
2818
|
-
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
2819
|
-
for (const f of parsed.findings) {
|
|
2820
|
-
this.findings.set(f.findingId, f);
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2889
|
+
if (existsSync3(this.issuesPath)) {
|
|
2890
|
+
const raw = readFileSync2(this.issuesPath, "utf-8");
|
|
2891
|
+
this.hydrate(raw);
|
|
2823
2892
|
}
|
|
2824
2893
|
} catch (err) {
|
|
2825
|
-
brakitDebug(`
|
|
2894
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
/** Parse and populate issues from a raw JSON string. */
|
|
2898
|
+
hydrate(raw) {
|
|
2899
|
+
const validated = validateIssuesData(JSON.parse(raw));
|
|
2900
|
+
if (!validated) return;
|
|
2901
|
+
for (const issue of validated.issues) {
|
|
2902
|
+
this.issues.set(issue.issueId, issue);
|
|
2826
2903
|
}
|
|
2827
2904
|
}
|
|
2828
2905
|
flush() {
|
|
@@ -2837,8 +2914,8 @@ var init_finding_store = __esm({
|
|
|
2837
2914
|
}
|
|
2838
2915
|
serialize() {
|
|
2839
2916
|
const data = {
|
|
2840
|
-
version:
|
|
2841
|
-
|
|
2917
|
+
version: ISSUES_DATA_VERSION,
|
|
2918
|
+
issues: [...this.issues.values()]
|
|
2842
2919
|
};
|
|
2843
2920
|
return JSON.stringify(data);
|
|
2844
2921
|
}
|
|
@@ -2849,7 +2926,7 @@ var init_finding_store = __esm({
|
|
|
2849
2926
|
// src/detect/project.ts
|
|
2850
2927
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
2851
2928
|
import { existsSync as existsSync4 } from "fs";
|
|
2852
|
-
import { join, relative } from "path";
|
|
2929
|
+
import { join as join2, relative } from "path";
|
|
2853
2930
|
function detectFrameworkFromDeps(allDeps) {
|
|
2854
2931
|
for (const f of FRAMEWORKS) {
|
|
2855
2932
|
if (allDeps[f.dep]) return f.name;
|
|
@@ -2857,10 +2934,10 @@ function detectFrameworkFromDeps(allDeps) {
|
|
|
2857
2934
|
return "unknown";
|
|
2858
2935
|
}
|
|
2859
2936
|
function detectPackageManagerSync(rootDir) {
|
|
2860
|
-
if (existsSync4(
|
|
2861
|
-
if (existsSync4(
|
|
2862
|
-
if (existsSync4(
|
|
2863
|
-
if (existsSync4(
|
|
2937
|
+
if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
|
|
2938
|
+
if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
2939
|
+
if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
2940
|
+
if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
|
|
2864
2941
|
return "unknown";
|
|
2865
2942
|
}
|
|
2866
2943
|
var FRAMEWORKS;
|
|
@@ -2878,6 +2955,44 @@ var init_project = __esm({
|
|
|
2878
2955
|
}
|
|
2879
2956
|
});
|
|
2880
2957
|
|
|
2958
|
+
// src/utils/response.ts
|
|
2959
|
+
function tryParseJson(body) {
|
|
2960
|
+
if (!body) return null;
|
|
2961
|
+
try {
|
|
2962
|
+
return JSON.parse(body);
|
|
2963
|
+
} catch {
|
|
2964
|
+
return null;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
function unwrapResponse(parsed) {
|
|
2968
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
2969
|
+
const obj = parsed;
|
|
2970
|
+
const keys = Object.keys(obj);
|
|
2971
|
+
if (keys.length > 3) return parsed;
|
|
2972
|
+
let best = null;
|
|
2973
|
+
let bestSize = 0;
|
|
2974
|
+
for (const key of keys) {
|
|
2975
|
+
const val = obj[key];
|
|
2976
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
2977
|
+
best = val;
|
|
2978
|
+
bestSize = val.length;
|
|
2979
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
2980
|
+
const size = Object.keys(val).length;
|
|
2981
|
+
if (size > bestSize) {
|
|
2982
|
+
best = val;
|
|
2983
|
+
bestSize = size;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
2988
|
+
}
|
|
2989
|
+
var init_response = __esm({
|
|
2990
|
+
"src/utils/response.ts"() {
|
|
2991
|
+
"use strict";
|
|
2992
|
+
init_thresholds();
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2881
2996
|
// src/analysis/rules/patterns.ts
|
|
2882
2997
|
var SECRET_KEYS, TOKEN_PARAMS, SAFE_PARAMS, STACK_TRACE_RE, DB_CONN_RE, SQL_FRAGMENT_RE, SECRET_VAL_RE, LOG_SECRET_RE, MASKED_RE, EMAIL_RE, INTERNAL_ID_KEYS, INTERNAL_ID_SUFFIX, SELECT_STAR_RE, SELECT_DOT_STAR_RE, RULE_HINTS;
|
|
2883
2998
|
var init_patterns = __esm({
|
|
@@ -2911,30 +3026,23 @@ var init_patterns = __esm({
|
|
|
2911
3026
|
});
|
|
2912
3027
|
|
|
2913
3028
|
// src/analysis/rules/exposed-secret.ts
|
|
2914
|
-
function
|
|
2915
|
-
if (!body) return null;
|
|
2916
|
-
try {
|
|
2917
|
-
return JSON.parse(body);
|
|
2918
|
-
} catch {
|
|
2919
|
-
return null;
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
function findSecretKeys(obj, prefix) {
|
|
3029
|
+
function findSecretKeys(obj, prefix, depth = 0) {
|
|
2923
3030
|
const found = [];
|
|
3031
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
|
|
2924
3032
|
if (!obj || typeof obj !== "object") return found;
|
|
2925
3033
|
if (Array.isArray(obj)) {
|
|
2926
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
2927
|
-
found.push(...findSecretKeys(obj[i], prefix));
|
|
3034
|
+
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
3035
|
+
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
2928
3036
|
}
|
|
2929
3037
|
return found;
|
|
2930
3038
|
}
|
|
2931
3039
|
for (const k of Object.keys(obj)) {
|
|
2932
3040
|
const val = obj[k];
|
|
2933
|
-
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >=
|
|
3041
|
+
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
|
|
2934
3042
|
found.push(k);
|
|
2935
3043
|
}
|
|
2936
3044
|
if (typeof val === "object" && val !== null) {
|
|
2937
|
-
found.push(...findSecretKeys(val, prefix + k + "."));
|
|
3045
|
+
found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
|
|
2938
3046
|
}
|
|
2939
3047
|
}
|
|
2940
3048
|
return found;
|
|
@@ -2944,6 +3052,8 @@ var init_exposed_secret = __esm({
|
|
|
2944
3052
|
"src/analysis/rules/exposed-secret.ts"() {
|
|
2945
3053
|
"use strict";
|
|
2946
3054
|
init_patterns();
|
|
3055
|
+
init_limits();
|
|
3056
|
+
init_http_status();
|
|
2947
3057
|
exposedSecretRule = {
|
|
2948
3058
|
id: "exposed-secret",
|
|
2949
3059
|
severity: "critical",
|
|
@@ -2953,8 +3063,8 @@ var init_exposed_secret = __esm({
|
|
|
2953
3063
|
const findings = [];
|
|
2954
3064
|
const seen = /* @__PURE__ */ new Map();
|
|
2955
3065
|
for (const r of ctx.requests) {
|
|
2956
|
-
if (r.statusCode
|
|
2957
|
-
const parsed =
|
|
3066
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
3067
|
+
const parsed = ctx.parsedBodies.response.get(r.id);
|
|
2958
3068
|
if (!parsed) continue;
|
|
2959
3069
|
const keys = findSecretKeys(parsed, "");
|
|
2960
3070
|
if (keys.length === 0) continue;
|
|
@@ -3130,7 +3240,7 @@ var init_error_info_leak = __esm({
|
|
|
3130
3240
|
|
|
3131
3241
|
// src/analysis/rules/insecure-cookie.ts
|
|
3132
3242
|
function isFrameworkResponse(r) {
|
|
3133
|
-
if (r.statusCode
|
|
3243
|
+
if (isRedirect(r.statusCode)) return true;
|
|
3134
3244
|
if (r.path?.startsWith("/__")) return true;
|
|
3135
3245
|
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
3136
3246
|
return false;
|
|
@@ -3140,6 +3250,7 @@ var init_insecure_cookie = __esm({
|
|
|
3140
3250
|
"src/analysis/rules/insecure-cookie.ts"() {
|
|
3141
3251
|
"use strict";
|
|
3142
3252
|
init_patterns();
|
|
3253
|
+
init_http_status();
|
|
3143
3254
|
insecureCookieRule = {
|
|
3144
3255
|
id: "insecure-cookie",
|
|
3145
3256
|
severity: "warning",
|
|
@@ -3257,51 +3368,14 @@ var init_cors_credentials = __esm({
|
|
|
3257
3368
|
}
|
|
3258
3369
|
});
|
|
3259
3370
|
|
|
3260
|
-
// src/utils/response.ts
|
|
3261
|
-
function unwrapResponse(parsed) {
|
|
3262
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3263
|
-
const obj = parsed;
|
|
3264
|
-
const keys = Object.keys(obj);
|
|
3265
|
-
if (keys.length > 3) return parsed;
|
|
3266
|
-
let best = null;
|
|
3267
|
-
let bestSize = 0;
|
|
3268
|
-
for (const key of keys) {
|
|
3269
|
-
const val = obj[key];
|
|
3270
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
3271
|
-
best = val;
|
|
3272
|
-
bestSize = val.length;
|
|
3273
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3274
|
-
const size = Object.keys(val).length;
|
|
3275
|
-
if (size > bestSize) {
|
|
3276
|
-
best = val;
|
|
3277
|
-
bestSize = size;
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
3282
|
-
}
|
|
3283
|
-
var init_response = __esm({
|
|
3284
|
-
"src/utils/response.ts"() {
|
|
3285
|
-
"use strict";
|
|
3286
|
-
init_thresholds();
|
|
3287
|
-
}
|
|
3288
|
-
});
|
|
3289
|
-
|
|
3290
3371
|
// src/analysis/rules/response-pii-leak.ts
|
|
3291
|
-
function
|
|
3292
|
-
if (!body) return null;
|
|
3293
|
-
try {
|
|
3294
|
-
return JSON.parse(body);
|
|
3295
|
-
} catch {
|
|
3296
|
-
return null;
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
function findEmails(obj) {
|
|
3372
|
+
function findEmails(obj, depth = 0) {
|
|
3300
3373
|
const emails = [];
|
|
3374
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
|
|
3301
3375
|
if (!obj || typeof obj !== "object") return emails;
|
|
3302
3376
|
if (Array.isArray(obj)) {
|
|
3303
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
3304
|
-
emails.push(...findEmails(obj[i]));
|
|
3377
|
+
for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
3378
|
+
emails.push(...findEmails(obj[i], depth + 1));
|
|
3305
3379
|
}
|
|
3306
3380
|
return emails;
|
|
3307
3381
|
}
|
|
@@ -3309,7 +3383,7 @@ function findEmails(obj) {
|
|
|
3309
3383
|
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
3310
3384
|
emails.push(v);
|
|
3311
3385
|
} else if (typeof v === "object" && v !== null) {
|
|
3312
|
-
emails.push(...findEmails(v));
|
|
3386
|
+
emails.push(...findEmails(v, depth + 1));
|
|
3313
3387
|
}
|
|
3314
3388
|
}
|
|
3315
3389
|
return emails;
|
|
@@ -3352,7 +3426,7 @@ function detectFullRecordPII(target) {
|
|
|
3352
3426
|
function detectListPII(target) {
|
|
3353
3427
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
3354
3428
|
let itemsWithEmail = 0;
|
|
3355
|
-
for (let i = 0; i < Math.min(target.length,
|
|
3429
|
+
for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
3356
3430
|
const item = target[i];
|
|
3357
3431
|
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
3358
3432
|
itemsWithEmail++;
|
|
@@ -3369,15 +3443,15 @@ function detectPII(method, reqBody, resBody) {
|
|
|
3369
3443
|
const target = unwrapResponse(resBody);
|
|
3370
3444
|
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
3371
3445
|
}
|
|
3372
|
-
var WRITE_METHODS,
|
|
3446
|
+
var WRITE_METHODS, REASON_LABELS, responsePiiLeakRule;
|
|
3373
3447
|
var init_response_pii_leak = __esm({
|
|
3374
3448
|
"src/analysis/rules/response-pii-leak.ts"() {
|
|
3375
3449
|
"use strict";
|
|
3376
3450
|
init_patterns();
|
|
3377
3451
|
init_response();
|
|
3452
|
+
init_limits();
|
|
3453
|
+
init_http_status();
|
|
3378
3454
|
WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
3379
|
-
FULL_RECORD_MIN_FIELDS = 5;
|
|
3380
|
-
LIST_PII_MIN_ITEMS = 2;
|
|
3381
3455
|
REASON_LABELS = {
|
|
3382
3456
|
echo: "echoes back PII from the request body",
|
|
3383
3457
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -3392,10 +3466,10 @@ var init_response_pii_leak = __esm({
|
|
|
3392
3466
|
const findings = [];
|
|
3393
3467
|
const seen = /* @__PURE__ */ new Map();
|
|
3394
3468
|
for (const r of ctx.requests) {
|
|
3395
|
-
if (r.statusCode
|
|
3396
|
-
const resJson =
|
|
3469
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
3470
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
3397
3471
|
if (!resJson) continue;
|
|
3398
|
-
const reqJson =
|
|
3472
|
+
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
3399
3473
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
3400
3474
|
if (!detection) continue;
|
|
3401
3475
|
const ep = `${r.method} ${r.path}`;
|
|
@@ -3424,6 +3498,21 @@ var init_response_pii_leak = __esm({
|
|
|
3424
3498
|
});
|
|
3425
3499
|
|
|
3426
3500
|
// src/analysis/rules/scanner.ts
|
|
3501
|
+
function buildBodyCache(requests) {
|
|
3502
|
+
const response = /* @__PURE__ */ new Map();
|
|
3503
|
+
const request = /* @__PURE__ */ new Map();
|
|
3504
|
+
for (const r of requests) {
|
|
3505
|
+
if (r.responseBody) {
|
|
3506
|
+
const parsed = tryParseJson(r.responseBody);
|
|
3507
|
+
if (parsed != null) response.set(r.id, parsed);
|
|
3508
|
+
}
|
|
3509
|
+
if (r.requestBody) {
|
|
3510
|
+
const parsed = tryParseJson(r.requestBody);
|
|
3511
|
+
if (parsed != null) request.set(r.id, parsed);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return { response, request };
|
|
3515
|
+
}
|
|
3427
3516
|
function createDefaultScanner() {
|
|
3428
3517
|
const scanner = new SecurityScanner();
|
|
3429
3518
|
scanner.register(exposedSecretRule);
|
|
@@ -3440,6 +3529,7 @@ var SecurityScanner;
|
|
|
3440
3529
|
var init_scanner = __esm({
|
|
3441
3530
|
"src/analysis/rules/scanner.ts"() {
|
|
3442
3531
|
"use strict";
|
|
3532
|
+
init_response();
|
|
3443
3533
|
init_exposed_secret();
|
|
3444
3534
|
init_token_in_url();
|
|
3445
3535
|
init_stack_trace_leak();
|
|
@@ -3453,7 +3543,11 @@ var init_scanner = __esm({
|
|
|
3453
3543
|
register(rule) {
|
|
3454
3544
|
this.rules.push(rule);
|
|
3455
3545
|
}
|
|
3456
|
-
scan(
|
|
3546
|
+
scan(input) {
|
|
3547
|
+
const ctx = {
|
|
3548
|
+
...input,
|
|
3549
|
+
parsedBodies: buildBodyCache(input.requests)
|
|
3550
|
+
};
|
|
3457
3551
|
const findings = [];
|
|
3458
3552
|
for (const rule of this.rules) {
|
|
3459
3553
|
try {
|
|
@@ -3526,16 +3620,22 @@ var init_collections = __esm({
|
|
|
3526
3620
|
});
|
|
3527
3621
|
|
|
3528
3622
|
// src/utils/endpoint.ts
|
|
3623
|
+
function normalizePath(path) {
|
|
3624
|
+
const qIdx = path.indexOf("?");
|
|
3625
|
+
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
3626
|
+
return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
|
|
3627
|
+
}
|
|
3529
3628
|
function getEndpointKey(method, path) {
|
|
3530
|
-
return `${method} ${path}`;
|
|
3629
|
+
return `${method} ${normalizePath(path)}`;
|
|
3531
3630
|
}
|
|
3532
3631
|
function extractEndpointFromDesc(desc) {
|
|
3533
3632
|
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
3534
3633
|
}
|
|
3535
|
-
var ENDPOINT_PREFIX_RE;
|
|
3634
|
+
var DYNAMIC_SEGMENT_RE, ENDPOINT_PREFIX_RE;
|
|
3536
3635
|
var init_endpoint = __esm({
|
|
3537
3636
|
"src/utils/endpoint.ts"() {
|
|
3538
3637
|
"use strict";
|
|
3638
|
+
DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
|
|
3539
3639
|
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
3540
3640
|
}
|
|
3541
3641
|
});
|
|
@@ -3589,6 +3689,15 @@ function windowByEndpoint(requests) {
|
|
|
3589
3689
|
}
|
|
3590
3690
|
return windowed;
|
|
3591
3691
|
}
|
|
3692
|
+
function extractActiveEndpoints(requests) {
|
|
3693
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
3694
|
+
for (const r of requests) {
|
|
3695
|
+
if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
|
|
3696
|
+
endpoints.add(getEndpointKey(r.method, r.path));
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return endpoints;
|
|
3700
|
+
}
|
|
3592
3701
|
function prepareContext(ctx) {
|
|
3593
3702
|
const nonStatic = ctx.requests.filter(
|
|
3594
3703
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -3606,7 +3715,7 @@ function prepareContext(ctx) {
|
|
|
3606
3715
|
endpointGroups.set(ep, g);
|
|
3607
3716
|
}
|
|
3608
3717
|
g.total++;
|
|
3609
|
-
if (r.statusCode
|
|
3718
|
+
if (isErrorStatus(r.statusCode)) g.errors++;
|
|
3610
3719
|
g.totalDuration += r.durationMs;
|
|
3611
3720
|
g.totalSize += r.responseSize ?? 0;
|
|
3612
3721
|
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
@@ -3643,6 +3752,7 @@ var init_prepare = __esm({
|
|
|
3643
3752
|
init_collections();
|
|
3644
3753
|
init_endpoint();
|
|
3645
3754
|
init_constants();
|
|
3755
|
+
init_http_status();
|
|
3646
3756
|
init_thresholds();
|
|
3647
3757
|
init_query_helpers();
|
|
3648
3758
|
}
|
|
@@ -4127,6 +4237,7 @@ var init_response_overfetch = __esm({
|
|
|
4127
4237
|
"use strict";
|
|
4128
4238
|
init_endpoint();
|
|
4129
4239
|
init_response();
|
|
4240
|
+
init_http_status();
|
|
4130
4241
|
init_patterns();
|
|
4131
4242
|
init_constants();
|
|
4132
4243
|
responseOverfetchRule = {
|
|
@@ -4135,7 +4246,7 @@ var init_response_overfetch = __esm({
|
|
|
4135
4246
|
const insights = [];
|
|
4136
4247
|
const seen = /* @__PURE__ */ new Set();
|
|
4137
4248
|
for (const r of ctx.nonStatic) {
|
|
4138
|
-
if (r.statusCode
|
|
4249
|
+
if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
|
|
4139
4250
|
const ep = getEndpointKey(r.method, r.path);
|
|
4140
4251
|
if (seen.has(ep)) continue;
|
|
4141
4252
|
let parsed;
|
|
@@ -4323,7 +4434,7 @@ function createDefaultInsightRunner() {
|
|
|
4323
4434
|
function computeInsights(ctx) {
|
|
4324
4435
|
return createDefaultInsightRunner().run(ctx);
|
|
4325
4436
|
}
|
|
4326
|
-
var
|
|
4437
|
+
var init_insights = __esm({
|
|
4327
4438
|
"src/analysis/insights/index.ts"() {
|
|
4328
4439
|
"use strict";
|
|
4329
4440
|
init_runner();
|
|
@@ -4333,95 +4444,48 @@ var init_insights2 = __esm({
|
|
|
4333
4444
|
});
|
|
4334
4445
|
|
|
4335
4446
|
// src/analysis/insights.ts
|
|
4336
|
-
var
|
|
4447
|
+
var init_insights2 = __esm({
|
|
4337
4448
|
"src/analysis/insights.ts"() {
|
|
4338
4449
|
"use strict";
|
|
4339
|
-
|
|
4450
|
+
init_insights();
|
|
4340
4451
|
}
|
|
4341
4452
|
});
|
|
4342
4453
|
|
|
4343
|
-
// src/analysis/
|
|
4344
|
-
function
|
|
4345
|
-
|
|
4346
|
-
return
|
|
4454
|
+
// src/analysis/issue-mappers.ts
|
|
4455
|
+
function categorizeInsight(type) {
|
|
4456
|
+
if (type === "security") return "security";
|
|
4457
|
+
if (type === "error" || type === "error-hotspot") return "reliability";
|
|
4458
|
+
return "performance";
|
|
4347
4459
|
}
|
|
4348
|
-
function
|
|
4349
|
-
return
|
|
4460
|
+
function insightToIssue(insight) {
|
|
4461
|
+
return {
|
|
4462
|
+
category: categorizeInsight(insight.type),
|
|
4463
|
+
rule: insight.type,
|
|
4464
|
+
severity: insight.severity,
|
|
4465
|
+
title: insight.title,
|
|
4466
|
+
desc: insight.desc,
|
|
4467
|
+
hint: insight.hint,
|
|
4468
|
+
detail: insight.detail,
|
|
4469
|
+
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
|
|
4470
|
+
nav: insight.nav
|
|
4471
|
+
};
|
|
4350
4472
|
}
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4473
|
+
function securityFindingToIssue(finding) {
|
|
4474
|
+
return {
|
|
4475
|
+
category: "security",
|
|
4476
|
+
rule: finding.rule,
|
|
4477
|
+
severity: finding.severity,
|
|
4478
|
+
title: finding.title,
|
|
4479
|
+
desc: finding.desc,
|
|
4480
|
+
hint: finding.hint,
|
|
4481
|
+
endpoint: finding.endpoint,
|
|
4482
|
+
nav: "security"
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
var init_issue_mappers = __esm({
|
|
4486
|
+
"src/analysis/issue-mappers.ts"() {
|
|
4354
4487
|
"use strict";
|
|
4355
4488
|
init_endpoint();
|
|
4356
|
-
init_finding_id();
|
|
4357
|
-
init_thresholds();
|
|
4358
|
-
InsightTracker = class {
|
|
4359
|
-
tracked = /* @__PURE__ */ new Map();
|
|
4360
|
-
enrichedIndex = /* @__PURE__ */ new Map();
|
|
4361
|
-
reconcile(current) {
|
|
4362
|
-
const currentKeys = /* @__PURE__ */ new Set();
|
|
4363
|
-
const now = Date.now();
|
|
4364
|
-
for (const insight of current) {
|
|
4365
|
-
const key = computeInsightKey(insight);
|
|
4366
|
-
currentKeys.add(key);
|
|
4367
|
-
const existing = this.tracked.get(key);
|
|
4368
|
-
this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
|
|
4369
|
-
if (existing) {
|
|
4370
|
-
existing.insight = insight;
|
|
4371
|
-
existing.lastSeenAt = now;
|
|
4372
|
-
existing.consecutiveAbsences = 0;
|
|
4373
|
-
if (existing.state === "resolved") {
|
|
4374
|
-
existing.state = "open";
|
|
4375
|
-
existing.resolvedAt = null;
|
|
4376
|
-
}
|
|
4377
|
-
} else {
|
|
4378
|
-
this.tracked.set(key, {
|
|
4379
|
-
key,
|
|
4380
|
-
state: "open",
|
|
4381
|
-
insight,
|
|
4382
|
-
firstSeenAt: now,
|
|
4383
|
-
lastSeenAt: now,
|
|
4384
|
-
resolvedAt: null,
|
|
4385
|
-
consecutiveAbsences: 0,
|
|
4386
|
-
aiStatus: null,
|
|
4387
|
-
aiNotes: null
|
|
4388
|
-
});
|
|
4389
|
-
}
|
|
4390
|
-
}
|
|
4391
|
-
for (const [, stateful] of this.tracked) {
|
|
4392
|
-
if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
|
|
4393
|
-
stateful.consecutiveAbsences++;
|
|
4394
|
-
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
4395
|
-
stateful.state = "resolved";
|
|
4396
|
-
stateful.resolvedAt = now;
|
|
4397
|
-
}
|
|
4398
|
-
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
4399
|
-
this.tracked.delete(stateful.key);
|
|
4400
|
-
this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4403
|
-
return [...this.tracked.values()];
|
|
4404
|
-
}
|
|
4405
|
-
reportFix(enrichedId, status, notes) {
|
|
4406
|
-
const key = this.enrichedIndex.get(enrichedId);
|
|
4407
|
-
if (!key) return false;
|
|
4408
|
-
const stateful = this.tracked.get(key);
|
|
4409
|
-
if (!stateful) return false;
|
|
4410
|
-
stateful.aiStatus = status;
|
|
4411
|
-
stateful.aiNotes = notes;
|
|
4412
|
-
if (status === "fixed") {
|
|
4413
|
-
stateful.state = "fixing";
|
|
4414
|
-
}
|
|
4415
|
-
return true;
|
|
4416
|
-
}
|
|
4417
|
-
getAll() {
|
|
4418
|
-
return [...this.tracked.values()];
|
|
4419
|
-
}
|
|
4420
|
-
clear() {
|
|
4421
|
-
this.tracked.clear();
|
|
4422
|
-
this.enrichedIndex.clear();
|
|
4423
|
-
}
|
|
4424
|
-
};
|
|
4425
4489
|
}
|
|
4426
4490
|
});
|
|
4427
4491
|
|
|
@@ -4434,8 +4498,10 @@ var init_engine = __esm({
|
|
|
4434
4498
|
init_disposable();
|
|
4435
4499
|
init_group();
|
|
4436
4500
|
init_rules();
|
|
4437
|
-
|
|
4438
|
-
|
|
4501
|
+
init_insights2();
|
|
4502
|
+
init_issue_mappers();
|
|
4503
|
+
init_issue_id();
|
|
4504
|
+
init_prepare();
|
|
4439
4505
|
AnalysisEngine = class {
|
|
4440
4506
|
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
4441
4507
|
this.registry = registry;
|
|
@@ -4443,10 +4509,8 @@ var init_engine = __esm({
|
|
|
4443
4509
|
this.scanner = createDefaultScanner();
|
|
4444
4510
|
}
|
|
4445
4511
|
scanner;
|
|
4446
|
-
insightTracker = new InsightTracker();
|
|
4447
4512
|
cachedInsights = [];
|
|
4448
4513
|
cachedFindings = [];
|
|
4449
|
-
cachedStatefulInsights = [];
|
|
4450
4514
|
debounceTimer = null;
|
|
4451
4515
|
subs = new SubscriptionBag();
|
|
4452
4516
|
start() {
|
|
@@ -4469,15 +4533,6 @@ var init_engine = __esm({
|
|
|
4469
4533
|
getFindings() {
|
|
4470
4534
|
return this.cachedFindings;
|
|
4471
4535
|
}
|
|
4472
|
-
getStatefulFindings() {
|
|
4473
|
-
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
4474
|
-
}
|
|
4475
|
-
getStatefulInsights() {
|
|
4476
|
-
return this.insightTracker.getAll();
|
|
4477
|
-
}
|
|
4478
|
-
reportInsightFix(enrichedId, status, notes) {
|
|
4479
|
-
return this.insightTracker.reportFix(enrichedId, status, notes);
|
|
4480
|
-
}
|
|
4481
4536
|
scheduleRecompute() {
|
|
4482
4537
|
if (this.debounceTimer) return;
|
|
4483
4538
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -4486,20 +4541,14 @@ var init_engine = __esm({
|
|
|
4486
4541
|
}, this.debounceMs);
|
|
4487
4542
|
}
|
|
4488
4543
|
recompute() {
|
|
4489
|
-
const
|
|
4544
|
+
const allRequests = this.registry.get("request-store").getAll();
|
|
4490
4545
|
const queries = this.registry.get("query-store").getAll();
|
|
4491
4546
|
const errors = this.registry.get("error-store").getAll();
|
|
4492
4547
|
const logs = this.registry.get("log-store").getAll();
|
|
4493
4548
|
const fetches = this.registry.get("fetch-store").getAll();
|
|
4549
|
+
const requests = windowByEndpoint(allRequests);
|
|
4494
4550
|
const flows = groupRequestsIntoFlows(requests);
|
|
4495
4551
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
4496
|
-
if (this.registry.has("finding-store")) {
|
|
4497
|
-
const findingStore = this.registry.get("finding-store");
|
|
4498
|
-
for (const finding of this.cachedFindings) {
|
|
4499
|
-
findingStore.upsert(finding, "passive");
|
|
4500
|
-
}
|
|
4501
|
-
findingStore.reconcilePassive(this.cachedFindings);
|
|
4502
|
-
}
|
|
4503
4552
|
this.cachedInsights = computeInsights({
|
|
4504
4553
|
requests,
|
|
4505
4554
|
queries,
|
|
@@ -4509,14 +4558,30 @@ var init_engine = __esm({
|
|
|
4509
4558
|
previousMetrics: this.registry.get("metrics-store").getAll(),
|
|
4510
4559
|
securityFindings: this.cachedFindings
|
|
4511
4560
|
});
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4561
|
+
if (this.registry.has("issue-store")) {
|
|
4562
|
+
const issueStore = this.registry.get("issue-store");
|
|
4563
|
+
for (const finding of this.cachedFindings) {
|
|
4564
|
+
issueStore.upsert(securityFindingToIssue(finding), "passive");
|
|
4565
|
+
}
|
|
4566
|
+
for (const insight of this.cachedInsights) {
|
|
4567
|
+
issueStore.upsert(insightToIssue(insight), "passive");
|
|
4568
|
+
}
|
|
4569
|
+
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
4570
|
+
for (const finding of this.cachedFindings) {
|
|
4571
|
+
currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
|
|
4572
|
+
}
|
|
4573
|
+
for (const insight of this.cachedInsights) {
|
|
4574
|
+
currentIssueIds.add(computeIssueId(insightToIssue(insight)));
|
|
4575
|
+
}
|
|
4576
|
+
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
4577
|
+
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
4578
|
+
const update = {
|
|
4579
|
+
insights: this.cachedInsights,
|
|
4580
|
+
findings: this.cachedFindings,
|
|
4581
|
+
issues: issueStore.getAll()
|
|
4582
|
+
};
|
|
4583
|
+
this.registry.get("event-bus").emit("analysis:updated", update);
|
|
4584
|
+
}
|
|
4520
4585
|
}
|
|
4521
4586
|
};
|
|
4522
4587
|
}
|
|
@@ -4527,14 +4592,14 @@ var VERSION;
|
|
|
4527
4592
|
var init_src = __esm({
|
|
4528
4593
|
"src/index.ts"() {
|
|
4529
4594
|
"use strict";
|
|
4530
|
-
|
|
4595
|
+
init_issue_store();
|
|
4531
4596
|
init_project();
|
|
4532
4597
|
init_adapter_registry();
|
|
4533
4598
|
init_rules();
|
|
4534
4599
|
init_engine();
|
|
4535
|
-
init_insights3();
|
|
4536
4600
|
init_insights2();
|
|
4537
|
-
|
|
4601
|
+
init_insights();
|
|
4602
|
+
VERSION = "0.8.6";
|
|
4538
4603
|
}
|
|
4539
4604
|
});
|
|
4540
4605
|
|
|
@@ -5148,7 +5213,7 @@ function getFlowInsights() {
|
|
|
5148
5213
|
}
|
|
5149
5214
|
`;
|
|
5150
5215
|
}
|
|
5151
|
-
var
|
|
5216
|
+
var init_insights3 = __esm({
|
|
5152
5217
|
"src/dashboard/client/views/flows/insights.ts"() {
|
|
5153
5218
|
"use strict";
|
|
5154
5219
|
init_constants();
|
|
@@ -5340,7 +5405,7 @@ function getFlowsView() {
|
|
|
5340
5405
|
var init_flows2 = __esm({
|
|
5341
5406
|
"src/dashboard/client/views/flows.ts"() {
|
|
5342
5407
|
"use strict";
|
|
5343
|
-
|
|
5408
|
+
init_insights3();
|
|
5344
5409
|
init_detail();
|
|
5345
5410
|
}
|
|
5346
5411
|
});
|
|
@@ -6480,8 +6545,8 @@ function getOverviewRender() {
|
|
|
6480
6545
|
'<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
|
|
6481
6546
|
container.appendChild(summary);
|
|
6482
6547
|
|
|
6483
|
-
var all = state.
|
|
6484
|
-
var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
|
|
6548
|
+
var all = state.issues || [];
|
|
6549
|
+
var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing' || si.state === 'regressed'; });
|
|
6485
6550
|
var resolved = all.filter(function(si) { return si.state === 'resolved'; });
|
|
6486
6551
|
|
|
6487
6552
|
if (open.length === 0 && resolved.length === 0) {
|
|
@@ -6513,31 +6578,35 @@ function getOverviewRender() {
|
|
|
6513
6578
|
|
|
6514
6579
|
for (var i = 0; i < open.length; i++) {
|
|
6515
6580
|
(function(si) {
|
|
6516
|
-
var
|
|
6581
|
+
var issue = si.issue;
|
|
6517
6582
|
var card = document.createElement('div');
|
|
6518
6583
|
card.className = 'ov-card';
|
|
6519
6584
|
|
|
6520
|
-
var sevCfg = SEV[
|
|
6585
|
+
var sevCfg = SEV[issue.severity];
|
|
6521
6586
|
var iconCls = sevCfg.cls;
|
|
6522
6587
|
var iconChar = sevCfg.icon;
|
|
6523
6588
|
|
|
6524
6589
|
var expandHtml = '';
|
|
6525
|
-
if (
|
|
6526
|
-
if (
|
|
6527
|
-
expandHtml += '<span class="ov-card-link" data-nav="' +
|
|
6590
|
+
if (issue.detail) expandHtml += issue.detail;
|
|
6591
|
+
if (issue.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(issue.hint) + '</div>';
|
|
6592
|
+
if (issue.nav) expandHtml += '<span class="ov-card-link" data-nav="' + issue.nav + '">View in ' + (NAV_LABELS[issue.nav] || issue.nav) + ' \\u2192</span>';
|
|
6528
6593
|
|
|
6529
6594
|
var aiBadge = '';
|
|
6530
6595
|
if (si.state === 'fixing' && si.aiStatus === 'fixed') {
|
|
6531
6596
|
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
|
|
6532
6597
|
} else if (si.aiStatus === 'wont_fix') {
|
|
6533
6598
|
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
|
|
6599
|
+
} else if (si.state === 'regressed') {
|
|
6600
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
|
|
6534
6601
|
}
|
|
6535
6602
|
|
|
6603
|
+
var occBadge = si.occurrences > 1 ? ' <span class="sec-item-count">' + si.occurrences + 'x</span>' : '';
|
|
6604
|
+
|
|
6536
6605
|
card.innerHTML =
|
|
6537
6606
|
'<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
6538
6607
|
'<div class="ov-card-body">' +
|
|
6539
|
-
'<div class="ov-card-title">' + escHtml(
|
|
6540
|
-
'<div class="ov-card-desc">' +
|
|
6608
|
+
'<div class="ov-card-title">' + escHtml(issue.title) + occBadge + aiBadge + '</div>' +
|
|
6609
|
+
'<div class="ov-card-desc">' + issue.desc + '</div>' +
|
|
6541
6610
|
'<div class="ov-card-expand">' + expandHtml + '</div>' +
|
|
6542
6611
|
'</div>' +
|
|
6543
6612
|
'<span class="ov-card-arrow">\\u2192</span>';
|
|
@@ -6583,14 +6652,14 @@ function getOverviewRender() {
|
|
|
6583
6652
|
resolvedCards.className = 'ov-cards';
|
|
6584
6653
|
|
|
6585
6654
|
for (var ri = 0; ri < resolved.length; ri++) {
|
|
6586
|
-
var
|
|
6655
|
+
var rIssue = resolved[ri].issue;
|
|
6587
6656
|
var rCard = document.createElement('div');
|
|
6588
6657
|
rCard.className = 'ov-card ov-card-resolved';
|
|
6589
6658
|
rCard.innerHTML =
|
|
6590
6659
|
'<span class="ov-card-icon resolved">\\u2713</span>' +
|
|
6591
6660
|
'<div class="ov-card-body">' +
|
|
6592
|
-
'<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(
|
|
6593
|
-
'<div class="ov-card-desc">' +
|
|
6661
|
+
'<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rIssue.title) + '</div>' +
|
|
6662
|
+
'<div class="ov-card-desc">' + rIssue.desc + '</div>' +
|
|
6594
6663
|
'</div>';
|
|
6595
6664
|
resolvedCards.appendChild(rCard);
|
|
6596
6665
|
}
|
|
@@ -6628,30 +6697,12 @@ function getSecurityView() {
|
|
|
6628
6697
|
container.innerHTML = '';
|
|
6629
6698
|
var SEV = ${SEVERITY_MAP};
|
|
6630
6699
|
|
|
6631
|
-
var all = (state.
|
|
6632
|
-
var
|
|
6633
|
-
for (var ix = 0; ix < insightsList.length; ix++) {
|
|
6634
|
-
var si = insightsList[ix];
|
|
6635
|
-
all.push({
|
|
6636
|
-
findingId: si.key,
|
|
6637
|
-
state: si.state,
|
|
6638
|
-
aiStatus: si.aiStatus,
|
|
6639
|
-
aiNotes: si.aiNotes,
|
|
6640
|
-
finding: {
|
|
6641
|
-
severity: si.insight.severity,
|
|
6642
|
-
rule: 'insight-' + si.insight.type,
|
|
6643
|
-
title: si.insight.title,
|
|
6644
|
-
desc: si.insight.desc,
|
|
6645
|
-
hint: si.insight.hint,
|
|
6646
|
-
endpoint: si.insight.nav || 'global',
|
|
6647
|
-
count: 1
|
|
6648
|
-
}
|
|
6649
|
-
});
|
|
6650
|
-
}
|
|
6651
|
-
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
|
|
6700
|
+
var all = (state.issues || []).slice();
|
|
6701
|
+
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing' || f.state === 'regressed'; });
|
|
6652
6702
|
var resolved = all.filter(function(f) { return f.state === 'resolved'; });
|
|
6703
|
+
var stale = all.filter(function(f) { return f.state === 'stale'; });
|
|
6653
6704
|
|
|
6654
|
-
if (open.length === 0 && resolved.length === 0) {
|
|
6705
|
+
if (open.length === 0 && resolved.length === 0 && stale.length === 0) {
|
|
6655
6706
|
var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
|
|
6656
6707
|
if (!hasData) {
|
|
6657
6708
|
container.innerHTML = '<div class="empty"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
|
|
@@ -6663,7 +6714,7 @@ function getSecurityView() {
|
|
|
6663
6714
|
|
|
6664
6715
|
var critCount = 0, warnCount = 0, infoCount = 0;
|
|
6665
6716
|
for (var ci = 0; ci < open.length; ci++) {
|
|
6666
|
-
var sev = open[ci].
|
|
6717
|
+
var sev = open[ci].issue.severity;
|
|
6667
6718
|
if (sev === 'critical') critCount++;
|
|
6668
6719
|
else if (sev === 'info') infoCount++;
|
|
6669
6720
|
else warnCount++;
|
|
@@ -6696,7 +6747,7 @@ function getSecurityView() {
|
|
|
6696
6747
|
var groupOrder = [];
|
|
6697
6748
|
for (var gi = 0; gi < open.length; gi++) {
|
|
6698
6749
|
var sf = open[gi];
|
|
6699
|
-
var f = sf.
|
|
6750
|
+
var f = sf.issue;
|
|
6700
6751
|
if (!groups[f.rule]) {
|
|
6701
6752
|
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
6702
6753
|
groupOrder.push(f.rule);
|
|
@@ -6739,7 +6790,7 @@ function getSecurityView() {
|
|
|
6739
6790
|
list.className = 'sec-items';
|
|
6740
6791
|
for (var ii = 0; ii < group.items.length; ii++) {
|
|
6741
6792
|
var sf2 = group.items[ii];
|
|
6742
|
-
var item = sf2.
|
|
6793
|
+
var item = sf2.issue;
|
|
6743
6794
|
var row = document.createElement('div');
|
|
6744
6795
|
row.className = 'sec-item';
|
|
6745
6796
|
var aiBadge = '';
|
|
@@ -6747,11 +6798,14 @@ function getSecurityView() {
|
|
|
6747
6798
|
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
|
|
6748
6799
|
} else if (sf2.aiStatus === 'wont_fix') {
|
|
6749
6800
|
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
|
|
6801
|
+
} else if (sf2.state === 'regressed') {
|
|
6802
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing" style="background:var(--red)">regressed</span>';
|
|
6750
6803
|
}
|
|
6751
6804
|
var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
|
|
6805
|
+
var occBadge = sf2.occurrences > 1 ? '<span class="sec-item-count">' + sf2.occurrences + 'x</span>' : '';
|
|
6752
6806
|
row.innerHTML =
|
|
6753
6807
|
'<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
|
|
6754
|
-
|
|
6808
|
+
occBadge +
|
|
6755
6809
|
aiBadge + aiNotes;
|
|
6756
6810
|
list.appendChild(row);
|
|
6757
6811
|
}
|
|
@@ -6772,20 +6826,44 @@ function getSecurityView() {
|
|
|
6772
6826
|
resolvedItems.className = 'sec-items';
|
|
6773
6827
|
for (var ri = 0; ri < resolved.length; ri++) {
|
|
6774
6828
|
var rsf = resolved[ri];
|
|
6775
|
-
var rf = rsf.
|
|
6829
|
+
var rf = rsf.issue;
|
|
6776
6830
|
var rRow = document.createElement('div');
|
|
6777
6831
|
rRow.className = 'sec-item sec-item-resolved';
|
|
6778
6832
|
var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
|
|
6779
6833
|
var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
|
|
6780
6834
|
rRow.innerHTML =
|
|
6781
6835
|
'<span class="sec-resolved-item-icon">\\u2713</span>' +
|
|
6782
|
-
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>' +
|
|
6836
|
+
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint || 'global') + '</div>' +
|
|
6783
6837
|
verifiedBadge + rNotes;
|
|
6784
6838
|
resolvedItems.appendChild(rRow);
|
|
6785
6839
|
}
|
|
6786
6840
|
resolvedGroup.appendChild(resolvedItems);
|
|
6787
6841
|
container.appendChild(resolvedGroup);
|
|
6788
6842
|
}
|
|
6843
|
+
|
|
6844
|
+
if (stale.length > 0) {
|
|
6845
|
+
var staleTitle = document.createElement('div');
|
|
6846
|
+
staleTitle.className = 'sec-resolved-title';
|
|
6847
|
+
staleTitle.innerHTML = '<span style="color:var(--text-muted)">\\u23F8</span> Stale <span class="sec-resolved-count">' + stale.length + '</span>';
|
|
6848
|
+
container.appendChild(staleTitle);
|
|
6849
|
+
|
|
6850
|
+
var staleGroup = document.createElement('div');
|
|
6851
|
+
staleGroup.className = 'sec-group sec-group-resolved';
|
|
6852
|
+
var staleItems = document.createElement('div');
|
|
6853
|
+
staleItems.className = 'sec-items';
|
|
6854
|
+
for (var sti = 0; sti < stale.length; sti++) {
|
|
6855
|
+
var ssf = stale[sti];
|
|
6856
|
+
var sf3 = ssf.issue;
|
|
6857
|
+
var sRow = document.createElement('div');
|
|
6858
|
+
sRow.className = 'sec-item sec-item-resolved';
|
|
6859
|
+
sRow.innerHTML =
|
|
6860
|
+
'<span style="color:var(--text-muted)">\\u23F8</span>' +
|
|
6861
|
+
'<div class="sec-item-desc" style="color:var(--text-muted)">' + escHtml(sf3.title) + ' \\u2014 endpoint inactive</div>';
|
|
6862
|
+
staleItems.appendChild(sRow);
|
|
6863
|
+
}
|
|
6864
|
+
staleGroup.appendChild(staleItems);
|
|
6865
|
+
container.appendChild(staleGroup);
|
|
6866
|
+
}
|
|
6789
6867
|
}
|
|
6790
6868
|
`;
|
|
6791
6869
|
}
|
|
@@ -6823,13 +6901,7 @@ function getApp() {
|
|
|
6823
6901
|
try {
|
|
6824
6902
|
var res3 = await fetch('${DASHBOARD_API_INSIGHTS}');
|
|
6825
6903
|
var data3 = await res3.json();
|
|
6826
|
-
state.
|
|
6827
|
-
} catch(e) { console.warn('[brakit]', e); }
|
|
6828
|
-
|
|
6829
|
-
try {
|
|
6830
|
-
var res4 = await fetch('${DASHBOARD_API_SECURITY}');
|
|
6831
|
-
var data4 = await res4.json();
|
|
6832
|
-
state.findings = data4.findings || [];
|
|
6904
|
+
state.issues = data3.issues || [];
|
|
6833
6905
|
} catch(e) { console.warn('[brakit]', e); }
|
|
6834
6906
|
|
|
6835
6907
|
updateStats();
|
|
@@ -6868,19 +6940,13 @@ function getApp() {
|
|
|
6868
6940
|
registerTelemetryListener('error_event', 'errors', prependErrorRow);
|
|
6869
6941
|
registerTelemetryListener('query', 'queries', prependQueryRow);
|
|
6870
6942
|
|
|
6871
|
-
events.addEventListener('
|
|
6872
|
-
state.
|
|
6943
|
+
events.addEventListener('issues', function(e) {
|
|
6944
|
+
state.issues = JSON.parse(e.data);
|
|
6873
6945
|
if (state.activeView === 'overview') renderOverview();
|
|
6874
6946
|
if (state.activeView === 'security') renderSecurity();
|
|
6875
6947
|
updateStats();
|
|
6876
6948
|
});
|
|
6877
6949
|
|
|
6878
|
-
events.addEventListener('security', function(e) {
|
|
6879
|
-
state.findings = JSON.parse(e.data);
|
|
6880
|
-
if (state.activeView === 'security') renderSecurity();
|
|
6881
|
-
updateStats();
|
|
6882
|
-
});
|
|
6883
|
-
|
|
6884
6950
|
window.addEventListener('beforeunload', function() {
|
|
6885
6951
|
events.close();
|
|
6886
6952
|
clearTimeout(reloadTimer);
|
|
@@ -6959,9 +7025,9 @@ function getApp() {
|
|
|
6959
7025
|
if (queryCount) queryCount.textContent = state.queries.length;
|
|
6960
7026
|
var secCount = document.getElementById('sidebar-count-security');
|
|
6961
7027
|
if (secCount) {
|
|
6962
|
-
var
|
|
6963
|
-
secCount.textContent =
|
|
6964
|
-
secCount.style.display =
|
|
7028
|
+
var numIssues = (state.issues || []).filter(function(f) { return f.state !== 'resolved' && f.state !== 'stale'; }).length;
|
|
7029
|
+
secCount.textContent = numIssues;
|
|
7030
|
+
secCount.style.display = numIssues > 0 ? '' : 'none';
|
|
6965
7031
|
}
|
|
6966
7032
|
}
|
|
6967
7033
|
|
|
@@ -6979,7 +7045,7 @@ function getApp() {
|
|
|
6979
7045
|
if (!confirm('This will clear all data including performance metrics history. Continue?')) return;
|
|
6980
7046
|
await fetch('${DASHBOARD_API_CLEAR}', {method: 'POST'});
|
|
6981
7047
|
state.flows = []; state.requests = []; state.fetches = []; state.errors = []; state.logs = []; state.queries = [];
|
|
6982
|
-
state.
|
|
7048
|
+
state.issues = [];
|
|
6983
7049
|
graphData = []; selectedEndpoint = ${ALL_ENDPOINTS_SELECTOR}; timelineCache = {};
|
|
6984
7050
|
renderFlows(); renderRequests(); renderFetches(); renderErrors(); renderLogs(); renderQueries(); renderGraph(); renderOverview(); renderSecurity(); updateStats();
|
|
6985
7051
|
showToast('Cleared');
|
|
@@ -7072,8 +7138,8 @@ var init_page = __esm({
|
|
|
7072
7138
|
});
|
|
7073
7139
|
|
|
7074
7140
|
// src/telemetry/config.ts
|
|
7075
|
-
import { homedir } from "os";
|
|
7076
|
-
import { join as
|
|
7141
|
+
import { homedir as homedir2 } from "os";
|
|
7142
|
+
import { join as join3 } from "path";
|
|
7077
7143
|
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
7078
7144
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
7079
7145
|
function readConfig() {
|
|
@@ -7087,9 +7153,9 @@ function readConfig() {
|
|
|
7087
7153
|
function writeConfig(config) {
|
|
7088
7154
|
try {
|
|
7089
7155
|
if (!existsSync5(CONFIG_DIR))
|
|
7090
|
-
mkdirSync3(CONFIG_DIR, { recursive: true, mode:
|
|
7156
|
+
mkdirSync3(CONFIG_DIR, { recursive: true, mode: DIR_MODE_OWNER_ONLY });
|
|
7091
7157
|
writeFileSync3(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
|
|
7092
|
-
mode:
|
|
7158
|
+
mode: FILE_MODE_OWNER_ONLY
|
|
7093
7159
|
});
|
|
7094
7160
|
} catch {
|
|
7095
7161
|
}
|
|
@@ -7117,8 +7183,9 @@ var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
|
|
|
7117
7183
|
var init_config = __esm({
|
|
7118
7184
|
"src/telemetry/config.ts"() {
|
|
7119
7185
|
"use strict";
|
|
7120
|
-
|
|
7121
|
-
|
|
7186
|
+
init_network();
|
|
7187
|
+
CONFIG_DIR = join3(homedir2(), ".brakit");
|
|
7188
|
+
CONFIG_PATH = join3(CONFIG_DIR, "config.json");
|
|
7122
7189
|
cachedEnabled = null;
|
|
7123
7190
|
}
|
|
7124
7191
|
});
|
|
@@ -7150,12 +7217,12 @@ function recordDashboardOpened() {
|
|
|
7150
7217
|
}
|
|
7151
7218
|
function speedBucket(ms) {
|
|
7152
7219
|
if (ms === 0) return "none";
|
|
7153
|
-
|
|
7154
|
-
if (ms <
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
return
|
|
7220
|
+
const t = SPEED_BUCKET_THRESHOLDS;
|
|
7221
|
+
if (ms < t[0]) return `<${t[0]}ms`;
|
|
7222
|
+
for (let i = 1; i < t.length; i++) {
|
|
7223
|
+
if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
|
|
7224
|
+
}
|
|
7225
|
+
return `>${t[t.length - 1]}ms`;
|
|
7159
7226
|
}
|
|
7160
7227
|
function trackSession(registry) {
|
|
7161
7228
|
if (!isTelemetryEnabled()) return;
|
|
@@ -7255,7 +7322,6 @@ function isDashboardRequest(url) {
|
|
|
7255
7322
|
}
|
|
7256
7323
|
function createDashboardHandler(registry) {
|
|
7257
7324
|
const metricsStore = registry.get("metrics-store");
|
|
7258
|
-
const analysisEngine = registry.has("analysis-engine") ? registry.get("analysis-engine") : void 0;
|
|
7259
7325
|
const routes = {
|
|
7260
7326
|
[DASHBOARD_API_REQUESTS]: createRequestsHandler(registry),
|
|
7261
7327
|
[DASHBOARD_API_EVENTS]: createSSEHandler(registry),
|
|
@@ -7270,17 +7336,14 @@ function createDashboardHandler(registry) {
|
|
|
7270
7336
|
[DASHBOARD_API_INGEST]: createIngestHandler(registry),
|
|
7271
7337
|
[DASHBOARD_API_ACTIVITY]: createActivityHandler(registry)
|
|
7272
7338
|
};
|
|
7273
|
-
if (
|
|
7274
|
-
|
|
7275
|
-
routes[
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
findingStore,
|
|
7282
|
-
registry.get("event-bus"),
|
|
7283
|
-
analysisEngine
|
|
7339
|
+
if (registry.has("issue-store")) {
|
|
7340
|
+
const issueStore = registry.get("issue-store");
|
|
7341
|
+
routes[DASHBOARD_API_INSIGHTS] = createIssuesHandler(issueStore);
|
|
7342
|
+
routes[DASHBOARD_API_SECURITY] = createIssuesHandler(issueStore);
|
|
7343
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(issueStore);
|
|
7344
|
+
routes[DASHBOARD_API_FINDINGS_REPORT] = createIssuesReportHandler(
|
|
7345
|
+
issueStore,
|
|
7346
|
+
registry.get("event-bus")
|
|
7284
7347
|
);
|
|
7285
7348
|
}
|
|
7286
7349
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
@@ -7314,8 +7377,7 @@ var init_router = __esm({
|
|
|
7314
7377
|
init_constants();
|
|
7315
7378
|
init_http();
|
|
7316
7379
|
init_api();
|
|
7317
|
-
|
|
7318
|
-
init_findings();
|
|
7380
|
+
init_issues();
|
|
7319
7381
|
init_sse();
|
|
7320
7382
|
init_page();
|
|
7321
7383
|
init_telemetry2();
|
|
@@ -7603,6 +7665,7 @@ var init_metrics_store = __esm({
|
|
|
7603
7665
|
"use strict";
|
|
7604
7666
|
init_constants();
|
|
7605
7667
|
init_math();
|
|
7668
|
+
init_http_status();
|
|
7606
7669
|
init_endpoint();
|
|
7607
7670
|
MetricsStore = class {
|
|
7608
7671
|
constructor(persistence) {
|
|
@@ -7614,6 +7677,7 @@ var init_metrics_store = __esm({
|
|
|
7614
7677
|
sessionId = randomUUID6();
|
|
7615
7678
|
sessionStart = Date.now();
|
|
7616
7679
|
flushTimer = null;
|
|
7680
|
+
dirty = false;
|
|
7617
7681
|
accumulators = /* @__PURE__ */ new Map();
|
|
7618
7682
|
pendingPoints = /* @__PURE__ */ new Map();
|
|
7619
7683
|
start() {
|
|
@@ -7639,21 +7703,27 @@ var init_metrics_store = __esm({
|
|
|
7639
7703
|
}
|
|
7640
7704
|
recordRequest(req, metrics) {
|
|
7641
7705
|
if (req.isStatic) return;
|
|
7706
|
+
this.dirty = true;
|
|
7642
7707
|
const key = getEndpointKey(req.method, req.path);
|
|
7643
7708
|
let acc = this.accumulators.get(key);
|
|
7644
7709
|
if (!acc) {
|
|
7710
|
+
if (this.accumulators.size >= MAX_UNIQUE_ENDPOINTS) return;
|
|
7645
7711
|
acc = createAccumulator();
|
|
7646
7712
|
this.accumulators.set(key, acc);
|
|
7647
7713
|
}
|
|
7648
|
-
acc.durations.
|
|
7649
|
-
|
|
7650
|
-
|
|
7714
|
+
if (acc.durations.length < MAX_ACCUMULATOR_ENTRIES) {
|
|
7715
|
+
acc.durations.push(req.durationMs);
|
|
7716
|
+
}
|
|
7717
|
+
if (acc.queryCounts.length < MAX_ACCUMULATOR_ENTRIES) {
|
|
7718
|
+
acc.queryCounts.push(metrics.queryCount);
|
|
7719
|
+
}
|
|
7720
|
+
if (isErrorStatus(req.statusCode)) acc.errorCount++;
|
|
7651
7721
|
acc.totalDurationSum += req.durationMs;
|
|
7652
7722
|
acc.totalRequestCount++;
|
|
7653
7723
|
acc.totalQuerySum += metrics.queryCount;
|
|
7654
7724
|
acc.totalQueryTimeMs += metrics.queryTimeMs;
|
|
7655
7725
|
acc.totalFetchTimeMs += metrics.fetchTimeMs;
|
|
7656
|
-
if (req.statusCode
|
|
7726
|
+
if (isErrorStatus(req.statusCode)) acc.totalErrorCount++;
|
|
7657
7727
|
const timestamp = Math.round(
|
|
7658
7728
|
Date.now() - (performance.now() - req.startedAt)
|
|
7659
7729
|
);
|
|
@@ -7667,10 +7737,13 @@ var init_metrics_store = __esm({
|
|
|
7667
7737
|
};
|
|
7668
7738
|
let pending2 = this.pendingPoints.get(key);
|
|
7669
7739
|
if (!pending2) {
|
|
7740
|
+
if (this.pendingPoints.size >= MAX_UNIQUE_ENDPOINTS) return;
|
|
7670
7741
|
pending2 = [];
|
|
7671
7742
|
this.pendingPoints.set(key, pending2);
|
|
7672
7743
|
}
|
|
7673
|
-
pending2.
|
|
7744
|
+
if (pending2.length < MAX_ACCUMULATOR_ENTRIES) {
|
|
7745
|
+
pending2.push(point);
|
|
7746
|
+
}
|
|
7674
7747
|
}
|
|
7675
7748
|
getAll() {
|
|
7676
7749
|
return this.data.endpoints;
|
|
@@ -7693,7 +7766,7 @@ var init_metrics_store = __esm({
|
|
|
7693
7766
|
for (const [endpoint, requests] of merged) {
|
|
7694
7767
|
if (requests.length === 0) continue;
|
|
7695
7768
|
const durations = requests.map((r) => r.durationMs);
|
|
7696
|
-
const errors = requests.filter((r) => r.statusCode
|
|
7769
|
+
const errors = requests.filter((r) => isErrorStatus(r.statusCode)).length;
|
|
7697
7770
|
const totalQueries = requests.reduce((s, r) => s + r.queryCount, 0);
|
|
7698
7771
|
const totalQueryTime = requests.reduce((s, r) => s + (r.queryTimeMs ?? 0), 0);
|
|
7699
7772
|
const totalFetchTime = requests.reduce((s, r) => s + (r.fetchTimeMs ?? 0), 0);
|
|
@@ -7723,6 +7796,7 @@ var init_metrics_store = __esm({
|
|
|
7723
7796
|
this.endpointIndex.clear();
|
|
7724
7797
|
this.accumulators.clear();
|
|
7725
7798
|
this.pendingPoints.clear();
|
|
7799
|
+
this.dirty = false;
|
|
7726
7800
|
this.persistence.remove();
|
|
7727
7801
|
}
|
|
7728
7802
|
flush(sync = false) {
|
|
@@ -7763,11 +7837,13 @@ var init_metrics_store = __esm({
|
|
|
7763
7837
|
epMetrics.dataPoints = existing.concat(points).slice(-METRICS_MAX_DATA_POINTS);
|
|
7764
7838
|
}
|
|
7765
7839
|
this.pendingPoints.clear();
|
|
7840
|
+
if (!this.dirty) return;
|
|
7766
7841
|
if (sync) {
|
|
7767
7842
|
this.persistence.saveSync(this.data);
|
|
7768
7843
|
} else {
|
|
7769
7844
|
this.persistence.save(this.data);
|
|
7770
7845
|
}
|
|
7846
|
+
this.dirty = false;
|
|
7771
7847
|
}
|
|
7772
7848
|
getOrCreateEndpoint(endpoint) {
|
|
7773
7849
|
let ep = this.endpointIndex.get(endpoint);
|
|
@@ -7784,9 +7860,9 @@ var init_metrics_store = __esm({
|
|
|
7784
7860
|
|
|
7785
7861
|
// src/store/metrics/persistence.ts
|
|
7786
7862
|
import { readFile as readFile4 } from "fs/promises";
|
|
7787
|
-
import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
|
|
7863
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
7788
7864
|
import { resolve as resolve3 } from "path";
|
|
7789
|
-
var FileMetricsPersistence;
|
|
7865
|
+
var DEFAULT_METRICS, FileMetricsPersistence;
|
|
7790
7866
|
var init_persistence = __esm({
|
|
7791
7867
|
"src/store/metrics/persistence.ts"() {
|
|
7792
7868
|
"use strict";
|
|
@@ -7795,45 +7871,41 @@ var init_persistence = __esm({
|
|
|
7795
7871
|
init_fs();
|
|
7796
7872
|
init_log();
|
|
7797
7873
|
init_type_guards();
|
|
7874
|
+
DEFAULT_METRICS = { version: 1, endpoints: [] };
|
|
7798
7875
|
FileMetricsPersistence = class {
|
|
7799
7876
|
metricsPath;
|
|
7800
7877
|
writer;
|
|
7801
|
-
constructor(
|
|
7802
|
-
this.metricsPath = resolve3(
|
|
7878
|
+
constructor(dataDir) {
|
|
7879
|
+
this.metricsPath = resolve3(dataDir, METRICS_FILE);
|
|
7803
7880
|
this.writer = new AtomicWriter({
|
|
7804
|
-
dir:
|
|
7881
|
+
dir: dataDir,
|
|
7805
7882
|
filePath: this.metricsPath,
|
|
7806
|
-
gitignoreEntry: METRICS_DIR,
|
|
7807
7883
|
label: "metrics"
|
|
7808
7884
|
});
|
|
7809
7885
|
}
|
|
7810
7886
|
load() {
|
|
7811
7887
|
try {
|
|
7812
7888
|
if (existsSync6(this.metricsPath)) {
|
|
7813
|
-
|
|
7814
|
-
const parsed = JSON.parse(raw);
|
|
7815
|
-
if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
|
|
7816
|
-
return parsed;
|
|
7817
|
-
}
|
|
7889
|
+
return this.parseMetrics(readFileSync4(this.metricsPath, "utf-8"));
|
|
7818
7890
|
}
|
|
7819
7891
|
} catch (err) {
|
|
7820
|
-
brakitWarn(`failed to load
|
|
7892
|
+
brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
|
|
7821
7893
|
}
|
|
7822
|
-
return {
|
|
7894
|
+
return { ...DEFAULT_METRICS };
|
|
7823
7895
|
}
|
|
7824
7896
|
async loadAsync() {
|
|
7825
7897
|
try {
|
|
7826
7898
|
if (await fileExists(this.metricsPath)) {
|
|
7827
|
-
|
|
7828
|
-
const parsed = JSON.parse(raw);
|
|
7829
|
-
if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
|
|
7830
|
-
return parsed;
|
|
7831
|
-
}
|
|
7899
|
+
return this.parseMetrics(await readFile4(this.metricsPath, "utf-8"));
|
|
7832
7900
|
}
|
|
7833
7901
|
} catch (err) {
|
|
7834
|
-
brakitWarn(`failed to load
|
|
7902
|
+
brakitWarn(`failed to load ${this.metricsPath}: ${getErrorMessage(err)}`);
|
|
7835
7903
|
}
|
|
7836
|
-
return {
|
|
7904
|
+
return { ...DEFAULT_METRICS };
|
|
7905
|
+
}
|
|
7906
|
+
/** Parse and validate metrics JSON, returning default empty data on invalid input. */
|
|
7907
|
+
parseMetrics(raw) {
|
|
7908
|
+
return validateMetricsData(JSON.parse(raw)) ?? { ...DEFAULT_METRICS };
|
|
7837
7909
|
}
|
|
7838
7910
|
save(data) {
|
|
7839
7911
|
this.writer.writeAsync(JSON.stringify(data));
|
|
@@ -7844,9 +7916,10 @@ var init_persistence = __esm({
|
|
|
7844
7916
|
remove() {
|
|
7845
7917
|
try {
|
|
7846
7918
|
if (existsSync6(this.metricsPath)) {
|
|
7847
|
-
|
|
7919
|
+
unlinkSync2(this.metricsPath);
|
|
7848
7920
|
}
|
|
7849
|
-
} catch {
|
|
7921
|
+
} catch (err) {
|
|
7922
|
+
brakitDebug(`failed to remove metrics file: ${getErrorMessage(err)}`);
|
|
7850
7923
|
}
|
|
7851
7924
|
}
|
|
7852
7925
|
};
|
|
@@ -7883,14 +7956,14 @@ function colorTitle(severity, text) {
|
|
|
7883
7956
|
function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
|
|
7884
7957
|
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
7885
7958
|
}
|
|
7886
|
-
function formatConsoleLine(
|
|
7887
|
-
const icon = severityIcon(
|
|
7888
|
-
const title = colorTitle(
|
|
7889
|
-
const desc = pc.dim(truncate(
|
|
7959
|
+
function formatConsoleLine(issue, suffix) {
|
|
7960
|
+
const icon = severityIcon(issue.severity);
|
|
7961
|
+
const title = colorTitle(issue.severity, issue.title);
|
|
7962
|
+
const desc = pc.dim(truncate(issue.desc) + (suffix ?? ""));
|
|
7890
7963
|
let line = ` ${icon} ${title} \u2014 ${desc}`;
|
|
7891
|
-
if (
|
|
7964
|
+
if (issue.detail) {
|
|
7892
7965
|
line += `
|
|
7893
|
-
${pc.dim("\u2514 " +
|
|
7966
|
+
${pc.dim("\u2514 " + issue.detail)}`;
|
|
7894
7967
|
}
|
|
7895
7968
|
return line;
|
|
7896
7969
|
}
|
|
@@ -7900,26 +7973,37 @@ function startTerminalInsights(registry, proxyPort) {
|
|
|
7900
7973
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
7901
7974
|
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
7902
7975
|
const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
|
|
7903
|
-
return bus.on("analysis:updated", ({
|
|
7976
|
+
return bus.on("analysis:updated", ({ issues }) => {
|
|
7904
7977
|
const newLines = [];
|
|
7905
7978
|
const resolvedLines = [];
|
|
7906
|
-
|
|
7979
|
+
const regressedLines = [];
|
|
7980
|
+
for (const si of issues) {
|
|
7907
7981
|
if (si.state === "resolved") {
|
|
7908
|
-
if (resolvedKeys.has(si.
|
|
7909
|
-
resolvedKeys.add(si.
|
|
7910
|
-
printedKeys.delete(si.
|
|
7911
|
-
const title = pc.green(pc.bold(`\u2713 ${si.
|
|
7912
|
-
const desc = pc.dim(truncate(si.
|
|
7982
|
+
if (resolvedKeys.has(si.issueId)) continue;
|
|
7983
|
+
resolvedKeys.add(si.issueId);
|
|
7984
|
+
printedKeys.delete(si.issueId);
|
|
7985
|
+
const title = pc.green(pc.bold(`\u2713 ${si.issue.title}`));
|
|
7986
|
+
const desc = pc.dim(truncate(si.issue.desc));
|
|
7913
7987
|
resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
|
|
7914
7988
|
continue;
|
|
7915
7989
|
}
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7990
|
+
if (si.state === "regressed") {
|
|
7991
|
+
if (!printedKeys.has(si.issueId)) {
|
|
7992
|
+
printedKeys.add(si.issueId);
|
|
7993
|
+
resolvedKeys.delete(si.issueId);
|
|
7994
|
+
const title = pc.red(pc.bold(`\u26A0 ${si.issue.title}`));
|
|
7995
|
+
const desc = pc.dim(truncate(si.issue.desc));
|
|
7996
|
+
regressedLines.push(` ${title} \u2014 ${desc} ${pc.red("regressed")}`);
|
|
7997
|
+
}
|
|
7998
|
+
continue;
|
|
7999
|
+
}
|
|
8000
|
+
resolvedKeys.delete(si.issueId);
|
|
8001
|
+
if (si.issue.severity === "info") continue;
|
|
8002
|
+
if (printedKeys.has(si.issueId)) continue;
|
|
8003
|
+
printedKeys.add(si.issueId);
|
|
7920
8004
|
let suffix;
|
|
7921
|
-
if (si.
|
|
7922
|
-
const endpoint =
|
|
8005
|
+
if (si.issue.rule === "slow") {
|
|
8006
|
+
const endpoint = si.issue.endpoint;
|
|
7923
8007
|
if (endpoint) {
|
|
7924
8008
|
const ep = metricsStore.getEndpoint(endpoint);
|
|
7925
8009
|
if (ep && ep.sessions.length > 1) {
|
|
@@ -7928,7 +8012,7 @@ function startTerminalInsights(registry, proxyPort) {
|
|
|
7928
8012
|
}
|
|
7929
8013
|
}
|
|
7930
8014
|
}
|
|
7931
|
-
newLines.push(formatConsoleLine(si.
|
|
8015
|
+
newLines.push(formatConsoleLine(si.issue, suffix));
|
|
7932
8016
|
}
|
|
7933
8017
|
if (newLines.length > 0) {
|
|
7934
8018
|
print("");
|
|
@@ -7936,6 +8020,12 @@ function startTerminalInsights(registry, proxyPort) {
|
|
|
7936
8020
|
print("");
|
|
7937
8021
|
print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
|
|
7938
8022
|
}
|
|
8023
|
+
if (regressedLines.length > 0) {
|
|
8024
|
+
print("");
|
|
8025
|
+
for (const line of regressedLines) print(line);
|
|
8026
|
+
print("");
|
|
8027
|
+
print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.red("Issues came back after being resolved!")}`);
|
|
8028
|
+
}
|
|
7939
8029
|
if (resolvedLines.length > 0) {
|
|
7940
8030
|
print("");
|
|
7941
8031
|
for (const line of resolvedLines) print(line);
|
|
@@ -7952,7 +8042,6 @@ var init_terminal = __esm({
|
|
|
7952
8042
|
init_constants();
|
|
7953
8043
|
init_limits();
|
|
7954
8044
|
init_severity();
|
|
7955
|
-
init_endpoint();
|
|
7956
8045
|
SEVERITY_COLOR = {
|
|
7957
8046
|
critical: pc.red,
|
|
7958
8047
|
warning: pc.yellow,
|
|
@@ -8050,12 +8139,7 @@ function decompressAsync(body, encoding) {
|
|
|
8050
8139
|
if (!decompressor) return Promise.resolve(body);
|
|
8051
8140
|
return new Promise((resolve5) => {
|
|
8052
8141
|
decompressor(body, (err, result) => {
|
|
8053
|
-
|
|
8054
|
-
brakitDebug(`decompress failed: ${err.message}`);
|
|
8055
|
-
resolve5(body);
|
|
8056
|
-
} else {
|
|
8057
|
-
resolve5(result);
|
|
8058
|
-
}
|
|
8142
|
+
resolve5(err ? body : result);
|
|
8059
8143
|
});
|
|
8060
8144
|
});
|
|
8061
8145
|
}
|
|
@@ -8072,18 +8156,23 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
8072
8156
|
let resSize = 0;
|
|
8073
8157
|
const originalWrite = res.write;
|
|
8074
8158
|
const originalEnd = res.end;
|
|
8159
|
+
let truncated = false;
|
|
8075
8160
|
res.write = function(...args) {
|
|
8076
8161
|
try {
|
|
8077
8162
|
const chunk = args[0];
|
|
8078
|
-
if (chunk != null && typeof chunk !== "function"
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8163
|
+
if (chunk != null && typeof chunk !== "function") {
|
|
8164
|
+
if (resSize < DEFAULT_MAX_BODY_CAPTURE) {
|
|
8165
|
+
const buf = toBuffer(chunk);
|
|
8166
|
+
if (buf) {
|
|
8167
|
+
resChunks.push(buf);
|
|
8168
|
+
resSize += buf.length;
|
|
8169
|
+
}
|
|
8170
|
+
} else {
|
|
8171
|
+
truncated = true;
|
|
8083
8172
|
}
|
|
8084
8173
|
}
|
|
8085
8174
|
} catch (e) {
|
|
8086
|
-
brakitDebug(`capture write: ${e
|
|
8175
|
+
brakitDebug(`capture write: ${getErrorMessage(e)}`);
|
|
8087
8176
|
}
|
|
8088
8177
|
return originalWrite.apply(this, args);
|
|
8089
8178
|
};
|
|
@@ -8097,7 +8186,7 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
8097
8186
|
}
|
|
8098
8187
|
}
|
|
8099
8188
|
} catch (e) {
|
|
8100
|
-
brakitDebug(`capture end: ${e
|
|
8189
|
+
brakitDebug(`capture end: ${getErrorMessage(e)}`);
|
|
8101
8190
|
}
|
|
8102
8191
|
const result = originalEnd.apply(this, args);
|
|
8103
8192
|
const endTime = performance.now();
|
|
@@ -8109,7 +8198,7 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
8109
8198
|
void (async () => {
|
|
8110
8199
|
try {
|
|
8111
8200
|
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
8112
|
-
if (body && encoding) {
|
|
8201
|
+
if (body && encoding && !truncated) {
|
|
8113
8202
|
body = await decompressAsync(body, encoding);
|
|
8114
8203
|
}
|
|
8115
8204
|
requestStore.capture({
|
|
@@ -8127,7 +8216,7 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
8127
8216
|
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
8128
8217
|
});
|
|
8129
8218
|
} catch (e) {
|
|
8130
|
-
brakitDebug(`capture store: ${e
|
|
8219
|
+
brakitDebug(`capture store: ${getErrorMessage(e)}`);
|
|
8131
8220
|
}
|
|
8132
8221
|
})();
|
|
8133
8222
|
return result;
|
|
@@ -8138,6 +8227,7 @@ var init_capture = __esm({
|
|
|
8138
8227
|
"use strict";
|
|
8139
8228
|
init_constants();
|
|
8140
8229
|
init_log();
|
|
8230
|
+
init_type_guards();
|
|
8141
8231
|
}
|
|
8142
8232
|
});
|
|
8143
8233
|
|
|
@@ -8216,7 +8306,7 @@ __export(setup_exports, {
|
|
|
8216
8306
|
setup: () => setup
|
|
8217
8307
|
});
|
|
8218
8308
|
import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
8219
|
-
import { existsSync as existsSync7, unlinkSync as
|
|
8309
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
8220
8310
|
import { resolve as resolve4 } from "path";
|
|
8221
8311
|
function setup() {
|
|
8222
8312
|
if (initPromise) return initPromise;
|
|
@@ -8224,6 +8314,7 @@ function setup() {
|
|
|
8224
8314
|
return initPromise;
|
|
8225
8315
|
}
|
|
8226
8316
|
async function doSetup() {
|
|
8317
|
+
brakitDebug(`[setup] doSetup called at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
8227
8318
|
const bus = new EventBus();
|
|
8228
8319
|
const registry = new ServiceRegistry();
|
|
8229
8320
|
const requestStore = new RequestStore();
|
|
@@ -8254,7 +8345,9 @@ async function doSetup() {
|
|
|
8254
8345
|
const cwd = process.cwd();
|
|
8255
8346
|
let framework = "unknown";
|
|
8256
8347
|
try {
|
|
8257
|
-
const pkg = JSON.parse(
|
|
8348
|
+
const pkg = JSON.parse(
|
|
8349
|
+
await readFile5(resolve4(cwd, "package.json"), "utf-8")
|
|
8350
|
+
);
|
|
8258
8351
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
8259
8352
|
framework = detectFrameworkFromDeps(allDeps);
|
|
8260
8353
|
} catch {
|
|
@@ -8265,12 +8358,13 @@ async function doSetup() {
|
|
|
8265
8358
|
false,
|
|
8266
8359
|
adapterRegistry.getActive().map((a) => a.name)
|
|
8267
8360
|
);
|
|
8268
|
-
const
|
|
8361
|
+
const dataDir = getProjectDataDir(cwd);
|
|
8362
|
+
const metricsStore = new MetricsStore(new FileMetricsPersistence(dataDir));
|
|
8269
8363
|
metricsStore.start();
|
|
8270
8364
|
registry.register("metrics-store", metricsStore);
|
|
8271
|
-
const
|
|
8272
|
-
|
|
8273
|
-
registry.register("
|
|
8365
|
+
const issueStore = new IssueStore(dataDir);
|
|
8366
|
+
issueStore.start();
|
|
8367
|
+
registry.register("issue-store", issueStore);
|
|
8274
8368
|
const analysisEngine = new AnalysisEngine(registry);
|
|
8275
8369
|
analysisEngine.start();
|
|
8276
8370
|
registry.register("analysis-engine", analysisEngine);
|
|
@@ -8297,6 +8391,7 @@ async function doSetup() {
|
|
|
8297
8391
|
requestStore,
|
|
8298
8392
|
onFirstRequest(port) {
|
|
8299
8393
|
setBrakitPort(port);
|
|
8394
|
+
brakitDebug(`[setup] onFirstRequest fired, port=${port}`);
|
|
8300
8395
|
void (async () => {
|
|
8301
8396
|
try {
|
|
8302
8397
|
const dir = resolve4(cwd, METRICS_DIR);
|
|
@@ -8304,19 +8399,29 @@ async function doSetup() {
|
|
|
8304
8399
|
const portPath = resolve4(cwd, PORT_FILE);
|
|
8305
8400
|
try {
|
|
8306
8401
|
const old = await readFile5(portPath, "utf-8");
|
|
8307
|
-
if (old.trim()
|
|
8308
|
-
brakitDebug(`
|
|
8402
|
+
if (old.trim() === String(port)) {
|
|
8403
|
+
brakitDebug(`[setup] port file already correct, skipping write`);
|
|
8404
|
+
return;
|
|
8405
|
+
}
|
|
8406
|
+
if (old.trim()) {
|
|
8407
|
+
brakitDebug(
|
|
8408
|
+
`Overwriting stale port file (was ${old.trim()}, now ${port})`
|
|
8409
|
+
);
|
|
8309
8410
|
}
|
|
8310
8411
|
} catch {
|
|
8412
|
+
brakitDebug(`[setup] no existing port file, will create`);
|
|
8311
8413
|
}
|
|
8312
8414
|
await writeFile3(portPath, String(port));
|
|
8415
|
+
brakitDebug(`[setup] wrote port file: ${portPath}`);
|
|
8313
8416
|
} catch (err) {
|
|
8314
|
-
brakitDebug(`port file write failed: ${err
|
|
8417
|
+
brakitDebug(`port file write failed: ${getErrorMessage(err)}`);
|
|
8315
8418
|
}
|
|
8316
8419
|
})();
|
|
8317
8420
|
terminalDispose = startTerminalInsights(registry, port);
|
|
8318
|
-
process.stdout.write(
|
|
8319
|
-
`
|
|
8421
|
+
process.stdout.write(
|
|
8422
|
+
` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
8423
|
+
`
|
|
8424
|
+
);
|
|
8320
8425
|
}
|
|
8321
8426
|
});
|
|
8322
8427
|
let telemetrySent = false;
|
|
@@ -8336,12 +8441,13 @@ async function doSetup() {
|
|
|
8336
8441
|
uninstallInterceptor();
|
|
8337
8442
|
terminalDispose?.();
|
|
8338
8443
|
analysisEngine.stop();
|
|
8339
|
-
|
|
8444
|
+
issueStore.stop();
|
|
8340
8445
|
metricsStore.stop();
|
|
8341
8446
|
try {
|
|
8342
8447
|
const portPath = resolve4(cwd, PORT_FILE);
|
|
8343
|
-
if (existsSync7(portPath))
|
|
8344
|
-
} catch {
|
|
8448
|
+
if (existsSync7(portPath)) unlinkSync3(portPath);
|
|
8449
|
+
} catch (err) {
|
|
8450
|
+
brakitDebug(`[setup] port file cleanup failed: ${getErrorMessage(err)}`);
|
|
8345
8451
|
}
|
|
8346
8452
|
};
|
|
8347
8453
|
health.setTeardown(runTeardown);
|
|
@@ -8369,7 +8475,7 @@ var init_setup = __esm({
|
|
|
8369
8475
|
init_error_store();
|
|
8370
8476
|
init_query_store();
|
|
8371
8477
|
init_store();
|
|
8372
|
-
|
|
8478
|
+
init_issue_store();
|
|
8373
8479
|
init_engine();
|
|
8374
8480
|
init_terminal();
|
|
8375
8481
|
init_src();
|
|
@@ -8377,6 +8483,8 @@ var init_setup = __esm({
|
|
|
8377
8483
|
init_health2();
|
|
8378
8484
|
init_interceptor();
|
|
8379
8485
|
init_log();
|
|
8486
|
+
init_type_guards();
|
|
8487
|
+
init_fs();
|
|
8380
8488
|
init_project();
|
|
8381
8489
|
init_telemetry2();
|
|
8382
8490
|
initPromise = null;
|