brakit 0.8.4 → 0.8.5
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/dist/api.d.ts +26 -9
- package/dist/api.js +209 -99
- package/dist/bin/brakit.js +509 -218
- package/dist/dashboard.html +2652 -0
- package/dist/mcp/server.js +195 -90
- package/dist/runtime/index.js +854 -388
- package/package.json +3 -2
package/dist/runtime/index.js
CHANGED
|
@@ -9,28 +9,29 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/constants/routes.ts
|
|
12
|
-
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, VALID_TABS;
|
|
12
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS;
|
|
13
13
|
var init_routes = __esm({
|
|
14
14
|
"src/constants/routes.ts"() {
|
|
15
15
|
"use strict";
|
|
16
16
|
DASHBOARD_PREFIX = "/__brakit";
|
|
17
|
-
DASHBOARD_API_REQUESTS =
|
|
18
|
-
DASHBOARD_API_EVENTS =
|
|
19
|
-
DASHBOARD_API_FLOWS =
|
|
20
|
-
DASHBOARD_API_CLEAR =
|
|
21
|
-
DASHBOARD_API_LOGS =
|
|
22
|
-
DASHBOARD_API_FETCHES =
|
|
23
|
-
DASHBOARD_API_ERRORS =
|
|
24
|
-
DASHBOARD_API_QUERIES =
|
|
25
|
-
DASHBOARD_API_INGEST =
|
|
26
|
-
DASHBOARD_API_METRICS =
|
|
27
|
-
DASHBOARD_API_ACTIVITY =
|
|
28
|
-
DASHBOARD_API_METRICS_LIVE =
|
|
29
|
-
DASHBOARD_API_INSIGHTS =
|
|
30
|
-
DASHBOARD_API_SECURITY =
|
|
31
|
-
DASHBOARD_API_TAB =
|
|
32
|
-
DASHBOARD_API_FINDINGS =
|
|
33
|
-
|
|
17
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
18
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
19
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
20
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
21
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
22
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
23
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
24
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
25
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
26
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
27
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
28
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
29
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
30
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
31
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
32
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
33
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
34
|
+
VALID_TABS_TUPLE = [
|
|
34
35
|
"overview",
|
|
35
36
|
"actions",
|
|
36
37
|
"requests",
|
|
@@ -40,12 +41,13 @@ var init_routes = __esm({
|
|
|
40
41
|
"logs",
|
|
41
42
|
"performance",
|
|
42
43
|
"security"
|
|
43
|
-
]
|
|
44
|
+
];
|
|
45
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
44
46
|
}
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
// src/constants/limits.ts
|
|
48
|
-
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;
|
|
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, FINDING_ID_HASH_LENGTH, FINDINGS_DATA_VERSION, SENSITIVE_MASK_PLACEHOLDER;
|
|
49
51
|
var init_limits = __esm({
|
|
50
52
|
"src/constants/limits.ts"() {
|
|
51
53
|
"use strict";
|
|
@@ -54,10 +56,15 @@ var init_limits = __esm({
|
|
|
54
56
|
DEFAULT_API_LIMIT = 500;
|
|
55
57
|
MAX_TELEMETRY_ENTRIES = 1e3;
|
|
56
58
|
MAX_TAB_NAME_LENGTH = 32;
|
|
57
|
-
MAX_INGEST_BYTES =
|
|
59
|
+
MAX_INGEST_BYTES = 10485760;
|
|
58
60
|
TERMINAL_TRUNCATE_LENGTH = 80;
|
|
59
61
|
SENSITIVE_MASK_MIN_LENGTH = 8;
|
|
60
62
|
SENSITIVE_MASK_VISIBLE_CHARS = 4;
|
|
63
|
+
MAX_JSON_BODY_BYTES = 65536;
|
|
64
|
+
ANALYSIS_DEBOUNCE_MS = 300;
|
|
65
|
+
FINDING_ID_HASH_LENGTH = 16;
|
|
66
|
+
FINDINGS_DATA_VERSION = 1;
|
|
67
|
+
SENSITIVE_MASK_PLACEHOLDER = "****";
|
|
61
68
|
}
|
|
62
69
|
});
|
|
63
70
|
|
|
@@ -104,31 +111,23 @@ var init_transport = __esm({
|
|
|
104
111
|
"src/constants/transport.ts"() {
|
|
105
112
|
"use strict";
|
|
106
113
|
SSE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
107
|
-
NOISE_HOSTS = [
|
|
108
|
-
|
|
109
|
-
"telemetry.nextjs.org",
|
|
110
|
-
"vitejs.dev"
|
|
111
|
-
];
|
|
112
|
-
NOISE_PATH_PATTERNS = [
|
|
113
|
-
".hot-update.",
|
|
114
|
-
"__webpack",
|
|
115
|
-
"__vite"
|
|
116
|
-
];
|
|
114
|
+
NOISE_HOSTS = ["registry.npmjs.org", "telemetry.nextjs.org", "vitejs.dev"];
|
|
115
|
+
NOISE_PATH_PATTERNS = [".hot-update.", "__webpack", "__vite"];
|
|
117
116
|
}
|
|
118
117
|
});
|
|
119
118
|
|
|
120
119
|
// src/constants/metrics.ts
|
|
121
|
-
var METRICS_DIR, METRICS_FILE,
|
|
120
|
+
var METRICS_DIR, METRICS_FILE, PORT_FILE, FINDINGS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, FINDINGS_FLUSH_INTERVAL_MS;
|
|
122
121
|
var init_metrics = __esm({
|
|
123
122
|
"src/constants/metrics.ts"() {
|
|
124
123
|
"use strict";
|
|
125
124
|
METRICS_DIR = ".brakit";
|
|
126
125
|
METRICS_FILE = ".brakit/metrics.json";
|
|
126
|
+
PORT_FILE = ".brakit/port";
|
|
127
|
+
FINDINGS_FILE = ".brakit/findings.json";
|
|
127
128
|
METRICS_FLUSH_INTERVAL_MS = 3e4;
|
|
128
129
|
METRICS_MAX_SESSIONS = 50;
|
|
129
130
|
METRICS_MAX_DATA_POINTS = 200;
|
|
130
|
-
PORT_FILE = ".brakit/port";
|
|
131
|
-
FINDINGS_FILE = ".brakit/findings.json";
|
|
132
131
|
FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
133
132
|
}
|
|
134
133
|
});
|
|
@@ -150,38 +149,36 @@ var init_headers = __esm({
|
|
|
150
149
|
});
|
|
151
150
|
|
|
152
151
|
// src/constants/network.ts
|
|
153
|
-
var LOCALHOST_IPS, LOCALHOST_HOSTNAMES,
|
|
152
|
+
var CLOUD_SIGNALS, MAX_HEALTH_ERRORS, RECOVERY_WINDOW_MS, LOCALHOST_IPS, LOCALHOST_HOSTNAMES, URL_PARSE_BASE;
|
|
154
153
|
var init_network = __esm({
|
|
155
154
|
"src/constants/network.ts"() {
|
|
156
155
|
"use strict";
|
|
157
|
-
LOCALHOST_IPS = /* @__PURE__ */ new Set([
|
|
158
|
-
"127.0.0.1",
|
|
159
|
-
"::1",
|
|
160
|
-
"::ffff:127.0.0.1"
|
|
161
|
-
]);
|
|
162
|
-
LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
163
|
-
"localhost",
|
|
164
|
-
"127.0.0.1",
|
|
165
|
-
"::1"
|
|
166
|
-
]);
|
|
167
156
|
CLOUD_SIGNALS = [
|
|
168
157
|
"VERCEL",
|
|
169
158
|
"VERCEL_ENV",
|
|
170
159
|
"NETLIFY",
|
|
171
160
|
"AWS_LAMBDA_FUNCTION_NAME",
|
|
172
161
|
"AWS_EXECUTION_ENV",
|
|
162
|
+
"ECS_CONTAINER_METADATA_URI",
|
|
173
163
|
"GOOGLE_CLOUD_PROJECT",
|
|
174
164
|
"GCP_PROJECT",
|
|
165
|
+
"K_SERVICE",
|
|
175
166
|
"AZURE_FUNCTIONS_ENVIRONMENT",
|
|
167
|
+
"WEBSITE_SITE_NAME",
|
|
176
168
|
"FLY_APP_NAME",
|
|
177
169
|
"RAILWAY_ENVIRONMENT",
|
|
178
170
|
"RENDER",
|
|
179
|
-
"
|
|
171
|
+
"HEROKU_APP_NAME",
|
|
172
|
+
"DYNO",
|
|
173
|
+
"CF_INSTANCE_GUID",
|
|
180
174
|
"CF_PAGES",
|
|
181
|
-
"KUBERNETES_SERVICE_HOST"
|
|
182
|
-
"ECS_CONTAINER_METADATA_URI"
|
|
175
|
+
"KUBERNETES_SERVICE_HOST"
|
|
183
176
|
];
|
|
184
177
|
MAX_HEALTH_ERRORS = 10;
|
|
178
|
+
RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
179
|
+
LOCALHOST_IPS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
180
|
+
LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
181
|
+
URL_PARSE_BASE = "http://localhost";
|
|
185
182
|
}
|
|
186
183
|
});
|
|
187
184
|
|
|
@@ -204,7 +201,7 @@ var init_encoding = __esm({
|
|
|
204
201
|
});
|
|
205
202
|
|
|
206
203
|
// src/constants/severity.ts
|
|
207
|
-
var SEVERITY_ICON
|
|
204
|
+
var SEVERITY_ICON;
|
|
208
205
|
var init_severity = __esm({
|
|
209
206
|
"src/constants/severity.ts"() {
|
|
210
207
|
"use strict";
|
|
@@ -213,28 +210,27 @@ var init_severity = __esm({
|
|
|
213
210
|
warning: "\u26A0",
|
|
214
211
|
info: "\u2139"
|
|
215
212
|
};
|
|
216
|
-
SEVERITY_CRITICAL = "critical";
|
|
217
|
-
SEVERITY_WARNING = "warning";
|
|
218
|
-
SEVERITY_INFO = "info";
|
|
219
|
-
SEVERITY_ICON_MAP = {
|
|
220
|
-
[SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
|
|
221
|
-
[SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
|
|
222
|
-
[SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
|
|
223
|
-
};
|
|
224
213
|
}
|
|
225
214
|
});
|
|
226
215
|
|
|
227
216
|
// src/constants/telemetry.ts
|
|
228
|
-
var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS
|
|
217
|
+
var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
|
|
229
218
|
var init_telemetry = __esm({
|
|
230
219
|
"src/constants/telemetry.ts"() {
|
|
231
220
|
"use strict";
|
|
232
221
|
POSTHOG_HOST = "https://us.i.posthog.com";
|
|
233
222
|
POSTHOG_CAPTURE_PATH = "/i/v0/e/";
|
|
234
223
|
POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// src/constants/lifecycle.ts
|
|
228
|
+
var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES;
|
|
229
|
+
var init_lifecycle = __esm({
|
|
230
|
+
"src/constants/lifecycle.ts"() {
|
|
231
|
+
"use strict";
|
|
232
|
+
VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
233
|
+
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
238
234
|
}
|
|
239
235
|
});
|
|
240
236
|
|
|
@@ -253,6 +249,7 @@ var init_constants = __esm({
|
|
|
253
249
|
init_encoding();
|
|
254
250
|
init_severity();
|
|
255
251
|
init_telemetry();
|
|
252
|
+
init_lifecycle();
|
|
256
253
|
}
|
|
257
254
|
});
|
|
258
255
|
|
|
@@ -419,11 +416,12 @@ function createCaptureError(emit) {
|
|
|
419
416
|
}
|
|
420
417
|
function setupErrorHook(emit) {
|
|
421
418
|
const captureError = createCaptureError(emit);
|
|
422
|
-
|
|
419
|
+
const brakitExceptionHandler = (err) => {
|
|
423
420
|
captureError(err);
|
|
424
|
-
process.
|
|
421
|
+
process.removeListener("uncaughtException", brakitExceptionHandler);
|
|
425
422
|
throw err;
|
|
426
|
-
}
|
|
423
|
+
};
|
|
424
|
+
process.on("uncaughtException", brakitExceptionHandler);
|
|
427
425
|
process.on("unhandledRejection", (reason) => {
|
|
428
426
|
captureError(reason);
|
|
429
427
|
});
|
|
@@ -610,7 +608,10 @@ var init_pg = __esm({
|
|
|
610
608
|
const result = saved.apply(this, args);
|
|
611
609
|
if (result && typeof result.then === "function") {
|
|
612
610
|
return result.then((res) => {
|
|
613
|
-
|
|
611
|
+
try {
|
|
612
|
+
emitQuery(res?.rowCount ?? void 0);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
614
615
|
return res;
|
|
615
616
|
});
|
|
616
617
|
}
|
|
@@ -688,7 +689,10 @@ var init_mysql2 = __esm({
|
|
|
688
689
|
const result = orig.apply(this, args);
|
|
689
690
|
if (result && typeof result.then === "function") {
|
|
690
691
|
return result.then((res) => {
|
|
691
|
-
|
|
692
|
+
try {
|
|
693
|
+
emitQuery();
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
692
696
|
return res;
|
|
693
697
|
});
|
|
694
698
|
}
|
|
@@ -803,6 +807,27 @@ var init_adapters = __esm({
|
|
|
803
807
|
}
|
|
804
808
|
});
|
|
805
809
|
|
|
810
|
+
// src/constants/http.ts
|
|
811
|
+
var HTTP_OK, HTTP_NO_CONTENT, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_PAYLOAD_TOO_LARGE, HTTP_INTERNAL_ERROR, SECURITY_HEADERS;
|
|
812
|
+
var init_http = __esm({
|
|
813
|
+
"src/constants/http.ts"() {
|
|
814
|
+
"use strict";
|
|
815
|
+
HTTP_OK = 200;
|
|
816
|
+
HTTP_NO_CONTENT = 204;
|
|
817
|
+
HTTP_BAD_REQUEST = 400;
|
|
818
|
+
HTTP_NOT_FOUND = 404;
|
|
819
|
+
HTTP_METHOD_NOT_ALLOWED = 405;
|
|
820
|
+
HTTP_PAYLOAD_TOO_LARGE = 413;
|
|
821
|
+
HTTP_INTERNAL_ERROR = 500;
|
|
822
|
+
SECURITY_HEADERS = {
|
|
823
|
+
"x-content-type-options": "nosniff",
|
|
824
|
+
"x-frame-options": "DENY",
|
|
825
|
+
"referrer-policy": "no-referrer",
|
|
826
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
806
831
|
// src/analysis/categorize.ts
|
|
807
832
|
function detectCategory(req) {
|
|
808
833
|
const { method, url, statusCode, responseHeaders } = req;
|
|
@@ -1184,12 +1209,12 @@ var init_group = __esm({
|
|
|
1184
1209
|
});
|
|
1185
1210
|
|
|
1186
1211
|
// src/dashboard/api/shared.ts
|
|
1187
|
-
function maskSensitiveHeaders(
|
|
1212
|
+
function maskSensitiveHeaders(headers2) {
|
|
1188
1213
|
const masked = {};
|
|
1189
|
-
for (const [key, value] of Object.entries(
|
|
1214
|
+
for (const [key, value] of Object.entries(headers2)) {
|
|
1190
1215
|
if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
|
|
1191
1216
|
const s = String(value);
|
|
1192
|
-
masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ?
|
|
1217
|
+
masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? SENSITIVE_MASK_PLACEHOLDER : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
|
|
1193
1218
|
} else {
|
|
1194
1219
|
masked[key] = value;
|
|
1195
1220
|
}
|
|
@@ -1209,14 +1234,14 @@ function getCorsOrigin(req) {
|
|
|
1209
1234
|
}
|
|
1210
1235
|
function getJsonHeaders(req) {
|
|
1211
1236
|
const corsOrigin = getCorsOrigin(req);
|
|
1212
|
-
const
|
|
1237
|
+
const headers2 = {
|
|
1213
1238
|
"content-type": "application/json",
|
|
1214
1239
|
"cache-control": "no-cache"
|
|
1215
1240
|
};
|
|
1216
1241
|
if (corsOrigin) {
|
|
1217
|
-
|
|
1242
|
+
headers2["access-control-allow-origin"] = corsOrigin;
|
|
1218
1243
|
}
|
|
1219
|
-
return
|
|
1244
|
+
return headers2;
|
|
1220
1245
|
}
|
|
1221
1246
|
function sendJson(req, res, status, data) {
|
|
1222
1247
|
res.writeHead(status, getJsonHeaders(req));
|
|
@@ -1224,23 +1249,58 @@ function sendJson(req, res, status, data) {
|
|
|
1224
1249
|
}
|
|
1225
1250
|
function requireGet(req, res) {
|
|
1226
1251
|
if (req.method !== "GET") {
|
|
1227
|
-
sendJson(req, res,
|
|
1252
|
+
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1228
1253
|
return false;
|
|
1229
1254
|
}
|
|
1230
1255
|
return true;
|
|
1231
1256
|
}
|
|
1257
|
+
function parseRequestUrl(req) {
|
|
1258
|
+
return new URL(req.url ?? "/", URL_PARSE_BASE);
|
|
1259
|
+
}
|
|
1260
|
+
function readJsonBody(req, res, maxBytes = MAX_JSON_BODY_BYTES) {
|
|
1261
|
+
return new Promise((resolve5) => {
|
|
1262
|
+
const chunks = [];
|
|
1263
|
+
let size = 0;
|
|
1264
|
+
req.on("data", (chunk) => {
|
|
1265
|
+
size += chunk.length;
|
|
1266
|
+
if (size > maxBytes) {
|
|
1267
|
+
sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
|
|
1268
|
+
req.destroy();
|
|
1269
|
+
resolve5(null);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
chunks.push(chunk);
|
|
1273
|
+
});
|
|
1274
|
+
req.on("end", () => {
|
|
1275
|
+
if (size > maxBytes) {
|
|
1276
|
+
resolve5(null);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
resolve5(JSON.parse(Buffer.concat(chunks).toString()));
|
|
1281
|
+
} catch {
|
|
1282
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON body" });
|
|
1283
|
+
resolve5(null);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
req.on("error", () => {
|
|
1287
|
+
resolve5(null);
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1232
1291
|
function handleTelemetryGet(req, res, store) {
|
|
1233
1292
|
if (!requireGet(req, res)) return;
|
|
1234
|
-
const url =
|
|
1293
|
+
const url = parseRequestUrl(req);
|
|
1235
1294
|
const requestId = url.searchParams.get("requestId");
|
|
1236
1295
|
const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
|
|
1237
|
-
sendJson(req, res,
|
|
1296
|
+
sendJson(req, res, HTTP_OK, { total: entries.length, entries: entries.reverse() });
|
|
1238
1297
|
}
|
|
1239
1298
|
var init_shared2 = __esm({
|
|
1240
1299
|
"src/dashboard/api/shared.ts"() {
|
|
1241
1300
|
"use strict";
|
|
1242
1301
|
init_constants();
|
|
1243
1302
|
init_limits();
|
|
1303
|
+
init_http();
|
|
1244
1304
|
}
|
|
1245
1305
|
});
|
|
1246
1306
|
|
|
@@ -1255,7 +1315,7 @@ function sanitizeRequest(r) {
|
|
|
1255
1315
|
function createRequestsHandler(registry) {
|
|
1256
1316
|
return (req, res) => {
|
|
1257
1317
|
if (!requireGet(req, res)) return;
|
|
1258
|
-
const url =
|
|
1318
|
+
const url = parseRequestUrl(req);
|
|
1259
1319
|
const method = url.searchParams.get("method");
|
|
1260
1320
|
const status = url.searchParams.get("status");
|
|
1261
1321
|
const search = url.searchParams.get("search");
|
|
@@ -1288,7 +1348,7 @@ function createRequestsHandler(registry) {
|
|
|
1288
1348
|
const total = results.length;
|
|
1289
1349
|
results = results.slice(offset, offset + limit);
|
|
1290
1350
|
const sanitized = results.map(sanitizeRequest);
|
|
1291
|
-
sendJson(req, res,
|
|
1351
|
+
sendJson(req, res, HTTP_OK, { total, requests: sanitized });
|
|
1292
1352
|
};
|
|
1293
1353
|
}
|
|
1294
1354
|
function createFlowsHandler(registry) {
|
|
@@ -1298,13 +1358,13 @@ function createFlowsHandler(registry) {
|
|
|
1298
1358
|
...flow,
|
|
1299
1359
|
requests: flow.requests.map(sanitizeRequest)
|
|
1300
1360
|
}));
|
|
1301
|
-
sendJson(req, res,
|
|
1361
|
+
sendJson(req, res, HTTP_OK, { total: flows.length, flows });
|
|
1302
1362
|
};
|
|
1303
1363
|
}
|
|
1304
1364
|
function createClearHandler(registry) {
|
|
1305
1365
|
return (req, res) => {
|
|
1306
1366
|
if (req.method !== "POST") {
|
|
1307
|
-
sendJson(req, res,
|
|
1367
|
+
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1308
1368
|
return;
|
|
1309
1369
|
}
|
|
1310
1370
|
registry.get("request-store").clear();
|
|
@@ -1315,7 +1375,7 @@ function createClearHandler(registry) {
|
|
|
1315
1375
|
registry.get("metrics-store").reset();
|
|
1316
1376
|
if (registry.has("finding-store")) registry.get("finding-store").clear();
|
|
1317
1377
|
registry.get("event-bus").emit("store:cleared", void 0);
|
|
1318
|
-
sendJson(req, res,
|
|
1378
|
+
sendJson(req, res, HTTP_OK, { cleared: true });
|
|
1319
1379
|
};
|
|
1320
1380
|
}
|
|
1321
1381
|
function createFetchesHandler(registry) {
|
|
@@ -1335,10 +1395,158 @@ var init_handlers = __esm({
|
|
|
1335
1395
|
"use strict";
|
|
1336
1396
|
init_group();
|
|
1337
1397
|
init_constants();
|
|
1398
|
+
init_http();
|
|
1338
1399
|
init_shared2();
|
|
1339
1400
|
}
|
|
1340
1401
|
});
|
|
1341
1402
|
|
|
1403
|
+
// src/utils/type-guards.ts
|
|
1404
|
+
function isString(val) {
|
|
1405
|
+
return typeof val === "string";
|
|
1406
|
+
}
|
|
1407
|
+
function isNumber(val) {
|
|
1408
|
+
return typeof val === "number" && !isNaN(val);
|
|
1409
|
+
}
|
|
1410
|
+
function isBoolean(val) {
|
|
1411
|
+
return typeof val === "boolean";
|
|
1412
|
+
}
|
|
1413
|
+
function getErrorMessage(err) {
|
|
1414
|
+
if (err instanceof Error) return err.message;
|
|
1415
|
+
if (typeof err === "string") return err;
|
|
1416
|
+
return String(err);
|
|
1417
|
+
}
|
|
1418
|
+
function isValidFindingState(val) {
|
|
1419
|
+
return typeof val === "string" && VALID_FINDING_STATES.has(val);
|
|
1420
|
+
}
|
|
1421
|
+
function isValidAiFixStatus(val) {
|
|
1422
|
+
return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
|
|
1423
|
+
}
|
|
1424
|
+
var init_type_guards = __esm({
|
|
1425
|
+
"src/utils/type-guards.ts"() {
|
|
1426
|
+
"use strict";
|
|
1427
|
+
init_lifecycle();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// src/dashboard/api/sdk-event-parser.ts
|
|
1432
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1433
|
+
function str(val, fallback) {
|
|
1434
|
+
return isString(val) ? val : fallback;
|
|
1435
|
+
}
|
|
1436
|
+
function strOrUndef(val) {
|
|
1437
|
+
return isString(val) ? val : void 0;
|
|
1438
|
+
}
|
|
1439
|
+
function num(val, fallback) {
|
|
1440
|
+
return isNumber(val) ? val : fallback;
|
|
1441
|
+
}
|
|
1442
|
+
function numOrUndef(val) {
|
|
1443
|
+
return isNumber(val) ? val : void 0;
|
|
1444
|
+
}
|
|
1445
|
+
function headers(val) {
|
|
1446
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1447
|
+
return val;
|
|
1448
|
+
}
|
|
1449
|
+
return {};
|
|
1450
|
+
}
|
|
1451
|
+
function parseQueryEvent(data, ts, parentRequestId) {
|
|
1452
|
+
return {
|
|
1453
|
+
driver: str(data.source, "sdk"),
|
|
1454
|
+
source: strOrUndef(data.source),
|
|
1455
|
+
sql: strOrUndef(data.sql),
|
|
1456
|
+
model: strOrUndef(data.model),
|
|
1457
|
+
operation: strOrUndef(data.operation),
|
|
1458
|
+
normalizedOp: strOrUndef(data.normalizedOp) ?? strOrUndef(data.operation) ?? "OTHER",
|
|
1459
|
+
table: str(data.table, ""),
|
|
1460
|
+
durationMs: num(data.duration, num(data.durationMs, 0)),
|
|
1461
|
+
rowCount: numOrUndef(data.rowCount),
|
|
1462
|
+
parentRequestId,
|
|
1463
|
+
timestamp: ts
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function parseFetchEvent(data, ts, parentRequestId) {
|
|
1467
|
+
return {
|
|
1468
|
+
url: str(data.url, ""),
|
|
1469
|
+
method: str(data.method, "GET"),
|
|
1470
|
+
statusCode: num(data.statusCode, 0),
|
|
1471
|
+
durationMs: num(data.duration, num(data.durationMs, 0)),
|
|
1472
|
+
parentRequestId,
|
|
1473
|
+
timestamp: ts
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function parseLogEvent(data, ts, parentRequestId) {
|
|
1477
|
+
return {
|
|
1478
|
+
level: str(data.level, "log"),
|
|
1479
|
+
message: str(data.message, ""),
|
|
1480
|
+
parentRequestId,
|
|
1481
|
+
timestamp: ts
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function parseErrorEvent(data, ts, parentRequestId) {
|
|
1485
|
+
return {
|
|
1486
|
+
name: str(data.name, "Error"),
|
|
1487
|
+
message: str(data.message, ""),
|
|
1488
|
+
stack: str(data.stack, ""),
|
|
1489
|
+
parentRequestId,
|
|
1490
|
+
timestamp: ts
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function parseAuthEvent(data, ts, parentRequestId) {
|
|
1494
|
+
return {
|
|
1495
|
+
level: "info",
|
|
1496
|
+
message: `[auth] ${str(data.provider, "unknown")}: ${str(data.result, "check")}`,
|
|
1497
|
+
parentRequestId,
|
|
1498
|
+
timestamp: ts
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
function parseRequestEvent(data, ts) {
|
|
1502
|
+
const url = str(data.url, "");
|
|
1503
|
+
return {
|
|
1504
|
+
id: str(data.id, randomUUID3()),
|
|
1505
|
+
method: str(data.method, "GET"),
|
|
1506
|
+
url,
|
|
1507
|
+
path: url.split("?")[0],
|
|
1508
|
+
headers: headers(data.headers),
|
|
1509
|
+
requestBody: isString(data.requestBody) ? data.requestBody : null,
|
|
1510
|
+
statusCode: num(data.statusCode, 200),
|
|
1511
|
+
responseHeaders: headers(data.responseHeaders),
|
|
1512
|
+
responseBody: isString(data.responseBody) ? data.responseBody : null,
|
|
1513
|
+
startedAt: ts,
|
|
1514
|
+
durationMs: num(data.durationMs, 0),
|
|
1515
|
+
responseSize: num(data.responseSize, 0),
|
|
1516
|
+
isStatic: isBoolean(data.isStatic) ? data.isStatic : false
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function routeSDKEvent(event, stores) {
|
|
1520
|
+
const ts = event.timestamp || Date.now();
|
|
1521
|
+
const parentRequestId = event.requestId ?? null;
|
|
1522
|
+
switch (event.type) {
|
|
1523
|
+
case "db.query":
|
|
1524
|
+
stores.addQuery(parseQueryEvent(event.data, ts, parentRequestId));
|
|
1525
|
+
break;
|
|
1526
|
+
case "fetch":
|
|
1527
|
+
stores.addFetch(parseFetchEvent(event.data, ts, parentRequestId));
|
|
1528
|
+
break;
|
|
1529
|
+
case "log":
|
|
1530
|
+
stores.addLog(parseLogEvent(event.data, ts, parentRequestId));
|
|
1531
|
+
break;
|
|
1532
|
+
case "error":
|
|
1533
|
+
stores.addError(parseErrorEvent(event.data, ts, parentRequestId));
|
|
1534
|
+
break;
|
|
1535
|
+
case "auth.check":
|
|
1536
|
+
stores.addLog(parseAuthEvent(event.data, ts, parentRequestId));
|
|
1537
|
+
break;
|
|
1538
|
+
case "request":
|
|
1539
|
+
stores.addRequest(parseRequestEvent(event.data, ts));
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
var init_sdk_event_parser = __esm({
|
|
1544
|
+
"src/dashboard/api/sdk-event-parser.ts"() {
|
|
1545
|
+
"use strict";
|
|
1546
|
+
init_type_guards();
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1342
1550
|
// src/dashboard/api/ingest.ts
|
|
1343
1551
|
function isBrakitBatch(msg) {
|
|
1344
1552
|
return typeof msg === "object" && msg !== null && "_brakit" in msg && msg._brakit === true && !("version" in msg);
|
|
@@ -1363,65 +1571,21 @@ function createIngestHandler(registry) {
|
|
|
1363
1571
|
break;
|
|
1364
1572
|
}
|
|
1365
1573
|
};
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
normalizedOp: event.data.normalizedOp ?? event.data.operation ?? "OTHER",
|
|
1378
|
-
table: event.data.table ?? "",
|
|
1379
|
-
durationMs: event.data.duration ?? event.data.durationMs ?? 0,
|
|
1380
|
-
rowCount: event.data.rowCount,
|
|
1381
|
-
parentRequestId,
|
|
1382
|
-
timestamp: ts
|
|
1383
|
-
});
|
|
1384
|
-
break;
|
|
1385
|
-
case "fetch":
|
|
1386
|
-
registry.get("fetch-store").add({
|
|
1387
|
-
url: event.data.url ?? "",
|
|
1388
|
-
method: event.data.method ?? "GET",
|
|
1389
|
-
statusCode: event.data.statusCode ?? 0,
|
|
1390
|
-
durationMs: event.data.duration ?? event.data.durationMs ?? 0,
|
|
1391
|
-
parentRequestId,
|
|
1392
|
-
timestamp: ts
|
|
1393
|
-
});
|
|
1394
|
-
break;
|
|
1395
|
-
case "log":
|
|
1396
|
-
registry.get("log-store").add({
|
|
1397
|
-
level: event.data.level ?? "log",
|
|
1398
|
-
message: event.data.message ?? "",
|
|
1399
|
-
parentRequestId,
|
|
1400
|
-
timestamp: ts
|
|
1401
|
-
});
|
|
1402
|
-
break;
|
|
1403
|
-
case "error":
|
|
1404
|
-
registry.get("error-store").add({
|
|
1405
|
-
name: event.data.name ?? "Error",
|
|
1406
|
-
message: event.data.message ?? "",
|
|
1407
|
-
stack: event.data.stack ?? "",
|
|
1408
|
-
parentRequestId,
|
|
1409
|
-
timestamp: ts
|
|
1410
|
-
});
|
|
1411
|
-
break;
|
|
1412
|
-
case "auth.check":
|
|
1413
|
-
registry.get("log-store").add({
|
|
1414
|
-
level: "info",
|
|
1415
|
-
message: `[auth] ${event.data.provider ?? "unknown"}: ${event.data.result ?? "check"}`,
|
|
1416
|
-
parentRequestId,
|
|
1417
|
-
timestamp: ts
|
|
1418
|
-
});
|
|
1419
|
-
break;
|
|
1420
|
-
}
|
|
1574
|
+
const queryStore = registry.get("query-store");
|
|
1575
|
+
const fetchStore = registry.get("fetch-store");
|
|
1576
|
+
const logStore = registry.get("log-store");
|
|
1577
|
+
const errorStore = registry.get("error-store");
|
|
1578
|
+
const requestStore = registry.get("request-store");
|
|
1579
|
+
const stores = {
|
|
1580
|
+
addQuery: (data) => queryStore.add(data),
|
|
1581
|
+
addFetch: (data) => fetchStore.add(data),
|
|
1582
|
+
addLog: (data) => logStore.add(data),
|
|
1583
|
+
addError: (data) => errorStore.add(data),
|
|
1584
|
+
addRequest: (data) => requestStore.add(data)
|
|
1421
1585
|
};
|
|
1422
1586
|
return (req, res) => {
|
|
1423
1587
|
if (req.method !== "POST") {
|
|
1424
|
-
sendJson(req, res,
|
|
1588
|
+
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1425
1589
|
return;
|
|
1426
1590
|
}
|
|
1427
1591
|
const chunks = [];
|
|
@@ -1429,7 +1593,7 @@ function createIngestHandler(registry) {
|
|
|
1429
1593
|
req.on("data", (chunk) => {
|
|
1430
1594
|
totalSize += chunk.length;
|
|
1431
1595
|
if (totalSize > MAX_INGEST_BYTES) {
|
|
1432
|
-
sendJson(req, res,
|
|
1596
|
+
sendJson(req, res, HTTP_PAYLOAD_TOO_LARGE, { error: "Payload too large" });
|
|
1433
1597
|
req.destroy();
|
|
1434
1598
|
return;
|
|
1435
1599
|
}
|
|
@@ -1441,9 +1605,9 @@ function createIngestHandler(registry) {
|
|
|
1441
1605
|
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1442
1606
|
if (isSDKPayload(body)) {
|
|
1443
1607
|
for (const event of body.events) {
|
|
1444
|
-
routeSDKEvent(event);
|
|
1608
|
+
routeSDKEvent(event, stores);
|
|
1445
1609
|
}
|
|
1446
|
-
res.writeHead(
|
|
1610
|
+
res.writeHead(HTTP_NO_CONTENT);
|
|
1447
1611
|
res.end();
|
|
1448
1612
|
return;
|
|
1449
1613
|
}
|
|
@@ -1451,13 +1615,19 @@ function createIngestHandler(registry) {
|
|
|
1451
1615
|
for (const event of body.events) {
|
|
1452
1616
|
routeEvent(event);
|
|
1453
1617
|
}
|
|
1454
|
-
res.writeHead(
|
|
1618
|
+
res.writeHead(HTTP_NO_CONTENT);
|
|
1455
1619
|
res.end();
|
|
1456
1620
|
return;
|
|
1457
1621
|
}
|
|
1458
|
-
sendJson(req, res,
|
|
1622
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid batch" });
|
|
1459
1623
|
} catch {
|
|
1460
|
-
sendJson(req, res,
|
|
1624
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "Invalid JSON" });
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
req.on("error", () => {
|
|
1628
|
+
if (!res.headersSent) {
|
|
1629
|
+
res.writeHead(HTTP_BAD_REQUEST);
|
|
1630
|
+
res.end();
|
|
1461
1631
|
}
|
|
1462
1632
|
});
|
|
1463
1633
|
};
|
|
@@ -1466,7 +1636,9 @@ var init_ingest = __esm({
|
|
|
1466
1636
|
"src/dashboard/api/ingest.ts"() {
|
|
1467
1637
|
"use strict";
|
|
1468
1638
|
init_limits();
|
|
1639
|
+
init_http();
|
|
1469
1640
|
init_shared2();
|
|
1641
|
+
init_sdk_event_parser();
|
|
1470
1642
|
}
|
|
1471
1643
|
});
|
|
1472
1644
|
|
|
@@ -1474,20 +1646,21 @@ var init_ingest = __esm({
|
|
|
1474
1646
|
function createMetricsHandler(metricsStore) {
|
|
1475
1647
|
return (req, res) => {
|
|
1476
1648
|
if (!requireGet(req, res)) return;
|
|
1477
|
-
const url =
|
|
1649
|
+
const url = parseRequestUrl(req);
|
|
1478
1650
|
const endpoint = url.searchParams.get("endpoint");
|
|
1479
1651
|
if (endpoint) {
|
|
1480
1652
|
const ep = metricsStore.getEndpoint(endpoint);
|
|
1481
|
-
sendJson(req, res,
|
|
1653
|
+
sendJson(req, res, HTTP_OK, { endpoints: ep ? [ep] : [] });
|
|
1482
1654
|
return;
|
|
1483
1655
|
}
|
|
1484
|
-
sendJson(req, res,
|
|
1656
|
+
sendJson(req, res, HTTP_OK, { endpoints: metricsStore.getAll() });
|
|
1485
1657
|
};
|
|
1486
1658
|
}
|
|
1487
1659
|
var init_metrics2 = __esm({
|
|
1488
1660
|
"src/dashboard/api/metrics.ts"() {
|
|
1489
1661
|
"use strict";
|
|
1490
1662
|
init_shared2();
|
|
1663
|
+
init_http();
|
|
1491
1664
|
}
|
|
1492
1665
|
});
|
|
1493
1666
|
|
|
@@ -1505,15 +1678,34 @@ var init_metrics_live = __esm({
|
|
|
1505
1678
|
}
|
|
1506
1679
|
});
|
|
1507
1680
|
|
|
1681
|
+
// src/utils/log.ts
|
|
1682
|
+
function brakitWarn(message) {
|
|
1683
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
1684
|
+
`);
|
|
1685
|
+
}
|
|
1686
|
+
function brakitDebug(message) {
|
|
1687
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
1688
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
1689
|
+
`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
var PREFIX;
|
|
1693
|
+
var init_log = __esm({
|
|
1694
|
+
"src/utils/log.ts"() {
|
|
1695
|
+
"use strict";
|
|
1696
|
+
PREFIX = "[brakit]";
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1508
1700
|
// src/dashboard/api/activity.ts
|
|
1509
1701
|
function createActivityHandler(registry) {
|
|
1510
1702
|
return (req, res) => {
|
|
1511
1703
|
if (!requireGet(req, res)) return;
|
|
1512
1704
|
try {
|
|
1513
|
-
const url =
|
|
1705
|
+
const url = parseRequestUrl(req);
|
|
1514
1706
|
const requestId = url.searchParams.get("requestId");
|
|
1515
1707
|
if (!requestId) {
|
|
1516
|
-
sendJson(req, res,
|
|
1708
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "requestId parameter required" });
|
|
1517
1709
|
return;
|
|
1518
1710
|
}
|
|
1519
1711
|
const fetches = registry.get("fetch-store").getByRequest(requestId);
|
|
@@ -1530,7 +1722,7 @@ function createActivityHandler(registry) {
|
|
|
1530
1722
|
for (const q of queries)
|
|
1531
1723
|
timeline.push({ type: "query", timestamp: q.timestamp, data: { ...q } });
|
|
1532
1724
|
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
|
1533
|
-
sendJson(req, res,
|
|
1725
|
+
sendJson(req, res, HTTP_OK, {
|
|
1534
1726
|
requestId,
|
|
1535
1727
|
total: timeline.length,
|
|
1536
1728
|
timeline,
|
|
@@ -1542,9 +1734,9 @@ function createActivityHandler(registry) {
|
|
|
1542
1734
|
}
|
|
1543
1735
|
});
|
|
1544
1736
|
} catch (err) {
|
|
1545
|
-
|
|
1737
|
+
brakitDebug(`activity handler error: ${err}`);
|
|
1546
1738
|
if (!res.headersSent) {
|
|
1547
|
-
sendJson(req, res,
|
|
1739
|
+
sendJson(req, res, HTTP_INTERNAL_ERROR, { error: "Internal error" });
|
|
1548
1740
|
}
|
|
1549
1741
|
}
|
|
1550
1742
|
};
|
|
@@ -1553,6 +1745,8 @@ var init_activity = __esm({
|
|
|
1553
1745
|
"src/dashboard/api/activity.ts"() {
|
|
1554
1746
|
"use strict";
|
|
1555
1747
|
init_shared2();
|
|
1748
|
+
init_http();
|
|
1749
|
+
init_log();
|
|
1556
1750
|
}
|
|
1557
1751
|
});
|
|
1558
1752
|
|
|
@@ -1572,19 +1766,20 @@ var init_api = __esm({
|
|
|
1572
1766
|
function createInsightsHandler(engine) {
|
|
1573
1767
|
return (req, res) => {
|
|
1574
1768
|
if (!requireGet(req, res)) return;
|
|
1575
|
-
sendJson(req, res,
|
|
1769
|
+
sendJson(req, res, HTTP_OK, { insights: engine.getStatefulInsights() });
|
|
1576
1770
|
};
|
|
1577
1771
|
}
|
|
1578
1772
|
function createSecurityHandler(engine) {
|
|
1579
1773
|
return (req, res) => {
|
|
1580
1774
|
if (!requireGet(req, res)) return;
|
|
1581
|
-
sendJson(req, res,
|
|
1775
|
+
sendJson(req, res, HTTP_OK, { findings: engine.getStatefulFindings() });
|
|
1582
1776
|
};
|
|
1583
1777
|
}
|
|
1584
1778
|
var init_insights = __esm({
|
|
1585
1779
|
"src/dashboard/api/insights.ts"() {
|
|
1586
1780
|
"use strict";
|
|
1587
1781
|
init_shared2();
|
|
1782
|
+
init_http();
|
|
1588
1783
|
}
|
|
1589
1784
|
});
|
|
1590
1785
|
|
|
@@ -1592,99 +1787,160 @@ var init_insights = __esm({
|
|
|
1592
1787
|
function createFindingsHandler(findingStore) {
|
|
1593
1788
|
return (req, res) => {
|
|
1594
1789
|
if (!requireGet(req, res)) return;
|
|
1595
|
-
const url =
|
|
1790
|
+
const url = parseRequestUrl(req);
|
|
1596
1791
|
const stateParam = url.searchParams.get("state");
|
|
1597
1792
|
let findings;
|
|
1598
|
-
if (stateParam &&
|
|
1793
|
+
if (stateParam && isValidFindingState(stateParam)) {
|
|
1599
1794
|
findings = findingStore.getByState(stateParam);
|
|
1600
1795
|
} else {
|
|
1601
1796
|
findings = findingStore.getAll();
|
|
1602
1797
|
}
|
|
1603
|
-
sendJson(req, res,
|
|
1798
|
+
sendJson(req, res, HTTP_OK, {
|
|
1604
1799
|
total: findings.length,
|
|
1605
1800
|
findings
|
|
1606
1801
|
});
|
|
1607
1802
|
};
|
|
1608
1803
|
}
|
|
1609
|
-
|
|
1804
|
+
function createFindingsReportHandler(findingStore, eventBus, analysisEngine) {
|
|
1805
|
+
return async (req, res) => {
|
|
1806
|
+
if (req.method !== "POST") {
|
|
1807
|
+
sendJson(req, res, HTTP_METHOD_NOT_ALLOWED, { error: "Method not allowed" });
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const body = await readJsonBody(req, res);
|
|
1811
|
+
if (!body) return;
|
|
1812
|
+
const { findingId, status, notes } = body;
|
|
1813
|
+
if (!findingId || typeof findingId !== "string") {
|
|
1814
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "findingId is required" });
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (!isValidAiFixStatus(status)) {
|
|
1818
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "status must be 'fixed' or 'wont_fix'" });
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (!notes || typeof notes !== "string") {
|
|
1822
|
+
sendJson(req, res, HTTP_BAD_REQUEST, { error: "notes is required" });
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const findingOk = findingStore.reportFix(findingId, status, notes);
|
|
1826
|
+
if (findingOk) {
|
|
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
|
+
});
|
|
1838
|
+
sendJson(req, res, HTTP_OK, { ok: true });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
sendJson(req, res, HTTP_NOT_FOUND, { error: "Finding not found" });
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1610
1844
|
var init_findings = __esm({
|
|
1611
1845
|
"src/dashboard/api/findings.ts"() {
|
|
1612
1846
|
"use strict";
|
|
1613
1847
|
init_shared2();
|
|
1614
|
-
|
|
1848
|
+
init_type_guards();
|
|
1849
|
+
init_http();
|
|
1615
1850
|
}
|
|
1616
1851
|
});
|
|
1617
1852
|
|
|
1618
|
-
// src/
|
|
1619
|
-
var
|
|
1620
|
-
var
|
|
1621
|
-
"src/
|
|
1853
|
+
// src/constants/events.ts
|
|
1854
|
+
var SSE_EVENT_FETCH, SSE_EVENT_LOG, SSE_EVENT_ERROR, SSE_EVENT_QUERY, SSE_EVENT_INSIGHTS, SSE_EVENT_SECURITY;
|
|
1855
|
+
var init_events = __esm({
|
|
1856
|
+
"src/constants/events.ts"() {
|
|
1622
1857
|
"use strict";
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
for (const d of this.items) d.dispose();
|
|
1630
|
-
this.items.length = 0;
|
|
1631
|
-
}
|
|
1632
|
-
};
|
|
1858
|
+
SSE_EVENT_FETCH = "fetch";
|
|
1859
|
+
SSE_EVENT_LOG = "log";
|
|
1860
|
+
SSE_EVENT_ERROR = "error_event";
|
|
1861
|
+
SSE_EVENT_QUERY = "query";
|
|
1862
|
+
SSE_EVENT_INSIGHTS = "insights";
|
|
1863
|
+
SSE_EVENT_SECURITY = "security";
|
|
1633
1864
|
}
|
|
1634
1865
|
});
|
|
1635
1866
|
|
|
1636
1867
|
// src/dashboard/sse.ts
|
|
1637
1868
|
function createSSEHandler(registry) {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
connection: "keep-alive",
|
|
1643
|
-
"access-control-allow-origin": "*"
|
|
1644
|
-
});
|
|
1645
|
-
res.write(":ok\n\n");
|
|
1646
|
-
const writeEvent = (eventType, data) => {
|
|
1647
|
-
if (res.destroyed) return;
|
|
1648
|
-
if (eventType) {
|
|
1649
|
-
res.write(`event: ${eventType}
|
|
1869
|
+
const clients = /* @__PURE__ */ new Set();
|
|
1870
|
+
function broadcast(eventType, data) {
|
|
1871
|
+
if (clients.size === 0) return;
|
|
1872
|
+
const frame = eventType ? `event: ${eventType}
|
|
1650
1873
|
data: ${data}
|
|
1651
1874
|
|
|
1652
|
-
`
|
|
1653
|
-
} else {
|
|
1654
|
-
res.write(`data: ${data}
|
|
1875
|
+
` : `data: ${data}
|
|
1655
1876
|
|
|
1656
|
-
|
|
1877
|
+
`;
|
|
1878
|
+
for (const client of clients) {
|
|
1879
|
+
if (client.res.destroyed) {
|
|
1880
|
+
clients.delete(client);
|
|
1881
|
+
continue;
|
|
1657
1882
|
}
|
|
1883
|
+
try {
|
|
1884
|
+
client.res.write(frame);
|
|
1885
|
+
} catch {
|
|
1886
|
+
clients.delete(client);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const bus = registry.get("event-bus");
|
|
1891
|
+
bus.on("request:completed", (r) => broadcast(null, JSON.stringify(r)));
|
|
1892
|
+
bus.on("telemetry:fetch", (e) => broadcast(SSE_EVENT_FETCH, JSON.stringify(e)));
|
|
1893
|
+
bus.on("telemetry:log", (e) => broadcast(SSE_EVENT_LOG, JSON.stringify(e)));
|
|
1894
|
+
bus.on("telemetry:error", (e) => broadcast(SSE_EVENT_ERROR, JSON.stringify(e)));
|
|
1895
|
+
bus.on("telemetry:query", (e) => broadcast(SSE_EVENT_QUERY, JSON.stringify(e)));
|
|
1896
|
+
bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
|
|
1897
|
+
broadcast(SSE_EVENT_INSIGHTS, JSON.stringify(statefulInsights));
|
|
1898
|
+
broadcast(SSE_EVENT_SECURITY, JSON.stringify(statefulFindings));
|
|
1899
|
+
});
|
|
1900
|
+
bus.on("findings:changed", (findings) => {
|
|
1901
|
+
broadcast(SSE_EVENT_SECURITY, JSON.stringify(findings));
|
|
1902
|
+
});
|
|
1903
|
+
return (req, res) => {
|
|
1904
|
+
const headers2 = {
|
|
1905
|
+
"content-type": "text/event-stream",
|
|
1906
|
+
"cache-control": "no-cache",
|
|
1907
|
+
connection: "keep-alive"
|
|
1658
1908
|
};
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
subs.add(bus.on("telemetry:query", (e) => writeEvent("query", JSON.stringify(e))));
|
|
1666
|
-
subs.add(bus.on("analysis:updated", ({ statefulInsights, statefulFindings }) => {
|
|
1667
|
-
writeEvent("insights", JSON.stringify(statefulInsights));
|
|
1668
|
-
writeEvent("security", JSON.stringify(statefulFindings));
|
|
1669
|
-
}));
|
|
1909
|
+
const corsOrigin = getCorsOrigin(req);
|
|
1910
|
+
if (corsOrigin) {
|
|
1911
|
+
headers2["access-control-allow-origin"] = corsOrigin;
|
|
1912
|
+
}
|
|
1913
|
+
res.writeHead(HTTP_OK, headers2);
|
|
1914
|
+
res.write(":ok\n\n");
|
|
1670
1915
|
const heartbeat = setInterval(() => {
|
|
1671
1916
|
if (res.destroyed) {
|
|
1672
1917
|
clearInterval(heartbeat);
|
|
1918
|
+
clients.delete(client);
|
|
1673
1919
|
return;
|
|
1674
1920
|
}
|
|
1675
|
-
|
|
1921
|
+
try {
|
|
1922
|
+
res.write(":heartbeat\n\n");
|
|
1923
|
+
} catch {
|
|
1924
|
+
clearInterval(heartbeat);
|
|
1925
|
+
clients.delete(client);
|
|
1926
|
+
}
|
|
1676
1927
|
}, SSE_HEARTBEAT_INTERVAL_MS);
|
|
1928
|
+
heartbeat.unref();
|
|
1929
|
+
const client = { res, heartbeat };
|
|
1930
|
+
clients.add(client);
|
|
1677
1931
|
req.on("close", () => {
|
|
1678
1932
|
clearInterval(heartbeat);
|
|
1679
|
-
|
|
1933
|
+
clients.delete(client);
|
|
1680
1934
|
});
|
|
1681
1935
|
};
|
|
1682
1936
|
}
|
|
1683
1937
|
var init_sse = __esm({
|
|
1684
1938
|
"src/dashboard/sse.ts"() {
|
|
1685
1939
|
"use strict";
|
|
1686
|
-
init_disposable();
|
|
1687
1940
|
init_constants();
|
|
1941
|
+
init_http();
|
|
1942
|
+
init_events();
|
|
1943
|
+
init_shared2();
|
|
1688
1944
|
}
|
|
1689
1945
|
});
|
|
1690
1946
|
|
|
@@ -2203,6 +2459,13 @@ function getSecurityStyles() {
|
|
|
2203
2459
|
.sec-item-resolved{color:var(--text-muted)}
|
|
2204
2460
|
.sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
|
|
2205
2461
|
.sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
|
|
2462
|
+
|
|
2463
|
+
/* AI status badges */
|
|
2464
|
+
.sec-ai-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:8px;margin-left:8px;white-space:nowrap}
|
|
2465
|
+
.sec-ai-fixing{background:rgba(217,119,6,.1);color:var(--amber)}
|
|
2466
|
+
.sec-ai-wontfix{background:rgba(107,114,128,.1);color:var(--text-muted)}
|
|
2467
|
+
.sec-ai-verified{background:rgba(22,163,74,.1);color:var(--green)}
|
|
2468
|
+
.sec-ai-notes{font-size:11px;color:var(--text-muted);font-style:italic;margin-top:2px;padding-left:0}
|
|
2206
2469
|
`;
|
|
2207
2470
|
}
|
|
2208
2471
|
var init_security = __esm({
|
|
@@ -2266,9 +2529,17 @@ var init_styles = __esm({
|
|
|
2266
2529
|
});
|
|
2267
2530
|
|
|
2268
2531
|
// src/utils/fs.ts
|
|
2269
|
-
import { access } from "fs/promises";
|
|
2532
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
2270
2533
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2271
2534
|
import { resolve } from "path";
|
|
2535
|
+
async function fileExists(path) {
|
|
2536
|
+
try {
|
|
2537
|
+
await access(path);
|
|
2538
|
+
return true;
|
|
2539
|
+
} catch {
|
|
2540
|
+
return false;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2272
2543
|
function ensureGitignore(dir, entry) {
|
|
2273
2544
|
try {
|
|
2274
2545
|
const gitignorePath = resolve(dir, "../.gitignore");
|
|
@@ -2282,28 +2553,22 @@ function ensureGitignore(dir, entry) {
|
|
|
2282
2553
|
} catch {
|
|
2283
2554
|
}
|
|
2284
2555
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
}
|
|
2296
|
-
function brakitDebug(message) {
|
|
2297
|
-
if (process.env.DEBUG_BRAKIT) {
|
|
2298
|
-
process.stderr.write(`${PREFIX}:debug ${message}
|
|
2299
|
-
`);
|
|
2556
|
+
async function ensureGitignoreAsync(dir, entry) {
|
|
2557
|
+
try {
|
|
2558
|
+
const gitignorePath = resolve(dir, "../.gitignore");
|
|
2559
|
+
if (await fileExists(gitignorePath)) {
|
|
2560
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
2561
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
2562
|
+
await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
2563
|
+
} else {
|
|
2564
|
+
await writeFile(gitignorePath, entry + "\n");
|
|
2565
|
+
}
|
|
2566
|
+
} catch {
|
|
2300
2567
|
}
|
|
2301
2568
|
}
|
|
2302
|
-
var
|
|
2303
|
-
|
|
2304
|
-
"src/utils/log.ts"() {
|
|
2569
|
+
var init_fs = __esm({
|
|
2570
|
+
"src/utils/fs.ts"() {
|
|
2305
2571
|
"use strict";
|
|
2306
|
-
PREFIX = "[brakit]";
|
|
2307
2572
|
}
|
|
2308
2573
|
});
|
|
2309
2574
|
|
|
@@ -2321,6 +2586,7 @@ var init_atomic_writer = __esm({
|
|
|
2321
2586
|
"use strict";
|
|
2322
2587
|
init_fs();
|
|
2323
2588
|
init_log();
|
|
2589
|
+
init_type_guards();
|
|
2324
2590
|
AtomicWriter = class {
|
|
2325
2591
|
constructor(opts) {
|
|
2326
2592
|
this.opts = opts;
|
|
@@ -2335,7 +2601,7 @@ var init_atomic_writer = __esm({
|
|
|
2335
2601
|
writeFileSync2(this.tmpPath, content);
|
|
2336
2602
|
renameSync(this.tmpPath, this.opts.filePath);
|
|
2337
2603
|
} catch (err) {
|
|
2338
|
-
brakitWarn(`failed to save ${this.opts.label}: ${err
|
|
2604
|
+
brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
|
|
2339
2605
|
}
|
|
2340
2606
|
}
|
|
2341
2607
|
async writeAsync(content) {
|
|
@@ -2349,13 +2615,14 @@ var init_atomic_writer = __esm({
|
|
|
2349
2615
|
await writeFile2(this.tmpPath, content);
|
|
2350
2616
|
await rename(this.tmpPath, this.opts.filePath);
|
|
2351
2617
|
} catch (err) {
|
|
2352
|
-
brakitWarn(`failed to save ${this.opts.label}: ${err
|
|
2618
|
+
brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
|
|
2353
2619
|
} finally {
|
|
2354
2620
|
this.writing = false;
|
|
2355
2621
|
if (this.pendingContent !== null) {
|
|
2356
2622
|
const next = this.pendingContent;
|
|
2357
2623
|
this.pendingContent = null;
|
|
2358
|
-
this.writeAsync(next)
|
|
2624
|
+
this.writeAsync(next).catch(() => {
|
|
2625
|
+
});
|
|
2359
2626
|
}
|
|
2360
2627
|
}
|
|
2361
2628
|
}
|
|
@@ -2368,10 +2635,10 @@ var init_atomic_writer = __esm({
|
|
|
2368
2635
|
}
|
|
2369
2636
|
}
|
|
2370
2637
|
async ensureDirAsync() {
|
|
2371
|
-
if (!
|
|
2638
|
+
if (!await fileExists(this.opts.dir)) {
|
|
2372
2639
|
await mkdir(this.opts.dir, { recursive: true });
|
|
2373
2640
|
if (this.opts.gitignoreEntry) {
|
|
2374
|
-
|
|
2641
|
+
await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
|
|
2375
2642
|
}
|
|
2376
2643
|
}
|
|
2377
2644
|
}
|
|
@@ -2383,23 +2650,32 @@ var init_atomic_writer = __esm({
|
|
|
2383
2650
|
import { createHash } from "crypto";
|
|
2384
2651
|
function computeFindingId(finding) {
|
|
2385
2652
|
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
2386
|
-
return createHash("sha256").update(key).digest("hex").slice(0,
|
|
2653
|
+
return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
|
|
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);
|
|
2387
2658
|
}
|
|
2388
2659
|
var init_finding_id = __esm({
|
|
2389
2660
|
"src/store/finding-id.ts"() {
|
|
2390
2661
|
"use strict";
|
|
2662
|
+
init_limits();
|
|
2391
2663
|
}
|
|
2392
2664
|
});
|
|
2393
2665
|
|
|
2394
2666
|
// src/store/finding-store.ts
|
|
2667
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2395
2668
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
2396
2669
|
import { resolve as resolve2 } from "path";
|
|
2397
2670
|
var FindingStore;
|
|
2398
2671
|
var init_finding_store = __esm({
|
|
2399
2672
|
"src/store/finding-store.ts"() {
|
|
2400
2673
|
"use strict";
|
|
2674
|
+
init_fs();
|
|
2401
2675
|
init_constants();
|
|
2676
|
+
init_limits();
|
|
2402
2677
|
init_atomic_writer();
|
|
2678
|
+
init_log();
|
|
2403
2679
|
init_finding_id();
|
|
2404
2680
|
FindingStore = class {
|
|
2405
2681
|
constructor(rootDir) {
|
|
@@ -2412,7 +2688,6 @@ var init_finding_store = __esm({
|
|
|
2412
2688
|
gitignoreEntry: METRICS_DIR,
|
|
2413
2689
|
label: "findings"
|
|
2414
2690
|
});
|
|
2415
|
-
this.load();
|
|
2416
2691
|
}
|
|
2417
2692
|
findings = /* @__PURE__ */ new Map();
|
|
2418
2693
|
flushTimer = null;
|
|
@@ -2420,6 +2695,8 @@ var init_finding_store = __esm({
|
|
|
2420
2695
|
writer;
|
|
2421
2696
|
findingsPath;
|
|
2422
2697
|
start() {
|
|
2698
|
+
this.loadAsync().catch(() => {
|
|
2699
|
+
});
|
|
2423
2700
|
this.flushTimer = setInterval(
|
|
2424
2701
|
() => this.flush(),
|
|
2425
2702
|
FINDINGS_FLUSH_INTERVAL_MS
|
|
@@ -2456,7 +2733,9 @@ var init_finding_store = __esm({
|
|
|
2456
2733
|
firstSeenAt: now,
|
|
2457
2734
|
lastSeenAt: now,
|
|
2458
2735
|
resolvedAt: null,
|
|
2459
|
-
occurrences: 1
|
|
2736
|
+
occurrences: 1,
|
|
2737
|
+
aiStatus: null,
|
|
2738
|
+
aiNotes: null
|
|
2460
2739
|
};
|
|
2461
2740
|
this.findings.set(id, stateful);
|
|
2462
2741
|
this.dirty = true;
|
|
@@ -2472,6 +2751,17 @@ var init_finding_store = __esm({
|
|
|
2472
2751
|
this.dirty = true;
|
|
2473
2752
|
return true;
|
|
2474
2753
|
}
|
|
2754
|
+
reportFix(findingId, status, notes) {
|
|
2755
|
+
const finding = this.findings.get(findingId);
|
|
2756
|
+
if (!finding) return false;
|
|
2757
|
+
finding.aiStatus = status;
|
|
2758
|
+
finding.aiNotes = notes;
|
|
2759
|
+
if (status === "fixed") {
|
|
2760
|
+
finding.state = "fixing";
|
|
2761
|
+
}
|
|
2762
|
+
this.dirty = true;
|
|
2763
|
+
return true;
|
|
2764
|
+
}
|
|
2475
2765
|
/**
|
|
2476
2766
|
* Reconcile passive findings against the current analysis results.
|
|
2477
2767
|
*
|
|
@@ -2484,7 +2774,7 @@ var init_finding_store = __esm({
|
|
|
2484
2774
|
reconcilePassive(currentFindings) {
|
|
2485
2775
|
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
2486
2776
|
for (const [id, stateful] of this.findings) {
|
|
2487
|
-
if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
|
|
2777
|
+
if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
|
|
2488
2778
|
stateful.state = "resolved";
|
|
2489
2779
|
stateful.resolvedAt = Date.now();
|
|
2490
2780
|
this.dirty = true;
|
|
@@ -2504,18 +2794,35 @@ var init_finding_store = __esm({
|
|
|
2504
2794
|
this.findings.clear();
|
|
2505
2795
|
this.dirty = true;
|
|
2506
2796
|
}
|
|
2507
|
-
|
|
2797
|
+
async loadAsync() {
|
|
2798
|
+
try {
|
|
2799
|
+
if (await fileExists(this.findingsPath)) {
|
|
2800
|
+
const raw = await readFile2(this.findingsPath, "utf-8");
|
|
2801
|
+
const parsed = JSON.parse(raw);
|
|
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
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
/** Sync load for tests only — not used in production paths. */
|
|
2813
|
+
loadSync() {
|
|
2508
2814
|
try {
|
|
2509
2815
|
if (existsSync3(this.findingsPath)) {
|
|
2510
2816
|
const raw = readFileSync2(this.findingsPath, "utf-8");
|
|
2511
2817
|
const parsed = JSON.parse(raw);
|
|
2512
|
-
if (parsed?.version ===
|
|
2818
|
+
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
2513
2819
|
for (const f of parsed.findings) {
|
|
2514
2820
|
this.findings.set(f.findingId, f);
|
|
2515
2821
|
}
|
|
2516
2822
|
}
|
|
2517
2823
|
}
|
|
2518
|
-
} catch {
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
|
|
2519
2826
|
}
|
|
2520
2827
|
}
|
|
2521
2828
|
flush() {
|
|
@@ -2530,7 +2837,7 @@ var init_finding_store = __esm({
|
|
|
2530
2837
|
}
|
|
2531
2838
|
serialize() {
|
|
2532
2839
|
const data = {
|
|
2533
|
-
version:
|
|
2840
|
+
version: FINDINGS_DATA_VERSION,
|
|
2534
2841
|
findings: [...this.findings.values()]
|
|
2535
2842
|
};
|
|
2536
2843
|
return JSON.stringify(data);
|
|
@@ -2540,9 +2847,9 @@ var init_finding_store = __esm({
|
|
|
2540
2847
|
});
|
|
2541
2848
|
|
|
2542
2849
|
// src/detect/project.ts
|
|
2543
|
-
import { readFile as
|
|
2850
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
2544
2851
|
import { existsSync as existsSync4 } from "fs";
|
|
2545
|
-
import { join } from "path";
|
|
2852
|
+
import { join, relative } from "path";
|
|
2546
2853
|
function detectFrameworkFromDeps(allDeps) {
|
|
2547
2854
|
for (const f of FRAMEWORKS) {
|
|
2548
2855
|
if (allDeps[f.dep]) return f.name;
|
|
@@ -2579,11 +2886,11 @@ var init_patterns = __esm({
|
|
|
2579
2886
|
SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
2580
2887
|
TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
2581
2888
|
SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
2582
|
-
STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections
|
|
2889
|
+
STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
|
|
2583
2890
|
DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
2584
2891
|
SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
2585
|
-
SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
2586
|
-
LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
2892
|
+
SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
2893
|
+
LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
2587
2894
|
MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
2588
2895
|
EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
2589
2896
|
INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -2595,9 +2902,9 @@ var init_patterns = __esm({
|
|
|
2595
2902
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
2596
2903
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
2597
2904
|
"error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
|
|
2905
|
+
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
2598
2906
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
2599
2907
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
2600
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
2601
2908
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
2602
2909
|
};
|
|
2603
2910
|
}
|
|
@@ -3021,48 +3328,47 @@ function hasInternalIds(obj) {
|
|
|
3021
3328
|
}
|
|
3022
3329
|
return false;
|
|
3023
3330
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3331
|
+
function detectEchoPII(method, reqBody, target) {
|
|
3332
|
+
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
3333
|
+
const reqEmails = findEmails(reqBody);
|
|
3334
|
+
if (reqEmails.length === 0) return null;
|
|
3335
|
+
const resEmails = findEmails(target);
|
|
3336
|
+
const echoed = reqEmails.filter((e) => resEmails.includes(e));
|
|
3337
|
+
if (echoed.length === 0) return null;
|
|
3338
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
3339
|
+
if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
|
|
3340
|
+
return { reason: "echo", emailCount: echoed.length };
|
|
3038
3341
|
}
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
function detectFullRecordPII(target) {
|
|
3345
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
3346
|
+
const fields = topLevelFieldCount(target);
|
|
3347
|
+
if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
|
|
3348
|
+
const emails = findEmails(target);
|
|
3349
|
+
if (emails.length === 0) return null;
|
|
3350
|
+
return { reason: "full-record", emailCount: emails.length };
|
|
3351
|
+
}
|
|
3352
|
+
function detectListPII(target) {
|
|
3353
|
+
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
3354
|
+
let itemsWithEmail = 0;
|
|
3355
|
+
for (let i = 0; i < Math.min(target.length, 10); i++) {
|
|
3356
|
+
const item = target[i];
|
|
3357
|
+
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
3358
|
+
itemsWithEmail++;
|
|
3046
3359
|
}
|
|
3047
3360
|
}
|
|
3048
|
-
if (
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
if (item && typeof item === "object") {
|
|
3053
|
-
const emails = findEmails(item);
|
|
3054
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
3058
|
-
const first = target[0];
|
|
3059
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
3060
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3361
|
+
if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
|
|
3362
|
+
const first = target[0];
|
|
3363
|
+
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
3364
|
+
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
3063
3365
|
}
|
|
3064
3366
|
return null;
|
|
3065
3367
|
}
|
|
3368
|
+
function detectPII(method, reqBody, resBody) {
|
|
3369
|
+
const target = unwrapResponse(resBody);
|
|
3370
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
3371
|
+
}
|
|
3066
3372
|
var WRITE_METHODS, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, REASON_LABELS, responsePiiLeakRule;
|
|
3067
3373
|
var init_response_pii_leak = __esm({
|
|
3068
3374
|
"src/analysis/rules/response-pii-leak.ts"() {
|
|
@@ -3180,6 +3486,24 @@ var init_rules = __esm({
|
|
|
3180
3486
|
}
|
|
3181
3487
|
});
|
|
3182
3488
|
|
|
3489
|
+
// src/core/disposable.ts
|
|
3490
|
+
var SubscriptionBag;
|
|
3491
|
+
var init_disposable = __esm({
|
|
3492
|
+
"src/core/disposable.ts"() {
|
|
3493
|
+
"use strict";
|
|
3494
|
+
SubscriptionBag = class {
|
|
3495
|
+
items = [];
|
|
3496
|
+
add(teardown) {
|
|
3497
|
+
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
3498
|
+
}
|
|
3499
|
+
dispose() {
|
|
3500
|
+
for (const d of this.items) d.dispose();
|
|
3501
|
+
this.items.length = 0;
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3183
3507
|
// src/utils/collections.ts
|
|
3184
3508
|
function groupBy(items, keyFn) {
|
|
3185
3509
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -4021,14 +4345,19 @@ function computeInsightKey(insight) {
|
|
|
4021
4345
|
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
4022
4346
|
return `${insight.type}:${identifier}`;
|
|
4023
4347
|
}
|
|
4348
|
+
function enrichedIdFromInsight(insight) {
|
|
4349
|
+
return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
|
|
4350
|
+
}
|
|
4024
4351
|
var InsightTracker;
|
|
4025
4352
|
var init_insight_tracker = __esm({
|
|
4026
4353
|
"src/analysis/insight-tracker.ts"() {
|
|
4027
4354
|
"use strict";
|
|
4028
4355
|
init_endpoint();
|
|
4356
|
+
init_finding_id();
|
|
4029
4357
|
init_thresholds();
|
|
4030
4358
|
InsightTracker = class {
|
|
4031
4359
|
tracked = /* @__PURE__ */ new Map();
|
|
4360
|
+
enrichedIndex = /* @__PURE__ */ new Map();
|
|
4032
4361
|
reconcile(current) {
|
|
4033
4362
|
const currentKeys = /* @__PURE__ */ new Set();
|
|
4034
4363
|
const now = Date.now();
|
|
@@ -4036,6 +4365,7 @@ var init_insight_tracker = __esm({
|
|
|
4036
4365
|
const key = computeInsightKey(insight);
|
|
4037
4366
|
currentKeys.add(key);
|
|
4038
4367
|
const existing = this.tracked.get(key);
|
|
4368
|
+
this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
|
|
4039
4369
|
if (existing) {
|
|
4040
4370
|
existing.insight = insight;
|
|
4041
4371
|
existing.lastSeenAt = now;
|
|
@@ -4052,28 +4382,44 @@ var init_insight_tracker = __esm({
|
|
|
4052
4382
|
firstSeenAt: now,
|
|
4053
4383
|
lastSeenAt: now,
|
|
4054
4384
|
resolvedAt: null,
|
|
4055
|
-
consecutiveAbsences: 0
|
|
4385
|
+
consecutiveAbsences: 0,
|
|
4386
|
+
aiStatus: null,
|
|
4387
|
+
aiNotes: null
|
|
4056
4388
|
});
|
|
4057
4389
|
}
|
|
4058
4390
|
}
|
|
4059
|
-
for (const [
|
|
4060
|
-
if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
|
|
4391
|
+
for (const [, stateful] of this.tracked) {
|
|
4392
|
+
if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
|
|
4061
4393
|
stateful.consecutiveAbsences++;
|
|
4062
4394
|
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
4063
4395
|
stateful.state = "resolved";
|
|
4064
4396
|
stateful.resolvedAt = now;
|
|
4065
4397
|
}
|
|
4066
4398
|
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
4067
|
-
this.tracked.delete(key);
|
|
4399
|
+
this.tracked.delete(stateful.key);
|
|
4400
|
+
this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
|
|
4068
4401
|
}
|
|
4069
4402
|
}
|
|
4070
4403
|
return [...this.tracked.values()];
|
|
4071
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
|
+
}
|
|
4072
4417
|
getAll() {
|
|
4073
4418
|
return [...this.tracked.values()];
|
|
4074
4419
|
}
|
|
4075
4420
|
clear() {
|
|
4076
4421
|
this.tracked.clear();
|
|
4422
|
+
this.enrichedIndex.clear();
|
|
4077
4423
|
}
|
|
4078
4424
|
};
|
|
4079
4425
|
}
|
|
@@ -4084,13 +4430,14 @@ var AnalysisEngine;
|
|
|
4084
4430
|
var init_engine = __esm({
|
|
4085
4431
|
"src/analysis/engine.ts"() {
|
|
4086
4432
|
"use strict";
|
|
4433
|
+
init_limits();
|
|
4087
4434
|
init_disposable();
|
|
4088
4435
|
init_group();
|
|
4089
4436
|
init_rules();
|
|
4090
4437
|
init_insights3();
|
|
4091
4438
|
init_insight_tracker();
|
|
4092
4439
|
AnalysisEngine = class {
|
|
4093
|
-
constructor(registry, debounceMs =
|
|
4440
|
+
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
4094
4441
|
this.registry = registry;
|
|
4095
4442
|
this.debounceMs = debounceMs;
|
|
4096
4443
|
this.scanner = createDefaultScanner();
|
|
@@ -4126,7 +4473,10 @@ var init_engine = __esm({
|
|
|
4126
4473
|
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
4127
4474
|
}
|
|
4128
4475
|
getStatefulInsights() {
|
|
4129
|
-
return this.
|
|
4476
|
+
return this.insightTracker.getAll();
|
|
4477
|
+
}
|
|
4478
|
+
reportInsightFix(enrichedId, status, notes) {
|
|
4479
|
+
return this.insightTracker.reportFix(enrichedId, status, notes);
|
|
4130
4480
|
}
|
|
4131
4481
|
scheduleRecompute() {
|
|
4132
4482
|
if (this.debounceTimer) return;
|
|
@@ -4184,7 +4534,7 @@ var init_src = __esm({
|
|
|
4184
4534
|
init_engine();
|
|
4185
4535
|
init_insights3();
|
|
4186
4536
|
init_insights2();
|
|
4187
|
-
VERSION = "0.8.
|
|
4537
|
+
VERSION = "0.8.5";
|
|
4188
4538
|
}
|
|
4189
4539
|
});
|
|
4190
4540
|
|
|
@@ -4873,7 +5223,7 @@ function getFlowDetail() {
|
|
|
4873
5223
|
h += '<span>' + req.durationMs + 'ms</span>';
|
|
4874
5224
|
if (req.responseSize) h += '<span>' + formatSize(req.responseSize) + '</span>';
|
|
4875
5225
|
h += '</div>';
|
|
4876
|
-
h += '<div class="request-timeline tl-hidden" data-request-id="' + req.id + '" data-request-started="' + req.startedAt + '"></div>';
|
|
5226
|
+
h += '<div class="request-timeline tl-hidden" data-request-id="' + escHtml(req.id) + '" data-request-started="' + escHtml(String(req.startedAt)) + '"></div>';
|
|
4877
5227
|
h += '<div class="detail-grid">';
|
|
4878
5228
|
h += '<div class="detail-section"><h4>Request Headers</h4><pre>' + formatHeaders(req.headers) + '</pre></div>';
|
|
4879
5229
|
h += '<div class="detail-section"><h4>Response Headers</h4><pre>' + formatHeaders(req.responseHeaders) + '</pre></div>';
|
|
@@ -6131,7 +6481,7 @@ function getOverviewRender() {
|
|
|
6131
6481
|
container.appendChild(summary);
|
|
6132
6482
|
|
|
6133
6483
|
var all = state.insights || [];
|
|
6134
|
-
var open = all.filter(function(si) { return si.state === 'open'; });
|
|
6484
|
+
var open = all.filter(function(si) { return si.state === 'open' || si.state === 'fixing'; });
|
|
6135
6485
|
var resolved = all.filter(function(si) { return si.state === 'resolved'; });
|
|
6136
6486
|
|
|
6137
6487
|
if (open.length === 0 && resolved.length === 0) {
|
|
@@ -6176,10 +6526,17 @@ function getOverviewRender() {
|
|
|
6176
6526
|
if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
|
|
6177
6527
|
expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
|
|
6178
6528
|
|
|
6529
|
+
var aiBadge = '';
|
|
6530
|
+
if (si.state === 'fixing' && si.aiStatus === 'fixed') {
|
|
6531
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
|
|
6532
|
+
} else if (si.aiStatus === 'wont_fix') {
|
|
6533
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
|
|
6534
|
+
}
|
|
6535
|
+
|
|
6179
6536
|
card.innerHTML =
|
|
6180
6537
|
'<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
6181
6538
|
'<div class="ov-card-body">' +
|
|
6182
|
-
'<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
|
|
6539
|
+
'<div class="ov-card-title">' + escHtml(insight.title) + aiBadge + '</div>' +
|
|
6183
6540
|
'<div class="ov-card-desc">' + insight.desc + '</div>' +
|
|
6184
6541
|
'<div class="ov-card-expand">' + expandHtml + '</div>' +
|
|
6185
6542
|
'</div>' +
|
|
@@ -6271,7 +6628,26 @@ function getSecurityView() {
|
|
|
6271
6628
|
container.innerHTML = '';
|
|
6272
6629
|
var SEV = ${SEVERITY_MAP};
|
|
6273
6630
|
|
|
6274
|
-
var all = state.findings || [];
|
|
6631
|
+
var all = (state.findings || []).slice();
|
|
6632
|
+
var insightsList = state.insights || [];
|
|
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
|
+
}
|
|
6275
6651
|
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
|
|
6276
6652
|
var resolved = all.filter(function(f) { return f.state === 'resolved'; });
|
|
6277
6653
|
|
|
@@ -6319,12 +6695,13 @@ function getSecurityView() {
|
|
|
6319
6695
|
var groups = {};
|
|
6320
6696
|
var groupOrder = [];
|
|
6321
6697
|
for (var gi = 0; gi < open.length; gi++) {
|
|
6322
|
-
var
|
|
6698
|
+
var sf = open[gi];
|
|
6699
|
+
var f = sf.finding;
|
|
6323
6700
|
if (!groups[f.rule]) {
|
|
6324
6701
|
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
6325
6702
|
groupOrder.push(f.rule);
|
|
6326
6703
|
}
|
|
6327
|
-
groups[f.rule].items.push(
|
|
6704
|
+
groups[f.rule].items.push(sf);
|
|
6328
6705
|
}
|
|
6329
6706
|
|
|
6330
6707
|
groupOrder.sort(function(a, b) {
|
|
@@ -6361,12 +6738,21 @@ function getSecurityView() {
|
|
|
6361
6738
|
var list = document.createElement('div');
|
|
6362
6739
|
list.className = 'sec-items';
|
|
6363
6740
|
for (var ii = 0; ii < group.items.length; ii++) {
|
|
6364
|
-
var
|
|
6741
|
+
var sf2 = group.items[ii];
|
|
6742
|
+
var item = sf2.finding;
|
|
6365
6743
|
var row = document.createElement('div');
|
|
6366
6744
|
row.className = 'sec-item';
|
|
6745
|
+
var aiBadge = '';
|
|
6746
|
+
if (sf2.state === 'fixing' && sf2.aiStatus === 'fixed') {
|
|
6747
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-fixing">AI fixed \\u2014 awaiting verification</span>';
|
|
6748
|
+
} else if (sf2.aiStatus === 'wont_fix') {
|
|
6749
|
+
aiBadge = '<span class="sec-ai-badge sec-ai-wontfix">AI: won\\u2019t fix</span>';
|
|
6750
|
+
}
|
|
6751
|
+
var aiNotes = sf2.aiNotes ? '<div class="sec-ai-notes">' + escHtml(sf2.aiNotes) + '</div>' : '';
|
|
6367
6752
|
row.innerHTML =
|
|
6368
6753
|
'<div class="sec-item-desc">' + escHtml(item.desc) + '</div>' +
|
|
6369
|
-
(item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '')
|
|
6754
|
+
(item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '') +
|
|
6755
|
+
aiBadge + aiNotes;
|
|
6370
6756
|
list.appendChild(row);
|
|
6371
6757
|
}
|
|
6372
6758
|
section.appendChild(list);
|
|
@@ -6385,12 +6771,16 @@ function getSecurityView() {
|
|
|
6385
6771
|
var resolvedItems = document.createElement('div');
|
|
6386
6772
|
resolvedItems.className = 'sec-items';
|
|
6387
6773
|
for (var ri = 0; ri < resolved.length; ri++) {
|
|
6388
|
-
var
|
|
6774
|
+
var rsf = resolved[ri];
|
|
6775
|
+
var rf = rsf.finding;
|
|
6389
6776
|
var rRow = document.createElement('div');
|
|
6390
6777
|
rRow.className = 'sec-item sec-item-resolved';
|
|
6778
|
+
var verifiedBadge = rsf.aiStatus === 'fixed' ? '<span class="sec-ai-badge sec-ai-verified">Verified fix</span>' : '';
|
|
6779
|
+
var rNotes = rsf.aiNotes ? '<div class="sec-ai-notes">' + escHtml(rsf.aiNotes) + '</div>' : '';
|
|
6391
6780
|
rRow.innerHTML =
|
|
6392
6781
|
'<span class="sec-resolved-item-icon">\\u2713</span>' +
|
|
6393
|
-
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>'
|
|
6782
|
+
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>' +
|
|
6783
|
+
verifiedBadge + rNotes;
|
|
6394
6784
|
resolvedItems.appendChild(rRow);
|
|
6395
6785
|
}
|
|
6396
6786
|
resolvedGroup.appendChild(resolvedItems);
|
|
@@ -6481,6 +6871,7 @@ function getApp() {
|
|
|
6481
6871
|
events.addEventListener('insights', function(e) {
|
|
6482
6872
|
state.insights = JSON.parse(e.data);
|
|
6483
6873
|
if (state.activeView === 'overview') renderOverview();
|
|
6874
|
+
if (state.activeView === 'security') renderSecurity();
|
|
6484
6875
|
updateStats();
|
|
6485
6876
|
});
|
|
6486
6877
|
|
|
@@ -6684,7 +7075,7 @@ var init_page = __esm({
|
|
|
6684
7075
|
import { homedir } from "os";
|
|
6685
7076
|
import { join as join2 } from "path";
|
|
6686
7077
|
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
6687
|
-
import { randomUUID as
|
|
7078
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
6688
7079
|
function readConfig() {
|
|
6689
7080
|
try {
|
|
6690
7081
|
if (!existsSync5(CONFIG_PATH)) return null;
|
|
@@ -6708,27 +7099,33 @@ function getOrCreateConfig() {
|
|
|
6708
7099
|
if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
|
|
6709
7100
|
return existing;
|
|
6710
7101
|
}
|
|
6711
|
-
const config = { telemetry: true, anonymousId:
|
|
7102
|
+
const config = { telemetry: true, anonymousId: randomUUID4() };
|
|
6712
7103
|
writeConfig(config);
|
|
6713
7104
|
return config;
|
|
6714
7105
|
}
|
|
6715
7106
|
function isTelemetryEnabled() {
|
|
7107
|
+
if (cachedEnabled !== null) return cachedEnabled;
|
|
6716
7108
|
const env = process.env.BRAKIT_TELEMETRY;
|
|
6717
|
-
if (env !== void 0)
|
|
6718
|
-
|
|
7109
|
+
if (env !== void 0) {
|
|
7110
|
+
cachedEnabled = env !== "false" && env !== "0" && env !== "off";
|
|
7111
|
+
return cachedEnabled;
|
|
7112
|
+
}
|
|
7113
|
+
cachedEnabled = readConfig()?.telemetry ?? true;
|
|
7114
|
+
return cachedEnabled;
|
|
6719
7115
|
}
|
|
6720
|
-
var CONFIG_DIR, CONFIG_PATH;
|
|
7116
|
+
var CONFIG_DIR, CONFIG_PATH, cachedEnabled;
|
|
6721
7117
|
var init_config = __esm({
|
|
6722
7118
|
"src/telemetry/config.ts"() {
|
|
6723
7119
|
"use strict";
|
|
6724
7120
|
CONFIG_DIR = join2(homedir(), ".brakit");
|
|
6725
7121
|
CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
7122
|
+
cachedEnabled = null;
|
|
6726
7123
|
}
|
|
6727
7124
|
});
|
|
6728
7125
|
|
|
6729
7126
|
// src/telemetry/index.ts
|
|
6730
7127
|
import { platform, release, arch } from "os";
|
|
6731
|
-
import {
|
|
7128
|
+
import { spawn } from "child_process";
|
|
6732
7129
|
function initSession(framework, packageManager, isCustomCommand, adapters) {
|
|
6733
7130
|
session.startTime = Date.now();
|
|
6734
7131
|
session.framework = framework;
|
|
@@ -6815,14 +7212,15 @@ function trackSession(registry) {
|
|
|
6815
7212
|
try {
|
|
6816
7213
|
const body = JSON.stringify(payload);
|
|
6817
7214
|
const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
|
|
6818
|
-
|
|
7215
|
+
const child = spawn(
|
|
6819
7216
|
process.execPath,
|
|
6820
7217
|
[
|
|
6821
7218
|
"-e",
|
|
6822
7219
|
`fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
|
|
6823
7220
|
],
|
|
6824
|
-
{
|
|
7221
|
+
{ detached: true, stdio: "ignore" }
|
|
6825
7222
|
);
|
|
7223
|
+
child.unref();
|
|
6826
7224
|
} catch {
|
|
6827
7225
|
}
|
|
6828
7226
|
}
|
|
@@ -6877,7 +7275,13 @@ function createDashboardHandler(registry) {
|
|
|
6877
7275
|
routes[DASHBOARD_API_SECURITY] = createSecurityHandler(analysisEngine);
|
|
6878
7276
|
}
|
|
6879
7277
|
if (registry.has("finding-store")) {
|
|
6880
|
-
|
|
7278
|
+
const findingStore = registry.get("finding-store");
|
|
7279
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(findingStore);
|
|
7280
|
+
routes[DASHBOARD_API_FINDINGS_REPORT] = createFindingsReportHandler(
|
|
7281
|
+
findingStore,
|
|
7282
|
+
registry.get("event-bus"),
|
|
7283
|
+
analysisEngine
|
|
7284
|
+
);
|
|
6881
7285
|
}
|
|
6882
7286
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
6883
7287
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
@@ -6885,7 +7289,7 @@ function createDashboardHandler(registry) {
|
|
|
6885
7289
|
const tab = decodeURIComponent(raw).slice(0, MAX_TAB_NAME_LENGTH);
|
|
6886
7290
|
if (VALID_TABS.has(tab) && isTelemetryEnabled()) recordTabViewed(tab);
|
|
6887
7291
|
}
|
|
6888
|
-
res.writeHead(
|
|
7292
|
+
res.writeHead(HTTP_NO_CONTENT);
|
|
6889
7293
|
res.end();
|
|
6890
7294
|
};
|
|
6891
7295
|
return (req, res, config) => {
|
|
@@ -6896,7 +7300,7 @@ function createDashboardHandler(registry) {
|
|
|
6896
7300
|
return;
|
|
6897
7301
|
}
|
|
6898
7302
|
if (isTelemetryEnabled()) recordDashboardOpened();
|
|
6899
|
-
res.writeHead(
|
|
7303
|
+
res.writeHead(HTTP_OK, {
|
|
6900
7304
|
"content-type": "text/html; charset=utf-8",
|
|
6901
7305
|
"cache-control": "no-cache",
|
|
6902
7306
|
...SECURITY_HEADERS
|
|
@@ -6904,23 +7308,17 @@ function createDashboardHandler(registry) {
|
|
|
6904
7308
|
res.end(getDashboardHtml(config));
|
|
6905
7309
|
};
|
|
6906
7310
|
}
|
|
6907
|
-
var SECURITY_HEADERS;
|
|
6908
7311
|
var init_router = __esm({
|
|
6909
7312
|
"src/dashboard/router.ts"() {
|
|
6910
7313
|
"use strict";
|
|
6911
7314
|
init_constants();
|
|
7315
|
+
init_http();
|
|
6912
7316
|
init_api();
|
|
6913
7317
|
init_insights();
|
|
6914
7318
|
init_findings();
|
|
6915
7319
|
init_sse();
|
|
6916
7320
|
init_page();
|
|
6917
7321
|
init_telemetry2();
|
|
6918
|
-
SECURITY_HEADERS = {
|
|
6919
|
-
"x-content-type-options": "nosniff",
|
|
6920
|
-
"x-frame-options": "DENY",
|
|
6921
|
-
"referrer-policy": "no-referrer",
|
|
6922
|
-
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:"
|
|
6923
|
-
};
|
|
6924
7322
|
}
|
|
6925
7323
|
});
|
|
6926
7324
|
|
|
@@ -6929,6 +7327,7 @@ var EventBus;
|
|
|
6929
7327
|
var init_event_bus = __esm({
|
|
6930
7328
|
"src/core/event-bus.ts"() {
|
|
6931
7329
|
"use strict";
|
|
7330
|
+
init_log();
|
|
6932
7331
|
EventBus = class {
|
|
6933
7332
|
listeners = /* @__PURE__ */ new Map();
|
|
6934
7333
|
emit(channel, data) {
|
|
@@ -6937,7 +7336,8 @@ var init_event_bus = __esm({
|
|
|
6937
7336
|
for (const fn of set) {
|
|
6938
7337
|
try {
|
|
6939
7338
|
fn(data);
|
|
6940
|
-
} catch {
|
|
7339
|
+
} catch (err) {
|
|
7340
|
+
brakitDebug(`EventBus listener threw on channel "${channel}": ${err}`);
|
|
6941
7341
|
}
|
|
6942
7342
|
}
|
|
6943
7343
|
}
|
|
@@ -7001,9 +7401,9 @@ var init_static_patterns = __esm({
|
|
|
7001
7401
|
});
|
|
7002
7402
|
|
|
7003
7403
|
// src/store/request-store.ts
|
|
7004
|
-
function flattenHeaders(
|
|
7404
|
+
function flattenHeaders(headers2) {
|
|
7005
7405
|
const flat = {};
|
|
7006
|
-
for (const [key, value] of Object.entries(
|
|
7406
|
+
for (const [key, value] of Object.entries(headers2)) {
|
|
7007
7407
|
if (value === void 0) continue;
|
|
7008
7408
|
flat[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
7009
7409
|
}
|
|
@@ -7059,6 +7459,15 @@ var init_request_store = __esm({
|
|
|
7059
7459
|
}
|
|
7060
7460
|
return entry;
|
|
7061
7461
|
}
|
|
7462
|
+
add(entry) {
|
|
7463
|
+
this.requests.push(entry);
|
|
7464
|
+
if (this.requests.length > this.maxEntries) {
|
|
7465
|
+
this.requests.shift();
|
|
7466
|
+
}
|
|
7467
|
+
for (const fn of this.listeners) {
|
|
7468
|
+
fn(entry);
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7062
7471
|
getAll() {
|
|
7063
7472
|
return this.requests;
|
|
7064
7473
|
}
|
|
@@ -7077,7 +7486,7 @@ var init_request_store = __esm({
|
|
|
7077
7486
|
});
|
|
7078
7487
|
|
|
7079
7488
|
// src/store/telemetry-store.ts
|
|
7080
|
-
import { randomUUID as
|
|
7489
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7081
7490
|
var TelemetryStore;
|
|
7082
7491
|
var init_telemetry_store = __esm({
|
|
7083
7492
|
"src/store/telemetry-store.ts"() {
|
|
@@ -7090,7 +7499,7 @@ var init_telemetry_store = __esm({
|
|
|
7090
7499
|
entries = [];
|
|
7091
7500
|
listeners = [];
|
|
7092
7501
|
add(data) {
|
|
7093
|
-
const entry = { id:
|
|
7502
|
+
const entry = { id: randomUUID5(), ...data };
|
|
7094
7503
|
this.entries.push(entry);
|
|
7095
7504
|
if (this.entries.length > this.maxEntries) this.entries.shift();
|
|
7096
7505
|
for (const fn of this.listeners) fn(entry);
|
|
@@ -7174,7 +7583,7 @@ var init_math = __esm({
|
|
|
7174
7583
|
});
|
|
7175
7584
|
|
|
7176
7585
|
// src/store/metrics/metrics-store.ts
|
|
7177
|
-
import { randomUUID as
|
|
7586
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
7178
7587
|
function createAccumulator() {
|
|
7179
7588
|
return {
|
|
7180
7589
|
durations: [],
|
|
@@ -7198,19 +7607,23 @@ var init_metrics_store = __esm({
|
|
|
7198
7607
|
MetricsStore = class {
|
|
7199
7608
|
constructor(persistence) {
|
|
7200
7609
|
this.persistence = persistence;
|
|
7201
|
-
this.data =
|
|
7202
|
-
for (const ep of this.data.endpoints) {
|
|
7203
|
-
this.endpointIndex.set(ep.endpoint, ep);
|
|
7204
|
-
}
|
|
7610
|
+
this.data = { version: 1, endpoints: [] };
|
|
7205
7611
|
}
|
|
7206
7612
|
data;
|
|
7207
7613
|
endpointIndex = /* @__PURE__ */ new Map();
|
|
7208
|
-
sessionId =
|
|
7614
|
+
sessionId = randomUUID6();
|
|
7209
7615
|
sessionStart = Date.now();
|
|
7210
7616
|
flushTimer = null;
|
|
7211
7617
|
accumulators = /* @__PURE__ */ new Map();
|
|
7212
7618
|
pendingPoints = /* @__PURE__ */ new Map();
|
|
7213
7619
|
start() {
|
|
7620
|
+
this.persistence.loadAsync().then((data) => {
|
|
7621
|
+
this.data = data;
|
|
7622
|
+
for (const ep of this.data.endpoints) {
|
|
7623
|
+
this.endpointIndex.set(ep.endpoint, ep);
|
|
7624
|
+
}
|
|
7625
|
+
}).catch(() => {
|
|
7626
|
+
});
|
|
7214
7627
|
this.flushTimer = setInterval(
|
|
7215
7628
|
() => this.flush(),
|
|
7216
7629
|
METRICS_FLUSH_INTERVAL_MS
|
|
@@ -7370,6 +7783,7 @@ var init_metrics_store = __esm({
|
|
|
7370
7783
|
});
|
|
7371
7784
|
|
|
7372
7785
|
// src/store/metrics/persistence.ts
|
|
7786
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
7373
7787
|
import { readFileSync as readFileSync4, existsSync as existsSync6, unlinkSync } from "fs";
|
|
7374
7788
|
import { resolve as resolve3 } from "path";
|
|
7375
7789
|
var FileMetricsPersistence;
|
|
@@ -7378,7 +7792,9 @@ var init_persistence = __esm({
|
|
|
7378
7792
|
"use strict";
|
|
7379
7793
|
init_constants();
|
|
7380
7794
|
init_atomic_writer();
|
|
7795
|
+
init_fs();
|
|
7381
7796
|
init_log();
|
|
7797
|
+
init_type_guards();
|
|
7382
7798
|
FileMetricsPersistence = class {
|
|
7383
7799
|
metricsPath;
|
|
7384
7800
|
writer;
|
|
@@ -7401,7 +7817,21 @@ var init_persistence = __esm({
|
|
|
7401
7817
|
}
|
|
7402
7818
|
}
|
|
7403
7819
|
} catch (err) {
|
|
7404
|
-
brakitWarn(`failed to load metrics: ${err
|
|
7820
|
+
brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
|
|
7821
|
+
}
|
|
7822
|
+
return { version: 1, endpoints: [] };
|
|
7823
|
+
}
|
|
7824
|
+
async loadAsync() {
|
|
7825
|
+
try {
|
|
7826
|
+
if (await fileExists(this.metricsPath)) {
|
|
7827
|
+
const raw = await readFile4(this.metricsPath, "utf-8");
|
|
7828
|
+
const parsed = JSON.parse(raw);
|
|
7829
|
+
if (parsed?.version === 1 && Array.isArray(parsed.endpoints)) {
|
|
7830
|
+
return parsed;
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
} catch (err) {
|
|
7834
|
+
brakitWarn(`failed to load metrics: ${getErrorMessage(err)}`);
|
|
7405
7835
|
}
|
|
7406
7836
|
return { version: 1, endpoints: [] };
|
|
7407
7837
|
}
|
|
@@ -7540,17 +7970,28 @@ var init_health2 = __esm({
|
|
|
7540
7970
|
BrakitHealth = class {
|
|
7541
7971
|
errorCount = 0;
|
|
7542
7972
|
disabled = false;
|
|
7973
|
+
disabledAt = 0;
|
|
7543
7974
|
teardownFn = null;
|
|
7544
7975
|
reportError() {
|
|
7545
7976
|
this.errorCount++;
|
|
7546
7977
|
if (this.errorCount >= MAX_HEALTH_ERRORS && !this.disabled) {
|
|
7547
7978
|
this.disabled = true;
|
|
7548
|
-
|
|
7979
|
+
this.disabledAt = Date.now();
|
|
7980
|
+
try {
|
|
7981
|
+
process.stderr.write("brakit: too many errors, disabling temporarily.\n");
|
|
7982
|
+
} catch {
|
|
7983
|
+
}
|
|
7549
7984
|
this.teardownFn?.();
|
|
7550
7985
|
}
|
|
7551
7986
|
}
|
|
7552
7987
|
isActive() {
|
|
7553
|
-
|
|
7988
|
+
if (!this.disabled) return true;
|
|
7989
|
+
if (Date.now() - this.disabledAt > RECOVERY_WINDOW_MS) {
|
|
7990
|
+
this.disabled = false;
|
|
7991
|
+
this.errorCount = 0;
|
|
7992
|
+
return true;
|
|
7993
|
+
}
|
|
7994
|
+
return false;
|
|
7554
7995
|
}
|
|
7555
7996
|
setTeardown(fn) {
|
|
7556
7997
|
this.teardownFn = fn;
|
|
@@ -7591,10 +8032,10 @@ var init_guard = __esm({
|
|
|
7591
8032
|
});
|
|
7592
8033
|
|
|
7593
8034
|
// src/runtime/capture.ts
|
|
7594
|
-
import {
|
|
7595
|
-
function outgoingToIncoming(
|
|
8035
|
+
import { gunzip, brotliDecompress, inflate } from "zlib";
|
|
8036
|
+
function outgoingToIncoming(headers2) {
|
|
7596
8037
|
const result = {};
|
|
7597
|
-
for (const [key, value] of Object.entries(
|
|
8038
|
+
for (const [key, value] of Object.entries(headers2)) {
|
|
7598
8039
|
if (value === void 0) continue;
|
|
7599
8040
|
if (Array.isArray(value)) {
|
|
7600
8041
|
result[key] = value.map(String);
|
|
@@ -7604,15 +8045,19 @@ function outgoingToIncoming(headers) {
|
|
|
7604
8045
|
}
|
|
7605
8046
|
return result;
|
|
7606
8047
|
}
|
|
7607
|
-
function
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
8048
|
+
function decompressAsync(body, encoding) {
|
|
8049
|
+
const decompressor = encoding === CONTENT_ENCODING_GZIP ? gunzip : encoding === CONTENT_ENCODING_BR ? brotliDecompress : encoding === CONTENT_ENCODING_DEFLATE ? inflate : null;
|
|
8050
|
+
if (!decompressor) return Promise.resolve(body);
|
|
8051
|
+
return new Promise((resolve5) => {
|
|
8052
|
+
decompressor(body, (err, result) => {
|
|
8053
|
+
if (err) {
|
|
8054
|
+
brakitDebug(`decompress failed: ${err.message}`);
|
|
8055
|
+
resolve5(body);
|
|
8056
|
+
} else {
|
|
8057
|
+
resolve5(result);
|
|
8058
|
+
}
|
|
8059
|
+
});
|
|
8060
|
+
});
|
|
7616
8061
|
}
|
|
7617
8062
|
function toBuffer(chunk) {
|
|
7618
8063
|
if (Buffer.isBuffer(chunk)) return chunk;
|
|
@@ -7656,29 +8101,35 @@ function captureInProcess(req, res, requestId, requestStore) {
|
|
|
7656
8101
|
}
|
|
7657
8102
|
const result = originalEnd.apply(this, args);
|
|
7658
8103
|
const endTime = performance.now();
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
8104
|
+
const encoding = String(res.getHeader("content-encoding") ?? "").toLowerCase();
|
|
8105
|
+
const statusCode = res.statusCode;
|
|
8106
|
+
const responseHeaders = outgoingToIncoming(res.getHeaders());
|
|
8107
|
+
const responseContentType = String(res.getHeader("content-type") ?? "");
|
|
8108
|
+
const capturedChunks = resChunks.slice();
|
|
8109
|
+
void (async () => {
|
|
8110
|
+
try {
|
|
8111
|
+
let body = capturedChunks.length > 0 ? Buffer.concat(capturedChunks) : null;
|
|
8112
|
+
if (body && encoding) {
|
|
8113
|
+
body = await decompressAsync(body, encoding);
|
|
8114
|
+
}
|
|
8115
|
+
requestStore.capture({
|
|
8116
|
+
requestId,
|
|
8117
|
+
method,
|
|
8118
|
+
url: req.url ?? "/",
|
|
8119
|
+
requestHeaders: req.headers,
|
|
8120
|
+
requestBody: null,
|
|
8121
|
+
statusCode,
|
|
8122
|
+
responseHeaders,
|
|
8123
|
+
responseBody: body,
|
|
8124
|
+
responseContentType,
|
|
8125
|
+
startTime,
|
|
8126
|
+
endTime,
|
|
8127
|
+
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
8128
|
+
});
|
|
8129
|
+
} catch (e) {
|
|
8130
|
+
brakitDebug(`capture store: ${e.message}`);
|
|
7664
8131
|
}
|
|
7665
|
-
|
|
7666
|
-
requestId,
|
|
7667
|
-
method,
|
|
7668
|
-
url: req.url ?? "/",
|
|
7669
|
-
requestHeaders: req.headers,
|
|
7670
|
-
requestBody: null,
|
|
7671
|
-
statusCode: res.statusCode,
|
|
7672
|
-
responseHeaders: outgoingToIncoming(res.getHeaders()),
|
|
7673
|
-
responseBody: body,
|
|
7674
|
-
responseContentType: String(res.getHeader("content-type") ?? ""),
|
|
7675
|
-
startTime,
|
|
7676
|
-
endTime,
|
|
7677
|
-
config: { maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE }
|
|
7678
|
-
});
|
|
7679
|
-
} catch (e) {
|
|
7680
|
-
brakitDebug(`capture store: ${e.message}`);
|
|
7681
|
-
}
|
|
8132
|
+
})();
|
|
7682
8133
|
return result;
|
|
7683
8134
|
};
|
|
7684
8135
|
}
|
|
@@ -7692,7 +8143,7 @@ var init_capture = __esm({
|
|
|
7692
8143
|
|
|
7693
8144
|
// src/runtime/interceptor.ts
|
|
7694
8145
|
import http from "http";
|
|
7695
|
-
import { randomUUID as
|
|
8146
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
7696
8147
|
function installInterceptor(deps) {
|
|
7697
8148
|
originalEmit = http.Server.prototype.emit;
|
|
7698
8149
|
const saved = originalEmit;
|
|
@@ -7718,14 +8169,14 @@ function installInterceptor(deps) {
|
|
|
7718
8169
|
}
|
|
7719
8170
|
if (isDashboardRequest(url)) {
|
|
7720
8171
|
if (!isLocalRequest(req)) {
|
|
7721
|
-
res.writeHead(
|
|
8172
|
+
res.writeHead(HTTP_NOT_FOUND);
|
|
7722
8173
|
res.end("Not Found");
|
|
7723
8174
|
return true;
|
|
7724
8175
|
}
|
|
7725
8176
|
deps.handleDashboard(req, res, deps.config);
|
|
7726
8177
|
return true;
|
|
7727
8178
|
}
|
|
7728
|
-
const requestId =
|
|
8179
|
+
const requestId = randomUUID7();
|
|
7729
8180
|
const ctx = {
|
|
7730
8181
|
requestId,
|
|
7731
8182
|
url,
|
|
@@ -7754,6 +8205,7 @@ var init_interceptor = __esm({
|
|
|
7754
8205
|
init_safe_wrap();
|
|
7755
8206
|
init_guard();
|
|
7756
8207
|
init_capture();
|
|
8208
|
+
init_http();
|
|
7757
8209
|
originalEmit = null;
|
|
7758
8210
|
}
|
|
7759
8211
|
});
|
|
@@ -7763,11 +8215,15 @@ var setup_exports = {};
|
|
|
7763
8215
|
__export(setup_exports, {
|
|
7764
8216
|
setup: () => setup
|
|
7765
8217
|
});
|
|
7766
|
-
import {
|
|
8218
|
+
import { readFile as readFile5, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
8219
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
7767
8220
|
import { resolve as resolve4 } from "path";
|
|
7768
8221
|
function setup() {
|
|
7769
|
-
if (
|
|
7770
|
-
|
|
8222
|
+
if (initPromise) return initPromise;
|
|
8223
|
+
initPromise = doSetup();
|
|
8224
|
+
return initPromise;
|
|
8225
|
+
}
|
|
8226
|
+
async function doSetup() {
|
|
7771
8227
|
const bus = new EventBus();
|
|
7772
8228
|
const registry = new ServiceRegistry();
|
|
7773
8229
|
const requestStore = new RequestStore();
|
|
@@ -7798,7 +8254,7 @@ function setup() {
|
|
|
7798
8254
|
const cwd = process.cwd();
|
|
7799
8255
|
let framework = "unknown";
|
|
7800
8256
|
try {
|
|
7801
|
-
const pkg = JSON.parse(
|
|
8257
|
+
const pkg = JSON.parse(await readFile5(resolve4(cwd, "package.json"), "utf-8"));
|
|
7802
8258
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7803
8259
|
framework = detectFrameworkFromDeps(allDeps);
|
|
7804
8260
|
} catch {
|
|
@@ -7841,29 +8297,42 @@ function setup() {
|
|
|
7841
8297
|
requestStore,
|
|
7842
8298
|
onFirstRequest(port) {
|
|
7843
8299
|
setBrakitPort(port);
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
8300
|
+
void (async () => {
|
|
8301
|
+
try {
|
|
8302
|
+
const dir = resolve4(cwd, METRICS_DIR);
|
|
8303
|
+
await mkdir2(dir, { recursive: true });
|
|
8304
|
+
const portPath = resolve4(cwd, PORT_FILE);
|
|
8305
|
+
try {
|
|
8306
|
+
const old = await readFile5(portPath, "utf-8");
|
|
8307
|
+
if (old.trim() && old.trim() !== String(port)) {
|
|
8308
|
+
brakitDebug(`Overwriting stale port file (was ${old.trim()}, now ${port})`);
|
|
8309
|
+
}
|
|
8310
|
+
} catch {
|
|
8311
|
+
}
|
|
8312
|
+
await writeFile3(portPath, String(port));
|
|
8313
|
+
} catch (err) {
|
|
8314
|
+
brakitDebug(`port file write failed: ${err.message}`);
|
|
7851
8315
|
}
|
|
7852
|
-
}
|
|
7853
|
-
writeFileSync4(portPath, String(port));
|
|
8316
|
+
})();
|
|
7854
8317
|
terminalDispose = startTerminalInsights(registry, port);
|
|
7855
8318
|
process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
7856
8319
|
`);
|
|
7857
8320
|
}
|
|
7858
8321
|
});
|
|
7859
|
-
let
|
|
7860
|
-
const
|
|
7861
|
-
if (
|
|
7862
|
-
|
|
8322
|
+
let telemetrySent = false;
|
|
8323
|
+
const sendTelemetry = () => {
|
|
8324
|
+
if (telemetrySent) return;
|
|
8325
|
+
telemetrySent = true;
|
|
7863
8326
|
recordRequestCount(requestStore.getAll().length);
|
|
7864
8327
|
recordInsightTypes(analysisEngine.getInsights().map((i) => i.type));
|
|
7865
8328
|
recordRulesTriggered(analysisEngine.getFindings().map((f) => f.rule));
|
|
7866
8329
|
trackSession(registry);
|
|
8330
|
+
};
|
|
8331
|
+
let teardownCalled = false;
|
|
8332
|
+
const runTeardown = () => {
|
|
8333
|
+
if (teardownCalled) return;
|
|
8334
|
+
teardownCalled = true;
|
|
8335
|
+
sendTelemetry();
|
|
7867
8336
|
uninstallInterceptor();
|
|
7868
8337
|
terminalDispose?.();
|
|
7869
8338
|
analysisEngine.stop();
|
|
@@ -7876,16 +8345,14 @@ function setup() {
|
|
|
7876
8345
|
}
|
|
7877
8346
|
};
|
|
7878
8347
|
health.setTeardown(runTeardown);
|
|
7879
|
-
process.
|
|
7880
|
-
|
|
7881
|
-
process.exit(SIGNAL_EXIT_SIGINT);
|
|
8348
|
+
process.on("beforeExit", () => {
|
|
8349
|
+
sendTelemetry();
|
|
7882
8350
|
});
|
|
7883
|
-
process.
|
|
8351
|
+
process.on("exit", () => {
|
|
7884
8352
|
runTeardown();
|
|
7885
|
-
process.exit(SIGNAL_EXIT_SIGTERM);
|
|
7886
8353
|
});
|
|
7887
8354
|
}
|
|
7888
|
-
var
|
|
8355
|
+
var initPromise;
|
|
7889
8356
|
var init_setup = __esm({
|
|
7890
8357
|
"src/runtime/setup.ts"() {
|
|
7891
8358
|
"use strict";
|
|
@@ -7907,13 +8374,12 @@ var init_setup = __esm({
|
|
|
7907
8374
|
init_terminal();
|
|
7908
8375
|
init_src();
|
|
7909
8376
|
init_constants();
|
|
7910
|
-
init_telemetry();
|
|
7911
8377
|
init_health2();
|
|
7912
8378
|
init_interceptor();
|
|
7913
8379
|
init_log();
|
|
7914
8380
|
init_project();
|
|
7915
8381
|
init_telemetry2();
|
|
7916
|
-
|
|
8382
|
+
initPromise = null;
|
|
7917
8383
|
}
|
|
7918
8384
|
});
|
|
7919
8385
|
|
|
@@ -7932,7 +8398,7 @@ function shouldActivate() {
|
|
|
7932
8398
|
if (shouldActivate()) {
|
|
7933
8399
|
try {
|
|
7934
8400
|
const { setup: setup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
7935
|
-
setup2();
|
|
8401
|
+
await setup2();
|
|
7936
8402
|
} catch (err) {
|
|
7937
8403
|
console.warn("brakit: failed to start \u2014", err?.message);
|
|
7938
8404
|
}
|