@node9/policy-engine 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/index.d.mts +95 -3
- package/dist/index.d.ts +95 -3
- package/dist/index.js +832 -30
- package/dist/index.mjs +826 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ __export(src_exports, {
|
|
|
37
37
|
CANONICAL_EXTRACTOR_HASH: () => CANONICAL_EXTRACTOR_HASH,
|
|
38
38
|
CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
|
|
39
39
|
COMMAND_WRAPPERS: () => COMMAND_WRAPPERS,
|
|
40
|
+
COPY_VERBS: () => COPY_VERBS,
|
|
40
41
|
COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
|
|
41
42
|
DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
|
|
42
43
|
DESTINATION_ARGS: () => DESTINATION_ARGS,
|
|
@@ -51,6 +52,7 @@ __export(src_exports, {
|
|
|
51
52
|
LOOP_MAX_RECORDS: () => LOOP_MAX_RECORDS,
|
|
52
53
|
LOOP_THRESHOLD_FOR_WASTE: () => LOOP_THRESHOLD_FOR_WASTE,
|
|
53
54
|
NET_BINARIES: () => NET_BINARIES,
|
|
55
|
+
PATTERN_VERB_NAMES: () => PATTERN_VERB_NAMES,
|
|
54
56
|
PRIVILEGE_ESCALATION_RE: () => PRIVILEGE_ESCALATION_RE,
|
|
55
57
|
REALTIME_PII_PATTERNS: () => REALTIME_PII_PATTERNS,
|
|
56
58
|
SCAN_SIGNAL_WEIGHTS: () => SCAN_SIGNAL_WEIGHTS,
|
|
@@ -87,6 +89,7 @@ __export(src_exports, {
|
|
|
87
89
|
extractSessionLevelFindings: () => extractSessionLevelFindings,
|
|
88
90
|
extractShellDestTokens: () => extractShellDestTokens,
|
|
89
91
|
extractShellDestinations: () => extractShellDestinations,
|
|
92
|
+
fileOperandFlagsOf: () => fileOperandFlagsOf,
|
|
90
93
|
getCompiledRegex: () => getCompiledRegex,
|
|
91
94
|
getNestedValue: () => getNestedValue,
|
|
92
95
|
hostMatches: () => hostMatches,
|
|
@@ -106,9 +109,12 @@ __export(src_exports, {
|
|
|
106
109
|
normalizeIpLiteral: () => normalizeIpLiteral,
|
|
107
110
|
parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
|
|
108
111
|
parseDestHost: () => parseDestHost,
|
|
112
|
+
patternShapeOf: () => patternShapeOf,
|
|
113
|
+
positionedArgs: () => positionedArgs,
|
|
109
114
|
previewArgs: () => previewArgs,
|
|
110
115
|
redactText: () => redactText,
|
|
111
116
|
resolvePinned: () => resolvePinned,
|
|
117
|
+
sampleCopyCommand: () => sampleCopyCommand,
|
|
112
118
|
scanArgs: () => scanArgs,
|
|
113
119
|
scanInjection: () => scanInjection,
|
|
114
120
|
scanText: () => scanText,
|
|
@@ -1251,6 +1257,19 @@ function parseShared(command) {
|
|
|
1251
1257
|
astCache.set(command, parsed);
|
|
1252
1258
|
return parsed;
|
|
1253
1259
|
}
|
|
1260
|
+
function byteOffsetToCharIndex(command) {
|
|
1261
|
+
if (!/[^\u0000-\u007F]/.test(command)) return null;
|
|
1262
|
+
const map = /* @__PURE__ */ new Map();
|
|
1263
|
+
let byte = 0;
|
|
1264
|
+
for (let i = 0; i < command.length; ) {
|
|
1265
|
+
map.set(byte, i);
|
|
1266
|
+
const cp = command.codePointAt(i);
|
|
1267
|
+
byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
1268
|
+
i += cp > 65535 ? 2 : 1;
|
|
1269
|
+
}
|
|
1270
|
+
map.set(byte, command.length);
|
|
1271
|
+
return (b) => map.get(b) ?? -1;
|
|
1272
|
+
}
|
|
1254
1273
|
function cachedNormalize(command, compute) {
|
|
1255
1274
|
const hit = normalizeCache.get(command);
|
|
1256
1275
|
if (hit !== void 0) {
|
|
@@ -1280,6 +1299,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1280
1299
|
const f = parseShared(command);
|
|
1281
1300
|
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1282
1301
|
try {
|
|
1302
|
+
const toCharIndex = byteOffsetToCharIndex(command);
|
|
1303
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
1283
1304
|
const strips = [];
|
|
1284
1305
|
const rewrites = [];
|
|
1285
1306
|
const quoteOnlyRewrites = [];
|
|
@@ -1300,8 +1321,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1300
1321
|
const quotedNode = nextParts[0];
|
|
1301
1322
|
const nt = syntax.NodeType(quotedNode);
|
|
1302
1323
|
const markStrip = () => {
|
|
1303
|
-
const s = next.Pos().Offset();
|
|
1304
|
-
const e = next.End().Offset();
|
|
1324
|
+
const s = at(next.Pos().Offset());
|
|
1325
|
+
const e = at(next.End().Offset());
|
|
1326
|
+
if (s < 0 || e < 0) return;
|
|
1305
1327
|
strips.push([s, e]);
|
|
1306
1328
|
msgSpans.add(`${s}:${e}`);
|
|
1307
1329
|
};
|
|
@@ -1318,8 +1340,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1318
1340
|
}
|
|
1319
1341
|
}
|
|
1320
1342
|
for (const arg of args) {
|
|
1321
|
-
const s = arg.Pos().Offset();
|
|
1322
|
-
const e = arg.End().Offset();
|
|
1343
|
+
const s = at(arg.Pos().Offset());
|
|
1344
|
+
const e = at(arg.End().Offset());
|
|
1345
|
+
if (s < 0 || e < 0) continue;
|
|
1323
1346
|
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1324
1347
|
const resolved = resolveWordLiteral(arg);
|
|
1325
1348
|
if (resolved === null) continue;
|
|
@@ -1442,12 +1465,410 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1442
1465
|
"nl",
|
|
1443
1466
|
"dd"
|
|
1444
1467
|
]);
|
|
1468
|
+
var GREP_SHAPE = {
|
|
1469
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1470
|
+
"-A",
|
|
1471
|
+
"-B",
|
|
1472
|
+
"-C",
|
|
1473
|
+
"-D",
|
|
1474
|
+
"-d",
|
|
1475
|
+
"-e",
|
|
1476
|
+
"-f",
|
|
1477
|
+
"-m",
|
|
1478
|
+
"--after-context",
|
|
1479
|
+
"--before-context",
|
|
1480
|
+
"--binary-files",
|
|
1481
|
+
"--context",
|
|
1482
|
+
"--devices",
|
|
1483
|
+
"--directories",
|
|
1484
|
+
"--exclude",
|
|
1485
|
+
"--exclude-dir",
|
|
1486
|
+
"--exclude-from",
|
|
1487
|
+
"--file",
|
|
1488
|
+
"--include",
|
|
1489
|
+
"--label",
|
|
1490
|
+
"--max-count",
|
|
1491
|
+
"--regexp"
|
|
1492
|
+
]),
|
|
1493
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1494
|
+
"-E",
|
|
1495
|
+
"-F",
|
|
1496
|
+
"-G",
|
|
1497
|
+
"-P",
|
|
1498
|
+
"-i",
|
|
1499
|
+
"-y",
|
|
1500
|
+
"-v",
|
|
1501
|
+
"-V",
|
|
1502
|
+
"-w",
|
|
1503
|
+
"-x",
|
|
1504
|
+
"-c",
|
|
1505
|
+
"-l",
|
|
1506
|
+
"-L",
|
|
1507
|
+
"-o",
|
|
1508
|
+
"-q",
|
|
1509
|
+
"-s",
|
|
1510
|
+
"-b",
|
|
1511
|
+
"-H",
|
|
1512
|
+
"-h",
|
|
1513
|
+
"-n",
|
|
1514
|
+
"-T",
|
|
1515
|
+
"-Z",
|
|
1516
|
+
"-z",
|
|
1517
|
+
"-R",
|
|
1518
|
+
"-r",
|
|
1519
|
+
"-U",
|
|
1520
|
+
"-u",
|
|
1521
|
+
"-I",
|
|
1522
|
+
"-a",
|
|
1523
|
+
"--basic-regexp",
|
|
1524
|
+
"--binary",
|
|
1525
|
+
"--byte-offset",
|
|
1526
|
+
"--color",
|
|
1527
|
+
"--colour",
|
|
1528
|
+
"--count",
|
|
1529
|
+
"--dereference-recursive",
|
|
1530
|
+
"--extended-regexp",
|
|
1531
|
+
"--files-with-matches",
|
|
1532
|
+
"--files-without-match",
|
|
1533
|
+
"--fixed-strings",
|
|
1534
|
+
"--help",
|
|
1535
|
+
"--ignore-case",
|
|
1536
|
+
"--initial-tab",
|
|
1537
|
+
"--invert-match",
|
|
1538
|
+
"--line-buffered",
|
|
1539
|
+
"--line-number",
|
|
1540
|
+
"--line-regexp",
|
|
1541
|
+
"--no-filename",
|
|
1542
|
+
"--no-group-separator",
|
|
1543
|
+
"--no-ignore-case",
|
|
1544
|
+
"--no-messages",
|
|
1545
|
+
"--null",
|
|
1546
|
+
"--null-data",
|
|
1547
|
+
"--only-matching",
|
|
1548
|
+
"--perl-regexp",
|
|
1549
|
+
"--quiet",
|
|
1550
|
+
"--recursive",
|
|
1551
|
+
"--silent",
|
|
1552
|
+
"--text",
|
|
1553
|
+
"--version",
|
|
1554
|
+
"--with-filename",
|
|
1555
|
+
"--word-regexp"
|
|
1556
|
+
]),
|
|
1557
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1558
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
|
|
1559
|
+
};
|
|
1560
|
+
var PATTERN_VERBS = {
|
|
1561
|
+
grep: GREP_SHAPE,
|
|
1562
|
+
// /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
|
|
1563
|
+
// `grep -E` and `grep -F`. Read in full, not assumed.
|
|
1564
|
+
egrep: GREP_SHAPE,
|
|
1565
|
+
fgrep: GREP_SHAPE,
|
|
1566
|
+
// ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
|
|
1567
|
+
// was not installed on the measuring machine; it is, and that stale claim is why
|
|
1568
|
+
// this table shipped incomplete for two rounds, blocking ordinary searches like
|
|
1569
|
+
// `rg --sort-files .env src` (/code-review round 4).
|
|
1570
|
+
rg: {
|
|
1571
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1572
|
+
"-A",
|
|
1573
|
+
"-B",
|
|
1574
|
+
"-C",
|
|
1575
|
+
"-d",
|
|
1576
|
+
"-E",
|
|
1577
|
+
"-e",
|
|
1578
|
+
"-f",
|
|
1579
|
+
"-g",
|
|
1580
|
+
"-j",
|
|
1581
|
+
"-M",
|
|
1582
|
+
"-m",
|
|
1583
|
+
"-r",
|
|
1584
|
+
"-t",
|
|
1585
|
+
"-T",
|
|
1586
|
+
"--after-context",
|
|
1587
|
+
"--before-context",
|
|
1588
|
+
"--color",
|
|
1589
|
+
"--colors",
|
|
1590
|
+
"--context",
|
|
1591
|
+
"--context-separator",
|
|
1592
|
+
"--dfa-size-limit",
|
|
1593
|
+
"--encoding",
|
|
1594
|
+
"--engine",
|
|
1595
|
+
"--field-context-separator",
|
|
1596
|
+
"--field-match-separator",
|
|
1597
|
+
"--file",
|
|
1598
|
+
"--generate",
|
|
1599
|
+
"--glob",
|
|
1600
|
+
"--hostname-bin",
|
|
1601
|
+
"--hyperlink-format",
|
|
1602
|
+
"--iglob",
|
|
1603
|
+
"--ignore-file",
|
|
1604
|
+
"--max-columns",
|
|
1605
|
+
"--max-count",
|
|
1606
|
+
"--max-depth",
|
|
1607
|
+
// An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
|
|
1608
|
+
// alias lines as plain switches and swept it into noValue, which hard-blocked
|
|
1609
|
+
// `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
|
|
1610
|
+
"--maxdepth",
|
|
1611
|
+
"--max-filesize",
|
|
1612
|
+
"--path-separator",
|
|
1613
|
+
"--pre",
|
|
1614
|
+
"--pre-glob",
|
|
1615
|
+
"--regexp",
|
|
1616
|
+
"--regex-size-limit",
|
|
1617
|
+
"--replace",
|
|
1618
|
+
"--sort",
|
|
1619
|
+
"--sortr",
|
|
1620
|
+
"--threads",
|
|
1621
|
+
"--type",
|
|
1622
|
+
"--type-add",
|
|
1623
|
+
"--type-clear",
|
|
1624
|
+
"--type-not"
|
|
1625
|
+
]),
|
|
1626
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1627
|
+
"-.",
|
|
1628
|
+
"-0",
|
|
1629
|
+
"-a",
|
|
1630
|
+
"-b",
|
|
1631
|
+
"-c",
|
|
1632
|
+
"-F",
|
|
1633
|
+
"-h",
|
|
1634
|
+
"-H",
|
|
1635
|
+
"-i",
|
|
1636
|
+
"-I",
|
|
1637
|
+
"-l",
|
|
1638
|
+
"-L",
|
|
1639
|
+
"-n",
|
|
1640
|
+
"-N",
|
|
1641
|
+
"-o",
|
|
1642
|
+
"-p",
|
|
1643
|
+
"-P",
|
|
1644
|
+
"-q",
|
|
1645
|
+
"-s",
|
|
1646
|
+
"-S",
|
|
1647
|
+
"-u",
|
|
1648
|
+
"-U",
|
|
1649
|
+
"-v",
|
|
1650
|
+
"-V",
|
|
1651
|
+
"-w",
|
|
1652
|
+
"-x",
|
|
1653
|
+
"-z",
|
|
1654
|
+
"--auto-hybrid-regex",
|
|
1655
|
+
"--binary",
|
|
1656
|
+
"--block-buffered",
|
|
1657
|
+
"--byte-offset",
|
|
1658
|
+
"--case-sensitive",
|
|
1659
|
+
"--column",
|
|
1660
|
+
"--count",
|
|
1661
|
+
"--count-matches",
|
|
1662
|
+
"--crlf",
|
|
1663
|
+
"--debug",
|
|
1664
|
+
"--files",
|
|
1665
|
+
"--files-with-matches",
|
|
1666
|
+
"--files-without-match",
|
|
1667
|
+
"--fixed-strings",
|
|
1668
|
+
"--follow",
|
|
1669
|
+
"--glob-case-insensitive",
|
|
1670
|
+
"--heading",
|
|
1671
|
+
"--help",
|
|
1672
|
+
"--hidden",
|
|
1673
|
+
"--ignore-case",
|
|
1674
|
+
"--ignore-file-case-insensitive",
|
|
1675
|
+
"--include-zero",
|
|
1676
|
+
"--invert-match",
|
|
1677
|
+
"--json",
|
|
1678
|
+
"--line-buffered",
|
|
1679
|
+
"--line-number",
|
|
1680
|
+
"--line-regexp",
|
|
1681
|
+
"--max-columns-preview",
|
|
1682
|
+
"--mmap",
|
|
1683
|
+
"--multiline",
|
|
1684
|
+
"--multiline-dotall",
|
|
1685
|
+
"--no-column",
|
|
1686
|
+
"--no-config",
|
|
1687
|
+
"--no-context-separator",
|
|
1688
|
+
"--no-encoding",
|
|
1689
|
+
"--no-filename",
|
|
1690
|
+
"--no-ignore",
|
|
1691
|
+
"--no-ignore-dot",
|
|
1692
|
+
"--no-ignore-exclude",
|
|
1693
|
+
"--no-ignore-files",
|
|
1694
|
+
"--no-ignore-global",
|
|
1695
|
+
"--no-ignore-messages",
|
|
1696
|
+
"--no-ignore-parent",
|
|
1697
|
+
"--no-ignore-vcs",
|
|
1698
|
+
"--no-line-number",
|
|
1699
|
+
"--no-messages",
|
|
1700
|
+
"--no-pcre2-unicode",
|
|
1701
|
+
"--no-pre",
|
|
1702
|
+
"--no-require-git",
|
|
1703
|
+
"--no-unicode",
|
|
1704
|
+
"--null",
|
|
1705
|
+
"--null-data",
|
|
1706
|
+
"--one-file-system",
|
|
1707
|
+
"--only-matching",
|
|
1708
|
+
"--passthru",
|
|
1709
|
+
"--pcre2",
|
|
1710
|
+
"--pcre2-version",
|
|
1711
|
+
"--pretty",
|
|
1712
|
+
"--print0",
|
|
1713
|
+
"--quiet",
|
|
1714
|
+
"--search-zip",
|
|
1715
|
+
"--smart-case",
|
|
1716
|
+
"--sort-files",
|
|
1717
|
+
"--stats",
|
|
1718
|
+
"--stop-on-nonmatch",
|
|
1719
|
+
"--text",
|
|
1720
|
+
"--trace",
|
|
1721
|
+
"--trim",
|
|
1722
|
+
"--type-list",
|
|
1723
|
+
"--unrestricted",
|
|
1724
|
+
"--version",
|
|
1725
|
+
"--vimgrep",
|
|
1726
|
+
"--with-filename",
|
|
1727
|
+
"--word-regexp",
|
|
1728
|
+
// The COMPLETE remainder of `rg --help`, added after /code-review round 5
|
|
1729
|
+
// found 40 documented switches in neither set. The table is now the whole
|
|
1730
|
+
// option list: `rg --help` yields 149 long options, 35 of them value-taking.
|
|
1731
|
+
"--ignore",
|
|
1732
|
+
"--ignore-dot",
|
|
1733
|
+
"--ignore-exclude",
|
|
1734
|
+
"--ignore-files",
|
|
1735
|
+
"--ignore-global",
|
|
1736
|
+
"--ignore-messages",
|
|
1737
|
+
"--ignore-parent",
|
|
1738
|
+
"--ignore-vcs",
|
|
1739
|
+
"--messages",
|
|
1740
|
+
"--no-auto-hybrid-regex",
|
|
1741
|
+
"--no-binary",
|
|
1742
|
+
"--no-block-buffered",
|
|
1743
|
+
"--no-byte-offset",
|
|
1744
|
+
"--no-crlf",
|
|
1745
|
+
"--no-fixed-strings",
|
|
1746
|
+
"--no-follow",
|
|
1747
|
+
"--no-glob-case-insensitive",
|
|
1748
|
+
"--no-heading",
|
|
1749
|
+
"--no-hidden",
|
|
1750
|
+
"--no-ignore-file-case-insensitive",
|
|
1751
|
+
"--no-include-zero",
|
|
1752
|
+
"--no-invert-match",
|
|
1753
|
+
"--no-json",
|
|
1754
|
+
"--no-line-buffered",
|
|
1755
|
+
"--no-max-columns-preview",
|
|
1756
|
+
"--no-mmap",
|
|
1757
|
+
"--no-multiline",
|
|
1758
|
+
"--no-multiline-dotall",
|
|
1759
|
+
"--no-one-file-system",
|
|
1760
|
+
"--no-pcre2",
|
|
1761
|
+
"--no-search-zip",
|
|
1762
|
+
"--no-sort-files",
|
|
1763
|
+
"--no-stats",
|
|
1764
|
+
"--no-text",
|
|
1765
|
+
"--no-trim",
|
|
1766
|
+
"--passthrough",
|
|
1767
|
+
"--pcre2-unicode",
|
|
1768
|
+
"--require-git",
|
|
1769
|
+
"--unicode"
|
|
1770
|
+
]),
|
|
1771
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1772
|
+
// `--files` and `--type-list` list or enumerate without a pattern. Founder
|
|
1773
|
+
// decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
|
|
1774
|
+
// by leaving the directory in the judged list.
|
|
1775
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
|
|
1779
|
+
var patternShapeOf = (verb) => PATTERN_VERBS[verb];
|
|
1780
|
+
var fileOperandFlagsOf = (verb) => FILE_OPERAND_FLAGS[verb];
|
|
1781
|
+
var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
|
|
1782
|
+
var READER_VALUE_LETTERS = {
|
|
1783
|
+
awk: ["F", "v", "f"],
|
|
1784
|
+
gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
|
|
1785
|
+
sed: ["e", "f", "i", "l"],
|
|
1786
|
+
sort: ["C", "k", "o", "S", "t", "T"]
|
|
1787
|
+
};
|
|
1788
|
+
var FILE_OPERAND_FLAGS = {
|
|
1789
|
+
grep: GREP_FILE_OPERANDS,
|
|
1790
|
+
egrep: GREP_FILE_OPERANDS,
|
|
1791
|
+
fgrep: GREP_FILE_OPERANDS,
|
|
1792
|
+
// rg and gawk are NOT installed on the measuring machine: these two rows are
|
|
1793
|
+
// from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
|
|
1794
|
+
// `-f/--file`) and are marked as such in the spec.
|
|
1795
|
+
// `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
|
|
1796
|
+
// a credential makes rg open it -- the same reason grep's `--include` is here.
|
|
1797
|
+
// Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
|
|
1798
|
+
// and printed its contents.
|
|
1799
|
+
rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
|
|
1800
|
+
sed: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1801
|
+
awk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1802
|
+
gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1803
|
+
sort: /* @__PURE__ */ new Set(["--files0-from"])
|
|
1804
|
+
};
|
|
1805
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
1806
|
+
var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
|
|
1807
|
+
var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
|
|
1808
|
+
var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
|
|
1809
|
+
var RSYNC_SKIP = [
|
|
1810
|
+
"e",
|
|
1811
|
+
"--rsh",
|
|
1812
|
+
"--exclude",
|
|
1813
|
+
"--exclude-from",
|
|
1814
|
+
"--include",
|
|
1815
|
+
"--include-from",
|
|
1816
|
+
"--files-from",
|
|
1817
|
+
"f",
|
|
1818
|
+
"--filter"
|
|
1819
|
+
];
|
|
1820
|
+
var CP_VALUE_LETTERS = ["S", "t"];
|
|
1821
|
+
var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
|
|
1822
|
+
var COPY_VERBS = {
|
|
1823
|
+
cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1824
|
+
mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1825
|
+
install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
|
|
1826
|
+
ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1827
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
|
|
1828
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
|
|
1829
|
+
tar: {
|
|
1830
|
+
source: "archive",
|
|
1831
|
+
archive: "tar",
|
|
1832
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
|
|
1833
|
+
valueLetters: TAR_VALUE_LETTERS
|
|
1834
|
+
},
|
|
1835
|
+
zip: {
|
|
1836
|
+
source: "archive",
|
|
1837
|
+
archive: "zip",
|
|
1838
|
+
skipFlags: ["x", "i", "--exclude", "--include"],
|
|
1839
|
+
valueLetters: ZIP_VALUE_LETTERS
|
|
1840
|
+
},
|
|
1841
|
+
ar: { source: "archive", archive: "ar" },
|
|
1842
|
+
// 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
|
|
1843
|
+
// ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
|
|
1844
|
+
// and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
|
|
1845
|
+
// drop the credential as an exclusion operand (/code-review round 8), and the
|
|
1846
|
+
// derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
|
|
1847
|
+
// what keeps the two tables honest about it.
|
|
1848
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
|
|
1849
|
+
gzip: { source: "all" },
|
|
1850
|
+
bzip2: { source: "all" },
|
|
1851
|
+
xz: { source: "all" },
|
|
1852
|
+
"docker cp": { source: "allButLast" },
|
|
1853
|
+
"kubectl cp": { source: "allButLast" },
|
|
1854
|
+
"gsutil cp": { source: "allButLast" },
|
|
1855
|
+
"gsutil rsync": { source: "allButLast" },
|
|
1856
|
+
"rclone copy": { source: "allButLast" },
|
|
1857
|
+
"rclone sync": { source: "allButLast" },
|
|
1858
|
+
"aws s3 cp": { source: "allButLast" },
|
|
1859
|
+
"aws s3 mv": { source: "allButLast" },
|
|
1860
|
+
"aws s3 sync": { source: "allButLast" },
|
|
1861
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
1862
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
1863
|
+
};
|
|
1864
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
1865
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1445
1866
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1446
1867
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1447
1868
|
// reader right after `"` / `'`, and without these two characters the
|
|
1448
1869
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1449
1870
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1450
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1871
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1451
1872
|
);
|
|
1452
1873
|
var HOME_CACHE_ALLOWLIST = [
|
|
1453
1874
|
".cache",
|
|
@@ -1882,20 +2303,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
1882
2303
|
}
|
|
1883
2304
|
return true;
|
|
1884
2305
|
}
|
|
1885
|
-
function
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
1890
|
-
const flags = [];
|
|
1891
|
-
const paths = [];
|
|
1892
|
-
for (let i = 1; i < words.length; i++) {
|
|
2306
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2307
|
+
const out = [];
|
|
2308
|
+
let afterFlag = null;
|
|
2309
|
+
for (let i = from; i < to; i++) {
|
|
1893
2310
|
const v = words[i];
|
|
1894
|
-
if (v === null)
|
|
1895
|
-
|
|
1896
|
-
|
|
2311
|
+
if (v === null) {
|
|
2312
|
+
afterFlag = null;
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (v.startsWith("-")) {
|
|
2316
|
+
afterFlag = v;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2320
|
+
afterFlag = null;
|
|
1897
2321
|
}
|
|
1898
|
-
return
|
|
2322
|
+
return out;
|
|
2323
|
+
}
|
|
2324
|
+
function extractLiteralArgs(callExpr) {
|
|
2325
|
+
const rawArgs = callExpr.Args || [];
|
|
2326
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
2327
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
2328
|
+
const name = baseWord(words[0]);
|
|
2329
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
2330
|
+
const args = positionedArgs(words);
|
|
2331
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
1899
2332
|
}
|
|
1900
2333
|
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
1901
2334
|
"curl",
|
|
@@ -2272,7 +2705,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2272
2705
|
return result?.verdict !== "block";
|
|
2273
2706
|
}
|
|
2274
2707
|
if (nodeType !== "CallExpr") return true;
|
|
2275
|
-
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2708
|
+
const { name, flags, paths, words, args } = extractLiteralArgs(n);
|
|
2276
2709
|
if (!name) return true;
|
|
2277
2710
|
if (name === "rm") {
|
|
2278
2711
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2312,13 +2745,16 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2312
2745
|
return true;
|
|
2313
2746
|
}
|
|
2314
2747
|
}
|
|
2315
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
2748
|
+
const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
|
|
2316
2749
|
if (readPaths) {
|
|
2317
2750
|
for (const p of readPaths) {
|
|
2318
2751
|
result = stricter(result, matchSensitivePath2(p));
|
|
2319
2752
|
if (result?.verdict === "block") return false;
|
|
2320
2753
|
}
|
|
2321
2754
|
}
|
|
2755
|
+
for (const p of copySourcePaths(words)) {
|
|
2756
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2757
|
+
}
|
|
2322
2758
|
return true;
|
|
2323
2759
|
});
|
|
2324
2760
|
return result;
|
|
@@ -2331,6 +2767,208 @@ function stricter(a, b) {
|
|
|
2331
2767
|
if (!b) return a;
|
|
2332
2768
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2333
2769
|
}
|
|
2770
|
+
function flagInfo(w) {
|
|
2771
|
+
if (w.startsWith("--")) {
|
|
2772
|
+
const eq = w.indexOf("=");
|
|
2773
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
2774
|
+
}
|
|
2775
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
2776
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
2777
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
2778
|
+
}
|
|
2779
|
+
function flagIs(w, names) {
|
|
2780
|
+
if (w === null) return false;
|
|
2781
|
+
const f = flagInfo(w);
|
|
2782
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
2783
|
+
}
|
|
2784
|
+
function operandOf(a, names, valueLetters) {
|
|
2785
|
+
if (!names || a.afterFlag === null) return false;
|
|
2786
|
+
const w = a.afterFlag;
|
|
2787
|
+
if (!w.startsWith("--") && valueLetters) {
|
|
2788
|
+
const letters = w.slice(1);
|
|
2789
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2790
|
+
if (!valueLetters.includes(letters[i])) continue;
|
|
2791
|
+
return i === letters.length - 1 && names.includes(letters[i]);
|
|
2792
|
+
}
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2795
|
+
if (w.startsWith("--") && !w.includes("=")) {
|
|
2796
|
+
const longs = names.filter((n) => n.startsWith("--"));
|
|
2797
|
+
if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
|
|
2798
|
+
}
|
|
2799
|
+
return flagIs(w, names) && flagInfo(w).attached === null;
|
|
2800
|
+
}
|
|
2801
|
+
function resolveCopyShape(words, h) {
|
|
2802
|
+
const verb = baseWord(words[h]);
|
|
2803
|
+
if (!verb) return null;
|
|
2804
|
+
const direct = COPY_VERBS[verb];
|
|
2805
|
+
if (direct) return { shape: direct, last: h };
|
|
2806
|
+
const slots = positionedArgs(words, h + 1);
|
|
2807
|
+
for (let i = 0; i < slots.length; i++) {
|
|
2808
|
+
for (let n = 3; n >= 1; n--) {
|
|
2809
|
+
const part = slots.slice(i, i + n);
|
|
2810
|
+
if (part.length < n) continue;
|
|
2811
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
2812
|
+
const shape = COPY_VERBS[key];
|
|
2813
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
2814
|
+
}
|
|
2815
|
+
if (slots[i].afterFlag === null) return null;
|
|
2816
|
+
}
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
2820
|
+
function findStartPoints(words, h) {
|
|
2821
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2822
|
+
if (k < 0) return { k, starts: [] };
|
|
2823
|
+
const firstPredicate = words.findIndex(
|
|
2824
|
+
(w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2825
|
+
);
|
|
2826
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
2827
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
2828
|
+
}
|
|
2829
|
+
function copySourcePaths(words) {
|
|
2830
|
+
const h = unwrapCommandHead(words);
|
|
2831
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
2832
|
+
if (fi >= 0) {
|
|
2833
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
2834
|
+
if (k < 0) return [];
|
|
2835
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
2836
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
2837
|
+
}
|
|
2838
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2839
|
+
const r = resolveCopyShape(words, h);
|
|
2840
|
+
if (!r) return [];
|
|
2841
|
+
const { shape, last } = r;
|
|
2842
|
+
const args = positionedArgs(words, last + 1);
|
|
2843
|
+
const tail = words.slice(last + 1);
|
|
2844
|
+
const copyOptionsEnd = tail.findIndex((w) => w === "--");
|
|
2845
|
+
const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
|
|
2846
|
+
const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
|
|
2847
|
+
const firstValueLetter = (w) => {
|
|
2848
|
+
const letters = w.slice(1);
|
|
2849
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2850
|
+
if ((shape.valueLetters ?? []).includes(letters[i]))
|
|
2851
|
+
return { letter: letters[i], last: i === letters.length - 1 };
|
|
2852
|
+
}
|
|
2853
|
+
return null;
|
|
2854
|
+
};
|
|
2855
|
+
const isTargetDirFlag = (w) => {
|
|
2856
|
+
if (w.startsWith("--")) {
|
|
2857
|
+
const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
|
|
2858
|
+
return name.length >= 3 && "--target-directory".startsWith(name);
|
|
2859
|
+
}
|
|
2860
|
+
return firstValueLetter(w)?.letter === "t";
|
|
2861
|
+
};
|
|
2862
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
|
|
2863
|
+
const targetTakesNextWord = (w) => {
|
|
2864
|
+
if (w.startsWith("--"))
|
|
2865
|
+
return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
|
|
2866
|
+
const f = firstValueLetter(w);
|
|
2867
|
+
return f !== null && f.letter === "t" && f.last;
|
|
2868
|
+
};
|
|
2869
|
+
const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
|
|
2870
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
2871
|
+
const dynamicDest = lastOperand === null;
|
|
2872
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
2873
|
+
let src;
|
|
2874
|
+
switch (shape.source) {
|
|
2875
|
+
case "all":
|
|
2876
|
+
src = args;
|
|
2877
|
+
break;
|
|
2878
|
+
case "first":
|
|
2879
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
2880
|
+
break;
|
|
2881
|
+
case "flagOperand": {
|
|
2882
|
+
const namesSource = (f) => {
|
|
2883
|
+
const names = shape.sourceFlags ?? [];
|
|
2884
|
+
if (f.long !== null)
|
|
2885
|
+
return names.some(
|
|
2886
|
+
(n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
|
|
2887
|
+
);
|
|
2888
|
+
return f.letter !== null && names.includes(f.letter);
|
|
2889
|
+
};
|
|
2890
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
|
|
2891
|
+
return [
|
|
2892
|
+
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
|
|
2893
|
+
...inline
|
|
2894
|
+
];
|
|
2895
|
+
}
|
|
2896
|
+
case "archive":
|
|
2897
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
2898
|
+
break;
|
|
2899
|
+
case "allButLast":
|
|
2900
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
2901
|
+
break;
|
|
2902
|
+
}
|
|
2903
|
+
const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
2904
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
|
|
2905
|
+
const jailedSources = sources.filter((p) => matchSensitivePath2(p));
|
|
2906
|
+
const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
|
|
2907
|
+
if (jailedSources.length === 0) return [];
|
|
2908
|
+
if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
|
|
2909
|
+
}
|
|
2910
|
+
return sources;
|
|
2911
|
+
}
|
|
2912
|
+
function archiveInputs(kind, args, tail) {
|
|
2913
|
+
const first = args[0];
|
|
2914
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
2915
|
+
if (kind === "tar") {
|
|
2916
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
2917
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
2918
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
2919
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
2920
|
+
if (extracting && !writing) return [];
|
|
2921
|
+
void mode;
|
|
2922
|
+
if (!bareKey) return args;
|
|
2923
|
+
let i = 1;
|
|
2924
|
+
const fromDirs = [];
|
|
2925
|
+
for (const ch of first.value) {
|
|
2926
|
+
if (!TAR_VALUE_LETTERS.includes(ch)) continue;
|
|
2927
|
+
const operand = args[i];
|
|
2928
|
+
if (!operand || operand.afterFlag !== null) break;
|
|
2929
|
+
i += 1;
|
|
2930
|
+
if (ch === "C") fromDirs.push(operand);
|
|
2931
|
+
}
|
|
2932
|
+
return [...fromDirs, ...args.slice(i)];
|
|
2933
|
+
}
|
|
2934
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
2935
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
2936
|
+
return args.slice(2);
|
|
2937
|
+
}
|
|
2938
|
+
function sampleCopyCommand(verb, src) {
|
|
2939
|
+
const shape = COPY_VERBS[verb];
|
|
2940
|
+
if (!shape) throw new Error(`not a copy verb: ${verb}`);
|
|
2941
|
+
const remote = /^(scp|rsync|aws |gsutil|gcloud|rclone|docker|kubectl)/.test(verb);
|
|
2942
|
+
switch (shape.source) {
|
|
2943
|
+
case "first":
|
|
2944
|
+
return `${verb} -s ${src} /tmp/n9-link`;
|
|
2945
|
+
case "all":
|
|
2946
|
+
return `${verb} -c ${src} > /tmp/n9-out`;
|
|
2947
|
+
case "flagOperand":
|
|
2948
|
+
return `${verb} -f ${src} -c n9`;
|
|
2949
|
+
case "archive":
|
|
2950
|
+
return shape.archive === "tar" ? `tar czf /tmp/n9-out.tgz ${src}` : shape.archive === "zip" ? `zip -r /tmp/n9-out.zip ${src}` : shape.archive === "ar" ? `ar rc /tmp/n9-out.a ${src}` : `7z a /tmp/n9-out.7z ${src}`;
|
|
2951
|
+
case "allButLast":
|
|
2952
|
+
return `${verb} ${src} ${remote ? verb.startsWith("scp") || verb === "rsync" ? "user@host.invalid:/tmp/" : verb.startsWith("docker") || verb.startsWith("kubectl") ? "ctr:/tmp/" : "remote:/n9/" : "/tmp/n9-copy"}`;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
var COPY_RULE_OF = {
|
|
2956
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
2957
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
2958
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
2959
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
2960
|
+
};
|
|
2961
|
+
function copyVerdictOf(hit) {
|
|
2962
|
+
if (!hit) return null;
|
|
2963
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
2964
|
+
if (!ruleName) return null;
|
|
2965
|
+
return {
|
|
2966
|
+
ruleName,
|
|
2967
|
+
verdict: "review",
|
|
2968
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
2969
|
+
path: hit.path
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2334
2972
|
function matchSensitivePath2(p) {
|
|
2335
2973
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2336
2974
|
if (sp.match(p))
|
|
@@ -2338,18 +2976,160 @@ function matchSensitivePath2(p) {
|
|
|
2338
2976
|
}
|
|
2339
2977
|
return null;
|
|
2340
2978
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2979
|
+
function baseWord(w) {
|
|
2980
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2981
|
+
}
|
|
2982
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
2983
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
2984
|
+
function namesFlag(name, candidates, known) {
|
|
2985
|
+
if (candidates.has(name)) return true;
|
|
2986
|
+
if (!name.startsWith("--") || name.length < 3) return false;
|
|
2987
|
+
if (known?.has(name)) return false;
|
|
2988
|
+
for (const c of candidates) if (c.startsWith(name)) return true;
|
|
2989
|
+
return false;
|
|
2990
|
+
}
|
|
2991
|
+
function knownLongFlags(verb) {
|
|
2992
|
+
const out = /* @__PURE__ */ new Set();
|
|
2993
|
+
const shape = PATTERN_VERBS[verb];
|
|
2994
|
+
if (shape) {
|
|
2995
|
+
for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
|
|
2996
|
+
for (const f of set) if (f.startsWith("--")) out.add(f);
|
|
2997
|
+
}
|
|
2998
|
+
for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
|
|
2999
|
+
return out;
|
|
3000
|
+
}
|
|
3001
|
+
function flagNamesOf(token, shape) {
|
|
3002
|
+
if (token.startsWith("--")) {
|
|
3003
|
+
const eq = token.indexOf("=");
|
|
3004
|
+
return [eq > 0 ? token.slice(0, eq) : token];
|
|
3005
|
+
}
|
|
3006
|
+
const out = [];
|
|
3007
|
+
for (const c of token.slice(1)) {
|
|
3008
|
+
const name = `-${c}`;
|
|
3009
|
+
out.push(name);
|
|
3010
|
+
if (shape?.takesValue.has(name)) break;
|
|
3011
|
+
}
|
|
3012
|
+
return out;
|
|
3013
|
+
}
|
|
3014
|
+
var NONE = { kind: "none" };
|
|
3015
|
+
var UNKNOWN = { kind: "unknown" };
|
|
3016
|
+
function flagEffect(token, shape, known) {
|
|
3017
|
+
if (token === "--") return NONE;
|
|
3018
|
+
if (/^-+$/.test(token)) return UNKNOWN;
|
|
3019
|
+
if (/^-\d+$/.test(token)) return NONE;
|
|
3020
|
+
if (token.startsWith("--")) {
|
|
3021
|
+
if (token.includes("=")) return NONE;
|
|
3022
|
+
const takes = namesFlag(token, shape.takesValue, known);
|
|
3023
|
+
const none = namesFlag(token, shape.noValue, known);
|
|
3024
|
+
if (takes && none) return UNKNOWN;
|
|
3025
|
+
if (takes) return { kind: "takes", flag: token };
|
|
3026
|
+
if (none) return NONE;
|
|
3027
|
+
return UNKNOWN;
|
|
3028
|
+
}
|
|
3029
|
+
const letters = token.slice(1);
|
|
3030
|
+
if (!letters) return NONE;
|
|
3031
|
+
for (let i = 0; i < letters.length; i++) {
|
|
3032
|
+
const name = `-${letters[i]}`;
|
|
3033
|
+
if (shape.takesValue.has(name)) {
|
|
3034
|
+
return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
|
|
3035
|
+
}
|
|
3036
|
+
if (!shape.noValue.has(name)) return UNKNOWN;
|
|
3037
|
+
}
|
|
3038
|
+
return NONE;
|
|
3039
|
+
}
|
|
3040
|
+
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
3041
|
+
const shape = PATTERN_VERBS[verb];
|
|
3042
|
+
if (!shape) return args.map((a) => a.value);
|
|
3043
|
+
const known = knownLongFlags(verb);
|
|
3044
|
+
const names = flags.flatMap((f) => flagNamesOf(f, shape));
|
|
3045
|
+
const patternElsewhere = names.some(
|
|
3046
|
+
(n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
|
|
3047
|
+
);
|
|
3048
|
+
const excused = /* @__PURE__ */ new Set();
|
|
3049
|
+
const fileFlags = FILE_OPERAND_FLAGS[verb];
|
|
3050
|
+
const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
|
|
3051
|
+
const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
|
|
3052
|
+
for (const a of args) {
|
|
3053
|
+
if (a.afterFlag === null || pastOptions(a)) continue;
|
|
3054
|
+
const e = flagEffect(a.afterFlag, shape, known);
|
|
3055
|
+
if (e.kind !== "takes") continue;
|
|
3056
|
+
if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
|
|
3057
|
+
excused.add(a);
|
|
3058
|
+
}
|
|
3059
|
+
const patternArgv = (() => {
|
|
3060
|
+
for (let i = from; i < words.length; i++) {
|
|
3061
|
+
const w = words[i];
|
|
3062
|
+
if (w === null) return -1;
|
|
3063
|
+
if (w === "--") return i + 1;
|
|
3064
|
+
if (w.startsWith("-")) {
|
|
3065
|
+
const e = flagEffect(w, shape, known);
|
|
3066
|
+
if (e.kind === "takes") i += 1;
|
|
3067
|
+
else if (e.kind === "unknown") return -1;
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
return i;
|
|
3071
|
+
}
|
|
3072
|
+
return -1;
|
|
3073
|
+
})();
|
|
3074
|
+
if (!patternElsewhere && patternArgv >= 0) {
|
|
3075
|
+
const a = args.find((x) => x.argv === patternArgv);
|
|
3076
|
+
if (a) excused.add(a);
|
|
3077
|
+
}
|
|
3078
|
+
return args.filter((a) => !excused.has(a)).map((a) => a.value);
|
|
3079
|
+
}
|
|
3080
|
+
function flagOperandFiles(verb, words, from) {
|
|
3081
|
+
const flags = FILE_OPERAND_FLAGS[verb];
|
|
3082
|
+
if (!flags) return [];
|
|
3083
|
+
const known = knownLongFlags(verb);
|
|
3084
|
+
const out = [];
|
|
3085
|
+
for (let i = from; i < words.length; i++) {
|
|
3086
|
+
const w = words[i];
|
|
3087
|
+
if (w === null || !w.startsWith("-") || w === "--") continue;
|
|
3088
|
+
if (w.startsWith("--")) {
|
|
3089
|
+
const eq = w.indexOf("=");
|
|
3090
|
+
if (eq <= 0) continue;
|
|
3091
|
+
const name = w.slice(0, eq);
|
|
3092
|
+
const value = w.slice(eq + 1);
|
|
3093
|
+
if (!value) continue;
|
|
3094
|
+
if (namesFlag(name, flags, known)) {
|
|
3095
|
+
out.push(value);
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
const shape2 = PATTERN_VERBS[verb];
|
|
3099
|
+
if (!shape2) continue;
|
|
3100
|
+
const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
|
|
3101
|
+
if (!recognised) out.push(value);
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
const shape = PATTERN_VERBS[verb];
|
|
3105
|
+
const letters = w.slice(1);
|
|
3106
|
+
for (let j = 0; j < letters.length; j++) {
|
|
3107
|
+
const name = `-${letters[j]}`;
|
|
3108
|
+
const isFileFlag = flags.has(name);
|
|
3109
|
+
const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
|
|
3110
|
+
if (!argTaking) continue;
|
|
3111
|
+
const attached = letters.slice(j + 1);
|
|
3112
|
+
if (isFileFlag && attached) out.push(attached);
|
|
3113
|
+
break;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
return out;
|
|
3117
|
+
}
|
|
2343
3118
|
function wrappedReadPaths(words, name) {
|
|
2344
3119
|
if (name === "find") {
|
|
2345
|
-
const k = words
|
|
2346
|
-
|
|
2347
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2348
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
3120
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
3121
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2349
3122
|
}
|
|
2350
3123
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2351
3124
|
const h = unwrapCommandHead(words);
|
|
2352
|
-
|
|
3125
|
+
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
3126
|
+
const head = baseWord(words[h]);
|
|
3127
|
+
const rest = words.slice(h + 1);
|
|
3128
|
+
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
3129
|
+
return [
|
|
3130
|
+
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
3131
|
+
...flagOperandFiles(head, words, h + 1)
|
|
3132
|
+
];
|
|
2353
3133
|
}
|
|
2354
3134
|
function literalShellPayload(words, name) {
|
|
2355
3135
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -3233,6 +4013,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3233
4013
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3234
4014
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3235
4015
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
4016
|
+
let pendingAstReview;
|
|
3236
4017
|
if (bashCommand !== null) {
|
|
3237
4018
|
const pipeVerdict = pipeChainVerdict(
|
|
3238
4019
|
bashCommand,
|
|
@@ -3244,7 +4025,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3244
4025
|
if (fsVerdict) {
|
|
3245
4026
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3246
4027
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3247
|
-
|
|
4028
|
+
const astVerdict = {
|
|
3248
4029
|
decision: fsVerdict.verdict,
|
|
3249
4030
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3250
4031
|
reason: fsVerdict.reason,
|
|
@@ -3252,6 +4033,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3252
4033
|
ruleName: fsVerdict.ruleName,
|
|
3253
4034
|
ruleDescription: fsVerdict.reason
|
|
3254
4035
|
};
|
|
4036
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
4037
|
+
pendingAstReview = astVerdict;
|
|
3255
4038
|
}
|
|
3256
4039
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3257
4040
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3294,7 +4077,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3294
4077
|
const matchedRule = resolvePinned(matches);
|
|
3295
4078
|
if (matchedRule) {
|
|
3296
4079
|
if (matchedRule.verdict === "allow")
|
|
3297
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
4080
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3298
4081
|
return {
|
|
3299
4082
|
decision: matchedRule.verdict,
|
|
3300
4083
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3321,6 +4104,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3321
4104
|
allTokens = analyzed.allTokens;
|
|
3322
4105
|
pathTokens = analyzed.paths;
|
|
3323
4106
|
const candidates = [];
|
|
4107
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3324
4108
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3325
4109
|
if (evalVerdict === "block") {
|
|
3326
4110
|
return {
|
|
@@ -4427,6 +5211,12 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
4427
5211
|
"read-ssh",
|
|
4428
5212
|
"read-gcp",
|
|
4429
5213
|
"read-cred",
|
|
5214
|
+
// Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
|
|
5215
|
+
// read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
|
|
5216
|
+
// (below), so copy-env joins the high list, not this one (/code-review).
|
|
5217
|
+
"copy-ssh",
|
|
5218
|
+
"copy-aws",
|
|
5219
|
+
"copy-cred",
|
|
4430
5220
|
"delete-repo",
|
|
4431
5221
|
"helm-uninstall",
|
|
4432
5222
|
"drop-table",
|
|
@@ -4439,6 +5229,7 @@ function classifyRuleSeverity(name, verdict) {
|
|
|
4439
5229
|
];
|
|
4440
5230
|
if (criticalPatterns.some((p) => n.includes(p))) return "critical";
|
|
4441
5231
|
const highPatterns = [
|
|
5232
|
+
"copy-env",
|
|
4442
5233
|
"force-push",
|
|
4443
5234
|
"force_push",
|
|
4444
5235
|
"git-destructive",
|
|
@@ -4456,6 +5247,11 @@ function narrativeRuleLabel(name) {
|
|
|
4456
5247
|
const map = {
|
|
4457
5248
|
"read-aws": "AWS credentials read",
|
|
4458
5249
|
"read-ssh": "SSH private key read",
|
|
5250
|
+
// Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
|
|
5251
|
+
"copy-ssh": "SSH private key copied out",
|
|
5252
|
+
"copy-aws": "AWS credentials copied out",
|
|
5253
|
+
"copy-env": ".env file copied out",
|
|
5254
|
+
"copy-cred": "credential file copied out",
|
|
4459
5255
|
"read-gcp": "GCP credentials read",
|
|
4460
5256
|
"read-cred": "credential file read",
|
|
4461
5257
|
"delete-repo": "GitHub repository deletion",
|
|
@@ -4853,8 +5649,8 @@ function matchCanaryArgs(args, values) {
|
|
|
4853
5649
|
|
|
4854
5650
|
// src/scan/canonical.ts
|
|
4855
5651
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
4856
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
4857
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
5652
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v15";
|
|
5653
|
+
var CANONICAL_EXTRACTOR_HASH = "5dad4c8f07121f6c";
|
|
4858
5654
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
4859
5655
|
function extractCanonicalFindings(call, ctx) {
|
|
4860
5656
|
const out = [];
|
|
@@ -5231,6 +6027,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5231
6027
|
CANONICAL_EXTRACTOR_HASH,
|
|
5232
6028
|
CANONICAL_EXTRACTOR_VERSION,
|
|
5233
6029
|
COMMAND_WRAPPERS,
|
|
6030
|
+
COPY_VERBS,
|
|
5234
6031
|
COST_PER_LOOP_ITER_USD,
|
|
5235
6032
|
DEFAULT_EGRESS_ALLOWLIST,
|
|
5236
6033
|
DESTINATION_ARGS,
|
|
@@ -5245,6 +6042,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5245
6042
|
LOOP_MAX_RECORDS,
|
|
5246
6043
|
LOOP_THRESHOLD_FOR_WASTE,
|
|
5247
6044
|
NET_BINARIES,
|
|
6045
|
+
PATTERN_VERB_NAMES,
|
|
5248
6046
|
PRIVILEGE_ESCALATION_RE,
|
|
5249
6047
|
REALTIME_PII_PATTERNS,
|
|
5250
6048
|
SCAN_SIGNAL_WEIGHTS,
|
|
@@ -5281,6 +6079,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5281
6079
|
extractSessionLevelFindings,
|
|
5282
6080
|
extractShellDestTokens,
|
|
5283
6081
|
extractShellDestinations,
|
|
6082
|
+
fileOperandFlagsOf,
|
|
5284
6083
|
getCompiledRegex,
|
|
5285
6084
|
getNestedValue,
|
|
5286
6085
|
hostMatches,
|
|
@@ -5300,9 +6099,12 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
5300
6099
|
normalizeIpLiteral,
|
|
5301
6100
|
parseAllSshHostsFromCommand,
|
|
5302
6101
|
parseDestHost,
|
|
6102
|
+
patternShapeOf,
|
|
6103
|
+
positionedArgs,
|
|
5303
6104
|
previewArgs,
|
|
5304
6105
|
redactText,
|
|
5305
6106
|
resolvePinned,
|
|
6107
|
+
sampleCopyCommand,
|
|
5306
6108
|
scanArgs,
|
|
5307
6109
|
scanInjection,
|
|
5308
6110
|
scanText,
|