brakit 0.8.4 → 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 +209 -99
- package/dist/bin/brakit.js +509 -218
- package/dist/dashboard.html +2652 -0
- package/dist/mcp/server.js +195 -90
- package/dist/runtime/index.js +854 -388
- package/package.json +3 -2
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IncomingHttpHeaders } from 'node:http';
|
|
2
2
|
|
|
3
|
-
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
|
|
3
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
4
4
|
type FlatHeaders = Record<string, string>;
|
|
5
5
|
interface TracedRequest {
|
|
6
6
|
id: string;
|
|
@@ -19,7 +19,7 @@ interface TracedRequest {
|
|
|
19
19
|
}
|
|
20
20
|
type RequestListener = (req: TracedRequest) => void;
|
|
21
21
|
|
|
22
|
-
type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "custom" | "unknown";
|
|
22
|
+
type Framework = "nextjs" | "remix" | "nuxt" | "vite" | "astro" | "flask" | "fastapi" | "django" | "custom" | "unknown";
|
|
23
23
|
interface DetectedProject {
|
|
24
24
|
framework: Framework;
|
|
25
25
|
devCommand: string;
|
|
@@ -79,7 +79,7 @@ interface TracedError extends TelemetryEntry {
|
|
|
79
79
|
}
|
|
80
80
|
type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
|
|
81
81
|
interface TracedQuery extends TelemetryEntry {
|
|
82
|
-
driver: "pg" | "mysql2" | "prisma" |
|
|
82
|
+
driver: "pg" | "mysql2" | "prisma" | "sdk";
|
|
83
83
|
sql?: string;
|
|
84
84
|
model?: string;
|
|
85
85
|
operation?: string;
|
|
@@ -160,8 +160,11 @@ interface SecurityFinding {
|
|
|
160
160
|
count: number;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
declare const FINDINGS_DATA_VERSION = 1;
|
|
164
|
+
|
|
163
165
|
type FindingState = "open" | "fixing" | "resolved";
|
|
164
166
|
type FindingSource = "passive";
|
|
167
|
+
type AiFixStatus = "fixed" | "wont_fix";
|
|
165
168
|
interface StatefulFinding {
|
|
166
169
|
/** Stable ID derived from rule + endpoint + description hash */
|
|
167
170
|
findingId: string;
|
|
@@ -172,9 +175,13 @@ interface StatefulFinding {
|
|
|
172
175
|
lastSeenAt: number;
|
|
173
176
|
resolvedAt: number | null;
|
|
174
177
|
occurrences: number;
|
|
178
|
+
/** What AI reported after attempting a fix */
|
|
179
|
+
aiStatus: AiFixStatus | null;
|
|
180
|
+
/** AI's summary of what was done or why it can't be fixed */
|
|
181
|
+
aiNotes: string | null;
|
|
175
182
|
}
|
|
176
183
|
interface FindingsData {
|
|
177
|
-
version:
|
|
184
|
+
version: typeof FINDINGS_DATA_VERSION;
|
|
178
185
|
findings: StatefulFinding[];
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -220,7 +227,7 @@ interface PreparedInsightContext extends InsightContext {
|
|
|
220
227
|
endpointGroups: ReadonlyMap<string, EndpointGroup>;
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
type InsightState =
|
|
230
|
+
type InsightState = FindingState;
|
|
224
231
|
interface StatefulInsight {
|
|
225
232
|
key: string;
|
|
226
233
|
state: InsightState;
|
|
@@ -230,6 +237,8 @@ interface StatefulInsight {
|
|
|
230
237
|
resolvedAt: number | null;
|
|
231
238
|
/** Consecutive recompute cycles where the insight was not detected. */
|
|
232
239
|
consecutiveAbsences: number;
|
|
240
|
+
aiStatus: AiFixStatus | null;
|
|
241
|
+
aiNotes: string | null;
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
declare class FindingStore {
|
|
@@ -244,6 +253,7 @@ declare class FindingStore {
|
|
|
244
253
|
stop(): void;
|
|
245
254
|
upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
|
|
246
255
|
transition(findingId: string, state: FindingState): boolean;
|
|
256
|
+
reportFix(findingId: string, status: AiFixStatus, notes: string): boolean;
|
|
247
257
|
/**
|
|
248
258
|
* Reconcile passive findings against the current analysis results.
|
|
249
259
|
*
|
|
@@ -258,7 +268,9 @@ declare class FindingStore {
|
|
|
258
268
|
getByState(state: FindingState): readonly StatefulFinding[];
|
|
259
269
|
get(findingId: string): StatefulFinding | undefined;
|
|
260
270
|
clear(): void;
|
|
261
|
-
private
|
|
271
|
+
private loadAsync;
|
|
272
|
+
/** Sync load for tests only — not used in production paths. */
|
|
273
|
+
loadSync(): void;
|
|
262
274
|
private flush;
|
|
263
275
|
private flushSync;
|
|
264
276
|
private serialize;
|
|
@@ -317,8 +329,8 @@ declare class AdapterRegistry {
|
|
|
317
329
|
}
|
|
318
330
|
|
|
319
331
|
interface AnalysisUpdate {
|
|
320
|
-
insights: Insight[];
|
|
321
|
-
findings: SecurityFinding[];
|
|
332
|
+
insights: readonly Insight[];
|
|
333
|
+
findings: readonly SecurityFinding[];
|
|
322
334
|
statefulFindings: readonly StatefulFinding[];
|
|
323
335
|
statefulInsights: readonly StatefulInsight[];
|
|
324
336
|
}
|
|
@@ -329,7 +341,8 @@ interface ChannelMap {
|
|
|
329
341
|
"telemetry:error": Omit<TracedError, "id">;
|
|
330
342
|
"request:completed": TracedRequest;
|
|
331
343
|
"analysis:updated": AnalysisUpdate;
|
|
332
|
-
"
|
|
344
|
+
"findings:changed": readonly StatefulFinding[];
|
|
345
|
+
"store:cleared": undefined;
|
|
333
346
|
}
|
|
334
347
|
type Listener<T> = (data: T) => void;
|
|
335
348
|
declare class EventBus {
|
|
@@ -362,6 +375,7 @@ interface TelemetryStoreInterface<T extends TelemetryEntry> {
|
|
|
362
375
|
}
|
|
363
376
|
interface RequestStoreInterface {
|
|
364
377
|
capture(input: CaptureInput): TracedRequest;
|
|
378
|
+
add(entry: TracedRequest): void;
|
|
365
379
|
getAll(): readonly TracedRequest[];
|
|
366
380
|
clear(): void;
|
|
367
381
|
}
|
|
@@ -377,6 +391,7 @@ interface MetricsStoreInterface {
|
|
|
377
391
|
interface FindingStoreInterface {
|
|
378
392
|
upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
|
|
379
393
|
transition(findingId: string, state: FindingState): boolean;
|
|
394
|
+
reportFix(findingId: string, status: AiFixStatus, notes: string): boolean;
|
|
380
395
|
reconcilePassive(findings: readonly SecurityFinding[]): void;
|
|
381
396
|
getAll(): readonly StatefulFinding[];
|
|
382
397
|
getByState(state: FindingState): readonly StatefulFinding[];
|
|
@@ -393,6 +408,7 @@ interface AnalysisEngineInterface {
|
|
|
393
408
|
getFindings(): readonly SecurityFinding[];
|
|
394
409
|
getStatefulInsights(): readonly StatefulInsight[];
|
|
395
410
|
getStatefulFindings(): readonly StatefulFinding[];
|
|
411
|
+
reportInsightFix(enrichedId: string, status: AiFixStatus, notes: string): boolean;
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
interface ServiceMap {
|
|
@@ -430,6 +446,7 @@ declare class AnalysisEngine {
|
|
|
430
446
|
getFindings(): readonly SecurityFinding[];
|
|
431
447
|
getStatefulFindings(): readonly StatefulFinding[];
|
|
432
448
|
getStatefulInsights(): readonly StatefulInsight[];
|
|
449
|
+
reportInsightFix(enrichedId: string, status: AiFixStatus, notes: string): boolean;
|
|
433
450
|
private scheduleRecompute;
|
|
434
451
|
recompute(): void;
|
|
435
452
|
}
|
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,9 +385,9 @@ var FindingStore = class {
|
|
|
298
385
|
};
|
|
299
386
|
|
|
300
387
|
// src/detect/project.ts
|
|
301
|
-
import { readFile as
|
|
388
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
302
389
|
import { existsSync as existsSync4 } from "fs";
|
|
303
|
-
import { join } from "path";
|
|
390
|
+
import { join, relative } from "path";
|
|
304
391
|
var FRAMEWORKS = [
|
|
305
392
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
306
393
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -310,7 +397,7 @@ var FRAMEWORKS = [
|
|
|
310
397
|
];
|
|
311
398
|
async function detectProject(rootDir) {
|
|
312
399
|
const pkgPath = join(rootDir, "package.json");
|
|
313
|
-
const raw = await
|
|
400
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
314
401
|
const pkg = JSON.parse(raw);
|
|
315
402
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
316
403
|
const framework = detectFrameworkFromDeps(allDeps);
|
|
@@ -372,11 +459,11 @@ var AdapterRegistry = class {
|
|
|
372
459
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
373
460
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
374
461
|
var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
375
|
-
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+/;
|
|
376
463
|
var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
377
464
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
378
|
-
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
379
|
-
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;
|
|
380
467
|
var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
381
468
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
382
469
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -388,9 +475,9 @@ var RULE_HINTS = {
|
|
|
388
475
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
389
476
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
390
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.",
|
|
391
479
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
392
480
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
393
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
394
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."
|
|
395
482
|
};
|
|
396
483
|
|
|
@@ -760,48 +847,47 @@ function hasInternalIds(obj) {
|
|
|
760
847
|
}
|
|
761
848
|
return false;
|
|
762
849
|
}
|
|
763
|
-
function
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
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 };
|
|
777
860
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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++;
|
|
785
878
|
}
|
|
786
879
|
}
|
|
787
|
-
if (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (item && typeof item === "object") {
|
|
792
|
-
const emails = findEmails(item);
|
|
793
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
797
|
-
const first = target[0];
|
|
798
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
799
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
800
|
-
}
|
|
801
|
-
}
|
|
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 };
|
|
802
884
|
}
|
|
803
885
|
return null;
|
|
804
886
|
}
|
|
887
|
+
function detectPII(method, reqBody, resBody) {
|
|
888
|
+
const target = unwrapResponse(resBody);
|
|
889
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
890
|
+
}
|
|
805
891
|
var REASON_LABELS = {
|
|
806
892
|
echo: "echoes back PII from the request body",
|
|
807
893
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -1929,8 +2015,12 @@ function computeInsightKey(insight) {
|
|
|
1929
2015
|
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
1930
2016
|
return `${insight.type}:${identifier}`;
|
|
1931
2017
|
}
|
|
2018
|
+
function enrichedIdFromInsight(insight) {
|
|
2019
|
+
return computeInsightId(insight.type, insight.nav ?? "global", insight.desc);
|
|
2020
|
+
}
|
|
1932
2021
|
var InsightTracker = class {
|
|
1933
2022
|
tracked = /* @__PURE__ */ new Map();
|
|
2023
|
+
enrichedIndex = /* @__PURE__ */ new Map();
|
|
1934
2024
|
reconcile(current) {
|
|
1935
2025
|
const currentKeys = /* @__PURE__ */ new Set();
|
|
1936
2026
|
const now = Date.now();
|
|
@@ -1938,6 +2028,7 @@ var InsightTracker = class {
|
|
|
1938
2028
|
const key = computeInsightKey(insight);
|
|
1939
2029
|
currentKeys.add(key);
|
|
1940
2030
|
const existing = this.tracked.get(key);
|
|
2031
|
+
this.enrichedIndex.set(enrichedIdFromInsight(insight), key);
|
|
1941
2032
|
if (existing) {
|
|
1942
2033
|
existing.insight = insight;
|
|
1943
2034
|
existing.lastSeenAt = now;
|
|
@@ -1954,34 +2045,50 @@ var InsightTracker = class {
|
|
|
1954
2045
|
firstSeenAt: now,
|
|
1955
2046
|
lastSeenAt: now,
|
|
1956
2047
|
resolvedAt: null,
|
|
1957
|
-
consecutiveAbsences: 0
|
|
2048
|
+
consecutiveAbsences: 0,
|
|
2049
|
+
aiStatus: null,
|
|
2050
|
+
aiNotes: null
|
|
1958
2051
|
});
|
|
1959
2052
|
}
|
|
1960
2053
|
}
|
|
1961
|
-
for (const [
|
|
1962
|
-
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)) {
|
|
1963
2056
|
stateful.consecutiveAbsences++;
|
|
1964
2057
|
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
1965
2058
|
stateful.state = "resolved";
|
|
1966
2059
|
stateful.resolvedAt = now;
|
|
1967
2060
|
}
|
|
1968
2061
|
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
1969
|
-
this.tracked.delete(key);
|
|
2062
|
+
this.tracked.delete(stateful.key);
|
|
2063
|
+
this.enrichedIndex.delete(enrichedIdFromInsight(stateful.insight));
|
|
1970
2064
|
}
|
|
1971
2065
|
}
|
|
1972
2066
|
return [...this.tracked.values()];
|
|
1973
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
|
+
}
|
|
1974
2080
|
getAll() {
|
|
1975
2081
|
return [...this.tracked.values()];
|
|
1976
2082
|
}
|
|
1977
2083
|
clear() {
|
|
1978
2084
|
this.tracked.clear();
|
|
2085
|
+
this.enrichedIndex.clear();
|
|
1979
2086
|
}
|
|
1980
2087
|
};
|
|
1981
2088
|
|
|
1982
2089
|
// src/analysis/engine.ts
|
|
1983
2090
|
var AnalysisEngine = class {
|
|
1984
|
-
constructor(registry, debounceMs =
|
|
2091
|
+
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
1985
2092
|
this.registry = registry;
|
|
1986
2093
|
this.debounceMs = debounceMs;
|
|
1987
2094
|
this.scanner = createDefaultScanner();
|
|
@@ -2017,7 +2124,10 @@ var AnalysisEngine = class {
|
|
|
2017
2124
|
return this.registry.has("finding-store") ? this.registry.get("finding-store").getAll() : [];
|
|
2018
2125
|
}
|
|
2019
2126
|
getStatefulInsights() {
|
|
2020
|
-
return this.
|
|
2127
|
+
return this.insightTracker.getAll();
|
|
2128
|
+
}
|
|
2129
|
+
reportInsightFix(enrichedId, status, notes) {
|
|
2130
|
+
return this.insightTracker.reportFix(enrichedId, status, notes);
|
|
2021
2131
|
}
|
|
2022
2132
|
scheduleRecompute() {
|
|
2023
2133
|
if (this.debounceTimer) return;
|
|
@@ -2062,7 +2172,7 @@ var AnalysisEngine = class {
|
|
|
2062
2172
|
};
|
|
2063
2173
|
|
|
2064
2174
|
// src/index.ts
|
|
2065
|
-
var VERSION = "0.8.
|
|
2175
|
+
var VERSION = "0.8.5";
|
|
2066
2176
|
export {
|
|
2067
2177
|
AdapterRegistry,
|
|
2068
2178
|
AnalysisEngine,
|