@node9/proxy 2.13.1 → 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 +830 -33
- package/dist/cli.mjs +830 -33
- package/dist/dashboard.mjs +814 -31
- package/dist/index.js +787 -28
- package/dist/index.mjs +787 -28
- package/dist/scan-ink.mjs +372 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1480,6 +1480,19 @@ function parseShared(command) {
|
|
|
1480
1480
|
astCache.set(command, parsed);
|
|
1481
1481
|
return parsed;
|
|
1482
1482
|
}
|
|
1483
|
+
function byteOffsetToCharIndex(command) {
|
|
1484
|
+
if (!/[^\u0000-\u007F]/.test(command)) return null;
|
|
1485
|
+
const map = /* @__PURE__ */ new Map();
|
|
1486
|
+
let byte = 0;
|
|
1487
|
+
for (let i = 0; i < command.length; ) {
|
|
1488
|
+
map.set(byte, i);
|
|
1489
|
+
const cp = command.codePointAt(i);
|
|
1490
|
+
byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
1491
|
+
i += cp > 65535 ? 2 : 1;
|
|
1492
|
+
}
|
|
1493
|
+
map.set(byte, command.length);
|
|
1494
|
+
return (b) => map.get(b) ?? -1;
|
|
1495
|
+
}
|
|
1483
1496
|
function cachedNormalize(command, compute) {
|
|
1484
1497
|
const hit = normalizeCache.get(command);
|
|
1485
1498
|
if (hit !== void 0) {
|
|
@@ -1509,6 +1522,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1509
1522
|
const f = parseShared(command);
|
|
1510
1523
|
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1511
1524
|
try {
|
|
1525
|
+
const toCharIndex = byteOffsetToCharIndex(command);
|
|
1526
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
1512
1527
|
const strips = [];
|
|
1513
1528
|
const rewrites = [];
|
|
1514
1529
|
const quoteOnlyRewrites = [];
|
|
@@ -1529,8 +1544,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1529
1544
|
const quotedNode = nextParts[0];
|
|
1530
1545
|
const nt = syntax.NodeType(quotedNode);
|
|
1531
1546
|
const markStrip = () => {
|
|
1532
|
-
const s = next.Pos().Offset();
|
|
1533
|
-
const e = next.End().Offset();
|
|
1547
|
+
const s = at(next.Pos().Offset());
|
|
1548
|
+
const e = at(next.End().Offset());
|
|
1549
|
+
if (s < 0 || e < 0) return;
|
|
1534
1550
|
strips.push([s, e]);
|
|
1535
1551
|
msgSpans.add(`${s}:${e}`);
|
|
1536
1552
|
};
|
|
@@ -1547,8 +1563,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1547
1563
|
}
|
|
1548
1564
|
}
|
|
1549
1565
|
for (const arg of args) {
|
|
1550
|
-
const s = arg.Pos().Offset();
|
|
1551
|
-
const e = arg.End().Offset();
|
|
1566
|
+
const s = at(arg.Pos().Offset());
|
|
1567
|
+
const e = at(arg.End().Offset());
|
|
1568
|
+
if (s < 0 || e < 0) continue;
|
|
1552
1569
|
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1553
1570
|
const resolved = resolveWordLiteral(arg);
|
|
1554
1571
|
if (resolved === null) continue;
|
|
@@ -1670,12 +1687,408 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1670
1687
|
"nl",
|
|
1671
1688
|
"dd"
|
|
1672
1689
|
]);
|
|
1690
|
+
var GREP_SHAPE = {
|
|
1691
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1692
|
+
"-A",
|
|
1693
|
+
"-B",
|
|
1694
|
+
"-C",
|
|
1695
|
+
"-D",
|
|
1696
|
+
"-d",
|
|
1697
|
+
"-e",
|
|
1698
|
+
"-f",
|
|
1699
|
+
"-m",
|
|
1700
|
+
"--after-context",
|
|
1701
|
+
"--before-context",
|
|
1702
|
+
"--binary-files",
|
|
1703
|
+
"--context",
|
|
1704
|
+
"--devices",
|
|
1705
|
+
"--directories",
|
|
1706
|
+
"--exclude",
|
|
1707
|
+
"--exclude-dir",
|
|
1708
|
+
"--exclude-from",
|
|
1709
|
+
"--file",
|
|
1710
|
+
"--include",
|
|
1711
|
+
"--label",
|
|
1712
|
+
"--max-count",
|
|
1713
|
+
"--regexp"
|
|
1714
|
+
]),
|
|
1715
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1716
|
+
"-E",
|
|
1717
|
+
"-F",
|
|
1718
|
+
"-G",
|
|
1719
|
+
"-P",
|
|
1720
|
+
"-i",
|
|
1721
|
+
"-y",
|
|
1722
|
+
"-v",
|
|
1723
|
+
"-V",
|
|
1724
|
+
"-w",
|
|
1725
|
+
"-x",
|
|
1726
|
+
"-c",
|
|
1727
|
+
"-l",
|
|
1728
|
+
"-L",
|
|
1729
|
+
"-o",
|
|
1730
|
+
"-q",
|
|
1731
|
+
"-s",
|
|
1732
|
+
"-b",
|
|
1733
|
+
"-H",
|
|
1734
|
+
"-h",
|
|
1735
|
+
"-n",
|
|
1736
|
+
"-T",
|
|
1737
|
+
"-Z",
|
|
1738
|
+
"-z",
|
|
1739
|
+
"-R",
|
|
1740
|
+
"-r",
|
|
1741
|
+
"-U",
|
|
1742
|
+
"-u",
|
|
1743
|
+
"-I",
|
|
1744
|
+
"-a",
|
|
1745
|
+
"--basic-regexp",
|
|
1746
|
+
"--binary",
|
|
1747
|
+
"--byte-offset",
|
|
1748
|
+
"--color",
|
|
1749
|
+
"--colour",
|
|
1750
|
+
"--count",
|
|
1751
|
+
"--dereference-recursive",
|
|
1752
|
+
"--extended-regexp",
|
|
1753
|
+
"--files-with-matches",
|
|
1754
|
+
"--files-without-match",
|
|
1755
|
+
"--fixed-strings",
|
|
1756
|
+
"--help",
|
|
1757
|
+
"--ignore-case",
|
|
1758
|
+
"--initial-tab",
|
|
1759
|
+
"--invert-match",
|
|
1760
|
+
"--line-buffered",
|
|
1761
|
+
"--line-number",
|
|
1762
|
+
"--line-regexp",
|
|
1763
|
+
"--no-filename",
|
|
1764
|
+
"--no-group-separator",
|
|
1765
|
+
"--no-ignore-case",
|
|
1766
|
+
"--no-messages",
|
|
1767
|
+
"--null",
|
|
1768
|
+
"--null-data",
|
|
1769
|
+
"--only-matching",
|
|
1770
|
+
"--perl-regexp",
|
|
1771
|
+
"--quiet",
|
|
1772
|
+
"--recursive",
|
|
1773
|
+
"--silent",
|
|
1774
|
+
"--text",
|
|
1775
|
+
"--version",
|
|
1776
|
+
"--with-filename",
|
|
1777
|
+
"--word-regexp"
|
|
1778
|
+
]),
|
|
1779
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1780
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
|
|
1781
|
+
};
|
|
1782
|
+
var PATTERN_VERBS = {
|
|
1783
|
+
grep: GREP_SHAPE,
|
|
1784
|
+
// /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
|
|
1785
|
+
// `grep -E` and `grep -F`. Read in full, not assumed.
|
|
1786
|
+
egrep: GREP_SHAPE,
|
|
1787
|
+
fgrep: GREP_SHAPE,
|
|
1788
|
+
// ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
|
|
1789
|
+
// was not installed on the measuring machine; it is, and that stale claim is why
|
|
1790
|
+
// this table shipped incomplete for two rounds, blocking ordinary searches like
|
|
1791
|
+
// `rg --sort-files .env src` (/code-review round 4).
|
|
1792
|
+
rg: {
|
|
1793
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1794
|
+
"-A",
|
|
1795
|
+
"-B",
|
|
1796
|
+
"-C",
|
|
1797
|
+
"-d",
|
|
1798
|
+
"-E",
|
|
1799
|
+
"-e",
|
|
1800
|
+
"-f",
|
|
1801
|
+
"-g",
|
|
1802
|
+
"-j",
|
|
1803
|
+
"-M",
|
|
1804
|
+
"-m",
|
|
1805
|
+
"-r",
|
|
1806
|
+
"-t",
|
|
1807
|
+
"-T",
|
|
1808
|
+
"--after-context",
|
|
1809
|
+
"--before-context",
|
|
1810
|
+
"--color",
|
|
1811
|
+
"--colors",
|
|
1812
|
+
"--context",
|
|
1813
|
+
"--context-separator",
|
|
1814
|
+
"--dfa-size-limit",
|
|
1815
|
+
"--encoding",
|
|
1816
|
+
"--engine",
|
|
1817
|
+
"--field-context-separator",
|
|
1818
|
+
"--field-match-separator",
|
|
1819
|
+
"--file",
|
|
1820
|
+
"--generate",
|
|
1821
|
+
"--glob",
|
|
1822
|
+
"--hostname-bin",
|
|
1823
|
+
"--hyperlink-format",
|
|
1824
|
+
"--iglob",
|
|
1825
|
+
"--ignore-file",
|
|
1826
|
+
"--max-columns",
|
|
1827
|
+
"--max-count",
|
|
1828
|
+
"--max-depth",
|
|
1829
|
+
// An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
|
|
1830
|
+
// alias lines as plain switches and swept it into noValue, which hard-blocked
|
|
1831
|
+
// `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
|
|
1832
|
+
"--maxdepth",
|
|
1833
|
+
"--max-filesize",
|
|
1834
|
+
"--path-separator",
|
|
1835
|
+
"--pre",
|
|
1836
|
+
"--pre-glob",
|
|
1837
|
+
"--regexp",
|
|
1838
|
+
"--regex-size-limit",
|
|
1839
|
+
"--replace",
|
|
1840
|
+
"--sort",
|
|
1841
|
+
"--sortr",
|
|
1842
|
+
"--threads",
|
|
1843
|
+
"--type",
|
|
1844
|
+
"--type-add",
|
|
1845
|
+
"--type-clear",
|
|
1846
|
+
"--type-not"
|
|
1847
|
+
]),
|
|
1848
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1849
|
+
"-.",
|
|
1850
|
+
"-0",
|
|
1851
|
+
"-a",
|
|
1852
|
+
"-b",
|
|
1853
|
+
"-c",
|
|
1854
|
+
"-F",
|
|
1855
|
+
"-h",
|
|
1856
|
+
"-H",
|
|
1857
|
+
"-i",
|
|
1858
|
+
"-I",
|
|
1859
|
+
"-l",
|
|
1860
|
+
"-L",
|
|
1861
|
+
"-n",
|
|
1862
|
+
"-N",
|
|
1863
|
+
"-o",
|
|
1864
|
+
"-p",
|
|
1865
|
+
"-P",
|
|
1866
|
+
"-q",
|
|
1867
|
+
"-s",
|
|
1868
|
+
"-S",
|
|
1869
|
+
"-u",
|
|
1870
|
+
"-U",
|
|
1871
|
+
"-v",
|
|
1872
|
+
"-V",
|
|
1873
|
+
"-w",
|
|
1874
|
+
"-x",
|
|
1875
|
+
"-z",
|
|
1876
|
+
"--auto-hybrid-regex",
|
|
1877
|
+
"--binary",
|
|
1878
|
+
"--block-buffered",
|
|
1879
|
+
"--byte-offset",
|
|
1880
|
+
"--case-sensitive",
|
|
1881
|
+
"--column",
|
|
1882
|
+
"--count",
|
|
1883
|
+
"--count-matches",
|
|
1884
|
+
"--crlf",
|
|
1885
|
+
"--debug",
|
|
1886
|
+
"--files",
|
|
1887
|
+
"--files-with-matches",
|
|
1888
|
+
"--files-without-match",
|
|
1889
|
+
"--fixed-strings",
|
|
1890
|
+
"--follow",
|
|
1891
|
+
"--glob-case-insensitive",
|
|
1892
|
+
"--heading",
|
|
1893
|
+
"--help",
|
|
1894
|
+
"--hidden",
|
|
1895
|
+
"--ignore-case",
|
|
1896
|
+
"--ignore-file-case-insensitive",
|
|
1897
|
+
"--include-zero",
|
|
1898
|
+
"--invert-match",
|
|
1899
|
+
"--json",
|
|
1900
|
+
"--line-buffered",
|
|
1901
|
+
"--line-number",
|
|
1902
|
+
"--line-regexp",
|
|
1903
|
+
"--max-columns-preview",
|
|
1904
|
+
"--mmap",
|
|
1905
|
+
"--multiline",
|
|
1906
|
+
"--multiline-dotall",
|
|
1907
|
+
"--no-column",
|
|
1908
|
+
"--no-config",
|
|
1909
|
+
"--no-context-separator",
|
|
1910
|
+
"--no-encoding",
|
|
1911
|
+
"--no-filename",
|
|
1912
|
+
"--no-ignore",
|
|
1913
|
+
"--no-ignore-dot",
|
|
1914
|
+
"--no-ignore-exclude",
|
|
1915
|
+
"--no-ignore-files",
|
|
1916
|
+
"--no-ignore-global",
|
|
1917
|
+
"--no-ignore-messages",
|
|
1918
|
+
"--no-ignore-parent",
|
|
1919
|
+
"--no-ignore-vcs",
|
|
1920
|
+
"--no-line-number",
|
|
1921
|
+
"--no-messages",
|
|
1922
|
+
"--no-pcre2-unicode",
|
|
1923
|
+
"--no-pre",
|
|
1924
|
+
"--no-require-git",
|
|
1925
|
+
"--no-unicode",
|
|
1926
|
+
"--null",
|
|
1927
|
+
"--null-data",
|
|
1928
|
+
"--one-file-system",
|
|
1929
|
+
"--only-matching",
|
|
1930
|
+
"--passthru",
|
|
1931
|
+
"--pcre2",
|
|
1932
|
+
"--pcre2-version",
|
|
1933
|
+
"--pretty",
|
|
1934
|
+
"--print0",
|
|
1935
|
+
"--quiet",
|
|
1936
|
+
"--search-zip",
|
|
1937
|
+
"--smart-case",
|
|
1938
|
+
"--sort-files",
|
|
1939
|
+
"--stats",
|
|
1940
|
+
"--stop-on-nonmatch",
|
|
1941
|
+
"--text",
|
|
1942
|
+
"--trace",
|
|
1943
|
+
"--trim",
|
|
1944
|
+
"--type-list",
|
|
1945
|
+
"--unrestricted",
|
|
1946
|
+
"--version",
|
|
1947
|
+
"--vimgrep",
|
|
1948
|
+
"--with-filename",
|
|
1949
|
+
"--word-regexp",
|
|
1950
|
+
// The COMPLETE remainder of `rg --help`, added after /code-review round 5
|
|
1951
|
+
// found 40 documented switches in neither set. The table is now the whole
|
|
1952
|
+
// option list: `rg --help` yields 149 long options, 35 of them value-taking.
|
|
1953
|
+
"--ignore",
|
|
1954
|
+
"--ignore-dot",
|
|
1955
|
+
"--ignore-exclude",
|
|
1956
|
+
"--ignore-files",
|
|
1957
|
+
"--ignore-global",
|
|
1958
|
+
"--ignore-messages",
|
|
1959
|
+
"--ignore-parent",
|
|
1960
|
+
"--ignore-vcs",
|
|
1961
|
+
"--messages",
|
|
1962
|
+
"--no-auto-hybrid-regex",
|
|
1963
|
+
"--no-binary",
|
|
1964
|
+
"--no-block-buffered",
|
|
1965
|
+
"--no-byte-offset",
|
|
1966
|
+
"--no-crlf",
|
|
1967
|
+
"--no-fixed-strings",
|
|
1968
|
+
"--no-follow",
|
|
1969
|
+
"--no-glob-case-insensitive",
|
|
1970
|
+
"--no-heading",
|
|
1971
|
+
"--no-hidden",
|
|
1972
|
+
"--no-ignore-file-case-insensitive",
|
|
1973
|
+
"--no-include-zero",
|
|
1974
|
+
"--no-invert-match",
|
|
1975
|
+
"--no-json",
|
|
1976
|
+
"--no-line-buffered",
|
|
1977
|
+
"--no-max-columns-preview",
|
|
1978
|
+
"--no-mmap",
|
|
1979
|
+
"--no-multiline",
|
|
1980
|
+
"--no-multiline-dotall",
|
|
1981
|
+
"--no-one-file-system",
|
|
1982
|
+
"--no-pcre2",
|
|
1983
|
+
"--no-search-zip",
|
|
1984
|
+
"--no-sort-files",
|
|
1985
|
+
"--no-stats",
|
|
1986
|
+
"--no-text",
|
|
1987
|
+
"--no-trim",
|
|
1988
|
+
"--passthrough",
|
|
1989
|
+
"--pcre2-unicode",
|
|
1990
|
+
"--require-git",
|
|
1991
|
+
"--unicode"
|
|
1992
|
+
]),
|
|
1993
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1994
|
+
// `--files` and `--type-list` list or enumerate without a pattern. Founder
|
|
1995
|
+
// decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
|
|
1996
|
+
// by leaving the directory in the judged list.
|
|
1997
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
|
|
2001
|
+
var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
|
|
2002
|
+
var READER_VALUE_LETTERS = {
|
|
2003
|
+
awk: ["F", "v", "f"],
|
|
2004
|
+
gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
|
|
2005
|
+
sed: ["e", "f", "i", "l"],
|
|
2006
|
+
sort: ["C", "k", "o", "S", "t", "T"]
|
|
2007
|
+
};
|
|
2008
|
+
var FILE_OPERAND_FLAGS = {
|
|
2009
|
+
grep: GREP_FILE_OPERANDS,
|
|
2010
|
+
egrep: GREP_FILE_OPERANDS,
|
|
2011
|
+
fgrep: GREP_FILE_OPERANDS,
|
|
2012
|
+
// rg and gawk are NOT installed on the measuring machine: these two rows are
|
|
2013
|
+
// from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
|
|
2014
|
+
// `-f/--file`) and are marked as such in the spec.
|
|
2015
|
+
// `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
|
|
2016
|
+
// a credential makes rg open it -- the same reason grep's `--include` is here.
|
|
2017
|
+
// Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
|
|
2018
|
+
// and printed its contents.
|
|
2019
|
+
rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
|
|
2020
|
+
sed: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2021
|
+
awk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2022
|
+
gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2023
|
+
sort: /* @__PURE__ */ new Set(["--files0-from"])
|
|
2024
|
+
};
|
|
2025
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
2026
|
+
var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
|
|
2027
|
+
var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
|
|
2028
|
+
var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
|
|
2029
|
+
var RSYNC_SKIP = [
|
|
2030
|
+
"e",
|
|
2031
|
+
"--rsh",
|
|
2032
|
+
"--exclude",
|
|
2033
|
+
"--exclude-from",
|
|
2034
|
+
"--include",
|
|
2035
|
+
"--include-from",
|
|
2036
|
+
"--files-from",
|
|
2037
|
+
"f",
|
|
2038
|
+
"--filter"
|
|
2039
|
+
];
|
|
2040
|
+
var CP_VALUE_LETTERS = ["S", "t"];
|
|
2041
|
+
var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
|
|
2042
|
+
var COPY_VERBS = {
|
|
2043
|
+
cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2044
|
+
mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2045
|
+
install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
|
|
2046
|
+
ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2047
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
|
|
2048
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
|
|
2049
|
+
tar: {
|
|
2050
|
+
source: "archive",
|
|
2051
|
+
archive: "tar",
|
|
2052
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
|
|
2053
|
+
valueLetters: TAR_VALUE_LETTERS
|
|
2054
|
+
},
|
|
2055
|
+
zip: {
|
|
2056
|
+
source: "archive",
|
|
2057
|
+
archive: "zip",
|
|
2058
|
+
skipFlags: ["x", "i", "--exclude", "--include"],
|
|
2059
|
+
valueLetters: ZIP_VALUE_LETTERS
|
|
2060
|
+
},
|
|
2061
|
+
ar: { source: "archive", archive: "ar" },
|
|
2062
|
+
// 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
|
|
2063
|
+
// ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
|
|
2064
|
+
// and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
|
|
2065
|
+
// drop the credential as an exclusion operand (/code-review round 8), and the
|
|
2066
|
+
// derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
|
|
2067
|
+
// what keeps the two tables honest about it.
|
|
2068
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
|
|
2069
|
+
gzip: { source: "all" },
|
|
2070
|
+
bzip2: { source: "all" },
|
|
2071
|
+
xz: { source: "all" },
|
|
2072
|
+
"docker cp": { source: "allButLast" },
|
|
2073
|
+
"kubectl cp": { source: "allButLast" },
|
|
2074
|
+
"gsutil cp": { source: "allButLast" },
|
|
2075
|
+
"gsutil rsync": { source: "allButLast" },
|
|
2076
|
+
"rclone copy": { source: "allButLast" },
|
|
2077
|
+
"rclone sync": { source: "allButLast" },
|
|
2078
|
+
"aws s3 cp": { source: "allButLast" },
|
|
2079
|
+
"aws s3 mv": { source: "allButLast" },
|
|
2080
|
+
"aws s3 sync": { source: "allButLast" },
|
|
2081
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
2082
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
2083
|
+
};
|
|
2084
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
2085
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1673
2086
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1674
2087
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1675
2088
|
// reader right after `"` / `'`, and without these two characters the
|
|
1676
2089
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1677
2090
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1678
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
2091
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1679
2092
|
);
|
|
1680
2093
|
var HOME_CACHE_ALLOWLIST = [
|
|
1681
2094
|
".cache",
|
|
@@ -2110,20 +2523,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
2110
2523
|
}
|
|
2111
2524
|
return true;
|
|
2112
2525
|
}
|
|
2113
|
-
function
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
2118
|
-
const flags = [];
|
|
2119
|
-
const paths = [];
|
|
2120
|
-
for (let i = 1; i < words.length; i++) {
|
|
2526
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2527
|
+
const out = [];
|
|
2528
|
+
let afterFlag = null;
|
|
2529
|
+
for (let i = from; i < to; i++) {
|
|
2121
2530
|
const v = words[i];
|
|
2122
|
-
if (v === null)
|
|
2123
|
-
|
|
2124
|
-
|
|
2531
|
+
if (v === null) {
|
|
2532
|
+
afterFlag = null;
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
if (v.startsWith("-")) {
|
|
2536
|
+
afterFlag = v;
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2540
|
+
afterFlag = null;
|
|
2125
2541
|
}
|
|
2126
|
-
return
|
|
2542
|
+
return out;
|
|
2543
|
+
}
|
|
2544
|
+
function extractLiteralArgs(callExpr) {
|
|
2545
|
+
const rawArgs = callExpr.Args || [];
|
|
2546
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
2547
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
2548
|
+
const name = baseWord(words[0]);
|
|
2549
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
2550
|
+
const args = positionedArgs(words);
|
|
2551
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
2127
2552
|
}
|
|
2128
2553
|
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2129
2554
|
"curl",
|
|
@@ -2500,7 +2925,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2500
2925
|
return result?.verdict !== "block";
|
|
2501
2926
|
}
|
|
2502
2927
|
if (nodeType !== "CallExpr") return true;
|
|
2503
|
-
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2928
|
+
const { name, flags, paths, words, args } = extractLiteralArgs(n);
|
|
2504
2929
|
if (!name) return true;
|
|
2505
2930
|
if (name === "rm") {
|
|
2506
2931
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2540,13 +2965,16 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2540
2965
|
return true;
|
|
2541
2966
|
}
|
|
2542
2967
|
}
|
|
2543
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
2968
|
+
const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
|
|
2544
2969
|
if (readPaths) {
|
|
2545
2970
|
for (const p of readPaths) {
|
|
2546
2971
|
result = stricter(result, matchSensitivePath2(p));
|
|
2547
2972
|
if (result?.verdict === "block") return false;
|
|
2548
2973
|
}
|
|
2549
2974
|
}
|
|
2975
|
+
for (const p of copySourcePaths(words)) {
|
|
2976
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2977
|
+
}
|
|
2550
2978
|
return true;
|
|
2551
2979
|
});
|
|
2552
2980
|
return result;
|
|
@@ -2559,6 +2987,191 @@ function stricter(a, b) {
|
|
|
2559
2987
|
if (!b) return a;
|
|
2560
2988
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2561
2989
|
}
|
|
2990
|
+
function flagInfo(w) {
|
|
2991
|
+
if (w.startsWith("--")) {
|
|
2992
|
+
const eq = w.indexOf("=");
|
|
2993
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
2994
|
+
}
|
|
2995
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
2996
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
2997
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
2998
|
+
}
|
|
2999
|
+
function flagIs(w, names) {
|
|
3000
|
+
if (w === null) return false;
|
|
3001
|
+
const f = flagInfo(w);
|
|
3002
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
3003
|
+
}
|
|
3004
|
+
function operandOf(a, names, valueLetters) {
|
|
3005
|
+
if (!names || a.afterFlag === null) return false;
|
|
3006
|
+
const w = a.afterFlag;
|
|
3007
|
+
if (!w.startsWith("--") && valueLetters) {
|
|
3008
|
+
const letters = w.slice(1);
|
|
3009
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3010
|
+
if (!valueLetters.includes(letters[i])) continue;
|
|
3011
|
+
return i === letters.length - 1 && names.includes(letters[i]);
|
|
3012
|
+
}
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
if (w.startsWith("--") && !w.includes("=")) {
|
|
3016
|
+
const longs = names.filter((n) => n.startsWith("--"));
|
|
3017
|
+
if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
|
|
3018
|
+
}
|
|
3019
|
+
return flagIs(w, names) && flagInfo(w).attached === null;
|
|
3020
|
+
}
|
|
3021
|
+
function resolveCopyShape(words, h) {
|
|
3022
|
+
const verb = baseWord(words[h]);
|
|
3023
|
+
if (!verb) return null;
|
|
3024
|
+
const direct = COPY_VERBS[verb];
|
|
3025
|
+
if (direct) return { shape: direct, last: h };
|
|
3026
|
+
const slots = positionedArgs(words, h + 1);
|
|
3027
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3028
|
+
for (let n = 3; n >= 1; n--) {
|
|
3029
|
+
const part = slots.slice(i, i + n);
|
|
3030
|
+
if (part.length < n) continue;
|
|
3031
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
3032
|
+
const shape = COPY_VERBS[key];
|
|
3033
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
3034
|
+
}
|
|
3035
|
+
if (slots[i].afterFlag === null) return null;
|
|
3036
|
+
}
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
3040
|
+
function findStartPoints(words, h) {
|
|
3041
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
3042
|
+
if (k < 0) return { k, starts: [] };
|
|
3043
|
+
const firstPredicate = words.findIndex(
|
|
3044
|
+
(w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
3045
|
+
);
|
|
3046
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
3047
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
3048
|
+
}
|
|
3049
|
+
function copySourcePaths(words) {
|
|
3050
|
+
const h = unwrapCommandHead(words);
|
|
3051
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
3052
|
+
if (fi >= 0) {
|
|
3053
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
3054
|
+
if (k < 0) return [];
|
|
3055
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
3056
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
3057
|
+
}
|
|
3058
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
3059
|
+
const r = resolveCopyShape(words, h);
|
|
3060
|
+
if (!r) return [];
|
|
3061
|
+
const { shape, last } = r;
|
|
3062
|
+
const args = positionedArgs(words, last + 1);
|
|
3063
|
+
const tail = words.slice(last + 1);
|
|
3064
|
+
const copyOptionsEnd = tail.findIndex((w) => w === "--");
|
|
3065
|
+
const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
|
|
3066
|
+
const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
|
|
3067
|
+
const firstValueLetter = (w) => {
|
|
3068
|
+
const letters = w.slice(1);
|
|
3069
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3070
|
+
if ((shape.valueLetters ?? []).includes(letters[i]))
|
|
3071
|
+
return { letter: letters[i], last: i === letters.length - 1 };
|
|
3072
|
+
}
|
|
3073
|
+
return null;
|
|
3074
|
+
};
|
|
3075
|
+
const isTargetDirFlag = (w) => {
|
|
3076
|
+
if (w.startsWith("--")) {
|
|
3077
|
+
const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
|
|
3078
|
+
return name.length >= 3 && "--target-directory".startsWith(name);
|
|
3079
|
+
}
|
|
3080
|
+
return firstValueLetter(w)?.letter === "t";
|
|
3081
|
+
};
|
|
3082
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
|
|
3083
|
+
const targetTakesNextWord = (w) => {
|
|
3084
|
+
if (w.startsWith("--"))
|
|
3085
|
+
return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
|
|
3086
|
+
const f = firstValueLetter(w);
|
|
3087
|
+
return f !== null && f.letter === "t" && f.last;
|
|
3088
|
+
};
|
|
3089
|
+
const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
|
|
3090
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
3091
|
+
const dynamicDest = lastOperand === null;
|
|
3092
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
3093
|
+
let src;
|
|
3094
|
+
switch (shape.source) {
|
|
3095
|
+
case "all":
|
|
3096
|
+
src = args;
|
|
3097
|
+
break;
|
|
3098
|
+
case "first":
|
|
3099
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
3100
|
+
break;
|
|
3101
|
+
case "flagOperand": {
|
|
3102
|
+
const namesSource = (f) => {
|
|
3103
|
+
const names = shape.sourceFlags ?? [];
|
|
3104
|
+
if (f.long !== null)
|
|
3105
|
+
return names.some(
|
|
3106
|
+
(n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
|
|
3107
|
+
);
|
|
3108
|
+
return f.letter !== null && names.includes(f.letter);
|
|
3109
|
+
};
|
|
3110
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
|
|
3111
|
+
return [
|
|
3112
|
+
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
|
|
3113
|
+
...inline
|
|
3114
|
+
];
|
|
3115
|
+
}
|
|
3116
|
+
case "archive":
|
|
3117
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
3118
|
+
break;
|
|
3119
|
+
case "allButLast":
|
|
3120
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
3121
|
+
break;
|
|
3122
|
+
}
|
|
3123
|
+
const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
3124
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
|
|
3125
|
+
const jailedSources = sources.filter((p) => matchSensitivePath2(p));
|
|
3126
|
+
const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
|
|
3127
|
+
if (jailedSources.length === 0) return [];
|
|
3128
|
+
if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
|
|
3129
|
+
}
|
|
3130
|
+
return sources;
|
|
3131
|
+
}
|
|
3132
|
+
function archiveInputs(kind, args, tail) {
|
|
3133
|
+
const first = args[0];
|
|
3134
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
3135
|
+
if (kind === "tar") {
|
|
3136
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
3137
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
3138
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
3139
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
3140
|
+
if (extracting && !writing) return [];
|
|
3141
|
+
void mode;
|
|
3142
|
+
if (!bareKey) return args;
|
|
3143
|
+
let i = 1;
|
|
3144
|
+
const fromDirs = [];
|
|
3145
|
+
for (const ch of first.value) {
|
|
3146
|
+
if (!TAR_VALUE_LETTERS.includes(ch)) continue;
|
|
3147
|
+
const operand = args[i];
|
|
3148
|
+
if (!operand || operand.afterFlag !== null) break;
|
|
3149
|
+
i += 1;
|
|
3150
|
+
if (ch === "C") fromDirs.push(operand);
|
|
3151
|
+
}
|
|
3152
|
+
return [...fromDirs, ...args.slice(i)];
|
|
3153
|
+
}
|
|
3154
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
3155
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
3156
|
+
return args.slice(2);
|
|
3157
|
+
}
|
|
3158
|
+
var COPY_RULE_OF = {
|
|
3159
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
3160
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
3161
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
3162
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
3163
|
+
};
|
|
3164
|
+
function copyVerdictOf(hit) {
|
|
3165
|
+
if (!hit) return null;
|
|
3166
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
3167
|
+
if (!ruleName) return null;
|
|
3168
|
+
return {
|
|
3169
|
+
ruleName,
|
|
3170
|
+
verdict: "review",
|
|
3171
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
3172
|
+
path: hit.path
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
2562
3175
|
function matchSensitivePath2(p) {
|
|
2563
3176
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2564
3177
|
if (sp.match(p))
|
|
@@ -2566,18 +3179,160 @@ function matchSensitivePath2(p) {
|
|
|
2566
3179
|
}
|
|
2567
3180
|
return null;
|
|
2568
3181
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
3182
|
+
function baseWord(w) {
|
|
3183
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
3184
|
+
}
|
|
3185
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
3186
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
3187
|
+
function namesFlag(name, candidates, known) {
|
|
3188
|
+
if (candidates.has(name)) return true;
|
|
3189
|
+
if (!name.startsWith("--") || name.length < 3) return false;
|
|
3190
|
+
if (known?.has(name)) return false;
|
|
3191
|
+
for (const c of candidates) if (c.startsWith(name)) return true;
|
|
3192
|
+
return false;
|
|
3193
|
+
}
|
|
3194
|
+
function knownLongFlags(verb) {
|
|
3195
|
+
const out = /* @__PURE__ */ new Set();
|
|
3196
|
+
const shape = PATTERN_VERBS[verb];
|
|
3197
|
+
if (shape) {
|
|
3198
|
+
for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
|
|
3199
|
+
for (const f of set) if (f.startsWith("--")) out.add(f);
|
|
3200
|
+
}
|
|
3201
|
+
for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
|
|
3202
|
+
return out;
|
|
3203
|
+
}
|
|
3204
|
+
function flagNamesOf(token, shape) {
|
|
3205
|
+
if (token.startsWith("--")) {
|
|
3206
|
+
const eq = token.indexOf("=");
|
|
3207
|
+
return [eq > 0 ? token.slice(0, eq) : token];
|
|
3208
|
+
}
|
|
3209
|
+
const out = [];
|
|
3210
|
+
for (const c of token.slice(1)) {
|
|
3211
|
+
const name = `-${c}`;
|
|
3212
|
+
out.push(name);
|
|
3213
|
+
if (shape?.takesValue.has(name)) break;
|
|
3214
|
+
}
|
|
3215
|
+
return out;
|
|
3216
|
+
}
|
|
3217
|
+
var NONE = { kind: "none" };
|
|
3218
|
+
var UNKNOWN = { kind: "unknown" };
|
|
3219
|
+
function flagEffect(token, shape, known) {
|
|
3220
|
+
if (token === "--") return NONE;
|
|
3221
|
+
if (/^-+$/.test(token)) return UNKNOWN;
|
|
3222
|
+
if (/^-\d+$/.test(token)) return NONE;
|
|
3223
|
+
if (token.startsWith("--")) {
|
|
3224
|
+
if (token.includes("=")) return NONE;
|
|
3225
|
+
const takes = namesFlag(token, shape.takesValue, known);
|
|
3226
|
+
const none = namesFlag(token, shape.noValue, known);
|
|
3227
|
+
if (takes && none) return UNKNOWN;
|
|
3228
|
+
if (takes) return { kind: "takes", flag: token };
|
|
3229
|
+
if (none) return NONE;
|
|
3230
|
+
return UNKNOWN;
|
|
3231
|
+
}
|
|
3232
|
+
const letters = token.slice(1);
|
|
3233
|
+
if (!letters) return NONE;
|
|
3234
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3235
|
+
const name = `-${letters[i]}`;
|
|
3236
|
+
if (shape.takesValue.has(name)) {
|
|
3237
|
+
return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
|
|
3238
|
+
}
|
|
3239
|
+
if (!shape.noValue.has(name)) return UNKNOWN;
|
|
3240
|
+
}
|
|
3241
|
+
return NONE;
|
|
3242
|
+
}
|
|
3243
|
+
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
3244
|
+
const shape = PATTERN_VERBS[verb];
|
|
3245
|
+
if (!shape) return args.map((a) => a.value);
|
|
3246
|
+
const known = knownLongFlags(verb);
|
|
3247
|
+
const names = flags.flatMap((f) => flagNamesOf(f, shape));
|
|
3248
|
+
const patternElsewhere = names.some(
|
|
3249
|
+
(n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
|
|
3250
|
+
);
|
|
3251
|
+
const excused = /* @__PURE__ */ new Set();
|
|
3252
|
+
const fileFlags = FILE_OPERAND_FLAGS[verb];
|
|
3253
|
+
const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
|
|
3254
|
+
const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
|
|
3255
|
+
for (const a of args) {
|
|
3256
|
+
if (a.afterFlag === null || pastOptions(a)) continue;
|
|
3257
|
+
const e = flagEffect(a.afterFlag, shape, known);
|
|
3258
|
+
if (e.kind !== "takes") continue;
|
|
3259
|
+
if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
|
|
3260
|
+
excused.add(a);
|
|
3261
|
+
}
|
|
3262
|
+
const patternArgv = (() => {
|
|
3263
|
+
for (let i = from; i < words.length; i++) {
|
|
3264
|
+
const w = words[i];
|
|
3265
|
+
if (w === null) return -1;
|
|
3266
|
+
if (w === "--") return i + 1;
|
|
3267
|
+
if (w.startsWith("-")) {
|
|
3268
|
+
const e = flagEffect(w, shape, known);
|
|
3269
|
+
if (e.kind === "takes") i += 1;
|
|
3270
|
+
else if (e.kind === "unknown") return -1;
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
return i;
|
|
3274
|
+
}
|
|
3275
|
+
return -1;
|
|
3276
|
+
})();
|
|
3277
|
+
if (!patternElsewhere && patternArgv >= 0) {
|
|
3278
|
+
const a = args.find((x) => x.argv === patternArgv);
|
|
3279
|
+
if (a) excused.add(a);
|
|
3280
|
+
}
|
|
3281
|
+
return args.filter((a) => !excused.has(a)).map((a) => a.value);
|
|
3282
|
+
}
|
|
3283
|
+
function flagOperandFiles(verb, words, from) {
|
|
3284
|
+
const flags = FILE_OPERAND_FLAGS[verb];
|
|
3285
|
+
if (!flags) return [];
|
|
3286
|
+
const known = knownLongFlags(verb);
|
|
3287
|
+
const out = [];
|
|
3288
|
+
for (let i = from; i < words.length; i++) {
|
|
3289
|
+
const w = words[i];
|
|
3290
|
+
if (w === null || !w.startsWith("-") || w === "--") continue;
|
|
3291
|
+
if (w.startsWith("--")) {
|
|
3292
|
+
const eq = w.indexOf("=");
|
|
3293
|
+
if (eq <= 0) continue;
|
|
3294
|
+
const name = w.slice(0, eq);
|
|
3295
|
+
const value = w.slice(eq + 1);
|
|
3296
|
+
if (!value) continue;
|
|
3297
|
+
if (namesFlag(name, flags, known)) {
|
|
3298
|
+
out.push(value);
|
|
3299
|
+
continue;
|
|
3300
|
+
}
|
|
3301
|
+
const shape2 = PATTERN_VERBS[verb];
|
|
3302
|
+
if (!shape2) continue;
|
|
3303
|
+
const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
|
|
3304
|
+
if (!recognised) out.push(value);
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
const shape = PATTERN_VERBS[verb];
|
|
3308
|
+
const letters = w.slice(1);
|
|
3309
|
+
for (let j = 0; j < letters.length; j++) {
|
|
3310
|
+
const name = `-${letters[j]}`;
|
|
3311
|
+
const isFileFlag = flags.has(name);
|
|
3312
|
+
const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
|
|
3313
|
+
if (!argTaking) continue;
|
|
3314
|
+
const attached = letters.slice(j + 1);
|
|
3315
|
+
if (isFileFlag && attached) out.push(attached);
|
|
3316
|
+
break;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
return out;
|
|
3320
|
+
}
|
|
2571
3321
|
function wrappedReadPaths(words, name) {
|
|
2572
3322
|
if (name === "find") {
|
|
2573
|
-
const k = words
|
|
2574
|
-
|
|
2575
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2576
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
3323
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
3324
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2577
3325
|
}
|
|
2578
3326
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2579
3327
|
const h = unwrapCommandHead(words);
|
|
2580
|
-
|
|
3328
|
+
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
3329
|
+
const head = baseWord(words[h]);
|
|
3330
|
+
const rest = words.slice(h + 1);
|
|
3331
|
+
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
3332
|
+
return [
|
|
3333
|
+
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
3334
|
+
...flagOperandFiles(head, words, h + 1)
|
|
3335
|
+
];
|
|
2581
3336
|
}
|
|
2582
3337
|
function literalShellPayload(words, name) {
|
|
2583
3338
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -3434,6 +4189,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3434
4189
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3435
4190
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3436
4191
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
4192
|
+
let pendingAstReview;
|
|
3437
4193
|
if (bashCommand !== null) {
|
|
3438
4194
|
const pipeVerdict = pipeChainVerdict(
|
|
3439
4195
|
bashCommand,
|
|
@@ -3445,7 +4201,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3445
4201
|
if (fsVerdict) {
|
|
3446
4202
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3447
4203
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3448
|
-
|
|
4204
|
+
const astVerdict = {
|
|
3449
4205
|
decision: fsVerdict.verdict,
|
|
3450
4206
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3451
4207
|
reason: fsVerdict.reason,
|
|
@@ -3453,6 +4209,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3453
4209
|
ruleName: fsVerdict.ruleName,
|
|
3454
4210
|
ruleDescription: fsVerdict.reason
|
|
3455
4211
|
};
|
|
4212
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
4213
|
+
pendingAstReview = astVerdict;
|
|
3456
4214
|
}
|
|
3457
4215
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3458
4216
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3495,7 +4253,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3495
4253
|
const matchedRule = resolvePinned(matches);
|
|
3496
4254
|
if (matchedRule) {
|
|
3497
4255
|
if (matchedRule.verdict === "allow")
|
|
3498
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
4256
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3499
4257
|
return {
|
|
3500
4258
|
decision: matchedRule.verdict,
|
|
3501
4259
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3522,6 +4280,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3522
4280
|
allTokens = analyzed.allTokens;
|
|
3523
4281
|
pathTokens = analyzed.paths;
|
|
3524
4282
|
const candidates = [];
|
|
4283
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3525
4284
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3526
4285
|
if (evalVerdict === "block") {
|
|
3527
4286
|
return {
|