@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.js
CHANGED
|
@@ -961,6 +961,30 @@ function expandIPv6(host) {
|
|
|
961
961
|
}
|
|
962
962
|
return out;
|
|
963
963
|
}
|
|
964
|
+
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;
|
|
965
|
+
function base64UrlToBase64(segment) {
|
|
966
|
+
const padded = segment.length % 4 === 0 ? segment : segment + "=".repeat(4 - segment.length % 4);
|
|
967
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
968
|
+
}
|
|
969
|
+
function jwtClaimSpans(text, claim, value) {
|
|
970
|
+
const found = /* @__PURE__ */ new Set();
|
|
971
|
+
if (typeof text !== "string" || typeof claim !== "string" || claim === "") return [];
|
|
972
|
+
const wanted = String(value);
|
|
973
|
+
for (const [token] of text.matchAll(JWT_CANDIDATE)) {
|
|
974
|
+
const parts = token.split(".");
|
|
975
|
+
let payload;
|
|
976
|
+
try {
|
|
977
|
+
payload = JSON.parse(base64DecodeUtf8(base64UrlToBase64(parts[1])));
|
|
978
|
+
} catch {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
982
|
+
if (!Object.prototype.hasOwnProperty.call(payload, claim)) continue;
|
|
983
|
+
const actual = payload[claim];
|
|
984
|
+
if (typeof actual === "string" && actual === wanted) found.add(token);
|
|
985
|
+
}
|
|
986
|
+
return [...found];
|
|
987
|
+
}
|
|
964
988
|
var warnedScopes = /* @__PURE__ */ new Set();
|
|
965
989
|
function warnUnrecognisedScope(when) {
|
|
966
990
|
const key = Object.keys(when).sort().join(",");
|
|
@@ -989,16 +1013,30 @@ function ruleAppliesTo(when, resolver) {
|
|
|
989
1013
|
return true;
|
|
990
1014
|
}
|
|
991
1015
|
}
|
|
1016
|
+
function foldPathForScope(value) {
|
|
1017
|
+
const out = String(value).toLowerCase();
|
|
1018
|
+
return out.endsWith("/") ? out.slice(0, -1) : out;
|
|
1019
|
+
}
|
|
1020
|
+
var REGEX_SYNTAX = /[\\^$*+?()[\]{}|]/;
|
|
1021
|
+
var REGEX_FORM = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
1022
|
+
function isRegexForm(pattern) {
|
|
1023
|
+
const parts = REGEX_FORM.exec(pattern);
|
|
1024
|
+
return parts !== null && REGEX_SYNTAX.test(parts[1]);
|
|
1025
|
+
}
|
|
992
1026
|
function pathMatches(pattern, path) {
|
|
993
|
-
if (
|
|
1027
|
+
if (isRegexForm(pattern)) {
|
|
994
1028
|
const re = safeRegExp(pattern);
|
|
995
1029
|
return re ? re.test(path) : false;
|
|
996
1030
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1031
|
+
const target = foldPathForScope(path);
|
|
1032
|
+
const folded = foldPathForScope(pattern);
|
|
1033
|
+
if (folded.includes("*")) {
|
|
1034
|
+
const trailing = folded.endsWith("/*");
|
|
1035
|
+
const body = (trailing ? folded.slice(0, -2) : folded).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*");
|
|
1036
|
+
const re = safeRegExp("/^" + body + (trailing ? "(?:/[^?]*)?" : "") + "$/");
|
|
1037
|
+
return re ? re.test(target) : false;
|
|
1000
1038
|
}
|
|
1001
|
-
return
|
|
1039
|
+
return target === folded;
|
|
1002
1040
|
}
|
|
1003
1041
|
function normalizeDefaultPort(host) {
|
|
1004
1042
|
return String(host).toLowerCase().replace(/:(?:80|443)$/, "");
|
|
@@ -1151,6 +1189,8 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
1151
1189
|
const stripped = strValue.replace(/[ _\-,]/g, "");
|
|
1152
1190
|
return ctypeResult(strValue, /^[\w$-]*$/.test(stripped), matchVal);
|
|
1153
1191
|
}
|
|
1192
|
+
case "jwt_claim_equals":
|
|
1193
|
+
return jwtClaimSpans(strValue, matchObj?.claim, matchVal).length > 0;
|
|
1154
1194
|
case "array_key_value":
|
|
1155
1195
|
return arrayKeyValue(value, matchObj);
|
|
1156
1196
|
// file_contains (uploaded-file content scanning) is NOT WordPress-specific, but is
|
|
@@ -1916,13 +1956,43 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
1916
1956
|
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])/" } }]
|
|
1917
1957
|
},
|
|
1918
1958
|
{
|
|
1919
|
-
id: "resp-
|
|
1920
|
-
title: "
|
|
1959
|
+
id: "resp-supabase-secret-key",
|
|
1960
|
+
title: "Supabase secret key in response body",
|
|
1921
1961
|
phase: "response",
|
|
1922
1962
|
category: "secret-exposure",
|
|
1923
1963
|
action: "redact",
|
|
1964
|
+
// Supabase's opaque key format, which is not a JWT and so is not covered by the JWT rule below.
|
|
1965
|
+
// `sb_secret_` is the elevated one — documented as full access, bypassing Row Level Security,
|
|
1966
|
+
// backend-only — while `sb_publishable_` is meant for public clients and must pass untouched.
|
|
1967
|
+
// Both key systems are live at once: new keys sit alongside the legacy `anon` / `service_role`
|
|
1968
|
+
// JWTs rather than replacing them, so this rule and the JWT rule cover different halves.
|
|
1969
|
+
//
|
|
1970
|
+
// The documented grammar is the prefix, 22 base64url characters, `_`, then an 8-character
|
|
1971
|
+
// base64url checksum. Matching it exactly is what keeps the mask on the key and off the response
|
|
1972
|
+
// around it: a variable-length suffix would either run past the key into adjacent syntax or stop
|
|
1973
|
+
// short and leave the tail of a real key visible. The trailing guard rejects a longer run of key
|
|
1974
|
+
// characters, which is not this format.
|
|
1975
|
+
prefilter: ["sb_secret_"],
|
|
1976
|
+
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_-])/" } }]
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
id: "resp-supabase-service-role-key",
|
|
1980
|
+
title: "Supabase service_role key in response body",
|
|
1981
|
+
phase: "response",
|
|
1982
|
+
category: "secret-exposure",
|
|
1983
|
+
action: "redact",
|
|
1984
|
+
// Scoped to the one role that must never leave: `service_role` bypasses Row Level Security
|
|
1985
|
+
// entirely. A JWT in a response body is not inherently a leak — the Supabase `anon` key is public
|
|
1986
|
+
// by design and a login response carries the user's own access token — so this decides on the
|
|
1987
|
+
// decoded payload rather than on the shape of the token.
|
|
1988
|
+
//
|
|
1989
|
+
// `jwt_claim_equals` also yields the matching token spans, which is what lets `redact` mask those
|
|
1990
|
+
// tokens and serve the rest of the response.
|
|
1991
|
+
//
|
|
1992
|
+
// A broad any-JWT rule is a reasonable thing to want; it belongs in the managed hardening bundle as
|
|
1993
|
+
// an opt-in, observe-only rule rather than an on-by-default mask.
|
|
1924
1994
|
prefilter: ["eyJ"],
|
|
1925
|
-
rule_v2: [{ parameter: "response.body", match: { type: "
|
|
1995
|
+
rule_v2: [{ parameter: "response.body", match: { type: "jwt_claim_equals", claim: "role", value: "service_role" } }]
|
|
1926
1996
|
},
|
|
1927
1997
|
{
|
|
1928
1998
|
id: "resp-db-connection-string",
|
|
@@ -2205,6 +2275,7 @@ var MATCH_TYPES = Object.freeze([
|
|
|
2205
2275
|
"internal_host",
|
|
2206
2276
|
"is_numeric",
|
|
2207
2277
|
"isset",
|
|
2278
|
+
"jwt_claim_equals",
|
|
2208
2279
|
"less_than",
|
|
2209
2280
|
"more_than",
|
|
2210
2281
|
"not_contains",
|
|
@@ -2230,6 +2301,8 @@ var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
|
2230
2301
|
"cors_reflected"
|
|
2231
2302
|
]);
|
|
2232
2303
|
var OPERAND_SHAPES = Object.freeze([
|
|
2304
|
+
"positive-number",
|
|
2305
|
+
"boolean",
|
|
2233
2306
|
"non-empty-string",
|
|
2234
2307
|
"regex-literal",
|
|
2235
2308
|
"string-or-string-list",
|
|
@@ -2254,6 +2327,14 @@ var MATCH_OPERANDS = Object.freeze({
|
|
|
2254
2327
|
required: Object.freeze(["key", "match"]),
|
|
2255
2328
|
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
2256
2329
|
}),
|
|
2330
|
+
// Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
|
|
2331
|
+
// `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
|
|
2332
|
+
// exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
|
|
2333
|
+
// oversized, claim missing, claim not a string, or a different value — is a no-match.
|
|
2334
|
+
jwt_claim_equals: Object.freeze({
|
|
2335
|
+
required: Object.freeze(["claim", "value"]),
|
|
2336
|
+
shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
|
|
2337
|
+
}),
|
|
2257
2338
|
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
2258
2339
|
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
2259
2340
|
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
@@ -2279,6 +2360,10 @@ function operandShapeProblem(shape, value) {
|
|
|
2279
2360
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
2280
2361
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
2281
2362
|
switch (shape) {
|
|
2363
|
+
case "positive-number":
|
|
2364
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
2365
|
+
case "boolean":
|
|
2366
|
+
return typeof value === "boolean" ? null : "must be true or false";
|
|
2282
2367
|
case "non-empty-string":
|
|
2283
2368
|
return isFilledString(value) ? null : "must be a non-empty string";
|
|
2284
2369
|
case "regex-literal":
|
|
@@ -2361,6 +2446,13 @@ var ACTIONS = Object.freeze([
|
|
|
2361
2446
|
"remove-header",
|
|
2362
2447
|
"harden-cookie"
|
|
2363
2448
|
]);
|
|
2449
|
+
var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
2450
|
+
max_bytes: "positive-number",
|
|
2451
|
+
bypass_limit: "boolean",
|
|
2452
|
+
ensure: "boolean",
|
|
2453
|
+
prefilter: "non-empty-string-list"
|
|
2454
|
+
});
|
|
2455
|
+
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
2364
2456
|
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
2365
2457
|
var RULE_PROPERTIES = Object.freeze([
|
|
2366
2458
|
"id",
|
|
@@ -2536,6 +2628,24 @@ function whenProblem(when) {
|
|
|
2536
2628
|
function isNonEmptyString(value) {
|
|
2537
2629
|
return typeof value === "string" && value.trim() !== "";
|
|
2538
2630
|
}
|
|
2631
|
+
function nullPropertyProblem(rule) {
|
|
2632
|
+
if (!rule || typeof rule !== "object") return null;
|
|
2633
|
+
for (const property of RULE_PROPERTIES) {
|
|
2634
|
+
if (property in rule && rule[property] === null) {
|
|
2635
|
+
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`;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return null;
|
|
2639
|
+
}
|
|
2640
|
+
function rulePropertyProblem(rule) {
|
|
2641
|
+
if (!rule || typeof rule !== "object") return null;
|
|
2642
|
+
for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
|
|
2643
|
+
if (rule[property] === void 0 || rule[property] === null) continue;
|
|
2644
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
2645
|
+
if (problem) return `"${property}" ${problem}`;
|
|
2646
|
+
}
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2539
2649
|
function actionProblem(action, rule) {
|
|
2540
2650
|
if (action === void 0 || action === null) return null;
|
|
2541
2651
|
if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
|
|
@@ -2600,13 +2710,15 @@ function idOf(rule) {
|
|
|
2600
2710
|
}
|
|
2601
2711
|
function ruleProblem(rule) {
|
|
2602
2712
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
2713
|
+
const nullReason = nullPropertyProblem(rule);
|
|
2714
|
+
if (nullReason) return nullReason;
|
|
2603
2715
|
if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
|
|
2604
2716
|
const actionReason = actionProblem(rule.action, rule);
|
|
2605
2717
|
if (actionReason) return actionReason;
|
|
2606
2718
|
const scopeReason = whenProblem(rule.when);
|
|
2607
2719
|
if (scopeReason) return scopeReason;
|
|
2608
|
-
const
|
|
2609
|
-
if (
|
|
2720
|
+
const propertyReason = rulePropertyProblem(rule);
|
|
2721
|
+
if (propertyReason) return propertyReason;
|
|
2610
2722
|
return conditionsProblem(rule.rule_v2);
|
|
2611
2723
|
}
|
|
2612
2724
|
function conditionsProblem(conditions, depth = 0) {
|
|
@@ -3685,7 +3797,7 @@ async function createProtection(options = {}) {
|
|
|
3685
3797
|
let reporter = null;
|
|
3686
3798
|
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
3687
3799
|
try {
|
|
3688
|
-
({ reportManifest: reporter } = await import("./refresh-manifest-
|
|
3800
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-KNZQBC4V.js"));
|
|
3689
3801
|
} catch (err) {
|
|
3690
3802
|
notify(onError, err, "onError");
|
|
3691
3803
|
}
|
|
@@ -3897,6 +4009,8 @@ function extractRedactors(rule) {
|
|
|
3897
4009
|
}
|
|
3898
4010
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
3899
4011
|
out.push({ literal: String(m.value) });
|
|
4012
|
+
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
4013
|
+
out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") } });
|
|
3900
4014
|
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
3901
4015
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
3902
4016
|
for (const key of keys) {
|
|
@@ -3915,7 +4029,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
3915
4029
|
let out = body;
|
|
3916
4030
|
for (const r of redactors) {
|
|
3917
4031
|
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
3918
|
-
else if (r.
|
|
4032
|
+
else if (r.jwtClaim) {
|
|
4033
|
+
for (const token of jwtClaimSpans(out, r.jwtClaim.claim, r.jwtClaim.value)) {
|
|
4034
|
+
out = out.split(token).join(transform ? transform(token) : mask);
|
|
4035
|
+
}
|
|
4036
|
+
} else if (r.literal) {
|
|
3919
4037
|
const re = new RegExp(r.literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
3920
4038
|
out = out.replace(re, (m) => transform ? transform(m) : mask);
|
|
3921
4039
|
}
|