brakit 0.7.6 → 0.8.1
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 +22 -4
- package/dist/api.d.ts +99 -30
- package/dist/api.js +348 -49
- package/dist/bin/brakit.js +1128 -29
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +743 -0
- package/dist/runtime/index.js +680 -173
- 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,11 +29,12 @@ 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
|
|
|
35
36
|
// src/constants/limits.ts
|
|
36
|
-
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH;
|
|
37
|
+
var MAX_REQUEST_ENTRIES, DEFAULT_MAX_BODY_CAPTURE, DEFAULT_API_LIMIT, MAX_TELEMETRY_ENTRIES, MAX_TAB_NAME_LENGTH, MAX_INGEST_BYTES, TERMINAL_TRUNCATE_LENGTH, SENSITIVE_MASK_MIN_LENGTH, SENSITIVE_MASK_VISIBLE_CHARS;
|
|
37
38
|
var init_limits = __esm({
|
|
38
39
|
"src/constants/limits.ts"() {
|
|
39
40
|
"use strict";
|
|
@@ -42,11 +43,15 @@ var init_limits = __esm({
|
|
|
42
43
|
DEFAULT_API_LIMIT = 500;
|
|
43
44
|
MAX_TELEMETRY_ENTRIES = 1e3;
|
|
44
45
|
MAX_TAB_NAME_LENGTH = 32;
|
|
46
|
+
MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
47
|
+
TERMINAL_TRUNCATE_LENGTH = 80;
|
|
48
|
+
SENSITIVE_MASK_MIN_LENGTH = 8;
|
|
49
|
+
SENSITIVE_MASK_VISIBLE_CHARS = 4;
|
|
45
50
|
}
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
// src/constants/thresholds.ts
|
|
49
|
-
var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS;
|
|
54
|
+
var FLOW_GAP_MS, SLOW_REQUEST_THRESHOLD_MS, MIN_POLLING_SEQUENCE, ENDPOINT_TRUNCATE_LENGTH, N1_QUERY_THRESHOLD, ERROR_RATE_THRESHOLD_PCT, SLOW_ENDPOINT_THRESHOLD_MS, MIN_REQUESTS_FOR_INSIGHT, HIGH_QUERY_COUNT_PER_REQ, CROSS_ENDPOINT_MIN_ENDPOINTS, CROSS_ENDPOINT_PCT, CROSS_ENDPOINT_MIN_OCCURRENCES, REDUNDANT_QUERY_MIN_COUNT, LARGE_RESPONSE_BYTES, HIGH_ROW_COUNT, OVERFETCH_MIN_REQUESTS, OVERFETCH_MIN_FIELDS, OVERFETCH_MIN_INTERNAL_IDS, OVERFETCH_NULL_RATIO, REGRESSION_PCT_THRESHOLD, REGRESSION_MIN_INCREASE_MS, REGRESSION_MIN_REQUESTS, QUERY_COUNT_REGRESSION_RATIO, OVERFETCH_MANY_FIELDS, OVERFETCH_UNWRAP_MIN_SIZE, MAX_DUPLICATE_INSIGHTS, INSIGHT_WINDOW_PER_ENDPOINT, RESOLVE_AFTER_ABSENCES, RESOLVED_INSIGHT_TTL_MS;
|
|
50
55
|
var init_thresholds = __esm({
|
|
51
56
|
"src/constants/thresholds.ts"() {
|
|
52
57
|
"use strict";
|
|
@@ -76,6 +81,9 @@ var init_thresholds = __esm({
|
|
|
76
81
|
OVERFETCH_MANY_FIELDS = 12;
|
|
77
82
|
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
78
83
|
MAX_DUPLICATE_INSIGHTS = 3;
|
|
84
|
+
INSIGHT_WINDOW_PER_ENDPOINT = 2;
|
|
85
|
+
RESOLVE_AFTER_ABSENCES = 3;
|
|
86
|
+
RESOLVED_INSIGHT_TTL_MS = 18e5;
|
|
79
87
|
}
|
|
80
88
|
});
|
|
81
89
|
|
|
@@ -93,7 +101,7 @@ var init_transport = __esm({
|
|
|
93
101
|
});
|
|
94
102
|
|
|
95
103
|
// src/constants/metrics.ts
|
|
96
|
-
var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS;
|
|
104
|
+
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
105
|
var init_metrics = __esm({
|
|
98
106
|
"src/constants/metrics.ts"() {
|
|
99
107
|
"use strict";
|
|
@@ -102,13 +110,25 @@ var init_metrics = __esm({
|
|
|
102
110
|
METRICS_FLUSH_INTERVAL_MS = 3e4;
|
|
103
111
|
METRICS_MAX_SESSIONS = 50;
|
|
104
112
|
METRICS_MAX_DATA_POINTS = 200;
|
|
113
|
+
PORT_FILE = ".brakit/port";
|
|
114
|
+
FINDINGS_FILE = ".brakit/findings.json";
|
|
115
|
+
FINDINGS_FLUSH_INTERVAL_MS = 1e4;
|
|
105
116
|
}
|
|
106
117
|
});
|
|
107
118
|
|
|
108
119
|
// src/constants/headers.ts
|
|
120
|
+
var SENSITIVE_HEADER_NAMES;
|
|
109
121
|
var init_headers = __esm({
|
|
110
122
|
"src/constants/headers.ts"() {
|
|
111
123
|
"use strict";
|
|
124
|
+
SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
125
|
+
"authorization",
|
|
126
|
+
"cookie",
|
|
127
|
+
"set-cookie",
|
|
128
|
+
"proxy-authorization",
|
|
129
|
+
"x-api-key",
|
|
130
|
+
"x-auth-token"
|
|
131
|
+
]);
|
|
112
132
|
}
|
|
113
133
|
});
|
|
114
134
|
|
|
@@ -148,6 +168,13 @@ var init_network = __esm({
|
|
|
148
168
|
}
|
|
149
169
|
});
|
|
150
170
|
|
|
171
|
+
// src/constants/mcp.ts
|
|
172
|
+
var init_mcp = __esm({
|
|
173
|
+
"src/constants/mcp.ts"() {
|
|
174
|
+
"use strict";
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
151
178
|
// src/constants/index.ts
|
|
152
179
|
var init_constants = __esm({
|
|
153
180
|
"src/constants/index.ts"() {
|
|
@@ -159,6 +186,7 @@ var init_constants = __esm({
|
|
|
159
186
|
init_metrics();
|
|
160
187
|
init_headers();
|
|
161
188
|
init_network();
|
|
189
|
+
init_mcp();
|
|
162
190
|
}
|
|
163
191
|
});
|
|
164
192
|
|
|
@@ -1309,9 +1337,14 @@ var init_math = __esm({
|
|
|
1309
1337
|
function getEndpointKey(method, path) {
|
|
1310
1338
|
return `${method} ${path}`;
|
|
1311
1339
|
}
|
|
1340
|
+
function extractEndpointFromDesc(desc) {
|
|
1341
|
+
return desc.match(ENDPOINT_PREFIX_RE)?.[1] ?? null;
|
|
1342
|
+
}
|
|
1343
|
+
var ENDPOINT_PREFIX_RE;
|
|
1312
1344
|
var init_endpoint = __esm({
|
|
1313
1345
|
"src/utils/endpoint.ts"() {
|
|
1314
1346
|
"use strict";
|
|
1347
|
+
ENDPOINT_PREFIX_RE = /^(\S+\s+\S+)/;
|
|
1315
1348
|
}
|
|
1316
1349
|
});
|
|
1317
1350
|
|
|
@@ -1654,7 +1687,7 @@ function maskSensitiveHeaders(headers) {
|
|
|
1654
1687
|
for (const [key, value] of Object.entries(headers)) {
|
|
1655
1688
|
if (SENSITIVE_HEADER_NAMES.has(key.toLowerCase())) {
|
|
1656
1689
|
const s = String(value);
|
|
1657
|
-
masked[key] = s.length <=
|
|
1690
|
+
masked[key] = s.length <= SENSITIVE_MASK_MIN_LENGTH ? "****" : s.slice(0, SENSITIVE_MASK_VISIBLE_CHARS) + "..." + s.slice(-SENSITIVE_MASK_VISIBLE_CHARS);
|
|
1658
1691
|
} else {
|
|
1659
1692
|
masked[key] = value;
|
|
1660
1693
|
}
|
|
@@ -1701,19 +1734,11 @@ function handleTelemetryGet(req, res, store) {
|
|
|
1701
1734
|
const entries = requestId ? store.getByRequest(requestId) : [...store.getAll()];
|
|
1702
1735
|
sendJson(req, res, 200, { total: entries.length, entries: entries.reverse() });
|
|
1703
1736
|
}
|
|
1704
|
-
var SENSITIVE_HEADER_NAMES;
|
|
1705
1737
|
var init_shared2 = __esm({
|
|
1706
1738
|
"src/dashboard/api/shared.ts"() {
|
|
1707
1739
|
"use strict";
|
|
1708
1740
|
init_constants();
|
|
1709
|
-
|
|
1710
|
-
"authorization",
|
|
1711
|
-
"cookie",
|
|
1712
|
-
"set-cookie",
|
|
1713
|
-
"proxy-authorization",
|
|
1714
|
-
"x-api-key",
|
|
1715
|
-
"x-auth-token"
|
|
1716
|
-
]);
|
|
1741
|
+
init_limits();
|
|
1717
1742
|
}
|
|
1718
1743
|
});
|
|
1719
1744
|
|
|
@@ -1770,7 +1795,7 @@ function handleApiFlows(req, res) {
|
|
|
1770
1795
|
}));
|
|
1771
1796
|
sendJson(req, res, 200, { total: flows.length, flows });
|
|
1772
1797
|
}
|
|
1773
|
-
function createClearHandler(metricsStore) {
|
|
1798
|
+
function createClearHandler(metricsStore, findingStore) {
|
|
1774
1799
|
return (req, res) => {
|
|
1775
1800
|
if (req.method !== "POST") {
|
|
1776
1801
|
sendJson(req, res, 405, { error: "Method not allowed" });
|
|
@@ -1782,6 +1807,7 @@ function createClearHandler(metricsStore) {
|
|
|
1782
1807
|
defaultErrorStore.clear();
|
|
1783
1808
|
defaultQueryStore.clear();
|
|
1784
1809
|
metricsStore.reset();
|
|
1810
|
+
findingStore?.clear();
|
|
1785
1811
|
sendJson(req, res, 200, { cleared: true });
|
|
1786
1812
|
};
|
|
1787
1813
|
}
|
|
@@ -1892,7 +1918,6 @@ function handleApiIngest(req, res) {
|
|
|
1892
1918
|
sendJson(req, res, 405, { error: "Method not allowed" });
|
|
1893
1919
|
return;
|
|
1894
1920
|
}
|
|
1895
|
-
const MAX_INGEST_BYTES = 10 * 1024 * 1024;
|
|
1896
1921
|
const chunks = [];
|
|
1897
1922
|
let totalSize = 0;
|
|
1898
1923
|
req.on("data", (chunk) => {
|
|
@@ -1934,6 +1959,7 @@ var init_ingest = __esm({
|
|
|
1934
1959
|
"src/dashboard/api/ingest.ts"() {
|
|
1935
1960
|
"use strict";
|
|
1936
1961
|
init_store();
|
|
1962
|
+
init_limits();
|
|
1937
1963
|
init_shared2();
|
|
1938
1964
|
}
|
|
1939
1965
|
});
|
|
@@ -2039,13 +2065,13 @@ var init_api = __esm({
|
|
|
2039
2065
|
function createInsightsHandler(engine) {
|
|
2040
2066
|
return (req, res) => {
|
|
2041
2067
|
if (!requireGet(req, res)) return;
|
|
2042
|
-
sendJson(req, res, 200, { insights: engine.
|
|
2068
|
+
sendJson(req, res, 200, { insights: engine.getStatefulInsights() });
|
|
2043
2069
|
};
|
|
2044
2070
|
}
|
|
2045
2071
|
function createSecurityHandler(engine) {
|
|
2046
2072
|
return (req, res) => {
|
|
2047
2073
|
if (!requireGet(req, res)) return;
|
|
2048
|
-
sendJson(req, res, 200, { findings: engine.
|
|
2074
|
+
sendJson(req, res, 200, { findings: engine.getStatefulFindings() });
|
|
2049
2075
|
};
|
|
2050
2076
|
}
|
|
2051
2077
|
var init_insights = __esm({
|
|
@@ -2055,6 +2081,33 @@ var init_insights = __esm({
|
|
|
2055
2081
|
}
|
|
2056
2082
|
});
|
|
2057
2083
|
|
|
2084
|
+
// src/dashboard/api/findings.ts
|
|
2085
|
+
function createFindingsHandler(findingStore) {
|
|
2086
|
+
return (req, res) => {
|
|
2087
|
+
if (!requireGet(req, res)) return;
|
|
2088
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2089
|
+
const stateParam = url.searchParams.get("state");
|
|
2090
|
+
let findings;
|
|
2091
|
+
if (stateParam && VALID_STATES.has(stateParam)) {
|
|
2092
|
+
findings = findingStore.getByState(stateParam);
|
|
2093
|
+
} else {
|
|
2094
|
+
findings = findingStore.getAll();
|
|
2095
|
+
}
|
|
2096
|
+
sendJson(req, res, 200, {
|
|
2097
|
+
total: findings.length,
|
|
2098
|
+
findings
|
|
2099
|
+
});
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
var VALID_STATES;
|
|
2103
|
+
var init_findings = __esm({
|
|
2104
|
+
"src/dashboard/api/findings.ts"() {
|
|
2105
|
+
"use strict";
|
|
2106
|
+
init_shared2();
|
|
2107
|
+
VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
|
|
2058
2111
|
// src/dashboard/sse.ts
|
|
2059
2112
|
function createSSEHandler(engine) {
|
|
2060
2113
|
return (req, res) => {
|
|
@@ -2093,9 +2146,9 @@ data: ${data}
|
|
|
2093
2146
|
const queryListener = (entry) => {
|
|
2094
2147
|
writeEvent("query", JSON.stringify(entry));
|
|
2095
2148
|
};
|
|
2096
|
-
const analysisListener = engine ? (
|
|
2097
|
-
writeEvent("insights", JSON.stringify(
|
|
2098
|
-
writeEvent("security", JSON.stringify(
|
|
2149
|
+
const analysisListener = engine ? ({ statefulInsights, statefulFindings }) => {
|
|
2150
|
+
writeEvent("insights", JSON.stringify(statefulInsights));
|
|
2151
|
+
writeEvent("security", JSON.stringify(statefulFindings));
|
|
2099
2152
|
} : void 0;
|
|
2100
2153
|
onRequest(requestListener);
|
|
2101
2154
|
defaultFetchStore.onEntry(fetchListener);
|
|
@@ -2145,6 +2198,7 @@ function getBaseStyles() {
|
|
|
2145
2198
|
--amber:#d97706;
|
|
2146
2199
|
--red:#dc2626;
|
|
2147
2200
|
--cyan:#0891b2;
|
|
2201
|
+
--green-bg:rgba(22,163,74,0.08);--green-bg-subtle:rgba(22,163,74,0.05);--green-border:rgba(22,163,74,0.2);--green-border-subtle:rgba(22,163,74,0.15);
|
|
2148
2202
|
--sidebar-width:232px;--header-height:52px;
|
|
2149
2203
|
--radius:8px;--radius-sm:6px;
|
|
2150
2204
|
--shadow-sm:0 1px 2px rgba(0,0,0,0.05);
|
|
@@ -2261,7 +2315,7 @@ function getFlowStyles() {
|
|
|
2261
2315
|
.flow-label{font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
|
2262
2316
|
.flow-req-count{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;text-align:right}
|
|
2263
2317
|
.flow-badge-pill{font-size:11px;flex-shrink:0;font-family:var(--mono);font-weight:600;padding:2px 10px;border-radius:10px;text-align:center}
|
|
2264
|
-
.flow-badge-pill.badge-clean{background:
|
|
2318
|
+
.flow-badge-pill.badge-clean{background:var(--green-bg);color:var(--green)}
|
|
2265
2319
|
.flow-badge-pill.badge-warn{background:rgba(217,119,6,0.07);color:var(--amber)}
|
|
2266
2320
|
.flow-badge-pill.badge-error{background:rgba(220,38,38,0.07);color:var(--red)}
|
|
2267
2321
|
.flow-duration{font-family:var(--mono);font-size:12px;color:var(--text-muted);flex-shrink:0;width:60px;text-align:right}
|
|
@@ -2281,7 +2335,7 @@ function getFlowStyles() {
|
|
|
2281
2335
|
|
|
2282
2336
|
/* Method badges */
|
|
2283
2337
|
.method-badge{display:inline-flex;align-items:center;justify-content:center;padding:3px 8px;border-radius:5px;font-size:10px;font-weight:700;font-family:var(--mono);letter-spacing:.3px;flex-shrink:0}
|
|
2284
|
-
.method-badge-GET{background:
|
|
2338
|
+
.method-badge-GET{background:var(--green-bg);color:var(--green)}
|
|
2285
2339
|
.method-badge-POST{background:rgba(37,99,235,0.08);color:var(--blue)}
|
|
2286
2340
|
.method-badge-PUT,.method-badge-PATCH{background:rgba(217,119,6,0.08);color:var(--amber)}
|
|
2287
2341
|
.method-badge-DELETE{background:rgba(220,38,38,0.08);color:var(--red)}
|
|
@@ -2289,7 +2343,7 @@ function getFlowStyles() {
|
|
|
2289
2343
|
|
|
2290
2344
|
/* Status pills */
|
|
2291
2345
|
.status-pill{display:inline-flex;align-items:center;padding:1px 7px;border-radius:4px;font-size:11px;font-weight:600;font-family:var(--mono);flex-shrink:0}
|
|
2292
|
-
.status-pill-2xx{background:
|
|
2346
|
+
.status-pill-2xx{background:var(--green-bg);color:var(--green)}
|
|
2293
2347
|
.status-pill-3xx{background:rgba(8,145,178,0.07);color:var(--cyan)}
|
|
2294
2348
|
.status-pill-4xx{background:rgba(217,119,6,0.07);color:var(--amber)}
|
|
2295
2349
|
.status-pill-5xx{background:rgba(220,38,38,0.07);color:var(--red)}
|
|
@@ -2556,6 +2610,7 @@ function getOverviewStyles() {
|
|
|
2556
2610
|
.ov-card-icon.critical{background:rgba(220,38,38,.08);color:var(--red)}
|
|
2557
2611
|
.ov-card-icon.warning{background:rgba(217,119,6,.08);color:var(--amber)}
|
|
2558
2612
|
.ov-card-icon.info{background:rgba(37,99,235,.08);color:var(--blue)}
|
|
2613
|
+
.ov-card-icon.resolved{background:var(--green-bg);color:var(--green)}
|
|
2559
2614
|
.ov-card-body{flex:1;min-width:0}
|
|
2560
2615
|
.ov-card-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px}
|
|
2561
2616
|
.ov-card-desc{font-size:12px;color:var(--text-dim);line-height:1.5}
|
|
@@ -2572,8 +2627,13 @@ function getOverviewStyles() {
|
|
|
2572
2627
|
.ov-detail-item{font-size:12px;color:var(--text);font-family:var(--mono);padding:2px 0}
|
|
2573
2628
|
|
|
2574
2629
|
/* All-clear banner */
|
|
2575
|
-
.ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:
|
|
2630
|
+
.ov-clear{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--green-bg-subtle);border:1px solid var(--green-border);border-radius:var(--radius);color:var(--green);font-size:13px;font-weight:500}
|
|
2576
2631
|
.ov-clear-icon{font-size:16px}
|
|
2632
|
+
|
|
2633
|
+
/* Resolved section */
|
|
2634
|
+
.ov-resolved-title{margin-top:24px}
|
|
2635
|
+
.ov-card-resolved{opacity:.7;border-color:var(--green-border);cursor:default}
|
|
2636
|
+
.ov-card-resolved:hover{opacity:1;box-shadow:var(--shadow-sm)}
|
|
2577
2637
|
`;
|
|
2578
2638
|
}
|
|
2579
2639
|
var init_overview = __esm({
|
|
@@ -2589,7 +2649,7 @@ function getSecurityStyles() {
|
|
|
2589
2649
|
.sec-container{padding:24px 28px}
|
|
2590
2650
|
|
|
2591
2651
|
/* All-clear */
|
|
2592
|
-
.sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:
|
|
2652
|
+
.sec-clear{display:flex;align-items:center;gap:16px;padding:20px 24px;background:var(--green-bg-subtle);border:1px solid var(--green-border-subtle);border-radius:var(--radius);margin-bottom:24px}
|
|
2593
2653
|
.sec-clear-icon{font-size:24px;color:var(--green);flex-shrink:0}
|
|
2594
2654
|
.sec-clear-title{font-size:15px;font-weight:600;color:var(--green);margin-bottom:2px}
|
|
2595
2655
|
.sec-clear-sub{font-size:12px;color:var(--text-dim)}
|
|
@@ -2625,6 +2685,19 @@ function getSecurityStyles() {
|
|
|
2625
2685
|
.sec-item-desc{color:var(--text-dim);line-height:1.5;flex:1;min-width:0}
|
|
2626
2686
|
.sec-item-desc strong{color:var(--text);font-family:var(--mono);font-weight:600}
|
|
2627
2687
|
.sec-item-count{font-size:10px;font-family:var(--mono);color:var(--text-muted);flex-shrink:0;margin-left:12px}
|
|
2688
|
+
|
|
2689
|
+
/* Resolved badge in summary */
|
|
2690
|
+
.sec-resolved-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:10px;background:var(--green-bg);color:var(--green);margin-left:12px}
|
|
2691
|
+
|
|
2692
|
+
/* Resolved section */
|
|
2693
|
+
.sec-resolved-title{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;color:var(--text-dim);margin:20px 0 8px 0}
|
|
2694
|
+
.sec-resolved-check{color:var(--green);font-size:14px}
|
|
2695
|
+
.sec-resolved-count{font-size:11px;font-family:var(--mono);color:var(--text-muted);background:var(--bg-muted);padding:1px 8px;border-radius:10px;border:1px solid var(--border)}
|
|
2696
|
+
.sec-group-resolved{opacity:.7;border-color:var(--green-border)}
|
|
2697
|
+
.sec-group-resolved:hover{opacity:1}
|
|
2698
|
+
.sec-item-resolved{color:var(--text-muted)}
|
|
2699
|
+
.sec-item-resolved .sec-item-desc{text-decoration:line-through;text-decoration-color:var(--text-muted)}
|
|
2700
|
+
.sec-resolved-item-icon{color:var(--green);font-size:12px;flex-shrink:0;margin-right:8px}
|
|
2628
2701
|
`;
|
|
2629
2702
|
}
|
|
2630
2703
|
var init_security = __esm({
|
|
@@ -2687,6 +2760,197 @@ var init_styles = __esm({
|
|
|
2687
2760
|
}
|
|
2688
2761
|
});
|
|
2689
2762
|
|
|
2763
|
+
// src/store/finding-id.ts
|
|
2764
|
+
import { createHash } from "crypto";
|
|
2765
|
+
function computeFindingId(finding) {
|
|
2766
|
+
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
2767
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
2768
|
+
}
|
|
2769
|
+
var init_finding_id = __esm({
|
|
2770
|
+
"src/store/finding-id.ts"() {
|
|
2771
|
+
"use strict";
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
// src/store/finding-store.ts
|
|
2776
|
+
import {
|
|
2777
|
+
readFileSync as readFileSync3,
|
|
2778
|
+
writeFileSync as writeFileSync3,
|
|
2779
|
+
existsSync as existsSync3,
|
|
2780
|
+
mkdirSync as mkdirSync3,
|
|
2781
|
+
renameSync as renameSync2
|
|
2782
|
+
} from "fs";
|
|
2783
|
+
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
2784
|
+
import { resolve as resolve3 } from "path";
|
|
2785
|
+
var FindingStore;
|
|
2786
|
+
var init_finding_store = __esm({
|
|
2787
|
+
"src/store/finding-store.ts"() {
|
|
2788
|
+
"use strict";
|
|
2789
|
+
init_constants();
|
|
2790
|
+
init_fs();
|
|
2791
|
+
init_finding_id();
|
|
2792
|
+
FindingStore = class {
|
|
2793
|
+
constructor(rootDir) {
|
|
2794
|
+
this.rootDir = rootDir;
|
|
2795
|
+
this.metricsDir = resolve3(rootDir, METRICS_DIR);
|
|
2796
|
+
this.findingsPath = resolve3(rootDir, FINDINGS_FILE);
|
|
2797
|
+
this.tmpPath = this.findingsPath + ".tmp";
|
|
2798
|
+
this.load();
|
|
2799
|
+
}
|
|
2800
|
+
findings = /* @__PURE__ */ new Map();
|
|
2801
|
+
flushTimer = null;
|
|
2802
|
+
dirty = false;
|
|
2803
|
+
writing = false;
|
|
2804
|
+
findingsPath;
|
|
2805
|
+
tmpPath;
|
|
2806
|
+
metricsDir;
|
|
2807
|
+
start() {
|
|
2808
|
+
this.flushTimer = setInterval(
|
|
2809
|
+
() => this.flush(),
|
|
2810
|
+
FINDINGS_FLUSH_INTERVAL_MS
|
|
2811
|
+
);
|
|
2812
|
+
this.flushTimer.unref();
|
|
2813
|
+
}
|
|
2814
|
+
stop() {
|
|
2815
|
+
if (this.flushTimer) {
|
|
2816
|
+
clearInterval(this.flushTimer);
|
|
2817
|
+
this.flushTimer = null;
|
|
2818
|
+
}
|
|
2819
|
+
this.flushSync();
|
|
2820
|
+
}
|
|
2821
|
+
upsert(finding, source) {
|
|
2822
|
+
const id = computeFindingId(finding);
|
|
2823
|
+
const existing = this.findings.get(id);
|
|
2824
|
+
const now = Date.now();
|
|
2825
|
+
if (existing) {
|
|
2826
|
+
existing.lastSeenAt = now;
|
|
2827
|
+
existing.occurrences++;
|
|
2828
|
+
existing.finding = finding;
|
|
2829
|
+
if (existing.state === "resolved") {
|
|
2830
|
+
existing.state = "open";
|
|
2831
|
+
existing.resolvedAt = null;
|
|
2832
|
+
}
|
|
2833
|
+
this.dirty = true;
|
|
2834
|
+
return existing;
|
|
2835
|
+
}
|
|
2836
|
+
const stateful = {
|
|
2837
|
+
findingId: id,
|
|
2838
|
+
state: "open",
|
|
2839
|
+
source,
|
|
2840
|
+
finding,
|
|
2841
|
+
firstSeenAt: now,
|
|
2842
|
+
lastSeenAt: now,
|
|
2843
|
+
resolvedAt: null,
|
|
2844
|
+
occurrences: 1
|
|
2845
|
+
};
|
|
2846
|
+
this.findings.set(id, stateful);
|
|
2847
|
+
this.dirty = true;
|
|
2848
|
+
return stateful;
|
|
2849
|
+
}
|
|
2850
|
+
transition(findingId, state) {
|
|
2851
|
+
const finding = this.findings.get(findingId);
|
|
2852
|
+
if (!finding) return false;
|
|
2853
|
+
finding.state = state;
|
|
2854
|
+
if (state === "resolved") {
|
|
2855
|
+
finding.resolvedAt = Date.now();
|
|
2856
|
+
}
|
|
2857
|
+
this.dirty = true;
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2860
|
+
reconcilePassive(currentFindings) {
|
|
2861
|
+
const currentIds = new Set(currentFindings.map(computeFindingId));
|
|
2862
|
+
for (const [id, stateful] of this.findings) {
|
|
2863
|
+
if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
|
|
2864
|
+
stateful.state = "resolved";
|
|
2865
|
+
stateful.resolvedAt = Date.now();
|
|
2866
|
+
this.dirty = true;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
getAll() {
|
|
2871
|
+
return [...this.findings.values()];
|
|
2872
|
+
}
|
|
2873
|
+
getByState(state) {
|
|
2874
|
+
return [...this.findings.values()].filter((f) => f.state === state);
|
|
2875
|
+
}
|
|
2876
|
+
get(findingId) {
|
|
2877
|
+
return this.findings.get(findingId);
|
|
2878
|
+
}
|
|
2879
|
+
clear() {
|
|
2880
|
+
this.findings.clear();
|
|
2881
|
+
this.dirty = true;
|
|
2882
|
+
}
|
|
2883
|
+
load() {
|
|
2884
|
+
try {
|
|
2885
|
+
if (existsSync3(this.findingsPath)) {
|
|
2886
|
+
const raw = readFileSync3(this.findingsPath, "utf-8");
|
|
2887
|
+
const parsed = JSON.parse(raw);
|
|
2888
|
+
if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
|
|
2889
|
+
for (const f of parsed.findings) {
|
|
2890
|
+
this.findings.set(f.findingId, f);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
} catch {
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
flush() {
|
|
2898
|
+
if (!this.dirty) return;
|
|
2899
|
+
this.writeAsync();
|
|
2900
|
+
}
|
|
2901
|
+
flushSync() {
|
|
2902
|
+
if (!this.dirty) return;
|
|
2903
|
+
try {
|
|
2904
|
+
this.ensureDir();
|
|
2905
|
+
const data = {
|
|
2906
|
+
version: 1,
|
|
2907
|
+
findings: [...this.findings.values()]
|
|
2908
|
+
};
|
|
2909
|
+
writeFileSync3(this.tmpPath, JSON.stringify(data));
|
|
2910
|
+
renameSync2(this.tmpPath, this.findingsPath);
|
|
2911
|
+
this.dirty = false;
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
process.stderr.write(
|
|
2914
|
+
`[brakit] failed to save findings: ${err.message}
|
|
2915
|
+
`
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
async writeAsync() {
|
|
2920
|
+
if (this.writing) return;
|
|
2921
|
+
this.writing = true;
|
|
2922
|
+
try {
|
|
2923
|
+
if (!existsSync3(this.metricsDir)) {
|
|
2924
|
+
await mkdir2(this.metricsDir, { recursive: true });
|
|
2925
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
2926
|
+
}
|
|
2927
|
+
const data = {
|
|
2928
|
+
version: 1,
|
|
2929
|
+
findings: [...this.findings.values()]
|
|
2930
|
+
};
|
|
2931
|
+
await writeFile3(this.tmpPath, JSON.stringify(data));
|
|
2932
|
+
await rename2(this.tmpPath, this.findingsPath);
|
|
2933
|
+
this.dirty = false;
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
process.stderr.write(
|
|
2936
|
+
`[brakit] failed to save findings: ${err.message}
|
|
2937
|
+
`
|
|
2938
|
+
);
|
|
2939
|
+
} finally {
|
|
2940
|
+
this.writing = false;
|
|
2941
|
+
if (this.dirty) this.writeAsync();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
ensureDir() {
|
|
2945
|
+
if (!existsSync3(this.metricsDir)) {
|
|
2946
|
+
mkdirSync3(this.metricsDir, { recursive: true });
|
|
2947
|
+
ensureGitignore(this.metricsDir, METRICS_DIR);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2690
2954
|
// src/detect/project.ts
|
|
2691
2955
|
import { readFile as readFile2 } from "fs/promises";
|
|
2692
2956
|
import { join } from "path";
|
|
@@ -3359,6 +3623,23 @@ function createEndpointGroup() {
|
|
|
3359
3623
|
queryShapeDurations: /* @__PURE__ */ new Map()
|
|
3360
3624
|
};
|
|
3361
3625
|
}
|
|
3626
|
+
function windowByEndpoint(requests) {
|
|
3627
|
+
const byEndpoint = /* @__PURE__ */ new Map();
|
|
3628
|
+
for (const r of requests) {
|
|
3629
|
+
const ep = getEndpointKey(r.method, r.path);
|
|
3630
|
+
let list = byEndpoint.get(ep);
|
|
3631
|
+
if (!list) {
|
|
3632
|
+
list = [];
|
|
3633
|
+
byEndpoint.set(ep, list);
|
|
3634
|
+
}
|
|
3635
|
+
list.push(r);
|
|
3636
|
+
}
|
|
3637
|
+
const windowed = [];
|
|
3638
|
+
for (const [, reqs] of byEndpoint) {
|
|
3639
|
+
windowed.push(...reqs.slice(-INSIGHT_WINDOW_PER_ENDPOINT));
|
|
3640
|
+
}
|
|
3641
|
+
return windowed;
|
|
3642
|
+
}
|
|
3362
3643
|
function prepareContext(ctx) {
|
|
3363
3644
|
const nonStatic = ctx.requests.filter(
|
|
3364
3645
|
(r) => !r.isStatic && (!r.path || !r.path.startsWith(DASHBOARD_PREFIX))
|
|
@@ -3366,8 +3647,9 @@ function prepareContext(ctx) {
|
|
|
3366
3647
|
const queriesByReq = groupBy(ctx.queries, (q) => q.parentRequestId);
|
|
3367
3648
|
const fetchesByReq = groupBy(ctx.fetches, (f) => f.parentRequestId);
|
|
3368
3649
|
const reqById = new Map(nonStatic.map((r) => [r.id, r]));
|
|
3650
|
+
const recent = windowByEndpoint(nonStatic);
|
|
3369
3651
|
const endpointGroups = /* @__PURE__ */ new Map();
|
|
3370
|
-
for (const r of
|
|
3652
|
+
for (const r of recent) {
|
|
3371
3653
|
const ep = getEndpointKey(r.method, r.path);
|
|
3372
3654
|
let g = endpointGroups.get(ep);
|
|
3373
3655
|
if (!g) {
|
|
@@ -3412,6 +3694,7 @@ var init_prepare = __esm({
|
|
|
3412
3694
|
init_collections();
|
|
3413
3695
|
init_endpoint();
|
|
3414
3696
|
init_constants();
|
|
3697
|
+
init_thresholds();
|
|
3415
3698
|
init_query_helpers();
|
|
3416
3699
|
}
|
|
3417
3700
|
});
|
|
@@ -4100,6 +4383,69 @@ var init_insights3 = __esm({
|
|
|
4100
4383
|
}
|
|
4101
4384
|
});
|
|
4102
4385
|
|
|
4386
|
+
// src/analysis/insight-tracker.ts
|
|
4387
|
+
function computeInsightKey(insight) {
|
|
4388
|
+
const identifier = extractEndpointFromDesc(insight.desc) ?? insight.title;
|
|
4389
|
+
return `${insight.type}:${identifier}`;
|
|
4390
|
+
}
|
|
4391
|
+
var InsightTracker;
|
|
4392
|
+
var init_insight_tracker = __esm({
|
|
4393
|
+
"src/analysis/insight-tracker.ts"() {
|
|
4394
|
+
"use strict";
|
|
4395
|
+
init_endpoint();
|
|
4396
|
+
init_thresholds();
|
|
4397
|
+
InsightTracker = class {
|
|
4398
|
+
tracked = /* @__PURE__ */ new Map();
|
|
4399
|
+
reconcile(current) {
|
|
4400
|
+
const currentKeys = /* @__PURE__ */ new Set();
|
|
4401
|
+
const now = Date.now();
|
|
4402
|
+
for (const insight of current) {
|
|
4403
|
+
const key = computeInsightKey(insight);
|
|
4404
|
+
currentKeys.add(key);
|
|
4405
|
+
const existing = this.tracked.get(key);
|
|
4406
|
+
if (existing) {
|
|
4407
|
+
existing.insight = insight;
|
|
4408
|
+
existing.lastSeenAt = now;
|
|
4409
|
+
existing.consecutiveAbsences = 0;
|
|
4410
|
+
if (existing.state === "resolved") {
|
|
4411
|
+
existing.state = "open";
|
|
4412
|
+
existing.resolvedAt = null;
|
|
4413
|
+
}
|
|
4414
|
+
} else {
|
|
4415
|
+
this.tracked.set(key, {
|
|
4416
|
+
key,
|
|
4417
|
+
state: "open",
|
|
4418
|
+
insight,
|
|
4419
|
+
firstSeenAt: now,
|
|
4420
|
+
lastSeenAt: now,
|
|
4421
|
+
resolvedAt: null,
|
|
4422
|
+
consecutiveAbsences: 0
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
for (const [key, stateful] of this.tracked) {
|
|
4427
|
+
if (stateful.state === "open" && !currentKeys.has(stateful.key)) {
|
|
4428
|
+
stateful.consecutiveAbsences++;
|
|
4429
|
+
if (stateful.consecutiveAbsences >= RESOLVE_AFTER_ABSENCES) {
|
|
4430
|
+
stateful.state = "resolved";
|
|
4431
|
+
stateful.resolvedAt = now;
|
|
4432
|
+
}
|
|
4433
|
+
} else if (stateful.state === "resolved" && stateful.resolvedAt !== null && now - stateful.resolvedAt > RESOLVED_INSIGHT_TTL_MS) {
|
|
4434
|
+
this.tracked.delete(key);
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
return [...this.tracked.values()];
|
|
4438
|
+
}
|
|
4439
|
+
getAll() {
|
|
4440
|
+
return [...this.tracked.values()];
|
|
4441
|
+
}
|
|
4442
|
+
clear() {
|
|
4443
|
+
this.tracked.clear();
|
|
4444
|
+
}
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
});
|
|
4448
|
+
|
|
4103
4449
|
// src/analysis/engine.ts
|
|
4104
4450
|
var AnalysisEngine;
|
|
4105
4451
|
var init_engine = __esm({
|
|
@@ -4111,9 +4457,11 @@ var init_engine = __esm({
|
|
|
4111
4457
|
init_group();
|
|
4112
4458
|
init_rules();
|
|
4113
4459
|
init_insights3();
|
|
4460
|
+
init_insight_tracker();
|
|
4114
4461
|
AnalysisEngine = class {
|
|
4115
|
-
constructor(metricsStore, debounceMs = 300) {
|
|
4462
|
+
constructor(metricsStore, findingStore, debounceMs = 300) {
|
|
4116
4463
|
this.metricsStore = metricsStore;
|
|
4464
|
+
this.findingStore = findingStore;
|
|
4117
4465
|
this.debounceMs = debounceMs;
|
|
4118
4466
|
this.scanner = createDefaultScanner();
|
|
4119
4467
|
this.boundRequestListener = () => this.scheduleRecompute();
|
|
@@ -4122,8 +4470,10 @@ var init_engine = __esm({
|
|
|
4122
4470
|
this.boundLogListener = () => this.scheduleRecompute();
|
|
4123
4471
|
}
|
|
4124
4472
|
scanner;
|
|
4473
|
+
insightTracker = new InsightTracker();
|
|
4125
4474
|
cachedInsights = [];
|
|
4126
4475
|
cachedFindings = [];
|
|
4476
|
+
cachedStatefulInsights = [];
|
|
4127
4477
|
debounceTimer = null;
|
|
4128
4478
|
listeners = [];
|
|
4129
4479
|
boundRequestListener;
|
|
@@ -4159,6 +4509,12 @@ var init_engine = __esm({
|
|
|
4159
4509
|
getFindings() {
|
|
4160
4510
|
return this.cachedFindings;
|
|
4161
4511
|
}
|
|
4512
|
+
getStatefulFindings() {
|
|
4513
|
+
return this.findingStore?.getAll() ?? [];
|
|
4514
|
+
}
|
|
4515
|
+
getStatefulInsights() {
|
|
4516
|
+
return this.cachedStatefulInsights;
|
|
4517
|
+
}
|
|
4162
4518
|
scheduleRecompute() {
|
|
4163
4519
|
if (this.debounceTimer) return;
|
|
4164
4520
|
this.debounceTimer = setTimeout(() => {
|
|
@@ -4174,6 +4530,12 @@ var init_engine = __esm({
|
|
|
4174
4530
|
const fetches = defaultFetchStore.getAll();
|
|
4175
4531
|
const flows = groupRequestsIntoFlows(requests);
|
|
4176
4532
|
this.cachedFindings = this.scanner.scan({ requests, logs });
|
|
4533
|
+
if (this.findingStore) {
|
|
4534
|
+
for (const finding of this.cachedFindings) {
|
|
4535
|
+
this.findingStore.upsert(finding, "passive");
|
|
4536
|
+
}
|
|
4537
|
+
this.findingStore.reconcilePassive(this.cachedFindings);
|
|
4538
|
+
}
|
|
4177
4539
|
this.cachedInsights = computeInsights({
|
|
4178
4540
|
requests,
|
|
4179
4541
|
queries,
|
|
@@ -4183,9 +4545,16 @@ var init_engine = __esm({
|
|
|
4183
4545
|
previousMetrics: this.metricsStore.getAll(),
|
|
4184
4546
|
securityFindings: this.cachedFindings
|
|
4185
4547
|
});
|
|
4548
|
+
this.cachedStatefulInsights = this.insightTracker.reconcile(this.cachedInsights);
|
|
4549
|
+
const update = {
|
|
4550
|
+
insights: this.cachedInsights,
|
|
4551
|
+
findings: this.cachedFindings,
|
|
4552
|
+
statefulFindings: this.getStatefulFindings(),
|
|
4553
|
+
statefulInsights: this.cachedStatefulInsights
|
|
4554
|
+
};
|
|
4186
4555
|
for (const fn of this.listeners) {
|
|
4187
4556
|
try {
|
|
4188
|
-
fn(
|
|
4557
|
+
fn(update);
|
|
4189
4558
|
} catch {
|
|
4190
4559
|
}
|
|
4191
4560
|
}
|
|
@@ -4199,13 +4568,14 @@ var VERSION;
|
|
|
4199
4568
|
var init_src = __esm({
|
|
4200
4569
|
"src/index.ts"() {
|
|
4201
4570
|
"use strict";
|
|
4571
|
+
init_finding_store();
|
|
4202
4572
|
init_project();
|
|
4203
4573
|
init_adapter_registry();
|
|
4204
4574
|
init_rules();
|
|
4205
4575
|
init_engine();
|
|
4206
4576
|
init_insights3();
|
|
4207
4577
|
init_insights2();
|
|
4208
|
-
VERSION = "0.
|
|
4578
|
+
VERSION = "0.8.1";
|
|
4209
4579
|
}
|
|
4210
4580
|
});
|
|
4211
4581
|
|
|
@@ -4383,7 +4753,7 @@ var init_thresholds2 = __esm({
|
|
|
4383
4753
|
});
|
|
4384
4754
|
|
|
4385
4755
|
// src/dashboard/client/constants/display.ts
|
|
4386
|
-
var QUERY_OP_COLORS, LOG_LEVEL_COLORS, GRAPH_COLORS, DOT_COLORS, HEALTH_GRADES, CHART_GRID_COLOR, CHART_LABEL_COLOR, CHART_FONT, CHART_FONT_SM, CHART_FONT_XS, CHART_PAD, TL_TYPE_COLORS, TL_TYPE_LABELS, SENSITIVE_HEADERS, HTTP_STATUS_MAP, NAV_LABELS, CURL_SKIP_HEADERS;
|
|
4756
|
+
var QUERY_OP_COLORS, LOG_LEVEL_COLORS, GRAPH_COLORS, DOT_COLORS, HEALTH_GRADES, CHART_GRID_COLOR, CHART_LABEL_COLOR, CHART_FONT, CHART_FONT_SM, CHART_FONT_XS, CHART_PAD, TL_TYPE_COLORS, TL_TYPE_LABELS, SENSITIVE_HEADERS, HTTP_STATUS_MAP, NAV_LABELS, CURL_SKIP_HEADERS, SEVERITY_MAP;
|
|
4387
4757
|
var init_display = __esm({
|
|
4388
4758
|
"src/dashboard/client/constants/display.ts"() {
|
|
4389
4759
|
"use strict";
|
|
@@ -4411,6 +4781,7 @@ var init_display = __esm({
|
|
|
4411
4781
|
HTTP_STATUS_MAP = `{400:'Bad Request',401:'Unauthorized',403:'Forbidden',404:'Not Found',405:'Method Not Allowed',408:'Timeout',409:'Conflict',422:'Unprocessable',429:'Too Many Requests',500:'Internal Server Error',502:'Bad Gateway',503:'Service Unavailable',504:'Gateway Timeout'}`;
|
|
4412
4782
|
NAV_LABELS = `{ queries: 'Queries', requests: 'Requests', actions: 'Actions', errors: 'Errors', security: 'Security', fetches: 'Fetches', logs: 'Logs', performance: 'Performance' }`;
|
|
4413
4783
|
CURL_SKIP_HEADERS = `['host', 'connection', 'accept-encoding']`;
|
|
4784
|
+
SEVERITY_MAP = `{ critical: { icon: '\\u2717', cls: 'critical', sort: 0 }, warning: { icon: '\\u26A0', cls: 'warning', sort: 1 }, info: { icon: '\\u2139', cls: 'info', sort: 2 } }`;
|
|
4414
4785
|
}
|
|
4415
4786
|
});
|
|
4416
4787
|
|
|
@@ -6150,9 +6521,11 @@ function getOverviewRender() {
|
|
|
6150
6521
|
'<div class="ov-stat"><span class="ov-stat-value">' + state.fetches.length + '</span><span class="ov-stat-label">Fetches</span></div>';
|
|
6151
6522
|
container.appendChild(summary);
|
|
6152
6523
|
|
|
6153
|
-
var
|
|
6524
|
+
var all = state.insights || [];
|
|
6525
|
+
var open = all.filter(function(si) { return si.state === 'open'; });
|
|
6526
|
+
var resolved = all.filter(function(si) { return si.state === 'resolved'; });
|
|
6154
6527
|
|
|
6155
|
-
if (
|
|
6528
|
+
if (open.length === 0 && resolved.length === 0) {
|
|
6156
6529
|
var clear = document.createElement('div');
|
|
6157
6530
|
clear.className = 'ov-clear';
|
|
6158
6531
|
clear.innerHTML = '<span class="ov-clear-icon">\\u2713</span>All clear \u2014 no issues detected';
|
|
@@ -6160,67 +6533,104 @@ function getOverviewRender() {
|
|
|
6160
6533
|
return;
|
|
6161
6534
|
}
|
|
6162
6535
|
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
cards.className = 'ov-cards';
|
|
6536
|
+
if (open.length === 0 && resolved.length > 0) {
|
|
6537
|
+
var allFixed = document.createElement('div');
|
|
6538
|
+
allFixed.className = 'ov-clear';
|
|
6539
|
+
allFixed.innerHTML = '<span class="ov-clear-icon">\\u2713</span>All issues resolved \u2014 ' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed';
|
|
6540
|
+
container.appendChild(allFixed);
|
|
6541
|
+
}
|
|
6170
6542
|
|
|
6171
6543
|
var NAV_LABELS = ${NAV_LABELS};
|
|
6544
|
+
var SEV = ${SEVERITY_MAP};
|
|
6172
6545
|
|
|
6173
|
-
|
|
6174
|
-
(
|
|
6175
|
-
|
|
6176
|
-
|
|
6546
|
+
if (open.length > 0) {
|
|
6547
|
+
var title = document.createElement('div');
|
|
6548
|
+
title.className = 'ov-section-title';
|
|
6549
|
+
title.innerHTML = 'Issues Found <span class="ov-issue-count">' + open.length + '</span>';
|
|
6550
|
+
container.appendChild(title);
|
|
6177
6551
|
|
|
6178
|
-
|
|
6179
|
-
|
|
6552
|
+
var cards = document.createElement('div');
|
|
6553
|
+
cards.className = 'ov-cards';
|
|
6554
|
+
|
|
6555
|
+
for (var i = 0; i < open.length; i++) {
|
|
6556
|
+
(function(si) {
|
|
6557
|
+
var insight = si.insight;
|
|
6558
|
+
var card = document.createElement('div');
|
|
6559
|
+
card.className = 'ov-card';
|
|
6560
|
+
|
|
6561
|
+
var sevCfg = SEV[insight.severity];
|
|
6562
|
+
var iconCls = sevCfg.cls;
|
|
6563
|
+
var iconChar = sevCfg.icon;
|
|
6564
|
+
|
|
6565
|
+
var expandHtml = '';
|
|
6566
|
+
if (insight.detail) expandHtml += insight.detail;
|
|
6567
|
+
if (insight.hint) expandHtml += '<div class="ov-card-hint">' + escHtml(insight.hint) + '</div>';
|
|
6568
|
+
expandHtml += '<span class="ov-card-link" data-nav="' + insight.nav + '">View in ' + (NAV_LABELS[insight.nav] || insight.nav) + ' \\u2192</span>';
|
|
6569
|
+
|
|
6570
|
+
card.innerHTML =
|
|
6571
|
+
'<span class="ov-card-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
6572
|
+
'<div class="ov-card-body">' +
|
|
6573
|
+
'<div class="ov-card-title">' + escHtml(insight.title) + '</div>' +
|
|
6574
|
+
'<div class="ov-card-desc">' + insight.desc + '</div>' +
|
|
6575
|
+
'<div class="ov-card-expand">' + expandHtml + '</div>' +
|
|
6576
|
+
'</div>' +
|
|
6577
|
+
'<span class="ov-card-arrow">\\u2192</span>';
|
|
6578
|
+
|
|
6579
|
+
card.addEventListener('click', function(e) {
|
|
6580
|
+
var target = e.target;
|
|
6581
|
+
while (target && target !== card) {
|
|
6582
|
+
if (target.classList && target.classList.contains('ov-card-link')) {
|
|
6583
|
+
var navView = target.getAttribute('data-nav');
|
|
6584
|
+
var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
|
|
6585
|
+
if (sidebarItem) sidebarItem.click();
|
|
6586
|
+
return;
|
|
6587
|
+
}
|
|
6588
|
+
target = target.parentElement;
|
|
6589
|
+
}
|
|
6590
|
+
var expand = card.querySelector('.ov-card-expand');
|
|
6591
|
+
var arrow = card.querySelector('.ov-card-arrow');
|
|
6592
|
+
if (card.classList.contains('expanded')) {
|
|
6593
|
+
card.classList.remove('expanded');
|
|
6594
|
+
expand.style.display = 'none';
|
|
6595
|
+
arrow.textContent = '\\u2192';
|
|
6596
|
+
} else {
|
|
6597
|
+
card.classList.add('expanded');
|
|
6598
|
+
expand.style.display = 'block';
|
|
6599
|
+
arrow.textContent = '\\u2193';
|
|
6600
|
+
}
|
|
6601
|
+
});
|
|
6180
6602
|
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6603
|
+
cards.appendChild(card);
|
|
6604
|
+
})(open[i]);
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
container.appendChild(cards);
|
|
6608
|
+
}
|
|
6185
6609
|
|
|
6186
|
-
|
|
6187
|
-
|
|
6610
|
+
if (resolved.length > 0) {
|
|
6611
|
+
var resolvedTitle = document.createElement('div');
|
|
6612
|
+
resolvedTitle.className = 'ov-section-title ov-resolved-title';
|
|
6613
|
+
resolvedTitle.innerHTML = '<span style="color:var(--green)">\\u2713</span> Resolved <span class="ov-issue-count">' + resolved.length + '</span>';
|
|
6614
|
+
container.appendChild(resolvedTitle);
|
|
6615
|
+
|
|
6616
|
+
var resolvedCards = document.createElement('div');
|
|
6617
|
+
resolvedCards.className = 'ov-cards';
|
|
6618
|
+
|
|
6619
|
+
for (var ri = 0; ri < resolved.length; ri++) {
|
|
6620
|
+
var rInsight = resolved[ri].insight;
|
|
6621
|
+
var rCard = document.createElement('div');
|
|
6622
|
+
rCard.className = 'ov-card ov-card-resolved';
|
|
6623
|
+
rCard.innerHTML =
|
|
6624
|
+
'<span class="ov-card-icon resolved">\\u2713</span>' +
|
|
6188
6625
|
'<div class="ov-card-body">' +
|
|
6189
|
-
'<div class="ov-card-title">' + escHtml(
|
|
6190
|
-
'<div class="ov-card-desc">' +
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
card.addEventListener('click', function(e) {
|
|
6196
|
-
var target = e.target;
|
|
6197
|
-
while (target && target !== card) {
|
|
6198
|
-
if (target.classList && target.classList.contains('ov-card-link')) {
|
|
6199
|
-
var navView = target.getAttribute('data-nav');
|
|
6200
|
-
var sidebarItem = document.querySelector('.sidebar-item[data-view="' + navView + '"]');
|
|
6201
|
-
if (sidebarItem) sidebarItem.click();
|
|
6202
|
-
return;
|
|
6203
|
-
}
|
|
6204
|
-
target = target.parentElement;
|
|
6205
|
-
}
|
|
6206
|
-
var expand = card.querySelector('.ov-card-expand');
|
|
6207
|
-
var arrow = card.querySelector('.ov-card-arrow');
|
|
6208
|
-
if (card.classList.contains('expanded')) {
|
|
6209
|
-
card.classList.remove('expanded');
|
|
6210
|
-
expand.style.display = 'none';
|
|
6211
|
-
arrow.textContent = '\\u2192';
|
|
6212
|
-
} else {
|
|
6213
|
-
card.classList.add('expanded');
|
|
6214
|
-
expand.style.display = 'block';
|
|
6215
|
-
arrow.textContent = '\\u2193';
|
|
6216
|
-
}
|
|
6217
|
-
});
|
|
6626
|
+
'<div class="ov-card-title" style="text-decoration:line-through;color:var(--text-muted)">' + escHtml(rInsight.title) + '</div>' +
|
|
6627
|
+
'<div class="ov-card-desc">' + rInsight.desc + '</div>' +
|
|
6628
|
+
'</div>';
|
|
6629
|
+
resolvedCards.appendChild(rCard);
|
|
6630
|
+
}
|
|
6218
6631
|
|
|
6219
|
-
|
|
6220
|
-
})(insights[i]);
|
|
6632
|
+
container.appendChild(resolvedCards);
|
|
6221
6633
|
}
|
|
6222
|
-
|
|
6223
|
-
container.appendChild(cards);
|
|
6224
6634
|
}
|
|
6225
6635
|
`;
|
|
6226
6636
|
}
|
|
@@ -6250,10 +6660,13 @@ function getSecurityView() {
|
|
|
6250
6660
|
var container = document.getElementById('security-content');
|
|
6251
6661
|
if (!container) return;
|
|
6252
6662
|
container.innerHTML = '';
|
|
6663
|
+
var SEV = ${SEVERITY_MAP};
|
|
6253
6664
|
|
|
6254
|
-
var
|
|
6665
|
+
var all = state.findings || [];
|
|
6666
|
+
var open = all.filter(function(f) { return f.state === 'open' || f.state === 'fixing'; });
|
|
6667
|
+
var resolved = all.filter(function(f) { return f.state === 'resolved'; });
|
|
6255
6668
|
|
|
6256
|
-
if (
|
|
6669
|
+
if (open.length === 0 && resolved.length === 0) {
|
|
6257
6670
|
var hasData = state.requests.length > 0 || state.logs.length > 0 || state.queries.length > 0;
|
|
6258
6671
|
if (!hasData) {
|
|
6259
6672
|
container.innerHTML = '<div class="empty" style="height:400px"><span class="empty-title">Waiting for requests...</span><span class="empty-sub">Start using your app to see security findings here</span></div>';
|
|
@@ -6264,17 +6677,20 @@ function getSecurityView() {
|
|
|
6264
6677
|
}
|
|
6265
6678
|
|
|
6266
6679
|
var critCount = 0, warnCount = 0, infoCount = 0;
|
|
6267
|
-
for (var ci = 0; ci <
|
|
6268
|
-
|
|
6269
|
-
|
|
6680
|
+
for (var ci = 0; ci < open.length; ci++) {
|
|
6681
|
+
var sev = open[ci].finding.severity;
|
|
6682
|
+
if (sev === 'critical') critCount++;
|
|
6683
|
+
else if (sev === 'info') infoCount++;
|
|
6270
6684
|
else warnCount++;
|
|
6271
6685
|
}
|
|
6686
|
+
|
|
6272
6687
|
var summaryEl = document.createElement('div');
|
|
6273
6688
|
summaryEl.className = 'sec-summary';
|
|
6274
6689
|
summaryEl.innerHTML =
|
|
6275
6690
|
'<div class="sec-summary-left">' +
|
|
6276
|
-
'<span class="sec-summary-count">' +
|
|
6277
|
-
'<span class="sec-summary-label">issue' + (
|
|
6691
|
+
'<span class="sec-summary-count">' + open.length + '</span>' +
|
|
6692
|
+
'<span class="sec-summary-label">open issue' + (open.length !== 1 ? 's' : '') + '</span>' +
|
|
6693
|
+
(resolved.length > 0 ? '<span class="sec-resolved-badge">' + resolved.length + ' resolved</span>' : '') +
|
|
6278
6694
|
'</div>' +
|
|
6279
6695
|
'<div class="sec-summary-right">' +
|
|
6280
6696
|
(critCount > 0 ? '<span class="sec-badge critical">' + critCount + ' critical</span>' : '') +
|
|
@@ -6283,60 +6699,93 @@ function getSecurityView() {
|
|
|
6283
6699
|
'</div>';
|
|
6284
6700
|
container.appendChild(summaryEl);
|
|
6285
6701
|
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
6292
|
-
groupOrder.push(f.rule);
|
|
6293
|
-
}
|
|
6294
|
-
groups[f.rule].items.push(f);
|
|
6702
|
+
if (open.length === 0 && resolved.length > 0) {
|
|
6703
|
+
var allFixed = document.createElement('div');
|
|
6704
|
+
allFixed.className = 'sec-clear';
|
|
6705
|
+
allFixed.innerHTML = '<span class="sec-clear-icon">\\u2713</span><div class="sec-clear-text"><div class="sec-clear-title">All issues resolved</div><div class="sec-clear-sub">' + resolved.length + ' finding' + (resolved.length !== 1 ? 's were' : ' was') + ' detected and fixed</div></div>';
|
|
6706
|
+
container.appendChild(allFixed);
|
|
6295
6707
|
}
|
|
6296
6708
|
|
|
6297
|
-
|
|
6298
|
-
var
|
|
6299
|
-
var
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6709
|
+
if (open.length > 0) {
|
|
6710
|
+
var groups = {};
|
|
6711
|
+
var groupOrder = [];
|
|
6712
|
+
for (var gi = 0; gi < open.length; gi++) {
|
|
6713
|
+
var f = open[gi].finding;
|
|
6714
|
+
if (!groups[f.rule]) {
|
|
6715
|
+
groups[f.rule] = { rule: f.rule, title: f.title, severity: f.severity, hint: f.hint, items: [] };
|
|
6716
|
+
groupOrder.push(f.rule);
|
|
6717
|
+
}
|
|
6718
|
+
groups[f.rule].items.push(f);
|
|
6719
|
+
}
|
|
6303
6720
|
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6721
|
+
groupOrder.sort(function(a, b) {
|
|
6722
|
+
var sa = SEV[groups[a].severity].sort;
|
|
6723
|
+
var sb = SEV[groups[b].severity].sort;
|
|
6724
|
+
if (sa !== sb) return sa - sb;
|
|
6725
|
+
return groups[b].items.length - groups[a].items.length;
|
|
6726
|
+
});
|
|
6308
6727
|
|
|
6309
|
-
var
|
|
6310
|
-
|
|
6728
|
+
for (var oi = 0; oi < groupOrder.length; oi++) {
|
|
6729
|
+
var group = groups[groupOrder[oi]];
|
|
6730
|
+
var section = document.createElement('div');
|
|
6731
|
+
section.className = 'sec-group';
|
|
6732
|
+
|
|
6733
|
+
var sevCfg = SEV[group.severity];
|
|
6734
|
+
var iconCls = sevCfg.cls;
|
|
6735
|
+
var iconChar = sevCfg.icon;
|
|
6736
|
+
|
|
6737
|
+
var header = document.createElement('div');
|
|
6738
|
+
header.className = 'sec-group-header';
|
|
6739
|
+
header.innerHTML =
|
|
6740
|
+
'<span class="sec-group-icon ' + iconCls + '">' + iconChar + '</span>' +
|
|
6741
|
+
'<span class="sec-group-title">' + escHtml(group.title) + '</span>' +
|
|
6742
|
+
'<span class="sec-group-count">' + group.items.length + '</span>';
|
|
6743
|
+
section.appendChild(header);
|
|
6744
|
+
|
|
6745
|
+
if (group.hint) {
|
|
6746
|
+
var hintEl = document.createElement('div');
|
|
6747
|
+
hintEl.className = 'sec-hint';
|
|
6748
|
+
hintEl.textContent = group.hint;
|
|
6749
|
+
section.appendChild(hintEl);
|
|
6750
|
+
}
|
|
6311
6751
|
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
}
|
|
6326
|
-
|
|
6327
|
-
var list = document.createElement('div');
|
|
6328
|
-
list.className = 'sec-items';
|
|
6329
|
-
for (var ii = 0; ii < group.items.length; ii++) {
|
|
6330
|
-
var item = group.items[ii];
|
|
6331
|
-
var row = document.createElement('div');
|
|
6332
|
-
row.className = 'sec-item';
|
|
6333
|
-
row.innerHTML =
|
|
6334
|
-
'<div class="sec-item-desc">' + item.desc + '</div>' +
|
|
6335
|
-
(item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
|
|
6336
|
-
list.appendChild(row);
|
|
6752
|
+
var list = document.createElement('div');
|
|
6753
|
+
list.className = 'sec-items';
|
|
6754
|
+
for (var ii = 0; ii < group.items.length; ii++) {
|
|
6755
|
+
var item = group.items[ii];
|
|
6756
|
+
var row = document.createElement('div');
|
|
6757
|
+
row.className = 'sec-item';
|
|
6758
|
+
row.innerHTML =
|
|
6759
|
+
'<div class="sec-item-desc">' + item.desc + '</div>' +
|
|
6760
|
+
(item.count > 1 ? '<span class="sec-item-count">' + item.count + 'x</span>' : '');
|
|
6761
|
+
list.appendChild(row);
|
|
6762
|
+
}
|
|
6763
|
+
section.appendChild(list);
|
|
6764
|
+
container.appendChild(section);
|
|
6337
6765
|
}
|
|
6338
|
-
|
|
6339
|
-
|
|
6766
|
+
}
|
|
6767
|
+
|
|
6768
|
+
if (resolved.length > 0) {
|
|
6769
|
+
var resolvedTitle = document.createElement('div');
|
|
6770
|
+
resolvedTitle.className = 'sec-resolved-title';
|
|
6771
|
+
resolvedTitle.innerHTML = '<span class="sec-resolved-check">\\u2713</span> Resolved <span class="sec-resolved-count">' + resolved.length + '</span>';
|
|
6772
|
+
container.appendChild(resolvedTitle);
|
|
6773
|
+
|
|
6774
|
+
var resolvedGroup = document.createElement('div');
|
|
6775
|
+
resolvedGroup.className = 'sec-group sec-group-resolved';
|
|
6776
|
+
var resolvedItems = document.createElement('div');
|
|
6777
|
+
resolvedItems.className = 'sec-items';
|
|
6778
|
+
for (var ri = 0; ri < resolved.length; ri++) {
|
|
6779
|
+
var rf = resolved[ri].finding;
|
|
6780
|
+
var rRow = document.createElement('div');
|
|
6781
|
+
rRow.className = 'sec-item sec-item-resolved';
|
|
6782
|
+
rRow.innerHTML =
|
|
6783
|
+
'<span class="sec-resolved-item-icon">\\u2713</span>' +
|
|
6784
|
+
'<div class="sec-item-desc">' + escHtml(rf.title) + ' \\u2014 ' + escHtml(rf.endpoint) + '</div>';
|
|
6785
|
+
resolvedItems.appendChild(rRow);
|
|
6786
|
+
}
|
|
6787
|
+
resolvedGroup.appendChild(resolvedItems);
|
|
6788
|
+
container.appendChild(resolvedGroup);
|
|
6340
6789
|
}
|
|
6341
6790
|
}
|
|
6342
6791
|
`;
|
|
@@ -6344,6 +6793,7 @@ function getSecurityView() {
|
|
|
6344
6793
|
var init_security3 = __esm({
|
|
6345
6794
|
"src/dashboard/client/views/security.ts"() {
|
|
6346
6795
|
"use strict";
|
|
6796
|
+
init_display();
|
|
6347
6797
|
}
|
|
6348
6798
|
});
|
|
6349
6799
|
|
|
@@ -6524,7 +6974,7 @@ function getApp() {
|
|
|
6524
6974
|
if (queryCount) queryCount.textContent = state.queries.length;
|
|
6525
6975
|
var secCount = document.getElementById('sidebar-count-security');
|
|
6526
6976
|
if (secCount) {
|
|
6527
|
-
var numFindings = (state.findings || []).length;
|
|
6977
|
+
var numFindings = (state.findings || []).filter(function(f) { return f.state !== 'resolved'; }).length;
|
|
6528
6978
|
secCount.textContent = numFindings;
|
|
6529
6979
|
secCount.style.display = numFindings > 0 ? '' : 'none';
|
|
6530
6980
|
}
|
|
@@ -6639,12 +7089,12 @@ var init_page = __esm({
|
|
|
6639
7089
|
// src/telemetry/config.ts
|
|
6640
7090
|
import { homedir } from "os";
|
|
6641
7091
|
import { join as join2 } from "path";
|
|
6642
|
-
import { existsSync as
|
|
7092
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
6643
7093
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6644
7094
|
function readConfig() {
|
|
6645
7095
|
try {
|
|
6646
|
-
if (!
|
|
6647
|
-
return JSON.parse(
|
|
7096
|
+
if (!existsSync4(CONFIG_PATH)) return null;
|
|
7097
|
+
return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
|
|
6648
7098
|
} catch {
|
|
6649
7099
|
return null;
|
|
6650
7100
|
}
|
|
@@ -6693,7 +7143,7 @@ function createDashboardHandler(deps) {
|
|
|
6693
7143
|
[DASHBOARD_API_REQUESTS]: handleApiRequests,
|
|
6694
7144
|
[DASHBOARD_API_EVENTS]: createSSEHandler(deps.analysisEngine),
|
|
6695
7145
|
[DASHBOARD_API_FLOWS]: handleApiFlows,
|
|
6696
|
-
[DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore),
|
|
7146
|
+
[DASHBOARD_API_CLEAR]: createClearHandler(deps.metricsStore, deps.findingStore),
|
|
6697
7147
|
[DASHBOARD_API_LOGS]: handleApiLogs,
|
|
6698
7148
|
[DASHBOARD_API_FETCHES]: handleApiFetches,
|
|
6699
7149
|
[DASHBOARD_API_ERRORS]: handleApiErrors,
|
|
@@ -6707,6 +7157,9 @@ function createDashboardHandler(deps) {
|
|
|
6707
7157
|
routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
|
|
6708
7158
|
routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
|
|
6709
7159
|
}
|
|
7160
|
+
if (deps.findingStore) {
|
|
7161
|
+
routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(deps.findingStore);
|
|
7162
|
+
}
|
|
6710
7163
|
routes[DASHBOARD_API_TAB] = (req, res) => {
|
|
6711
7164
|
const raw = (req.url ?? "").split("tab=")[1];
|
|
6712
7165
|
if (raw) {
|
|
@@ -6739,6 +7192,7 @@ var init_router = __esm({
|
|
|
6739
7192
|
init_constants();
|
|
6740
7193
|
init_api();
|
|
6741
7194
|
init_insights();
|
|
7195
|
+
init_findings();
|
|
6742
7196
|
init_sse();
|
|
6743
7197
|
init_page();
|
|
6744
7198
|
init_telemetry();
|
|
@@ -6761,30 +7215,39 @@ var init_router = __esm({
|
|
|
6761
7215
|
}
|
|
6762
7216
|
});
|
|
6763
7217
|
|
|
7218
|
+
// src/constants/severity.ts
|
|
7219
|
+
var SEVERITY_ICON;
|
|
7220
|
+
var init_severity = __esm({
|
|
7221
|
+
"src/constants/severity.ts"() {
|
|
7222
|
+
"use strict";
|
|
7223
|
+
SEVERITY_ICON = {
|
|
7224
|
+
critical: "\u2717",
|
|
7225
|
+
warning: "\u26A0",
|
|
7226
|
+
info: "\u2139"
|
|
7227
|
+
};
|
|
7228
|
+
}
|
|
7229
|
+
});
|
|
7230
|
+
|
|
6764
7231
|
// src/output/terminal.ts
|
|
6765
7232
|
import pc from "picocolors";
|
|
6766
7233
|
function print(line) {
|
|
6767
7234
|
process.stdout.write(line + "\n");
|
|
6768
7235
|
}
|
|
6769
7236
|
function severityIcon(severity) {
|
|
6770
|
-
|
|
6771
|
-
if (severity === "warning") return pc.yellow("\u26A0");
|
|
6772
|
-
return pc.dim("\u25CB");
|
|
7237
|
+
return SEVERITY_COLOR[severity](SEVERITY_ICON[severity]);
|
|
6773
7238
|
}
|
|
6774
7239
|
function colorTitle(severity, text) {
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
return pc.dim(text);
|
|
7240
|
+
const color = SEVERITY_COLOR[severity];
|
|
7241
|
+
return severity === "info" ? color(text) : color(pc.bold(text));
|
|
6778
7242
|
}
|
|
6779
|
-
function truncate(s, max =
|
|
7243
|
+
function truncate(s, max = TERMINAL_TRUNCATE_LENGTH) {
|
|
6780
7244
|
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
6781
7245
|
}
|
|
6782
|
-
function formatConsoleLine(insight,
|
|
7246
|
+
function formatConsoleLine(insight, suffix) {
|
|
6783
7247
|
const icon = severityIcon(insight.severity);
|
|
6784
7248
|
const title = colorTitle(insight.severity, insight.title);
|
|
6785
7249
|
const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
|
|
6786
|
-
|
|
6787
|
-
let line = ` ${icon} ${title} \u2014 ${desc} ${link}`;
|
|
7250
|
+
let line = ` ${icon} ${title} \u2014 ${desc}`;
|
|
6788
7251
|
if (insight.detail) {
|
|
6789
7252
|
line += `
|
|
6790
7253
|
${pc.dim("\u2514 " + insight.detail)}`;
|
|
@@ -6793,36 +7256,66 @@ function formatConsoleLine(insight, dashboardUrl, suffix) {
|
|
|
6793
7256
|
}
|
|
6794
7257
|
function createConsoleInsightListener(proxyPort, metricsStore) {
|
|
6795
7258
|
const printedKeys = /* @__PURE__ */ new Set();
|
|
7259
|
+
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
6796
7260
|
const dashUrl = `localhost:${proxyPort}${DASHBOARD_PREFIX}`;
|
|
6797
|
-
return (
|
|
6798
|
-
const
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
7261
|
+
return ({ statefulInsights }) => {
|
|
7262
|
+
const newLines = [];
|
|
7263
|
+
const resolvedLines = [];
|
|
7264
|
+
for (const si of statefulInsights) {
|
|
7265
|
+
if (si.state === "resolved") {
|
|
7266
|
+
if (resolvedKeys.has(si.key)) continue;
|
|
7267
|
+
resolvedKeys.add(si.key);
|
|
7268
|
+
printedKeys.delete(si.key);
|
|
7269
|
+
const title = pc.green(pc.bold(`\u2713 ${si.insight.title}`));
|
|
7270
|
+
const desc = pc.dim(truncate(si.insight.desc));
|
|
7271
|
+
resolvedLines.push(` ${title} \u2014 ${desc} ${pc.green("resolved")}`);
|
|
7272
|
+
continue;
|
|
7273
|
+
}
|
|
7274
|
+
resolvedKeys.delete(si.key);
|
|
7275
|
+
if (si.insight.severity === "info") continue;
|
|
7276
|
+
if (printedKeys.has(si.key)) continue;
|
|
7277
|
+
printedKeys.add(si.key);
|
|
6805
7278
|
let suffix;
|
|
6806
|
-
if (insight.type === "slow") {
|
|
6807
|
-
const
|
|
6808
|
-
if (
|
|
6809
|
-
const
|
|
6810
|
-
|
|
7279
|
+
if (si.insight.type === "slow") {
|
|
7280
|
+
const endpoint = extractEndpointFromDesc(si.insight.desc);
|
|
7281
|
+
if (endpoint) {
|
|
7282
|
+
const ep = metricsStore.getEndpoint(endpoint);
|
|
7283
|
+
if (ep && ep.sessions.length > 1) {
|
|
7284
|
+
const prev = ep.sessions[ep.sessions.length - 2];
|
|
7285
|
+
suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
|
|
7286
|
+
}
|
|
6811
7287
|
}
|
|
6812
7288
|
}
|
|
6813
|
-
|
|
7289
|
+
newLines.push(formatConsoleLine(si.insight, suffix));
|
|
7290
|
+
}
|
|
7291
|
+
if (newLines.length > 0) {
|
|
7292
|
+
print("");
|
|
7293
|
+
for (const line of newLines) print(line);
|
|
7294
|
+
print("");
|
|
7295
|
+
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"')}`);
|
|
6814
7296
|
}
|
|
6815
|
-
if (
|
|
7297
|
+
if (resolvedLines.length > 0) {
|
|
7298
|
+
print("");
|
|
7299
|
+
for (const line of resolvedLines) print(line);
|
|
6816
7300
|
print("");
|
|
6817
|
-
|
|
7301
|
+
print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.green("Issues fixed!")}`);
|
|
6818
7302
|
}
|
|
6819
7303
|
};
|
|
6820
7304
|
}
|
|
7305
|
+
var SEVERITY_COLOR;
|
|
6821
7306
|
var init_terminal = __esm({
|
|
6822
7307
|
"src/output/terminal.ts"() {
|
|
6823
7308
|
"use strict";
|
|
6824
7309
|
init_src();
|
|
6825
7310
|
init_constants();
|
|
7311
|
+
init_limits();
|
|
7312
|
+
init_severity();
|
|
7313
|
+
init_endpoint();
|
|
7314
|
+
SEVERITY_COLOR = {
|
|
7315
|
+
critical: pc.red,
|
|
7316
|
+
warning: pc.yellow,
|
|
7317
|
+
info: pc.dim
|
|
7318
|
+
};
|
|
6826
7319
|
}
|
|
6827
7320
|
});
|
|
6828
7321
|
|
|
@@ -7050,6 +7543,8 @@ var setup_exports = {};
|
|
|
7050
7543
|
__export(setup_exports, {
|
|
7051
7544
|
setup: () => setup
|
|
7052
7545
|
});
|
|
7546
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
7547
|
+
import { resolve as resolve4 } from "path";
|
|
7053
7548
|
function setup() {
|
|
7054
7549
|
if (initialized) return;
|
|
7055
7550
|
initialized = true;
|
|
@@ -7062,7 +7557,9 @@ function setup() {
|
|
|
7062
7557
|
const cwd = process.cwd();
|
|
7063
7558
|
const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
|
|
7064
7559
|
metricsStore.start();
|
|
7065
|
-
const
|
|
7560
|
+
const findingStore = new FindingStore(cwd);
|
|
7561
|
+
findingStore.start();
|
|
7562
|
+
const analysisEngine = new AnalysisEngine(metricsStore, findingStore);
|
|
7066
7563
|
analysisEngine.start();
|
|
7067
7564
|
const config = {
|
|
7068
7565
|
proxyPort: 0,
|
|
@@ -7070,7 +7567,7 @@ function setup() {
|
|
|
7070
7567
|
showStatic: false,
|
|
7071
7568
|
maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
|
|
7072
7569
|
};
|
|
7073
|
-
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
|
|
7570
|
+
const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine, findingStore });
|
|
7074
7571
|
onRequest((req) => {
|
|
7075
7572
|
const queries = defaultQueryStore.getByRequest(req.id);
|
|
7076
7573
|
const fetches = defaultFetchStore.getByRequest(req.id);
|
|
@@ -7084,6 +7581,9 @@ function setup() {
|
|
|
7084
7581
|
handleDashboard,
|
|
7085
7582
|
config,
|
|
7086
7583
|
onFirstRequest(port) {
|
|
7584
|
+
const dir = resolve4(cwd, METRICS_DIR);
|
|
7585
|
+
if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
|
|
7586
|
+
writeFileSync5(resolve4(cwd, PORT_FILE), String(port));
|
|
7087
7587
|
analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
|
|
7088
7588
|
process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
|
|
7089
7589
|
`);
|
|
@@ -7092,7 +7592,13 @@ function setup() {
|
|
|
7092
7592
|
health.setTeardown(() => {
|
|
7093
7593
|
uninstallInterceptor();
|
|
7094
7594
|
analysisEngine.stop();
|
|
7595
|
+
findingStore.stop();
|
|
7095
7596
|
metricsStore.stop();
|
|
7597
|
+
try {
|
|
7598
|
+
const portPath = resolve4(cwd, PORT_FILE);
|
|
7599
|
+
if (existsSync5(portPath)) unlinkSync2(portPath);
|
|
7600
|
+
} catch {
|
|
7601
|
+
}
|
|
7096
7602
|
});
|
|
7097
7603
|
}
|
|
7098
7604
|
function routeEvent2(event) {
|
|
@@ -7123,6 +7629,7 @@ var init_setup = __esm({
|
|
|
7123
7629
|
init_router();
|
|
7124
7630
|
init_request_log();
|
|
7125
7631
|
init_store();
|
|
7632
|
+
init_finding_store();
|
|
7126
7633
|
init_engine();
|
|
7127
7634
|
init_terminal();
|
|
7128
7635
|
init_src();
|