@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.cjs
CHANGED
|
@@ -682,7 +682,7 @@ async function walkNodeModules(cwd) {
|
|
|
682
682
|
seen.set(await (0, import_promises5.realpath)(cwd), -1);
|
|
683
683
|
} catch {
|
|
684
684
|
}
|
|
685
|
-
return dedupeByShallowest(await walk(root, 0, seen));
|
|
685
|
+
return dedupeByShallowest(await walk(root, 0, seen, cwd));
|
|
686
686
|
}
|
|
687
687
|
function dedupeByShallowest(found) {
|
|
688
688
|
const best = /* @__PURE__ */ new Map();
|
|
@@ -692,7 +692,7 @@ function dedupeByShallowest(found) {
|
|
|
692
692
|
}
|
|
693
693
|
return [...best.values()].map(({ entry }) => entry);
|
|
694
694
|
}
|
|
695
|
-
async function walk(dir, depth, seen) {
|
|
695
|
+
async function walk(dir, depth, seen, root) {
|
|
696
696
|
let names;
|
|
697
697
|
try {
|
|
698
698
|
names = await (0, import_promises5.readdir)(dir);
|
|
@@ -704,7 +704,7 @@ async function walk(dir, depth, seen) {
|
|
|
704
704
|
const fullPath = import_node_path4.default.join(dir, name);
|
|
705
705
|
const resolved = await claim(fullPath, seen, depth);
|
|
706
706
|
if (resolved === null) return [];
|
|
707
|
-
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen);
|
|
707
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
|
|
708
708
|
if (name.startsWith("@")) {
|
|
709
709
|
let subNames;
|
|
710
710
|
try {
|
|
@@ -717,16 +717,16 @@ async function walk(dir, depth, seen) {
|
|
|
717
717
|
const scopedDir = import_node_path4.default.join(fullPath, sub);
|
|
718
718
|
const scopedReal = await claim(scopedDir, seen, depth);
|
|
719
719
|
if (scopedReal === null) return [];
|
|
720
|
-
return packageAndNested(scopedDir, scopedReal, depth, seen);
|
|
720
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen, root);
|
|
721
721
|
});
|
|
722
722
|
return scoped.flat();
|
|
723
723
|
}
|
|
724
|
-
return packageAndNested(fullPath, resolved, depth, seen);
|
|
724
|
+
return packageAndNested(fullPath, resolved, depth, seen, root);
|
|
725
725
|
});
|
|
726
726
|
return perEntry.flat();
|
|
727
727
|
}
|
|
728
|
-
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
729
|
-
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
728
|
+
async function packageAndNested(pkgDir, resolved, depth, seen, root) {
|
|
729
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
|
|
730
730
|
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
731
731
|
}
|
|
732
732
|
async function mapLimit(items, fn) {
|
|
@@ -741,7 +741,10 @@ async function mapLimit(items, fn) {
|
|
|
741
741
|
await Promise.all(workers);
|
|
742
742
|
return results;
|
|
743
743
|
}
|
|
744
|
-
|
|
744
|
+
function installLocation(root, pkgDir, impl = import_node_path4.default) {
|
|
745
|
+
return impl.relative(root, pkgDir).split(impl.sep).join("/");
|
|
746
|
+
}
|
|
747
|
+
async function readPackage(pkgDir, depth, root) {
|
|
745
748
|
let raw;
|
|
746
749
|
try {
|
|
747
750
|
raw = await (0, import_promises5.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
|
|
@@ -760,14 +763,14 @@ async function readPackage(pkgDir, depth) {
|
|
|
760
763
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
761
764
|
return null;
|
|
762
765
|
}
|
|
763
|
-
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
766
|
+
return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
|
|
764
767
|
}
|
|
765
|
-
async function walkNested(pkgDir, depth, seen) {
|
|
768
|
+
async function walkNested(pkgDir, depth, seen, root) {
|
|
766
769
|
const nested = import_node_path4.default.join(pkgDir, "node_modules");
|
|
767
770
|
if (await claim(nested, seen, depth + 1) === null) {
|
|
768
771
|
return [];
|
|
769
772
|
}
|
|
770
|
-
return walk(nested, depth + 1, seen);
|
|
773
|
+
return walk(nested, depth + 1, seen, root);
|
|
771
774
|
}
|
|
772
775
|
async function claim(dir, seen, depth) {
|
|
773
776
|
let resolved;
|
|
@@ -1453,21 +1456,33 @@ var init_parsers = __esm({
|
|
|
1453
1456
|
});
|
|
1454
1457
|
|
|
1455
1458
|
// src/normalize.ts
|
|
1456
|
-
function buildWirePayload(manifest) {
|
|
1459
|
+
function buildWirePayload(manifest, options = {}) {
|
|
1460
|
+
const withPaths = options.installPaths === true;
|
|
1457
1461
|
const seen = /* @__PURE__ */ new Map();
|
|
1458
1462
|
const wirePackages = [];
|
|
1463
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
1464
|
+
let entriesWithoutPath = 0;
|
|
1459
1465
|
for (const entry of manifest.packages) {
|
|
1466
|
+
const identity = `${entry.name}@${entry.version}`;
|
|
1467
|
+
const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
|
|
1468
|
+
if (location === void 0) entriesWithoutPath++;
|
|
1469
|
+
const existing = byIdentity.get(identity);
|
|
1470
|
+
if (existing) {
|
|
1471
|
+
if (location !== void 0) addPath(existing, location);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1460
1474
|
const versions = seen.get(entry.name);
|
|
1461
1475
|
if (versions) {
|
|
1462
|
-
if (versions.has(entry.version)) {
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
1476
|
versions.add(entry.version);
|
|
1466
1477
|
} else {
|
|
1467
1478
|
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
1468
1479
|
}
|
|
1469
|
-
|
|
1480
|
+
const wire = { name: entry.name, version: entry.version };
|
|
1481
|
+
if (location !== void 0) addPath(wire, location);
|
|
1482
|
+
byIdentity.set(identity, wire);
|
|
1483
|
+
wirePackages.push(wire);
|
|
1470
1484
|
}
|
|
1485
|
+
for (const wire of wirePackages) wire.paths?.sort();
|
|
1471
1486
|
wirePackages.sort((a, b) => {
|
|
1472
1487
|
if (a.name === b.name) {
|
|
1473
1488
|
return compareVersions(a.version, b.version);
|
|
@@ -1481,7 +1496,18 @@ function buildWirePayload(manifest) {
|
|
|
1481
1496
|
}
|
|
1482
1497
|
}
|
|
1483
1498
|
return {
|
|
1484
|
-
payload: {
|
|
1499
|
+
payload: {
|
|
1500
|
+
ecosystem: manifest.ecosystem,
|
|
1501
|
+
packages: wirePackages,
|
|
1502
|
+
// Every entry had a location, so a missing path anywhere means the package is not installed there.
|
|
1503
|
+
// One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
|
|
1504
|
+
// no way to tell which entry was the incomplete one.
|
|
1505
|
+
//
|
|
1506
|
+
// No separate check for "locations were not requested": with them off every entry counts as without
|
|
1507
|
+
// one, so this is already false — which is the default, and the safe direction. An explicit
|
|
1508
|
+
// `withPaths &&` here was a guard no test could distinguish from its absence.
|
|
1509
|
+
installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
|
|
1510
|
+
},
|
|
1485
1511
|
stats: {
|
|
1486
1512
|
uniqueNames: seen.size,
|
|
1487
1513
|
duplicateNames,
|
|
@@ -1489,6 +1515,10 @@ function buildWirePayload(manifest) {
|
|
|
1489
1515
|
}
|
|
1490
1516
|
};
|
|
1491
1517
|
}
|
|
1518
|
+
function addPath(wire, installPath) {
|
|
1519
|
+
if (wire.paths === void 0) wire.paths = [installPath];
|
|
1520
|
+
else if (!wire.paths.includes(installPath)) wire.paths.push(installPath);
|
|
1521
|
+
}
|
|
1492
1522
|
function compareVersions(a, b) {
|
|
1493
1523
|
if (a === b) {
|
|
1494
1524
|
return 0;
|
|
@@ -2553,6 +2583,30 @@ function expandIPv6(host) {
|
|
|
2553
2583
|
}
|
|
2554
2584
|
return out;
|
|
2555
2585
|
}
|
|
2586
|
+
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;
|
|
2587
|
+
function base64UrlToBase64(segment) {
|
|
2588
|
+
const padded = segment.length % 4 === 0 ? segment : segment + "=".repeat(4 - segment.length % 4);
|
|
2589
|
+
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
2590
|
+
}
|
|
2591
|
+
function jwtClaimSpans(text, claim2, value) {
|
|
2592
|
+
const found = /* @__PURE__ */ new Set();
|
|
2593
|
+
if (typeof text !== "string" || typeof claim2 !== "string" || claim2 === "") return [];
|
|
2594
|
+
const wanted = String(value);
|
|
2595
|
+
for (const [token] of text.matchAll(JWT_CANDIDATE)) {
|
|
2596
|
+
const parts = token.split(".");
|
|
2597
|
+
let payload;
|
|
2598
|
+
try {
|
|
2599
|
+
payload = JSON.parse(base64DecodeUtf8(base64UrlToBase64(parts[1])));
|
|
2600
|
+
} catch {
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) continue;
|
|
2604
|
+
if (!Object.prototype.hasOwnProperty.call(payload, claim2)) continue;
|
|
2605
|
+
const actual = payload[claim2];
|
|
2606
|
+
if (typeof actual === "string" && actual === wanted) found.add(token);
|
|
2607
|
+
}
|
|
2608
|
+
return [...found];
|
|
2609
|
+
}
|
|
2556
2610
|
var warnedScopes = /* @__PURE__ */ new Set();
|
|
2557
2611
|
function warnUnrecognisedScope(when) {
|
|
2558
2612
|
const key = Object.keys(when).sort().join(",");
|
|
@@ -2581,16 +2635,30 @@ function ruleAppliesTo(when, resolver) {
|
|
|
2581
2635
|
return true;
|
|
2582
2636
|
}
|
|
2583
2637
|
}
|
|
2638
|
+
function foldPathForScope(value) {
|
|
2639
|
+
const out = String(value).toLowerCase();
|
|
2640
|
+
return out.endsWith("/") ? out.slice(0, -1) : out;
|
|
2641
|
+
}
|
|
2642
|
+
var REGEX_SYNTAX = /[\\^$*+?()[\]{}|]/;
|
|
2643
|
+
var REGEX_FORM = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
2644
|
+
function isRegexForm(pattern) {
|
|
2645
|
+
const parts = REGEX_FORM.exec(pattern);
|
|
2646
|
+
return parts !== null && REGEX_SYNTAX.test(parts[1]);
|
|
2647
|
+
}
|
|
2584
2648
|
function pathMatches(pattern, path7) {
|
|
2585
|
-
if (
|
|
2649
|
+
if (isRegexForm(pattern)) {
|
|
2586
2650
|
const re = safeRegExp(pattern);
|
|
2587
2651
|
return re ? re.test(path7) : false;
|
|
2588
2652
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2653
|
+
const target = foldPathForScope(path7);
|
|
2654
|
+
const folded = foldPathForScope(pattern);
|
|
2655
|
+
if (folded.includes("*")) {
|
|
2656
|
+
const trailing = folded.endsWith("/*");
|
|
2657
|
+
const body = (trailing ? folded.slice(0, -2) : folded).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*");
|
|
2658
|
+
const re = safeRegExp("/^" + body + (trailing ? "(?:/[^?]*)?" : "") + "$/");
|
|
2659
|
+
return re ? re.test(target) : false;
|
|
2592
2660
|
}
|
|
2593
|
-
return
|
|
2661
|
+
return target === folded;
|
|
2594
2662
|
}
|
|
2595
2663
|
function normalizeDefaultPort(host) {
|
|
2596
2664
|
return String(host).toLowerCase().replace(/:(?:80|443)$/, "");
|
|
@@ -2743,6 +2811,8 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
2743
2811
|
const stripped = strValue.replace(/[ _\-,]/g, "");
|
|
2744
2812
|
return ctypeResult(strValue, /^[\w$-]*$/.test(stripped), matchVal);
|
|
2745
2813
|
}
|
|
2814
|
+
case "jwt_claim_equals":
|
|
2815
|
+
return jwtClaimSpans(strValue, matchObj?.claim, matchVal).length > 0;
|
|
2746
2816
|
case "array_key_value":
|
|
2747
2817
|
return arrayKeyValue(value, matchObj);
|
|
2748
2818
|
// file_contains (uploaded-file content scanning) is NOT WordPress-specific, but is
|
|
@@ -3509,13 +3579,43 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
3509
3579
|
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])/" } }]
|
|
3510
3580
|
},
|
|
3511
3581
|
{
|
|
3512
|
-
id: "resp-
|
|
3513
|
-
title: "
|
|
3582
|
+
id: "resp-supabase-secret-key",
|
|
3583
|
+
title: "Supabase secret key in response body",
|
|
3514
3584
|
phase: "response",
|
|
3515
3585
|
category: "secret-exposure",
|
|
3516
3586
|
action: "redact",
|
|
3587
|
+
// Supabase's opaque key format, which is not a JWT and so is not covered by the JWT rule below.
|
|
3588
|
+
// `sb_secret_` is the elevated one — documented as full access, bypassing Row Level Security,
|
|
3589
|
+
// backend-only — while `sb_publishable_` is meant for public clients and must pass untouched.
|
|
3590
|
+
// Both key systems are live at once: new keys sit alongside the legacy `anon` / `service_role`
|
|
3591
|
+
// JWTs rather than replacing them, so this rule and the JWT rule cover different halves.
|
|
3592
|
+
//
|
|
3593
|
+
// The documented grammar is the prefix, 22 base64url characters, `_`, then an 8-character
|
|
3594
|
+
// base64url checksum. Matching it exactly is what keeps the mask on the key and off the response
|
|
3595
|
+
// around it: a variable-length suffix would either run past the key into adjacent syntax or stop
|
|
3596
|
+
// short and leave the tail of a real key visible. The trailing guard rejects a longer run of key
|
|
3597
|
+
// characters, which is not this format.
|
|
3598
|
+
prefilter: ["sb_secret_"],
|
|
3599
|
+
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_-])/" } }]
|
|
3600
|
+
},
|
|
3601
|
+
{
|
|
3602
|
+
id: "resp-supabase-service-role-key",
|
|
3603
|
+
title: "Supabase service_role key in response body",
|
|
3604
|
+
phase: "response",
|
|
3605
|
+
category: "secret-exposure",
|
|
3606
|
+
action: "redact",
|
|
3607
|
+
// Scoped to the one role that must never leave: `service_role` bypasses Row Level Security
|
|
3608
|
+
// entirely. A JWT in a response body is not inherently a leak — the Supabase `anon` key is public
|
|
3609
|
+
// by design and a login response carries the user's own access token — so this decides on the
|
|
3610
|
+
// decoded payload rather than on the shape of the token.
|
|
3611
|
+
//
|
|
3612
|
+
// `jwt_claim_equals` also yields the matching token spans, which is what lets `redact` mask those
|
|
3613
|
+
// tokens and serve the rest of the response.
|
|
3614
|
+
//
|
|
3615
|
+
// A broad any-JWT rule is a reasonable thing to want; it belongs in the managed hardening bundle as
|
|
3616
|
+
// an opt-in, observe-only rule rather than an on-by-default mask.
|
|
3517
3617
|
prefilter: ["eyJ"],
|
|
3518
|
-
rule_v2: [{ parameter: "response.body", match: { type: "
|
|
3618
|
+
rule_v2: [{ parameter: "response.body", match: { type: "jwt_claim_equals", claim: "role", value: "service_role" } }]
|
|
3519
3619
|
},
|
|
3520
3620
|
{
|
|
3521
3621
|
id: "resp-db-connection-string",
|
|
@@ -3798,6 +3898,7 @@ var MATCH_TYPES = Object.freeze([
|
|
|
3798
3898
|
"internal_host",
|
|
3799
3899
|
"is_numeric",
|
|
3800
3900
|
"isset",
|
|
3901
|
+
"jwt_claim_equals",
|
|
3801
3902
|
"less_than",
|
|
3802
3903
|
"more_than",
|
|
3803
3904
|
"not_contains",
|
|
@@ -3823,6 +3924,8 @@ var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
|
3823
3924
|
"cors_reflected"
|
|
3824
3925
|
]);
|
|
3825
3926
|
var OPERAND_SHAPES = Object.freeze([
|
|
3927
|
+
"positive-number",
|
|
3928
|
+
"boolean",
|
|
3826
3929
|
"non-empty-string",
|
|
3827
3930
|
"regex-literal",
|
|
3828
3931
|
"string-or-string-list",
|
|
@@ -3847,6 +3950,14 @@ var MATCH_OPERANDS = Object.freeze({
|
|
|
3847
3950
|
required: Object.freeze(["key", "match"]),
|
|
3848
3951
|
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
3849
3952
|
}),
|
|
3953
|
+
// Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
|
|
3954
|
+
// `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
|
|
3955
|
+
// exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
|
|
3956
|
+
// oversized, claim missing, claim not a string, or a different value — is a no-match.
|
|
3957
|
+
jwt_claim_equals: Object.freeze({
|
|
3958
|
+
required: Object.freeze(["claim", "value"]),
|
|
3959
|
+
shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
|
|
3960
|
+
}),
|
|
3850
3961
|
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
3851
3962
|
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
3852
3963
|
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
@@ -3872,6 +3983,10 @@ function operandShapeProblem(shape, value) {
|
|
|
3872
3983
|
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3873
3984
|
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
3874
3985
|
switch (shape) {
|
|
3986
|
+
case "positive-number":
|
|
3987
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
3988
|
+
case "boolean":
|
|
3989
|
+
return typeof value === "boolean" ? null : "must be true or false";
|
|
3875
3990
|
case "non-empty-string":
|
|
3876
3991
|
return isFilledString(value) ? null : "must be a non-empty string";
|
|
3877
3992
|
case "regex-literal":
|
|
@@ -3954,6 +4069,13 @@ var ACTIONS = Object.freeze([
|
|
|
3954
4069
|
"remove-header",
|
|
3955
4070
|
"harden-cookie"
|
|
3956
4071
|
]);
|
|
4072
|
+
var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
4073
|
+
max_bytes: "positive-number",
|
|
4074
|
+
bypass_limit: "boolean",
|
|
4075
|
+
ensure: "boolean",
|
|
4076
|
+
prefilter: "non-empty-string-list"
|
|
4077
|
+
});
|
|
4078
|
+
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
3957
4079
|
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
3958
4080
|
var RULE_PROPERTIES = Object.freeze([
|
|
3959
4081
|
"id",
|
|
@@ -4129,6 +4251,24 @@ function whenProblem(when) {
|
|
|
4129
4251
|
function isNonEmptyString(value) {
|
|
4130
4252
|
return typeof value === "string" && value.trim() !== "";
|
|
4131
4253
|
}
|
|
4254
|
+
function nullPropertyProblem(rule) {
|
|
4255
|
+
if (!rule || typeof rule !== "object") return null;
|
|
4256
|
+
for (const property of RULE_PROPERTIES) {
|
|
4257
|
+
if (property in rule && rule[property] === null) {
|
|
4258
|
+
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`;
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
return null;
|
|
4262
|
+
}
|
|
4263
|
+
function rulePropertyProblem(rule) {
|
|
4264
|
+
if (!rule || typeof rule !== "object") return null;
|
|
4265
|
+
for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
|
|
4266
|
+
if (rule[property] === void 0 || rule[property] === null) continue;
|
|
4267
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
4268
|
+
if (problem) return `"${property}" ${problem}`;
|
|
4269
|
+
}
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
4132
4272
|
function actionProblem(action, rule) {
|
|
4133
4273
|
if (action === void 0 || action === null) return null;
|
|
4134
4274
|
if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
|
|
@@ -4193,13 +4333,15 @@ function idOf(rule) {
|
|
|
4193
4333
|
}
|
|
4194
4334
|
function ruleProblem(rule) {
|
|
4195
4335
|
if (!rule || typeof rule !== "object") return "not an object";
|
|
4336
|
+
const nullReason = nullPropertyProblem(rule);
|
|
4337
|
+
if (nullReason) return nullReason;
|
|
4196
4338
|
if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
|
|
4197
4339
|
const actionReason = actionProblem(rule.action, rule);
|
|
4198
4340
|
if (actionReason) return actionReason;
|
|
4199
4341
|
const scopeReason = whenProblem(rule.when);
|
|
4200
4342
|
if (scopeReason) return scopeReason;
|
|
4201
|
-
const
|
|
4202
|
-
if (
|
|
4343
|
+
const propertyReason = rulePropertyProblem(rule);
|
|
4344
|
+
if (propertyReason) return propertyReason;
|
|
4203
4345
|
return conditionsProblem(rule.rule_v2);
|
|
4204
4346
|
}
|
|
4205
4347
|
function conditionsProblem(conditions, depth = 0) {
|
|
@@ -5491,6 +5633,8 @@ function extractRedactors(rule) {
|
|
|
5491
5633
|
}
|
|
5492
5634
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
5493
5635
|
out.push({ literal: String(m.value) });
|
|
5636
|
+
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
5637
|
+
out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") } });
|
|
5494
5638
|
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
5495
5639
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
5496
5640
|
for (const key of keys) {
|
|
@@ -5509,7 +5653,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
5509
5653
|
let out = body;
|
|
5510
5654
|
for (const r of redactors) {
|
|
5511
5655
|
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
5512
|
-
else if (r.
|
|
5656
|
+
else if (r.jwtClaim) {
|
|
5657
|
+
for (const token of jwtClaimSpans(out, r.jwtClaim.claim, r.jwtClaim.value)) {
|
|
5658
|
+
out = out.split(token).join(transform ? transform(token) : mask);
|
|
5659
|
+
}
|
|
5660
|
+
} else if (r.literal) {
|
|
5513
5661
|
const re = new RegExp(r.literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
5514
5662
|
out = out.replace(re, (m) => transform ? transform(m) : mask);
|
|
5515
5663
|
}
|