brakit 0.8.6 → 0.8.7
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 +7 -1
- package/dist/api.d.ts +4 -2
- package/dist/api.js +50 -27
- package/dist/bin/brakit.js +45 -10
- package/dist/dashboard-client.global.js +703 -0
- package/dist/dashboard.html +895 -2169
- package/dist/mcp/server.js +1 -1
- package/dist/runtime/index.js +1075 -3277
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<b>AI writes your code. Brakit watches what it does.</b> <br />
|
|
5
|
-
Every request, query, and
|
|
5
|
+
Every request, query, and API call mapped to the action that triggered it. See your entire backend at a glance. <br />
|
|
6
6
|
<b>Open source · Local first · Zero config · AI-native via MCP</b>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img width="700" src="docs/images/actions.png" alt="Brakit Actions — endpoints grouped by user action" />
|
|
37
|
+
<br />
|
|
38
|
+
<sub>Every endpoint grouped by the action that triggered it — see "Sign Up" and "Load Dashboard", not 47 raw requests</sub>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
35
41
|
<p align="center">
|
|
36
42
|
<img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard — issues surfaced automatically" />
|
|
37
43
|
<br />
|
package/dist/api.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ interface TelemetryEntry {
|
|
|
63
63
|
timestamp: number;
|
|
64
64
|
}
|
|
65
65
|
interface TracedFetch extends TelemetryEntry {
|
|
66
|
+
fetchId?: string;
|
|
66
67
|
url: string;
|
|
67
68
|
method: string;
|
|
68
69
|
statusCode: number;
|
|
@@ -75,11 +76,11 @@ interface TracedLog extends TelemetryEntry {
|
|
|
75
76
|
interface TracedError extends TelemetryEntry {
|
|
76
77
|
name: string;
|
|
77
78
|
message: string;
|
|
78
|
-
stack
|
|
79
|
+
stack?: string;
|
|
79
80
|
}
|
|
80
81
|
type NormalizedOp = "SELECT" | "INSERT" | "UPDATE" | "DELETE" | "OTHER";
|
|
81
82
|
interface TracedQuery extends TelemetryEntry {
|
|
82
|
-
driver: "pg" | "mysql2" | "prisma" | "sdk";
|
|
83
|
+
driver: "pg" | "mysql2" | "prisma" | "asyncpg" | "sqlalchemy" | "sdk";
|
|
83
84
|
sql?: string;
|
|
84
85
|
model?: string;
|
|
85
86
|
operation?: string;
|
|
@@ -88,6 +89,7 @@ interface TracedQuery extends TelemetryEntry {
|
|
|
88
89
|
normalizedOp?: NormalizedOp;
|
|
89
90
|
table?: string;
|
|
90
91
|
source?: string;
|
|
92
|
+
parentFetchId?: string;
|
|
91
93
|
}
|
|
92
94
|
type TelemetryEvent = {
|
|
93
95
|
type: "fetch";
|
package/dist/api.js
CHANGED
|
@@ -17,7 +17,7 @@ var ISSUES_DATA_VERSION = 2;
|
|
|
17
17
|
var SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
18
18
|
var PII_SCAN_ARRAY_LIMIT = 10;
|
|
19
19
|
var MIN_SECRET_VALUE_LENGTH = 8;
|
|
20
|
-
var FULL_RECORD_MIN_FIELDS =
|
|
20
|
+
var FULL_RECORD_MIN_FIELDS = 8;
|
|
21
21
|
var LIST_PII_MIN_ITEMS = 2;
|
|
22
22
|
var MAX_OBJECT_SCAN_DEPTH = 5;
|
|
23
23
|
var ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
@@ -132,11 +132,10 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
|
132
132
|
var AtomicWriter = class {
|
|
133
133
|
constructor(opts) {
|
|
134
134
|
this.opts = opts;
|
|
135
|
+
this.writing = false;
|
|
136
|
+
this.pendingContent = null;
|
|
135
137
|
this.tmpPath = opts.filePath + ".tmp";
|
|
136
138
|
}
|
|
137
|
-
tmpPath;
|
|
138
|
-
writing = false;
|
|
139
|
-
pendingContent = null;
|
|
140
139
|
writeSync(content) {
|
|
141
140
|
try {
|
|
142
141
|
this.ensureDir();
|
|
@@ -198,6 +197,9 @@ function computeIssueId(issue) {
|
|
|
198
197
|
var IssueStore = class {
|
|
199
198
|
constructor(dataDir) {
|
|
200
199
|
this.dataDir = dataDir;
|
|
200
|
+
this.issues = /* @__PURE__ */ new Map();
|
|
201
|
+
this.flushTimer = null;
|
|
202
|
+
this.dirty = false;
|
|
201
203
|
this.issuesPath = resolve2(dataDir, ISSUES_FILE);
|
|
202
204
|
this.writer = new AtomicWriter({
|
|
203
205
|
dir: dataDir,
|
|
@@ -205,11 +207,6 @@ var IssueStore = class {
|
|
|
205
207
|
label: "issues"
|
|
206
208
|
});
|
|
207
209
|
}
|
|
208
|
-
issues = /* @__PURE__ */ new Map();
|
|
209
|
-
flushTimer = null;
|
|
210
|
-
dirty = false;
|
|
211
|
-
writer;
|
|
212
|
-
issuesPath;
|
|
213
210
|
start() {
|
|
214
211
|
this.loadAsync().catch((err) => brakitDebug(`IssueStore: async load failed: ${err}`));
|
|
215
212
|
this.flushTimer = setInterval(
|
|
@@ -429,8 +426,10 @@ function detectFrameworkFromDeps(allDeps) {
|
|
|
429
426
|
|
|
430
427
|
// src/instrument/adapter-registry.ts
|
|
431
428
|
var AdapterRegistry = class {
|
|
432
|
-
|
|
433
|
-
|
|
429
|
+
constructor() {
|
|
430
|
+
this.adapters = [];
|
|
431
|
+
this.active = [];
|
|
432
|
+
}
|
|
434
433
|
register(adapter) {
|
|
435
434
|
this.adapters.push(adapter);
|
|
436
435
|
}
|
|
@@ -504,6 +503,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
|
504
503
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
505
504
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
506
505
|
var INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
506
|
+
var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
|
|
507
|
+
var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
|
|
507
508
|
var SELECT_STAR_RE = /^SELECT\s+\*/i;
|
|
508
509
|
var SELECT_DOT_STAR_RE = /\.\*\s+FROM/i;
|
|
509
510
|
var RULE_HINTS = {
|
|
@@ -854,6 +855,15 @@ function hasInternalIds(obj) {
|
|
|
854
855
|
}
|
|
855
856
|
return false;
|
|
856
857
|
}
|
|
858
|
+
function hasSensitiveFieldNames(obj, depth = 0) {
|
|
859
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
|
|
860
|
+
if (!obj || typeof obj !== "object") return false;
|
|
861
|
+
if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
|
|
862
|
+
for (const key of Object.keys(obj)) {
|
|
863
|
+
if (SENSITIVE_FIELD_NAMES.test(key)) return true;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
857
867
|
function detectEchoPII(method, reqBody, target) {
|
|
858
868
|
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
859
869
|
const reqEmails = findEmails(reqBody);
|
|
@@ -875,6 +885,13 @@ function detectFullRecordPII(target) {
|
|
|
875
885
|
if (emails.length === 0) return null;
|
|
876
886
|
return { reason: "full-record", emailCount: emails.length };
|
|
877
887
|
}
|
|
888
|
+
function detectSensitiveFieldPII(target) {
|
|
889
|
+
const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
890
|
+
if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
|
|
891
|
+
if (!hasSensitiveFieldNames(inspect)) return null;
|
|
892
|
+
if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
|
|
893
|
+
return { reason: "sensitive-fields", emailCount: 0 };
|
|
894
|
+
}
|
|
878
895
|
function detectListPII(target) {
|
|
879
896
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
880
897
|
let itemsWithEmail = 0;
|
|
@@ -893,12 +910,13 @@ function detectListPII(target) {
|
|
|
893
910
|
}
|
|
894
911
|
function detectPII(method, reqBody, resBody) {
|
|
895
912
|
const target = unwrapResponse(resBody);
|
|
896
|
-
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
913
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
897
914
|
}
|
|
898
915
|
var REASON_LABELS = {
|
|
899
916
|
echo: "echoes back PII from the request body",
|
|
900
917
|
"full-record": "returns a full record with email and internal IDs",
|
|
901
|
-
"list-pii": "returns a list of records containing email addresses"
|
|
918
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
919
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
902
920
|
};
|
|
903
921
|
var responsePiiLeakRule = {
|
|
904
922
|
id: "response-pii-leak",
|
|
@@ -910,14 +928,14 @@ var responsePiiLeakRule = {
|
|
|
910
928
|
const seen = /* @__PURE__ */ new Map();
|
|
911
929
|
for (const r of ctx.requests) {
|
|
912
930
|
if (isErrorStatus(r.statusCode)) continue;
|
|
931
|
+
if (SELF_SERVICE_PATH.test(r.path)) continue;
|
|
913
932
|
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
914
933
|
if (!resJson) continue;
|
|
915
934
|
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
916
935
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
917
936
|
if (!detection) continue;
|
|
918
937
|
const ep = `${r.method} ${r.path}`;
|
|
919
|
-
const
|
|
920
|
-
const existing = seen.get(dedupKey);
|
|
938
|
+
const existing = seen.get(ep);
|
|
921
939
|
if (existing) {
|
|
922
940
|
existing.count++;
|
|
923
941
|
continue;
|
|
@@ -926,12 +944,12 @@ var responsePiiLeakRule = {
|
|
|
926
944
|
severity: "warning",
|
|
927
945
|
rule: "response-pii-leak",
|
|
928
946
|
title: "PII Leak in Response",
|
|
929
|
-
desc: `${ep} \u2014
|
|
930
|
-
hint: this.hint
|
|
947
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
948
|
+
hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
|
|
931
949
|
endpoint: ep,
|
|
932
950
|
count: 1
|
|
933
951
|
};
|
|
934
|
-
seen.set(
|
|
952
|
+
seen.set(ep, finding);
|
|
935
953
|
findings.push(finding);
|
|
936
954
|
}
|
|
937
955
|
return findings;
|
|
@@ -955,7 +973,9 @@ function buildBodyCache(requests) {
|
|
|
955
973
|
return { response, request };
|
|
956
974
|
}
|
|
957
975
|
var SecurityScanner = class {
|
|
958
|
-
|
|
976
|
+
constructor() {
|
|
977
|
+
this.rules = [];
|
|
978
|
+
}
|
|
959
979
|
register(rule) {
|
|
960
980
|
this.rules.push(rule);
|
|
961
981
|
}
|
|
@@ -992,7 +1012,9 @@ function createDefaultScanner() {
|
|
|
992
1012
|
|
|
993
1013
|
// src/core/disposable.ts
|
|
994
1014
|
var SubscriptionBag = class {
|
|
995
|
-
|
|
1015
|
+
constructor() {
|
|
1016
|
+
this.items = [];
|
|
1017
|
+
}
|
|
996
1018
|
add(teardown) {
|
|
997
1019
|
this.items.push(typeof teardown === "function" ? { dispose: teardown } : teardown);
|
|
998
1020
|
}
|
|
@@ -1557,7 +1579,9 @@ function prepareContext(ctx) {
|
|
|
1557
1579
|
// src/analysis/insights/runner.ts
|
|
1558
1580
|
var SEVERITY_ORDER = { critical: 0, warning: 1, info: 2 };
|
|
1559
1581
|
var InsightRunner = class {
|
|
1560
|
-
|
|
1582
|
+
constructor() {
|
|
1583
|
+
this.rules = [];
|
|
1584
|
+
}
|
|
1561
1585
|
register(rule) {
|
|
1562
1586
|
this.rules.push(rule);
|
|
1563
1587
|
}
|
|
@@ -2123,13 +2147,12 @@ var AnalysisEngine = class {
|
|
|
2123
2147
|
constructor(registry, debounceMs = ANALYSIS_DEBOUNCE_MS) {
|
|
2124
2148
|
this.registry = registry;
|
|
2125
2149
|
this.debounceMs = debounceMs;
|
|
2150
|
+
this.cachedInsights = [];
|
|
2151
|
+
this.cachedFindings = [];
|
|
2152
|
+
this.debounceTimer = null;
|
|
2153
|
+
this.subs = new SubscriptionBag();
|
|
2126
2154
|
this.scanner = createDefaultScanner();
|
|
2127
2155
|
}
|
|
2128
|
-
scanner;
|
|
2129
|
-
cachedInsights = [];
|
|
2130
|
-
cachedFindings = [];
|
|
2131
|
-
debounceTimer = null;
|
|
2132
|
-
subs = new SubscriptionBag();
|
|
2133
2156
|
start() {
|
|
2134
2157
|
const bus = this.registry.get("event-bus");
|
|
2135
2158
|
this.subs.add(bus.on("request:completed", () => this.scheduleRecompute()));
|
|
@@ -2203,7 +2226,7 @@ var AnalysisEngine = class {
|
|
|
2203
2226
|
};
|
|
2204
2227
|
|
|
2205
2228
|
// src/index.ts
|
|
2206
|
-
var VERSION = "0.8.
|
|
2229
|
+
var VERSION = "0.8.7";
|
|
2207
2230
|
export {
|
|
2208
2231
|
AdapterRegistry,
|
|
2209
2232
|
AnalysisEngine,
|
package/dist/bin/brakit.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_limits = __esm({
|
|
|
18
18
|
SECRET_SCAN_ARRAY_LIMIT = 5;
|
|
19
19
|
PII_SCAN_ARRAY_LIMIT = 10;
|
|
20
20
|
MIN_SECRET_VALUE_LENGTH = 8;
|
|
21
|
-
FULL_RECORD_MIN_FIELDS =
|
|
21
|
+
FULL_RECORD_MIN_FIELDS = 8;
|
|
22
22
|
LIST_PII_MIN_ITEMS = 2;
|
|
23
23
|
MAX_OBJECT_SCAN_DEPTH = 5;
|
|
24
24
|
ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
|
|
@@ -172,7 +172,7 @@ var init_mcp = __esm({
|
|
|
172
172
|
MAX_TIMELINE_EVENTS = 20;
|
|
173
173
|
MAX_RESOLVED_DISPLAY = 5;
|
|
174
174
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
175
|
-
MCP_SERVER_VERSION = "0.8.
|
|
175
|
+
MCP_SERVER_VERSION = "0.8.7";
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
178
|
|
|
@@ -215,6 +215,20 @@ var init_cli = __esm({
|
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
// src/constants/timeline.ts
|
|
219
|
+
var init_timeline = __esm({
|
|
220
|
+
"src/constants/timeline.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// src/constants/sdk-events.ts
|
|
226
|
+
var init_sdk_events = __esm({
|
|
227
|
+
"src/constants/sdk-events.ts"() {
|
|
228
|
+
"use strict";
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
218
232
|
// src/constants/index.ts
|
|
219
233
|
var init_constants = __esm({
|
|
220
234
|
"src/constants/index.ts"() {
|
|
@@ -232,6 +246,8 @@ var init_constants = __esm({
|
|
|
232
246
|
init_telemetry();
|
|
233
247
|
init_lifecycle();
|
|
234
248
|
init_cli();
|
|
249
|
+
init_timeline();
|
|
250
|
+
init_sdk_events();
|
|
235
251
|
}
|
|
236
252
|
});
|
|
237
253
|
|
|
@@ -1399,6 +1415,8 @@ var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
|
1399
1415
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1400
1416
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
1401
1417
|
var INTERNAL_ID_SUFFIX = /Id$|_id$/;
|
|
1418
|
+
var SELF_SERVICE_PATH = /\/(?:me|account|profile|settings|self)(?=\/|\?|#|$)/i;
|
|
1419
|
+
var SENSITIVE_FIELD_NAMES = /^(phone|phoneNumber|phone_number|ssn|socialSecurityNumber|social_security_number|dateOfBirth|date_of_birth|dob|address|streetAddress|street_address|creditCard|credit_card|cardNumber|card_number|bankAccount|bank_account|passport|passportNumber|passport_number|nationalId|national_id)$/i;
|
|
1402
1420
|
var RULE_HINTS = {
|
|
1403
1421
|
"exposed-secret": "Never include secret fields in API responses. Strip sensitive fields before returning.",
|
|
1404
1422
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
@@ -1748,6 +1766,15 @@ function hasInternalIds(obj) {
|
|
|
1748
1766
|
}
|
|
1749
1767
|
return false;
|
|
1750
1768
|
}
|
|
1769
|
+
function hasSensitiveFieldNames(obj, depth = 0) {
|
|
1770
|
+
if (depth >= MAX_OBJECT_SCAN_DEPTH) return false;
|
|
1771
|
+
if (!obj || typeof obj !== "object") return false;
|
|
1772
|
+
if (Array.isArray(obj)) return obj.length > 0 && hasSensitiveFieldNames(obj[0], depth + 1);
|
|
1773
|
+
for (const key of Object.keys(obj)) {
|
|
1774
|
+
if (SENSITIVE_FIELD_NAMES.test(key)) return true;
|
|
1775
|
+
}
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1751
1778
|
function detectEchoPII(method, reqBody, target) {
|
|
1752
1779
|
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1753
1780
|
const reqEmails = findEmails(reqBody);
|
|
@@ -1769,6 +1796,13 @@ function detectFullRecordPII(target) {
|
|
|
1769
1796
|
if (emails.length === 0) return null;
|
|
1770
1797
|
return { reason: "full-record", emailCount: emails.length };
|
|
1771
1798
|
}
|
|
1799
|
+
function detectSensitiveFieldPII(target) {
|
|
1800
|
+
const inspect = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1801
|
+
if (!inspect || typeof inspect !== "object" || Array.isArray(inspect)) return null;
|
|
1802
|
+
if (!hasSensitiveFieldNames(inspect)) return null;
|
|
1803
|
+
if (!hasInternalIds(inspect) && topLevelFieldCount(inspect) < FULL_RECORD_MIN_FIELDS) return null;
|
|
1804
|
+
return { reason: "sensitive-fields", emailCount: 0 };
|
|
1805
|
+
}
|
|
1772
1806
|
function detectListPII(target) {
|
|
1773
1807
|
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1774
1808
|
let itemsWithEmail = 0;
|
|
@@ -1787,12 +1821,13 @@ function detectListPII(target) {
|
|
|
1787
1821
|
}
|
|
1788
1822
|
function detectPII(method, reqBody, resBody) {
|
|
1789
1823
|
const target = unwrapResponse(resBody);
|
|
1790
|
-
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1824
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target) ?? detectSensitiveFieldPII(target);
|
|
1791
1825
|
}
|
|
1792
1826
|
var REASON_LABELS = {
|
|
1793
1827
|
echo: "echoes back PII from the request body",
|
|
1794
1828
|
"full-record": "returns a full record with email and internal IDs",
|
|
1795
|
-
"list-pii": "returns a list of records containing email addresses"
|
|
1829
|
+
"list-pii": "returns a list of records containing email addresses",
|
|
1830
|
+
"sensitive-fields": "contains sensitive personal data fields (phone, SSN, date of birth, address, etc.)"
|
|
1796
1831
|
};
|
|
1797
1832
|
var responsePiiLeakRule = {
|
|
1798
1833
|
id: "response-pii-leak",
|
|
@@ -1804,14 +1839,14 @@ var responsePiiLeakRule = {
|
|
|
1804
1839
|
const seen = /* @__PURE__ */ new Map();
|
|
1805
1840
|
for (const r of ctx.requests) {
|
|
1806
1841
|
if (isErrorStatus(r.statusCode)) continue;
|
|
1842
|
+
if (SELF_SERVICE_PATH.test(r.path)) continue;
|
|
1807
1843
|
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
1808
1844
|
if (!resJson) continue;
|
|
1809
1845
|
const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
|
|
1810
1846
|
const detection = detectPII(r.method, reqJson, resJson);
|
|
1811
1847
|
if (!detection) continue;
|
|
1812
1848
|
const ep = `${r.method} ${r.path}`;
|
|
1813
|
-
const
|
|
1814
|
-
const existing = seen.get(dedupKey);
|
|
1849
|
+
const existing = seen.get(ep);
|
|
1815
1850
|
if (existing) {
|
|
1816
1851
|
existing.count++;
|
|
1817
1852
|
continue;
|
|
@@ -1820,12 +1855,12 @@ var responsePiiLeakRule = {
|
|
|
1820
1855
|
severity: "warning",
|
|
1821
1856
|
rule: "response-pii-leak",
|
|
1822
1857
|
title: "PII Leak in Response",
|
|
1823
|
-
desc: `${ep} \u2014
|
|
1824
|
-
hint: this.hint
|
|
1858
|
+
desc: `${ep} \u2014 exposes PII in response`,
|
|
1859
|
+
hint: `Detection: ${REASON_LABELS[detection.reason]}. ${this.hint}`,
|
|
1825
1860
|
endpoint: ep,
|
|
1826
1861
|
count: 1
|
|
1827
1862
|
};
|
|
1828
|
-
seen.set(
|
|
1863
|
+
seen.set(ep, finding);
|
|
1829
1864
|
findings.push(finding);
|
|
1830
1865
|
}
|
|
1831
1866
|
return findings;
|
|
@@ -1894,7 +1929,7 @@ init_constants();
|
|
|
1894
1929
|
init_endpoint();
|
|
1895
1930
|
|
|
1896
1931
|
// src/index.ts
|
|
1897
|
-
var VERSION = "0.8.
|
|
1932
|
+
var VERSION = "0.8.7";
|
|
1898
1933
|
|
|
1899
1934
|
// src/cli/commands/install.ts
|
|
1900
1935
|
init_constants();
|