@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.js
CHANGED
|
@@ -1510,6 +1510,19 @@ function parseShared(command) {
|
|
|
1510
1510
|
astCache.set(command, parsed);
|
|
1511
1511
|
return parsed;
|
|
1512
1512
|
}
|
|
1513
|
+
function byteOffsetToCharIndex(command) {
|
|
1514
|
+
if (!/[^\u0000-\u007F]/.test(command)) return null;
|
|
1515
|
+
const map = /* @__PURE__ */ new Map();
|
|
1516
|
+
let byte = 0;
|
|
1517
|
+
for (let i = 0; i < command.length; ) {
|
|
1518
|
+
map.set(byte, i);
|
|
1519
|
+
const cp = command.codePointAt(i);
|
|
1520
|
+
byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
1521
|
+
i += cp > 65535 ? 2 : 1;
|
|
1522
|
+
}
|
|
1523
|
+
map.set(byte, command.length);
|
|
1524
|
+
return (b) => map.get(b) ?? -1;
|
|
1525
|
+
}
|
|
1513
1526
|
function cachedNormalize(command, compute) {
|
|
1514
1527
|
const hit = normalizeCache.get(command);
|
|
1515
1528
|
if (hit !== void 0) {
|
|
@@ -1539,6 +1552,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1539
1552
|
const f = parseShared(command);
|
|
1540
1553
|
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1541
1554
|
try {
|
|
1555
|
+
const toCharIndex = byteOffsetToCharIndex(command);
|
|
1556
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
1542
1557
|
const strips = [];
|
|
1543
1558
|
const rewrites = [];
|
|
1544
1559
|
const quoteOnlyRewrites = [];
|
|
@@ -1559,8 +1574,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1559
1574
|
const quotedNode = nextParts[0];
|
|
1560
1575
|
const nt = syntax.NodeType(quotedNode);
|
|
1561
1576
|
const markStrip = () => {
|
|
1562
|
-
const s = next.Pos().Offset();
|
|
1563
|
-
const e = next.End().Offset();
|
|
1577
|
+
const s = at(next.Pos().Offset());
|
|
1578
|
+
const e = at(next.End().Offset());
|
|
1579
|
+
if (s < 0 || e < 0) return;
|
|
1564
1580
|
strips.push([s, e]);
|
|
1565
1581
|
msgSpans.add(`${s}:${e}`);
|
|
1566
1582
|
};
|
|
@@ -1577,8 +1593,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1577
1593
|
}
|
|
1578
1594
|
}
|
|
1579
1595
|
for (const arg of args) {
|
|
1580
|
-
const s = arg.Pos().Offset();
|
|
1581
|
-
const e = arg.End().Offset();
|
|
1596
|
+
const s = at(arg.Pos().Offset());
|
|
1597
|
+
const e = at(arg.End().Offset());
|
|
1598
|
+
if (s < 0 || e < 0) continue;
|
|
1582
1599
|
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1583
1600
|
const resolved = resolveWordLiteral(arg);
|
|
1584
1601
|
if (resolved === null) continue;
|
|
@@ -1700,12 +1717,408 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1700
1717
|
"nl",
|
|
1701
1718
|
"dd"
|
|
1702
1719
|
]);
|
|
1720
|
+
var GREP_SHAPE = {
|
|
1721
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1722
|
+
"-A",
|
|
1723
|
+
"-B",
|
|
1724
|
+
"-C",
|
|
1725
|
+
"-D",
|
|
1726
|
+
"-d",
|
|
1727
|
+
"-e",
|
|
1728
|
+
"-f",
|
|
1729
|
+
"-m",
|
|
1730
|
+
"--after-context",
|
|
1731
|
+
"--before-context",
|
|
1732
|
+
"--binary-files",
|
|
1733
|
+
"--context",
|
|
1734
|
+
"--devices",
|
|
1735
|
+
"--directories",
|
|
1736
|
+
"--exclude",
|
|
1737
|
+
"--exclude-dir",
|
|
1738
|
+
"--exclude-from",
|
|
1739
|
+
"--file",
|
|
1740
|
+
"--include",
|
|
1741
|
+
"--label",
|
|
1742
|
+
"--max-count",
|
|
1743
|
+
"--regexp"
|
|
1744
|
+
]),
|
|
1745
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1746
|
+
"-E",
|
|
1747
|
+
"-F",
|
|
1748
|
+
"-G",
|
|
1749
|
+
"-P",
|
|
1750
|
+
"-i",
|
|
1751
|
+
"-y",
|
|
1752
|
+
"-v",
|
|
1753
|
+
"-V",
|
|
1754
|
+
"-w",
|
|
1755
|
+
"-x",
|
|
1756
|
+
"-c",
|
|
1757
|
+
"-l",
|
|
1758
|
+
"-L",
|
|
1759
|
+
"-o",
|
|
1760
|
+
"-q",
|
|
1761
|
+
"-s",
|
|
1762
|
+
"-b",
|
|
1763
|
+
"-H",
|
|
1764
|
+
"-h",
|
|
1765
|
+
"-n",
|
|
1766
|
+
"-T",
|
|
1767
|
+
"-Z",
|
|
1768
|
+
"-z",
|
|
1769
|
+
"-R",
|
|
1770
|
+
"-r",
|
|
1771
|
+
"-U",
|
|
1772
|
+
"-u",
|
|
1773
|
+
"-I",
|
|
1774
|
+
"-a",
|
|
1775
|
+
"--basic-regexp",
|
|
1776
|
+
"--binary",
|
|
1777
|
+
"--byte-offset",
|
|
1778
|
+
"--color",
|
|
1779
|
+
"--colour",
|
|
1780
|
+
"--count",
|
|
1781
|
+
"--dereference-recursive",
|
|
1782
|
+
"--extended-regexp",
|
|
1783
|
+
"--files-with-matches",
|
|
1784
|
+
"--files-without-match",
|
|
1785
|
+
"--fixed-strings",
|
|
1786
|
+
"--help",
|
|
1787
|
+
"--ignore-case",
|
|
1788
|
+
"--initial-tab",
|
|
1789
|
+
"--invert-match",
|
|
1790
|
+
"--line-buffered",
|
|
1791
|
+
"--line-number",
|
|
1792
|
+
"--line-regexp",
|
|
1793
|
+
"--no-filename",
|
|
1794
|
+
"--no-group-separator",
|
|
1795
|
+
"--no-ignore-case",
|
|
1796
|
+
"--no-messages",
|
|
1797
|
+
"--null",
|
|
1798
|
+
"--null-data",
|
|
1799
|
+
"--only-matching",
|
|
1800
|
+
"--perl-regexp",
|
|
1801
|
+
"--quiet",
|
|
1802
|
+
"--recursive",
|
|
1803
|
+
"--silent",
|
|
1804
|
+
"--text",
|
|
1805
|
+
"--version",
|
|
1806
|
+
"--with-filename",
|
|
1807
|
+
"--word-regexp"
|
|
1808
|
+
]),
|
|
1809
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1810
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
|
|
1811
|
+
};
|
|
1812
|
+
var PATTERN_VERBS = {
|
|
1813
|
+
grep: GREP_SHAPE,
|
|
1814
|
+
// /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
|
|
1815
|
+
// `grep -E` and `grep -F`. Read in full, not assumed.
|
|
1816
|
+
egrep: GREP_SHAPE,
|
|
1817
|
+
fgrep: GREP_SHAPE,
|
|
1818
|
+
// ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
|
|
1819
|
+
// was not installed on the measuring machine; it is, and that stale claim is why
|
|
1820
|
+
// this table shipped incomplete for two rounds, blocking ordinary searches like
|
|
1821
|
+
// `rg --sort-files .env src` (/code-review round 4).
|
|
1822
|
+
rg: {
|
|
1823
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1824
|
+
"-A",
|
|
1825
|
+
"-B",
|
|
1826
|
+
"-C",
|
|
1827
|
+
"-d",
|
|
1828
|
+
"-E",
|
|
1829
|
+
"-e",
|
|
1830
|
+
"-f",
|
|
1831
|
+
"-g",
|
|
1832
|
+
"-j",
|
|
1833
|
+
"-M",
|
|
1834
|
+
"-m",
|
|
1835
|
+
"-r",
|
|
1836
|
+
"-t",
|
|
1837
|
+
"-T",
|
|
1838
|
+
"--after-context",
|
|
1839
|
+
"--before-context",
|
|
1840
|
+
"--color",
|
|
1841
|
+
"--colors",
|
|
1842
|
+
"--context",
|
|
1843
|
+
"--context-separator",
|
|
1844
|
+
"--dfa-size-limit",
|
|
1845
|
+
"--encoding",
|
|
1846
|
+
"--engine",
|
|
1847
|
+
"--field-context-separator",
|
|
1848
|
+
"--field-match-separator",
|
|
1849
|
+
"--file",
|
|
1850
|
+
"--generate",
|
|
1851
|
+
"--glob",
|
|
1852
|
+
"--hostname-bin",
|
|
1853
|
+
"--hyperlink-format",
|
|
1854
|
+
"--iglob",
|
|
1855
|
+
"--ignore-file",
|
|
1856
|
+
"--max-columns",
|
|
1857
|
+
"--max-count",
|
|
1858
|
+
"--max-depth",
|
|
1859
|
+
// An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
|
|
1860
|
+
// alias lines as plain switches and swept it into noValue, which hard-blocked
|
|
1861
|
+
// `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
|
|
1862
|
+
"--maxdepth",
|
|
1863
|
+
"--max-filesize",
|
|
1864
|
+
"--path-separator",
|
|
1865
|
+
"--pre",
|
|
1866
|
+
"--pre-glob",
|
|
1867
|
+
"--regexp",
|
|
1868
|
+
"--regex-size-limit",
|
|
1869
|
+
"--replace",
|
|
1870
|
+
"--sort",
|
|
1871
|
+
"--sortr",
|
|
1872
|
+
"--threads",
|
|
1873
|
+
"--type",
|
|
1874
|
+
"--type-add",
|
|
1875
|
+
"--type-clear",
|
|
1876
|
+
"--type-not"
|
|
1877
|
+
]),
|
|
1878
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1879
|
+
"-.",
|
|
1880
|
+
"-0",
|
|
1881
|
+
"-a",
|
|
1882
|
+
"-b",
|
|
1883
|
+
"-c",
|
|
1884
|
+
"-F",
|
|
1885
|
+
"-h",
|
|
1886
|
+
"-H",
|
|
1887
|
+
"-i",
|
|
1888
|
+
"-I",
|
|
1889
|
+
"-l",
|
|
1890
|
+
"-L",
|
|
1891
|
+
"-n",
|
|
1892
|
+
"-N",
|
|
1893
|
+
"-o",
|
|
1894
|
+
"-p",
|
|
1895
|
+
"-P",
|
|
1896
|
+
"-q",
|
|
1897
|
+
"-s",
|
|
1898
|
+
"-S",
|
|
1899
|
+
"-u",
|
|
1900
|
+
"-U",
|
|
1901
|
+
"-v",
|
|
1902
|
+
"-V",
|
|
1903
|
+
"-w",
|
|
1904
|
+
"-x",
|
|
1905
|
+
"-z",
|
|
1906
|
+
"--auto-hybrid-regex",
|
|
1907
|
+
"--binary",
|
|
1908
|
+
"--block-buffered",
|
|
1909
|
+
"--byte-offset",
|
|
1910
|
+
"--case-sensitive",
|
|
1911
|
+
"--column",
|
|
1912
|
+
"--count",
|
|
1913
|
+
"--count-matches",
|
|
1914
|
+
"--crlf",
|
|
1915
|
+
"--debug",
|
|
1916
|
+
"--files",
|
|
1917
|
+
"--files-with-matches",
|
|
1918
|
+
"--files-without-match",
|
|
1919
|
+
"--fixed-strings",
|
|
1920
|
+
"--follow",
|
|
1921
|
+
"--glob-case-insensitive",
|
|
1922
|
+
"--heading",
|
|
1923
|
+
"--help",
|
|
1924
|
+
"--hidden",
|
|
1925
|
+
"--ignore-case",
|
|
1926
|
+
"--ignore-file-case-insensitive",
|
|
1927
|
+
"--include-zero",
|
|
1928
|
+
"--invert-match",
|
|
1929
|
+
"--json",
|
|
1930
|
+
"--line-buffered",
|
|
1931
|
+
"--line-number",
|
|
1932
|
+
"--line-regexp",
|
|
1933
|
+
"--max-columns-preview",
|
|
1934
|
+
"--mmap",
|
|
1935
|
+
"--multiline",
|
|
1936
|
+
"--multiline-dotall",
|
|
1937
|
+
"--no-column",
|
|
1938
|
+
"--no-config",
|
|
1939
|
+
"--no-context-separator",
|
|
1940
|
+
"--no-encoding",
|
|
1941
|
+
"--no-filename",
|
|
1942
|
+
"--no-ignore",
|
|
1943
|
+
"--no-ignore-dot",
|
|
1944
|
+
"--no-ignore-exclude",
|
|
1945
|
+
"--no-ignore-files",
|
|
1946
|
+
"--no-ignore-global",
|
|
1947
|
+
"--no-ignore-messages",
|
|
1948
|
+
"--no-ignore-parent",
|
|
1949
|
+
"--no-ignore-vcs",
|
|
1950
|
+
"--no-line-number",
|
|
1951
|
+
"--no-messages",
|
|
1952
|
+
"--no-pcre2-unicode",
|
|
1953
|
+
"--no-pre",
|
|
1954
|
+
"--no-require-git",
|
|
1955
|
+
"--no-unicode",
|
|
1956
|
+
"--null",
|
|
1957
|
+
"--null-data",
|
|
1958
|
+
"--one-file-system",
|
|
1959
|
+
"--only-matching",
|
|
1960
|
+
"--passthru",
|
|
1961
|
+
"--pcre2",
|
|
1962
|
+
"--pcre2-version",
|
|
1963
|
+
"--pretty",
|
|
1964
|
+
"--print0",
|
|
1965
|
+
"--quiet",
|
|
1966
|
+
"--search-zip",
|
|
1967
|
+
"--smart-case",
|
|
1968
|
+
"--sort-files",
|
|
1969
|
+
"--stats",
|
|
1970
|
+
"--stop-on-nonmatch",
|
|
1971
|
+
"--text",
|
|
1972
|
+
"--trace",
|
|
1973
|
+
"--trim",
|
|
1974
|
+
"--type-list",
|
|
1975
|
+
"--unrestricted",
|
|
1976
|
+
"--version",
|
|
1977
|
+
"--vimgrep",
|
|
1978
|
+
"--with-filename",
|
|
1979
|
+
"--word-regexp",
|
|
1980
|
+
// The COMPLETE remainder of `rg --help`, added after /code-review round 5
|
|
1981
|
+
// found 40 documented switches in neither set. The table is now the whole
|
|
1982
|
+
// option list: `rg --help` yields 149 long options, 35 of them value-taking.
|
|
1983
|
+
"--ignore",
|
|
1984
|
+
"--ignore-dot",
|
|
1985
|
+
"--ignore-exclude",
|
|
1986
|
+
"--ignore-files",
|
|
1987
|
+
"--ignore-global",
|
|
1988
|
+
"--ignore-messages",
|
|
1989
|
+
"--ignore-parent",
|
|
1990
|
+
"--ignore-vcs",
|
|
1991
|
+
"--messages",
|
|
1992
|
+
"--no-auto-hybrid-regex",
|
|
1993
|
+
"--no-binary",
|
|
1994
|
+
"--no-block-buffered",
|
|
1995
|
+
"--no-byte-offset",
|
|
1996
|
+
"--no-crlf",
|
|
1997
|
+
"--no-fixed-strings",
|
|
1998
|
+
"--no-follow",
|
|
1999
|
+
"--no-glob-case-insensitive",
|
|
2000
|
+
"--no-heading",
|
|
2001
|
+
"--no-hidden",
|
|
2002
|
+
"--no-ignore-file-case-insensitive",
|
|
2003
|
+
"--no-include-zero",
|
|
2004
|
+
"--no-invert-match",
|
|
2005
|
+
"--no-json",
|
|
2006
|
+
"--no-line-buffered",
|
|
2007
|
+
"--no-max-columns-preview",
|
|
2008
|
+
"--no-mmap",
|
|
2009
|
+
"--no-multiline",
|
|
2010
|
+
"--no-multiline-dotall",
|
|
2011
|
+
"--no-one-file-system",
|
|
2012
|
+
"--no-pcre2",
|
|
2013
|
+
"--no-search-zip",
|
|
2014
|
+
"--no-sort-files",
|
|
2015
|
+
"--no-stats",
|
|
2016
|
+
"--no-text",
|
|
2017
|
+
"--no-trim",
|
|
2018
|
+
"--passthrough",
|
|
2019
|
+
"--pcre2-unicode",
|
|
2020
|
+
"--require-git",
|
|
2021
|
+
"--unicode"
|
|
2022
|
+
]),
|
|
2023
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
2024
|
+
// `--files` and `--type-list` list or enumerate without a pattern. Founder
|
|
2025
|
+
// decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
|
|
2026
|
+
// by leaving the directory in the judged list.
|
|
2027
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
|
|
2031
|
+
var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
|
|
2032
|
+
var READER_VALUE_LETTERS = {
|
|
2033
|
+
awk: ["F", "v", "f"],
|
|
2034
|
+
gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
|
|
2035
|
+
sed: ["e", "f", "i", "l"],
|
|
2036
|
+
sort: ["C", "k", "o", "S", "t", "T"]
|
|
2037
|
+
};
|
|
2038
|
+
var FILE_OPERAND_FLAGS = {
|
|
2039
|
+
grep: GREP_FILE_OPERANDS,
|
|
2040
|
+
egrep: GREP_FILE_OPERANDS,
|
|
2041
|
+
fgrep: GREP_FILE_OPERANDS,
|
|
2042
|
+
// rg and gawk are NOT installed on the measuring machine: these two rows are
|
|
2043
|
+
// from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
|
|
2044
|
+
// `-f/--file`) and are marked as such in the spec.
|
|
2045
|
+
// `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
|
|
2046
|
+
// a credential makes rg open it -- the same reason grep's `--include` is here.
|
|
2047
|
+
// Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
|
|
2048
|
+
// and printed its contents.
|
|
2049
|
+
rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
|
|
2050
|
+
sed: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2051
|
+
awk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2052
|
+
gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
2053
|
+
sort: /* @__PURE__ */ new Set(["--files0-from"])
|
|
2054
|
+
};
|
|
2055
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
2056
|
+
var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
|
|
2057
|
+
var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
|
|
2058
|
+
var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
|
|
2059
|
+
var RSYNC_SKIP = [
|
|
2060
|
+
"e",
|
|
2061
|
+
"--rsh",
|
|
2062
|
+
"--exclude",
|
|
2063
|
+
"--exclude-from",
|
|
2064
|
+
"--include",
|
|
2065
|
+
"--include-from",
|
|
2066
|
+
"--files-from",
|
|
2067
|
+
"f",
|
|
2068
|
+
"--filter"
|
|
2069
|
+
];
|
|
2070
|
+
var CP_VALUE_LETTERS = ["S", "t"];
|
|
2071
|
+
var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
|
|
2072
|
+
var COPY_VERBS = {
|
|
2073
|
+
cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2074
|
+
mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2075
|
+
install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
|
|
2076
|
+
ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
2077
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
|
|
2078
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
|
|
2079
|
+
tar: {
|
|
2080
|
+
source: "archive",
|
|
2081
|
+
archive: "tar",
|
|
2082
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
|
|
2083
|
+
valueLetters: TAR_VALUE_LETTERS
|
|
2084
|
+
},
|
|
2085
|
+
zip: {
|
|
2086
|
+
source: "archive",
|
|
2087
|
+
archive: "zip",
|
|
2088
|
+
skipFlags: ["x", "i", "--exclude", "--include"],
|
|
2089
|
+
valueLetters: ZIP_VALUE_LETTERS
|
|
2090
|
+
},
|
|
2091
|
+
ar: { source: "archive", archive: "ar" },
|
|
2092
|
+
// 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
|
|
2093
|
+
// ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
|
|
2094
|
+
// and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
|
|
2095
|
+
// drop the credential as an exclusion operand (/code-review round 8), and the
|
|
2096
|
+
// derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
|
|
2097
|
+
// what keeps the two tables honest about it.
|
|
2098
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
|
|
2099
|
+
gzip: { source: "all" },
|
|
2100
|
+
bzip2: { source: "all" },
|
|
2101
|
+
xz: { source: "all" },
|
|
2102
|
+
"docker cp": { source: "allButLast" },
|
|
2103
|
+
"kubectl cp": { source: "allButLast" },
|
|
2104
|
+
"gsutil cp": { source: "allButLast" },
|
|
2105
|
+
"gsutil rsync": { source: "allButLast" },
|
|
2106
|
+
"rclone copy": { source: "allButLast" },
|
|
2107
|
+
"rclone sync": { source: "allButLast" },
|
|
2108
|
+
"aws s3 cp": { source: "allButLast" },
|
|
2109
|
+
"aws s3 mv": { source: "allButLast" },
|
|
2110
|
+
"aws s3 sync": { source: "allButLast" },
|
|
2111
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
2112
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
2113
|
+
};
|
|
2114
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
2115
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1703
2116
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1704
2117
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1705
2118
|
// reader right after `"` / `'`, and without these two characters the
|
|
1706
2119
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1707
2120
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1708
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
2121
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1709
2122
|
);
|
|
1710
2123
|
var HOME_CACHE_ALLOWLIST = [
|
|
1711
2124
|
".cache",
|
|
@@ -2140,20 +2553,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
2140
2553
|
}
|
|
2141
2554
|
return true;
|
|
2142
2555
|
}
|
|
2143
|
-
function
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
2148
|
-
const flags = [];
|
|
2149
|
-
const paths = [];
|
|
2150
|
-
for (let i = 1; i < words.length; i++) {
|
|
2556
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2557
|
+
const out = [];
|
|
2558
|
+
let afterFlag = null;
|
|
2559
|
+
for (let i = from; i < to; i++) {
|
|
2151
2560
|
const v = words[i];
|
|
2152
|
-
if (v === null)
|
|
2153
|
-
|
|
2154
|
-
|
|
2561
|
+
if (v === null) {
|
|
2562
|
+
afterFlag = null;
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
if (v.startsWith("-")) {
|
|
2566
|
+
afterFlag = v;
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2570
|
+
afterFlag = null;
|
|
2155
2571
|
}
|
|
2156
|
-
return
|
|
2572
|
+
return out;
|
|
2573
|
+
}
|
|
2574
|
+
function extractLiteralArgs(callExpr) {
|
|
2575
|
+
const rawArgs = callExpr.Args || [];
|
|
2576
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
2577
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
2578
|
+
const name = baseWord(words[0]);
|
|
2579
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
2580
|
+
const args = positionedArgs(words);
|
|
2581
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
2157
2582
|
}
|
|
2158
2583
|
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2159
2584
|
"curl",
|
|
@@ -2530,7 +2955,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2530
2955
|
return result?.verdict !== "block";
|
|
2531
2956
|
}
|
|
2532
2957
|
if (nodeType !== "CallExpr") return true;
|
|
2533
|
-
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2958
|
+
const { name, flags, paths, words, args } = extractLiteralArgs(n);
|
|
2534
2959
|
if (!name) return true;
|
|
2535
2960
|
if (name === "rm") {
|
|
2536
2961
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2570,13 +2995,16 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2570
2995
|
return true;
|
|
2571
2996
|
}
|
|
2572
2997
|
}
|
|
2573
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
2998
|
+
const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
|
|
2574
2999
|
if (readPaths) {
|
|
2575
3000
|
for (const p of readPaths) {
|
|
2576
3001
|
result = stricter(result, matchSensitivePath2(p));
|
|
2577
3002
|
if (result?.verdict === "block") return false;
|
|
2578
3003
|
}
|
|
2579
3004
|
}
|
|
3005
|
+
for (const p of copySourcePaths(words)) {
|
|
3006
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
3007
|
+
}
|
|
2580
3008
|
return true;
|
|
2581
3009
|
});
|
|
2582
3010
|
return result;
|
|
@@ -2589,6 +3017,191 @@ function stricter(a, b) {
|
|
|
2589
3017
|
if (!b) return a;
|
|
2590
3018
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2591
3019
|
}
|
|
3020
|
+
function flagInfo(w) {
|
|
3021
|
+
if (w.startsWith("--")) {
|
|
3022
|
+
const eq = w.indexOf("=");
|
|
3023
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
3024
|
+
}
|
|
3025
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
3026
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
3027
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
3028
|
+
}
|
|
3029
|
+
function flagIs(w, names) {
|
|
3030
|
+
if (w === null) return false;
|
|
3031
|
+
const f = flagInfo(w);
|
|
3032
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
3033
|
+
}
|
|
3034
|
+
function operandOf(a, names, valueLetters) {
|
|
3035
|
+
if (!names || a.afterFlag === null) return false;
|
|
3036
|
+
const w = a.afterFlag;
|
|
3037
|
+
if (!w.startsWith("--") && valueLetters) {
|
|
3038
|
+
const letters = w.slice(1);
|
|
3039
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3040
|
+
if (!valueLetters.includes(letters[i])) continue;
|
|
3041
|
+
return i === letters.length - 1 && names.includes(letters[i]);
|
|
3042
|
+
}
|
|
3043
|
+
return false;
|
|
3044
|
+
}
|
|
3045
|
+
if (w.startsWith("--") && !w.includes("=")) {
|
|
3046
|
+
const longs = names.filter((n) => n.startsWith("--"));
|
|
3047
|
+
if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
|
|
3048
|
+
}
|
|
3049
|
+
return flagIs(w, names) && flagInfo(w).attached === null;
|
|
3050
|
+
}
|
|
3051
|
+
function resolveCopyShape(words, h) {
|
|
3052
|
+
const verb = baseWord(words[h]);
|
|
3053
|
+
if (!verb) return null;
|
|
3054
|
+
const direct = COPY_VERBS[verb];
|
|
3055
|
+
if (direct) return { shape: direct, last: h };
|
|
3056
|
+
const slots = positionedArgs(words, h + 1);
|
|
3057
|
+
for (let i = 0; i < slots.length; i++) {
|
|
3058
|
+
for (let n = 3; n >= 1; n--) {
|
|
3059
|
+
const part = slots.slice(i, i + n);
|
|
3060
|
+
if (part.length < n) continue;
|
|
3061
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
3062
|
+
const shape = COPY_VERBS[key];
|
|
3063
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
3064
|
+
}
|
|
3065
|
+
if (slots[i].afterFlag === null) return null;
|
|
3066
|
+
}
|
|
3067
|
+
return null;
|
|
3068
|
+
}
|
|
3069
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
3070
|
+
function findStartPoints(words, h) {
|
|
3071
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
3072
|
+
if (k < 0) return { k, starts: [] };
|
|
3073
|
+
const firstPredicate = words.findIndex(
|
|
3074
|
+
(w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
3075
|
+
);
|
|
3076
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
3077
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
3078
|
+
}
|
|
3079
|
+
function copySourcePaths(words) {
|
|
3080
|
+
const h = unwrapCommandHead(words);
|
|
3081
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
3082
|
+
if (fi >= 0) {
|
|
3083
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
3084
|
+
if (k < 0) return [];
|
|
3085
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
3086
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
3087
|
+
}
|
|
3088
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
3089
|
+
const r = resolveCopyShape(words, h);
|
|
3090
|
+
if (!r) return [];
|
|
3091
|
+
const { shape, last } = r;
|
|
3092
|
+
const args = positionedArgs(words, last + 1);
|
|
3093
|
+
const tail = words.slice(last + 1);
|
|
3094
|
+
const copyOptionsEnd = tail.findIndex((w) => w === "--");
|
|
3095
|
+
const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
|
|
3096
|
+
const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
|
|
3097
|
+
const firstValueLetter = (w) => {
|
|
3098
|
+
const letters = w.slice(1);
|
|
3099
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3100
|
+
if ((shape.valueLetters ?? []).includes(letters[i]))
|
|
3101
|
+
return { letter: letters[i], last: i === letters.length - 1 };
|
|
3102
|
+
}
|
|
3103
|
+
return null;
|
|
3104
|
+
};
|
|
3105
|
+
const isTargetDirFlag = (w) => {
|
|
3106
|
+
if (w.startsWith("--")) {
|
|
3107
|
+
const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
|
|
3108
|
+
return name.length >= 3 && "--target-directory".startsWith(name);
|
|
3109
|
+
}
|
|
3110
|
+
return firstValueLetter(w)?.letter === "t";
|
|
3111
|
+
};
|
|
3112
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
|
|
3113
|
+
const targetTakesNextWord = (w) => {
|
|
3114
|
+
if (w.startsWith("--"))
|
|
3115
|
+
return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
|
|
3116
|
+
const f = firstValueLetter(w);
|
|
3117
|
+
return f !== null && f.letter === "t" && f.last;
|
|
3118
|
+
};
|
|
3119
|
+
const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
|
|
3120
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
3121
|
+
const dynamicDest = lastOperand === null;
|
|
3122
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
3123
|
+
let src;
|
|
3124
|
+
switch (shape.source) {
|
|
3125
|
+
case "all":
|
|
3126
|
+
src = args;
|
|
3127
|
+
break;
|
|
3128
|
+
case "first":
|
|
3129
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
3130
|
+
break;
|
|
3131
|
+
case "flagOperand": {
|
|
3132
|
+
const namesSource = (f) => {
|
|
3133
|
+
const names = shape.sourceFlags ?? [];
|
|
3134
|
+
if (f.long !== null)
|
|
3135
|
+
return names.some(
|
|
3136
|
+
(n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
|
|
3137
|
+
);
|
|
3138
|
+
return f.letter !== null && names.includes(f.letter);
|
|
3139
|
+
};
|
|
3140
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
|
|
3141
|
+
return [
|
|
3142
|
+
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
|
|
3143
|
+
...inline
|
|
3144
|
+
];
|
|
3145
|
+
}
|
|
3146
|
+
case "archive":
|
|
3147
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
3148
|
+
break;
|
|
3149
|
+
case "allButLast":
|
|
3150
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
3151
|
+
break;
|
|
3152
|
+
}
|
|
3153
|
+
const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
3154
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
|
|
3155
|
+
const jailedSources = sources.filter((p) => matchSensitivePath2(p));
|
|
3156
|
+
const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
|
|
3157
|
+
if (jailedSources.length === 0) return [];
|
|
3158
|
+
if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
|
|
3159
|
+
}
|
|
3160
|
+
return sources;
|
|
3161
|
+
}
|
|
3162
|
+
function archiveInputs(kind, args, tail) {
|
|
3163
|
+
const first = args[0];
|
|
3164
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
3165
|
+
if (kind === "tar") {
|
|
3166
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
3167
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
3168
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
3169
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
3170
|
+
if (extracting && !writing) return [];
|
|
3171
|
+
void mode;
|
|
3172
|
+
if (!bareKey) return args;
|
|
3173
|
+
let i = 1;
|
|
3174
|
+
const fromDirs = [];
|
|
3175
|
+
for (const ch of first.value) {
|
|
3176
|
+
if (!TAR_VALUE_LETTERS.includes(ch)) continue;
|
|
3177
|
+
const operand = args[i];
|
|
3178
|
+
if (!operand || operand.afterFlag !== null) break;
|
|
3179
|
+
i += 1;
|
|
3180
|
+
if (ch === "C") fromDirs.push(operand);
|
|
3181
|
+
}
|
|
3182
|
+
return [...fromDirs, ...args.slice(i)];
|
|
3183
|
+
}
|
|
3184
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
3185
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
3186
|
+
return args.slice(2);
|
|
3187
|
+
}
|
|
3188
|
+
var COPY_RULE_OF = {
|
|
3189
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
3190
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
3191
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
3192
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
3193
|
+
};
|
|
3194
|
+
function copyVerdictOf(hit) {
|
|
3195
|
+
if (!hit) return null;
|
|
3196
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
3197
|
+
if (!ruleName) return null;
|
|
3198
|
+
return {
|
|
3199
|
+
ruleName,
|
|
3200
|
+
verdict: "review",
|
|
3201
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
3202
|
+
path: hit.path
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
2592
3205
|
function matchSensitivePath2(p) {
|
|
2593
3206
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2594
3207
|
if (sp.match(p))
|
|
@@ -2596,18 +3209,160 @@ function matchSensitivePath2(p) {
|
|
|
2596
3209
|
}
|
|
2597
3210
|
return null;
|
|
2598
3211
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
3212
|
+
function baseWord(w) {
|
|
3213
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
3214
|
+
}
|
|
3215
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
3216
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
3217
|
+
function namesFlag(name, candidates, known) {
|
|
3218
|
+
if (candidates.has(name)) return true;
|
|
3219
|
+
if (!name.startsWith("--") || name.length < 3) return false;
|
|
3220
|
+
if (known?.has(name)) return false;
|
|
3221
|
+
for (const c of candidates) if (c.startsWith(name)) return true;
|
|
3222
|
+
return false;
|
|
3223
|
+
}
|
|
3224
|
+
function knownLongFlags(verb) {
|
|
3225
|
+
const out = /* @__PURE__ */ new Set();
|
|
3226
|
+
const shape = PATTERN_VERBS[verb];
|
|
3227
|
+
if (shape) {
|
|
3228
|
+
for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
|
|
3229
|
+
for (const f of set) if (f.startsWith("--")) out.add(f);
|
|
3230
|
+
}
|
|
3231
|
+
for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
|
|
3232
|
+
return out;
|
|
3233
|
+
}
|
|
3234
|
+
function flagNamesOf(token, shape) {
|
|
3235
|
+
if (token.startsWith("--")) {
|
|
3236
|
+
const eq = token.indexOf("=");
|
|
3237
|
+
return [eq > 0 ? token.slice(0, eq) : token];
|
|
3238
|
+
}
|
|
3239
|
+
const out = [];
|
|
3240
|
+
for (const c of token.slice(1)) {
|
|
3241
|
+
const name = `-${c}`;
|
|
3242
|
+
out.push(name);
|
|
3243
|
+
if (shape?.takesValue.has(name)) break;
|
|
3244
|
+
}
|
|
3245
|
+
return out;
|
|
3246
|
+
}
|
|
3247
|
+
var NONE = { kind: "none" };
|
|
3248
|
+
var UNKNOWN = { kind: "unknown" };
|
|
3249
|
+
function flagEffect(token, shape, known) {
|
|
3250
|
+
if (token === "--") return NONE;
|
|
3251
|
+
if (/^-+$/.test(token)) return UNKNOWN;
|
|
3252
|
+
if (/^-\d+$/.test(token)) return NONE;
|
|
3253
|
+
if (token.startsWith("--")) {
|
|
3254
|
+
if (token.includes("=")) return NONE;
|
|
3255
|
+
const takes = namesFlag(token, shape.takesValue, known);
|
|
3256
|
+
const none = namesFlag(token, shape.noValue, known);
|
|
3257
|
+
if (takes && none) return UNKNOWN;
|
|
3258
|
+
if (takes) return { kind: "takes", flag: token };
|
|
3259
|
+
if (none) return NONE;
|
|
3260
|
+
return UNKNOWN;
|
|
3261
|
+
}
|
|
3262
|
+
const letters = token.slice(1);
|
|
3263
|
+
if (!letters) return NONE;
|
|
3264
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3265
|
+
const name = `-${letters[i]}`;
|
|
3266
|
+
if (shape.takesValue.has(name)) {
|
|
3267
|
+
return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
|
|
3268
|
+
}
|
|
3269
|
+
if (!shape.noValue.has(name)) return UNKNOWN;
|
|
3270
|
+
}
|
|
3271
|
+
return NONE;
|
|
3272
|
+
}
|
|
3273
|
+
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
3274
|
+
const shape = PATTERN_VERBS[verb];
|
|
3275
|
+
if (!shape) return args.map((a) => a.value);
|
|
3276
|
+
const known = knownLongFlags(verb);
|
|
3277
|
+
const names = flags.flatMap((f) => flagNamesOf(f, shape));
|
|
3278
|
+
const patternElsewhere = names.some(
|
|
3279
|
+
(n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
|
|
3280
|
+
);
|
|
3281
|
+
const excused = /* @__PURE__ */ new Set();
|
|
3282
|
+
const fileFlags = FILE_OPERAND_FLAGS[verb];
|
|
3283
|
+
const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
|
|
3284
|
+
const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
|
|
3285
|
+
for (const a of args) {
|
|
3286
|
+
if (a.afterFlag === null || pastOptions(a)) continue;
|
|
3287
|
+
const e = flagEffect(a.afterFlag, shape, known);
|
|
3288
|
+
if (e.kind !== "takes") continue;
|
|
3289
|
+
if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
|
|
3290
|
+
excused.add(a);
|
|
3291
|
+
}
|
|
3292
|
+
const patternArgv = (() => {
|
|
3293
|
+
for (let i = from; i < words.length; i++) {
|
|
3294
|
+
const w = words[i];
|
|
3295
|
+
if (w === null) return -1;
|
|
3296
|
+
if (w === "--") return i + 1;
|
|
3297
|
+
if (w.startsWith("-")) {
|
|
3298
|
+
const e = flagEffect(w, shape, known);
|
|
3299
|
+
if (e.kind === "takes") i += 1;
|
|
3300
|
+
else if (e.kind === "unknown") return -1;
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
return i;
|
|
3304
|
+
}
|
|
3305
|
+
return -1;
|
|
3306
|
+
})();
|
|
3307
|
+
if (!patternElsewhere && patternArgv >= 0) {
|
|
3308
|
+
const a = args.find((x) => x.argv === patternArgv);
|
|
3309
|
+
if (a) excused.add(a);
|
|
3310
|
+
}
|
|
3311
|
+
return args.filter((a) => !excused.has(a)).map((a) => a.value);
|
|
3312
|
+
}
|
|
3313
|
+
function flagOperandFiles(verb, words, from) {
|
|
3314
|
+
const flags = FILE_OPERAND_FLAGS[verb];
|
|
3315
|
+
if (!flags) return [];
|
|
3316
|
+
const known = knownLongFlags(verb);
|
|
3317
|
+
const out = [];
|
|
3318
|
+
for (let i = from; i < words.length; i++) {
|
|
3319
|
+
const w = words[i];
|
|
3320
|
+
if (w === null || !w.startsWith("-") || w === "--") continue;
|
|
3321
|
+
if (w.startsWith("--")) {
|
|
3322
|
+
const eq = w.indexOf("=");
|
|
3323
|
+
if (eq <= 0) continue;
|
|
3324
|
+
const name = w.slice(0, eq);
|
|
3325
|
+
const value = w.slice(eq + 1);
|
|
3326
|
+
if (!value) continue;
|
|
3327
|
+
if (namesFlag(name, flags, known)) {
|
|
3328
|
+
out.push(value);
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
const shape2 = PATTERN_VERBS[verb];
|
|
3332
|
+
if (!shape2) continue;
|
|
3333
|
+
const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
|
|
3334
|
+
if (!recognised) out.push(value);
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
const shape = PATTERN_VERBS[verb];
|
|
3338
|
+
const letters = w.slice(1);
|
|
3339
|
+
for (let j = 0; j < letters.length; j++) {
|
|
3340
|
+
const name = `-${letters[j]}`;
|
|
3341
|
+
const isFileFlag = flags.has(name);
|
|
3342
|
+
const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
|
|
3343
|
+
if (!argTaking) continue;
|
|
3344
|
+
const attached = letters.slice(j + 1);
|
|
3345
|
+
if (isFileFlag && attached) out.push(attached);
|
|
3346
|
+
break;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return out;
|
|
3350
|
+
}
|
|
2601
3351
|
function wrappedReadPaths(words, name) {
|
|
2602
3352
|
if (name === "find") {
|
|
2603
|
-
const k = words
|
|
2604
|
-
|
|
2605
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2606
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
3353
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
3354
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2607
3355
|
}
|
|
2608
3356
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2609
3357
|
const h = unwrapCommandHead(words);
|
|
2610
|
-
|
|
3358
|
+
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
3359
|
+
const head = baseWord(words[h]);
|
|
3360
|
+
const rest = words.slice(h + 1);
|
|
3361
|
+
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
3362
|
+
return [
|
|
3363
|
+
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
3364
|
+
...flagOperandFiles(head, words, h + 1)
|
|
3365
|
+
];
|
|
2611
3366
|
}
|
|
2612
3367
|
function literalShellPayload(words, name) {
|
|
2613
3368
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -3464,6 +4219,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3464
4219
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3465
4220
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3466
4221
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
4222
|
+
let pendingAstReview;
|
|
3467
4223
|
if (bashCommand !== null) {
|
|
3468
4224
|
const pipeVerdict = pipeChainVerdict(
|
|
3469
4225
|
bashCommand,
|
|
@@ -3475,7 +4231,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3475
4231
|
if (fsVerdict) {
|
|
3476
4232
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3477
4233
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3478
|
-
|
|
4234
|
+
const astVerdict = {
|
|
3479
4235
|
decision: fsVerdict.verdict,
|
|
3480
4236
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3481
4237
|
reason: fsVerdict.reason,
|
|
@@ -3483,6 +4239,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3483
4239
|
ruleName: fsVerdict.ruleName,
|
|
3484
4240
|
ruleDescription: fsVerdict.reason
|
|
3485
4241
|
};
|
|
4242
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
4243
|
+
pendingAstReview = astVerdict;
|
|
3486
4244
|
}
|
|
3487
4245
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3488
4246
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3525,7 +4283,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3525
4283
|
const matchedRule = resolvePinned(matches);
|
|
3526
4284
|
if (matchedRule) {
|
|
3527
4285
|
if (matchedRule.verdict === "allow")
|
|
3528
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
4286
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3529
4287
|
return {
|
|
3530
4288
|
decision: matchedRule.verdict,
|
|
3531
4289
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3552,6 +4310,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3552
4310
|
allTokens = analyzed.allTokens;
|
|
3553
4311
|
pathTokens = analyzed.paths;
|
|
3554
4312
|
const candidates = [];
|
|
4313
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3555
4314
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3556
4315
|
if (evalVerdict === "block") {
|
|
3557
4316
|
return {
|