brakit 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +26 -9
- package/dist/api.js +221 -112
- package/dist/bin/brakit.js +598 -282
- package/dist/dashboard.html +2652 -0
- package/dist/mcp/server.js +195 -90
- package/dist/runtime/index.js +1045 -386
- package/package.json +3 -2
package/dist/api.js
CHANGED
|
@@ -1,12 +1,83 @@
|
|
|
1
1
|
// src/store/finding-store.ts
|
|
2
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2
3
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
3
4
|
import { resolve as resolve2 } from "path";
|
|
4
5
|
|
|
6
|
+
// src/utils/fs.ts
|
|
7
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
async function fileExists(path) {
|
|
11
|
+
try {
|
|
12
|
+
await access(path);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function ensureGitignore(dir, entry) {
|
|
19
|
+
try {
|
|
20
|
+
const gitignorePath = resolve(dir, "../.gitignore");
|
|
21
|
+
if (existsSync(gitignorePath)) {
|
|
22
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
23
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
24
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
25
|
+
} else {
|
|
26
|
+
writeFileSync(gitignorePath, entry + "\n");
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function ensureGitignoreAsync(dir, entry) {
|
|
32
|
+
try {
|
|
33
|
+
const gitignorePath = resolve(dir, "../.gitignore");
|
|
34
|
+
if (await fileExists(gitignorePath)) {
|
|
35
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
36
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
37
|
+
await writeFile(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
38
|
+
} else {
|
|
39
|
+
await writeFile(gitignorePath, entry + "\n");
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
5
45
|
// src/constants/routes.ts
|
|
6
46
|
var DASHBOARD_PREFIX = "/__brakit";
|
|
47
|
+
var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
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);
|
|
7
76
|
|
|
8
77
|
// src/constants/limits.ts
|
|
9
|
-
var
|
|
78
|
+
var ANALYSIS_DEBOUNCE_MS = 300;
|
|
79
|
+
var FINDING_ID_HASH_LENGTH = 16;
|
|
80
|
+
var FINDINGS_DATA_VERSION = 1;
|
|
10
81
|
|
|
11
82
|
// src/constants/thresholds.ts
|
|
12
83
|
var FLOW_GAP_MS = 5e3;
|
|
@@ -44,15 +115,8 @@ var METRICS_DIR = ".brakit";
|
|
|
44
115
|
var FINDINGS_FILE = ".brakit/findings.json";
|
|
45
116
|
var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
46
117
|
|
|
47
|
-
// src/constants/
|
|
48
|
-
var
|
|
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
|
-
};
|
|
118
|
+
// src/constants/network.ts
|
|
119
|
+
var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
56
120
|
|
|
57
121
|
// src/utils/atomic-writer.ts
|
|
58
122
|
import {
|
|
@@ -63,38 +127,25 @@ import {
|
|
|
63
127
|
} from "fs";
|
|
64
128
|
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
65
129
|
|
|
66
|
-
// src/utils/fs.ts
|
|
67
|
-
import { access } from "fs/promises";
|
|
68
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
69
|
-
import { resolve } from "path";
|
|
70
|
-
async function fileExists(path) {
|
|
71
|
-
try {
|
|
72
|
-
await access(path);
|
|
73
|
-
return true;
|
|
74
|
-
} catch {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function ensureGitignore(dir, entry) {
|
|
79
|
-
try {
|
|
80
|
-
const gitignorePath = resolve(dir, "../.gitignore");
|
|
81
|
-
if (existsSync(gitignorePath)) {
|
|
82
|
-
const content = readFileSync(gitignorePath, "utf-8");
|
|
83
|
-
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
84
|
-
writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
85
|
-
} else {
|
|
86
|
-
writeFileSync(gitignorePath, entry + "\n");
|
|
87
|
-
}
|
|
88
|
-
} catch {
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
130
|
// src/utils/log.ts
|
|
93
131
|
var PREFIX = "[brakit]";
|
|
94
132
|
function brakitWarn(message) {
|
|
95
133
|
process.stderr.write(`${PREFIX} ${message}
|
|
96
134
|
`);
|
|
97
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
|
+
}
|
|
98
149
|
|
|
99
150
|
// src/utils/atomic-writer.ts
|
|
100
151
|
var AtomicWriter = class {
|
|
@@ -111,7 +162,7 @@ var AtomicWriter = class {
|
|
|
111
162
|
writeFileSync2(this.tmpPath, content);
|
|
112
163
|
renameSync(this.tmpPath, this.opts.filePath);
|
|
113
164
|
} catch (err) {
|
|
114
|
-
brakitWarn(`failed to save ${this.opts.label}: ${err
|
|
165
|
+
brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
|
|
115
166
|
}
|
|
116
167
|
}
|
|
117
168
|
async writeAsync(content) {
|
|
@@ -125,13 +176,14 @@ var AtomicWriter = class {
|
|
|
125
176
|
await writeFile2(this.tmpPath, content);
|
|
126
177
|
await rename(this.tmpPath, this.opts.filePath);
|
|
127
178
|
} catch (err) {
|
|
128
|
-
brakitWarn(`failed to save ${this.opts.label}: ${err
|
|
179
|
+
brakitWarn(`failed to save ${this.opts.label}: ${getErrorMessage(err)}`);
|
|
129
180
|
} finally {
|
|
130
181
|
this.writing = false;
|
|
131
182
|
if (this.pendingContent !== null) {
|
|
132
183
|
const next = this.pendingContent;
|
|
133
184
|
this.pendingContent = null;
|
|
134
|
-
this.writeAsync(next)
|
|
185
|
+
this.writeAsync(next).catch(() => {
|
|
186
|
+
});
|
|
135
187
|
}
|
|
136
188
|
}
|
|
137
189
|
}
|
|
@@ -144,10 +196,10 @@ var AtomicWriter = class {
|
|
|
144
196
|
}
|
|
145
197
|
}
|
|
146
198
|
async ensureDirAsync() {
|
|
147
|
-
if (!
|
|
199
|
+
if (!await fileExists(this.opts.dir)) {
|
|
148
200
|
await mkdir(this.opts.dir, { recursive: true });
|
|
149
201
|
if (this.opts.gitignoreEntry) {
|
|
150
|
-
|
|
202
|
+
await ensureGitignoreAsync(this.opts.dir, this.opts.gitignoreEntry);
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
}
|
|
@@ -157,7 +209,11 @@ var AtomicWriter = class {
|
|
|
157
209
|
import { createHash } from "crypto";
|
|
158
210
|
function computeFindingId(finding) {
|
|
159
211
|
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
160
|
-
return createHash("sha256").update(key).digest("hex").slice(0,
|
|
212
|
+
return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
|
|
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);
|
|
161
217
|
}
|
|
162
218
|
|
|
163
219
|
// src/store/finding-store.ts
|
|
@@ -172,7 +228,6 @@ var FindingStore = class {
|
|
|
172
228
|
gitignoreEntry: METRICS_DIR,
|
|
173
229
|
label: "findings"
|
|
174
230
|
});
|
|
175
|
-
this.load();
|
|
176
231
|
}
|
|
177
232
|
findings = /* @__PURE__ */ new Map();
|
|
178
233
|
flushTimer = null;
|
|
@@ -180,6 +235,8 @@ var FindingStore = class {
|
|
|
180
235
|
writer;
|
|
181
236
|
findingsPath;
|
|
182
237
|
start() {
|
|
238
|
+
this.loadAsync().catch(() => {
|
|
239
|
+
});
|
|
183
240
|
this.flushTimer = setInterval(
|
|
184
241
|
() => this.flush(),
|
|
185
242
|
FINDINGS_FLUSH_INTERVAL_MS
|
|
@@ -216,7 +273,9 @@ var FindingStore = class {
|
|
|
216
273
|
firstSeenAt: now,
|
|
217
274
|
lastSeenAt: now,
|
|
218
275
|
resolvedAt: null,
|
|
219
|
-
occurrences: 1
|
|
276
|
+
occurrences: 1,
|
|
277
|
+
aiStatus: null,
|
|
278
|
+
aiNotes: null
|
|
220
279
|
};
|
|
221
280
|
this.findings.set(id, stateful);
|
|
222
281
|
this.dirty = true;
|
|
@@ -232,6 +291,17 @@ var FindingStore = class {
|
|
|
232
291
|
this.dirty = true;
|
|
233
292
|
return true;
|
|
234
293
|
}
|
|
294
|
+
reportFix(findingId, status, notes) {
|
|
295
|
+
const finding = this.findings.get(findingId);
|
|
296
|
+
if (!finding) return false;
|
|
297
|
+
finding.aiStatus = status;
|
|
298
|
+
finding.aiNotes = notes;
|
|
299
|
+
if (status === "fixed") {
|
|
300
|
+
finding.state = "fixing";
|
|
301
|
+
}
|
|
302
|
+
this.dirty = true;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
235
305
|
/**
|
|
236
306
|
* Reconcile passive findings against the current analysis results.
|
|
237
307
|
*
|
|
@@ -244,7 +314,7 @@ var FindingStore = class {
|
|
|
244
314
|
reconcilePassive(currentFindings) {
|
|
245
315
|
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
246
316
|
for (const [id, stateful] of this.findings) {
|
|
247
|
-
if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
|
|
317
|
+
if (stateful.source === "passive" && (stateful.state === "open" || stateful.state === "fixing") && !currentIds.has(id)) {
|
|
248
318
|
stateful.state = "resolved";
|
|
249
319
|
stateful.resolvedAt = Date.now();
|
|
250
320
|
this.dirty = true;
|
|
@@ -264,18 +334,35 @@ var FindingStore = class {
|
|
|
264
334
|
this.findings.clear();
|
|
265
335
|
this.dirty = true;
|
|
266
336
|
}
|
|
267
|
-
|
|
337
|
+
async loadAsync() {
|
|
338
|
+
try {
|
|
339
|
+
if (await fileExists(this.findingsPath)) {
|
|
340
|
+
const raw = await readFile2(this.findingsPath, "utf-8");
|
|
341
|
+
const parsed = JSON.parse(raw);
|
|
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
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Sync load for tests only — not used in production paths. */
|
|
353
|
+
loadSync() {
|
|
268
354
|
try {
|
|
269
355
|
if (existsSync3(this.findingsPath)) {
|
|
270
356
|
const raw = readFileSync2(this.findingsPath, "utf-8");
|
|
271
357
|
const parsed = JSON.parse(raw);
|
|
272
|
-
if (parsed?.version ===
|
|
358
|
+
if (parsed?.version === FINDINGS_DATA_VERSION && Array.isArray(parsed.findings)) {
|
|
273
359
|
for (const f of parsed.findings) {
|
|
274
360
|
this.findings.set(f.findingId, f);
|
|
275
361
|
}
|
|
276
362
|
}
|
|
277
363
|
}
|
|
278
|
-
} catch {
|
|
364
|
+
} catch (err) {
|
|
365
|
+
brakitDebug(`FindingStore: could not load findings file, starting fresh: ${err}`);
|
|
279
366
|
}
|
|
280
367
|
}
|
|
281
368
|
flush() {
|
|
@@ -290,7 +377,7 @@ var FindingStore = class {
|
|
|
290
377
|
}
|
|
291
378
|
serialize() {
|
|
292
379
|
const data = {
|
|
293
|
-
version:
|
|
380
|
+
version: FINDINGS_DATA_VERSION,
|
|
294
381
|
findings: [...this.findings.values()]
|
|
295
382
|
};
|
|
296
383
|
return JSON.stringify(data);
|
|
@@ -298,8 +385,9 @@ var FindingStore = class {
|
|
|
298
385
|
};
|
|
299
386
|
|
|
300
387
|
// src/detect/project.ts
|
|
301
|
-
import { readFile as
|
|
302
|
-
import {
|
|
388
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
389
|
+
import { existsSync as existsSync4 } from "fs";
|
|
390
|
+
import { join, relative } from "path";
|
|
303
391
|
var FRAMEWORKS = [
|
|
304
392
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
305
393
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -309,22 +397,14 @@ var FRAMEWORKS = [
|
|
|
309
397
|
];
|
|
310
398
|
async function detectProject(rootDir) {
|
|
311
399
|
const pkgPath = join(rootDir, "package.json");
|
|
312
|
-
const raw = await
|
|
400
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
313
401
|
const pkg = JSON.parse(raw);
|
|
314
402
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (allDeps[f.dep]) {
|
|
321
|
-
framework = f.name;
|
|
322
|
-
devCommand = f.devCmd;
|
|
323
|
-
devBin = join(rootDir, "node_modules", ".bin", f.bin);
|
|
324
|
-
defaultPort = f.defaultPort;
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
403
|
+
const framework = detectFrameworkFromDeps(allDeps);
|
|
404
|
+
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
405
|
+
const devCommand = matched?.devCmd ?? "";
|
|
406
|
+
const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
407
|
+
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
328
408
|
const packageManager = await detectPackageManager(rootDir);
|
|
329
409
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
330
410
|
}
|
|
@@ -336,6 +416,12 @@ async function detectPackageManager(rootDir) {
|
|
|
336
416
|
if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
|
|
337
417
|
return "unknown";
|
|
338
418
|
}
|
|
419
|
+
function detectFrameworkFromDeps(allDeps) {
|
|
420
|
+
for (const f of FRAMEWORKS) {
|
|
421
|
+
if (allDeps[f.dep]) return f.name;
|
|
422
|
+
}
|
|
423
|
+
return "unknown";
|
|
424
|
+
}
|
|
339
425
|
|
|
340
426
|
// src/instrument/adapter-registry.ts
|
|
341
427
|
var AdapterRegistry = class {
|
|
@@ -373,11 +459,11 @@ var AdapterRegistry = class {
|
|
|
373
459
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
374
460
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
375
461
|
var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
376
|
-
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections
|
|
462
|
+
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
|
|
377
463
|
var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
378
464
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
379
|
-
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
380
|
-
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
465
|
+
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
466
|
+
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
381
467
|
var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
382
468
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
383
469
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -389,9 +475,9 @@ var RULE_HINTS = {
|
|
|
389
475
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
390
476
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
391
477
|
"error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
|
|
478
|
+
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
392
479
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
393
480
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
394
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
395
481
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
396
482
|
};
|
|
397
483
|
|
|
@@ -761,48 +847,47 @@ function hasInternalIds(obj) {
|
|
|
761
847
|
}
|
|
762
848
|
return false;
|
|
763
849
|
}
|
|
764
|
-
function
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
850
|
+
function detectEchoPII(method, reqBody, target) {
|
|
851
|
+
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
852
|
+
const reqEmails = findEmails(reqBody);
|
|
853
|
+
if (reqEmails.length === 0) return null;
|
|
854
|
+
const resEmails = findEmails(target);
|
|
855
|
+
const echoed = reqEmails.filter((e) => resEmails.includes(e));
|
|
856
|
+
if (echoed.length === 0) return null;
|
|
857
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
858
|
+
if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
|
|
859
|
+
return { reason: "echo", emailCount: echoed.length };
|
|
778
860
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
function detectFullRecordPII(target) {
|
|
864
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
865
|
+
const fields = topLevelFieldCount(target);
|
|
866
|
+
if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
|
|
867
|
+
const emails = findEmails(target);
|
|
868
|
+
if (emails.length === 0) return null;
|
|
869
|
+
return { reason: "full-record", emailCount: emails.length };
|
|
870
|
+
}
|
|
871
|
+
function detectListPII(target) {
|
|
872
|
+
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
873
|
+
let itemsWithEmail = 0;
|
|
874
|
+
for (let i = 0; i < Math.min(target.length, 10); i++) {
|
|
875
|
+
const item = target[i];
|
|
876
|
+
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
877
|
+
itemsWithEmail++;
|
|
786
878
|
}
|
|
787
879
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (item && typeof item === "object") {
|
|
793
|
-
const emails = findEmails(item);
|
|
794
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
798
|
-
const first = target[0];
|
|
799
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
800
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
880
|
+
if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
|
|
881
|
+
const first = target[0];
|
|
882
|
+
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
883
|
+
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
803
884
|
}
|
|
804
885
|
return null;
|
|
805
886
|
}
|
|
887
|
+
function detectPII(method, reqBody, resBody) {
|
|
888
|
+
const target = unwrapResponse(resBody);
|
|
889
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
890
|
+
}
|
|
806
891
|
var REASON_LABELS = {
|
|
807
892
|
echo: "echoes back PII from the request body",
|
|
808
893
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -1930,8 +2015,12 @@ function computeInsightKey(insight) {
|
|
|
1930
2015
|
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
1931
2016
|
return `${insight.type}:${identifier}`;
|
|
1932
2017
|
}
|
|
2018
|
+
function enrichedIdFromInsight(insight) {
|
|
2019
|
+
return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
|
|
2020
|
+
}
|
|
1933
2021
|
var InsightTracker = class {
|
|
1934
2022
|
tracked = /* @__PURE__ */ new Map();
|
|
2023
|
+
enrichedIndex = /* @__PURE__ */ new Map();
|
|
1935
2024
|
reconcile(current) {
|
|
1936
2025
|
const currentKeys = /* @__PURE__ */ new Set();
|
|
1937
2026
|
const now = Date.now();
|
|
@@ -1939,6 +2028,7 @@ var InsightTracker = class {
|
|
|
1939
2028
|
const key = computeInsightKey(insight);
|
|
1940
2029
|
currentKeys.add(key);
|
|
1941
2030
|
const existing = this.tracked.get(key);
|
|
2031
|
+
this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
|
|
1942
2032
|
if (existing) {
|
|
1943
2033
|
existing.insight = insight;
|
|
1944
2034
|
existing.lastSeenAt = now;
|
|
@@ -1955,34 +2045,50 @@ var InsightTracker = class {
|
|
|
1955
2045
|
firstSeenAt: now,
|
|
1956
2046
|
lastSeenAt: now,
|
|
1957
2047
|
resolvedAt: null,
|
|
1958
|
-
consecutiveAbsences: 0
|
|
2048
|
+
consecutiveAbsences: 0,
|
|
2049
|
+
aiStatus: null,
|
|
2050
|
+
aiNotes: null
|
|
1959
2051
|
});
|
|
1960
2052
|
}
|
|
1961
2053
|
}
|
|
1962
|
-
for (const [
|
|
1963
|
-
if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
|
|
2054
|
+
for (const [, stateful] of this.tracked) {
|
|
2055
|
+
if ((stateful.state === "open" || stateful.state === "fixing") && !currentKeys.has(stateful.key)) {
|
|
1964
2056
|
stateful.consecutiveAbsences++;
|
|
1965
2057
|
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
1966
2058
|
stateful.state = "resolved";
|
|
1967
2059
|
stateful.resolvedAt = now;
|
|
1968
2060
|
}
|
|
1969
2061
|
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
1970
|
-
this.tracked.delete(key);
|
|
2062
|
+
this.tracked.delete(stateful.key);
|
|
2063
|
+
this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
|
|
1971
2064
|
}
|
|
1972
2065
|
}
|
|
1973
2066
|
return [...this.tracked.values()];
|
|
1974
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
|
+
}
|
|
1975
2080
|
getAll() {
|
|
1976
2081
|
return [...this.tracked.values()];
|
|
1977
2082
|
}
|
|
1978
2083
|
clear() {
|
|
1979
2084
|
this.tracked.clear();
|
|
2085
|
+
this.enrichedIndex.clear();
|
|
1980
2086
|
}
|
|
1981
2087
|
};
|
|
1982
2088
|
|
|
1983
2089
|
// src/analysis/engine.ts
|
|
1984
2090
|
var AnalysisEngine = class {
|
|
1985
|
-
constructor(registry, debounceMs =
|
|
2091
|
+
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
1986
2092
|
this.registry = registry;
|
|
1987
2093
|
this.debounceMs = debounceMs;
|
|
1988
2094
|
this.scanner = createDefaultScanner();
|
|
@@ -2018,7 +2124,10 @@ var AnalysisEngine = class {
|
|
|
2018
2124
|
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
2019
2125
|
}
|
|
2020
2126
|
getStatefulInsights() {
|
|
2021
|
-
return this.
|
|
2127
|
+
return this.insightTracker.getAll();
|
|
2128
|
+
}
|
|
2129
|
+
reportInsightFix(enrichedId, status, notes) {
|
|
2130
|
+
return this.insightTracker.reportFix(enrichedId, status, notes);
|
|
2022
2131
|
}
|
|
2023
2132
|
scheduleRecompute() {
|
|
2024
2133
|
if (this.debounceTimer) return;
|
|
@@ -2063,7 +2172,7 @@ var AnalysisEngine = class {
|
|
|
2063
2172
|
};
|
|
2064
2173
|
|
|
2065
2174
|
// src/index.ts
|
|
2066
|
-
var VERSION = "0.8.
|
|
2175
|
+
var VERSION = "0.8.5";
|
|
2067
2176
|
export {
|
|
2068
2177
|
AdapterRegistry,
|
|
2069
2178
|
AnalysisEngine,
|