brakit 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -4
- package/dist/api.d.ts +99 -30
- package/dist/api.js +348 -49
- package/dist/bin/brakit.js +1128 -29
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +743 -0
- package/dist/runtime/index.js +680 -173
- package/package.json +5 -1
package/dist/api.js
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
// src/store/finding-store.ts
|
|
2
|
+
import {
|
|
3
|
+
readFileSync as readFileSync2,
|
|
4
|
+
writeFileSync as writeFileSync2,
|
|
5
|
+
existsSync as existsSync2,
|
|
6
|
+
mkdirSync as mkdirSync2,
|
|
7
|
+
renameSync
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
10
|
+
import { resolve as resolve2 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/constants/routes.ts
|
|
13
|
+
var DASHBOARD_PREFIX = "/__brakit";
|
|
14
|
+
|
|
15
|
+
// src/constants/limits.ts
|
|
16
|
+
var MAX_REQUEST_ENTRIES = 1e3;
|
|
17
|
+
var MAX_TELEMETRY_ENTRIES = 1e3;
|
|
18
|
+
var MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
19
|
+
|
|
20
|
+
// src/constants/thresholds.ts
|
|
21
|
+
var FLOW_GAP_MS = 5e3;
|
|
22
|
+
var SLOW_REQUEST_THRESHOLD_MS = 2e3;
|
|
23
|
+
var MIN_POLLING_SEQUENCE = 3;
|
|
24
|
+
var ENDPOINT_TRUNCATE_LENGTH = 12;
|
|
25
|
+
var N1_QUERY_THRESHOLD = 5;
|
|
26
|
+
var ERROR_RATE_THRESHOLD_PCT = 20;
|
|
27
|
+
var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
|
|
28
|
+
var MIN_REQUESTS_FOR_INSIGHT = 2;
|
|
29
|
+
var HIGH_QUERY_COUNT_PER_REQ = 5;
|
|
30
|
+
var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
|
|
31
|
+
var CROSS_ENDPOINT_PCT = 50;
|
|
32
|
+
var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
|
|
33
|
+
var REDUNDANT_QUERY_MIN_COUNT = 2;
|
|
34
|
+
var LARGE_RESPONSE_BYTES = 51200;
|
|
35
|
+
var HIGH_ROW_COUNT = 100;
|
|
36
|
+
var OVERFETCH_MIN_REQUESTS = 2;
|
|
37
|
+
var OVERFETCH_MIN_FIELDS = 8;
|
|
38
|
+
var OVERFETCH_MIN_INTERNAL_IDS = 2;
|
|
39
|
+
var OVERFETCH_NULL_RATIO = 0.3;
|
|
40
|
+
var REGRESSION_PCT_THRESHOLD = 50;
|
|
41
|
+
var REGRESSION_MIN_INCREASE_MS = 200;
|
|
42
|
+
var REGRESSION_MIN_REQUESTS = 5;
|
|
43
|
+
var QUERY_COUNT_REGRESSION_RATIO = 1.5;
|
|
44
|
+
var OVERFETCH_MANY_FIELDS = 12;
|
|
45
|
+
var OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
46
|
+
var MAX_DUPLICATE_INSIGHTS = 3;
|
|
47
|
+
var INSIGHT_WINDOW_PER_ENDPOINT = 2;
|
|
48
|
+
var RESOLVE_AFTER_ABSENCES = 3;
|
|
49
|
+
var RESOLVED_INSIGHT_TTL_MS = 18e5;
|
|
50
|
+
|
|
51
|
+
// src/constants/metrics.ts
|
|
52
|
+
var METRICS_DIR = ".brakit";
|
|
53
|
+
var FINDINGS_FILE = ".brakit/findings.json";
|
|
54
|
+
var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
4
55
|
|
|
5
56
|
// src/utils/fs.ts
|
|
6
57
|
import { access } from "fs/promises";
|
|
@@ -14,8 +65,191 @@ async function fileExists(path) {
|
|
|
14
65
|
return false;
|
|
15
66
|
}
|
|
16
67
|
}
|
|
68
|
+
function ensureGitignore(dir, entry) {
|
|
69
|
+
try {
|
|
70
|
+
const gitignorePath = resolve(dir, "../.gitignore");
|
|
71
|
+
if (existsSync(gitignorePath)) {
|
|
72
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
73
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
74
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
75
|
+
} else {
|
|
76
|
+
writeFileSync(gitignorePath, entry + "\n");
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/store/finding-id.ts
|
|
83
|
+
import { createHash } from "crypto";
|
|
84
|
+
function computeFindingId(finding) {
|
|
85
|
+
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
86
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/store/finding-store.ts
|
|
90
|
+
var FindingStore = class {
|
|
91
|
+
constructor(rootDir) {
|
|
92
|
+
this.rootDir = rootDir;
|
|
93
|
+
this.metricsDir = resolve2(rootDir, METRICS_DIR);
|
|
94
|
+
this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
|
|
95
|
+
this.tmpPath = this.findingsPath + ".tmp";
|
|
96
|
+
this.load();
|
|
97
|
+
}
|
|
98
|
+
findings = /* @__PURE__ */ new Map();
|
|
99
|
+
flushTimer = null;
|
|
100
|
+
dirty = false;
|
|
101
|
+
writing = false;
|
|
102
|
+
findingsPath;
|
|
103
|
+
tmpPath;
|
|
104
|
+
metricsDir;
|
|
105
|
+
start() {
|
|
106
|
+
this.flushTimer = setInterval(
|
|
107
|
+
() => this.flush(),
|
|
108
|
+
FINDINGS_FLUSH_INTERVAL_MS
|
|
109
|
+
);
|
|
110
|
+
this.flushTimer.unref();
|
|
111
|
+
}
|
|
112
|
+
stop() {
|
|
113
|
+
if (this.flushTimer) {
|
|
114
|
+
clearInterval(this.flushTimer);
|
|
115
|
+
this.flushTimer = null;
|
|
116
|
+
}
|
|
117
|
+
this.flushSync();
|
|
118
|
+
}
|
|
119
|
+
upsert(finding, source) {
|
|
120
|
+
const id = computeFindingId(finding);
|
|
121
|
+
const existing = this.findings.get(id);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (existing) {
|
|
124
|
+
existing.lastSeenAt = now;
|
|
125
|
+
existing.occurrences++;
|
|
126
|
+
existing.finding = finding;
|
|
127
|
+
if (existing.state === "resolved") {
|
|
128
|
+
existing.state = "open";
|
|
129
|
+
existing.resolvedAt = null;
|
|
130
|
+
}
|
|
131
|
+
this.dirty = true;
|
|
132
|
+
return existing;
|
|
133
|
+
}
|
|
134
|
+
const stateful = {
|
|
135
|
+
findingId: id,
|
|
136
|
+
state: "open",
|
|
137
|
+
source,
|
|
138
|
+
finding,
|
|
139
|
+
firstSeenAt: now,
|
|
140
|
+
lastSeenAt: now,
|
|
141
|
+
resolvedAt: null,
|
|
142
|
+
occurrences: 1
|
|
143
|
+
};
|
|
144
|
+
this.findings.set(id, stateful);
|
|
145
|
+
this.dirty = true;
|
|
146
|
+
return stateful;
|
|
147
|
+
}
|
|
148
|
+
transition(findingId, state) {
|
|
149
|
+
const finding = this.findings.get(findingId);
|
|
150
|
+
if (!finding) return false;
|
|
151
|
+
finding.state = state;
|
|
152
|
+
if (state === "resolved") {
|
|
153
|
+
finding.resolvedAt = Date.now();
|
|
154
|
+
}
|
|
155
|
+
this.dirty = true;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
reconcilePassive(currentFindings) {
|
|
159
|
+
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
160
|
+
for (const [id, stateful] of this.findings) {
|
|
161
|
+
if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
|
|
162
|
+
stateful.state = "resolved";
|
|
163
|
+
stateful.resolvedAt = Date.now();
|
|
164
|
+
this.dirty = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
getAll() {
|
|
169
|
+
return [...this.findings.values()];
|
|
170
|
+
}
|
|
171
|
+
getByState(state) {
|
|
172
|
+
return [...this.findings.values()].filter((f) => f.state === state);
|
|
173
|
+
}
|
|
174
|
+
get(findingId) {
|
|
175
|
+
return this.findings.get(findingId);
|
|
176
|
+
}
|
|
177
|
+
clear() {
|
|
178
|
+
this.findings.clear();
|
|
179
|
+
this.dirty = true;
|
|
180
|
+
}
|
|
181
|
+
load() {
|
|
182
|
+
try {
|
|
183
|
+
if (existsSync2(this.findingsPath)) {
|
|
184
|
+
const raw = readFileSync2(this.findingsPath, "utf-8");
|
|
185
|
+
const parsed = JSON.parse(raw);
|
|
186
|
+
if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
|
|
187
|
+
for (const f of parsed.findings) {
|
|
188
|
+
this.findings.set(f.findingId, f);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
flush() {
|
|
196
|
+
if (!this.dirty) return;
|
|
197
|
+
this.writeAsync();
|
|
198
|
+
}
|
|
199
|
+
flushSync() {
|
|
200
|
+
if (!this.dirty) return;
|
|
201
|
+
try {
|
|
202
|
+
this.ensureDir();
|
|
203
|
+
const data = {
|
|
204
|
+
version: 1,
|
|
205
|
+
findings: [...this.findings.values()]
|
|
206
|
+
};
|
|
207
|
+
writeFileSync2(this.tmpPath, JSON.stringify(data));
|
|
208
|
+
renameSync(this.tmpPath, this.findingsPath);
|
|
209
|
+
this.dirty = false;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
process.stderr.write(
|
|
212
|
+
`[brakit] failed to save findings: ${err.message}
|
|
213
|
+
`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async writeAsync() {
|
|
218
|
+
if (this.writing) return;
|
|
219
|
+
this.writing = true;
|
|
220
|
+
try {
|
|
221
|
+
if (!existsSync2(this.metricsDir)) {
|
|
222
|
+
await mkdir(this.metricsDir, { recursive: true });
|
|
223
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
224
|
+
}
|
|
225
|
+
const data = {
|
|
226
|
+
version: 1,
|
|
227
|
+
findings: [...this.findings.values()]
|
|
228
|
+
};
|
|
229
|
+
await writeFile2(this.tmpPath, JSON.stringify(data));
|
|
230
|
+
await rename(this.tmpPath, this.findingsPath);
|
|
231
|
+
this.dirty = false;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
process.stderr.write(
|
|
234
|
+
`[brakit] failed to save findings: ${err.message}
|
|
235
|
+
`
|
|
236
|
+
);
|
|
237
|
+
} finally {
|
|
238
|
+
this.writing = false;
|
|
239
|
+
if (this.dirty) this.writeAsync();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
ensureDir() {
|
|
243
|
+
if (!existsSync2(this.metricsDir)) {
|
|
244
|
+
mkdirSync2(this.metricsDir, { recursive: true });
|
|
245
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
17
249
|
|
|
18
250
|
// src/detect/project.ts
|
|
251
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
252
|
+
import { join } from "path";
|
|
19
253
|
var FRAMEWORKS = [
|
|
20
254
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
21
255
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -409,34 +643,6 @@ var corsCredentialsRule = {
|
|
|
409
643
|
}
|
|
410
644
|
};
|
|
411
645
|
|
|
412
|
-
// src/constants/thresholds.ts
|
|
413
|
-
var FLOW_GAP_MS = 5e3;
|
|
414
|
-
var SLOW_REQUEST_THRESHOLD_MS = 2e3;
|
|
415
|
-
var MIN_POLLING_SEQUENCE = 3;
|
|
416
|
-
var ENDPOINT_TRUNCATE_LENGTH = 12;
|
|
417
|
-
var N1_QUERY_THRESHOLD = 5;
|
|
418
|
-
var ERROR_RATE_THRESHOLD_PCT = 20;
|
|
419
|
-
var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
|
|
420
|
-
var MIN_REQUESTS_FOR_INSIGHT = 2;
|
|
421
|
-
var HIGH_QUERY_COUNT_PER_REQ = 5;
|
|
422
|
-
var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
|
|
423
|
-
var CROSS_ENDPOINT_PCT = 50;
|
|
424
|
-
var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
|
|
425
|
-
var REDUNDANT_QUERY_MIN_COUNT = 2;
|
|
426
|
-
var LARGE_RESPONSE_BYTES = 51200;
|
|
427
|
-
var HIGH_ROW_COUNT = 100;
|
|
428
|
-
var OVERFETCH_MIN_REQUESTS = 2;
|
|
429
|
-
var OVERFETCH_MIN_FIELDS = 8;
|
|
430
|
-
var OVERFETCH_MIN_INTERNAL_IDS = 2;
|
|
431
|
-
var OVERFETCH_NULL_RATIO = 0.3;
|
|
432
|
-
var REGRESSION_PCT_THRESHOLD = 50;
|
|
433
|
-
var REGRESSION_MIN_INCREASE_MS = 200;
|
|
434
|
-
var REGRESSION_MIN_REQUESTS = 5;
|
|
435
|
-
var QUERY_COUNT_REGRESSION_RATIO = 1.5;
|
|
436
|
-
var OVERFETCH_MANY_FIELDS = 12;
|
|
437
|
-
var OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
438
|
-
var MAX_DUPLICATE_INSIGHTS = 3;
|
|
439
|
-
|
|
440
646
|
// src/utils/response.ts
|
|
441
647
|
function unwrapResponse(parsed) {
|
|
442
648
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
@@ -623,13 +829,6 @@ function createDefaultScanner() {
|
|
|
623
829
|
return scanner;
|
|
624
830
|
}
|
|
625
831
|
|
|
626
|
-
// src/constants/routes.ts
|
|
627
|
-
var DASHBOARD_PREFIX = "/__brakit";
|
|
628
|
-
|
|
629
|
-
// src/constants/limits.ts
|
|
630
|
-
var MAX_REQUEST_ENTRIES = 1e3;
|
|
631
|
-
var MAX_TELEMETRY_ENTRIES = 1e3;
|
|
632
|
-
|
|
633
832
|
// src/utils/static-patterns.ts
|
|
634
833
|
var STATIC_PATTERNS = [
|
|
635
834
|
/^\/_next\//,
|
|
@@ -775,18 +974,22 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
775
974
|
function getEndpointKey(method, path) {
|
|
776
975
|
return `${method} ${path}`;
|
|
777
976
|
}
|
|
977
|
+
var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
978
|
+
function extractEndpointFromDesc(desc) {
|
|
979
|
+
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
980
|
+
}
|
|
778
981
|
|
|
779
982
|
// src/store/metrics/persistence.ts
|
|
780
983
|
import {
|
|
781
|
-
readFileSync as
|
|
782
|
-
writeFileSync as
|
|
783
|
-
mkdirSync as
|
|
784
|
-
existsSync as
|
|
984
|
+
readFileSync as readFileSync3,
|
|
985
|
+
writeFileSync as writeFileSync3,
|
|
986
|
+
mkdirSync as mkdirSync3,
|
|
987
|
+
existsSync as existsSync3,
|
|
785
988
|
unlinkSync,
|
|
786
|
-
renameSync
|
|
989
|
+
renameSync as renameSync2
|
|
787
990
|
} from "fs";
|
|
788
|
-
import { writeFile as
|
|
789
|
-
import { resolve as
|
|
991
|
+
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
992
|
+
import { resolve as resolve3 } from "path";
|
|
790
993
|
|
|
791
994
|
// src/analysis/group.ts
|
|
792
995
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -1215,6 +1418,23 @@ function createEndpointGroup() {
|
|
|
1215
1418
|
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
1216
1419
|
};
|
|
1217
1420
|
}
|
|
1421
|
+
function windowByEndpoint(requests) {
|
|
1422
|
+
const byEndpoint = /* @__PURE__ */ new Map();
|
|
1423
|
+
for (const r of requests) {
|
|
1424
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
1425
|
+
let list = byEndpoint.get(ep);
|
|
1426
|
+
if (!list) {
|
|
1427
|
+
list = [];
|
|
1428
|
+
byEndpoint.set(ep, list);
|
|
1429
|
+
}
|
|
1430
|
+
list.push(r);
|
|
1431
|
+
}
|
|
1432
|
+
const windowed = [];
|
|
1433
|
+
for (const [, reqs] of byEndpoint) {
|
|
1434
|
+
windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
|
|
1435
|
+
}
|
|
1436
|
+
return windowed;
|
|
1437
|
+
}
|
|
1218
1438
|
function prepareContext(ctx) {
|
|
1219
1439
|
const nonStatic = ctx.requests.filter(
|
|
1220
1440
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -1222,8 +1442,9 @@ function prepareContext(ctx) {
|
|
|
1222
1442
|
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
1223
1443
|
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
1224
1444
|
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
1445
|
+
const recent = windowByEndpoint(nonStatic);
|
|
1225
1446
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
1226
|
-
for (const r of
|
|
1447
|
+
for (const r of recent) {
|
|
1227
1448
|
const ep = getEndpointKey(r.method, r.path);
|
|
1228
1449
|
let g = endpointGroups.get(ep);
|
|
1229
1450
|
if (!g) {
|
|
@@ -1795,10 +2016,66 @@ function computeInsights(ctx) {
|
|
|
1795
2016
|
return createDefaultInsightRunner().run(ctx);
|
|
1796
2017
|
}
|
|
1797
2018
|
|
|
2019
|
+
// src/analysis/insight-tracker.ts
|
|
2020
|
+
function computeInsightKey(insight) {
|
|
2021
|
+
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
2022
|
+
return `${insight.type}:${identifier}`;
|
|
2023
|
+
}
|
|
2024
|
+
var InsightTracker = class {
|
|
2025
|
+
tracked = /* @__PURE__ */ new Map();
|
|
2026
|
+
reconcile(current) {
|
|
2027
|
+
const currentKeys = /* @__PURE__ */ new Set();
|
|
2028
|
+
const now = Date.now();
|
|
2029
|
+
for (const insight of current) {
|
|
2030
|
+
const key = computeInsightKey(insight);
|
|
2031
|
+
currentKeys.add(key);
|
|
2032
|
+
const existing = this.tracked.get(key);
|
|
2033
|
+
if (existing) {
|
|
2034
|
+
existing.insight = insight;
|
|
2035
|
+
existing.lastSeenAt = now;
|
|
2036
|
+
existing.consecutiveAbsences = 0;
|
|
2037
|
+
if (existing.state === "resolved") {
|
|
2038
|
+
existing.state = "open";
|
|
2039
|
+
existing.resolvedAt = null;
|
|
2040
|
+
}
|
|
2041
|
+
} else {
|
|
2042
|
+
this.tracked.set(key, {
|
|
2043
|
+
key,
|
|
2044
|
+
state: "open",
|
|
2045
|
+
insight,
|
|
2046
|
+
firstSeenAt: now,
|
|
2047
|
+
lastSeenAt: now,
|
|
2048
|
+
resolvedAt: null,
|
|
2049
|
+
consecutiveAbsences: 0
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
for (const [key, stateful] of this.tracked) {
|
|
2054
|
+
if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
|
|
2055
|
+
stateful.consecutiveAbsences++;
|
|
2056
|
+
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
2057
|
+
stateful.state = "resolved";
|
|
2058
|
+
stateful.resolvedAt = now;
|
|
2059
|
+
}
|
|
2060
|
+
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
2061
|
+
this.tracked.delete(key);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return [...this.tracked.values()];
|
|
2065
|
+
}
|
|
2066
|
+
getAll() {
|
|
2067
|
+
return [...this.tracked.values()];
|
|
2068
|
+
}
|
|
2069
|
+
clear() {
|
|
2070
|
+
this.tracked.clear();
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
1798
2074
|
// src/analysis/engine.ts
|
|
1799
2075
|
var AnalysisEngine = class {
|
|
1800
|
-
constructor(metricsStore, debounceMs = 300) {
|
|
2076
|
+
constructor(metricsStore, findingStore, debounceMs = 300) {
|
|
1801
2077
|
this.metricsStore = metricsStore;
|
|
2078
|
+
this.findingStore = findingStore;
|
|
1802
2079
|
this.debounceMs = debounceMs;
|
|
1803
2080
|
this.scanner = createDefaultScanner();
|
|
1804
2081
|
this.boundRequestListener = () => this.scheduleRecompute();
|
|
@@ -1807,8 +2084,10 @@ var AnalysisEngine = class {
|
|
|
1807
2084
|
this.boundLogListener = () => this.scheduleRecompute();
|
|
1808
2085
|
}
|
|
1809
2086
|
scanner;
|
|
2087
|
+
insightTracker = new InsightTracker();
|
|
1810
2088
|
cachedInsights = [];
|
|
1811
2089
|
cachedFindings = [];
|
|
2090
|
+
cachedStatefulInsights = [];
|
|
1812
2091
|
debounceTimer = null;
|
|
1813
2092
|
listeners = [];
|
|
1814
2093
|
boundRequestListener;
|
|
@@ -1844,6 +2123,12 @@ var AnalysisEngine = class {
|
|
|
1844
2123
|
getFindings() {
|
|
1845
2124
|
return this.cachedFindings;
|
|
1846
2125
|
}
|
|
2126
|
+
getStatefulFindings() {
|
|
2127
|
+
return this.findingStore?.getAll() ?? [];
|
|
2128
|
+
}
|
|
2129
|
+
getStatefulInsights() {
|
|
2130
|
+
return this.cachedStatefulInsights;
|
|
2131
|
+
}
|
|
1847
2132
|
scheduleRecompute() {
|
|
1848
2133
|
if (this.debounceTimer) return;
|
|
1849
2134
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -1859,6 +2144,12 @@ var AnalysisEngine = class {
|
|
|
1859
2144
|
const fetches = defaultFetchStore.getAll();
|
|
1860
2145
|
const flows = groupRequestsIntoFlows(requests);
|
|
1861
2146
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
2147
|
+
if (this.findingStore) {
|
|
2148
|
+
for (const finding of this.cachedFindings) {
|
|
2149
|
+
this.findingStore.upsert(finding, "passive");
|
|
2150
|
+
}
|
|
2151
|
+
this.findingStore.reconcilePassive(this.cachedFindings);
|
|
2152
|
+
}
|
|
1862
2153
|
this.cachedInsights = computeInsights({
|
|
1863
2154
|
requests,
|
|
1864
2155
|
queries,
|
|
@@ -1868,9 +2159,16 @@ var AnalysisEngine = class {
|
|
|
1868
2159
|
previousMetrics: this.metricsStore.getAll(),
|
|
1869
2160
|
securityFindings: this.cachedFindings
|
|
1870
2161
|
});
|
|
2162
|
+
this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
|
|
2163
|
+
const update = {
|
|
2164
|
+
insights: this.cachedInsights,
|
|
2165
|
+
findings: this.cachedFindings,
|
|
2166
|
+
statefulFindings: this.getStatefulFindings(),
|
|
2167
|
+
statefulInsights: this.cachedStatefulInsights
|
|
2168
|
+
};
|
|
1871
2169
|
for (const fn of this.listeners) {
|
|
1872
2170
|
try {
|
|
1873
|
-
fn(
|
|
2171
|
+
fn(update);
|
|
1874
2172
|
} catch {
|
|
1875
2173
|
}
|
|
1876
2174
|
}
|
|
@@ -1878,10 +2176,11 @@ var AnalysisEngine = class {
|
|
|
1878
2176
|
};
|
|
1879
2177
|
|
|
1880
2178
|
// src/index.ts
|
|
1881
|
-
var VERSION = "0.
|
|
2179
|
+
var VERSION = "0.8.1";
|
|
1882
2180
|
export {
|
|
1883
2181
|
AdapterRegistry,
|
|
1884
2182
|
AnalysisEngine,
|
|
2183
|
+
FindingStore,
|
|
1885
2184
|
InsightRunner,
|
|
1886
2185
|
SecurityScanner,
|
|
1887
2186
|
VERSION,
|