brakit 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/api.d.ts +120 -115
- package/dist/api.js +371 -340
- package/dist/bin/brakit.js +457 -332
- package/dist/dashboard.html +60 -59
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +637 -529
- package/package.json +1 -1
package/dist/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 = 5;
|
|
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,28 +129,6 @@ 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;
|
|
@@ -205,41 +186,35 @@ var AtomicWriter = class {
|
|
|
205
186
|
}
|
|
206
187
|
};
|
|
207
188
|
|
|
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);
|
|
189
|
+
// src/utils/issue-id.ts
|
|
190
|
+
import { createHash as createHash2 } from "crypto";
|
|
191
|
+
function computeIssueId(issue) {
|
|
192
|
+
const stableDesc = issue.desc.replace(/\d[\d,.]*\s*\w*/g, "#");
|
|
193
|
+
const key = `${issue.rule}:${issue.endpoint ?? "global"}:${stableDesc}`;
|
|
194
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, ISSUE_ID_HASH_LENGTH);
|
|
217
195
|
}
|
|
218
196
|
|
|
219
|
-
// src/store/
|
|
220
|
-
var
|
|
221
|
-
constructor(
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
|
|
197
|
+
// src/store/issue-store.ts
|
|
198
|
+
var IssueStore = class {
|
|
199
|
+
constructor(dataDir) {
|
|
200
|
+
this.dataDir = dataDir;
|
|
201
|
+
this.issuesPath = resolve2(dataDir, ISSUES_FILE);
|
|
225
202
|
this.writer = new AtomicWriter({
|
|
226
|
-
dir:
|
|
227
|
-
filePath: this.
|
|
228
|
-
|
|
229
|
-
label: "findings"
|
|
203
|
+
dir: dataDir,
|
|
204
|
+
filePath: this.issuesPath,
|
|
205
|
+
label: "issues"
|
|
230
206
|
});
|
|
231
207
|
}
|
|
232
|
-
|
|
208
|
+
issues = /* @__PURE__ */ new Map();
|
|
233
209
|
flushTimer = null;
|
|
234
210
|
dirty = false;
|
|
235
211
|
writer;
|
|
236
|
-
|
|
212
|
+
issuesPath;
|
|
237
213
|
start() {
|
|
238
|
-
this.loadAsync().catch(() => {
|
|
239
|
-
});
|
|
214
|
+
this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
|
|
240
215
|
this.flushTimer = setInterval(
|
|
241
216
|
() => this.flush(),
|
|
242
|
-
|
|
217
|
+
ISSUES_FLUSH_INTERVAL_MS
|
|
243
218
|
);
|
|
244
219
|
this.flushTimer.unref();
|
|
245
220
|
}
|
|
@@ -250,119 +225,148 @@ var FindingStore = class {
|
|
|
250
225
|
}
|
|
251
226
|
this.flushSync();
|
|
252
227
|
}
|
|
253
|
-
upsert(
|
|
254
|
-
const id =
|
|
255
|
-
const existing = this.
|
|
228
|
+
upsert(issue, source) {
|
|
229
|
+
const id = computeIssueId(issue);
|
|
230
|
+
const existing = this.issues.get(id);
|
|
256
231
|
const now = Date.now();
|
|
257
232
|
if (existing) {
|
|
258
233
|
existing.lastSeenAt = now;
|
|
259
234
|
existing.occurrences++;
|
|
260
|
-
existing.
|
|
261
|
-
|
|
262
|
-
|
|
235
|
+
existing.issue = issue;
|
|
236
|
+
existing.cleanHitsSinceLastSeen = 0;
|
|
237
|
+
if (existing.state === "resolved" || existing.state === "stale") {
|
|
238
|
+
existing.state = "regressed";
|
|
263
239
|
existing.resolvedAt = null;
|
|
264
240
|
}
|
|
265
241
|
this.dirty = true;
|
|
266
242
|
return existing;
|
|
267
243
|
}
|
|
268
244
|
const stateful = {
|
|
269
|
-
|
|
245
|
+
issueId: id,
|
|
270
246
|
state: "open",
|
|
271
247
|
source,
|
|
272
|
-
|
|
248
|
+
category: issue.category,
|
|
249
|
+
issue,
|
|
273
250
|
firstSeenAt: now,
|
|
274
251
|
lastSeenAt: now,
|
|
275
252
|
resolvedAt: null,
|
|
276
253
|
occurrences: 1,
|
|
254
|
+
cleanHitsSinceLastSeen: 0,
|
|
277
255
|
aiStatus: null,
|
|
278
256
|
aiNotes: null
|
|
279
257
|
};
|
|
280
|
-
this.
|
|
258
|
+
this.issues.set(id, stateful);
|
|
281
259
|
this.dirty = true;
|
|
282
260
|
return stateful;
|
|
283
261
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Reconcile issues against the current analysis results using evidence-based resolution.
|
|
264
|
+
*
|
|
265
|
+
* @param currentIssueIds - IDs of issues detected in the current analysis cycle
|
|
266
|
+
* @param activeEndpoints - Endpoints that had requests in the current cycle
|
|
267
|
+
*/
|
|
268
|
+
reconcile(currentIssueIds, activeEndpoints) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
for (const [, stateful] of this.issues) {
|
|
271
|
+
const isActive = stateful.state === "open" || stateful.state === "fixing" || stateful.state === "regressed";
|
|
272
|
+
if (!isActive) continue;
|
|
273
|
+
if (currentIssueIds.has(stateful.issueId)) continue;
|
|
274
|
+
const endpoint = stateful.issue.endpoint;
|
|
275
|
+
if (endpoint && activeEndpoints.has(endpoint)) {
|
|
276
|
+
stateful.cleanHitsSinceLastSeen++;
|
|
277
|
+
if (stateful.cleanHitsSinceLastSeen >= CLEAN_HITS_FOR_RESOLUTION) {
|
|
278
|
+
stateful.state = "resolved";
|
|
279
|
+
stateful.resolvedAt = now;
|
|
280
|
+
}
|
|
281
|
+
this.dirty = true;
|
|
282
|
+
} else if (now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS) {
|
|
283
|
+
stateful.state = "stale";
|
|
284
|
+
this.dirty = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const [id, stateful] of this.issues) {
|
|
288
|
+
if (stateful.state === "resolved" && stateful.resolvedAt && now - stateful.resolvedAt > ISSUE_PRUNE_TTL_MS) {
|
|
289
|
+
this.issues.delete(id);
|
|
290
|
+
this.dirty = true;
|
|
291
|
+
} else if (stateful.state === "stale" && now - stateful.lastSeenAt > STALE_ISSUE_TTL_MS + ISSUE_PRUNE_TTL_MS) {
|
|
292
|
+
this.issues.delete(id);
|
|
293
|
+
this.dirty = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
transition(issueId, state) {
|
|
298
|
+
const issue = this.issues.get(issueId);
|
|
299
|
+
if (!issue) return false;
|
|
300
|
+
issue.state = state;
|
|
288
301
|
if (state === "resolved") {
|
|
289
|
-
|
|
302
|
+
issue.resolvedAt = Date.now();
|
|
290
303
|
}
|
|
291
304
|
this.dirty = true;
|
|
292
305
|
return true;
|
|
293
306
|
}
|
|
294
|
-
reportFix(
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
297
|
-
|
|
298
|
-
|
|
307
|
+
reportFix(issueId, status, notes) {
|
|
308
|
+
const issue = this.issues.get(issueId);
|
|
309
|
+
if (!issue) return false;
|
|
310
|
+
issue.aiStatus = status;
|
|
311
|
+
issue.aiNotes = notes;
|
|
299
312
|
if (status === "fixed") {
|
|
300
|
-
|
|
313
|
+
issue.state = "fixing";
|
|
301
314
|
}
|
|
302
315
|
this.dirty = true;
|
|
303
316
|
return true;
|
|
304
317
|
}
|
|
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
318
|
getAll() {
|
|
325
|
-
return [...this.
|
|
319
|
+
return [...this.issues.values()];
|
|
326
320
|
}
|
|
327
321
|
getByState(state) {
|
|
328
|
-
return [...this.
|
|
322
|
+
return [...this.issues.values()].filter((i) => i.state === state);
|
|
323
|
+
}
|
|
324
|
+
getByCategory(category) {
|
|
325
|
+
return [...this.issues.values()].filter((i) => i.category === category);
|
|
329
326
|
}
|
|
330
|
-
get(
|
|
331
|
-
return this.
|
|
327
|
+
get(issueId) {
|
|
328
|
+
return this.issues.get(issueId);
|
|
332
329
|
}
|
|
333
330
|
clear() {
|
|
334
|
-
this.
|
|
335
|
-
this.dirty =
|
|
331
|
+
this.issues.clear();
|
|
332
|
+
this.dirty = false;
|
|
333
|
+
try {
|
|
334
|
+
if (existsSync3(this.issuesPath)) {
|
|
335
|
+
unlinkSync(this.issuesPath);
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
isDirty() {
|
|
341
|
+
return this.dirty;
|
|
336
342
|
}
|
|
337
343
|
async loadAsync() {
|
|
338
344
|
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
|
-
}
|
|
345
|
+
if (await fileExists(this.issuesPath)) {
|
|
346
|
+
const raw = await readFile2(this.issuesPath, "utf-8");
|
|
347
|
+
this.hydrate(raw);
|
|
347
348
|
}
|
|
348
349
|
} catch (err) {
|
|
349
|
-
brakitDebug(`
|
|
350
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
350
351
|
}
|
|
351
352
|
}
|
|
352
353
|
/** Sync load for tests only — not used in production paths. */
|
|
353
354
|
loadSync() {
|
|
354
355
|
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
|
-
}
|
|
356
|
+
if (existsSync3(this.issuesPath)) {
|
|
357
|
+
const raw = readFileSync2(this.issuesPath, "utf-8");
|
|
358
|
+
this.hydrate(raw);
|
|
363
359
|
}
|
|
364
360
|
} catch (err) {
|
|
365
|
-
brakitDebug(`
|
|
361
|
+
brakitDebug(`IssueStore: could not load issues file, starting fresh: ${err}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Parse and populate issues from a raw JSON string. */
|
|
365
|
+
hydrate(raw) {
|
|
366
|
+
const validated = validateIssuesData(JSON.parse(raw));
|
|
367
|
+
if (!validated) return;
|
|
368
|
+
for (const issue of validated.issues) {
|
|
369
|
+
this.issues.set(issue.issueId, issue);
|
|
366
370
|
}
|
|
367
371
|
}
|
|
368
372
|
flush() {
|
|
@@ -377,8 +381,8 @@ var FindingStore = class {
|
|
|
377
381
|
}
|
|
378
382
|
serialize() {
|
|
379
383
|
const data = {
|
|
380
|
-
version:
|
|
381
|
-
|
|
384
|
+
version: ISSUES_DATA_VERSION,
|
|
385
|
+
issues: [...this.issues.values()]
|
|
382
386
|
};
|
|
383
387
|
return JSON.stringify(data);
|
|
384
388
|
}
|
|
@@ -387,7 +391,7 @@ var FindingStore = class {
|
|
|
387
391
|
// src/detect/project.ts
|
|
388
392
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
389
393
|
import { existsSync as existsSync4 } from "fs";
|
|
390
|
-
import { join, relative } from "path";
|
|
394
|
+
import { join as join2, relative } from "path";
|
|
391
395
|
var FRAMEWORKS = [
|
|
392
396
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
393
397
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -396,24 +400,24 @@ var FRAMEWORKS = [
|
|
|
396
400
|
{ name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
|
|
397
401
|
];
|
|
398
402
|
async function detectProject(rootDir) {
|
|
399
|
-
const pkgPath =
|
|
403
|
+
const pkgPath = join2(rootDir, "package.json");
|
|
400
404
|
const raw = await readFile3(pkgPath, "utf-8");
|
|
401
405
|
const pkg = JSON.parse(raw);
|
|
402
406
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
403
407
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
404
408
|
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
405
409
|
const devCommand = matched?.devCmd ?? "";
|
|
406
|
-
const devBin = matched ?
|
|
410
|
+
const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
407
411
|
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
408
412
|
const packageManager = await detectPackageManager(rootDir);
|
|
409
413
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
410
414
|
}
|
|
411
415
|
async function detectPackageManager(rootDir) {
|
|
412
|
-
if (await fileExists(
|
|
413
|
-
if (await fileExists(
|
|
414
|
-
if (await fileExists(
|
|
415
|
-
if (await fileExists(
|
|
416
|
-
if (await fileExists(
|
|
416
|
+
if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
|
|
417
|
+
if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
|
|
418
|
+
if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
419
|
+
if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
|
|
420
|
+
if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
|
|
417
421
|
return "unknown";
|
|
418
422
|
}
|
|
419
423
|
function detectFrameworkFromDeps(allDeps) {
|
|
@@ -455,6 +459,38 @@ var AdapterRegistry = class {
|
|
|
455
459
|
}
|
|
456
460
|
};
|
|
457
461
|
|
|
462
|
+
// src/utils/response.ts
|
|
463
|
+
function tryParseJson(body) {
|
|
464
|
+
if (!body) return null;
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(body);
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function unwrapResponse(parsed) {
|
|
472
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
473
|
+
const obj = parsed;
|
|
474
|
+
const keys = Object.keys(obj);
|
|
475
|
+
if (keys.length > 3) return parsed;
|
|
476
|
+
let best = null;
|
|
477
|
+
let bestSize = 0;
|
|
478
|
+
for (const key of keys) {
|
|
479
|
+
const val = obj[key];
|
|
480
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
481
|
+
best = val;
|
|
482
|
+
bestSize = val.length;
|
|
483
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
484
|
+
const size = Object.keys(val).length;
|
|
485
|
+
if (size > bestSize) {
|
|
486
|
+
best = val;
|
|
487
|
+
bestSize = size;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
492
|
+
}
|
|
493
|
+
|
|
458
494
|
// src/analysis/rules/patterns.ts
|
|
459
495
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
460
496
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
@@ -481,31 +517,35 @@ var RULE_HINTS = {
|
|
|
481
517
|
"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
518
|
};
|
|
483
519
|
|
|
484
|
-
// src/
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
520
|
+
// src/utils/http-status.ts
|
|
521
|
+
function isErrorStatus(code) {
|
|
522
|
+
return code >= 400;
|
|
523
|
+
}
|
|
524
|
+
function isServerError(code) {
|
|
525
|
+
return code >= 500;
|
|
492
526
|
}
|
|
493
|
-
function
|
|
527
|
+
function isRedirect(code) {
|
|
528
|
+
return code >= 300 && code < 400;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/analysis/rules/exposed-secret.ts
|
|
532
|
+
function findSecretKeys(obj, prefix, depth = 0) {
|
|
494
533
|
const found = [];
|
|
534
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
|
|
495
535
|
if (!obj || typeof obj !== "object") return found;
|
|
496
536
|
if (Array.isArray(obj)) {
|
|
497
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
498
|
-
found.push(...findSecretKeys(obj[i], prefix));
|
|
537
|
+
for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
|
|
538
|
+
found.push(...findSecretKeys(obj[i], prefix, depth + 1));
|
|
499
539
|
}
|
|
500
540
|
return found;
|
|
501
541
|
}
|
|
502
542
|
for (const k of Object.keys(obj)) {
|
|
503
543
|
const val = obj[k];
|
|
504
|
-
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >=
|
|
544
|
+
if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
|
|
505
545
|
found.push(k);
|
|
506
546
|
}
|
|
507
547
|
if (typeof val === "object" && val !== null) {
|
|
508
|
-
found.push(...findSecretKeys(val, prefix + k + "."));
|
|
548
|
+
found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
|
|
509
549
|
}
|
|
510
550
|
}
|
|
511
551
|
return found;
|
|
@@ -519,8 +559,8 @@ var exposedSecretRule = {
|
|
|
519
559
|
const findings = [];
|
|
520
560
|
const seen = /* @__PURE__ */ new Map();
|
|
521
561
|
for (const r of ctx.requests) {
|
|
522
|
-
if (r.statusCode
|
|
523
|
-
const parsed =
|
|
562
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
563
|
+
const parsed = ctx.parsedBodies.response.get(r.id);
|
|
524
564
|
if (!parsed) continue;
|
|
525
565
|
const keys = findSecretKeys(parsed, "");
|
|
526
566
|
if (keys.length === 0) continue;
|
|
@@ -673,7 +713,7 @@ var errorInfoLeakRule = {
|
|
|
673
713
|
|
|
674
714
|
// src/analysis/rules/insecure-cookie.ts
|
|
675
715
|
function isFrameworkResponse(r) {
|
|
676
|
-
if (r.statusCode
|
|
716
|
+
if (isRedirect(r.statusCode)) return true;
|
|
677
717
|
if (r.path?.startsWith("/__")) return true;
|
|
678
718
|
if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
|
|
679
719
|
return false;
|
|
@@ -779,48 +819,15 @@ var corsCredentialsRule = {
|
|
|
779
819
|
}
|
|
780
820
|
};
|
|
781
821
|
|
|
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
822
|
// src/analysis/rules/response-pii-leak.ts
|
|
807
823
|
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) {
|
|
824
|
+
function findEmails(obj, depth = 0) {
|
|
819
825
|
const emails = [];
|
|
826
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
|
|
820
827
|
if (!obj || typeof obj !== "object") return emails;
|
|
821
828
|
if (Array.isArray(obj)) {
|
|
822
|
-
for (let i = 0; i < Math.min(obj.length,
|
|
823
|
-
emails.push(...findEmails(obj[i]));
|
|
829
|
+
for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
830
|
+
emails.push(...findEmails(obj[i], depth + 1));
|
|
824
831
|
}
|
|
825
832
|
return emails;
|
|
826
833
|
}
|
|
@@ -828,7 +835,7 @@ function findEmails(obj) {
|
|
|
828
835
|
if (typeof v === "string" && EMAIL_RE.test(v)) {
|
|
829
836
|
emails.push(v);
|
|
830
837
|
} else if (typeof v === "object" && v !== null) {
|
|
831
|
-
emails.push(...findEmails(v));
|
|
838
|
+
emails.push(...findEmails(v, depth + 1));
|
|
832
839
|
}
|
|
833
840
|
}
|
|
834
841
|
return emails;
|
|
@@ -871,7 +878,7 @@ function detectFullRecordPII(target) {
|
|
|
871
878
|
function detectListPII(target) {
|
|
872
879
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
873
880
|
let itemsWithEmail = 0;
|
|
874
|
-
for (let i = 0; i < Math.min(target.length,
|
|
881
|
+
for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
|
|
875
882
|
const item = target[i];
|
|
876
883
|
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
877
884
|
itemsWithEmail++;
|
|
@@ -902,10 +909,10 @@ var responsePiiLeakRule = {
|
|
|
902
909
|
const findings = [];
|
|
903
910
|
const seen = /* @__PURE__ */ new Map();
|
|
904
911
|
for (const r of ctx.requests) {
|
|
905
|
-
if (r.statusCode
|
|
906
|
-
const resJson =
|
|
912
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
913
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
907
914
|
if (!resJson) continue;
|
|
908
|
-
const reqJson =
|
|
915
|
+
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
909
916
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
910
917
|
if (!detection) continue;
|
|
911
918
|
const ep = `${r.method} ${r.path}`;
|
|
@@ -932,12 +939,31 @@ var responsePiiLeakRule = {
|
|
|
932
939
|
};
|
|
933
940
|
|
|
934
941
|
// src/analysis/rules/scanner.ts
|
|
942
|
+
function buildBodyCache(requests) {
|
|
943
|
+
const response = /* @__PURE__ */ new Map();
|
|
944
|
+
const request = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const r of requests) {
|
|
946
|
+
if (r.responseBody) {
|
|
947
|
+
const parsed = tryParseJson(r.responseBody);
|
|
948
|
+
if (parsed != null) response.set(r.id, parsed);
|
|
949
|
+
}
|
|
950
|
+
if (r.requestBody) {
|
|
951
|
+
const parsed = tryParseJson(r.requestBody);
|
|
952
|
+
if (parsed != null) request.set(r.id, parsed);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return { response, request };
|
|
956
|
+
}
|
|
935
957
|
var SecurityScanner = class {
|
|
936
958
|
rules = [];
|
|
937
959
|
register(rule) {
|
|
938
960
|
this.rules.push(rule);
|
|
939
961
|
}
|
|
940
|
-
scan(
|
|
962
|
+
scan(input) {
|
|
963
|
+
const ctx = {
|
|
964
|
+
...input,
|
|
965
|
+
parsedBodies: buildBodyCache(input.requests)
|
|
966
|
+
};
|
|
941
967
|
const findings = [];
|
|
942
968
|
for (const rule of this.rules) {
|
|
943
969
|
try {
|
|
@@ -979,6 +1005,41 @@ var SubscriptionBag = class {
|
|
|
979
1005
|
// src/analysis/group.ts
|
|
980
1006
|
import { randomUUID } from "crypto";
|
|
981
1007
|
|
|
1008
|
+
// src/constants/routes.ts
|
|
1009
|
+
var DASHBOARD_PREFIX = "/__brakit";
|
|
1010
|
+
var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
1011
|
+
var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
1012
|
+
var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
1013
|
+
var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
1014
|
+
var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
1015
|
+
var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
1016
|
+
var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
1017
|
+
var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
1018
|
+
var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
1019
|
+
var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
1020
|
+
var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
1021
|
+
var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
1022
|
+
var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
1023
|
+
var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
1024
|
+
var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
1025
|
+
var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
1026
|
+
var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
1027
|
+
var VALID_TABS_TUPLE = [
|
|
1028
|
+
"overview",
|
|
1029
|
+
"actions",
|
|
1030
|
+
"requests",
|
|
1031
|
+
"fetches",
|
|
1032
|
+
"queries",
|
|
1033
|
+
"errors",
|
|
1034
|
+
"logs",
|
|
1035
|
+
"performance",
|
|
1036
|
+
"security"
|
|
1037
|
+
];
|
|
1038
|
+
var VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
1039
|
+
|
|
1040
|
+
// src/constants/network.ts
|
|
1041
|
+
var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
1042
|
+
|
|
982
1043
|
// src/analysis/categorize.ts
|
|
983
1044
|
function detectCategory(req) {
|
|
984
1045
|
const { method, url, statusCode, responseHeaders } = req;
|
|
@@ -1042,7 +1103,7 @@ function labelRequest(req) {
|
|
|
1042
1103
|
function generateHumanLabel(req, category) {
|
|
1043
1104
|
const effectivePath = getEffectivePath(req);
|
|
1044
1105
|
const endpointName = getEndpointName(effectivePath);
|
|
1045
|
-
const failed = req.statusCode
|
|
1106
|
+
const failed = isErrorStatus(req.statusCode);
|
|
1046
1107
|
switch (category) {
|
|
1047
1108
|
case "auth-handshake":
|
|
1048
1109
|
return "Auth handshake";
|
|
@@ -1222,7 +1283,7 @@ function detectWarnings(requests) {
|
|
|
1222
1283
|
for (const req of slowRequests) {
|
|
1223
1284
|
warnings.push(`${req.label} took ${(req.durationMs / 1e3).toFixed(1)}s`);
|
|
1224
1285
|
}
|
|
1225
|
-
const errors = requests.filter((r) => r.statusCode
|
|
1286
|
+
const errors = requests.filter((r) => isServerError(r.statusCode));
|
|
1226
1287
|
for (const req of errors) {
|
|
1227
1288
|
warnings.push(`${req.label} \u2014 server error (${req.statusCode})`);
|
|
1228
1289
|
}
|
|
@@ -1278,7 +1339,7 @@ function buildFlow(rawRequests) {
|
|
|
1278
1339
|
requests,
|
|
1279
1340
|
startTime,
|
|
1280
1341
|
totalDurationMs: Math.round(endTime - startTime),
|
|
1281
|
-
hasErrors: requests.some((r) => r.statusCode
|
|
1342
|
+
hasErrors: requests.some((r) => isErrorStatus(r.statusCode)),
|
|
1282
1343
|
warnings: detectWarnings(rawRequests),
|
|
1283
1344
|
sourcePage,
|
|
1284
1345
|
redundancyPct
|
|
@@ -1346,8 +1407,14 @@ function groupBy(items, keyFn) {
|
|
|
1346
1407
|
}
|
|
1347
1408
|
|
|
1348
1409
|
// src/utils/endpoint.ts
|
|
1410
|
+
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;
|
|
1411
|
+
function normalizePath(path) {
|
|
1412
|
+
const qIdx = path.indexOf("?");
|
|
1413
|
+
const pathname = qIdx === -1 ? path : path.slice(0, qIdx);
|
|
1414
|
+
return pathname.split("/").map((seg) => seg && DYNAMIC_SEGMENT_RE.test(seg) ? ":id" : seg).join("/");
|
|
1415
|
+
}
|
|
1349
1416
|
function getEndpointKey(method, path) {
|
|
1350
|
-
return `${method} ${path}`;
|
|
1417
|
+
return `${method} ${normalizePath(path)}`;
|
|
1351
1418
|
}
|
|
1352
1419
|
var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
1353
1420
|
function extractEndpointFromDesc(desc) {
|
|
@@ -1429,6 +1496,15 @@ function windowByEndpoint(requests) {
|
|
|
1429
1496
|
}
|
|
1430
1497
|
return windowed;
|
|
1431
1498
|
}
|
|
1499
|
+
function extractActiveEndpoints(requests) {
|
|
1500
|
+
const endpoints = /* @__PURE__ */ new Set();
|
|
1501
|
+
for (const r of requests) {
|
|
1502
|
+
if (!r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))) {
|
|
1503
|
+
endpoints.add(getEndpointKey(r.method, r.path));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return endpoints;
|
|
1507
|
+
}
|
|
1432
1508
|
function prepareContext(ctx) {
|
|
1433
1509
|
const nonStatic = ctx.requests.filter(
|
|
1434
1510
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -1446,7 +1522,7 @@ function prepareContext(ctx) {
|
|
|
1446
1522
|
endpointGroups.set(ep, g);
|
|
1447
1523
|
}
|
|
1448
1524
|
g.total++;
|
|
1449
|
-
if (r.statusCode
|
|
1525
|
+
if (isErrorStatus(r.statusCode)) g.errors++;
|
|
1450
1526
|
g.totalDuration += r.durationMs;
|
|
1451
1527
|
g.totalSize += r.responseSize ?? 0;
|
|
1452
1528
|
const reqQueries = queriesByReq.get(r.id) ?? [];
|
|
@@ -1866,7 +1942,7 @@ var responseOverfetchRule = {
|
|
|
1866
1942
|
const insights = [];
|
|
1867
1943
|
const seen = /* @__PURE__ */ new Set();
|
|
1868
1944
|
for (const r of ctx.nonStatic) {
|
|
1869
|
-
if (r.statusCode
|
|
1945
|
+
if (isErrorStatus(r.statusCode) || !r.responseBody) continue;
|
|
1870
1946
|
const ep = getEndpointKey(r.method, r.path);
|
|
1871
1947
|
if (seen.has(ep)) continue;
|
|
1872
1948
|
let parsed;
|
|
@@ -2010,81 +2086,37 @@ function computeInsights(ctx) {
|
|
|
2010
2086
|
return createDefaultInsightRunner().run(ctx);
|
|
2011
2087
|
}
|
|
2012
2088
|
|
|
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
|
-
};
|
|
2089
|
+
// src/analysis/issue-mappers.ts
|
|
2090
|
+
function categorizeInsight(type) {
|
|
2091
|
+
if (type === "security") return "security";
|
|
2092
|
+
if (type === "error" || type === "error-hotspot") return "reliability";
|
|
2093
|
+
return "performance";
|
|
2094
|
+
}
|
|
2095
|
+
function insightToIssue(insight) {
|
|
2096
|
+
return {
|
|
2097
|
+
category: categorizeInsight(insight.type),
|
|
2098
|
+
rule: insight.type,
|
|
2099
|
+
severity: insight.severity,
|
|
2100
|
+
title: insight.title,
|
|
2101
|
+
desc: insight.desc,
|
|
2102
|
+
hint: insight.hint,
|
|
2103
|
+
detail: insight.detail,
|
|
2104
|
+
endpoint: extractEndpointFromDesc(insight.desc) ?? void 0,
|
|
2105
|
+
nav: insight.nav
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function securityFindingToIssue(finding) {
|
|
2109
|
+
return {
|
|
2110
|
+
category: "security",
|
|
2111
|
+
rule: finding.rule,
|
|
2112
|
+
severity: finding.severity,
|
|
2113
|
+
title: finding.title,
|
|
2114
|
+
desc: finding.desc,
|
|
2115
|
+
hint: finding.hint,
|
|
2116
|
+
endpoint: finding.endpoint,
|
|
2117
|
+
nav: "security"
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2088
2120
|
|
|
2089
2121
|
// src/analysis/engine.ts
|
|
2090
2122
|
var AnalysisEngine = class {
|
|
@@ -2094,10 +2126,8 @@ var AnalysisEngine = class {
|
|
|
2094
2126
|
this.scanner = createDefaultScanner();
|
|
2095
2127
|
}
|
|
2096
2128
|
scanner;
|
|
2097
|
-
insightTracker = new InsightTracker();
|
|
2098
2129
|
cachedInsights = [];
|
|
2099
2130
|
cachedFindings = [];
|
|
2100
|
-
cachedStatefulInsights = [];
|
|
2101
2131
|
debounceTimer = null;
|
|
2102
2132
|
subs = new SubscriptionBag();
|
|
2103
2133
|
start() {
|
|
@@ -2120,15 +2150,6 @@ var AnalysisEngine = class {
|
|
|
2120
2150
|
getFindings() {
|
|
2121
2151
|
return this.cachedFindings;
|
|
2122
2152
|
}
|
|
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
2153
|
scheduleRecompute() {
|
|
2133
2154
|
if (this.debounceTimer) return;
|
|
2134
2155
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -2137,20 +2158,14 @@ var AnalysisEngine = class {
|
|
|
2137
2158
|
}, this.debounceMs);
|
|
2138
2159
|
}
|
|
2139
2160
|
recompute() {
|
|
2140
|
-
const
|
|
2161
|
+
const allRequests = this.registry.get("request-store").getAll();
|
|
2141
2162
|
const queries = this.registry.get("query-store").getAll();
|
|
2142
2163
|
const errors = this.registry.get("error-store").getAll();
|
|
2143
2164
|
const logs = this.registry.get("log-store").getAll();
|
|
2144
2165
|
const fetches = this.registry.get("fetch-store").getAll();
|
|
2166
|
+
const requests = windowByEndpoint(allRequests);
|
|
2145
2167
|
const flows = groupRequestsIntoFlows(requests);
|
|
2146
2168
|
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
2169
|
this.cachedInsights = computeInsights({
|
|
2155
2170
|
requests,
|
|
2156
2171
|
queries,
|
|
@@ -2160,24 +2175,40 @@ var AnalysisEngine = class {
|
|
|
2160
2175
|
previousMetrics: this.registry.get("metrics-store").getAll(),
|
|
2161
2176
|
securityFindings: this.cachedFindings
|
|
2162
2177
|
});
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2178
|
+
if (this.registry.has("issue-store")) {
|
|
2179
|
+
const issueStore = this.registry.get("issue-store");
|
|
2180
|
+
for (const finding of this.cachedFindings) {
|
|
2181
|
+
issueStore.upsert(securityFindingToIssue(finding), "passive");
|
|
2182
|
+
}
|
|
2183
|
+
for (const insight of this.cachedInsights) {
|
|
2184
|
+
issueStore.upsert(insightToIssue(insight), "passive");
|
|
2185
|
+
}
|
|
2186
|
+
const currentIssueIds = /* @__PURE__ */ new Set();
|
|
2187
|
+
for (const finding of this.cachedFindings) {
|
|
2188
|
+
currentIssueIds.add(computeIssueId(securityFindingToIssue(finding)));
|
|
2189
|
+
}
|
|
2190
|
+
for (const insight of this.cachedInsights) {
|
|
2191
|
+
currentIssueIds.add(computeIssueId(insightToIssue(insight)));
|
|
2192
|
+
}
|
|
2193
|
+
const activeEndpoints = extractActiveEndpoints(allRequests);
|
|
2194
|
+
issueStore.reconcile(currentIssueIds, activeEndpoints);
|
|
2195
|
+
const update = {
|
|
2196
|
+
insights: this.cachedInsights,
|
|
2197
|
+
findings: this.cachedFindings,
|
|
2198
|
+
issues: issueStore.getAll()
|
|
2199
|
+
};
|
|
2200
|
+
this.registry.get("event-bus").emit("analysis:updated", update);
|
|
2201
|
+
}
|
|
2171
2202
|
}
|
|
2172
2203
|
};
|
|
2173
2204
|
|
|
2174
2205
|
// src/index.ts
|
|
2175
|
-
var VERSION = "0.8.
|
|
2206
|
+
var VERSION = "0.8.6";
|
|
2176
2207
|
export {
|
|
2177
2208
|
AdapterRegistry,
|
|
2178
2209
|
AnalysisEngine,
|
|
2179
|
-
FindingStore,
|
|
2180
2210
|
InsightRunner,
|
|
2211
|
+
IssueStore,
|
|
2181
2212
|
SecurityScanner,
|
|
2182
2213
|
VERSION,
|
|
2183
2214
|
computeInsights,
|