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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IncomingHttpHeaders } from 'node:http';
2
2
 
3
- type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | (string & {});
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" | string;
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: 1;
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 = "open" | "resolved";
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 load;
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
- "store:cleared": void;
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 MAX_INGEST_BYTES = 10 * 1024 * 1024;
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/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
- };
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.message}`);
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.message}`);
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 (!existsSync2(this.opts.dir)) {
199
+ if (!await fileExists(this.opts.dir)) {
148
200
  await mkdir(this.opts.dir, { recursive: true });
149
201
  if (this.opts.gitignoreEntry) {
150
- ensureGitignore(this.opts.dir, this.opts.gitignoreEntry);
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, 16);
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
- load() {
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 === 1 && Array.isArray(parsed.findings)) {
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: 1,
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 readFile2 } from "fs/promises";
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 readFile2(pkgPath, "utf-8");
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_\-\.+\/]{8,}/;
379
- var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
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 detectPII(method, reqBody, resBody) {
764
- const target = unwrapResponse(resBody);
765
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
766
- const reqEmails = findEmails(reqBody);
767
- if (reqEmails.length > 0) {
768
- const resEmails = findEmails(target);
769
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
770
- if (echoed.length > 0) {
771
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
772
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
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
- if (target && typeof target === "object" && !Array.isArray(target)) {
779
- const fields = topLevelFieldCount(target);
780
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
781
- const emails = findEmails(target);
782
- if (emails.length > 0) {
783
- return { reason: "full-record", emailCount: emails.length };
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 (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
788
- let itemsWithEmail = 0;
789
- for (let i = 0; i < Math.min(target.length, 10); i++) {
790
- const item = target[i];
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 [key, stateful] of this.tracked) {
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 = 300) {
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.cachedStatefulInsights;
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.4";
2175
+ var VERSION = "0.8.5";
2066
2176
  export {
2067
2177
  AdapterRegistry,
2068
2178
  AnalysisEngine,