brakit 0.8.0 → 0.8.2
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 +17 -6
- package/dist/api.d.ts +165 -82
- package/dist/api.js +245 -262
- package/dist/bin/brakit.js +132 -210
- package/dist/mcp/server.js +65 -15
- package/dist/runtime/index.js +2192 -1858
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
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";
|
|
2
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
10
3
|
import { resolve as resolve2 } from "path";
|
|
11
4
|
|
|
12
5
|
// src/constants/routes.ts
|
|
13
6
|
var DASHBOARD_PREFIX = "/__brakit";
|
|
14
7
|
|
|
15
8
|
// src/constants/limits.ts
|
|
16
|
-
var
|
|
17
|
-
var MAX_TELEMETRY_ENTRIES = 1e3;
|
|
9
|
+
var MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
18
10
|
|
|
19
11
|
// src/constants/thresholds.ts
|
|
20
12
|
var FLOW_GAP_MS = 5e3;
|
|
@@ -43,12 +35,34 @@ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
|
|
|
43
35
|
var OVERFETCH_MANY_FIELDS = 12;
|
|
44
36
|
var OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
45
37
|
var MAX_DUPLICATE_INSIGHTS = 3;
|
|
38
|
+
var INSIGHT_WINDOW_PER_ENDPOINT = 2;
|
|
39
|
+
var RESOLVE_AFTER_ABSENCES = 3;
|
|
40
|
+
var RESOLVED_INSIGHT_TTL_MS = 18e5;
|
|
46
41
|
|
|
47
42
|
// src/constants/metrics.ts
|
|
48
43
|
var METRICS_DIR = ".brakit";
|
|
49
44
|
var FINDINGS_FILE = ".brakit/findings.json";
|
|
50
45
|
var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
51
46
|
|
|
47
|
+
// src/constants/severity.ts
|
|
48
|
+
var SEVERITY_CRITICAL = "critical";
|
|
49
|
+
var SEVERITY_WARNING = "warning";
|
|
50
|
+
var SEVERITY_INFO = "info";
|
|
51
|
+
var SEVERITY_ICON_MAP = {
|
|
52
|
+
[SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
|
|
53
|
+
[SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
|
|
54
|
+
[SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/utils/atomic-writer.ts
|
|
58
|
+
import {
|
|
59
|
+
writeFileSync as writeFileSync2,
|
|
60
|
+
existsSync as existsSync2,
|
|
61
|
+
mkdirSync as mkdirSync2,
|
|
62
|
+
renameSync
|
|
63
|
+
} from "fs";
|
|
64
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
65
|
+
|
|
52
66
|
// src/utils/fs.ts
|
|
53
67
|
import { access } from "fs/promises";
|
|
54
68
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -75,6 +89,70 @@ function ensureGitignore(dir, entry) {
|
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
// src/utils/log.ts
|
|
93
|
+
var PREFIX = "[brakit]";
|
|
94
|
+
function brakitWarn(message) {
|
|
95
|
+
process.stderr.write(`${PREFIX} ${message}
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/utils/atomic-writer.ts
|
|
100
|
+
var AtomicWriter = class {
|
|
101
|
+
constructor(opts) {
|
|
102
|
+
this.opts = opts;
|
|
103
|
+
this.tmpPath = opts.filePath + ".tmp";
|
|
104
|
+
}
|
|
105
|
+
tmpPath;
|
|
106
|
+
writing = false;
|
|
107
|
+
pendingContent = null;
|
|
108
|
+
writeSync(content) {
|
|
109
|
+
try {
|
|
110
|
+
this.ensureDir();
|
|
111
|
+
writeFileSync2(this.tmpPath, content);
|
|
112
|
+
renameSync(this.tmpPath, this.opts.filePath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async writeAsync(content) {
|
|
118
|
+
if (this.writing) {
|
|
119
|
+
this.pendingContent = content;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.writing = true;
|
|
123
|
+
try {
|
|
124
|
+
await this.ensureDirAsync();
|
|
125
|
+
await writeFile2(this.tmpPath, content);
|
|
126
|
+
await rename(this.tmpPath, this.opts.filePath);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
brakitWarn(`failed to save ${this.opts.label}: ${err.message}`);
|
|
129
|
+
} finally {
|
|
130
|
+
this.writing = false;
|
|
131
|
+
if (this.pendingContent !== null) {
|
|
132
|
+
const next = this.pendingContent;
|
|
133
|
+
this.pendingContent = null;
|
|
134
|
+
this.writeAsync(next);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
ensureDir() {
|
|
139
|
+
if (!existsSync2(this.opts.dir)) {
|
|
140
|
+
mkdirSync2(this.opts.dir, { recursive: true });
|
|
141
|
+
if (this.opts.gitignoreEntry) {
|
|
142
|
+
ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async ensureDirAsync() {
|
|
147
|
+
if (!existsSync2(this.opts.dir)) {
|
|
148
|
+
await mkdir(this.opts.dir, { recursive: true });
|
|
149
|
+
if (this.opts.gitignoreEntry) {
|
|
150
|
+
ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
78
156
|
// src/store/finding-id.ts
|
|
79
157
|
import { createHash } from "crypto";
|
|
80
158
|
function computeFindingId(finding) {
|
|
@@ -86,18 +164,21 @@ function computeFindingId(finding) {
|
|
|
86
164
|
var FindingStore = class {
|
|
87
165
|
constructor(rootDir) {
|
|
88
166
|
this.rootDir = rootDir;
|
|
89
|
-
|
|
167
|
+
const metricsDir = resolve2(rootDir, METRICS_DIR);
|
|
90
168
|
this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
|
|
91
|
-
this.
|
|
169
|
+
this.writer = new AtomicWriter({
|
|
170
|
+
dir: metricsDir,
|
|
171
|
+
filePath: this.findingsPath,
|
|
172
|
+
gitignoreEntry: METRICS_DIR,
|
|
173
|
+
label: "findings"
|
|
174
|
+
});
|
|
92
175
|
this.load();
|
|
93
176
|
}
|
|
94
177
|
findings = /* @__PURE__ */ new Map();
|
|
95
178
|
flushTimer = null;
|
|
96
179
|
dirty = false;
|
|
97
|
-
|
|
180
|
+
writer;
|
|
98
181
|
findingsPath;
|
|
99
|
-
tmpPath;
|
|
100
|
-
metricsDir;
|
|
101
182
|
start() {
|
|
102
183
|
this.flushTimer = setInterval(
|
|
103
184
|
() => this.flush(),
|
|
@@ -151,6 +232,15 @@ var FindingStore = class {
|
|
|
151
232
|
this.dirty = true;
|
|
152
233
|
return true;
|
|
153
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Reconcile passive findings against the current analysis results.
|
|
237
|
+
*
|
|
238
|
+
* Passive findings are detected by continuous scanning (not user-triggered).
|
|
239
|
+
* When a previously-seen finding is absent from the current results, it means
|
|
240
|
+
* the issue has been fixed — transition it to "resolved" automatically.
|
|
241
|
+
* Active findings (from MCP verify-fix) are not auto-resolved because they
|
|
242
|
+
* require explicit verification.
|
|
243
|
+
*/
|
|
154
244
|
reconcilePassive(currentFindings) {
|
|
155
245
|
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
156
246
|
for (const [id, stateful] of this.findings) {
|
|
@@ -176,7 +266,7 @@ var FindingStore = class {
|
|
|
176
266
|
}
|
|
177
267
|
load() {
|
|
178
268
|
try {
|
|
179
|
-
if (
|
|
269
|
+
if (existsSync3(this.findingsPath)) {
|
|
180
270
|
const raw = readFileSync2(this.findingsPath, "utf-8");
|
|
181
271
|
const parsed = JSON.parse(raw);
|
|
182
272
|
if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
|
|
@@ -190,56 +280,20 @@ var FindingStore = class {
|
|
|
190
280
|
}
|
|
191
281
|
flush() {
|
|
192
282
|
if (!this.dirty) return;
|
|
193
|
-
this.writeAsync();
|
|
283
|
+
this.writer.writeAsync(this.serialize());
|
|
284
|
+
this.dirty = false;
|
|
194
285
|
}
|
|
195
286
|
flushSync() {
|
|
196
287
|
if (!this.dirty) return;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const data = {
|
|
200
|
-
version: 1,
|
|
201
|
-
findings: [...this.findings.values()]
|
|
202
|
-
};
|
|
203
|
-
writeFileSync2(this.tmpPath, JSON.stringify(data));
|
|
204
|
-
renameSync(this.tmpPath, this.findingsPath);
|
|
205
|
-
this.dirty = false;
|
|
206
|
-
} catch (err) {
|
|
207
|
-
process.stderr.write(
|
|
208
|
-
`[brakit] failed to save findings: ${err.message}
|
|
209
|
-
`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
288
|
+
this.writer.writeSync(this.serialize());
|
|
289
|
+
this.dirty = false;
|
|
212
290
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
220
|
-
}
|
|
221
|
-
const data = {
|
|
222
|
-
version: 1,
|
|
223
|
-
findings: [...this.findings.values()]
|
|
224
|
-
};
|
|
225
|
-
await writeFile2(this.tmpPath, JSON.stringify(data));
|
|
226
|
-
await rename(this.tmpPath, this.findingsPath);
|
|
227
|
-
this.dirty = false;
|
|
228
|
-
} catch (err) {
|
|
229
|
-
process.stderr.write(
|
|
230
|
-
`[brakit] failed to save findings: ${err.message}
|
|
231
|
-
`
|
|
232
|
-
);
|
|
233
|
-
} finally {
|
|
234
|
-
this.writing = false;
|
|
235
|
-
if (this.dirty) this.writeAsync();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
ensureDir() {
|
|
239
|
-
if (!existsSync2(this.metricsDir)) {
|
|
240
|
-
mkdirSync2(this.metricsDir, { recursive: true });
|
|
241
|
-
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
242
|
-
}
|
|
291
|
+
serialize() {
|
|
292
|
+
const data = {
|
|
293
|
+
version: 1,
|
|
294
|
+
findings: [...this.findings.values()]
|
|
295
|
+
};
|
|
296
|
+
return JSON.stringify(data);
|
|
243
297
|
}
|
|
244
298
|
};
|
|
245
299
|
|
|
@@ -825,166 +879,20 @@ function createDefaultScanner() {
|
|
|
825
879
|
return scanner;
|
|
826
880
|
}
|
|
827
881
|
|
|
828
|
-
// src/
|
|
829
|
-
var
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
/^\/__nextjs/
|
|
834
|
-
];
|
|
835
|
-
function isStaticPath(urlPath) {
|
|
836
|
-
return STATIC_PATTERNS.some((p) => p.test(urlPath));
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// src/store/request-store.ts
|
|
840
|
-
function flattenHeaders(headers) {
|
|
841
|
-
const flat = {};
|
|
842
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
843
|
-
if (value === void 0) continue;
|
|
844
|
-
flat[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
845
|
-
}
|
|
846
|
-
return flat;
|
|
847
|
-
}
|
|
848
|
-
var RequestStore = class {
|
|
849
|
-
constructor(maxEntries = MAX_REQUEST_ENTRIES) {
|
|
850
|
-
this.maxEntries = maxEntries;
|
|
851
|
-
}
|
|
852
|
-
requests = [];
|
|
853
|
-
listeners = [];
|
|
854
|
-
capture(input) {
|
|
855
|
-
const url = input.url;
|
|
856
|
-
const path = url.split("?")[0];
|
|
857
|
-
let requestBodyStr = null;
|
|
858
|
-
if (input.requestBody && input.requestBody.length > 0) {
|
|
859
|
-
requestBodyStr = input.requestBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
860
|
-
}
|
|
861
|
-
let responseBodyStr = null;
|
|
862
|
-
if (input.responseBody && input.responseBody.length > 0) {
|
|
863
|
-
const ct = input.responseContentType;
|
|
864
|
-
if (ct.includes("json") || ct.includes("text") || ct.includes("html")) {
|
|
865
|
-
responseBodyStr = input.responseBody.subarray(0, input.config.maxBodyCapture).toString("utf-8");
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
const entry = {
|
|
869
|
-
id: input.requestId,
|
|
870
|
-
method: input.method,
|
|
871
|
-
url,
|
|
872
|
-
path,
|
|
873
|
-
headers: flattenHeaders(input.requestHeaders),
|
|
874
|
-
requestBody: requestBodyStr,
|
|
875
|
-
statusCode: input.statusCode,
|
|
876
|
-
responseHeaders: flattenHeaders(input.responseHeaders),
|
|
877
|
-
responseBody: responseBodyStr,
|
|
878
|
-
startedAt: input.startTime,
|
|
879
|
-
durationMs: Math.round((input.endTime ?? performance.now()) - input.startTime),
|
|
880
|
-
responseSize: input.responseBody?.length ?? 0,
|
|
881
|
-
isStatic: isStaticPath(path)
|
|
882
|
-
};
|
|
883
|
-
this.requests.push(entry);
|
|
884
|
-
if (this.requests.length > this.maxEntries) {
|
|
885
|
-
this.requests.shift();
|
|
886
|
-
}
|
|
887
|
-
for (const fn of this.listeners) {
|
|
888
|
-
fn(entry);
|
|
889
|
-
}
|
|
890
|
-
return entry;
|
|
891
|
-
}
|
|
892
|
-
getAll() {
|
|
893
|
-
return this.requests;
|
|
894
|
-
}
|
|
895
|
-
clear() {
|
|
896
|
-
this.requests.length = 0;
|
|
882
|
+
// src/core/disposable.ts
|
|
883
|
+
var SubscriptionBag = class {
|
|
884
|
+
items = [];
|
|
885
|
+
add(teardown) {
|
|
886
|
+
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
897
887
|
}
|
|
898
|
-
|
|
899
|
-
this.
|
|
900
|
-
|
|
901
|
-
offRequest(fn) {
|
|
902
|
-
const idx = this.listeners.indexOf(fn);
|
|
903
|
-
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
888
|
+
dispose() {
|
|
889
|
+
for (const d of this.items) d.dispose();
|
|
890
|
+
this.items.length = 0;
|
|
904
891
|
}
|
|
905
892
|
};
|
|
906
893
|
|
|
907
|
-
// src/store/request-log.ts
|
|
908
|
-
var defaultStore = new RequestStore();
|
|
909
|
-
var getRequests = () => defaultStore.getAll();
|
|
910
|
-
var onRequest = (fn) => defaultStore.onRequest(fn);
|
|
911
|
-
var offRequest = (fn) => defaultStore.offRequest(fn);
|
|
912
|
-
|
|
913
|
-
// src/store/telemetry-store.ts
|
|
914
|
-
import { randomUUID } from "crypto";
|
|
915
|
-
var TelemetryStore = class {
|
|
916
|
-
constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
|
|
917
|
-
this.maxEntries = maxEntries;
|
|
918
|
-
}
|
|
919
|
-
entries = [];
|
|
920
|
-
listeners = [];
|
|
921
|
-
add(data) {
|
|
922
|
-
const entry = { id: randomUUID(), ...data };
|
|
923
|
-
this.entries.push(entry);
|
|
924
|
-
if (this.entries.length > this.maxEntries) this.entries.shift();
|
|
925
|
-
for (const fn of this.listeners) fn(entry);
|
|
926
|
-
return entry;
|
|
927
|
-
}
|
|
928
|
-
getAll() {
|
|
929
|
-
return this.entries;
|
|
930
|
-
}
|
|
931
|
-
getByRequest(requestId) {
|
|
932
|
-
return this.entries.filter((e) => e.parentRequestId === requestId);
|
|
933
|
-
}
|
|
934
|
-
clear() {
|
|
935
|
-
this.entries.length = 0;
|
|
936
|
-
}
|
|
937
|
-
onEntry(fn) {
|
|
938
|
-
this.listeners.push(fn);
|
|
939
|
-
}
|
|
940
|
-
offEntry(fn) {
|
|
941
|
-
const idx = this.listeners.indexOf(fn);
|
|
942
|
-
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
// src/store/fetch-store.ts
|
|
947
|
-
var FetchStore = class extends TelemetryStore {
|
|
948
|
-
};
|
|
949
|
-
var defaultFetchStore = new FetchStore();
|
|
950
|
-
|
|
951
|
-
// src/store/log-store.ts
|
|
952
|
-
var LogStore = class extends TelemetryStore {
|
|
953
|
-
};
|
|
954
|
-
var defaultLogStore = new LogStore();
|
|
955
|
-
|
|
956
|
-
// src/store/error-store.ts
|
|
957
|
-
var ErrorStore = class extends TelemetryStore {
|
|
958
|
-
};
|
|
959
|
-
var defaultErrorStore = new ErrorStore();
|
|
960
|
-
|
|
961
|
-
// src/store/query-store.ts
|
|
962
|
-
var QueryStore = class extends TelemetryStore {
|
|
963
|
-
};
|
|
964
|
-
var defaultQueryStore = new QueryStore();
|
|
965
|
-
|
|
966
|
-
// src/store/metrics/metrics-store.ts
|
|
967
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
968
|
-
|
|
969
|
-
// src/utils/endpoint.ts
|
|
970
|
-
function getEndpointKey(method, path) {
|
|
971
|
-
return `${method} ${path}`;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// src/store/metrics/persistence.ts
|
|
975
|
-
import {
|
|
976
|
-
readFileSync as readFileSync3,
|
|
977
|
-
writeFileSync as writeFileSync3,
|
|
978
|
-
mkdirSync as mkdirSync3,
|
|
979
|
-
existsSync as existsSync3,
|
|
980
|
-
unlinkSync,
|
|
981
|
-
renameSync as renameSync2
|
|
982
|
-
} from "fs";
|
|
983
|
-
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
984
|
-
import { resolve as resolve3 } from "path";
|
|
985
|
-
|
|
986
894
|
// src/analysis/group.ts
|
|
987
|
-
import { randomUUID
|
|
895
|
+
import { randomUUID } from "crypto";
|
|
988
896
|
|
|
989
897
|
// src/analysis/categorize.ts
|
|
990
898
|
function detectCategory(req) {
|
|
@@ -1280,7 +1188,7 @@ function buildFlow(rawRequests) {
|
|
|
1280
1188
|
const redundancyPct = nonStaticCount > 0 ? Math.round(duplicateCount / nonStaticCount * 100) : 0;
|
|
1281
1189
|
const sourcePage = getDominantSourcePage(rawRequests);
|
|
1282
1190
|
return {
|
|
1283
|
-
id:
|
|
1191
|
+
id: randomUUID(),
|
|
1284
1192
|
label: deriveFlowLabel(requests, sourcePage),
|
|
1285
1193
|
requests,
|
|
1286
1194
|
startTime,
|
|
@@ -1352,6 +1260,15 @@ function groupBy(items, keyFn) {
|
|
|
1352
1260
|
return map;
|
|
1353
1261
|
}
|
|
1354
1262
|
|
|
1263
|
+
// src/utils/endpoint.ts
|
|
1264
|
+
function getEndpointKey(method, path) {
|
|
1265
|
+
return `${method} ${path}`;
|
|
1266
|
+
}
|
|
1267
|
+
var ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
1268
|
+
function extractEndpointFromDesc(desc) {
|
|
1269
|
+
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1355
1272
|
// src/instrument/adapters/normalize.ts
|
|
1356
1273
|
function normalizeSQL(sql) {
|
|
1357
1274
|
if (!sql) return { op: "OTHER", table: "" };
|
|
@@ -1410,6 +1327,23 @@ function createEndpointGroup() {
|
|
|
1410
1327
|
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
1411
1328
|
};
|
|
1412
1329
|
}
|
|
1330
|
+
function windowByEndpoint(requests) {
|
|
1331
|
+
const byEndpoint = /* @__PURE__ */ new Map();
|
|
1332
|
+
for (const r of requests) {
|
|
1333
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
1334
|
+
let list = byEndpoint.get(ep);
|
|
1335
|
+
if (!list) {
|
|
1336
|
+
list = [];
|
|
1337
|
+
byEndpoint.set(ep, list);
|
|
1338
|
+
}
|
|
1339
|
+
list.push(r);
|
|
1340
|
+
}
|
|
1341
|
+
const windowed = [];
|
|
1342
|
+
for (const [, reqs] of byEndpoint) {
|
|
1343
|
+
windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
|
|
1344
|
+
}
|
|
1345
|
+
return windowed;
|
|
1346
|
+
}
|
|
1413
1347
|
function prepareContext(ctx) {
|
|
1414
1348
|
const nonStatic = ctx.requests.filter(
|
|
1415
1349
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -1417,8 +1351,9 @@ function prepareContext(ctx) {
|
|
|
1417
1351
|
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
1418
1352
|
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
1419
1353
|
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
1354
|
+
const recent = windowByEndpoint(nonStatic);
|
|
1420
1355
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
1421
|
-
for (const r of
|
|
1356
|
+
for (const r of recent) {
|
|
1422
1357
|
const ep = getEndpointKey(r.method, r.path);
|
|
1423
1358
|
let g = endpointGroups.get(ep);
|
|
1424
1359
|
if (!g) {
|
|
@@ -1990,56 +1925,101 @@ function computeInsights(ctx) {
|
|
|
1990
1925
|
return createDefaultInsightRunner().run(ctx);
|
|
1991
1926
|
}
|
|
1992
1927
|
|
|
1928
|
+
// src/analysis/insight-tracker.ts
|
|
1929
|
+
function computeInsightKey(insight) {
|
|
1930
|
+
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
1931
|
+
return `${insight.type}:${identifier}`;
|
|
1932
|
+
}
|
|
1933
|
+
var InsightTracker = class {
|
|
1934
|
+
tracked = /* @__PURE__ */ new Map();
|
|
1935
|
+
reconcile(current) {
|
|
1936
|
+
const currentKeys = /* @__PURE__ */ new Set();
|
|
1937
|
+
const now = Date.now();
|
|
1938
|
+
for (const insight of current) {
|
|
1939
|
+
const key = computeInsightKey(insight);
|
|
1940
|
+
currentKeys.add(key);
|
|
1941
|
+
const existing = this.tracked.get(key);
|
|
1942
|
+
if (existing) {
|
|
1943
|
+
existing.insight = insight;
|
|
1944
|
+
existing.lastSeenAt = now;
|
|
1945
|
+
existing.consecutiveAbsences = 0;
|
|
1946
|
+
if (existing.state === "resolved") {
|
|
1947
|
+
existing.state = "open";
|
|
1948
|
+
existing.resolvedAt = null;
|
|
1949
|
+
}
|
|
1950
|
+
} else {
|
|
1951
|
+
this.tracked.set(key, {
|
|
1952
|
+
key,
|
|
1953
|
+
state: "open",
|
|
1954
|
+
insight,
|
|
1955
|
+
firstSeenAt: now,
|
|
1956
|
+
lastSeenAt: now,
|
|
1957
|
+
resolvedAt: null,
|
|
1958
|
+
consecutiveAbsences: 0
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
for (const [key, stateful] of this.tracked) {
|
|
1963
|
+
if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
|
|
1964
|
+
stateful.consecutiveAbsences++;
|
|
1965
|
+
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
1966
|
+
stateful.state = "resolved";
|
|
1967
|
+
stateful.resolvedAt = now;
|
|
1968
|
+
}
|
|
1969
|
+
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
1970
|
+
this.tracked.delete(key);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return [...this.tracked.values()];
|
|
1974
|
+
}
|
|
1975
|
+
getAll() {
|
|
1976
|
+
return [...this.tracked.values()];
|
|
1977
|
+
}
|
|
1978
|
+
clear() {
|
|
1979
|
+
this.tracked.clear();
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1993
1983
|
// src/analysis/engine.ts
|
|
1994
1984
|
var AnalysisEngine = class {
|
|
1995
|
-
constructor(
|
|
1996
|
-
this.
|
|
1997
|
-
this.findingStore = findingStore;
|
|
1985
|
+
constructor(registry, debounceMs = 300) {
|
|
1986
|
+
this.registry = registry;
|
|
1998
1987
|
this.debounceMs = debounceMs;
|
|
1999
1988
|
this.scanner = createDefaultScanner();
|
|
2000
|
-
this.boundRequestListener = () => this.scheduleRecompute();
|
|
2001
|
-
this.boundQueryListener = () => this.scheduleRecompute();
|
|
2002
|
-
this.boundErrorListener = () => this.scheduleRecompute();
|
|
2003
|
-
this.boundLogListener = () => this.scheduleRecompute();
|
|
2004
1989
|
}
|
|
2005
1990
|
scanner;
|
|
1991
|
+
insightTracker = new InsightTracker();
|
|
2006
1992
|
cachedInsights = [];
|
|
2007
1993
|
cachedFindings = [];
|
|
1994
|
+
cachedStatefulInsights = [];
|
|
2008
1995
|
debounceTimer = null;
|
|
2009
|
-
|
|
2010
|
-
boundRequestListener;
|
|
2011
|
-
boundQueryListener;
|
|
2012
|
-
boundErrorListener;
|
|
2013
|
-
boundLogListener;
|
|
1996
|
+
subs = new SubscriptionBag();
|
|
2014
1997
|
start() {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1998
|
+
const bus = this.registry.get("event-bus");
|
|
1999
|
+
this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
|
|
2000
|
+
this.subs.add(bus.on("telemetry:query", () => this.scheduleRecompute()));
|
|
2001
|
+
this.subs.add(bus.on("telemetry:error", () => this.scheduleRecompute()));
|
|
2002
|
+
this.subs.add(bus.on("telemetry:log", () => this.scheduleRecompute()));
|
|
2019
2003
|
}
|
|
2020
2004
|
stop() {
|
|
2021
|
-
|
|
2022
|
-
defaultQueryStore.offEntry(this.boundQueryListener);
|
|
2023
|
-
defaultErrorStore.offEntry(this.boundErrorListener);
|
|
2024
|
-
defaultLogStore.offEntry(this.boundLogListener);
|
|
2005
|
+
this.subs.dispose();
|
|
2025
2006
|
if (this.debounceTimer) {
|
|
2026
2007
|
clearTimeout(this.debounceTimer);
|
|
2027
2008
|
this.debounceTimer = null;
|
|
2028
2009
|
}
|
|
2029
2010
|
}
|
|
2030
|
-
onUpdate(fn) {
|
|
2031
|
-
this.listeners.push(fn);
|
|
2032
|
-
}
|
|
2033
|
-
offUpdate(fn) {
|
|
2034
|
-
const idx = this.listeners.indexOf(fn);
|
|
2035
|
-
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
2036
|
-
}
|
|
2037
2011
|
getInsights() {
|
|
2038
2012
|
return this.cachedInsights;
|
|
2039
2013
|
}
|
|
2040
2014
|
getFindings() {
|
|
2041
2015
|
return this.cachedFindings;
|
|
2042
2016
|
}
|
|
2017
|
+
getStatefulFindings() {
|
|
2018
|
+
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
2019
|
+
}
|
|
2020
|
+
getStatefulInsights() {
|
|
2021
|
+
return this.cachedStatefulInsights;
|
|
2022
|
+
}
|
|
2043
2023
|
scheduleRecompute() {
|
|
2044
2024
|
if (this.debounceTimer) return;
|
|
2045
2025
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -2048,18 +2028,19 @@ var AnalysisEngine = class {
|
|
|
2048
2028
|
}, this.debounceMs);
|
|
2049
2029
|
}
|
|
2050
2030
|
recompute() {
|
|
2051
|
-
const requests =
|
|
2052
|
-
const queries =
|
|
2053
|
-
const errors =
|
|
2054
|
-
const logs =
|
|
2055
|
-
const fetches =
|
|
2031
|
+
const requests = this.registry.get("request-store").getAll();
|
|
2032
|
+
const queries = this.registry.get("query-store").getAll();
|
|
2033
|
+
const errors = this.registry.get("error-store").getAll();
|
|
2034
|
+
const logs = this.registry.get("log-store").getAll();
|
|
2035
|
+
const fetches = this.registry.get("fetch-store").getAll();
|
|
2056
2036
|
const flows = groupRequestsIntoFlows(requests);
|
|
2057
2037
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
2058
|
-
if (this.
|
|
2038
|
+
if (this.registry.has("finding-store")) {
|
|
2039
|
+
const findingStore = this.registry.get("finding-store");
|
|
2059
2040
|
for (const finding of this.cachedFindings) {
|
|
2060
|
-
|
|
2041
|
+
findingStore.upsert(finding, "passive");
|
|
2061
2042
|
}
|
|
2062
|
-
|
|
2043
|
+
findingStore.reconcilePassive(this.cachedFindings);
|
|
2063
2044
|
}
|
|
2064
2045
|
this.cachedInsights = computeInsights({
|
|
2065
2046
|
requests,
|
|
@@ -2067,20 +2048,22 @@ var AnalysisEngine = class {
|
|
|
2067
2048
|
errors,
|
|
2068
2049
|
flows,
|
|
2069
2050
|
fetches,
|
|
2070
|
-
previousMetrics: this.
|
|
2051
|
+
previousMetrics: this.registry.get("metrics-store").getAll(),
|
|
2071
2052
|
securityFindings: this.cachedFindings
|
|
2072
2053
|
});
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2054
|
+
this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
|
|
2055
|
+
const update = {
|
|
2056
|
+
insights: this.cachedInsights,
|
|
2057
|
+
findings: this.cachedFindings,
|
|
2058
|
+
statefulFindings: this.getStatefulFindings(),
|
|
2059
|
+
statefulInsights: this.cachedStatefulInsights
|
|
2060
|
+
};
|
|
2061
|
+
this.registry.get("event-bus").emit("analysis:updated", update);
|
|
2079
2062
|
}
|
|
2080
2063
|
};
|
|
2081
2064
|
|
|
2082
2065
|
// src/index.ts
|
|
2083
|
-
var VERSION = "0.8.
|
|
2066
|
+
var VERSION = "0.8.2";
|
|
2084
2067
|
export {
|
|
2085
2068
|
AdapterRegistry,
|
|
2086
2069
|
AnalysisEngine,
|