@patchstack/connect 0.3.8 → 0.3.9
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/AGENT-INSTALL.md +9 -11
- package/README.md +29 -5
- package/dist/cli.js +169 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +74 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +68 -1
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +311 -18
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +26 -3
- package/dist/protect.js +311 -18
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/dist/protect.cjs
CHANGED
|
@@ -146,10 +146,50 @@ var RequestResolver = class {
|
|
|
146
146
|
return this.#resolveServer(key);
|
|
147
147
|
case "files":
|
|
148
148
|
return this.#resolveFiles(key);
|
|
149
|
+
case "response":
|
|
150
|
+
return this.#resolveResponse(key);
|
|
151
|
+
case "egress":
|
|
152
|
+
return this.#resolveEgress(key);
|
|
149
153
|
default:
|
|
150
154
|
return [];
|
|
151
155
|
}
|
|
152
156
|
}
|
|
157
|
+
// Response-phase sources (req._response = { status, headers, body }). Lets rules inspect
|
|
158
|
+
// what the app is about to SEND — e.g. a leaked secret in the body — regardless of route.
|
|
159
|
+
#resolveResponse(key) {
|
|
160
|
+
const resp = this.#req._response;
|
|
161
|
+
if (!resp) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
if (key === "status") {
|
|
165
|
+
return resp.status !== void 0 ? [String(resp.status)] : [];
|
|
166
|
+
}
|
|
167
|
+
if (key === "body") {
|
|
168
|
+
return resp.body != null && resp.body !== "" ? [resp.body] : [];
|
|
169
|
+
}
|
|
170
|
+
if (key === "headers") {
|
|
171
|
+
const headers = resp.headers ?? {};
|
|
172
|
+
return [Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n")];
|
|
173
|
+
}
|
|
174
|
+
if (key.startsWith("header.")) {
|
|
175
|
+
const name = key.slice("header.".length).toLowerCase();
|
|
176
|
+
const value = (resp.headers ?? {})[name];
|
|
177
|
+
return value !== void 0 ? [value] : [];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
// Egress-phase sources (req._egress = { url, host, method }). Lets rules inspect an
|
|
182
|
+
// OUTBOUND request the app is about to make — SSRF at the egress boundary.
|
|
183
|
+
#resolveEgress(key) {
|
|
184
|
+
const eg = this.#req._egress;
|
|
185
|
+
if (!eg) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
if (key === "url") return eg.url ? [eg.url] : [];
|
|
189
|
+
if (key === "host") return eg.host ? [eg.host] : [];
|
|
190
|
+
if (key === "method") return eg.method ? [eg.method] : [];
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
153
193
|
applyMutations(mutations, value) {
|
|
154
194
|
if (!mutations || !Array.isArray(mutations)) {
|
|
155
195
|
return value;
|
|
@@ -642,6 +682,24 @@ function warnUnsupportedMatchType(type) {
|
|
|
642
682
|
`[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
|
|
643
683
|
);
|
|
644
684
|
}
|
|
685
|
+
function isInternalHost(hostname) {
|
|
686
|
+
if (!hostname) return false;
|
|
687
|
+
const host = String(hostname).toLowerCase().replace(/^\[|\]$/g, "");
|
|
688
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
|
|
689
|
+
if (host === "metadata.google.internal") return true;
|
|
690
|
+
if (host === "::1" || host === "::") return true;
|
|
691
|
+
if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
692
|
+
const v4 = host.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
693
|
+
if (v4) {
|
|
694
|
+
const a = Number(v4[1]);
|
|
695
|
+
const b = Number(v4[2]);
|
|
696
|
+
if (a === 127 || a === 10 || a === 0) return true;
|
|
697
|
+
if (a === 169 && b === 254) return true;
|
|
698
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
699
|
+
if (a === 192 && b === 168) return true;
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
645
703
|
function matchValue(type, value, matchVal, matchObj) {
|
|
646
704
|
if (value === null || value === void 0) {
|
|
647
705
|
if (type === "isset") {
|
|
@@ -708,6 +766,8 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
708
766
|
return false;
|
|
709
767
|
}
|
|
710
768
|
}
|
|
769
|
+
case "internal_host":
|
|
770
|
+
return isInternalHost(strValue);
|
|
711
771
|
case "quotes":
|
|
712
772
|
// engine-php exposes `inline_js_xss` as an alias of `quotes`.
|
|
713
773
|
case "inline_js_xss":
|
|
@@ -819,7 +879,8 @@ var RuleEngine = class {
|
|
|
819
879
|
if (parameter === "rules" && Array.isArray(condition.rules)) {
|
|
820
880
|
return this.#evaluateRule(condition.rules, resolver);
|
|
821
881
|
}
|
|
822
|
-
const
|
|
882
|
+
const params = Array.isArray(parameter) ? parameter : [parameter];
|
|
883
|
+
const values = params.flatMap((p) => resolver.resolve(p));
|
|
823
884
|
if (values.length === 0) {
|
|
824
885
|
return false;
|
|
825
886
|
}
|
|
@@ -1001,6 +1062,98 @@ function parseCookies2(header) {
|
|
|
1001
1062
|
return cookies;
|
|
1002
1063
|
}
|
|
1003
1064
|
|
|
1065
|
+
// src/protect/egress.js
|
|
1066
|
+
function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1067
|
+
const original = globalThis.fetch;
|
|
1068
|
+
if (typeof original !== "function" || original.__patchstackGuarded || typeof shouldBlock !== "function") {
|
|
1069
|
+
return () => {
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
const guarded = async (input, init) => {
|
|
1073
|
+
const url = typeof input === "string" ? input : input && input.url || String(input);
|
|
1074
|
+
let host = null;
|
|
1075
|
+
try {
|
|
1076
|
+
host = new URL(url).hostname;
|
|
1077
|
+
} catch {
|
|
1078
|
+
host = null;
|
|
1079
|
+
}
|
|
1080
|
+
const method = init && init.method || input && input.method || "GET";
|
|
1081
|
+
if (shouldBlock(url, host, method)) {
|
|
1082
|
+
onBlock?.({ url, host, method });
|
|
1083
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
|
|
1084
|
+
}
|
|
1085
|
+
return original(input, init);
|
|
1086
|
+
};
|
|
1087
|
+
guarded.__patchstackGuarded = true;
|
|
1088
|
+
globalThis.fetch = guarded;
|
|
1089
|
+
return () => {
|
|
1090
|
+
if (globalThis.fetch === guarded) {
|
|
1091
|
+
globalThis.fetch = original;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/protect/defaults.js
|
|
1097
|
+
var DEFAULT_RESPONSE_RULES = [
|
|
1098
|
+
{
|
|
1099
|
+
id: "resp-private-key",
|
|
1100
|
+
title: "Private key in response body",
|
|
1101
|
+
phase: "response",
|
|
1102
|
+
category: "secret-exposure",
|
|
1103
|
+
action: "redact",
|
|
1104
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/" } }]
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
id: "resp-aws-access-key",
|
|
1108
|
+
title: "AWS access key id in response body",
|
|
1109
|
+
phase: "response",
|
|
1110
|
+
category: "secret-exposure",
|
|
1111
|
+
action: "redact",
|
|
1112
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/" } }]
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
id: "resp-gcp-api-key",
|
|
1116
|
+
title: "Google API key in response body",
|
|
1117
|
+
phase: "response",
|
|
1118
|
+
category: "secret-exposure",
|
|
1119
|
+
action: "redact",
|
|
1120
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\bAIza[0-9A-Za-z_-]{35}\\b/" } }]
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: "resp-jwt",
|
|
1124
|
+
title: "JWT in response body",
|
|
1125
|
+
phase: "response",
|
|
1126
|
+
category: "secret-exposure",
|
|
1127
|
+
action: "redact",
|
|
1128
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\beyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/" } }]
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
id: "resp-db-connection-string",
|
|
1132
|
+
title: "Database connection string with credentials in response body",
|
|
1133
|
+
phase: "response",
|
|
1134
|
+
category: "secret-exposure",
|
|
1135
|
+
action: "redact",
|
|
1136
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqps?):\\/\\/[^\\s:@\\/]+:[^\\s:@\\/]+@/i" } }]
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
id: "resp-stack-trace",
|
|
1140
|
+
title: "Node stack trace leaking in response body",
|
|
1141
|
+
phase: "response",
|
|
1142
|
+
category: "info-exposure",
|
|
1143
|
+
action: "redact",
|
|
1144
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
|
|
1145
|
+
}
|
|
1146
|
+
];
|
|
1147
|
+
var DEFAULT_EGRESS_RULES = [
|
|
1148
|
+
{
|
|
1149
|
+
id: "egress-internal-address",
|
|
1150
|
+
title: "Outbound request to an internal / metadata address (SSRF)",
|
|
1151
|
+
phase: "egress",
|
|
1152
|
+
category: "ssrf",
|
|
1153
|
+
rule_v2: [{ parameter: "egress.host", match: { type: "internal_host" } }]
|
|
1154
|
+
}
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1004
1157
|
// src/protect/runtime.js
|
|
1005
1158
|
var import_node_fs = require("fs");
|
|
1006
1159
|
var import_node_path = require("path");
|
|
@@ -1079,16 +1232,75 @@ async function createProtection(options = {}) {
|
|
|
1079
1232
|
const onError = options.onError;
|
|
1080
1233
|
const onDetect = options.onDetect ?? defaultOnDetect;
|
|
1081
1234
|
const bundle = await resolveRules(options);
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1235
|
+
const incoming = bundle.firewall ?? [];
|
|
1236
|
+
const requestRules = byPhase(incoming, "request");
|
|
1237
|
+
const responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
|
|
1238
|
+
const egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
|
|
1239
|
+
const engine = new RuleEngine({
|
|
1240
|
+
firewall: requestRules,
|
|
1241
|
+
whitelists: bundle.whitelists,
|
|
1242
|
+
whitelist_keys: bundle.whitelist_keys,
|
|
1243
|
+
onError
|
|
1244
|
+
});
|
|
1245
|
+
const responseRuleSet = responseRules.map((rule) => ({
|
|
1246
|
+
rule,
|
|
1247
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
1248
|
+
redactors: rule.action === "redact" ? extractRedactors(rule) : null
|
|
1249
|
+
}));
|
|
1250
|
+
const egressEngine = new RuleEngine({ firewall: egressRules, onError });
|
|
1251
|
+
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
1252
|
+
const decide = (phase, result, block, allow2) => {
|
|
1253
|
+
if (!result || !result.blocked) return allow2();
|
|
1254
|
+
onDetect({ phase, mode, category: result.rule?.category, rule: result.rule, message: result.message });
|
|
1255
|
+
return mode === "block" ? block() : allow2();
|
|
1256
|
+
};
|
|
1257
|
+
const screenFetchResponse = async (response) => {
|
|
1258
|
+
const text = await readTextResponse(response);
|
|
1259
|
+
if (text == null) return response;
|
|
1260
|
+
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
1261
|
+
let blockRule = null;
|
|
1262
|
+
const redactions = [];
|
|
1263
|
+
for (const { rule, engine: re, redactors } of responseRuleSet) {
|
|
1264
|
+
let result;
|
|
1265
|
+
try {
|
|
1266
|
+
result = re.evaluate({ _response: { ...meta, body: text } });
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
onError?.(err);
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!result.blocked) continue;
|
|
1272
|
+
onDetect({ phase: "response", mode, category: rule.category, rule, message: result.message });
|
|
1273
|
+
if (mode !== "block") continue;
|
|
1274
|
+
if (redactors && redactors.length) redactions.push({ rule, redactors });
|
|
1275
|
+
else if (!blockRule) blockRule = rule;
|
|
1276
|
+
}
|
|
1277
|
+
if (mode !== "block") return response;
|
|
1278
|
+
if (blockRule) return leakResponse();
|
|
1279
|
+
if (redactions.length) {
|
|
1280
|
+
let body = text;
|
|
1281
|
+
for (const { rule, redactors } of redactions) body = applyRedactors(body, redactors, maskFn(rule.category));
|
|
1282
|
+
return rebuildResponse(response, body);
|
|
1283
|
+
}
|
|
1284
|
+
return response;
|
|
1285
|
+
};
|
|
1286
|
+
const allow = new Set((options.allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
1287
|
+
const egressShouldBlock = (url, host, method) => {
|
|
1288
|
+
if (host && allow.has(host.toLowerCase())) return false;
|
|
1289
|
+
let result;
|
|
1290
|
+
try {
|
|
1291
|
+
result = egressEngine.evaluate({ _egress: { url, host, method } });
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
onError?.(err);
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
if (!result.blocked) return false;
|
|
1297
|
+
onDetect({ phase: "egress", mode, category: result.rule?.category, rule: result.rule, message: result.message });
|
|
1298
|
+
return mode === "block";
|
|
1087
1299
|
};
|
|
1088
1300
|
const protection = {
|
|
1089
1301
|
mode,
|
|
1090
|
-
rules:
|
|
1091
|
-
// (request) => Response | null (null = allow, caller proceeds)
|
|
1302
|
+
rules: { request: requestRules, response: responseRules, egress: egressRules },
|
|
1303
|
+
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
1092
1304
|
fetchGuard() {
|
|
1093
1305
|
return async (request) => {
|
|
1094
1306
|
let result;
|
|
@@ -1098,15 +1310,20 @@ async function createProtection(options = {}) {
|
|
|
1098
1310
|
onError?.(err);
|
|
1099
1311
|
return null;
|
|
1100
1312
|
}
|
|
1101
|
-
return decide(result, () => blockResponse(result), () => null);
|
|
1313
|
+
return decide("request", result, () => blockResponse(result), () => null);
|
|
1102
1314
|
};
|
|
1103
1315
|
},
|
|
1104
|
-
// Wrap a fetch handler:
|
|
1316
|
+
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
1105
1317
|
fetch(handler) {
|
|
1106
1318
|
const guard = protection.fetchGuard();
|
|
1107
|
-
return async (request, ...rest) =>
|
|
1319
|
+
return async (request, ...rest) => {
|
|
1320
|
+
const blocked = await guard(request);
|
|
1321
|
+
if (blocked) return blocked;
|
|
1322
|
+
const response = await handler(request, ...rest);
|
|
1323
|
+
return screenFetchResponse(response);
|
|
1324
|
+
};
|
|
1108
1325
|
},
|
|
1109
|
-
// Express middleware (expects express-parsed req.query/req.body).
|
|
1326
|
+
// Express middleware (request phase; expects express-parsed req.query/req.body).
|
|
1110
1327
|
express() {
|
|
1111
1328
|
return (req, res, next) => {
|
|
1112
1329
|
let result;
|
|
@@ -1116,10 +1333,10 @@ async function createProtection(options = {}) {
|
|
|
1116
1333
|
onError?.(err);
|
|
1117
1334
|
return next();
|
|
1118
1335
|
}
|
|
1119
|
-
decide(result, () => res.status(403).json(blockBody(result)), () => next());
|
|
1336
|
+
decide("request", result, () => res.status(403).json(blockBody(result)), () => next());
|
|
1120
1337
|
};
|
|
1121
1338
|
},
|
|
1122
|
-
// Node / Connect middleware — buffers the body itself (
|
|
1339
|
+
// Node / Connect middleware — buffers the body itself (request phase).
|
|
1123
1340
|
node(nodeOptions = {}) {
|
|
1124
1341
|
const maxBytes = nodeOptions.maxBodyBytes ?? 1024 * 1024;
|
|
1125
1342
|
return (req, res, next) => {
|
|
@@ -1140,28 +1357,104 @@ async function createProtection(options = {}) {
|
|
|
1140
1357
|
});
|
|
1141
1358
|
req.on("end", () => {
|
|
1142
1359
|
const rawBody = overflow ? "" : Buffer.concat(chunks).toString("utf8");
|
|
1360
|
+
let shaped;
|
|
1143
1361
|
let result;
|
|
1144
1362
|
try {
|
|
1145
|
-
|
|
1363
|
+
shaped = fromNodeRequest(req, rawBody);
|
|
1364
|
+
result = engine.evaluate(shaped);
|
|
1146
1365
|
} catch (err) {
|
|
1147
1366
|
onError?.(err);
|
|
1148
1367
|
return next();
|
|
1149
1368
|
}
|
|
1150
1369
|
decide(
|
|
1370
|
+
"request",
|
|
1151
1371
|
result,
|
|
1152
1372
|
() => {
|
|
1153
1373
|
res.statusCode = 403;
|
|
1154
1374
|
res.setHeader("content-type", "application/json");
|
|
1155
1375
|
res.end(JSON.stringify(blockBody(result)));
|
|
1156
1376
|
},
|
|
1157
|
-
() =>
|
|
1377
|
+
() => {
|
|
1378
|
+
if (req.body === void 0) req.body = shaped.body;
|
|
1379
|
+
next();
|
|
1380
|
+
}
|
|
1158
1381
|
);
|
|
1159
1382
|
});
|
|
1160
1383
|
};
|
|
1161
1384
|
}
|
|
1162
1385
|
};
|
|
1386
|
+
if (options.egress) {
|
|
1387
|
+
protection.uninstallEgress = installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
|
|
1388
|
+
}
|
|
1163
1389
|
return protection;
|
|
1164
1390
|
}
|
|
1391
|
+
function byPhase(rules, phase) {
|
|
1392
|
+
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
1393
|
+
}
|
|
1394
|
+
async function readTextResponse(response) {
|
|
1395
|
+
if (!response || typeof response.clone !== "function") return null;
|
|
1396
|
+
const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
|
|
1397
|
+
const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1398
|
+
if (!isText) return null;
|
|
1399
|
+
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
1400
|
+
if (len && len > 512 * 1024) return null;
|
|
1401
|
+
try {
|
|
1402
|
+
const text = await response.clone().text();
|
|
1403
|
+
return text.length > 512 * 1024 ? null : text;
|
|
1404
|
+
} catch {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function headerObject(headers) {
|
|
1409
|
+
const out = {};
|
|
1410
|
+
headers?.forEach?.((v, k) => {
|
|
1411
|
+
out[k.toLowerCase()] = v;
|
|
1412
|
+
});
|
|
1413
|
+
return out;
|
|
1414
|
+
}
|
|
1415
|
+
function extractRedactors(rule) {
|
|
1416
|
+
const out = [];
|
|
1417
|
+
const walk = (conds) => {
|
|
1418
|
+
for (const c of conds ?? []) {
|
|
1419
|
+
if (Array.isArray(c.rules)) walk(c.rules);
|
|
1420
|
+
const m = c.match;
|
|
1421
|
+
if (!m) continue;
|
|
1422
|
+
if (m.type === "regex" && typeof m.value === "string") {
|
|
1423
|
+
const parsed = m.value.match(/^\/(.+)\/([a-z]*)$/is);
|
|
1424
|
+
if (parsed) {
|
|
1425
|
+
const flags = parsed[2].includes("g") ? parsed[2] : parsed[2] + "g";
|
|
1426
|
+
try {
|
|
1427
|
+
out.push({ re: new RegExp(parsed[1], flags) });
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
1432
|
+
out.push({ literal: String(m.value) });
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
walk(rule.rule_v2);
|
|
1437
|
+
return out;
|
|
1438
|
+
}
|
|
1439
|
+
function applyRedactors(body, redactors, mask) {
|
|
1440
|
+
let out = body;
|
|
1441
|
+
for (const r of redactors) {
|
|
1442
|
+
if (r.re) out = out.replace(r.re, mask);
|
|
1443
|
+
else if (r.literal) out = out.split(r.literal).join(mask);
|
|
1444
|
+
}
|
|
1445
|
+
return out;
|
|
1446
|
+
}
|
|
1447
|
+
function rebuildResponse(response, body) {
|
|
1448
|
+
const headers = new Headers(response.headers);
|
|
1449
|
+
headers.delete("content-length");
|
|
1450
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
1451
|
+
}
|
|
1452
|
+
function leakResponse() {
|
|
1453
|
+
return new Response(JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" }), {
|
|
1454
|
+
status: 500,
|
|
1455
|
+
headers: { "content-type": "application/json" }
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1165
1458
|
async function resolveRules(options) {
|
|
1166
1459
|
if (options.rules) {
|
|
1167
1460
|
return normalizeBundle(options.rules);
|
|
@@ -1226,9 +1519,9 @@ function blockResponse(result) {
|
|
|
1226
1519
|
headers: { "content-type": "application/json" }
|
|
1227
1520
|
});
|
|
1228
1521
|
}
|
|
1229
|
-
function defaultOnDetect({ mode, rule, message }) {
|
|
1522
|
+
function defaultOnDetect({ phase, mode, category, rule, message }) {
|
|
1230
1523
|
const tag = mode === "block" ? "BLOCK" : "DETECT (dry-run)";
|
|
1231
|
-
console.warn(`[patchstack] ${tag} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
|
|
1524
|
+
console.warn(`[patchstack] ${tag} phase=${phase ?? "request"} category=${category ?? "?"} rule=${rule?.id ?? "?"} ${message ?? ""}`.trim());
|
|
1232
1525
|
}
|
|
1233
1526
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1234
1527
|
0 && (module.exports = {
|