brakit 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/api.d.ts +124 -117
- package/dist/api.js +417 -363
- package/dist/bin/brakit.js +499 -339
- package/dist/dashboard-client.global.js +703 -0
- package/dist/dashboard.html +895 -2168
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +2934 -5028
- package/package.json +4 -2
package/dist/api.js
CHANGED
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
// src/store/
|
|
1
|
+
// src/store/issue-store.ts
|
|
2
2
|
import { readFile as readFile2 } from "fs/promises";
|
|
3
|
-
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
3
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
|
|
4
4
|
import { resolve as resolve2 } from "path";
|
|
5
5
|
|
|
6
6
|
// src/utils/fs.ts
|
|
7
7
|
import { access, readFile, writeFile } from "fs/promises";
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
-
import {
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { resolve, join } from "path";
|
|
12
|
+
|
|
13
|
+
// src/constants/limits.ts
|
|
14
|
+
var ANALYSIS_DEBOUNCE_MS = 300;
|
|
15
|
+
var ISSUE_ID_HASH_LENGTH = 16;
|
|
16
|
+
var ISSUES_DATA_VERSION = 2;
|
|
17
|
+
var SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
18
|
+
var PII_SCAN_ARRAY_LIMIT = 10;
|
|
19
|
+
var MIN_SECRET_VALUE_LENGTH = 8;
|
|
20
|
+
var FULL_RECORD_MIN_FIELDS = 8;
|
|
21
|
+
var LIST_PII_MIN_ITEMS = 2;
|
|
22
|
+
var MAX_OBJECT_SCAN_DEPTH = 5;
|
|
23
|
+
var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
24
|
+
|
|
25
|
+
// src/utils/log.ts
|
|
26
|
+
var PREFIX = "[brakit]";
|
|
27
|
+
function brakitWarn(message) {
|
|
28
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function brakitDebug(message) {
|
|
32
|
+
if (process.env.DEBUG_BRAKIT) {
|
|
33
|
+
process.stderr.write(`${PREFIX}:debug ${message}
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/utils/type-guards.ts
|
|
39
|
+
function getErrorMessage(err) {
|
|
40
|
+
if (err instanceof Error) return err.message;
|
|
41
|
+
if (typeof err === "string") return err;
|
|
42
|
+
return String(err);
|
|
43
|
+
}
|
|
44
|
+
function validateIssuesData(parsed) {
|
|
45
|
+
if (parsed != null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.version === ISSUES_DATA_VERSION && Array.isArray(parsed.issues)) {
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/utils/fs.ts
|
|
10
52
|
async function fileExists(path) {
|
|
11
53
|
try {
|
|
12
54
|
await access(path);
|
|
@@ -25,7 +67,8 @@ function ensureGitignore(dir, entry) {
|
|
|
25
67
|
} else {
|
|
26
68
|
writeFileSync(gitignorePath, entry + "\n");
|
|
27
69
|
}
|
|
28
|
-
} catch {
|
|
70
|
+
} catch (err) {
|
|
71
|
+
brakitDebug(`ensureGitignore failed: ${getErrorMessage(err)}`);
|
|
29
72
|
}
|
|
30
73
|
}
|
|
31
74
|
async function ensureGitignoreAsync(dir, entry) {
|
|
@@ -38,46 +81,14 @@ async function ensureGitignoreAsync(dir, entry) {
|
|
|
38
81
|
} else {
|
|
39
82
|
await writeFile(gitignorePath, entry + "\n");
|
|
40
83
|
}
|
|
41
|
-
} catch {
|
|
84
|
+
} catch (err) {
|
|
85
|
+
brakitDebug(`ensureGitignoreAsync failed: ${getErrorMessage(err)}`);
|
|
42
86
|
}
|
|
43
87
|
}
|
|
44
88
|
|
|
45
|
-
// src/constants/
|
|
46
|
-
var
|
|
47
|
-
var
|
|
48
|
-
var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
49
|
-
var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
50
|
-
var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
51
|
-
var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
52
|
-
var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
53
|
-
var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
54
|
-
var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
55
|
-
var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
56
|
-
var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
57
|
-
var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
58
|
-
var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
59
|
-
var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
60
|
-
var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
61
|
-
var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
62
|
-
var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
63
|
-
var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
64
|
-
var VALID_TABS_TUPLE = [
|
|
65
|
-
"overview",
|
|
66
|
-
"actions",
|
|
67
|
-
"requests",
|
|
68
|
-
"fetches",
|
|
69
|
-
"queries",
|
|
70
|
-
"errors",
|
|
71
|
-
"logs",
|
|
72
|
-
"performance",
|
|
73
|
-
"security"
|
|
74
|
-
];
|
|
75
|
-
var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
76
|
-
|
|
77
|
-
// src/constants/limits.ts
|
|
78
|
-
var ANALYSIS_DEBOUNCE_MS = 300;
|
|
79
|
-
var FINDING_ID_HASH_LENGTH = 16;
|
|
80
|
-
var FINDINGS_DATA_VERSION = 1;
|
|
89
|
+
// src/constants/metrics.ts
|
|
90
|
+
var ISSUES_FILE = "issues.json";
|
|
91
|
+
var ISSUES_FLUSH_INTERVAL_MS = 1e4;
|
|
81
92
|
|
|
82
93
|
// src/constants/thresholds.ts
|
|
83
94
|
var FLOW_GAP_MS = 5e3;
|
|
@@ -106,17 +117,9 @@ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
|
|
|
106
117
|
var OVERFETCH_MANY_FIELDS = 12;
|
|
107
118
|
var OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
108
119
|
var MAX_DUPLICATE_INSIGHTS = 3;
|
|
109
|
-
var INSIGHT_WINDOW_PER_ENDPOINT =
|
|
110
|
-
var
|
|
111
|
-
var
|
|
112
|
-
|
|
113
|
-
// src/constants/metrics.ts
|
|
114
|
-
var METRICS_DIR = ".brakit";
|
|
115
|
-
var FINDINGS_FILE = ".brakit/findings.json";
|
|
116
|
-
var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
117
|
-
|
|
118
|
-
// src/constants/network.ts
|
|
119
|
-
var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
120
|
+
var INSIGHT_WINDOW_PER_ENDPOINT = 20;
|
|
121
|
+
var CLEAN_HITS_FOR_RESOLUTION = 5;
|
|
122
|
+
var STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
|
|
120
123
|
|
|
121
124
|
// src/utils/atomic-writer.ts
|
|
122
125
|
import {
|
|
@@ -126,36 +129,13 @@ import {
|
|
|
126
129
|
renameSync
|
|
127
130
|
} from "fs";
|
|
128
131
|
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
129
|
-
|
|
130
|
-
// src/utils/log.ts
|
|
131
|
-
var PREFIX = "[brakit]";
|
|
132
|
-
function brakitWarn(message) {
|
|
133
|
-
process.stderr.write(`${PREFIX} ${message}
|
|
134
|
-
`);
|
|
135
|
-
}
|
|
136
|
-
function brakitDebug(message) {
|
|
137
|
-
if (process.env.DEBUG_BRAKIT) {
|
|
138
|
-
process.stderr.write(`${PREFIX}:debug ${message}
|
|
139
|
-
`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// src/utils/type-guards.ts
|
|
144
|
-
function getErrorMessage(err) {
|
|
145
|
-
if (err instanceof Error) return err.message;
|
|
146
|
-
if (typeof err === "string") return err;
|
|
147
|
-
return String(err);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// src/utils/atomic-writer.ts
|
|
151
132
|
var AtomicWriter = class {
|
|
152
133
|
constructor(opts) {
|
|
153
134
|
this.opts = opts;
|
|
135
|
+
this.writing = false;
|
|
136
|
+
this.pendingContent = null;
|
|
154
137
|
this.tmpPath = opts.filePath + ".tmp";
|
|
155
138
|
}
|
|
156
|
-
tmpPath;
|
|
157
|
-
writing = false;
|
|
158
|
-
pendingContent = null;
|
|
159
139
|
writeSync(content) {
|
|
160
140
|
try {
|
|
161
141
|
this.ensureDir();
|
|
@@ -205,41 +185,33 @@ var AtomicWriter = class {
|
|
|
205
185
|
}
|
|
206
186
|
};
|
|
207
187
|
|
|
208
|
-
// src/
|
|
209
|
-
import { createHash } from "crypto";
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
function computeInsightId(type, endpoint, desc) {
|
|
215
|
-
const key = `${type}:${endpoint}:${desc}`;
|
|
216
|
-
return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
|
|
188
|
+
// src/utils/issue-id.ts
|
|
189
|
+
import { createHash as createHash2 } from "crypto";
|
|
190
|
+
function computeIssueId(issue) {
|
|
191
|
+
const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
|
|
192
|
+
const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
|
|
193
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
|
|
217
194
|
}
|
|
218
195
|
|
|
219
|
-
// src/store/
|
|
220
|
-
var
|
|
221
|
-
constructor(
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
this.
|
|
196
|
+
// src/store/issue-store.ts
|
|
197
|
+
var IssueStore = class {
|
|
198
|
+
constructor(dataDir) {
|
|
199
|
+
this.dataDir = dataDir;
|
|
200
|
+
this.issues = /* @__PURE__ */ new Map();
|
|
201
|
+
this.flushTimer = null;
|
|
202
|
+
this.dirty = false;
|
|
203
|
+
this.issuesPath = resolve2(dataDir, ISSUES_FILE);
|
|
225
204
|
this.writer = new AtomicWriter({
|
|
226
|
-
dir:
|
|
227
|
-
filePath: this.
|
|
228
|
-
|
|
229
|
-
label: "findings"
|
|
205
|
+
dir: dataDir,
|
|
206
|
+
filePath: this.issuesPath,
|
|
207
|
+
label: "issues"
|
|
230
208
|
});
|
|
231
209
|
}
|
|
232
|
-
findings = /* @__PURE__ */ new Map();
|
|
233
|
-
flushTimer = null;
|
|
234
|
-
dirty = false;
|
|
235
|
-
writer;
|
|
236
|
-
findingsPath;
|
|
237
210
|
start() {
|
|
238
|
-
this.loadAsync().catch(() => {
|
|
239
|
-
});
|
|
211
|
+
this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
|
|
240
212
|
this.flushTimer = setInterval(
|
|
241
213
|
() => this.flush(),
|
|
242
|
-
|
|
214
|
+
ISSUES_FLUSH_INTERVAL_MS
|
|
243
215
|
);
|
|
244
216
|
this.flushTimer.unref();
|
|
245
217
|
}
|
|
@@ -250,119 +222,148 @@ var FindingStore = class {
|
|
|
250
222
|
}
|
|
251
223
|
this.flushSync();
|
|
252
224
|
}
|
|
253
|
-
upsert(
|
|
254
|
-
const id =
|
|
255
|
-
const existing = this.
|
|
225
|
+
upsert(issue, source) {
|
|
226
|
+
const id = computeIssueId(issue);
|
|
227
|
+
const existing = this.issues.get(id);
|
|
256
228
|
const now = Date.now();
|
|
257
229
|
if (existing) {
|
|
258
230
|
existing.lastSeenAt = now;
|
|
259
231
|
existing.occurrences++;
|
|
260
|
-
existing.
|
|
261
|
-
|
|
262
|
-
|
|
232
|
+
existing.issue = issue;
|
|
233
|
+
existing.cleanHitsSinceLastSeen = 0;
|
|
234
|
+
if (existing.state === "resolved" || existing.state === "stale") {
|
|
235
|
+
existing.state = "regressed";
|
|
263
236
|
existing.resolvedAt = null;
|
|
264
237
|
}
|
|
265
238
|
this.dirty = true;
|
|
266
239
|
return existing;
|
|
267
240
|
}
|
|
268
241
|
const stateful = {
|
|
269
|
-
|
|
242
|
+
issueId: id,
|
|
270
243
|
state: "open",
|
|
271
244
|
source,
|
|
272
|
-
|
|
245
|
+
category: issue.category,
|
|
246
|
+
issue,
|
|
273
247
|
firstSeenAt: now,
|
|
274
248
|
lastSeenAt: now,
|
|
275
249
|
resolvedAt: null,
|
|
276
250
|
occurrences: 1,
|
|
251
|
+
cleanHitsSinceLastSeen: 0,
|
|
277
252
|
aiStatus: null,
|
|
278
253
|
aiNotes: null
|
|
279
254
|
};
|
|
280
|
-
this.
|
|
255
|
+
this.issues.set(id, stateful);
|
|
281
256
|
this.dirty = true;
|
|
282
257
|
return stateful;
|
|
283
258
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Reconcile issues against the current analysis results using evidence-based resolution.
|
|
261
|
+
*
|
|
262
|
+
* @param currentIssueIds - IDs of issues detected in the current analysis cycle
|
|
263
|
+
* @param activeEndpoints - Endpoints that had requests in the current cycle
|
|
264
|
+
*/
|
|
265
|
+
reconcile(currentIssueIds, activeEndpoints) {
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
for (const [, stateful] of this.issues) {
|
|
268
|
+
const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
|
|
269
|
+
if (!isActive) continue;
|
|
270
|
+
if (currentIssueIds.has(stateful.issueId)) continue;
|
|
271
|
+
const endpoint = stateful.issue.endpoint;
|
|
272
|
+
if (endpoint && activeEndpoints.has(endpoint)) {
|
|
273
|
+
stateful.cleanHitsSinceLastSeen++;
|
|
274
|
+
if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
|
|
275
|
+
stateful.state = "resolved";
|
|
276
|
+
stateful.resolvedAt = now;
|
|
277
|
+
}
|
|
278
|
+
this.dirty = true;
|
|
279
|
+
} else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
|
|
280
|
+
stateful.state = "stale";
|
|
281
|
+
this.dirty = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const [id, stateful] of this.issues) {
|
|
285
|
+
if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
|
|
286
|
+
this.issues.delete(id);
|
|
287
|
+
this.dirty = true;
|
|
288
|
+
} else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
|
|
289
|
+
this.issues.delete(id);
|
|
290
|
+
this.dirty = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
transition(issueId, state) {
|
|
295
|
+
const issue = this.issues.get(issueId);
|
|
296
|
+
if (!issue) return false;
|
|
297
|
+
issue.state = state;
|
|
288
298
|
if (state === "resolved") {
|
|
289
|
-
|
|
299
|
+
issue.resolvedAt = Date.now();
|
|
290
300
|
}
|
|
291
301
|
this.dirty = true;
|
|
292
302
|
return true;
|
|
293
303
|
}
|
|
294
|
-
reportFix(
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
reportFix(issueId, status, notes) {
|
|
305
|
+
const issue = this.issues.get(issueId);
|
|
306
|
+
if (!issue) return false;
|
|
307
|
+
issue.aiStatus = status;
|
|
308
|
+
issue.aiNotes = notes;
|
|
299
309
|
if (status === "fixed") {
|
|
300
|
-
|
|
310
|
+
issue.state = "fixing";
|
|
301
311
|
}
|
|
302
312
|
this.dirty = true;
|
|
303
313
|
return true;
|
|
304
314
|
}
|
|
305
|
-
/**
|
|
306
|
-
* Reconcile passive findings against the current analysis results.
|
|
307
|
-
*
|
|
308
|
-
* Passive findings are detected by continuous scanning (not user-triggered).
|
|
309
|
-
* When a previously-seen finding is absent from the current results, it means
|
|
310
|
-
* the issue has been fixed — transition it to "resolved" automatically.
|
|
311
|
-
* Active findings (from MCP verify-fix) are not auto-resolved because they
|
|
312
|
-
* require explicit verification.
|
|
313
|
-
*/
|
|
314
|
-
reconcilePassive(currentFindings) {
|
|
315
|
-
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
316
|
-
for (const [id, stateful] of this.findings) {
|
|
317
|
-
if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
|
|
318
|
-
stateful.state = "resolved";
|
|
319
|
-
stateful.resolvedAt = Date.now();
|
|
320
|
-
this.dirty = true;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
315
|
getAll() {
|
|
325
|
-
return [...this.
|
|
316
|
+
return [...this.issues.values()];
|
|
326
317
|
}
|
|
327
318
|
getByState(state) {
|
|
328
|
-
return [...this.
|
|
319
|
+
return [...this.issues.values()].filter((i) => i.state === state);
|
|
320
|
+
}
|
|
321
|
+
getByCategory(category) {
|
|
322
|
+
return [...this.issues.values()].filter((i) => i.category === category);
|
|
329
323
|
}
|
|
330
|
-
get(
|
|
331
|
-
return this.
|
|
324
|
+
get(issueId) {
|
|
325
|
+
return this.issues.get(issueId);
|
|
332
326
|
}
|
|
333
327
|
clear() {
|
|
334
|
-
this.
|
|
335
|
-
this.dirty =
|
|
328
|
+
this.issues.clear();
|
|
329
|
+
this.dirty = false;
|
|
330
|
+
try {
|
|
331
|
+
if (existsSync3(this.issuesPath)) {
|
|
332
|
+
unlinkSync(this.issuesPath);
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
isDirty() {
|
|
338
|
+
return this.dirty;
|
|
336
339
|
}
|
|
337
340
|
async loadAsync() {
|
|
338
341
|
try {
|
|
339
|
-
if (await fileExists(this.
|
|
340
|
-
const raw = await readFile2(this.
|
|
341
|
-
|
|
342
|
-
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
343
|
-
for (const f of parsed.findings) {
|
|
344
|
-
this.findings.set(f.findingId, f);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
342
|
+
if (await fileExists(this.issuesPath)) {
|
|
343
|
+
const raw = await readFile2(this.issuesPath, "utf-8");
|
|
344
|
+
this.hydrate(raw);
|
|
347
345
|
}
|
|
348
346
|
} catch (err) {
|
|
349
|
-
brakitDebug(`
|
|
347
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
350
348
|
}
|
|
351
349
|
}
|
|
352
350
|
/** Sync load for tests only — not used in production paths. */
|
|
353
351
|
loadSync() {
|
|
354
352
|
try {
|
|
355
|
-
if (existsSync3(this.
|
|
356
|
-
const raw = readFileSync2(this.
|
|
357
|
-
|
|
358
|
-
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
359
|
-
for (const f of parsed.findings) {
|
|
360
|
-
this.findings.set(f.findingId, f);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
353
|
+
if (existsSync3(this.issuesPath)) {
|
|
354
|
+
const raw = readFileSync2(this.issuesPath, "utf-8");
|
|
355
|
+
this.hydrate(raw);
|
|
363
356
|
}
|
|
364
357
|
} catch (err) {
|
|
365
|
-
brakitDebug(`
|
|
358
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/** Parse and populate issues from a raw JSON string. */
|
|
362
|
+
hydrate(raw) {
|
|
363
|
+
const validated = validateIssuesData(JSON.parse(raw));
|
|
364
|
+
if (!validated) return;
|
|
365
|
+
for (const issue of validated.issues) {
|
|
366
|
+
this.issues.set(issue.issueId, issue);
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
flush() {
|
|
@@ -377,8 +378,8 @@ var FindingStore = class {
|
|
|
377
378
|
}
|
|
378
379
|
serialize() {
|
|
379
380
|
const data = {
|
|
380
|
-
version:
|
|
381
|
-
|
|
381
|
+
version: ISSUES_DATA_VERSION,
|
|
382
|
+
issues: [...this.issues.values()]
|
|
382
383
|
};
|
|
383
384
|
return JSON.stringify(data);
|
|
384
385
|
}
|
|
@@ -387,7 +388,7 @@ var FindingStore = class {
|
|
|
387
388
|
// src/detect/project.ts
|
|
388
389
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
389
390
|
import { existsSync as existsSync4 } from "fs";
|
|
390
|
-
import { join, relative } from "path";
|
|
391
|
+
import { join as join2, relative } from "path";
|
|
391
392
|
var FRAMEWORKS = [
|
|
392
393
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
393
394
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -396,24 +397,24 @@ var FRAMEWORKS = [
|
|
|
396
397
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
397
398
|
];
|
|
398
399
|
async function detectProject(rootDir) {
|
|
399
|
-
const pkgPath =
|
|
400
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
400
401
|
const raw = await readFile3(pkgPath, "utf-8");
|
|
401
402
|
const pkg = JSON.parse(raw);
|
|
402
403
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
403
404
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
404
405
|
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
405
406
|
const devCommand = matched?.devCmd ?? "";
|
|
406
|
-
const devBin = matched ?
|
|
407
|
+
const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
407
408
|
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
408
409
|
const packageManager = await detectPackageManager(rootDir);
|
|
409
410
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
410
411
|
}
|
|
411
412
|
async function detectPackageManager(rootDir) {
|
|
412
|
-
if (await fileExists(
|
|
413
|
-
if (await fileExists(
|
|
414
|
-
if (await fileExists(
|
|
415
|
-
if (await fileExists(
|
|
416
|
-
if (await fileExists(
|
|
413
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
414
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
415
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
416
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
417
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
417
418
|
return "unknown";
|
|
418
419
|
}
|
|
419
420
|
function detectFrameworkFromDeps(allDeps) {
|
|
@@ -425,8 +426,10 @@ function detectFrameworkFromDeps(allDeps) {
|
|
|
425
426
|
|
|
426
427
|
// src/instrument/adapter-registry.ts
|
|
427
428
|
var AdapterRegistry = class {
|
|
428
|
-
|
|
429
|
-
|
|
429
|
+
constructor() {
|
|
430
|
+
this.adapters = [];
|
|
431
|
+
this.active = [];
|
|
432
|
+
}
|
|
430
433
|
register(adapter) {
|
|
431
434
|
this.adapters.push(adapter);
|
|
432
435
|
}
|
|
@@ -455,6 +458,38 @@ var AdapterRegistry = class {
|
|
|
455
458
|
}
|
|
456
459
|
};
|
|
457
460
|
|
|
461
|
+
// src/utils/response.ts
|
|
462
|
+
function tryParseJson(body) {
|
|
463
|
+
if (!body) return null;
|
|
464
|
+
try {
|
|
465
|
+
return JSON.parse(body);
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function unwrapResponse(parsed) {
|
|
471
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
472
|
+
const obj = parsed;
|
|
473
|
+
const keys = Object.keys(obj);
|
|
474
|
+
if (keys.length > 3) return parsed;
|
|
475
|
+
let best = null;
|
|
476
|
+
let bestSize = 0;
|
|
477
|
+
for (const key of keys) {
|
|
478
|
+
const val = obj[key];
|
|
479
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
480
|
+
best = val;
|
|
481
|
+
bestSize = val.length;
|
|
482
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
483
|
+
const size = Object.keys(val).length;
|
|
484
|
+
if (size > bestSize) {
|
|
485
|
+
best = val;
|
|
486
|
+
bestSize = size;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
491
|
+
}
|
|
492
|
+
|
|
458
493
|
// src/analysis/rules/patterns.ts
|
|
459
494
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
460
495
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
@@ -468,6 +503,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
|
468
503
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
469
504
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
470
505
|
var INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
506
|
+
var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
|
|
507
|
+
var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
|
|
471
508
|
var SELECT_STAR_RE = /^SELECT\s+\*/i;
|
|
472
509
|
var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
|
|
473
510
|
var RULE_HINTS = {
|
|
@@ -481,31 +518,35 @@ var RULE_HINTS = {
|
|
|
481
518
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
482
519
|
};
|
|
483
520
|
|
|
484
|
-
// src/
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
521
|
+
// src/utils/http-status.ts
|
|
522
|
+
function isErrorStatus(code) {
|
|
523
|
+
return code >= 400;
|
|
524
|
+
}
|
|
525
|
+
function isServerError(code) {
|
|
526
|
+
return code >= 500;
|
|
527
|
+
}
|
|
528
|
+
function isRedirect(code) {
|
|
529
|
+
return code >= 300 && code < 400;
|
|
492
530
|
}
|
|
493
|
-
|
|
531
|
+
|
|
532
|
+
// src/analysis/rules/exposed-secret.ts
|
|
533
|
+
function findSecretKeys(obj, prefix, depth = 0) {
|
|
494
534
|
const found = [];
|
|
535
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
|
|
495
536
|
if (!obj || typeof obj !== "object") return found;
|
|
496
537
|
if (Array.isArray(obj)) {
|
|
497
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
498
|
-
found.push(...findSecretKeys(obj[i], prefix));
|
|
538
|
+
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
539
|
+
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
499
540
|
}
|
|
500
541
|
return found;
|
|
501
542
|
}
|
|
502
543
|
for (const k of Object.keys(obj)) {
|
|
503
544
|
const val = obj[k];
|
|
504
|
-
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >=
|
|
545
|
+
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
|
|
505
546
|
found.push(k);
|
|
506
547
|
}
|
|
507
548
|
if (typeof val === "object" && val !== null) {
|
|
508
|
-
found.push(...findSecretKeys(val, prefix + k + "."));
|
|
549
|
+
found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
|
|
509
550
|
}
|
|
510
551
|
}
|
|
511
552
|
return found;
|
|
@@ -519,8 +560,8 @@ var exposedSecretRule = {
|
|
|
519
560
|
const findings = [];
|
|
520
561
|
const seen = /* @__PURE__ */ new Map();
|
|
521
562
|
for (const r of ctx.requests) {
|
|
522
|
-
if (r.statusCode
|
|
523
|
-
const parsed =
|
|
563
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
564
|
+
const parsed = ctx.parsedBodies.response.get(r.id);
|
|
524
565
|
if (!parsed) continue;
|
|
525
566
|
const keys = findSecretKeys(parsed, "");
|
|
526
567
|
if (keys.length === 0) continue;
|
|
@@ -673,7 +714,7 @@ var errorInfoLeakRule = {
|
|
|
673
714
|
|
|
674
715
|
// src/analysis/rules/insecure-cookie.ts
|
|
675
716
|
function isFrameworkResponse(r) {
|
|
676
|
-
if (r.statusCode
|
|
717
|
+
if (isRedirect(r.statusCode)) return true;
|
|
677
718
|
if (r.path?.startsWith("/__")) return true;
|
|
678
719
|
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
679
720
|
return false;
|
|
@@ -779,48 +820,15 @@ var corsCredentialsRule = {
|
|
|
779
820
|
}
|
|
780
821
|
};
|
|
781
822
|
|
|
782
|
-
// src/utils/response.ts
|
|
783
|
-
function unwrapResponse(parsed) {
|
|
784
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
785
|
-
const obj = parsed;
|
|
786
|
-
const keys = Object.keys(obj);
|
|
787
|
-
if (keys.length > 3) return parsed;
|
|
788
|
-
let best = null;
|
|
789
|
-
let bestSize = 0;
|
|
790
|
-
for (const key of keys) {
|
|
791
|
-
const val = obj[key];
|
|
792
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
793
|
-
best = val;
|
|
794
|
-
bestSize = val.length;
|
|
795
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
796
|
-
const size = Object.keys(val).length;
|
|
797
|
-
if (size > bestSize) {
|
|
798
|
-
best = val;
|
|
799
|
-
bestSize = size;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
823
|
// src/analysis/rules/response-pii-leak.ts
|
|
807
824
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
808
|
-
|
|
809
|
-
var LIST_PII_MIN_ITEMS = 2;
|
|
810
|
-
function tryParseJson2(body) {
|
|
811
|
-
if (!body) return null;
|
|
812
|
-
try {
|
|
813
|
-
return JSON.parse(body);
|
|
814
|
-
} catch {
|
|
815
|
-
return null;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
function findEmails(obj) {
|
|
825
|
+
function findEmails(obj, depth = 0) {
|
|
819
826
|
const emails = [];
|
|
827
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
|
|
820
828
|
if (!obj || typeof obj !== "object") return emails;
|
|
821
829
|
if (Array.isArray(obj)) {
|
|
822
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
823
|
-
emails.push(...findEmails(obj[i]));
|
|
830
|
+
for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
831
|
+
emails.push(...findEmails(obj[i], depth + 1));
|
|
824
832
|
}
|
|
825
833
|
return emails;
|
|
826
834
|
}
|
|
@@ -828,7 +836,7 @@ function findEmails(obj) {
|
|
|
828
836
|
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
829
837
|
emails.push(v);
|
|
830
838
|
} else if (typeof v === "object" && v !== null) {
|
|
831
|
-
emails.push(...findEmails(v));
|
|
839
|
+
emails.push(...findEmails(v, depth + 1));
|
|
832
840
|
}
|
|
833
841
|
}
|
|
834
842
|
return emails;
|
|
@@ -847,6 +855,15 @@ function hasInternalIds(obj) {
|
|
|
847
855
|
}
|
|
848
856
|
return false;
|
|
849
857
|
}
|
|
858
|
+
function hasSensitiveFieldNames(obj, depth = 0) {
|
|
859
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
|
|
860
|
+
if (!obj || typeof obj !== "object") return false;
|
|
861
|
+
if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
|
|
862
|
+
for (const key of Object.keys(obj)) {
|
|
863
|
+
if (SENSITIVE_FIELD_NAMES.test(key)) return true;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
850
867
|
function detectEchoPII(method, reqBody, target) {
|
|
851
868
|
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
852
869
|
const reqEmails = findEmails(reqBody);
|
|
@@ -868,10 +885,17 @@ function detectFullRecordPII(target) {
|
|
|
868
885
|
if (emails.length === 0) return null;
|
|
869
886
|
return { reason: "full-record", emailCount: emails.length };
|
|
870
887
|
}
|
|
888
|
+
function detectSensitiveFieldPII(target) {
|
|
889
|
+
const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
890
|
+
if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
|
|
891
|
+
if (!hasSensitiveFieldNames(inspect)) return null;
|
|
892
|
+
if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
|
|
893
|
+
return { reason: "sensitive-fields", emailCount: 0 };
|
|
894
|
+
}
|
|
871
895
|
function detectListPII(target) {
|
|
872
896
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
873
897
|
let itemsWithEmail = 0;
|
|
874
|
-
for (let i = 0; i < Math.min(target.length,
|
|
898
|
+
for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
875
899
|
const item = target[i];
|
|
876
900
|
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
877
901
|
itemsWithEmail++;
|
|
@@ -886,12 +910,13 @@ function detectListPII(target) {
|
|
|
886
910
|
}
|
|
887
911
|
function detectPII(method, reqBody, resBody) {
|
|
888
912
|
const target = unwrapResponse(resBody);
|
|
889
|
-
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
913
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
890
914
|
}
|
|
891
915
|
var REASON_LABELS = {
|
|
892
916
|
echo: "echoes back PII from the request body",
|
|
893
917
|
"full-record": "returns a full record with email and internal IDs",
|
|
894
|
-
"list-pii": "returns a list of records containing email addresses"
|
|
918
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
919
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
895
920
|
};
|
|
896
921
|
var responsePiiLeakRule = {
|
|
897
922
|
id: "response-pii-leak",
|
|
@@ -902,15 +927,15 @@ var responsePiiLeakRule = {
|
|
|
902
927
|
const findings = [];
|
|
903
928
|
const seen = /* @__PURE__ */ new Map();
|
|
904
929
|
for (const r of ctx.requests) {
|
|
905
|
-
if (r.statusCode
|
|
906
|
-
|
|
930
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
931
|
+
if (SELF_SERVICE_PATH.test(r.path)) continue;
|
|
932
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
907
933
|
if (!resJson) continue;
|
|
908
|
-
const reqJson =
|
|
934
|
+
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
909
935
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
910
936
|
if (!detection) continue;
|
|
911
937
|
const ep = `${r.method} ${r.path}`;
|
|
912
|
-
const
|
|
913
|
-
const existing = seen.get(dedupKey);
|
|
938
|
+
const existing = seen.get(ep);
|
|
914
939
|
if (existing) {
|
|
915
940
|
existing.count++;
|
|
916
941
|
continue;
|
|
@@ -919,12 +944,12 @@ var responsePiiLeakRule = {
|
|
|
919
944
|
severity: "warning",
|
|
920
945
|
rule: "response-pii-leak",
|
|
921
946
|
title: "PII Leak in Response",
|
|
922
|
-
desc: `${ep} \u2014
|
|
923
|
-
hint: this.hint
|
|
947
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
948
|
+
hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
|
|
924
949
|
endpoint: ep,
|
|
925
950
|
count: 1
|
|
926
951
|
};
|
|
927
|
-
seen.set(
|
|
952
|
+
seen.set(ep, finding);
|
|
928
953
|
findings.push(finding);
|
|
929
954
|
}
|
|
930
955
|
return findings;
|
|
@@ -932,12 +957,33 @@ var responsePiiLeakRule = {
|
|
|
932
957
|
};
|
|
933
958
|
|
|
934
959
|
// src/analysis/rules/scanner.ts
|
|
960
|
+
function buildBodyCache(requests) {
|
|
961
|
+
const response = /* @__PURE__ */ new Map();
|
|
962
|
+
const request = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const r of requests) {
|
|
964
|
+
if (r.responseBody) {
|
|
965
|
+
const parsed = tryParseJson(r.responseBody);
|
|
966
|
+
if (parsed != null) response.set(r.id, parsed);
|
|
967
|
+
}
|
|
968
|
+
if (r.requestBody) {
|
|
969
|
+
const parsed = tryParseJson(r.requestBody);
|
|
970
|
+
if (parsed != null) request.set(r.id, parsed);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return { response, request };
|
|
974
|
+
}
|
|
935
975
|
var SecurityScanner = class {
|
|
936
|
-
|
|
976
|
+
constructor() {
|
|
977
|
+
this.rules = [];
|
|
978
|
+
}
|
|
937
979
|
register(rule) {
|
|
938
980
|
this.rules.push(rule);
|
|
939
981
|
}
|
|
940
|
-
scan(
|
|
982
|
+
scan(input) {
|
|
983
|
+
const ctx = {
|
|
984
|
+
...input,
|
|
985
|
+
parsedBodies: buildBodyCache(input.requests)
|
|
986
|
+
};
|
|
941
987
|
const findings = [];
|
|
942
988
|
for (const rule of this.rules) {
|
|
943
989
|
try {
|
|
@@ -966,7 +1012,9 @@ function createDefaultScanner() {
|
|
|
966
1012
|
|
|
967
1013
|
// src/core/disposable.ts
|
|
968
1014
|
var SubscriptionBag = class {
|
|
969
|
-
|
|
1015
|
+
constructor() {
|
|
1016
|
+
this.items = [];
|
|
1017
|
+
}
|
|
970
1018
|
add(teardown) {
|
|
971
1019
|
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
972
1020
|
}
|
|
@@ -979,6 +1027,41 @@ var SubscriptionBag = class {
|
|
|
979
1027
|
// src/analysis/group.ts
|
|
980
1028
|
import { randomUUID } from "crypto";
|
|
981
1029
|
|
|
1030
|
+
// src/constants/routes.ts
|
|
1031
|
+
var DASHBOARD_PREFIX = "/__brakit";
|
|
1032
|
+
var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
1033
|
+
var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
1034
|
+
var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
1035
|
+
var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
1036
|
+
var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
1037
|
+
var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
1038
|
+
var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
1039
|
+
var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
1040
|
+
var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
1041
|
+
var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
1042
|
+
var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
1043
|
+
var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
1044
|
+
var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
1045
|
+
var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
1046
|
+
var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
1047
|
+
var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
1048
|
+
var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
1049
|
+
var VALID_TABS_TUPLE = [
|
|
1050
|
+
"overview",
|
|
1051
|
+
"actions",
|
|
1052
|
+
"requests",
|
|
1053
|
+
"fetches",
|
|
1054
|
+
"queries",
|
|
1055
|
+
"errors",
|
|
1056
|
+
"logs",
|
|
1057
|
+
"performance",
|
|
1058
|
+
"security"
|
|
1059
|
+
];
|
|
1060
|
+
var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
1061
|
+
|
|
1062
|
+
// src/constants/network.ts
|
|
1063
|
+
var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
1064
|
+
|
|
982
1065
|
// src/analysis/categorize.ts
|
|
983
1066
|
function detectCategory(req) {
|
|
984
1067
|
const { method, url, statusCode, responseHeaders } = req;
|
|
@@ -1042,7 +1125,7 @@ function labelRequest(req) {
|
|
|
1042
1125
|
function generateHumanLabel(req, category) {
|
|
1043
1126
|
const effectivePath = getEffectivePath(req);
|
|
1044
1127
|
const endpointName = getEndpointName(effectivePath);
|
|
1045
|
-
const failed = req.statusCode
|
|
1128
|
+
const failed = isErrorStatus(req.statusCode);
|
|
1046
1129
|
switch (category) {
|
|
1047
1130
|
case "auth-handshake":
|
|
1048
1131
|
return "Auth handshake";
|
|
@@ -1222,7 +1305,7 @@ function detectWarnings(requests) {
|
|
|
1222
1305
|
for (const req of slowRequests) {
|
|
1223
1306
|
warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
|
|
1224
1307
|
}
|
|
1225
|
-
const errors = requests.filter((r) => r.statusCode
|
|
1308
|
+
const errors = requests.filter((r) => isServerError(r.statusCode));
|
|
1226
1309
|
for (const req of errors) {
|
|
1227
1310
|
warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
|
|
1228
1311
|
}
|
|
@@ -1278,7 +1361,7 @@ function buildFlow(rawRequests) {
|
|
|
1278
1361
|
requests,
|
|
1279
1362
|
startTime,
|
|
1280
1363
|
totalDurationMs: Math.round(endTime - startTime),
|
|
1281
|
-
hasErrors: requests.some((r) => r.statusCode
|
|
1364
|
+
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
|
|
1282
1365
|
warnings: detectWarnings(rawRequests),
|
|
1283
1366
|
sourcePage,
|
|
1284
1367
|
redundancyPct
|
|
@@ -1346,8 +1429,14 @@ function groupBy(items, keyFn) {
|
|
|
1346
1429
|
}
|
|
1347
1430
|
|
|
1348
1431
|
// src/utils/endpoint.ts
|
|
1432
|
+
var DYNAMIC_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\d+$|^[0-9a-f]{12,}$|^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9_-]{8,}$/i;
|
|
1433
|
+
function normalizePath(path) {
|
|
1434
|
+
const qIdx = path.indexOf("?");
|
|
1435
|
+
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
1436
|
+
return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
|
|
1437
|
+
}
|
|
1349
1438
|
function getEndpointKey(method, path) {
|
|
1350
|
-
return `${method} ${path}`;
|
|
1439
|
+
return `${method} ${normalizePath(path)}`;
|
|
1351
1440
|
}
|
|
1352
1441
|
var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
1353
1442
|
function extractEndpointFromDesc(desc) {
|
|
@@ -1429,6 +1518,15 @@ function windowByEndpoint(requests) {
|
|
|
1429
1518
|
}
|
|
1430
1519
|
return windowed;
|
|
1431
1520
|
}
|
|
1521
|
+
function extractActiveEndpoints(requests) {
|
|
1522
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
1523
|
+
for (const r of requests) {
|
|
1524
|
+
if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
|
|
1525
|
+
endpoints.add(getEndpointKey(r.method, r.path));
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return endpoints;
|
|
1529
|
+
}
|
|
1432
1530
|
function prepareContext(ctx) {
|
|
1433
1531
|
const nonStatic = ctx.requests.filter(
|
|
1434
1532
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -1446,7 +1544,7 @@ function prepareContext(ctx) {
|
|
|
1446
1544
|
endpointGroups.set(ep, g);
|
|
1447
1545
|
}
|
|
1448
1546
|
g.total++;
|
|
1449
|
-
if (r.statusCode
|
|
1547
|
+
if (isErrorStatus(r.statusCode)) g.errors++;
|
|
1450
1548
|
g.totalDuration += r.durationMs;
|
|
1451
1549
|
g.totalSize += r.responseSize ?? 0;
|
|
1452
1550
|
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
@@ -1481,7 +1579,9 @@ function prepareContext(ctx) {
|
|
|
1481
1579
|
// src/analysis/insights/runner.ts
|
|
1482
1580
|
var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
1483
1581
|
var InsightRunner = class {
|
|
1484
|
-
|
|
1582
|
+
constructor() {
|
|
1583
|
+
this.rules = [];
|
|
1584
|
+
}
|
|
1485
1585
|
register(rule) {
|
|
1486
1586
|
this.rules.push(rule);
|
|
1487
1587
|
}
|
|
@@ -1866,7 +1966,7 @@ var responseOverfetchRule = {
|
|
|
1866
1966
|
const insights = [];
|
|
1867
1967
|
const seen = /* @__PURE__ */ new Set();
|
|
1868
1968
|
for (const r of ctx.nonStatic) {
|
|
1869
|
-
if (r.statusCode
|
|
1969
|
+
if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
|
|
1870
1970
|
const ep = getEndpointKey(r.method, r.path);
|
|
1871
1971
|
if (seen.has(ep)) continue;
|
|
1872
1972
|
let parsed;
|
|
@@ -2010,96 +2110,49 @@ function computeInsights(ctx) {
|
|
|
2010
2110
|
return createDefaultInsightRunner().run(ctx);
|
|
2011
2111
|
}
|
|
2012
2112
|
|
|
2013
|
-
// src/analysis/
|
|
2014
|
-
function
|
|
2015
|
-
|
|
2016
|
-
return
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
insight,
|
|
2045
|
-
firstSeenAt: now,
|
|
2046
|
-
lastSeenAt: now,
|
|
2047
|
-
resolvedAt: null,
|
|
2048
|
-
consecutiveAbsences: 0,
|
|
2049
|
-
aiStatus: null,
|
|
2050
|
-
aiNotes: null
|
|
2051
|
-
});
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
for (const [, stateful] of this.tracked) {
|
|
2055
|
-
if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
|
|
2056
|
-
stateful.consecutiveAbsences++;
|
|
2057
|
-
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
2058
|
-
stateful.state = "resolved";
|
|
2059
|
-
stateful.resolvedAt = now;
|
|
2060
|
-
}
|
|
2061
|
-
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
2062
|
-
this.tracked.delete(stateful.key);
|
|
2063
|
-
this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
return [...this.tracked.values()];
|
|
2067
|
-
}
|
|
2068
|
-
reportFix(enrichedId, status, notes) {
|
|
2069
|
-
const key = this.enrichedIndex.get(enrichedId);
|
|
2070
|
-
if (!key) return false;
|
|
2071
|
-
const stateful = this.tracked.get(key);
|
|
2072
|
-
if (!stateful) return false;
|
|
2073
|
-
stateful.aiStatus = status;
|
|
2074
|
-
stateful.aiNotes = notes;
|
|
2075
|
-
if (status === "fixed") {
|
|
2076
|
-
stateful.state = "fixing";
|
|
2077
|
-
}
|
|
2078
|
-
return true;
|
|
2079
|
-
}
|
|
2080
|
-
getAll() {
|
|
2081
|
-
return [...this.tracked.values()];
|
|
2082
|
-
}
|
|
2083
|
-
clear() {
|
|
2084
|
-
this.tracked.clear();
|
|
2085
|
-
this.enrichedIndex.clear();
|
|
2086
|
-
}
|
|
2087
|
-
};
|
|
2113
|
+
// src/analysis/issue-mappers.ts
|
|
2114
|
+
function categorizeInsight(type) {
|
|
2115
|
+
if (type === "security") return "security";
|
|
2116
|
+
if (type === "error" || type === "error-hotspot") return "reliability";
|
|
2117
|
+
return "performance";
|
|
2118
|
+
}
|
|
2119
|
+
function insightToIssue(insight) {
|
|
2120
|
+
return {
|
|
2121
|
+
category: categorizeInsight(insight.type),
|
|
2122
|
+
rule: insight.type,
|
|
2123
|
+
severity: insight.severity,
|
|
2124
|
+
title: insight.title,
|
|
2125
|
+
desc: insight.desc,
|
|
2126
|
+
hint: insight.hint,
|
|
2127
|
+
detail: insight.detail,
|
|
2128
|
+
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
|
|
2129
|
+
nav: insight.nav
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function securityFindingToIssue(finding) {
|
|
2133
|
+
return {
|
|
2134
|
+
category: "security",
|
|
2135
|
+
rule: finding.rule,
|
|
2136
|
+
severity: finding.severity,
|
|
2137
|
+
title: finding.title,
|
|
2138
|
+
desc: finding.desc,
|
|
2139
|
+
hint: finding.hint,
|
|
2140
|
+
endpoint: finding.endpoint,
|
|
2141
|
+
nav: "security"
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2088
2144
|
|
|
2089
2145
|
// src/analysis/engine.ts
|
|
2090
2146
|
var AnalysisEngine = class {
|
|
2091
2147
|
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
2092
2148
|
this.registry = registry;
|
|
2093
2149
|
this.debounceMs = debounceMs;
|
|
2150
|
+
this.cachedInsights = [];
|
|
2151
|
+
this.cachedFindings = [];
|
|
2152
|
+
this.debounceTimer = null;
|
|
2153
|
+
this.subs = new SubscriptionBag();
|
|
2094
2154
|
this.scanner = createDefaultScanner();
|
|
2095
2155
|
}
|
|
2096
|
-
scanner;
|
|
2097
|
-
insightTracker = new InsightTracker();
|
|
2098
|
-
cachedInsights = [];
|
|
2099
|
-
cachedFindings = [];
|
|
2100
|
-
cachedStatefulInsights = [];
|
|
2101
|
-
debounceTimer = null;
|
|
2102
|
-
subs = new SubscriptionBag();
|
|
2103
2156
|
start() {
|
|
2104
2157
|
const bus = this.registry.get("event-bus");
|
|
2105
2158
|
this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
|
|
@@ -2120,15 +2173,6 @@ var AnalysisEngine = class {
|
|
|
2120
2173
|
getFindings() {
|
|
2121
2174
|
return this.cachedFindings;
|
|
2122
2175
|
}
|
|
2123
|
-
getStatefulFindings() {
|
|
2124
|
-
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
2125
|
-
}
|
|
2126
|
-
getStatefulInsights() {
|
|
2127
|
-
return this.insightTracker.getAll();
|
|
2128
|
-
}
|
|
2129
|
-
reportInsightFix(enrichedId, status, notes) {
|
|
2130
|
-
return this.insightTracker.reportFix(enrichedId, status, notes);
|
|
2131
|
-
}
|
|
2132
2176
|
scheduleRecompute() {
|
|
2133
2177
|
if (this.debounceTimer) return;
|
|
2134
2178
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -2137,20 +2181,14 @@ var AnalysisEngine = class {
|
|
|
2137
2181
|
}, this.debounceMs);
|
|
2138
2182
|
}
|
|
2139
2183
|
recompute() {
|
|
2140
|
-
const
|
|
2184
|
+
const allRequests = this.registry.get("request-store").getAll();
|
|
2141
2185
|
const queries = this.registry.get("query-store").getAll();
|
|
2142
2186
|
const errors = this.registry.get("error-store").getAll();
|
|
2143
2187
|
const logs = this.registry.get("log-store").getAll();
|
|
2144
2188
|
const fetches = this.registry.get("fetch-store").getAll();
|
|
2189
|
+
const requests = windowByEndpoint(allRequests);
|
|
2145
2190
|
const flows = groupRequestsIntoFlows(requests);
|
|
2146
2191
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
2147
|
-
if (this.registry.has("finding-store")) {
|
|
2148
|
-
const findingStore = this.registry.get("finding-store");
|
|
2149
|
-
for (const finding of this.cachedFindings) {
|
|
2150
|
-
findingStore.upsert(finding, "passive");
|
|
2151
|
-
}
|
|
2152
|
-
findingStore.reconcilePassive(this.cachedFindings);
|
|
2153
|
-
}
|
|
2154
2192
|
this.cachedInsights = computeInsights({
|
|
2155
2193
|
requests,
|
|
2156
2194
|
queries,
|
|
@@ -2160,24 +2198,40 @@ var AnalysisEngine = class {
|
|
|
2160
2198
|
previousMetrics: this.registry.get("metrics-store").getAll(),
|
|
2161
2199
|
securityFindings: this.cachedFindings
|
|
2162
2200
|
});
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2201
|
+
if (this.registry.has("issue-store")) {
|
|
2202
|
+
const issueStore = this.registry.get("issue-store");
|
|
2203
|
+
for (const finding of this.cachedFindings) {
|
|
2204
|
+
issueStore.upsert(securityFindingToIssue(finding), "passive");
|
|
2205
|
+
}
|
|
2206
|
+
for (const insight of this.cachedInsights) {
|
|
2207
|
+
issueStore.upsert(insightToIssue(insight), "passive");
|
|
2208
|
+
}
|
|
2209
|
+
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
2210
|
+
for (const finding of this.cachedFindings) {
|
|
2211
|
+
currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
|
|
2212
|
+
}
|
|
2213
|
+
for (const insight of this.cachedInsights) {
|
|
2214
|
+
currentIssueIds.add(computeIssueId(insightToIssue(insight)));
|
|
2215
|
+
}
|
|
2216
|
+
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
2217
|
+
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
2218
|
+
const update = {
|
|
2219
|
+
insights: this.cachedInsights,
|
|
2220
|
+
findings: this.cachedFindings,
|
|
2221
|
+
issues: issueStore.getAll()
|
|
2222
|
+
};
|
|
2223
|
+
this.registry.get("event-bus").emit("analysis:updated", update);
|
|
2224
|
+
}
|
|
2171
2225
|
}
|
|
2172
2226
|
};
|
|
2173
2227
|
|
|
2174
2228
|
// src/index.ts
|
|
2175
|
-
var VERSION = "0.8.
|
|
2229
|
+
var VERSION = "0.8.7";
|
|
2176
2230
|
export {
|
|
2177
2231
|
AdapterRegistry,
|
|
2178
2232
|
AnalysisEngine,
|
|
2179
|
-
FindingStore,
|
|
2180
2233
|
InsightRunner,
|
|
2234
|
+
IssueStore,
|
|
2181
2235
|
SecurityScanner,
|
|
2182
2236
|
VERSION,
|
|
2183
2237
|
computeInsights,
|