@node9/proxy 2.14.0 → 2.14.1

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/cli.js CHANGED
@@ -894,6 +894,19 @@ function parseShared(command) {
894
894
  astCache.set(command, parsed);
895
895
  return parsed;
896
896
  }
897
+ function byteOffsetToCharIndex(command) {
898
+ if (!/[^\u0000-\u007F]/.test(command)) return null;
899
+ const map = /* @__PURE__ */ new Map();
900
+ let byte = 0;
901
+ for (let i = 0; i < command.length; ) {
902
+ map.set(byte, i);
903
+ const cp = command.codePointAt(i);
904
+ byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
905
+ i += cp > 65535 ? 2 : 1;
906
+ }
907
+ map.set(byte, command.length);
908
+ return (b) => map.get(b) ?? -1;
909
+ }
897
910
  function cachedNormalize(command, compute) {
898
911
  const hit = normalizeCache.get(command);
899
912
  if (hit !== void 0) {
@@ -923,6 +936,8 @@ function normalizeCommandForPolicyImpl(command) {
923
936
  const f = parseShared(command);
924
937
  if (f === PARSE_FAIL) return { posix: command, separator: command };
925
938
  try {
939
+ const toCharIndex = byteOffsetToCharIndex(command);
940
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
926
941
  const strips = [];
927
942
  const rewrites = [];
928
943
  const quoteOnlyRewrites = [];
@@ -943,8 +958,9 @@ function normalizeCommandForPolicyImpl(command) {
943
958
  const quotedNode = nextParts[0];
944
959
  const nt = syntax.NodeType(quotedNode);
945
960
  const markStrip = () => {
946
- const s = next.Pos().Offset();
947
- const e = next.End().Offset();
961
+ const s = at(next.Pos().Offset());
962
+ const e = at(next.End().Offset());
963
+ if (s < 0 || e < 0) return;
948
964
  strips.push([s, e]);
949
965
  msgSpans.add(`${s}:${e}`);
950
966
  };
@@ -961,8 +977,9 @@ function normalizeCommandForPolicyImpl(command) {
961
977
  }
962
978
  }
963
979
  for (const arg of args) {
964
- const s = arg.Pos().Offset();
965
- const e = arg.End().Offset();
980
+ const s = at(arg.Pos().Offset());
981
+ const e = at(arg.End().Offset());
982
+ if (s < 0 || e < 0) continue;
966
983
  if (msgSpans.has(`${s}:${e}`)) continue;
967
984
  const resolved = resolveWordLiteral(arg);
968
985
  if (resolved === null) continue;
@@ -1588,7 +1605,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1588
1605
  return result?.verdict !== "block";
1589
1606
  }
1590
1607
  if (nodeType !== "CallExpr") return true;
1591
- const { name, flags, paths, words } = extractLiteralArgs(n);
1608
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
1592
1609
  if (!name) return true;
1593
1610
  if (name === "rm") {
1594
1611
  const flagStr = flags.join("").toLowerCase();
@@ -1628,7 +1645,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1628
1645
  return true;
1629
1646
  }
1630
1647
  }
1631
- const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
1648
+ const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
1632
1649
  if (readPaths) {
1633
1650
  for (const p of readPaths) {
1634
1651
  result = stricter(result, matchSensitivePath2(p));
@@ -1664,9 +1681,22 @@ function flagIs(w, names) {
1664
1681
  const f = flagInfo(w);
1665
1682
  return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1666
1683
  }
1667
- function operandOf(a, names) {
1684
+ function operandOf(a, names, valueLetters) {
1668
1685
  if (!names || a.afterFlag === null) return false;
1669
- return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1686
+ const w = a.afterFlag;
1687
+ if (!w.startsWith("--") && valueLetters) {
1688
+ const letters = w.slice(1);
1689
+ for (let i = 0; i < letters.length; i++) {
1690
+ if (!valueLetters.includes(letters[i])) continue;
1691
+ return i === letters.length - 1 && names.includes(letters[i]);
1692
+ }
1693
+ return false;
1694
+ }
1695
+ if (w.startsWith("--") && !w.includes("=")) {
1696
+ const longs = names.filter((n) => n.startsWith("--"));
1697
+ if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
1698
+ }
1699
+ return flagIs(w, names) && flagInfo(w).attached === null;
1670
1700
  }
1671
1701
  function resolveCopyShape(words, h) {
1672
1702
  const verb = baseWord(words[h]);
@@ -1690,7 +1720,7 @@ function findStartPoints(words, h) {
1690
1720
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1691
1721
  if (k < 0) return { k, starts: [] };
1692
1722
  const firstPredicate = words.findIndex(
1693
- (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1723
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
1694
1724
  );
1695
1725
  const end = firstPredicate > h ? firstPredicate : k;
1696
1726
  return { k, starts: positionalAfter(words, h + 1, end) };
@@ -1710,14 +1740,35 @@ function copySourcePaths(words) {
1710
1740
  const { shape, last } = r;
1711
1741
  const args = positionedArgs(words, last + 1);
1712
1742
  const tail = words.slice(last + 1);
1713
- const skipped = (a) => operandOf(a, shape.skipFlags);
1714
- const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1715
- const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1743
+ const copyOptionsEnd = tail.findIndex((w) => w === "--");
1744
+ const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
1745
+ const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
1746
+ const firstValueLetter = (w) => {
1747
+ const letters = w.slice(1);
1748
+ for (let i = 0; i < letters.length; i++) {
1749
+ if ((shape.valueLetters ?? []).includes(letters[i]))
1750
+ return { letter: letters[i], last: i === letters.length - 1 };
1751
+ }
1752
+ return null;
1753
+ };
1754
+ const isTargetDirFlag = (w) => {
1755
+ if (w.startsWith("--")) {
1756
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
1757
+ return name.length >= 3 && "--target-directory".startsWith(name);
1758
+ }
1759
+ return firstValueLetter(w)?.letter === "t";
1760
+ };
1761
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
1762
+ const targetTakesNextWord = (w) => {
1763
+ if (w.startsWith("--"))
1764
+ return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
1765
+ const f = firstValueLetter(w);
1766
+ return f !== null && f.letter === "t" && f.last;
1767
+ };
1768
+ const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
1716
1769
  const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1717
1770
  const dynamicDest = lastOperand === null;
1718
1771
  const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1719
- if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1720
- return [];
1721
1772
  let src;
1722
1773
  switch (shape.source) {
1723
1774
  case "all":
@@ -1727,11 +1778,17 @@ function copySourcePaths(words) {
1727
1778
  src = targetDir ? args : args.slice(0, 1);
1728
1779
  break;
1729
1780
  case "flagOperand": {
1730
- const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
1781
+ const namesSource = (f) => {
1782
+ const names = shape.sourceFlags ?? [];
1783
+ if (f.long !== null)
1784
+ return names.some(
1785
+ (n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
1786
+ );
1787
+ return f.letter !== null && names.includes(f.letter);
1788
+ };
1789
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
1731
1790
  return [
1732
- ...args.filter(
1733
- (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1734
- ).map((a) => a.value),
1791
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
1735
1792
  ...inline
1736
1793
  ];
1737
1794
  }
@@ -1742,7 +1799,14 @@ function copySourcePaths(words) {
1742
1799
  src = targetDir || dynamicDest ? args : args.slice(0, -1);
1743
1800
  break;
1744
1801
  }
1745
- return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1802
+ const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1803
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
1804
+ const jailedSources = sources.filter((p) => matchSensitivePath2(p));
1805
+ const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
1806
+ if (jailedSources.length === 0) return [];
1807
+ if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
1808
+ }
1809
+ return sources;
1746
1810
  }
1747
1811
  function archiveInputs(kind, args, tail) {
1748
1812
  const first = args[0];
@@ -1754,13 +1818,17 @@ function archiveInputs(kind, args, tail) {
1754
1818
  const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1755
1819
  if (extracting && !writing) return [];
1756
1820
  void mode;
1757
- let i = 0;
1758
- if (bareKey) {
1759
- i = 1;
1760
- const next = args[1];
1761
- if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1762
- }
1763
- return args.slice(i);
1821
+ if (!bareKey) return args;
1822
+ let i = 1;
1823
+ const fromDirs = [];
1824
+ for (const ch of first.value) {
1825
+ if (!TAR_VALUE_LETTERS.includes(ch)) continue;
1826
+ const operand = args[i];
1827
+ if (!operand || operand.afterFlag !== null) break;
1828
+ i += 1;
1829
+ if (ch === "C") fromDirs.push(operand);
1830
+ }
1831
+ return [...fromDirs, ...args.slice(i)];
1764
1832
  }
1765
1833
  if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1766
1834
  if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
@@ -1787,6 +1855,138 @@ function matchSensitivePath2(p) {
1787
1855
  function baseWord(w) {
1788
1856
  return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1789
1857
  }
1858
+ function namesFlag(name, candidates2, known) {
1859
+ if (candidates2.has(name)) return true;
1860
+ if (!name.startsWith("--") || name.length < 3) return false;
1861
+ if (known?.has(name)) return false;
1862
+ for (const c of candidates2) if (c.startsWith(name)) return true;
1863
+ return false;
1864
+ }
1865
+ function knownLongFlags(verb) {
1866
+ const out = /* @__PURE__ */ new Set();
1867
+ const shape = PATTERN_VERBS[verb];
1868
+ if (shape) {
1869
+ for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
1870
+ for (const f of set) if (f.startsWith("--")) out.add(f);
1871
+ }
1872
+ for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
1873
+ return out;
1874
+ }
1875
+ function flagNamesOf(token, shape) {
1876
+ if (token.startsWith("--")) {
1877
+ const eq = token.indexOf("=");
1878
+ return [eq > 0 ? token.slice(0, eq) : token];
1879
+ }
1880
+ const out = [];
1881
+ for (const c of token.slice(1)) {
1882
+ const name = `-${c}`;
1883
+ out.push(name);
1884
+ if (shape?.takesValue.has(name)) break;
1885
+ }
1886
+ return out;
1887
+ }
1888
+ function flagEffect(token, shape, known) {
1889
+ if (token === "--") return NONE;
1890
+ if (/^-+$/.test(token)) return UNKNOWN;
1891
+ if (/^-\d+$/.test(token)) return NONE;
1892
+ if (token.startsWith("--")) {
1893
+ if (token.includes("=")) return NONE;
1894
+ const takes = namesFlag(token, shape.takesValue, known);
1895
+ const none = namesFlag(token, shape.noValue, known);
1896
+ if (takes && none) return UNKNOWN;
1897
+ if (takes) return { kind: "takes", flag: token };
1898
+ if (none) return NONE;
1899
+ return UNKNOWN;
1900
+ }
1901
+ const letters = token.slice(1);
1902
+ if (!letters) return NONE;
1903
+ for (let i = 0; i < letters.length; i++) {
1904
+ const name = `-${letters[i]}`;
1905
+ if (shape.takesValue.has(name)) {
1906
+ return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
1907
+ }
1908
+ if (!shape.noValue.has(name)) return UNKNOWN;
1909
+ }
1910
+ return NONE;
1911
+ }
1912
+ function readTargets(verb, args, flags, words = [], from = 1) {
1913
+ const shape = PATTERN_VERBS[verb];
1914
+ if (!shape) return args.map((a) => a.value);
1915
+ const known = knownLongFlags(verb);
1916
+ const names = flags.flatMap((f) => flagNamesOf(f, shape));
1917
+ const patternElsewhere = names.some(
1918
+ (n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
1919
+ );
1920
+ const excused = /* @__PURE__ */ new Set();
1921
+ const fileFlags = FILE_OPERAND_FLAGS[verb];
1922
+ const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
1923
+ const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
1924
+ for (const a of args) {
1925
+ if (a.afterFlag === null || pastOptions(a)) continue;
1926
+ const e = flagEffect(a.afterFlag, shape, known);
1927
+ if (e.kind !== "takes") continue;
1928
+ if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
1929
+ excused.add(a);
1930
+ }
1931
+ const patternArgv = (() => {
1932
+ for (let i = from; i < words.length; i++) {
1933
+ const w = words[i];
1934
+ if (w === null) return -1;
1935
+ if (w === "--") return i + 1;
1936
+ if (w.startsWith("-")) {
1937
+ const e = flagEffect(w, shape, known);
1938
+ if (e.kind === "takes") i += 1;
1939
+ else if (e.kind === "unknown") return -1;
1940
+ continue;
1941
+ }
1942
+ return i;
1943
+ }
1944
+ return -1;
1945
+ })();
1946
+ if (!patternElsewhere && patternArgv >= 0) {
1947
+ const a = args.find((x) => x.argv === patternArgv);
1948
+ if (a) excused.add(a);
1949
+ }
1950
+ return args.filter((a) => !excused.has(a)).map((a) => a.value);
1951
+ }
1952
+ function flagOperandFiles(verb, words, from) {
1953
+ const flags = FILE_OPERAND_FLAGS[verb];
1954
+ if (!flags) return [];
1955
+ const known = knownLongFlags(verb);
1956
+ const out = [];
1957
+ for (let i = from; i < words.length; i++) {
1958
+ const w = words[i];
1959
+ if (w === null || !w.startsWith("-") || w === "--") continue;
1960
+ if (w.startsWith("--")) {
1961
+ const eq = w.indexOf("=");
1962
+ if (eq <= 0) continue;
1963
+ const name = w.slice(0, eq);
1964
+ const value = w.slice(eq + 1);
1965
+ if (!value) continue;
1966
+ if (namesFlag(name, flags, known)) {
1967
+ out.push(value);
1968
+ continue;
1969
+ }
1970
+ const shape2 = PATTERN_VERBS[verb];
1971
+ if (!shape2) continue;
1972
+ const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
1973
+ if (!recognised) out.push(value);
1974
+ continue;
1975
+ }
1976
+ const shape = PATTERN_VERBS[verb];
1977
+ const letters = w.slice(1);
1978
+ for (let j = 0; j < letters.length; j++) {
1979
+ const name = `-${letters[j]}`;
1980
+ const isFileFlag = flags.has(name);
1981
+ const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
1982
+ if (!argTaking) continue;
1983
+ const attached = letters.slice(j + 1);
1984
+ if (isFileFlag && attached) out.push(attached);
1985
+ break;
1986
+ }
1987
+ }
1988
+ return out;
1989
+ }
1790
1990
  function wrappedReadPaths(words, name) {
1791
1991
  if (name === "find") {
1792
1992
  const { k, starts } = findStartPoints(words, 0);
@@ -1794,7 +1994,14 @@ function wrappedReadPaths(words, name) {
1794
1994
  }
1795
1995
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1796
1996
  const h = unwrapCommandHead(words);
1797
- return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
1997
+ if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
1998
+ const head = baseWord(words[h]);
1999
+ const rest = words.slice(h + 1);
2000
+ const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
2001
+ return [
2002
+ ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
2003
+ ...flagOperandFiles(head, words, h + 1)
2004
+ ];
1798
2005
  }
1799
2006
  function literalShellPayload(words, name) {
1800
2007
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -3480,7 +3687,7 @@ function* stringValues(obj, depth = 0) {
3480
3687
  }
3481
3688
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3482
3689
  }
3483
- var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, SCP_VALUE_FLAGS, RSYNC_SKIP, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, 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, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3690
+ var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, 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, BASH_TOOL_NAMES, 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, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, 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, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, DESTINATION_ARGS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, 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, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3484
3691
  var init_dist = __esm({
3485
3692
  "packages/policy-engine/dist/index.mjs"() {
3486
3693
  "use strict";
@@ -4223,7 +4430,345 @@ var init_dist = __esm({
4223
4430
  "nl",
4224
4431
  "dd"
4225
4432
  ]);
4433
+ GREP_SHAPE = {
4434
+ takesValue: /* @__PURE__ */ new Set([
4435
+ "-A",
4436
+ "-B",
4437
+ "-C",
4438
+ "-D",
4439
+ "-d",
4440
+ "-e",
4441
+ "-f",
4442
+ "-m",
4443
+ "--after-context",
4444
+ "--before-context",
4445
+ "--binary-files",
4446
+ "--context",
4447
+ "--devices",
4448
+ "--directories",
4449
+ "--exclude",
4450
+ "--exclude-dir",
4451
+ "--exclude-from",
4452
+ "--file",
4453
+ "--include",
4454
+ "--label",
4455
+ "--max-count",
4456
+ "--regexp"
4457
+ ]),
4458
+ noValue: /* @__PURE__ */ new Set([
4459
+ "-E",
4460
+ "-F",
4461
+ "-G",
4462
+ "-P",
4463
+ "-i",
4464
+ "-y",
4465
+ "-v",
4466
+ "-V",
4467
+ "-w",
4468
+ "-x",
4469
+ "-c",
4470
+ "-l",
4471
+ "-L",
4472
+ "-o",
4473
+ "-q",
4474
+ "-s",
4475
+ "-b",
4476
+ "-H",
4477
+ "-h",
4478
+ "-n",
4479
+ "-T",
4480
+ "-Z",
4481
+ "-z",
4482
+ "-R",
4483
+ "-r",
4484
+ "-U",
4485
+ "-u",
4486
+ "-I",
4487
+ "-a",
4488
+ "--basic-regexp",
4489
+ "--binary",
4490
+ "--byte-offset",
4491
+ "--color",
4492
+ "--colour",
4493
+ "--count",
4494
+ "--dereference-recursive",
4495
+ "--extended-regexp",
4496
+ "--files-with-matches",
4497
+ "--files-without-match",
4498
+ "--fixed-strings",
4499
+ "--help",
4500
+ "--ignore-case",
4501
+ "--initial-tab",
4502
+ "--invert-match",
4503
+ "--line-buffered",
4504
+ "--line-number",
4505
+ "--line-regexp",
4506
+ "--no-filename",
4507
+ "--no-group-separator",
4508
+ "--no-ignore-case",
4509
+ "--no-messages",
4510
+ "--null",
4511
+ "--null-data",
4512
+ "--only-matching",
4513
+ "--perl-regexp",
4514
+ "--quiet",
4515
+ "--recursive",
4516
+ "--silent",
4517
+ "--text",
4518
+ "--version",
4519
+ "--with-filename",
4520
+ "--word-regexp"
4521
+ ]),
4522
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
4523
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
4524
+ };
4525
+ PATTERN_VERBS = {
4526
+ grep: GREP_SHAPE,
4527
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
4528
+ // `grep -E` and `grep -F`. Read in full, not assumed.
4529
+ egrep: GREP_SHAPE,
4530
+ fgrep: GREP_SHAPE,
4531
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
4532
+ // was not installed on the measuring machine; it is, and that stale claim is why
4533
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
4534
+ // `rg --sort-files .env src` (/code-review round 4).
4535
+ rg: {
4536
+ takesValue: /* @__PURE__ */ new Set([
4537
+ "-A",
4538
+ "-B",
4539
+ "-C",
4540
+ "-d",
4541
+ "-E",
4542
+ "-e",
4543
+ "-f",
4544
+ "-g",
4545
+ "-j",
4546
+ "-M",
4547
+ "-m",
4548
+ "-r",
4549
+ "-t",
4550
+ "-T",
4551
+ "--after-context",
4552
+ "--before-context",
4553
+ "--color",
4554
+ "--colors",
4555
+ "--context",
4556
+ "--context-separator",
4557
+ "--dfa-size-limit",
4558
+ "--encoding",
4559
+ "--engine",
4560
+ "--field-context-separator",
4561
+ "--field-match-separator",
4562
+ "--file",
4563
+ "--generate",
4564
+ "--glob",
4565
+ "--hostname-bin",
4566
+ "--hyperlink-format",
4567
+ "--iglob",
4568
+ "--ignore-file",
4569
+ "--max-columns",
4570
+ "--max-count",
4571
+ "--max-depth",
4572
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
4573
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
4574
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
4575
+ "--maxdepth",
4576
+ "--max-filesize",
4577
+ "--path-separator",
4578
+ "--pre",
4579
+ "--pre-glob",
4580
+ "--regexp",
4581
+ "--regex-size-limit",
4582
+ "--replace",
4583
+ "--sort",
4584
+ "--sortr",
4585
+ "--threads",
4586
+ "--type",
4587
+ "--type-add",
4588
+ "--type-clear",
4589
+ "--type-not"
4590
+ ]),
4591
+ noValue: /* @__PURE__ */ new Set([
4592
+ "-.",
4593
+ "-0",
4594
+ "-a",
4595
+ "-b",
4596
+ "-c",
4597
+ "-F",
4598
+ "-h",
4599
+ "-H",
4600
+ "-i",
4601
+ "-I",
4602
+ "-l",
4603
+ "-L",
4604
+ "-n",
4605
+ "-N",
4606
+ "-o",
4607
+ "-p",
4608
+ "-P",
4609
+ "-q",
4610
+ "-s",
4611
+ "-S",
4612
+ "-u",
4613
+ "-U",
4614
+ "-v",
4615
+ "-V",
4616
+ "-w",
4617
+ "-x",
4618
+ "-z",
4619
+ "--auto-hybrid-regex",
4620
+ "--binary",
4621
+ "--block-buffered",
4622
+ "--byte-offset",
4623
+ "--case-sensitive",
4624
+ "--column",
4625
+ "--count",
4626
+ "--count-matches",
4627
+ "--crlf",
4628
+ "--debug",
4629
+ "--files",
4630
+ "--files-with-matches",
4631
+ "--files-without-match",
4632
+ "--fixed-strings",
4633
+ "--follow",
4634
+ "--glob-case-insensitive",
4635
+ "--heading",
4636
+ "--help",
4637
+ "--hidden",
4638
+ "--ignore-case",
4639
+ "--ignore-file-case-insensitive",
4640
+ "--include-zero",
4641
+ "--invert-match",
4642
+ "--json",
4643
+ "--line-buffered",
4644
+ "--line-number",
4645
+ "--line-regexp",
4646
+ "--max-columns-preview",
4647
+ "--mmap",
4648
+ "--multiline",
4649
+ "--multiline-dotall",
4650
+ "--no-column",
4651
+ "--no-config",
4652
+ "--no-context-separator",
4653
+ "--no-encoding",
4654
+ "--no-filename",
4655
+ "--no-ignore",
4656
+ "--no-ignore-dot",
4657
+ "--no-ignore-exclude",
4658
+ "--no-ignore-files",
4659
+ "--no-ignore-global",
4660
+ "--no-ignore-messages",
4661
+ "--no-ignore-parent",
4662
+ "--no-ignore-vcs",
4663
+ "--no-line-number",
4664
+ "--no-messages",
4665
+ "--no-pcre2-unicode",
4666
+ "--no-pre",
4667
+ "--no-require-git",
4668
+ "--no-unicode",
4669
+ "--null",
4670
+ "--null-data",
4671
+ "--one-file-system",
4672
+ "--only-matching",
4673
+ "--passthru",
4674
+ "--pcre2",
4675
+ "--pcre2-version",
4676
+ "--pretty",
4677
+ "--print0",
4678
+ "--quiet",
4679
+ "--search-zip",
4680
+ "--smart-case",
4681
+ "--sort-files",
4682
+ "--stats",
4683
+ "--stop-on-nonmatch",
4684
+ "--text",
4685
+ "--trace",
4686
+ "--trim",
4687
+ "--type-list",
4688
+ "--unrestricted",
4689
+ "--version",
4690
+ "--vimgrep",
4691
+ "--with-filename",
4692
+ "--word-regexp",
4693
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
4694
+ // found 40 documented switches in neither set. The table is now the whole
4695
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
4696
+ "--ignore",
4697
+ "--ignore-dot",
4698
+ "--ignore-exclude",
4699
+ "--ignore-files",
4700
+ "--ignore-global",
4701
+ "--ignore-messages",
4702
+ "--ignore-parent",
4703
+ "--ignore-vcs",
4704
+ "--messages",
4705
+ "--no-auto-hybrid-regex",
4706
+ "--no-binary",
4707
+ "--no-block-buffered",
4708
+ "--no-byte-offset",
4709
+ "--no-crlf",
4710
+ "--no-fixed-strings",
4711
+ "--no-follow",
4712
+ "--no-glob-case-insensitive",
4713
+ "--no-heading",
4714
+ "--no-hidden",
4715
+ "--no-ignore-file-case-insensitive",
4716
+ "--no-include-zero",
4717
+ "--no-invert-match",
4718
+ "--no-json",
4719
+ "--no-line-buffered",
4720
+ "--no-max-columns-preview",
4721
+ "--no-mmap",
4722
+ "--no-multiline",
4723
+ "--no-multiline-dotall",
4724
+ "--no-one-file-system",
4725
+ "--no-pcre2",
4726
+ "--no-search-zip",
4727
+ "--no-sort-files",
4728
+ "--no-stats",
4729
+ "--no-text",
4730
+ "--no-trim",
4731
+ "--passthrough",
4732
+ "--pcre2-unicode",
4733
+ "--require-git",
4734
+ "--unicode"
4735
+ ]),
4736
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
4737
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
4738
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
4739
+ // by leaving the directory in the judged list.
4740
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
4741
+ }
4742
+ };
4743
+ PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
4744
+ GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
4745
+ READER_VALUE_LETTERS = {
4746
+ awk: ["F", "v", "f"],
4747
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
4748
+ sed: ["e", "f", "i", "l"],
4749
+ sort: ["C", "k", "o", "S", "t", "T"]
4750
+ };
4751
+ FILE_OPERAND_FLAGS = {
4752
+ grep: GREP_FILE_OPERANDS,
4753
+ egrep: GREP_FILE_OPERANDS,
4754
+ fgrep: GREP_FILE_OPERANDS,
4755
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
4756
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
4757
+ // `-f/--file`) and are marked as such in the spec.
4758
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
4759
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
4760
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
4761
+ // and printed its contents.
4762
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
4763
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
4764
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
4765
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
4766
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
4767
+ };
4226
4768
  SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
4769
+ TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
4770
+ ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
4771
+ RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
4227
4772
  RSYNC_SKIP = [
4228
4773
  "e",
4229
4774
  "--rsh",
@@ -4235,21 +4780,35 @@ var init_dist = __esm({
4235
4780
  "f",
4236
4781
  "--filter"
4237
4782
  ];
4783
+ CP_VALUE_LETTERS = ["S", "t"];
4784
+ INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
4238
4785
  COPY_VERBS = {
4239
- cp: { source: "allButLast", targetDirFlag: true },
4240
- mv: { source: "allButLast", targetDirFlag: true },
4241
- install: { source: "allButLast", targetDirFlag: true },
4242
- ln: { source: "first", targetDirFlag: true },
4243
- scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
4244
- rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
4786
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
4787
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
4788
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
4789
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
4790
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
4791
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
4245
4792
  tar: {
4246
4793
  source: "archive",
4247
4794
  archive: "tar",
4248
- skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
4795
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
4796
+ valueLetters: TAR_VALUE_LETTERS
4797
+ },
4798
+ zip: {
4799
+ source: "archive",
4800
+ archive: "zip",
4801
+ skipFlags: ["x", "i", "--exclude", "--include"],
4802
+ valueLetters: ZIP_VALUE_LETTERS
4249
4803
  },
4250
- zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
4251
4804
  ar: { source: "archive", archive: "ar" },
4252
- "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
4805
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
4806
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
4807
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
4808
+ // drop the credential as an exclusion operand (/code-review round 8), and the
4809
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
4810
+ // what keeps the two tables honest about it.
4811
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
4253
4812
  gzip: { source: "all" },
4254
4813
  bzip2: { source: "all" },
4255
4814
  xz: { source: "all" },
@@ -4571,6 +5130,8 @@ var init_dist = __esm({
4571
5130
  };
4572
5131
  isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
4573
5132
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
5133
+ NONE = { kind: "none" };
5134
+ UNKNOWN = { kind: "unknown" };
4574
5135
  DEFAULT_EGRESS_ALLOWLIST = [
4575
5136
  // node9's own control plane (api, app, dev-api, staging and the apex).
4576
5137
  // Without it, turning egress on asks the user to approve node9 itself.
@@ -5615,7 +6176,7 @@ var init_dist = __esm({
5615
6176
  { view: "separators-stripped", decoder: "separators", stripped: true }
5616
6177
  ];
5617
6178
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
5618
- CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
6179
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v15";
5619
6180
  DEDUPE_PREVIEW_LEN = 120;
5620
6181
  TERMINAL_ESCAPE_RE = // eslint-disable-next-line no-control-regex
5621
6182
  /\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;