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/README.md +87 -52
- package/dist/api.d.ts +47 -2
- package/dist/api.js +275 -94
- package/dist/bin/brakit.js +1143 -48
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +737 -0
- package/dist/runtime/index.js +302 -66
- package/package.json +5 -1
package/dist/runtime/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/constants/routes.ts
|
|
12
|
-
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB;
|
|
12
|
+
var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS;
|
|
13
13
|
var init_routes = __esm({
|
|
14
14
|
"src/constants/routes.ts"() {
|
|
15
15
|
"use strict";
|
|
@@ -29,6 +29,7 @@ var init_routes = __esm({
|
|
|
29
29
|
DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
|
|
30
30
|
DASHBOARD_API_SECURITY = "/__brakit/api/security";
|
|
31
31
|
DASHBOARD_API_TAB = "/__brakit/api/tab";
|
|
32
|
+
DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
|
|
32
33
|
}
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -93,7 +94,7 @@ var init_transport = __esm({
|
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
// src/constants/metrics.ts
|
|
96
|
-
var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS;
|
|
97
|
+
var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, PORT_FILE, FINDINGS_FILE, FINDINGS_FLUSH_INTERVAL_MS;
|
|
97
98
|
var init_metrics = __esm({
|
|
98
99
|
"src/constants/metrics.ts"() {
|
|
99
100
|
"use strict";
|
|
@@ -102,6 +103,9 @@ var init_metrics = __esm({
|
|
|
102
103
|
METRICS_FLUSH_INTERVAL_MS = 3e4;
|
|
103
104
|
METRICS_MAX_SESSIONS = 50;
|
|
104
105
|
METRICS_MAX_DATA_POINTS = 200;
|
|
106
|
+
PORT_FILE = ".brakit/port";
|
|
107
|
+
FINDINGS_FILE = ".brakit/findings.json";
|
|
108
|
+
FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
105
109
|
}
|
|
106
110
|
});
|
|
107
111
|
|
|
@@ -148,6 +152,13 @@ var init_network = __esm({
|
|
|
148
152
|
}
|
|
149
153
|
});
|
|
150
154
|
|
|
155
|
+
// src/constants/mcp.ts
|
|
156
|
+
var init_mcp = __esm({
|
|
157
|
+
"src/constants/mcp.ts"() {
|
|
158
|
+
"use strict";
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
151
162
|
// src/constants/index.ts
|
|
152
163
|
var init_constants = __esm({
|
|
153
164
|
"src/constants/index.ts"() {
|
|
@@ -159,6 +170,7 @@ var init_constants = __esm({
|
|
|
159
170
|
init_metrics();
|
|
160
171
|
init_headers();
|
|
161
172
|
init_network();
|
|
173
|
+
init_mcp();
|
|
162
174
|
}
|
|
163
175
|
});
|
|
164
176
|
|
|
@@ -2055,6 +2067,33 @@ var init_insights = __esm({
|
|
|
2055
2067
|
}
|
|
2056
2068
|
});
|
|
2057
2069
|
|
|
2070
|
+
// src/dashboard/api/findings.ts
|
|
2071
|
+
function createFindingsHandler(findingStore) {
|
|
2072
|
+
return (req, res) => {
|
|
2073
|
+
if (!requireGet(req, res)) return;
|
|
2074
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2075
|
+
const stateParam = url.searchParams.get("state");
|
|
2076
|
+
let findings;
|
|
2077
|
+
if (stateParam && VALID_STATES.has(stateParam)) {
|
|
2078
|
+
findings = findingStore.getByState(stateParam);
|
|
2079
|
+
} else {
|
|
2080
|
+
findings = findingStore.getAll();
|
|
2081
|
+
}
|
|
2082
|
+
sendJson(req, res, 200, {
|
|
2083
|
+
total: findings.length,
|
|
2084
|
+
findings
|
|
2085
|
+
});
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
var VALID_STATES;
|
|
2089
|
+
var init_findings = __esm({
|
|
2090
|
+
"src/dashboard/api/findings.ts"() {
|
|
2091
|
+
"use strict";
|
|
2092
|
+
init_shared2();
|
|
2093
|
+
VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2058
2097
|
// src/dashboard/sse.ts
|
|
2059
2098
|
function createSSEHandler(engine) {
|
|
2060
2099
|
return (req, res) => {
|
|
@@ -2687,6 +2726,197 @@ var init_styles = __esm({
|
|
|
2687
2726
|
}
|
|
2688
2727
|
});
|
|
2689
2728
|
|
|
2729
|
+
// src/store/finding-id.ts
|
|
2730
|
+
import { createHash } from "crypto";
|
|
2731
|
+
function computeFindingId(finding) {
|
|
2732
|
+
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
2733
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
2734
|
+
}
|
|
2735
|
+
var init_finding_id = __esm({
|
|
2736
|
+
"src/store/finding-id.ts"() {
|
|
2737
|
+
"use strict";
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
|
|
2741
|
+
// src/store/finding-store.ts
|
|
2742
|
+
import {
|
|
2743
|
+
readFileSync as readFileSync3,
|
|
2744
|
+
writeFileSync as writeFileSync3,
|
|
2745
|
+
existsSync as existsSync3,
|
|
2746
|
+
mkdirSync as mkdirSync3,
|
|
2747
|
+
renameSync as renameSync2
|
|
2748
|
+
} from "fs";
|
|
2749
|
+
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
2750
|
+
import { resolve as resolve3 } from "path";
|
|
2751
|
+
var FindingStore;
|
|
2752
|
+
var init_finding_store = __esm({
|
|
2753
|
+
"src/store/finding-store.ts"() {
|
|
2754
|
+
"use strict";
|
|
2755
|
+
init_constants();
|
|
2756
|
+
init_fs();
|
|
2757
|
+
init_finding_id();
|
|
2758
|
+
FindingStore = class {
|
|
2759
|
+
constructor(rootDir) {
|
|
2760
|
+
this.rootDir = rootDir;
|
|
2761
|
+
this.metricsDir = resolve3(rootDir, METRICS_DIR);
|
|
2762
|
+
this.findingsPath = resolve3(rootDir, FINDINGS_FILE);
|
|
2763
|
+
this.tmpPath = this.findingsPath + ".tmp";
|
|
2764
|
+
this.load();
|
|
2765
|
+
}
|
|
2766
|
+
findings = /* @__PURE__ */ new Map();
|
|
2767
|
+
flushTimer = null;
|
|
2768
|
+
dirty = false;
|
|
2769
|
+
writing = false;
|
|
2770
|
+
findingsPath;
|
|
2771
|
+
tmpPath;
|
|
2772
|
+
metricsDir;
|
|
2773
|
+
start() {
|
|
2774
|
+
this.flushTimer = setInterval(
|
|
2775
|
+
() => this.flush(),
|
|
2776
|
+
FINDINGS_FLUSH_INTERVAL_MS
|
|
2777
|
+
);
|
|
2778
|
+
this.flushTimer.unref();
|
|
2779
|
+
}
|
|
2780
|
+
stop() {
|
|
2781
|
+
if (this.flushTimer) {
|
|
2782
|
+
clearInterval(this.flushTimer);
|
|
2783
|
+
this.flushTimer = null;
|
|
2784
|
+
}
|
|
2785
|
+
this.flushSync();
|
|
2786
|
+
}
|
|
2787
|
+
upsert(finding, source) {
|
|
2788
|
+
const id = computeFindingId(finding);
|
|
2789
|
+
const existing = this.findings.get(id);
|
|
2790
|
+
const now = Date.now();
|
|
2791
|
+
if (existing) {
|
|
2792
|
+
existing.lastSeenAt = now;
|
|
2793
|
+
existing.occurrences++;
|
|
2794
|
+
existing.finding = finding;
|
|
2795
|
+
if (existing.state === "resolved") {
|
|
2796
|
+
existing.state = "open";
|
|
2797
|
+
existing.resolvedAt = null;
|
|
2798
|
+
}
|
|
2799
|
+
this.dirty = true;
|
|
2800
|
+
return existing;
|
|
2801
|
+
}
|
|
2802
|
+
const stateful = {
|
|
2803
|
+
findingId: id,
|
|
2804
|
+
state: "open",
|
|
2805
|
+
source,
|
|
2806
|
+
finding,
|
|
2807
|
+
firstSeenAt: now,
|
|
2808
|
+
lastSeenAt: now,
|
|
2809
|
+
resolvedAt: null,
|
|
2810
|
+
occurrences: 1
|
|
2811
|
+
};
|
|
2812
|
+
this.findings.set(id, stateful);
|
|
2813
|
+
this.dirty = true;
|
|
2814
|
+
return stateful;
|
|
2815
|
+
}
|
|
2816
|
+
transition(findingId, state) {
|
|
2817
|
+
const finding = this.findings.get(findingId);
|
|
2818
|
+
if (!finding) return false;
|
|
2819
|
+
finding.state = state;
|
|
2820
|
+
if (state === "resolved") {
|
|
2821
|
+
finding.resolvedAt = Date.now();
|
|
2822
|
+
}
|
|
2823
|
+
this.dirty = true;
|
|
2824
|
+
return true;
|
|
2825
|
+
}
|
|
2826
|
+
reconcilePassive(currentFindings) {
|
|
2827
|
+
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
2828
|
+
for (const [id, stateful] of this.findings) {
|
|
2829
|
+
if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
|
|
2830
|
+
stateful.state = "resolved";
|
|
2831
|
+
stateful.resolvedAt = Date.now();
|
|
2832
|
+
this.dirty = true;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
getAll() {
|
|
2837
|
+
return [...this.findings.values()];
|
|
2838
|
+
}
|
|
2839
|
+
getByState(state) {
|
|
2840
|
+
return [...this.findings.values()].filter((f) => f.state === state);
|
|
2841
|
+
}
|
|
2842
|
+
get(findingId) {
|
|
2843
|
+
return this.findings.get(findingId);
|
|
2844
|
+
}
|
|
2845
|
+
clear() {
|
|
2846
|
+
this.findings.clear();
|
|
2847
|
+
this.dirty = true;
|
|
2848
|
+
}
|
|
2849
|
+
load() {
|
|
2850
|
+
try {
|
|
2851
|
+
if (existsSync3(this.findingsPath)) {
|
|
2852
|
+
const raw = readFileSync3(this.findingsPath, "utf-8");
|
|
2853
|
+
const parsed = JSON.parse(raw);
|
|
2854
|
+
if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
|
|
2855
|
+
for (const f of parsed.findings) {
|
|
2856
|
+
this.findings.set(f.findingId, f);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
} catch {
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
flush() {
|
|
2864
|
+
if (!this.dirty) return;
|
|
2865
|
+
this.writeAsync();
|
|
2866
|
+
}
|
|
2867
|
+
flushSync() {
|
|
2868
|
+
if (!this.dirty) return;
|
|
2869
|
+
try {
|
|
2870
|
+
this.ensureDir();
|
|
2871
|
+
const data = {
|
|
2872
|
+
version: 1,
|
|
2873
|
+
findings: [...this.findings.values()]
|
|
2874
|
+
};
|
|
2875
|
+
writeFileSync3(this.tmpPath, JSON.stringify(data));
|
|
2876
|
+
renameSync2(this.tmpPath, this.findingsPath);
|
|
2877
|
+
this.dirty = false;
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
process.stderr.write(
|
|
2880
|
+
`[brakit] failed to save findings: ${err.message}
|
|
2881
|
+
`
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
async writeAsync() {
|
|
2886
|
+
if (this.writing) return;
|
|
2887
|
+
this.writing = true;
|
|
2888
|
+
try {
|
|
2889
|
+
if (!existsSync3(this.metricsDir)) {
|
|
2890
|
+
await mkdir2(this.metricsDir, { recursive: true });
|
|
2891
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
2892
|
+
}
|
|
2893
|
+
const data = {
|
|
2894
|
+
version: 1,
|
|
2895
|
+
findings: [...this.findings.values()]
|
|
2896
|
+
};
|
|
2897
|
+
await writeFile3(this.tmpPath, JSON.stringify(data));
|
|
2898
|
+
await rename2(this.tmpPath, this.findingsPath);
|
|
2899
|
+
this.dirty = false;
|
|
2900
|
+
} catch (err) {
|
|
2901
|
+
process.stderr.write(
|
|
2902
|
+
`[brakit] failed to save findings: ${err.message}
|
|
2903
|
+
`
|
|
2904
|
+
);
|
|
2905
|
+
} finally {
|
|
2906
|
+
this.writing = false;
|
|
2907
|
+
if (this.dirty) this.writeAsync();
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
ensureDir() {
|
|
2911
|
+
if (!existsSync3(this.metricsDir)) {
|
|
2912
|
+
mkdirSync3(this.metricsDir, { recursive: true });
|
|
2913
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
|
|
2690
2920
|
// src/detect/project.ts
|
|
2691
2921
|
import { readFile as readFile2 } from "fs/promises";
|
|
2692
2922
|
import { join } from "path";
|
|
@@ -3076,6 +3306,36 @@ var init_cors_credentials = __esm({
|
|
|
3076
3306
|
}
|
|
3077
3307
|
});
|
|
3078
3308
|
|
|
3309
|
+
// src/utils/response.ts
|
|
3310
|
+
function unwrapResponse(parsed) {
|
|
3311
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3312
|
+
const obj = parsed;
|
|
3313
|
+
const keys = Object.keys(obj);
|
|
3314
|
+
if (keys.length > 3) return parsed;
|
|
3315
|
+
let best = null;
|
|
3316
|
+
let bestSize = 0;
|
|
3317
|
+
for (const key of keys) {
|
|
3318
|
+
const val = obj[key];
|
|
3319
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
3320
|
+
best = val;
|
|
3321
|
+
bestSize = val.length;
|
|
3322
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3323
|
+
const size = Object.keys(val).length;
|
|
3324
|
+
if (size > bestSize) {
|
|
3325
|
+
best = val;
|
|
3326
|
+
bestSize = size;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
3331
|
+
}
|
|
3332
|
+
var init_response = __esm({
|
|
3333
|
+
"src/utils/response.ts"() {
|
|
3334
|
+
"use strict";
|
|
3335
|
+
init_thresholds();
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3079
3339
|
// src/analysis/rules/response-pii-leak.ts
|
|
3080
3340
|
function tryParseJson2(body) {
|
|
3081
3341
|
if (!body) return null;
|
|
@@ -3117,28 +3377,6 @@ function hasInternalIds(obj) {
|
|
|
3117
3377
|
}
|
|
3118
3378
|
return false;
|
|
3119
3379
|
}
|
|
3120
|
-
function unwrapResponse(parsed) {
|
|
3121
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3122
|
-
const obj = parsed;
|
|
3123
|
-
const keys = Object.keys(obj);
|
|
3124
|
-
if (keys.length > 3) return parsed;
|
|
3125
|
-
let best = null;
|
|
3126
|
-
let bestSize = 0;
|
|
3127
|
-
for (const key of keys) {
|
|
3128
|
-
const val = obj[key];
|
|
3129
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
3130
|
-
best = val;
|
|
3131
|
-
bestSize = val.length;
|
|
3132
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3133
|
-
const size = Object.keys(val).length;
|
|
3134
|
-
if (size > bestSize) {
|
|
3135
|
-
best = val;
|
|
3136
|
-
bestSize = size;
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
return best && bestSize >= 3 ? best : parsed;
|
|
3141
|
-
}
|
|
3142
3380
|
function detectPII(method, reqBody, resBody) {
|
|
3143
3381
|
const target = unwrapResponse(resBody);
|
|
3144
3382
|
if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
|
|
@@ -3186,6 +3424,7 @@ var init_response_pii_leak = __esm({
|
|
|
3186
3424
|
"src/analysis/rules/response-pii-leak.ts"() {
|
|
3187
3425
|
"use strict";
|
|
3188
3426
|
init_patterns();
|
|
3427
|
+
init_response();
|
|
3189
3428
|
WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
3190
3429
|
FULL_RECORD_MIN_FIELDS = 5;
|
|
3191
3430
|
LIST_PII_MIN_ITEMS = 2;
|
|
@@ -3879,36 +4118,6 @@ var init_high_rows = __esm({
|
|
|
3879
4118
|
}
|
|
3880
4119
|
});
|
|
3881
4120
|
|
|
3882
|
-
// src/utils/response.ts
|
|
3883
|
-
function unwrapResponse2(parsed) {
|
|
3884
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
3885
|
-
const obj = parsed;
|
|
3886
|
-
const keys = Object.keys(obj);
|
|
3887
|
-
if (keys.length > 3) return parsed;
|
|
3888
|
-
let best = null;
|
|
3889
|
-
let bestSize = 0;
|
|
3890
|
-
for (const key of keys) {
|
|
3891
|
-
const val = obj[key];
|
|
3892
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
3893
|
-
best = val;
|
|
3894
|
-
bestSize = val.length;
|
|
3895
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
3896
|
-
const size = Object.keys(val).length;
|
|
3897
|
-
if (size > bestSize) {
|
|
3898
|
-
best = val;
|
|
3899
|
-
bestSize = size;
|
|
3900
|
-
}
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
3904
|
-
}
|
|
3905
|
-
var init_response = __esm({
|
|
3906
|
-
"src/utils/response.ts"() {
|
|
3907
|
-
"use strict";
|
|
3908
|
-
init_thresholds();
|
|
3909
|
-
}
|
|
3910
|
-
});
|
|
3911
|
-
|
|
3912
4121
|
// src/analysis/insights/rules/response-overfetch.ts
|
|
3913
4122
|
var responseOverfetchRule;
|
|
3914
4123
|
var init_response_overfetch = __esm({
|
|
@@ -3933,7 +4142,7 @@ var init_response_overfetch = __esm({
|
|
|
3933
4142
|
} catch {
|
|
3934
4143
|
continue;
|
|
3935
4144
|
}
|
|
3936
|
-
const target =
|
|
4145
|
+
const target = unwrapResponse(parsed);
|
|
3937
4146
|
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
3938
4147
|
if (!inspectObj || typeof inspectObj !== "object" || Array.isArray(inspectObj)) continue;
|
|
3939
4148
|
const fields = Object.keys(inspectObj);
|
|
@@ -4133,8 +4342,9 @@ var init_engine = __esm({
|
|
|
4133
4342
|
init_rules();
|
|
4134
4343
|
init_insights3();
|
|
4135
4344
|
AnalysisEngine = class {
|
|
4136
|
-
constructor(metricsStore, debounceMs = 300) {
|
|
4345
|
+
constructor(metricsStore, findingStore, debounceMs = 300) {
|
|
4137
4346
|
this.metricsStore = metricsStore;
|
|
4347
|
+
this.findingStore = findingStore;
|
|
4138
4348
|
this.debounceMs = debounceMs;
|
|
4139
4349
|
this.scanner = createDefaultScanner();
|
|
4140
4350
|
this.boundRequestListener = () => this.scheduleRecompute();
|
|
@@ -4195,6 +4405,12 @@ var init_engine = __esm({
|
|
|
4195
4405
|
const fetches = defaultFetchStore.getAll();
|
|
4196
4406
|
const flows = groupRequestsIntoFlows(requests);
|
|
4197
4407
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
4408
|
+
if (this.findingStore) {
|
|
4409
|
+
for (const finding of this.cachedFindings) {
|
|
4410
|
+
this.findingStore.upsert(finding, "passive");
|
|
4411
|
+
}
|
|
4412
|
+
this.findingStore.reconcilePassive(this.cachedFindings);
|
|
4413
|
+
}
|
|
4198
4414
|
this.cachedInsights = computeInsights({
|
|
4199
4415
|
requests,
|
|
4200
4416
|
queries,
|
|
@@ -4220,13 +4436,14 @@ var VERSION;
|
|
|
4220
4436
|
var init_src = __esm({
|
|
4221
4437
|
"src/index.ts"() {
|
|
4222
4438
|
"use strict";
|
|
4439
|
+
init_finding_store();
|
|
4223
4440
|
init_project();
|
|
4224
4441
|
init_adapter_registry();
|
|
4225
4442
|
init_rules();
|
|
4226
4443
|
init_engine();
|
|
4227
4444
|
init_insights3();
|
|
4228
4445
|
init_insights2();
|
|
4229
|
-
VERSION = "0.
|
|
4446
|
+
VERSION = "0.8.0";
|
|
4230
4447
|
}
|
|
4231
4448
|
});
|
|
4232
4449
|
|
|
@@ -6660,12 +6877,12 @@ var init_page = __esm({
|
|
|
6660
6877
|
// src/telemetry/config.ts
|
|
6661
6878
|
import { homedir } from "os";
|
|
6662
6879
|
import { join as join2 } from "path";
|
|
6663
|
-
import { existsSync as
|
|
6880
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
6664
6881
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6665
6882
|
function readConfig() {
|
|
6666
6883
|
try {
|
|
6667
|
-
if (!
|
|
6668
|
-
return JSON.parse(
|
|
6884
|
+
if (!existsSync4(CONFIG_PATH)) return null;
|
|
6885
|
+
return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
6669
6886
|
} catch {
|
|
6670
6887
|
return null;
|
|
6671
6888
|
}
|
|
@@ -6728,6 +6945,9 @@ function createDashboardHandler(deps) {
|
|
|
6728
6945
|
routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
|
|
6729
6946
|
routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
|
|
6730
6947
|
}
|
|
6948
|
+
if (deps.findingStore) {
|
|
6949
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(deps.findingStore);
|
|
6950
|
+
}
|
|
6731
6951
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
6732
6952
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
6733
6953
|
if (raw) {
|
|
@@ -6760,6 +6980,7 @@ var init_router = __esm({
|
|
|
6760
6980
|
init_constants();
|
|
6761
6981
|
init_api();
|
|
6762
6982
|
init_insights();
|
|
6983
|
+
init_findings();
|
|
6763
6984
|
init_sse();
|
|
6764
6985
|
init_page();
|
|
6765
6986
|
init_telemetry();
|
|
@@ -6800,12 +7021,11 @@ function colorTitle(severity, text) {
|
|
|
6800
7021
|
function truncate(s, max = 80) {
|
|
6801
7022
|
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
6802
7023
|
}
|
|
6803
|
-
function formatConsoleLine(insight,
|
|
7024
|
+
function formatConsoleLine(insight, suffix) {
|
|
6804
7025
|
const icon = severityIcon(insight.severity);
|
|
6805
7026
|
const title = colorTitle(insight.severity, insight.title);
|
|
6806
7027
|
const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
|
|
6807
|
-
|
|
6808
|
-
let line = ` ${icon} ${title} \u2014 ${desc} ${link}`;
|
|
7028
|
+
let line = ` ${icon} ${title} \u2014 ${desc}`;
|
|
6809
7029
|
if (insight.detail) {
|
|
6810
7030
|
line += `
|
|
6811
7031
|
${pc.dim("\u2514 " + insight.detail)}`;
|
|
@@ -6831,11 +7051,13 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
|
6831
7051
|
suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
|
|
6832
7052
|
}
|
|
6833
7053
|
}
|
|
6834
|
-
lines.push(formatConsoleLine(insight,
|
|
7054
|
+
lines.push(formatConsoleLine(insight, suffix));
|
|
6835
7055
|
}
|
|
6836
7056
|
if (lines.length > 0) {
|
|
6837
7057
|
print("");
|
|
6838
7058
|
for (const line of lines) print(line);
|
|
7059
|
+
print("");
|
|
7060
|
+
print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
|
|
6839
7061
|
}
|
|
6840
7062
|
};
|
|
6841
7063
|
}
|
|
@@ -7071,6 +7293,8 @@ var setup_exports = {};
|
|
|
7071
7293
|
__export(setup_exports, {
|
|
7072
7294
|
setup: () => setup
|
|
7073
7295
|
});
|
|
7296
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
7297
|
+
import { resolve as resolve4 } from "path";
|
|
7074
7298
|
function setup() {
|
|
7075
7299
|
if (initialized) return;
|
|
7076
7300
|
initialized = true;
|
|
@@ -7083,7 +7307,9 @@ function setup() {
|
|
|
7083
7307
|
const cwd = process.cwd();
|
|
7084
7308
|
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
7085
7309
|
metricsStore.start();
|
|
7086
|
-
const
|
|
7310
|
+
const findingStore = new FindingStore(cwd);
|
|
7311
|
+
findingStore.start();
|
|
7312
|
+
const analysisEngine = new AnalysisEngine(metricsStore, findingStore);
|
|
7087
7313
|
analysisEngine.start();
|
|
7088
7314
|
const config = {
|
|
7089
7315
|
proxyPort: 0,
|
|
@@ -7091,7 +7317,7 @@ function setup() {
|
|
|
7091
7317
|
showStatic: false,
|
|
7092
7318
|
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
7093
7319
|
};
|
|
7094
|
-
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
7320
|
+
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine, findingStore });
|
|
7095
7321
|
onRequest((req) => {
|
|
7096
7322
|
const queries = defaultQueryStore.getByRequest(req.id);
|
|
7097
7323
|
const fetches = defaultFetchStore.getByRequest(req.id);
|
|
@@ -7105,6 +7331,9 @@ function setup() {
|
|
|
7105
7331
|
handleDashboard,
|
|
7106
7332
|
config,
|
|
7107
7333
|
onFirstRequest(port) {
|
|
7334
|
+
const dir = resolve4(cwd, METRICS_DIR);
|
|
7335
|
+
if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
|
|
7336
|
+
writeFileSync5(resolve4(cwd, PORT_FILE), String(port));
|
|
7108
7337
|
analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
|
|
7109
7338
|
process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
7110
7339
|
`);
|
|
@@ -7113,7 +7342,13 @@ function setup() {
|
|
|
7113
7342
|
health.setTeardown(() => {
|
|
7114
7343
|
uninstallInterceptor();
|
|
7115
7344
|
analysisEngine.stop();
|
|
7345
|
+
findingStore.stop();
|
|
7116
7346
|
metricsStore.stop();
|
|
7347
|
+
try {
|
|
7348
|
+
const portPath = resolve4(cwd, PORT_FILE);
|
|
7349
|
+
if (existsSync5(portPath)) unlinkSync2(portPath);
|
|
7350
|
+
} catch {
|
|
7351
|
+
}
|
|
7117
7352
|
});
|
|
7118
7353
|
}
|
|
7119
7354
|
function routeEvent2(event) {
|
|
@@ -7144,6 +7379,7 @@ var init_setup = __esm({
|
|
|
7144
7379
|
init_router();
|
|
7145
7380
|
init_request_log();
|
|
7146
7381
|
init_store();
|
|
7382
|
+
init_finding_store();
|
|
7147
7383
|
init_engine();
|
|
7148
7384
|
init_terminal();
|
|
7149
7385
|
init_src();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "See what your API is really doing. Security scanning, N+1 detection, duplicate calls, DB queries — one command, zero config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
29
|
"test": "vitest run",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
+
"test:contracts": "vitest run tests/contracts tests/adapters",
|
|
32
|
+
"test:integration": "vitest run tests/integration",
|
|
33
|
+
"ci": "npm run typecheck && npm test && npm run build",
|
|
31
34
|
"lint": "tsc --noEmit"
|
|
32
35
|
},
|
|
33
36
|
"repository": {
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
},
|
|
41
44
|
"author": "Brakit <dev@brakit.ai> (https://brakit.ai)",
|
|
42
45
|
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
43
47
|
"citty": "^0.1.6",
|
|
44
48
|
"picocolors": "^1.1.1"
|
|
45
49
|
},
|