@patchstack/connect 0.3.30 → 0.3.31
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 +5 -2
- package/README.md +16 -1
- package/dist/cli.js +214 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +48 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +48 -18
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +176 -28
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +129 -11
- package/dist/protect.edge.js.map +2 -2
- package/dist/protect.js +130 -12
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-ZSP76JWQ.js → refresh-manifest-KNZQBC4V.js} +48 -18
- package/dist/refresh-manifest-KNZQBC4V.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-ZSP76JWQ.js.map +0 -1
package/dist/protect.edge.js
CHANGED
|
@@ -990,6 +990,30 @@ function expandIPv6(host) {
|
|
|
990
990
|
}
|
|
991
991
|
return out;
|
|
992
992
|
}
|
|
993
|
+
var JWT_CANDIDATE = /(?<![A-Za-z0-9_-])(?<![A-Za-z0-9_-]\.)eyJ[A-Za-z0-9_-]{2,2048}\.[A-Za-z0-9_-]{2,8192}\.[A-Za-z0-9_-]{0,2048}(?![A-Za-z0-9_-])(?!\.[A-Za-z0-9_-])/g;
|
|
994
|
+
function base64UrlToBase64(segment) {
|
|
995
|
+
const padded = segment.length % 4 === 0 ? segment : segment + "=".repeat(4 - segment.length % 4);
|
|
996
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
997
|
+
}
|
|
998
|
+
function jwtClaimSpans(text, claim, value) {
|
|
999
|
+
const found = /* @__PURE__ */ new Set();
|
|
1000
|
+
if (typeof text !== "string" || typeof claim !== "string" || claim === "") return [];
|
|
1001
|
+
const wanted = String(value);
|
|
1002
|
+
for (const [token] of text.matchAll(JWT_CANDIDATE)) {
|
|
1003
|
+
const parts = token.split(".");
|
|
1004
|
+
let payload;
|
|
1005
|
+
try {
|
|
1006
|
+
payload = JSON.parse(base64DecodeUtf8(base64UrlToBase64(parts[1])));
|
|
1007
|
+
} catch {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
1011
|
+
if (!Object.prototype.hasOwnProperty.call(payload, claim)) continue;
|
|
1012
|
+
const actual = payload[claim];
|
|
1013
|
+
if (typeof actual === "string" && actual === wanted) found.add(token);
|
|
1014
|
+
}
|
|
1015
|
+
return [...found];
|
|
1016
|
+
}
|
|
993
1017
|
var warnedScopes = /* @__PURE__ */ new Set();
|
|
994
1018
|
function warnUnrecognisedScope(when) {
|
|
995
1019
|
const key = Object.keys(when).sort().join(",");
|
|
@@ -1018,16 +1042,30 @@ function ruleAppliesTo(when, resolver) {
|
|
|
1018
1042
|
return true;
|
|
1019
1043
|
}
|
|
1020
1044
|
}
|
|
1045
|
+
function foldPathForScope(value) {
|
|
1046
|
+
const out = String(value).toLowerCase();
|
|
1047
|
+
return out.endsWith("/") ? out.slice(0, -1) : out;
|
|
1048
|
+
}
|
|
1049
|
+
var REGEX_SYNTAX = /[\\^$*+?()[\]{}|]/;
|
|
1050
|
+
var REGEX_FORM = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
1051
|
+
function isRegexForm(pattern) {
|
|
1052
|
+
const parts = REGEX_FORM.exec(pattern);
|
|
1053
|
+
return parts !== null && REGEX_SYNTAX.test(parts[1]);
|
|
1054
|
+
}
|
|
1021
1055
|
function pathMatches(pattern, path) {
|
|
1022
|
-
if (
|
|
1056
|
+
if (isRegexForm(pattern)) {
|
|
1023
1057
|
const re = safeRegExp(pattern);
|
|
1024
1058
|
return re ? re.test(path) : false;
|
|
1025
1059
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1060
|
+
const target = foldPathForScope(path);
|
|
1061
|
+
const folded = foldPathForScope(pattern);
|
|
1062
|
+
if (folded.includes("*")) {
|
|
1063
|
+
const trailing = folded.endsWith("/*");
|
|
1064
|
+
const body = (trailing ? folded.slice(0, -2) : folded).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*");
|
|
1065
|
+
const re = safeRegExp("/^" + body + (trailing ? "(?:/[^?]*)?" : "") + "$/");
|
|
1066
|
+
return re ? re.test(target) : false;
|
|
1029
1067
|
}
|
|
1030
|
-
return
|
|
1068
|
+
return target === folded;
|
|
1031
1069
|
}
|
|
1032
1070
|
function normalizeDefaultPort(host) {
|
|
1033
1071
|
return String(host).toLowerCase().replace(/:(?:80|443)$/, "");
|
|
@@ -1180,6 +1218,8 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
1180
1218
|
const stripped = strValue.replace(/[ _\-,]/g, "");
|
|
1181
1219
|
return ctypeResult(strValue, /^[\w$-]*$/.test(stripped), matchVal);
|
|
1182
1220
|
}
|
|
1221
|
+
case "jwt_claim_equals":
|
|
1222
|
+
return jwtClaimSpans(strValue, matchObj?.claim, matchVal).length > 0;
|
|
1183
1223
|
case "array_key_value":
|
|
1184
1224
|
return arrayKeyValue(value, matchObj);
|
|
1185
1225
|
// file_contains (uploaded-file content scanning) is NOT WordPress-specific, but is
|
|
@@ -2008,13 +2048,43 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
2008
2048
|
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\b(?:sk_live_[0-9A-Za-z]{16,}|rk_live_[0-9A-Za-z]{16,}|gh[opsu]_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{60,}|glpat-[0-9A-Za-z_-]{20,}|xox[baprs]-[0-9A-Za-z-]{10,}|sk-ant-[0-9A-Za-z_-]{20,}|ya29\\.[0-9A-Za-z_-]{20,}|npm_[0-9A-Za-z]{36})(?![0-9A-Za-z])/" } }]
|
|
2009
2049
|
},
|
|
2010
2050
|
{
|
|
2011
|
-
id: "resp-
|
|
2012
|
-
title: "
|
|
2051
|
+
id: "resp-supabase-secret-key",
|
|
2052
|
+
title: "Supabase secret key in response body",
|
|
2013
2053
|
phase: "response",
|
|
2014
2054
|
category: "secret-exposure",
|
|
2015
2055
|
action: "redact",
|
|
2056
|
+
// Supabase's opaque key format, which is not a JWT and so is not covered by the JWT rule below.
|
|
2057
|
+
// `sb_secret_` is the elevated one — documented as full access, bypassing Row Level Security,
|
|
2058
|
+
// backend-only — while `sb_publishable_` is meant for public clients and must pass untouched.
|
|
2059
|
+
// Both key systems are live at once: new keys sit alongside the legacy `anon` / `service_role`
|
|
2060
|
+
// JWTs rather than replacing them, so this rule and the JWT rule cover different halves.
|
|
2061
|
+
//
|
|
2062
|
+
// The documented grammar is the prefix, 22 base64url characters, `_`, then an 8-character
|
|
2063
|
+
// base64url checksum. Matching it exactly is what keeps the mask on the key and off the response
|
|
2064
|
+
// around it: a variable-length suffix would either run past the key into adjacent syntax or stop
|
|
2065
|
+
// short and leave the tail of a real key visible. The trailing guard rejects a longer run of key
|
|
2066
|
+
// characters, which is not this format.
|
|
2067
|
+
prefilter: ["sb_secret_"],
|
|
2068
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\bsb_secret_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{8}(?![A-Za-z0-9_-])/" } }]
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
id: "resp-supabase-service-role-key",
|
|
2072
|
+
title: "Supabase service_role key in response body",
|
|
2073
|
+
phase: "response",
|
|
2074
|
+
category: "secret-exposure",
|
|
2075
|
+
action: "redact",
|
|
2076
|
+
// Scoped to the one role that must never leave: `service_role` bypasses Row Level Security
|
|
2077
|
+
// entirely. A JWT in a response body is not inherently a leak — the Supabase `anon` key is public
|
|
2078
|
+
// by design and a login response carries the user's own access token — so this decides on the
|
|
2079
|
+
// decoded payload rather than on the shape of the token.
|
|
2080
|
+
//
|
|
2081
|
+
// `jwt_claim_equals` also yields the matching token spans, which is what lets `redact` mask those
|
|
2082
|
+
// tokens and serve the rest of the response.
|
|
2083
|
+
//
|
|
2084
|
+
// A broad any-JWT rule is a reasonable thing to want; it belongs in the managed hardening bundle as
|
|
2085
|
+
// an opt-in, observe-only rule rather than an on-by-default mask.
|
|
2016
2086
|
prefilter: ["eyJ"],
|
|
2017
|
-
rule_v2: [{ parameter: "response.body", match: { type: "
|
|
2087
|
+
rule_v2: [{ parameter: "response.body", match: { type: "jwt_claim_equals", claim: "role", value: "service_role" } }]
|
|
2018
2088
|
},
|
|
2019
2089
|
{
|
|
2020
2090
|
id: "resp-db-connection-string",
|
|
@@ -2297,6 +2367,7 @@ var MATCH_TYPES = Object.freeze([
|
|
|
2297
2367
|
"internal_host",
|
|
2298
2368
|
"is_numeric",
|
|
2299
2369
|
"isset",
|
|
2370
|
+
"jwt_claim_equals",
|
|
2300
2371
|
"less_than",
|
|
2301
2372
|
"more_than",
|
|
2302
2373
|
"not_contains",
|
|
@@ -2322,6 +2393,8 @@ var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
|
2322
2393
|
"cors_reflected"
|
|
2323
2394
|
]);
|
|
2324
2395
|
var OPERAND_SHAPES = Object.freeze([
|
|
2396
|
+
"positive-number",
|
|
2397
|
+
"boolean",
|
|
2325
2398
|
"non-empty-string",
|
|
2326
2399
|
"regex-literal",
|
|
2327
2400
|
"string-or-string-list",
|
|
@@ -2346,6 +2419,14 @@ var MATCH_OPERANDS = Object.freeze({
|
|
|
2346
2419
|
required: Object.freeze(["key", "match"]),
|
|
2347
2420
|
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
2348
2421
|
}),
|
|
2422
|
+
// Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
|
|
2423
|
+
// `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
|
|
2424
|
+
// exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
|
|
2425
|
+
// oversized, claim missing, claim not a string, or a different value — is a no-match.
|
|
2426
|
+
jwt_claim_equals: Object.freeze({
|
|
2427
|
+
required: Object.freeze(["claim", "value"]),
|
|
2428
|
+
shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
|
|
2429
|
+
}),
|
|
2349
2430
|
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
2350
2431
|
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
2351
2432
|
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
@@ -2371,6 +2452,10 @@ function operandShapeProblem(shape, value) {
|
|
|
2371
2452
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
2372
2453
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
2373
2454
|
switch (shape) {
|
|
2455
|
+
case "positive-number":
|
|
2456
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
2457
|
+
case "boolean":
|
|
2458
|
+
return typeof value === "boolean" ? null : "must be true or false";
|
|
2374
2459
|
case "non-empty-string":
|
|
2375
2460
|
return isFilledString(value) ? null : "must be a non-empty string";
|
|
2376
2461
|
case "regex-literal":
|
|
@@ -2453,6 +2538,13 @@ var ACTIONS = Object.freeze([
|
|
|
2453
2538
|
"remove-header",
|
|
2454
2539
|
"harden-cookie"
|
|
2455
2540
|
]);
|
|
2541
|
+
var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
2542
|
+
max_bytes: "positive-number",
|
|
2543
|
+
bypass_limit: "boolean",
|
|
2544
|
+
ensure: "boolean",
|
|
2545
|
+
prefilter: "non-empty-string-list"
|
|
2546
|
+
});
|
|
2547
|
+
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
2456
2548
|
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
2457
2549
|
var RULE_PROPERTIES = Object.freeze([
|
|
2458
2550
|
"id",
|
|
@@ -2628,6 +2720,24 @@ function whenProblem(when) {
|
|
|
2628
2720
|
function isNonEmptyString(value) {
|
|
2629
2721
|
return typeof value === "string" && value.trim() !== "";
|
|
2630
2722
|
}
|
|
2723
|
+
function nullPropertyProblem(rule) {
|
|
2724
|
+
if (!rule || typeof rule !== "object") return null;
|
|
2725
|
+
for (const property of RULE_PROPERTIES) {
|
|
2726
|
+
if (property in rule && rule[property] === null) {
|
|
2727
|
+
return `"${property}" is present but null; omit it to mean the default, since a null is a value that was meant to be something and is not`;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
function rulePropertyProblem(rule) {
|
|
2733
|
+
if (!rule || typeof rule !== "object") return null;
|
|
2734
|
+
for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
|
|
2735
|
+
if (rule[property] === void 0 || rule[property] === null) continue;
|
|
2736
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
2737
|
+
if (problem) return `"${property}" ${problem}`;
|
|
2738
|
+
}
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2631
2741
|
function actionProblem(action, rule) {
|
|
2632
2742
|
if (action === void 0 || action === null) return null;
|
|
2633
2743
|
if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
|
|
@@ -2692,13 +2802,15 @@ function idOf(rule) {
|
|
|
2692
2802
|
}
|
|
2693
2803
|
function ruleProblem(rule) {
|
|
2694
2804
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
2805
|
+
const nullReason = nullPropertyProblem(rule);
|
|
2806
|
+
if (nullReason) return nullReason;
|
|
2695
2807
|
if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
|
|
2696
2808
|
const actionReason = actionProblem(rule.action, rule);
|
|
2697
2809
|
if (actionReason) return actionReason;
|
|
2698
2810
|
const scopeReason = whenProblem(rule.when);
|
|
2699
2811
|
if (scopeReason) return scopeReason;
|
|
2700
|
-
const
|
|
2701
|
-
if (
|
|
2812
|
+
const propertyReason = rulePropertyProblem(rule);
|
|
2813
|
+
if (propertyReason) return propertyReason;
|
|
2702
2814
|
return conditionsProblem(rule.rule_v2);
|
|
2703
2815
|
}
|
|
2704
2816
|
function conditionsProblem(conditions, depth = 0) {
|
|
@@ -3989,6 +4101,8 @@ function extractRedactors(rule) {
|
|
|
3989
4101
|
}
|
|
3990
4102
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
3991
4103
|
out.push({ literal: String(m.value) });
|
|
4104
|
+
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
4105
|
+
out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") } });
|
|
3992
4106
|
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
3993
4107
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
3994
4108
|
for (const key of keys) {
|
|
@@ -4007,7 +4121,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
4007
4121
|
let out = body;
|
|
4008
4122
|
for (const r of redactors) {
|
|
4009
4123
|
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
4010
|
-
else if (r.
|
|
4124
|
+
else if (r.jwtClaim) {
|
|
4125
|
+
for (const token of jwtClaimSpans(out, r.jwtClaim.claim, r.jwtClaim.value)) {
|
|
4126
|
+
out = out.split(token).join(transform ? transform(token) : mask);
|
|
4127
|
+
}
|
|
4128
|
+
} else if (r.literal) {
|
|
4011
4129
|
const re = new RegExp(r.literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
4012
4130
|
out = out.replace(re, (m) => transform ? transform(m) : mask);
|
|
4013
4131
|
}
|