brakit 0.7.5 → 0.8.0

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.js CHANGED
@@ -1,6 +1,53 @@
1
- // src/detect/project.ts
2
- import { readFile as readFile2 } from "fs/promises";
3
- import { join } from "path";
1
+ // src/store/finding-store.ts
2
+ import {
3
+ readFileSync as readFileSync2,
4
+ writeFileSync as writeFileSync2,
5
+ existsSync as existsSync2,
6
+ mkdirSync as mkdirSync2,
7
+ renameSync
8
+ } from "fs";
9
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
10
+ import { resolve as resolve2 } from "path";
11
+
12
+ // src/constants/routes.ts
13
+ var DASHBOARD_PREFIX = "/__brakit";
14
+
15
+ // src/constants/limits.ts
16
+ var MAX_REQUEST_ENTRIES = 1e3;
17
+ var MAX_TELEMETRY_ENTRIES = 1e3;
18
+
19
+ // src/constants/thresholds.ts
20
+ var FLOW_GAP_MS = 5e3;
21
+ var SLOW_REQUEST_THRESHOLD_MS = 2e3;
22
+ var MIN_POLLING_SEQUENCE = 3;
23
+ var ENDPOINT_TRUNCATE_LENGTH = 12;
24
+ var N1_QUERY_THRESHOLD = 5;
25
+ var ERROR_RATE_THRESHOLD_PCT = 20;
26
+ var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
27
+ var MIN_REQUESTS_FOR_INSIGHT = 2;
28
+ var HIGH_QUERY_COUNT_PER_REQ = 5;
29
+ var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
30
+ var CROSS_ENDPOINT_PCT = 50;
31
+ var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
32
+ var REDUNDANT_QUERY_MIN_COUNT = 2;
33
+ var LARGE_RESPONSE_BYTES = 51200;
34
+ var HIGH_ROW_COUNT = 100;
35
+ var OVERFETCH_MIN_REQUESTS = 2;
36
+ var OVERFETCH_MIN_FIELDS = 8;
37
+ var OVERFETCH_MIN_INTERNAL_IDS = 2;
38
+ var OVERFETCH_NULL_RATIO = 0.3;
39
+ var REGRESSION_PCT_THRESHOLD = 50;
40
+ var REGRESSION_MIN_INCREASE_MS = 200;
41
+ var REGRESSION_MIN_REQUESTS = 5;
42
+ var QUERY_COUNT_REGRESSION_RATIO = 1.5;
43
+ var OVERFETCH_MANY_FIELDS = 12;
44
+ var OVERFETCH_UNWRAP_MIN_SIZE = 3;
45
+ var MAX_DUPLICATE_INSIGHTS = 3;
46
+
47
+ // src/constants/metrics.ts
48
+ var METRICS_DIR = ".brakit";
49
+ var FINDINGS_FILE = ".brakit/findings.json";
50
+ var FINDINGS_FLUSH_INTERVAL_MS = 1e4;
4
51
 
5
52
  // src/utils/fs.ts
6
53
  import { access } from "fs/promises";
@@ -14,8 +61,191 @@ async function fileExists(path) {
14
61
  return false;
15
62
  }
16
63
  }
64
+ function ensureGitignore(dir, entry) {
65
+ try {
66
+ const gitignorePath = resolve(dir, "../.gitignore");
67
+ if (existsSync(gitignorePath)) {
68
+ const content = readFileSync(gitignorePath, "utf-8");
69
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
70
+ writeFileSync(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
71
+ } else {
72
+ writeFileSync(gitignorePath, entry + "\n");
73
+ }
74
+ } catch {
75
+ }
76
+ }
77
+
78
+ // src/store/finding-id.ts
79
+ import { createHash } from "crypto";
80
+ function computeFindingId(finding) {
81
+ const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
82
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
83
+ }
84
+
85
+ // src/store/finding-store.ts
86
+ var FindingStore = class {
87
+ constructor(rootDir) {
88
+ this.rootDir = rootDir;
89
+ this.metricsDir = resolve2(rootDir, METRICS_DIR);
90
+ this.findingsPath = resolve2(rootDir, FINDINGS_FILE);
91
+ this.tmpPath = this.findingsPath + ".tmp";
92
+ this.load();
93
+ }
94
+ findings = /* @__PURE__ */ new Map();
95
+ flushTimer = null;
96
+ dirty = false;
97
+ writing = false;
98
+ findingsPath;
99
+ tmpPath;
100
+ metricsDir;
101
+ start() {
102
+ this.flushTimer = setInterval(
103
+ () => this.flush(),
104
+ FINDINGS_FLUSH_INTERVAL_MS
105
+ );
106
+ this.flushTimer.unref();
107
+ }
108
+ stop() {
109
+ if (this.flushTimer) {
110
+ clearInterval(this.flushTimer);
111
+ this.flushTimer = null;
112
+ }
113
+ this.flushSync();
114
+ }
115
+ upsert(finding, source) {
116
+ const id = computeFindingId(finding);
117
+ const existing = this.findings.get(id);
118
+ const now = Date.now();
119
+ if (existing) {
120
+ existing.lastSeenAt = now;
121
+ existing.occurrences++;
122
+ existing.finding = finding;
123
+ if (existing.state === "resolved") {
124
+ existing.state = "open";
125
+ existing.resolvedAt = null;
126
+ }
127
+ this.dirty = true;
128
+ return existing;
129
+ }
130
+ const stateful = {
131
+ findingId: id,
132
+ state: "open",
133
+ source,
134
+ finding,
135
+ firstSeenAt: now,
136
+ lastSeenAt: now,
137
+ resolvedAt: null,
138
+ occurrences: 1
139
+ };
140
+ this.findings.set(id, stateful);
141
+ this.dirty = true;
142
+ return stateful;
143
+ }
144
+ transition(findingId, state) {
145
+ const finding = this.findings.get(findingId);
146
+ if (!finding) return false;
147
+ finding.state = state;
148
+ if (state === "resolved") {
149
+ finding.resolvedAt = Date.now();
150
+ }
151
+ this.dirty = true;
152
+ return true;
153
+ }
154
+ reconcilePassive(currentFindings) {
155
+ const currentIds = new Set(currentFindings.map(computeFindingId));
156
+ for (const [id, stateful] of this.findings) {
157
+ if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
158
+ stateful.state = "resolved";
159
+ stateful.resolvedAt = Date.now();
160
+ this.dirty = true;
161
+ }
162
+ }
163
+ }
164
+ getAll() {
165
+ return [...this.findings.values()];
166
+ }
167
+ getByState(state) {
168
+ return [...this.findings.values()].filter((f) => f.state === state);
169
+ }
170
+ get(findingId) {
171
+ return this.findings.get(findingId);
172
+ }
173
+ clear() {
174
+ this.findings.clear();
175
+ this.dirty = true;
176
+ }
177
+ load() {
178
+ try {
179
+ if (existsSync2(this.findingsPath)) {
180
+ const raw = readFileSync2(this.findingsPath, "utf-8");
181
+ const parsed = JSON.parse(raw);
182
+ if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
183
+ for (const f of parsed.findings) {
184
+ this.findings.set(f.findingId, f);
185
+ }
186
+ }
187
+ }
188
+ } catch {
189
+ }
190
+ }
191
+ flush() {
192
+ if (!this.dirty) return;
193
+ this.writeAsync();
194
+ }
195
+ flushSync() {
196
+ if (!this.dirty) return;
197
+ try {
198
+ this.ensureDir();
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
+ }
212
+ }
213
+ async writeAsync() {
214
+ if (this.writing) return;
215
+ this.writing = true;
216
+ try {
217
+ if (!existsSync2(this.metricsDir)) {
218
+ await mkdir(this.metricsDir, { recursive: true });
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
+ }
243
+ }
244
+ };
17
245
 
18
246
  // src/detect/project.ts
247
+ import { readFile as readFile2 } from "fs/promises";
248
+ import { join } from "path";
19
249
  var FRAMEWORKS = [
20
250
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
21
251
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -409,6 +639,30 @@ var corsCredentialsRule = {
409
639
  }
410
640
  };
411
641
 
642
+ // src/utils/response.ts
643
+ function unwrapResponse(parsed) {
644
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
645
+ const obj = parsed;
646
+ const keys = Object.keys(obj);
647
+ if (keys.length > 3) return parsed;
648
+ let best = null;
649
+ let bestSize = 0;
650
+ for (const key of keys) {
651
+ const val = obj[key];
652
+ if (Array.isArray(val) && val.length > bestSize) {
653
+ best = val;
654
+ bestSize = val.length;
655
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
656
+ const size = Object.keys(val).length;
657
+ if (size > bestSize) {
658
+ best = val;
659
+ bestSize = size;
660
+ }
661
+ }
662
+ }
663
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
664
+ }
665
+
412
666
  // src/analysis/rules/response-pii-leak.ts
413
667
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
414
668
  var FULL_RECORD_MIN_FIELDS = 5;
@@ -453,28 +707,6 @@ function hasInternalIds(obj) {
453
707
  }
454
708
  return false;
455
709
  }
456
- function unwrapResponse(parsed) {
457
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
458
- const obj = parsed;
459
- const keys = Object.keys(obj);
460
- if (keys.length > 3) return parsed;
461
- let best = null;
462
- let bestSize = 0;
463
- for (const key of keys) {
464
- const val = obj[key];
465
- if (Array.isArray(val) && val.length > bestSize) {
466
- best = val;
467
- bestSize = val.length;
468
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
469
- const size = Object.keys(val).length;
470
- if (size > bestSize) {
471
- best = val;
472
- bestSize = size;
473
- }
474
- }
475
- }
476
- return best && bestSize >= 3 ? best : parsed;
477
- }
478
710
  function detectPII(method, reqBody, resBody) {
479
711
  const target = unwrapResponse(resBody);
480
712
  if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
@@ -593,41 +825,6 @@ function createDefaultScanner() {
593
825
  return scanner;
594
826
  }
595
827
 
596
- // src/constants/routes.ts
597
- var DASHBOARD_PREFIX = "/__brakit";
598
-
599
- // src/constants/limits.ts
600
- var MAX_REQUEST_ENTRIES = 1e3;
601
- var MAX_TELEMETRY_ENTRIES = 1e3;
602
-
603
- // src/constants/thresholds.ts
604
- var FLOW_GAP_MS = 5e3;
605
- var SLOW_REQUEST_THRESHOLD_MS = 2e3;
606
- var MIN_POLLING_SEQUENCE = 3;
607
- var ENDPOINT_TRUNCATE_LENGTH = 12;
608
- var N1_QUERY_THRESHOLD = 5;
609
- var ERROR_RATE_THRESHOLD_PCT = 20;
610
- var SLOW_ENDPOINT_THRESHOLD_MS = 1e3;
611
- var MIN_REQUESTS_FOR_INSIGHT = 2;
612
- var HIGH_QUERY_COUNT_PER_REQ = 5;
613
- var CROSS_ENDPOINT_MIN_ENDPOINTS = 3;
614
- var CROSS_ENDPOINT_PCT = 50;
615
- var CROSS_ENDPOINT_MIN_OCCURRENCES = 5;
616
- var REDUNDANT_QUERY_MIN_COUNT = 2;
617
- var LARGE_RESPONSE_BYTES = 51200;
618
- var HIGH_ROW_COUNT = 100;
619
- var OVERFETCH_MIN_REQUESTS = 2;
620
- var OVERFETCH_MIN_FIELDS = 8;
621
- var OVERFETCH_MIN_INTERNAL_IDS = 2;
622
- var OVERFETCH_NULL_RATIO = 0.3;
623
- var REGRESSION_PCT_THRESHOLD = 50;
624
- var REGRESSION_MIN_INCREASE_MS = 200;
625
- var REGRESSION_MIN_REQUESTS = 5;
626
- var QUERY_COUNT_REGRESSION_RATIO = 1.5;
627
- var OVERFETCH_MANY_FIELDS = 12;
628
- var OVERFETCH_UNWRAP_MIN_SIZE = 3;
629
- var MAX_DUPLICATE_INSIGHTS = 3;
630
-
631
828
  // src/utils/static-patterns.ts
632
829
  var STATIC_PATTERNS = [
633
830
  /^\/_next\//,
@@ -776,15 +973,15 @@ function getEndpointKey(method, path) {
776
973
 
777
974
  // src/store/metrics/persistence.ts
778
975
  import {
779
- readFileSync as readFileSync2,
780
- writeFileSync as writeFileSync2,
781
- mkdirSync as mkdirSync2,
782
- existsSync as existsSync2,
976
+ readFileSync as readFileSync3,
977
+ writeFileSync as writeFileSync3,
978
+ mkdirSync as mkdirSync3,
979
+ existsSync as existsSync3,
783
980
  unlinkSync,
784
- renameSync
981
+ renameSync as renameSync2
785
982
  } from "fs";
786
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
787
- import { resolve as resolve2 } from "path";
983
+ import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
984
+ import { resolve as resolve3 } from "path";
788
985
 
789
986
  // src/analysis/group.ts
790
987
  import { randomUUID as randomUUID3 } from "crypto";
@@ -1642,30 +1839,6 @@ var highRowsRule = {
1642
1839
  }
1643
1840
  };
1644
1841
 
1645
- // src/utils/response.ts
1646
- function unwrapResponse2(parsed) {
1647
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1648
- const obj = parsed;
1649
- const keys = Object.keys(obj);
1650
- if (keys.length > 3) return parsed;
1651
- let best = null;
1652
- let bestSize = 0;
1653
- for (const key of keys) {
1654
- const val = obj[key];
1655
- if (Array.isArray(val) && val.length > bestSize) {
1656
- best = val;
1657
- bestSize = val.length;
1658
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1659
- const size = Object.keys(val).length;
1660
- if (size > bestSize) {
1661
- best = val;
1662
- bestSize = size;
1663
- }
1664
- }
1665
- }
1666
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1667
- }
1668
-
1669
1842
  // src/analysis/insights/rules/response-overfetch.ts
1670
1843
  var responseOverfetchRule = {
1671
1844
  id: "response-overfetch",
@@ -1682,7 +1855,7 @@ var responseOverfetchRule = {
1682
1855
  } catch {
1683
1856
  continue;
1684
1857
  }
1685
- const target = unwrapResponse2(parsed);
1858
+ const target = unwrapResponse(parsed);
1686
1859
  const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1687
1860
  if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
1688
1861
  const fields = Object.keys(inspectObj);
@@ -1819,8 +1992,9 @@ function computeInsights(ctx) {
1819
1992
 
1820
1993
  // src/analysis/engine.ts
1821
1994
  var AnalysisEngine = class {
1822
- constructor(metricsStore, debounceMs = 300) {
1995
+ constructor(metricsStore, findingStore, debounceMs = 300) {
1823
1996
  this.metricsStore = metricsStore;
1997
+ this.findingStore = findingStore;
1824
1998
  this.debounceMs = debounceMs;
1825
1999
  this.scanner = createDefaultScanner();
1826
2000
  this.boundRequestListener = () => this.scheduleRecompute();
@@ -1881,6 +2055,12 @@ var AnalysisEngine = class {
1881
2055
  const fetches = defaultFetchStore.getAll();
1882
2056
  const flows = groupRequestsIntoFlows(requests);
1883
2057
  this.cachedFindings = this.scanner.scan({ requests, logs });
2058
+ if (this.findingStore) {
2059
+ for (const finding of this.cachedFindings) {
2060
+ this.findingStore.upsert(finding, "passive");
2061
+ }
2062
+ this.findingStore.reconcilePassive(this.cachedFindings);
2063
+ }
1884
2064
  this.cachedInsights = computeInsights({
1885
2065
  requests,
1886
2066
  queries,
@@ -1900,10 +2080,11 @@ var AnalysisEngine = class {
1900
2080
  };
1901
2081
 
1902
2082
  // src/index.ts
1903
- var VERSION = "0.7.5";
2083
+ var VERSION = "0.8.0";
1904
2084
  export {
1905
2085
  AdapterRegistry,
1906
2086
  AnalysisEngine,
2087
+ FindingStore,
1907
2088
  InsightRunner,
1908
2089
  SecurityScanner,
1909
2090
  VERSION,