@node9/policy-engine 2.13.0 → 2.14.0

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/dist/index.mjs CHANGED
@@ -717,13 +717,32 @@ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
717
717
  })
718
718
  );
719
719
  var SENSITIVE_PATH_PATTERNS = [
720
- /[/\\]\.ssh[/\\]/i,
721
- /[/\\]\.aws[/\\]/i,
720
+ /[/\\]\.ssh([/\\]|$)/i,
721
+ /[/\\]\.aws([/\\]|$)/i,
722
722
  /[/\\]\.config[/\\]gcloud[/\\]/i,
723
723
  /[/\\]\.azure[/\\]/i,
724
724
  /[/\\]\.kube[/\\]config$/i,
725
- /[/\\]\.env($|\.)/i,
726
- // .env, .env.local, .env.production — not .envoy
725
+ // ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
726
+ // (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
727
+ // structural suffix chain rather than a hand-written list, `example|sample|
728
+ // template` exempt because a fixture stays a fixture whatever follows, and
729
+ // `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
730
+ // committed template, `.env.test.local` is gitignored and holds real values.
731
+ //
732
+ // It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
733
+ // blocked while `cat .env.example` allowed: the same file, opposite verdicts,
734
+ // decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
735
+ // which is the contract that now holds these copies in step, and stage 5 of
736
+ // doc/credential-jail-architecture.md, which replaces them with one generated
737
+ // source.
738
+ // ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
739
+ // fixture whatever follows it -- `.env.example.md` is documentation -- but
740
+ // `.env.example.local` is gitignored by the `.env*.local` convention and holds
741
+ // real values, exactly the reasoning that anchors `(?!\.test$)` rather than
742
+ // using `\b`. Without this branch the fixture exemption also bought a two-step
743
+ // bypass: `cp .env .env.sample`, then read the copy.
744
+ /[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
745
+ // .env + any suffix chain; fixtures exempt unless .local
727
746
  /[/\\]\.git-credentials$/i,
728
747
  /[/\\]\.npmrc$/i,
729
748
  /[/\\]\.docker[/\\]config\.json$/i,
@@ -1282,6 +1301,10 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1282
1301
  "od",
1283
1302
  "xxd",
1284
1303
  "hexdump",
1304
+ // Emits the file's bytes, re-encoded, so it is a read by the set's own test
1305
+ // ("does it emit file contents"). Absent until 2026-09-10, which is why
1306
+ // `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
1307
+ "base64",
1285
1308
  "strings",
1286
1309
  "sort",
1287
1310
  "uniq",
@@ -1289,8 +1312,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1289
1312
  "nl",
1290
1313
  "dd"
1291
1314
  ]);
1315
+ var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
1316
+ var RSYNC_SKIP = [
1317
+ "e",
1318
+ "--rsh",
1319
+ "--exclude",
1320
+ "--exclude-from",
1321
+ "--include",
1322
+ "--include-from",
1323
+ "--files-from",
1324
+ "f",
1325
+ "--filter"
1326
+ ];
1327
+ var COPY_VERBS = {
1328
+ cp: { source: "allButLast", targetDirFlag: true },
1329
+ mv: { source: "allButLast", targetDirFlag: true },
1330
+ install: { source: "allButLast", targetDirFlag: true },
1331
+ ln: { source: "first", targetDirFlag: true },
1332
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
1333
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
1334
+ tar: {
1335
+ source: "archive",
1336
+ archive: "tar",
1337
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
1338
+ },
1339
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1340
+ ar: { source: "archive", archive: "ar" },
1341
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
1342
+ gzip: { source: "all" },
1343
+ bzip2: { source: "all" },
1344
+ xz: { source: "all" },
1345
+ "docker cp": { source: "allButLast" },
1346
+ "kubectl cp": { source: "allButLast" },
1347
+ "gsutil cp": { source: "allButLast" },
1348
+ "gsutil rsync": { source: "allButLast" },
1349
+ "rclone copy": { source: "allButLast" },
1350
+ "rclone sync": { source: "allButLast" },
1351
+ "aws s3 cp": { source: "allButLast" },
1352
+ "aws s3 mv": { source: "allButLast" },
1353
+ "aws s3 sync": { source: "allButLast" },
1354
+ "gcloud storage cp": { source: "allButLast" },
1355
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
1356
+ };
1357
+ var TAR_MODE_WORD = /^[a-zA-Z]+$/;
1358
+ var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
1292
1359
  var FS_OP_PRESCREEN_RE = new RegExp(
1293
- `(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
1360
+ // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
1361
+ // reader right after `"` / `'`, and without these two characters the
1362
+ // prescreen rejected every string-wrapped read before the parser ran.
1363
+ // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
1364
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1294
1365
  );
1295
1366
  var HOME_CACHE_ALLOWLIST = [
1296
1367
  ".cache",
@@ -1311,12 +1382,12 @@ var SENSITIVE_PATH_RULES = [
1311
1382
  {
1312
1383
  rule: "shield:project-jail:block-read-ssh",
1313
1384
  reason: "Reading SSH private keys is blocked by project-jail shield",
1314
- match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
1385
+ match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
1315
1386
  },
1316
1387
  {
1317
1388
  rule: "shield:project-jail:block-read-aws",
1318
1389
  reason: "Reading AWS credentials is blocked by project-jail shield",
1319
- match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
1390
+ match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
1320
1391
  },
1321
1392
  {
1322
1393
  // Mirrors the JSON shield's `.env` pattern (project-jail.json's
@@ -1360,7 +1431,9 @@ var SENSITIVE_PATH_RULES = [
1360
1431
  // symmetry — silently exempts every `.env.test.*` file.
1361
1432
  //
1362
1433
  // shields.test.ts:983-995 is the canonical contract; keep both in step.
1363
- match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
1434
+ match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
1435
+ p
1436
+ )
1364
1437
  },
1365
1438
  {
1366
1439
  // verdict: 'review' (not 'block') is a deliberate design choice
@@ -1571,15 +1644,14 @@ function listOps() {
1571
1644
  return _listOps;
1572
1645
  }
1573
1646
  var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1647
+ var FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
1574
1648
  var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1575
1649
  function unwrapCommandHead(words) {
1576
1650
  let i = 0;
1577
1651
  while (i < words.length) {
1578
1652
  const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1579
1653
  if (head === "find") {
1580
- const x = words.findIndex(
1581
- (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1582
- );
1654
+ const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
1583
1655
  if (x < 0) break;
1584
1656
  i = x + 1;
1585
1657
  continue;
@@ -1601,7 +1673,11 @@ function unwrapCommandHead(words) {
1601
1673
  if (t.startsWith("-")) {
1602
1674
  i++;
1603
1675
  const nxt = words[i];
1604
- if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1676
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()) && // A reader is a command, never a flag's operand: `env - cat X`,
1677
+ // `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
1678
+ // swallowed and the jail needed a looser fallback whose cost was a
1679
+ // false positive on `sudo echo cat X`.
1680
+ !FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
1605
1681
  i++;
1606
1682
  continue;
1607
1683
  }
@@ -1720,38 +1796,43 @@ function isProtectedHomePath(rawPath) {
1720
1796
  }
1721
1797
  return true;
1722
1798
  }
1723
- function extractLiteralArgs(callExpr) {
1724
- const args = callExpr.Args || [];
1725
- if (args.length === 0) return { name: "", flags: [], paths: [] };
1726
- const litFromWord = (w) => {
1727
- const parts = w?.Parts || [];
1728
- let s = "";
1729
- for (const p of parts) {
1730
- const t = syntax.NodeType(p);
1731
- if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1732
- else if (t === "SglQuoted") s += p.Value ?? "";
1733
- else if (t === "DblQuoted") {
1734
- const inner = p.Parts || [];
1735
- if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1736
- s += inner.map((ip) => ip.Value ?? "").join("");
1737
- } else {
1738
- return null;
1739
- }
1799
+ function positionedArgs(words, from = 1, to = words.length) {
1800
+ const out = [];
1801
+ let afterFlag = null;
1802
+ for (let i = from; i < to; i++) {
1803
+ const v = words[i];
1804
+ if (v === null) {
1805
+ afterFlag = null;
1806
+ continue;
1740
1807
  }
1741
- return s;
1742
- };
1743
- const name = (litFromWord(args[0]) || "").toLowerCase();
1744
- const flags = [];
1745
- const paths = [];
1746
- for (let i = 1; i < args.length; i++) {
1747
- const v = litFromWord(args[i]);
1748
- if (v === null) continue;
1749
- if (v.startsWith("-")) flags.push(v);
1750
- else paths.push(v);
1808
+ if (v.startsWith("-")) {
1809
+ afterFlag = v;
1810
+ continue;
1811
+ }
1812
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1813
+ afterFlag = null;
1751
1814
  }
1752
- return { name, flags, paths };
1815
+ return out;
1753
1816
  }
1754
- var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
1817
+ function extractLiteralArgs(callExpr) {
1818
+ const rawArgs = callExpr.Args || [];
1819
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1820
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1821
+ const name = baseWord(words[0]);
1822
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1823
+ const args = positionedArgs(words);
1824
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1825
+ }
1826
+ var NET_BINARIES = /* @__PURE__ */ new Set([
1827
+ "curl",
1828
+ "wget",
1829
+ "scp",
1830
+ "ssh",
1831
+ "nc",
1832
+ "ncat",
1833
+ "netcat",
1834
+ "rsync"
1835
+ ]);
1755
1836
  var VALUE_FLAGS = {
1756
1837
  curl: /* @__PURE__ */ new Set([
1757
1838
  "-d",
@@ -2036,6 +2117,9 @@ function deriveRedirOp(sample) {
2036
2117
  }
2037
2118
  }
2038
2119
  var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
2120
+ var REDIR_FILE_IN_OPS = new Set(
2121
+ [deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
2122
+ );
2039
2123
  var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
2040
2124
  deriveRedirOp("cat <<X\nX"),
2041
2125
  deriveRedirOp("cat <<-X\nX")
@@ -2100,16 +2184,21 @@ function isRmCreatedInCommandCleanup(command) {
2100
2184
  }
2101
2185
  return sawRm && ok;
2102
2186
  }
2103
- function analyzeFsOperationImpl(command) {
2187
+ function analyzeFsOperationImpl(command, depth = 0) {
2104
2188
  const f = parseShared(command);
2105
2189
  if (f === PARSE_FAIL) return null;
2106
2190
  let result = null;
2107
2191
  try {
2108
2192
  syntax.Walk(f, (node) => {
2109
- if (!node || result) return false;
2193
+ if (!node || result?.verdict === "block") return false;
2110
2194
  const n = node;
2111
- if (syntax.NodeType(n) !== "CallExpr") return true;
2112
- const { name, flags, paths } = extractLiteralArgs(n);
2195
+ const nodeType = syntax.NodeType(n);
2196
+ if (nodeType === "Stmt") {
2197
+ result = stricter(result, jailedRedirectRead(n));
2198
+ return result?.verdict !== "block";
2199
+ }
2200
+ if (nodeType !== "CallExpr") return true;
2201
+ const { name, flags, paths, words } = extractLiteralArgs(n);
2113
2202
  if (!name) return true;
2114
2203
  if (name === "rm") {
2115
2204
  const flagStr = flags.join("").toLowerCase();
@@ -2138,21 +2227,27 @@ function analyzeFsOperationImpl(command) {
2138
2227
  }
2139
2228
  }
2140
2229
  }
2141
- if (FS_READ_TOOLS.has(name)) {
2142
- for (const p of paths) {
2143
- for (const sp of SENSITIVE_PATH_RULES) {
2144
- if (sp.match(p)) {
2145
- result = {
2146
- ruleName: sp.rule,
2147
- verdict: sp.verdict ?? "block",
2148
- reason: sp.reason,
2149
- path: p
2150
- };
2151
- return false;
2152
- }
2230
+ if (depth < 1) {
2231
+ const payload = literalShellPayload(words, name);
2232
+ if (payload !== null) {
2233
+ const inner = analyzeFsOperationImpl(payload, depth + 1);
2234
+ if (inner) {
2235
+ result = inner;
2236
+ return false;
2153
2237
  }
2238
+ return true;
2239
+ }
2240
+ }
2241
+ const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
2242
+ if (readPaths) {
2243
+ for (const p of readPaths) {
2244
+ result = stricter(result, matchSensitivePath2(p));
2245
+ if (result?.verdict === "block") return false;
2154
2246
  }
2155
2247
  }
2248
+ for (const p of copySourcePaths(words)) {
2249
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
2250
+ }
2156
2251
  return true;
2157
2252
  });
2158
2253
  return result;
@@ -2160,6 +2255,209 @@ function analyzeFsOperationImpl(command) {
2160
2255
  return null;
2161
2256
  }
2162
2257
  }
2258
+ function stricter(a, b) {
2259
+ if (!a) return b;
2260
+ if (!b) return a;
2261
+ return b.verdict === "block" && a.verdict !== "block" ? b : a;
2262
+ }
2263
+ function flagInfo(w) {
2264
+ if (w.startsWith("--")) {
2265
+ const eq = w.indexOf("=");
2266
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
2267
+ }
2268
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
2269
+ if (!m) return { letter: null, long: null, attached: null };
2270
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
2271
+ }
2272
+ function flagIs(w, names) {
2273
+ if (w === null) return false;
2274
+ const f = flagInfo(w);
2275
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2276
+ }
2277
+ function operandOf(a, names) {
2278
+ if (!names || a.afterFlag === null) return false;
2279
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
2280
+ }
2281
+ function resolveCopyShape(words, h) {
2282
+ const verb = baseWord(words[h]);
2283
+ if (!verb) return null;
2284
+ const direct = COPY_VERBS[verb];
2285
+ if (direct) return { shape: direct, last: h };
2286
+ const slots = positionedArgs(words, h + 1);
2287
+ for (let i = 0; i < slots.length; i++) {
2288
+ for (let n = 3; n >= 1; n--) {
2289
+ const part = slots.slice(i, i + n);
2290
+ if (part.length < n) continue;
2291
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
2292
+ const shape = COPY_VERBS[key];
2293
+ if (shape) return { shape, last: part[n - 1].argv };
2294
+ }
2295
+ if (slots[i].afterFlag === null) return null;
2296
+ }
2297
+ return null;
2298
+ }
2299
+ var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
2300
+ function findStartPoints(words, h) {
2301
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2302
+ if (k < 0) return { k, starts: [] };
2303
+ const firstPredicate = words.findIndex(
2304
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
2305
+ );
2306
+ const end = firstPredicate > h ? firstPredicate : k;
2307
+ return { k, starts: positionalAfter(words, h + 1, end) };
2308
+ }
2309
+ function copySourcePaths(words) {
2310
+ const h = unwrapCommandHead(words);
2311
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
2312
+ if (fi >= 0) {
2313
+ const { k, starts } = findStartPoints(words, fi);
2314
+ if (k < 0) return [];
2315
+ const action = unwrapCommandHead(words.slice(k + 1));
2316
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
2317
+ }
2318
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
2319
+ const r = resolveCopyShape(words, h);
2320
+ if (!r) return [];
2321
+ const { shape, last } = r;
2322
+ const args = positionedArgs(words, last + 1);
2323
+ const tail = words.slice(last + 1);
2324
+ const skipped = (a) => operandOf(a, shape.skipFlags);
2325
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
2326
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
2327
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2328
+ const dynamicDest = lastOperand === null;
2329
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2330
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2331
+ return [];
2332
+ let src;
2333
+ switch (shape.source) {
2334
+ case "all":
2335
+ src = args;
2336
+ break;
2337
+ case "first":
2338
+ src = targetDir ? args : args.slice(0, 1);
2339
+ break;
2340
+ case "flagOperand": {
2341
+ const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
2342
+ return [
2343
+ ...args.filter(
2344
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2345
+ ).map((a) => a.value),
2346
+ ...inline
2347
+ ];
2348
+ }
2349
+ case "archive":
2350
+ src = archiveInputs(shape.archive, args, tail);
2351
+ break;
2352
+ case "allButLast":
2353
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
2354
+ break;
2355
+ }
2356
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
2357
+ }
2358
+ function archiveInputs(kind, args, tail) {
2359
+ const first = args[0];
2360
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
2361
+ if (kind === "tar") {
2362
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
2363
+ const mode = (bareKey ? first.value : "") + flagsText;
2364
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
2365
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2366
+ if (extracting && !writing) return [];
2367
+ void mode;
2368
+ let i = 0;
2369
+ if (bareKey) {
2370
+ i = 1;
2371
+ const next = args[1];
2372
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
2373
+ }
2374
+ return args.slice(i);
2375
+ }
2376
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2377
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
2378
+ return args.slice(2);
2379
+ }
2380
+ function sampleCopyCommand(verb, src) {
2381
+ const shape = COPY_VERBS[verb];
2382
+ if (!shape) throw new Error(`not a copy verb: ${verb}`);
2383
+ const remote = /^(scp|rsync|aws |gsutil|gcloud|rclone|docker|kubectl)/.test(verb);
2384
+ switch (shape.source) {
2385
+ case "first":
2386
+ return `${verb} -s ${src} /tmp/n9-link`;
2387
+ case "all":
2388
+ return `${verb} -c ${src} > /tmp/n9-out`;
2389
+ case "flagOperand":
2390
+ return `${verb} -f ${src} -c n9`;
2391
+ case "archive":
2392
+ return shape.archive === "tar" ? `tar czf /tmp/n9-out.tgz ${src}` : shape.archive === "zip" ? `zip -r /tmp/n9-out.zip ${src}` : shape.archive === "ar" ? `ar rc /tmp/n9-out.a ${src}` : `7z a /tmp/n9-out.7z ${src}`;
2393
+ case "allButLast":
2394
+ return `${verb} ${src} ${remote ? verb.startsWith("scp") || verb === "rsync" ? "user@host.invalid:/tmp/" : verb.startsWith("docker") || verb.startsWith("kubectl") ? "ctr:/tmp/" : "remote:/n9/" : "/tmp/n9-copy"}`;
2395
+ }
2396
+ }
2397
+ var COPY_RULE_OF = {
2398
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
2399
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
2400
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
2401
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
2402
+ };
2403
+ function copyVerdictOf(hit) {
2404
+ if (!hit) return null;
2405
+ const ruleName = COPY_RULE_OF[hit.ruleName];
2406
+ if (!ruleName) return null;
2407
+ return {
2408
+ ruleName,
2409
+ verdict: "review",
2410
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
2411
+ path: hit.path
2412
+ };
2413
+ }
2414
+ function matchSensitivePath2(p) {
2415
+ for (const sp of SENSITIVE_PATH_RULES) {
2416
+ if (sp.match(p))
2417
+ return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
2418
+ }
2419
+ return null;
2420
+ }
2421
+ function baseWord(w) {
2422
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
2423
+ }
2424
+ var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2425
+ var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
2426
+ function wrappedReadPaths(words, name) {
2427
+ if (name === "find") {
2428
+ const { k, starts } = findStartPoints(words, 0);
2429
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
2430
+ }
2431
+ if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2432
+ const h = unwrapCommandHead(words);
2433
+ return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
2434
+ }
2435
+ function literalShellPayload(words, name) {
2436
+ const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
2437
+ const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
2438
+ if (head === "eval") {
2439
+ const rest = words.slice(h + 1);
2440
+ if (rest.length === 0 || rest.some((w) => w === null)) return null;
2441
+ return rest.join(" ");
2442
+ }
2443
+ if (SHELL_INTERPRETERS.has(head)) {
2444
+ const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
2445
+ if (c < 0) return null;
2446
+ return words[c + 1] ?? null;
2447
+ }
2448
+ return null;
2449
+ }
2450
+ function jailedRedirectRead(stmt) {
2451
+ const redirs = stmt.Redirs || [];
2452
+ for (const r of redirs) {
2453
+ if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
2454
+ const p = resolveWordLiteral(r.Word);
2455
+ if (p === null || p === "") continue;
2456
+ const hit = matchSensitivePath2(p);
2457
+ if (hit) return hit;
2458
+ }
2459
+ return null;
2460
+ }
2163
2461
  function analyzeShellCommand(command) {
2164
2462
  const actions = [];
2165
2463
  const paths = [];
@@ -2296,21 +2594,7 @@ function evaluateEgress(dests, policy) {
2296
2594
  }
2297
2595
 
2298
2596
  // src/policy/pipe-chain.ts
2299
- var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
2300
- "cat",
2301
- "head",
2302
- "tail",
2303
- "grep",
2304
- "awk",
2305
- "sed",
2306
- "cut",
2307
- "sort",
2308
- "tee",
2309
- "less",
2310
- "more",
2311
- "strings",
2312
- "xxd"
2313
- ]);
2597
+ var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
2314
2598
  var SINK_COMMANDS = /* @__PURE__ */ new Set([
2315
2599
  "curl",
2316
2600
  "wget",
@@ -2341,16 +2625,25 @@ var OBFUSCATORS = /* @__PURE__ */ new Set([
2341
2625
  "node"
2342
2626
  ]);
2343
2627
  var SENSITIVE_PATTERNS = [
2344
- /(?:^|\/)\.env(?:\.|$)/i,
2345
- // .env, .env.local, .env.production
2628
+ // Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
2629
+ /(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
2630
+ // .env chain; fixtures exempt unless .local
2346
2631
  /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
2347
2632
  // SSH private keys
2348
2633
  /\.pem$|\.key$|\.p12$|\.pfx$/i,
2349
2634
  // certificate files
2350
- /(?:^|\/)\.ssh\//i,
2351
- // ~/.ssh/ directory
2352
- /(?:^|\/)\.aws\/credentials/i,
2353
- // AWS credentials
2635
+ // The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
2636
+ // the directory counts wherever it appears, while the directory ITSELF counts
2637
+ // only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
2638
+ // `config/.ssh` is more likely a search pattern than a read. These are
2639
+ // extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
2640
+ // contract as the shell tier, so the same boundary is the right one.
2641
+ // Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
2642
+ // identical pipeline naming a file inside that directory.
2643
+ /(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
2644
+ // ~/.ssh/ and ~/.ssh
2645
+ /(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
2646
+ // AWS creds + dir
2354
2647
  /(?:^|\/)\.netrc$/i,
2355
2648
  // netrc (stores HTTP credentials)
2356
2649
  /(?:^|\/)(passwd|shadow|sudoers)$/i,
@@ -2384,8 +2677,8 @@ function splitOnPipe(cmd) {
2384
2677
  if (current.trim()) segments2.push(current.trim());
2385
2678
  return segments2.filter(Boolean);
2386
2679
  }
2387
- function positionalTokens(segment) {
2388
- return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
2680
+ function positionalTokens(tokens) {
2681
+ return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
2389
2682
  }
2390
2683
  function analyzePipeChain(command) {
2391
2684
  const segments2 = splitOnPipe(command);
@@ -2408,8 +2701,10 @@ function analyzePipeChain(command) {
2408
2701
  for (const segment of segments2) {
2409
2702
  const tokens = segment.split(/\s+/).filter(Boolean);
2410
2703
  if (tokens.length === 0) continue;
2411
- const binary = tokens[0].toLowerCase();
2412
- const args = positionalTokens(segment);
2704
+ const h = unwrapCommandHead(tokens);
2705
+ const head = h < tokens.length ? h : 0;
2706
+ const binary = tokens[head].toLowerCase();
2707
+ const args = positionalTokens(tokens.slice(head));
2413
2708
  if (SOURCE_COMMANDS.has(binary)) {
2414
2709
  sourceFiles.push(...args);
2415
2710
  if (args.some(isSensitivePath)) hasSensitiveSource = true;
@@ -3019,6 +3314,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3019
3314
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3020
3315
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3021
3316
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
3317
+ let pendingAstReview;
3022
3318
  if (bashCommand !== null) {
3023
3319
  const pipeVerdict = pipeChainVerdict(
3024
3320
  bashCommand,
@@ -3030,7 +3326,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3030
3326
  if (fsVerdict) {
3031
3327
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
3032
3328
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
3033
- return {
3329
+ const astVerdict = {
3034
3330
  decision: fsVerdict.verdict,
3035
3331
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
3036
3332
  reason: fsVerdict.reason,
@@ -3038,6 +3334,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3038
3334
  ruleName: fsVerdict.ruleName,
3039
3335
  ruleDescription: fsVerdict.reason
3040
3336
  };
3337
+ if (fsVerdict.verdict === "block") return astVerdict;
3338
+ pendingAstReview = astVerdict;
3041
3339
  }
3042
3340
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
3043
3341
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -3080,7 +3378,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3080
3378
  const matchedRule = resolvePinned(matches);
3081
3379
  if (matchedRule) {
3082
3380
  if (matchedRule.verdict === "allow")
3083
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3381
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3084
3382
  return {
3085
3383
  decision: matchedRule.verdict,
3086
3384
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -3107,6 +3405,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3107
3405
  allTokens = analyzed.allTokens;
3108
3406
  pathTokens = analyzed.paths;
3109
3407
  const candidates = [];
3408
+ if (pendingAstReview) candidates.push(pendingAstReview);
3110
3409
  const evalVerdict = detectDangerousShellExec(shellCommand);
3111
3410
  if (evalVerdict === "block") {
3112
3411
  return {
@@ -3852,7 +4151,7 @@ var project_jail_default = {
3852
4151
  {
3853
4152
  field: "command",
3854
4153
  op: "matches",
3855
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
4154
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
3856
4155
  flags: "i"
3857
4156
  }
3858
4157
  ],
@@ -3866,7 +4165,7 @@ var project_jail_default = {
3866
4165
  {
3867
4166
  field: "command",
3868
4167
  op: "matches",
3869
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
4168
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
3870
4169
  flags: "i"
3871
4170
  }
3872
4171
  ],
@@ -3880,7 +4179,7 @@ var project_jail_default = {
3880
4179
  {
3881
4180
  field: "command",
3882
4181
  op: "matches",
3883
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
4182
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
3884
4183
  flags: "i"
3885
4184
  }
3886
4185
  ],
@@ -3894,7 +4193,7 @@ var project_jail_default = {
3894
4193
  {
3895
4194
  field: "command",
3896
4195
  op: "matches",
3897
- value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
4196
+ value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
3898
4197
  flags: "i"
3899
4198
  }
3900
4199
  ],
@@ -3908,7 +4207,7 @@ var project_jail_default = {
3908
4207
  {
3909
4208
  field: "file_path",
3910
4209
  op: "matches",
3911
- value: "(^|[\\/\\\\])\\.ssh[\\/\\\\]",
4210
+ value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
3912
4211
  flags: "i"
3913
4212
  }
3914
4213
  ],
@@ -3922,7 +4221,7 @@ var project_jail_default = {
3922
4221
  {
3923
4222
  field: "file_path",
3924
4223
  op: "matches",
3925
- value: "(^|[\\/\\\\])\\.aws[\\/\\\\]",
4224
+ value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
3926
4225
  flags: "i"
3927
4226
  }
3928
4227
  ],
@@ -3936,7 +4235,7 @@ var project_jail_default = {
3936
4235
  {
3937
4236
  field: "file_path",
3938
4237
  op: "matches",
3939
- value: "(^|[\\/\\\\])\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?$",
4238
+ value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
3940
4239
  flags: "i"
3941
4240
  }
3942
4241
  ],
@@ -4213,6 +4512,12 @@ function classifyRuleSeverity(name, verdict) {
4213
4512
  "read-ssh",
4214
4513
  "read-gcp",
4215
4514
  "read-cred",
4515
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
4516
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
4517
+ // (below), so copy-env joins the high list, not this one (/code-review).
4518
+ "copy-ssh",
4519
+ "copy-aws",
4520
+ "copy-cred",
4216
4521
  "delete-repo",
4217
4522
  "helm-uninstall",
4218
4523
  "drop-table",
@@ -4225,6 +4530,7 @@ function classifyRuleSeverity(name, verdict) {
4225
4530
  ];
4226
4531
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
4227
4532
  const highPatterns = [
4533
+ "copy-env",
4228
4534
  "force-push",
4229
4535
  "force_push",
4230
4536
  "git-destructive",
@@ -4242,6 +4548,11 @@ function narrativeRuleLabel(name) {
4242
4548
  const map = {
4243
4549
  "read-aws": "AWS credentials read",
4244
4550
  "read-ssh": "SSH private key read",
4551
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
4552
+ "copy-ssh": "SSH private key copied out",
4553
+ "copy-aws": "AWS credentials copied out",
4554
+ "copy-env": ".env file copied out",
4555
+ "copy-cred": "credential file copied out",
4245
4556
  "read-gcp": "GCP credentials read",
4246
4557
  "read-cred": "credential file read",
4247
4558
  "delete-repo": "GitHub repository deletion",
@@ -4379,7 +4690,7 @@ function summarizeBlast(result, opts = {}) {
4379
4690
  // src/scan/destructive-regex.ts
4380
4691
  var DESTRUCTIVE_OP_RE = /\brm\s+-[rRf]+\b|\bDROP\s+(TABLE|DATABASE|COLLECTION|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bgit\s+push\s+(--force|-f)\b|\bFLUSHALL\b|\bFLUSHDB\b|\bkubectl\s+delete\b|\bhelm\s+uninstall\b/i;
4381
4692
  var PRIVILEGE_ESCALATION_RE = /\bchmod\s+(0?777|\+x)\b|\bchown\s+root\b/i;
4382
- var SENSITIVE_PATH_RE = /\.aws\/(credentials|config)\b|\.ssh\/(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b|\.env(\.|$|\b)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
4693
+ var SENSITIVE_PATH_RE = /[\\/]\.aws(?:[\\/]|$)|^\.aws[\\/]|[\\/]\.ssh(?:[\\/]|$)|^\.ssh[\\/]|(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)|\.config\/gcloud\/credentials\.db\b|\.docker\/config\.json\b|\.netrc\b|\.npmrc\b|\.node9\/credentials\.json\b/i;
4383
4694
  var FILE_TOOLS = /* @__PURE__ */ new Set([
4384
4695
  "read",
4385
4696
  "read_file",
@@ -4639,8 +4950,8 @@ function matchCanaryArgs(args, values) {
4639
4950
 
4640
4951
  // src/scan/canonical.ts
4641
4952
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
4642
- var CANONICAL_EXTRACTOR_VERSION = "canonical-v10";
4643
- var CANONICAL_EXTRACTOR_HASH = "c4edee9dc99eb69a";
4953
+ var CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
4954
+ var CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
4644
4955
  var DEDUPE_PREVIEW_LEN = 120;
4645
4956
  function extractCanonicalFindings(call, ctx) {
4646
4957
  const out = [];
@@ -5015,6 +5326,8 @@ export {
5015
5326
  CANARY_MIN_LENGTH,
5016
5327
  CANONICAL_EXTRACTOR_HASH,
5017
5328
  CANONICAL_EXTRACTOR_VERSION,
5329
+ COMMAND_WRAPPERS,
5330
+ COPY_VERBS,
5018
5331
  COST_PER_LOOP_ITER_USD,
5019
5332
  DEFAULT_EGRESS_ALLOWLIST,
5020
5333
  DESTINATION_ARGS,
@@ -5028,6 +5341,7 @@ export {
5028
5341
  LONG_OUTPUT_THRESHOLD_BYTES,
5029
5342
  LOOP_MAX_RECORDS,
5030
5343
  LOOP_THRESHOLD_FOR_WASTE,
5344
+ NET_BINARIES,
5031
5345
  PRIVILEGE_ESCALATION_RE,
5032
5346
  REALTIME_PII_PATTERNS,
5033
5347
  SCAN_SIGNAL_WEIGHTS,
@@ -5083,9 +5397,11 @@ export {
5083
5397
  normalizeIpLiteral,
5084
5398
  parseAllSshHostsFromCommand,
5085
5399
  parseDestHost,
5400
+ positionedArgs,
5086
5401
  previewArgs,
5087
5402
  redactText,
5088
5403
  resolvePinned,
5404
+ sampleCopyCommand,
5089
5405
  scanArgs,
5090
5406
  scanInjection,
5091
5407
  scanText,
@@ -5098,6 +5414,7 @@ export {
5098
5414
  toScanFinding,
5099
5415
  toolMatchesRule,
5100
5416
  truncateBlastPath,
5417
+ unwrapCommandHead,
5101
5418
  validateOverrides,
5102
5419
  validateRegex,
5103
5420
  validateShieldDefinition