@node9/policy-engine 2.14.0 → 2.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +47 -3
- package/dist/index.d.ts +47 -3
- package/dist/index.js +606 -37
- package/dist/index.mjs +603 -37
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1121,6 +1121,19 @@ function parseShared(command) {
|
|
|
1121
1121
|
astCache.set(command, parsed);
|
|
1122
1122
|
return parsed;
|
|
1123
1123
|
}
|
|
1124
|
+
function byteOffsetToCharIndex(command) {
|
|
1125
|
+
if (!/[^\u0000-\u007F]/.test(command)) return null;
|
|
1126
|
+
const map = /* @__PURE__ */ new Map();
|
|
1127
|
+
let byte = 0;
|
|
1128
|
+
for (let i = 0; i < command.length; ) {
|
|
1129
|
+
map.set(byte, i);
|
|
1130
|
+
const cp = command.codePointAt(i);
|
|
1131
|
+
byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
1132
|
+
i += cp > 65535 ? 2 : 1;
|
|
1133
|
+
}
|
|
1134
|
+
map.set(byte, command.length);
|
|
1135
|
+
return (b) => map.get(b) ?? -1;
|
|
1136
|
+
}
|
|
1124
1137
|
function cachedNormalize(command, compute) {
|
|
1125
1138
|
const hit = normalizeCache.get(command);
|
|
1126
1139
|
if (hit !== void 0) {
|
|
@@ -1150,6 +1163,8 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1150
1163
|
const f = parseShared(command);
|
|
1151
1164
|
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1152
1165
|
try {
|
|
1166
|
+
const toCharIndex = byteOffsetToCharIndex(command);
|
|
1167
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
1153
1168
|
const strips = [];
|
|
1154
1169
|
const rewrites = [];
|
|
1155
1170
|
const quoteOnlyRewrites = [];
|
|
@@ -1170,8 +1185,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1170
1185
|
const quotedNode = nextParts[0];
|
|
1171
1186
|
const nt = syntax.NodeType(quotedNode);
|
|
1172
1187
|
const markStrip = () => {
|
|
1173
|
-
const s = next.Pos().Offset();
|
|
1174
|
-
const e = next.End().Offset();
|
|
1188
|
+
const s = at(next.Pos().Offset());
|
|
1189
|
+
const e = at(next.End().Offset());
|
|
1190
|
+
if (s < 0 || e < 0) return;
|
|
1175
1191
|
strips.push([s, e]);
|
|
1176
1192
|
msgSpans.add(`${s}:${e}`);
|
|
1177
1193
|
};
|
|
@@ -1188,8 +1204,9 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1188
1204
|
}
|
|
1189
1205
|
}
|
|
1190
1206
|
for (const arg of args) {
|
|
1191
|
-
const s = arg.Pos().Offset();
|
|
1192
|
-
const e = arg.End().Offset();
|
|
1207
|
+
const s = at(arg.Pos().Offset());
|
|
1208
|
+
const e = at(arg.End().Offset());
|
|
1209
|
+
if (s < 0 || e < 0) continue;
|
|
1193
1210
|
if (msgSpans.has(`${s}:${e}`)) continue;
|
|
1194
1211
|
const resolved = resolveWordLiteral(arg);
|
|
1195
1212
|
if (resolved === null) continue;
|
|
@@ -1312,7 +1329,347 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1312
1329
|
"nl",
|
|
1313
1330
|
"dd"
|
|
1314
1331
|
]);
|
|
1332
|
+
var GREP_SHAPE = {
|
|
1333
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1334
|
+
"-A",
|
|
1335
|
+
"-B",
|
|
1336
|
+
"-C",
|
|
1337
|
+
"-D",
|
|
1338
|
+
"-d",
|
|
1339
|
+
"-e",
|
|
1340
|
+
"-f",
|
|
1341
|
+
"-m",
|
|
1342
|
+
"--after-context",
|
|
1343
|
+
"--before-context",
|
|
1344
|
+
"--binary-files",
|
|
1345
|
+
"--context",
|
|
1346
|
+
"--devices",
|
|
1347
|
+
"--directories",
|
|
1348
|
+
"--exclude",
|
|
1349
|
+
"--exclude-dir",
|
|
1350
|
+
"--exclude-from",
|
|
1351
|
+
"--file",
|
|
1352
|
+
"--include",
|
|
1353
|
+
"--label",
|
|
1354
|
+
"--max-count",
|
|
1355
|
+
"--regexp"
|
|
1356
|
+
]),
|
|
1357
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1358
|
+
"-E",
|
|
1359
|
+
"-F",
|
|
1360
|
+
"-G",
|
|
1361
|
+
"-P",
|
|
1362
|
+
"-i",
|
|
1363
|
+
"-y",
|
|
1364
|
+
"-v",
|
|
1365
|
+
"-V",
|
|
1366
|
+
"-w",
|
|
1367
|
+
"-x",
|
|
1368
|
+
"-c",
|
|
1369
|
+
"-l",
|
|
1370
|
+
"-L",
|
|
1371
|
+
"-o",
|
|
1372
|
+
"-q",
|
|
1373
|
+
"-s",
|
|
1374
|
+
"-b",
|
|
1375
|
+
"-H",
|
|
1376
|
+
"-h",
|
|
1377
|
+
"-n",
|
|
1378
|
+
"-T",
|
|
1379
|
+
"-Z",
|
|
1380
|
+
"-z",
|
|
1381
|
+
"-R",
|
|
1382
|
+
"-r",
|
|
1383
|
+
"-U",
|
|
1384
|
+
"-u",
|
|
1385
|
+
"-I",
|
|
1386
|
+
"-a",
|
|
1387
|
+
"--basic-regexp",
|
|
1388
|
+
"--binary",
|
|
1389
|
+
"--byte-offset",
|
|
1390
|
+
"--color",
|
|
1391
|
+
"--colour",
|
|
1392
|
+
"--count",
|
|
1393
|
+
"--dereference-recursive",
|
|
1394
|
+
"--extended-regexp",
|
|
1395
|
+
"--files-with-matches",
|
|
1396
|
+
"--files-without-match",
|
|
1397
|
+
"--fixed-strings",
|
|
1398
|
+
"--help",
|
|
1399
|
+
"--ignore-case",
|
|
1400
|
+
"--initial-tab",
|
|
1401
|
+
"--invert-match",
|
|
1402
|
+
"--line-buffered",
|
|
1403
|
+
"--line-number",
|
|
1404
|
+
"--line-regexp",
|
|
1405
|
+
"--no-filename",
|
|
1406
|
+
"--no-group-separator",
|
|
1407
|
+
"--no-ignore-case",
|
|
1408
|
+
"--no-messages",
|
|
1409
|
+
"--null",
|
|
1410
|
+
"--null-data",
|
|
1411
|
+
"--only-matching",
|
|
1412
|
+
"--perl-regexp",
|
|
1413
|
+
"--quiet",
|
|
1414
|
+
"--recursive",
|
|
1415
|
+
"--silent",
|
|
1416
|
+
"--text",
|
|
1417
|
+
"--version",
|
|
1418
|
+
"--with-filename",
|
|
1419
|
+
"--word-regexp"
|
|
1420
|
+
]),
|
|
1421
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1422
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
|
|
1423
|
+
};
|
|
1424
|
+
var PATTERN_VERBS = {
|
|
1425
|
+
grep: GREP_SHAPE,
|
|
1426
|
+
// /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
|
|
1427
|
+
// `grep -E` and `grep -F`. Read in full, not assumed.
|
|
1428
|
+
egrep: GREP_SHAPE,
|
|
1429
|
+
fgrep: GREP_SHAPE,
|
|
1430
|
+
// ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
|
|
1431
|
+
// was not installed on the measuring machine; it is, and that stale claim is why
|
|
1432
|
+
// this table shipped incomplete for two rounds, blocking ordinary searches like
|
|
1433
|
+
// `rg --sort-files .env src` (/code-review round 4).
|
|
1434
|
+
rg: {
|
|
1435
|
+
takesValue: /* @__PURE__ */ new Set([
|
|
1436
|
+
"-A",
|
|
1437
|
+
"-B",
|
|
1438
|
+
"-C",
|
|
1439
|
+
"-d",
|
|
1440
|
+
"-E",
|
|
1441
|
+
"-e",
|
|
1442
|
+
"-f",
|
|
1443
|
+
"-g",
|
|
1444
|
+
"-j",
|
|
1445
|
+
"-M",
|
|
1446
|
+
"-m",
|
|
1447
|
+
"-r",
|
|
1448
|
+
"-t",
|
|
1449
|
+
"-T",
|
|
1450
|
+
"--after-context",
|
|
1451
|
+
"--before-context",
|
|
1452
|
+
"--color",
|
|
1453
|
+
"--colors",
|
|
1454
|
+
"--context",
|
|
1455
|
+
"--context-separator",
|
|
1456
|
+
"--dfa-size-limit",
|
|
1457
|
+
"--encoding",
|
|
1458
|
+
"--engine",
|
|
1459
|
+
"--field-context-separator",
|
|
1460
|
+
"--field-match-separator",
|
|
1461
|
+
"--file",
|
|
1462
|
+
"--generate",
|
|
1463
|
+
"--glob",
|
|
1464
|
+
"--hostname-bin",
|
|
1465
|
+
"--hyperlink-format",
|
|
1466
|
+
"--iglob",
|
|
1467
|
+
"--ignore-file",
|
|
1468
|
+
"--max-columns",
|
|
1469
|
+
"--max-count",
|
|
1470
|
+
"--max-depth",
|
|
1471
|
+
// An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
|
|
1472
|
+
// alias lines as plain switches and swept it into noValue, which hard-blocked
|
|
1473
|
+
// `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
|
|
1474
|
+
"--maxdepth",
|
|
1475
|
+
"--max-filesize",
|
|
1476
|
+
"--path-separator",
|
|
1477
|
+
"--pre",
|
|
1478
|
+
"--pre-glob",
|
|
1479
|
+
"--regexp",
|
|
1480
|
+
"--regex-size-limit",
|
|
1481
|
+
"--replace",
|
|
1482
|
+
"--sort",
|
|
1483
|
+
"--sortr",
|
|
1484
|
+
"--threads",
|
|
1485
|
+
"--type",
|
|
1486
|
+
"--type-add",
|
|
1487
|
+
"--type-clear",
|
|
1488
|
+
"--type-not"
|
|
1489
|
+
]),
|
|
1490
|
+
noValue: /* @__PURE__ */ new Set([
|
|
1491
|
+
"-.",
|
|
1492
|
+
"-0",
|
|
1493
|
+
"-a",
|
|
1494
|
+
"-b",
|
|
1495
|
+
"-c",
|
|
1496
|
+
"-F",
|
|
1497
|
+
"-h",
|
|
1498
|
+
"-H",
|
|
1499
|
+
"-i",
|
|
1500
|
+
"-I",
|
|
1501
|
+
"-l",
|
|
1502
|
+
"-L",
|
|
1503
|
+
"-n",
|
|
1504
|
+
"-N",
|
|
1505
|
+
"-o",
|
|
1506
|
+
"-p",
|
|
1507
|
+
"-P",
|
|
1508
|
+
"-q",
|
|
1509
|
+
"-s",
|
|
1510
|
+
"-S",
|
|
1511
|
+
"-u",
|
|
1512
|
+
"-U",
|
|
1513
|
+
"-v",
|
|
1514
|
+
"-V",
|
|
1515
|
+
"-w",
|
|
1516
|
+
"-x",
|
|
1517
|
+
"-z",
|
|
1518
|
+
"--auto-hybrid-regex",
|
|
1519
|
+
"--binary",
|
|
1520
|
+
"--block-buffered",
|
|
1521
|
+
"--byte-offset",
|
|
1522
|
+
"--case-sensitive",
|
|
1523
|
+
"--column",
|
|
1524
|
+
"--count",
|
|
1525
|
+
"--count-matches",
|
|
1526
|
+
"--crlf",
|
|
1527
|
+
"--debug",
|
|
1528
|
+
"--files",
|
|
1529
|
+
"--files-with-matches",
|
|
1530
|
+
"--files-without-match",
|
|
1531
|
+
"--fixed-strings",
|
|
1532
|
+
"--follow",
|
|
1533
|
+
"--glob-case-insensitive",
|
|
1534
|
+
"--heading",
|
|
1535
|
+
"--help",
|
|
1536
|
+
"--hidden",
|
|
1537
|
+
"--ignore-case",
|
|
1538
|
+
"--ignore-file-case-insensitive",
|
|
1539
|
+
"--include-zero",
|
|
1540
|
+
"--invert-match",
|
|
1541
|
+
"--json",
|
|
1542
|
+
"--line-buffered",
|
|
1543
|
+
"--line-number",
|
|
1544
|
+
"--line-regexp",
|
|
1545
|
+
"--max-columns-preview",
|
|
1546
|
+
"--mmap",
|
|
1547
|
+
"--multiline",
|
|
1548
|
+
"--multiline-dotall",
|
|
1549
|
+
"--no-column",
|
|
1550
|
+
"--no-config",
|
|
1551
|
+
"--no-context-separator",
|
|
1552
|
+
"--no-encoding",
|
|
1553
|
+
"--no-filename",
|
|
1554
|
+
"--no-ignore",
|
|
1555
|
+
"--no-ignore-dot",
|
|
1556
|
+
"--no-ignore-exclude",
|
|
1557
|
+
"--no-ignore-files",
|
|
1558
|
+
"--no-ignore-global",
|
|
1559
|
+
"--no-ignore-messages",
|
|
1560
|
+
"--no-ignore-parent",
|
|
1561
|
+
"--no-ignore-vcs",
|
|
1562
|
+
"--no-line-number",
|
|
1563
|
+
"--no-messages",
|
|
1564
|
+
"--no-pcre2-unicode",
|
|
1565
|
+
"--no-pre",
|
|
1566
|
+
"--no-require-git",
|
|
1567
|
+
"--no-unicode",
|
|
1568
|
+
"--null",
|
|
1569
|
+
"--null-data",
|
|
1570
|
+
"--one-file-system",
|
|
1571
|
+
"--only-matching",
|
|
1572
|
+
"--passthru",
|
|
1573
|
+
"--pcre2",
|
|
1574
|
+
"--pcre2-version",
|
|
1575
|
+
"--pretty",
|
|
1576
|
+
"--print0",
|
|
1577
|
+
"--quiet",
|
|
1578
|
+
"--search-zip",
|
|
1579
|
+
"--smart-case",
|
|
1580
|
+
"--sort-files",
|
|
1581
|
+
"--stats",
|
|
1582
|
+
"--stop-on-nonmatch",
|
|
1583
|
+
"--text",
|
|
1584
|
+
"--trace",
|
|
1585
|
+
"--trim",
|
|
1586
|
+
"--type-list",
|
|
1587
|
+
"--unrestricted",
|
|
1588
|
+
"--version",
|
|
1589
|
+
"--vimgrep",
|
|
1590
|
+
"--with-filename",
|
|
1591
|
+
"--word-regexp",
|
|
1592
|
+
// The COMPLETE remainder of `rg --help`, added after /code-review round 5
|
|
1593
|
+
// found 40 documented switches in neither set. The table is now the whole
|
|
1594
|
+
// option list: `rg --help` yields 149 long options, 35 of them value-taking.
|
|
1595
|
+
"--ignore",
|
|
1596
|
+
"--ignore-dot",
|
|
1597
|
+
"--ignore-exclude",
|
|
1598
|
+
"--ignore-files",
|
|
1599
|
+
"--ignore-global",
|
|
1600
|
+
"--ignore-messages",
|
|
1601
|
+
"--ignore-parent",
|
|
1602
|
+
"--ignore-vcs",
|
|
1603
|
+
"--messages",
|
|
1604
|
+
"--no-auto-hybrid-regex",
|
|
1605
|
+
"--no-binary",
|
|
1606
|
+
"--no-block-buffered",
|
|
1607
|
+
"--no-byte-offset",
|
|
1608
|
+
"--no-crlf",
|
|
1609
|
+
"--no-fixed-strings",
|
|
1610
|
+
"--no-follow",
|
|
1611
|
+
"--no-glob-case-insensitive",
|
|
1612
|
+
"--no-heading",
|
|
1613
|
+
"--no-hidden",
|
|
1614
|
+
"--no-ignore-file-case-insensitive",
|
|
1615
|
+
"--no-include-zero",
|
|
1616
|
+
"--no-invert-match",
|
|
1617
|
+
"--no-json",
|
|
1618
|
+
"--no-line-buffered",
|
|
1619
|
+
"--no-max-columns-preview",
|
|
1620
|
+
"--no-mmap",
|
|
1621
|
+
"--no-multiline",
|
|
1622
|
+
"--no-multiline-dotall",
|
|
1623
|
+
"--no-one-file-system",
|
|
1624
|
+
"--no-pcre2",
|
|
1625
|
+
"--no-search-zip",
|
|
1626
|
+
"--no-sort-files",
|
|
1627
|
+
"--no-stats",
|
|
1628
|
+
"--no-text",
|
|
1629
|
+
"--no-trim",
|
|
1630
|
+
"--passthrough",
|
|
1631
|
+
"--pcre2-unicode",
|
|
1632
|
+
"--require-git",
|
|
1633
|
+
"--unicode"
|
|
1634
|
+
]),
|
|
1635
|
+
patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
|
|
1636
|
+
// `--files` and `--type-list` list or enumerate without a pattern. Founder
|
|
1637
|
+
// decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
|
|
1638
|
+
// by leaving the directory in the judged list.
|
|
1639
|
+
noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
var PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
|
|
1643
|
+
var patternShapeOf = (verb) => PATTERN_VERBS[verb];
|
|
1644
|
+
var fileOperandFlagsOf = (verb) => FILE_OPERAND_FLAGS[verb];
|
|
1645
|
+
var GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
|
|
1646
|
+
var READER_VALUE_LETTERS = {
|
|
1647
|
+
awk: ["F", "v", "f"],
|
|
1648
|
+
gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
|
|
1649
|
+
sed: ["e", "f", "i", "l"],
|
|
1650
|
+
sort: ["C", "k", "o", "S", "t", "T"]
|
|
1651
|
+
};
|
|
1652
|
+
var FILE_OPERAND_FLAGS = {
|
|
1653
|
+
grep: GREP_FILE_OPERANDS,
|
|
1654
|
+
egrep: GREP_FILE_OPERANDS,
|
|
1655
|
+
fgrep: GREP_FILE_OPERANDS,
|
|
1656
|
+
// rg and gawk are NOT installed on the measuring machine: these two rows are
|
|
1657
|
+
// from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
|
|
1658
|
+
// `-f/--file`) and are marked as such in the spec.
|
|
1659
|
+
// `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
|
|
1660
|
+
// a credential makes rg open it -- the same reason grep's `--include` is here.
|
|
1661
|
+
// Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
|
|
1662
|
+
// and printed its contents.
|
|
1663
|
+
rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
|
|
1664
|
+
sed: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1665
|
+
awk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1666
|
+
gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
|
|
1667
|
+
sort: /* @__PURE__ */ new Set(["--files0-from"])
|
|
1668
|
+
};
|
|
1315
1669
|
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
1670
|
+
var TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
|
|
1671
|
+
var ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
|
|
1672
|
+
var RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
|
|
1316
1673
|
var RSYNC_SKIP = [
|
|
1317
1674
|
"e",
|
|
1318
1675
|
"--rsh",
|
|
@@ -1324,21 +1681,35 @@ var RSYNC_SKIP = [
|
|
|
1324
1681
|
"f",
|
|
1325
1682
|
"--filter"
|
|
1326
1683
|
];
|
|
1684
|
+
var CP_VALUE_LETTERS = ["S", "t"];
|
|
1685
|
+
var INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
|
|
1327
1686
|
var COPY_VERBS = {
|
|
1328
|
-
cp: { source: "allButLast", targetDirFlag: true },
|
|
1329
|
-
mv: { source: "allButLast", targetDirFlag: true },
|
|
1330
|
-
install: { source: "allButLast", targetDirFlag: true },
|
|
1331
|
-
ln: { source: "first", targetDirFlag: true },
|
|
1332
|
-
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
|
|
1333
|
-
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
|
|
1687
|
+
cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1688
|
+
mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1689
|
+
install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
|
|
1690
|
+
ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
|
|
1691
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
|
|
1692
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
|
|
1334
1693
|
tar: {
|
|
1335
1694
|
source: "archive",
|
|
1336
1695
|
archive: "tar",
|
|
1337
|
-
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
|
|
1696
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
|
|
1697
|
+
valueLetters: TAR_VALUE_LETTERS
|
|
1698
|
+
},
|
|
1699
|
+
zip: {
|
|
1700
|
+
source: "archive",
|
|
1701
|
+
archive: "zip",
|
|
1702
|
+
skipFlags: ["x", "i", "--exclude", "--include"],
|
|
1703
|
+
valueLetters: ZIP_VALUE_LETTERS
|
|
1338
1704
|
},
|
|
1339
|
-
zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
|
|
1340
1705
|
ar: { source: "archive", archive: "ar" },
|
|
1341
|
-
|
|
1706
|
+
// 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
|
|
1707
|
+
// ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
|
|
1708
|
+
// and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
|
|
1709
|
+
// drop the credential as an exclusion operand (/code-review round 8), and the
|
|
1710
|
+
// derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
|
|
1711
|
+
// what keeps the two tables honest about it.
|
|
1712
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
|
|
1342
1713
|
gzip: { source: "all" },
|
|
1343
1714
|
bzip2: { source: "all" },
|
|
1344
1715
|
xz: { source: "all" },
|
|
@@ -2198,7 +2569,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2198
2569
|
return result?.verdict !== "block";
|
|
2199
2570
|
}
|
|
2200
2571
|
if (nodeType !== "CallExpr") return true;
|
|
2201
|
-
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2572
|
+
const { name, flags, paths, words, args } = extractLiteralArgs(n);
|
|
2202
2573
|
if (!name) return true;
|
|
2203
2574
|
if (name === "rm") {
|
|
2204
2575
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2238,7 +2609,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2238
2609
|
return true;
|
|
2239
2610
|
}
|
|
2240
2611
|
}
|
|
2241
|
-
const readPaths = FS_READ_TOOLS.has(name) ?
|
|
2612
|
+
const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
|
|
2242
2613
|
if (readPaths) {
|
|
2243
2614
|
for (const p of readPaths) {
|
|
2244
2615
|
result = stricter(result, matchSensitivePath2(p));
|
|
@@ -2274,9 +2645,22 @@ function flagIs(w, names) {
|
|
|
2274
2645
|
const f = flagInfo(w);
|
|
2275
2646
|
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
2276
2647
|
}
|
|
2277
|
-
function operandOf(a, names) {
|
|
2648
|
+
function operandOf(a, names, valueLetters) {
|
|
2278
2649
|
if (!names || a.afterFlag === null) return false;
|
|
2279
|
-
|
|
2650
|
+
const w = a.afterFlag;
|
|
2651
|
+
if (!w.startsWith("--") && valueLetters) {
|
|
2652
|
+
const letters = w.slice(1);
|
|
2653
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2654
|
+
if (!valueLetters.includes(letters[i])) continue;
|
|
2655
|
+
return i === letters.length - 1 && names.includes(letters[i]);
|
|
2656
|
+
}
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
if (w.startsWith("--") && !w.includes("=")) {
|
|
2660
|
+
const longs = names.filter((n) => n.startsWith("--"));
|
|
2661
|
+
if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
|
|
2662
|
+
}
|
|
2663
|
+
return flagIs(w, names) && flagInfo(w).attached === null;
|
|
2280
2664
|
}
|
|
2281
2665
|
function resolveCopyShape(words, h) {
|
|
2282
2666
|
const verb = baseWord(words[h]);
|
|
@@ -2301,7 +2685,7 @@ function findStartPoints(words, h) {
|
|
|
2301
2685
|
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2302
2686
|
if (k < 0) return { k, starts: [] };
|
|
2303
2687
|
const firstPredicate = words.findIndex(
|
|
2304
|
-
(w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2688
|
+
(w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2305
2689
|
);
|
|
2306
2690
|
const end = firstPredicate > h ? firstPredicate : k;
|
|
2307
2691
|
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
@@ -2321,14 +2705,35 @@ function copySourcePaths(words) {
|
|
|
2321
2705
|
const { shape, last } = r;
|
|
2322
2706
|
const args = positionedArgs(words, last + 1);
|
|
2323
2707
|
const tail = words.slice(last + 1);
|
|
2324
|
-
const
|
|
2325
|
-
const
|
|
2326
|
-
const
|
|
2708
|
+
const copyOptionsEnd = tail.findIndex((w) => w === "--");
|
|
2709
|
+
const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
|
|
2710
|
+
const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
|
|
2711
|
+
const firstValueLetter = (w) => {
|
|
2712
|
+
const letters = w.slice(1);
|
|
2713
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2714
|
+
if ((shape.valueLetters ?? []).includes(letters[i]))
|
|
2715
|
+
return { letter: letters[i], last: i === letters.length - 1 };
|
|
2716
|
+
}
|
|
2717
|
+
return null;
|
|
2718
|
+
};
|
|
2719
|
+
const isTargetDirFlag = (w) => {
|
|
2720
|
+
if (w.startsWith("--")) {
|
|
2721
|
+
const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
|
|
2722
|
+
return name.length >= 3 && "--target-directory".startsWith(name);
|
|
2723
|
+
}
|
|
2724
|
+
return firstValueLetter(w)?.letter === "t";
|
|
2725
|
+
};
|
|
2726
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
|
|
2727
|
+
const targetTakesNextWord = (w) => {
|
|
2728
|
+
if (w.startsWith("--"))
|
|
2729
|
+
return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
|
|
2730
|
+
const f = firstValueLetter(w);
|
|
2731
|
+
return f !== null && f.letter === "t" && f.last;
|
|
2732
|
+
};
|
|
2733
|
+
const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
|
|
2327
2734
|
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
2328
2735
|
const dynamicDest = lastOperand === null;
|
|
2329
2736
|
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
2330
|
-
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
|
|
2331
|
-
return [];
|
|
2332
2737
|
let src;
|
|
2333
2738
|
switch (shape.source) {
|
|
2334
2739
|
case "all":
|
|
@@ -2338,11 +2743,17 @@ function copySourcePaths(words) {
|
|
|
2338
2743
|
src = targetDir ? args : args.slice(0, 1);
|
|
2339
2744
|
break;
|
|
2340
2745
|
case "flagOperand": {
|
|
2341
|
-
const
|
|
2746
|
+
const namesSource = (f) => {
|
|
2747
|
+
const names = shape.sourceFlags ?? [];
|
|
2748
|
+
if (f.long !== null)
|
|
2749
|
+
return names.some(
|
|
2750
|
+
(n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
|
|
2751
|
+
);
|
|
2752
|
+
return f.letter !== null && names.includes(f.letter);
|
|
2753
|
+
};
|
|
2754
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
|
|
2342
2755
|
return [
|
|
2343
|
-
...args.filter(
|
|
2344
|
-
(a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
|
|
2345
|
-
).map((a) => a.value),
|
|
2756
|
+
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
|
|
2346
2757
|
...inline
|
|
2347
2758
|
];
|
|
2348
2759
|
}
|
|
@@ -2353,7 +2764,14 @@ function copySourcePaths(words) {
|
|
|
2353
2764
|
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
2354
2765
|
break;
|
|
2355
2766
|
}
|
|
2356
|
-
|
|
2767
|
+
const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
2768
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
|
|
2769
|
+
const jailedSources = sources.filter((p) => matchSensitivePath2(p));
|
|
2770
|
+
const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
|
|
2771
|
+
if (jailedSources.length === 0) return [];
|
|
2772
|
+
if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
|
|
2773
|
+
}
|
|
2774
|
+
return sources;
|
|
2357
2775
|
}
|
|
2358
2776
|
function archiveInputs(kind, args, tail) {
|
|
2359
2777
|
const first = args[0];
|
|
@@ -2365,13 +2783,17 @@ function archiveInputs(kind, args, tail) {
|
|
|
2365
2783
|
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
2366
2784
|
if (extracting && !writing) return [];
|
|
2367
2785
|
void mode;
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
if (
|
|
2786
|
+
if (!bareKey) return args;
|
|
2787
|
+
let i = 1;
|
|
2788
|
+
const fromDirs = [];
|
|
2789
|
+
for (const ch of first.value) {
|
|
2790
|
+
if (!TAR_VALUE_LETTERS.includes(ch)) continue;
|
|
2791
|
+
const operand = args[i];
|
|
2792
|
+
if (!operand || operand.afterFlag !== null) break;
|
|
2793
|
+
i += 1;
|
|
2794
|
+
if (ch === "C") fromDirs.push(operand);
|
|
2373
2795
|
}
|
|
2374
|
-
return args.slice(i);
|
|
2796
|
+
return [...fromDirs, ...args.slice(i)];
|
|
2375
2797
|
}
|
|
2376
2798
|
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
2377
2799
|
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
@@ -2423,6 +2845,140 @@ function baseWord(w) {
|
|
|
2423
2845
|
}
|
|
2424
2846
|
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
2425
2847
|
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
2848
|
+
function namesFlag(name, candidates, known) {
|
|
2849
|
+
if (candidates.has(name)) return true;
|
|
2850
|
+
if (!name.startsWith("--") || name.length < 3) return false;
|
|
2851
|
+
if (known?.has(name)) return false;
|
|
2852
|
+
for (const c of candidates) if (c.startsWith(name)) return true;
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
function knownLongFlags(verb) {
|
|
2856
|
+
const out = /* @__PURE__ */ new Set();
|
|
2857
|
+
const shape = PATTERN_VERBS[verb];
|
|
2858
|
+
if (shape) {
|
|
2859
|
+
for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
|
|
2860
|
+
for (const f of set) if (f.startsWith("--")) out.add(f);
|
|
2861
|
+
}
|
|
2862
|
+
for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
|
|
2863
|
+
return out;
|
|
2864
|
+
}
|
|
2865
|
+
function flagNamesOf(token, shape) {
|
|
2866
|
+
if (token.startsWith("--")) {
|
|
2867
|
+
const eq = token.indexOf("=");
|
|
2868
|
+
return [eq > 0 ? token.slice(0, eq) : token];
|
|
2869
|
+
}
|
|
2870
|
+
const out = [];
|
|
2871
|
+
for (const c of token.slice(1)) {
|
|
2872
|
+
const name = `-${c}`;
|
|
2873
|
+
out.push(name);
|
|
2874
|
+
if (shape?.takesValue.has(name)) break;
|
|
2875
|
+
}
|
|
2876
|
+
return out;
|
|
2877
|
+
}
|
|
2878
|
+
var NONE = { kind: "none" };
|
|
2879
|
+
var UNKNOWN = { kind: "unknown" };
|
|
2880
|
+
function flagEffect(token, shape, known) {
|
|
2881
|
+
if (token === "--") return NONE;
|
|
2882
|
+
if (/^-+$/.test(token)) return UNKNOWN;
|
|
2883
|
+
if (/^-\d+$/.test(token)) return NONE;
|
|
2884
|
+
if (token.startsWith("--")) {
|
|
2885
|
+
if (token.includes("=")) return NONE;
|
|
2886
|
+
const takes = namesFlag(token, shape.takesValue, known);
|
|
2887
|
+
const none = namesFlag(token, shape.noValue, known);
|
|
2888
|
+
if (takes && none) return UNKNOWN;
|
|
2889
|
+
if (takes) return { kind: "takes", flag: token };
|
|
2890
|
+
if (none) return NONE;
|
|
2891
|
+
return UNKNOWN;
|
|
2892
|
+
}
|
|
2893
|
+
const letters = token.slice(1);
|
|
2894
|
+
if (!letters) return NONE;
|
|
2895
|
+
for (let i = 0; i < letters.length; i++) {
|
|
2896
|
+
const name = `-${letters[i]}`;
|
|
2897
|
+
if (shape.takesValue.has(name)) {
|
|
2898
|
+
return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
|
|
2899
|
+
}
|
|
2900
|
+
if (!shape.noValue.has(name)) return UNKNOWN;
|
|
2901
|
+
}
|
|
2902
|
+
return NONE;
|
|
2903
|
+
}
|
|
2904
|
+
function readTargets(verb, args, flags, words = [], from = 1) {
|
|
2905
|
+
const shape = PATTERN_VERBS[verb];
|
|
2906
|
+
if (!shape) return args.map((a) => a.value);
|
|
2907
|
+
const known = knownLongFlags(verb);
|
|
2908
|
+
const names = flags.flatMap((f) => flagNamesOf(f, shape));
|
|
2909
|
+
const patternElsewhere = names.some(
|
|
2910
|
+
(n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
|
|
2911
|
+
);
|
|
2912
|
+
const excused = /* @__PURE__ */ new Set();
|
|
2913
|
+
const fileFlags = FILE_OPERAND_FLAGS[verb];
|
|
2914
|
+
const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
|
|
2915
|
+
const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
|
|
2916
|
+
for (const a of args) {
|
|
2917
|
+
if (a.afterFlag === null || pastOptions(a)) continue;
|
|
2918
|
+
const e = flagEffect(a.afterFlag, shape, known);
|
|
2919
|
+
if (e.kind !== "takes") continue;
|
|
2920
|
+
if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
|
|
2921
|
+
excused.add(a);
|
|
2922
|
+
}
|
|
2923
|
+
const patternArgv = (() => {
|
|
2924
|
+
for (let i = from; i < words.length; i++) {
|
|
2925
|
+
const w = words[i];
|
|
2926
|
+
if (w === null) return -1;
|
|
2927
|
+
if (w === "--") return i + 1;
|
|
2928
|
+
if (w.startsWith("-")) {
|
|
2929
|
+
const e = flagEffect(w, shape, known);
|
|
2930
|
+
if (e.kind === "takes") i += 1;
|
|
2931
|
+
else if (e.kind === "unknown") return -1;
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
return i;
|
|
2935
|
+
}
|
|
2936
|
+
return -1;
|
|
2937
|
+
})();
|
|
2938
|
+
if (!patternElsewhere && patternArgv >= 0) {
|
|
2939
|
+
const a = args.find((x) => x.argv === patternArgv);
|
|
2940
|
+
if (a) excused.add(a);
|
|
2941
|
+
}
|
|
2942
|
+
return args.filter((a) => !excused.has(a)).map((a) => a.value);
|
|
2943
|
+
}
|
|
2944
|
+
function flagOperandFiles(verb, words, from) {
|
|
2945
|
+
const flags = FILE_OPERAND_FLAGS[verb];
|
|
2946
|
+
if (!flags) return [];
|
|
2947
|
+
const known = knownLongFlags(verb);
|
|
2948
|
+
const out = [];
|
|
2949
|
+
for (let i = from; i < words.length; i++) {
|
|
2950
|
+
const w = words[i];
|
|
2951
|
+
if (w === null || !w.startsWith("-") || w === "--") continue;
|
|
2952
|
+
if (w.startsWith("--")) {
|
|
2953
|
+
const eq = w.indexOf("=");
|
|
2954
|
+
if (eq <= 0) continue;
|
|
2955
|
+
const name = w.slice(0, eq);
|
|
2956
|
+
const value = w.slice(eq + 1);
|
|
2957
|
+
if (!value) continue;
|
|
2958
|
+
if (namesFlag(name, flags, known)) {
|
|
2959
|
+
out.push(value);
|
|
2960
|
+
continue;
|
|
2961
|
+
}
|
|
2962
|
+
const shape2 = PATTERN_VERBS[verb];
|
|
2963
|
+
if (!shape2) continue;
|
|
2964
|
+
const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
|
|
2965
|
+
if (!recognised) out.push(value);
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
const shape = PATTERN_VERBS[verb];
|
|
2969
|
+
const letters = w.slice(1);
|
|
2970
|
+
for (let j = 0; j < letters.length; j++) {
|
|
2971
|
+
const name = `-${letters[j]}`;
|
|
2972
|
+
const isFileFlag = flags.has(name);
|
|
2973
|
+
const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
|
|
2974
|
+
if (!argTaking) continue;
|
|
2975
|
+
const attached = letters.slice(j + 1);
|
|
2976
|
+
if (isFileFlag && attached) out.push(attached);
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return out;
|
|
2981
|
+
}
|
|
2426
2982
|
function wrappedReadPaths(words, name) {
|
|
2427
2983
|
if (name === "find") {
|
|
2428
2984
|
const { k, starts } = findStartPoints(words, 0);
|
|
@@ -2430,7 +2986,14 @@ function wrappedReadPaths(words, name) {
|
|
|
2430
2986
|
}
|
|
2431
2987
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2432
2988
|
const h = unwrapCommandHead(words);
|
|
2433
|
-
|
|
2989
|
+
if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
|
|
2990
|
+
const head = baseWord(words[h]);
|
|
2991
|
+
const rest = words.slice(h + 1);
|
|
2992
|
+
const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
|
|
2993
|
+
return [
|
|
2994
|
+
...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
|
|
2995
|
+
...flagOperandFiles(head, words, h + 1)
|
|
2996
|
+
];
|
|
2434
2997
|
}
|
|
2435
2998
|
function literalShellPayload(words, name) {
|
|
2436
2999
|
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
@@ -4950,8 +5513,8 @@ function matchCanaryArgs(args, values) {
|
|
|
4950
5513
|
|
|
4951
5514
|
// src/scan/canonical.ts
|
|
4952
5515
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
4953
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
4954
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
5516
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v15";
|
|
5517
|
+
var CANONICAL_EXTRACTOR_HASH = "5dad4c8f07121f6c";
|
|
4955
5518
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
4956
5519
|
function extractCanonicalFindings(call, ctx) {
|
|
4957
5520
|
const out = [];
|
|
@@ -5342,6 +5905,7 @@ export {
|
|
|
5342
5905
|
LOOP_MAX_RECORDS,
|
|
5343
5906
|
LOOP_THRESHOLD_FOR_WASTE,
|
|
5344
5907
|
NET_BINARIES,
|
|
5908
|
+
PATTERN_VERB_NAMES,
|
|
5345
5909
|
PRIVILEGE_ESCALATION_RE,
|
|
5346
5910
|
REALTIME_PII_PATTERNS,
|
|
5347
5911
|
SCAN_SIGNAL_WEIGHTS,
|
|
@@ -5378,6 +5942,7 @@ export {
|
|
|
5378
5942
|
extractSessionLevelFindings,
|
|
5379
5943
|
extractShellDestTokens,
|
|
5380
5944
|
extractShellDestinations,
|
|
5945
|
+
fileOperandFlagsOf,
|
|
5381
5946
|
getCompiledRegex,
|
|
5382
5947
|
getNestedValue,
|
|
5383
5948
|
hostMatches,
|
|
@@ -5397,6 +5962,7 @@ export {
|
|
|
5397
5962
|
normalizeIpLiteral,
|
|
5398
5963
|
parseAllSshHostsFromCommand,
|
|
5399
5964
|
parseDestHost,
|
|
5965
|
+
patternShapeOf,
|
|
5400
5966
|
positionedArgs,
|
|
5401
5967
|
previewArgs,
|
|
5402
5968
|
redactText,
|