@node9/proxy 2.14.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -848,6 +848,19 @@ function parseShared(command) {
848
848
  astCache.set(command, parsed);
849
849
  return parsed;
850
850
  }
851
+ function byteOffsetToCharIndex(command) {
852
+ if (!/[^\u0000-\u007F]/.test(command)) return null;
853
+ const map = /* @__PURE__ */ new Map();
854
+ let byte = 0;
855
+ for (let i = 0; i < command.length; ) {
856
+ map.set(byte, i);
857
+ const cp = command.codePointAt(i);
858
+ byte += cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
859
+ i += cp > 65535 ? 2 : 1;
860
+ }
861
+ map.set(byte, command.length);
862
+ return (b) => map.get(b) ?? -1;
863
+ }
851
864
  function cachedNormalize(command, compute) {
852
865
  const hit = normalizeCache.get(command);
853
866
  if (hit !== void 0) {
@@ -877,6 +890,8 @@ function normalizeCommandForPolicyImpl(command) {
877
890
  const f = parseShared(command);
878
891
  if (f === PARSE_FAIL) return { posix: command, separator: command };
879
892
  try {
893
+ const toCharIndex = byteOffsetToCharIndex(command);
894
+ const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
880
895
  const strips = [];
881
896
  const rewrites = [];
882
897
  const quoteOnlyRewrites = [];
@@ -897,8 +912,9 @@ function normalizeCommandForPolicyImpl(command) {
897
912
  const quotedNode = nextParts[0];
898
913
  const nt = syntax.NodeType(quotedNode);
899
914
  const markStrip = () => {
900
- const s = next.Pos().Offset();
901
- const e = next.End().Offset();
915
+ const s = at(next.Pos().Offset());
916
+ const e = at(next.End().Offset());
917
+ if (s < 0 || e < 0) return;
902
918
  strips.push([s, e]);
903
919
  msgSpans.add(`${s}:${e}`);
904
920
  };
@@ -915,8 +931,9 @@ function normalizeCommandForPolicyImpl(command) {
915
931
  }
916
932
  }
917
933
  for (const arg of args) {
918
- const s = arg.Pos().Offset();
919
- const e = arg.End().Offset();
934
+ const s = at(arg.Pos().Offset());
935
+ const e = at(arg.End().Offset());
936
+ if (s < 0 || e < 0) continue;
920
937
  if (msgSpans.has(`${s}:${e}`)) continue;
921
938
  const resolved = resolveWordLiteral(arg);
922
939
  if (resolved === null) continue;
@@ -1178,7 +1195,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1178
1195
  return result?.verdict !== "block";
1179
1196
  }
1180
1197
  if (nodeType !== "CallExpr") return true;
1181
- const { name, flags, paths, words } = extractLiteralArgs(n);
1198
+ const { name, flags, paths, words, args } = extractLiteralArgs(n);
1182
1199
  if (!name) return true;
1183
1200
  if (name === "rm") {
1184
1201
  const flagStr = flags.join("").toLowerCase();
@@ -1218,7 +1235,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1218
1235
  return true;
1219
1236
  }
1220
1237
  }
1221
- const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
1238
+ const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
1222
1239
  if (readPaths) {
1223
1240
  for (const p of readPaths) {
1224
1241
  result = stricter(result, matchSensitivePath2(p));
@@ -1254,9 +1271,22 @@ function flagIs(w, names) {
1254
1271
  const f = flagInfo(w);
1255
1272
  return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
1256
1273
  }
1257
- function operandOf(a, names) {
1274
+ function operandOf(a, names, valueLetters) {
1258
1275
  if (!names || a.afterFlag === null) return false;
1259
- return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
1276
+ const w = a.afterFlag;
1277
+ if (!w.startsWith("--") && valueLetters) {
1278
+ const letters = w.slice(1);
1279
+ for (let i = 0; i < letters.length; i++) {
1280
+ if (!valueLetters.includes(letters[i])) continue;
1281
+ return i === letters.length - 1 && names.includes(letters[i]);
1282
+ }
1283
+ return false;
1284
+ }
1285
+ if (w.startsWith("--") && !w.includes("=")) {
1286
+ const longs = names.filter((n) => n.startsWith("--"));
1287
+ if (longs.some((n) => n === w || w.length >= 3 && n.startsWith(w))) return true;
1288
+ }
1289
+ return flagIs(w, names) && flagInfo(w).attached === null;
1260
1290
  }
1261
1291
  function resolveCopyShape(words, h) {
1262
1292
  const verb = baseWord(words[h]);
@@ -1280,7 +1310,7 @@ function findStartPoints(words, h) {
1280
1310
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
1281
1311
  if (k < 0) return { k, starts: [] };
1282
1312
  const firstPredicate = words.findIndex(
1283
- (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
1313
+ (w, i) => i > h && w !== null && w !== "--" && w.startsWith("-") && !FIND_OPTIONS.has(w)
1284
1314
  );
1285
1315
  const end = firstPredicate > h ? firstPredicate : k;
1286
1316
  return { k, starts: positionalAfter(words, h + 1, end) };
@@ -1300,14 +1330,35 @@ function copySourcePaths(words) {
1300
1330
  const { shape, last } = r;
1301
1331
  const args = positionedArgs(words, last + 1);
1302
1332
  const tail = words.slice(last + 1);
1303
- const skipped = (a) => operandOf(a, shape.skipFlags);
1304
- const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
1305
- const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
1333
+ const copyOptionsEnd = tail.findIndex((w) => w === "--");
1334
+ const copyPastOptions = (a) => copyOptionsEnd >= 0 && a.argv > last + 1 + copyOptionsEnd;
1335
+ const skipped = (a) => !copyPastOptions(a) && operandOf(a, shape.skipFlags, shape.valueLetters);
1336
+ const firstValueLetter = (w) => {
1337
+ const letters = w.slice(1);
1338
+ for (let i = 0; i < letters.length; i++) {
1339
+ if ((shape.valueLetters ?? []).includes(letters[i]))
1340
+ return { letter: letters[i], last: i === letters.length - 1 };
1341
+ }
1342
+ return null;
1343
+ };
1344
+ const isTargetDirFlag = (w) => {
1345
+ if (w.startsWith("--")) {
1346
+ const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
1347
+ return name.length >= 3 && "--target-directory".startsWith(name);
1348
+ }
1349
+ return firstValueLetter(w)?.letter === "t";
1350
+ };
1351
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && isTargetDirFlag(w));
1352
+ const targetTakesNextWord = (w) => {
1353
+ if (w.startsWith("--"))
1354
+ return !w.includes("=") && w.length >= 3 && "--target-directory".startsWith(w);
1355
+ const f = firstValueLetter(w);
1356
+ return f !== null && f.letter === "t" && f.last;
1357
+ };
1358
+ const targetOperand = (a) => targetDir && a.afterFlag !== null && !copyPastOptions(a) && targetTakesNextWord(a.afterFlag);
1306
1359
  const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
1307
1360
  const dynamicDest = lastOperand === null;
1308
1361
  const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
1309
- if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
1310
- return [];
1311
1362
  let src;
1312
1363
  switch (shape.source) {
1313
1364
  case "all":
@@ -1317,11 +1368,17 @@ function copySourcePaths(words) {
1317
1368
  src = targetDir ? args : args.slice(0, 1);
1318
1369
  break;
1319
1370
  case "flagOperand": {
1320
- const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
1371
+ const namesSource = (f) => {
1372
+ const names = shape.sourceFlags ?? [];
1373
+ if (f.long !== null)
1374
+ return names.some(
1375
+ (n) => n.startsWith("--") && (n === f.long || f.long.length >= 3 && n.startsWith(f.long))
1376
+ );
1377
+ return f.letter !== null && names.includes(f.letter);
1378
+ };
1379
+ const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => f.attached);
1321
1380
  return [
1322
- ...args.filter(
1323
- (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
1324
- ).map((a) => a.value),
1381
+ ...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
1325
1382
  ...inline
1326
1383
  ];
1327
1384
  }
@@ -1332,7 +1389,14 @@ function copySourcePaths(words) {
1332
1389
  src = targetDir || dynamicDest ? args : args.slice(0, -1);
1333
1390
  break;
1334
1391
  }
1335
- return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1392
+ const sources = src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
1393
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand)) {
1394
+ const jailedSources = sources.filter((p) => matchSensitivePath2(p));
1395
+ const dirOf = (p) => /[\\/]/.test(p) ? p.replace(/[\\/][^\\/]*$/, "") : "";
1396
+ if (jailedSources.length === 0) return [];
1397
+ if (jailedSources.every((p) => dirOf(p) === dirOf(lastOperand))) return [];
1398
+ }
1399
+ return sources;
1336
1400
  }
1337
1401
  function archiveInputs(kind, args, tail) {
1338
1402
  const first = args[0];
@@ -1344,13 +1408,17 @@ function archiveInputs(kind, args, tail) {
1344
1408
  const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
1345
1409
  if (extracting && !writing) return [];
1346
1410
  void mode;
1347
- let i = 0;
1348
- if (bareKey) {
1349
- i = 1;
1350
- const next = args[1];
1351
- if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
1352
- }
1353
- return args.slice(i);
1411
+ if (!bareKey) return args;
1412
+ let i = 1;
1413
+ const fromDirs = [];
1414
+ for (const ch of first.value) {
1415
+ if (!TAR_VALUE_LETTERS.includes(ch)) continue;
1416
+ const operand = args[i];
1417
+ if (!operand || operand.afterFlag !== null) break;
1418
+ i += 1;
1419
+ if (ch === "C") fromDirs.push(operand);
1420
+ }
1421
+ return [...fromDirs, ...args.slice(i)];
1354
1422
  }
1355
1423
  if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
1356
1424
  if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
@@ -1377,6 +1445,138 @@ function matchSensitivePath2(p) {
1377
1445
  function baseWord(w) {
1378
1446
  return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
1379
1447
  }
1448
+ function namesFlag(name, candidates, known) {
1449
+ if (candidates.has(name)) return true;
1450
+ if (!name.startsWith("--") || name.length < 3) return false;
1451
+ if (known?.has(name)) return false;
1452
+ for (const c of candidates) if (c.startsWith(name)) return true;
1453
+ return false;
1454
+ }
1455
+ function knownLongFlags(verb) {
1456
+ const out = /* @__PURE__ */ new Set();
1457
+ const shape = PATTERN_VERBS[verb];
1458
+ if (shape) {
1459
+ for (const set of [shape.takesValue, shape.noValue, shape.patternFlags, shape.noPatternFlags])
1460
+ for (const f of set) if (f.startsWith("--")) out.add(f);
1461
+ }
1462
+ for (const f of FILE_OPERAND_FLAGS[verb] ?? []) if (f.startsWith("--")) out.add(f);
1463
+ return out;
1464
+ }
1465
+ function flagNamesOf(token, shape) {
1466
+ if (token.startsWith("--")) {
1467
+ const eq = token.indexOf("=");
1468
+ return [eq > 0 ? token.slice(0, eq) : token];
1469
+ }
1470
+ const out = [];
1471
+ for (const c of token.slice(1)) {
1472
+ const name = `-${c}`;
1473
+ out.push(name);
1474
+ if (shape?.takesValue.has(name)) break;
1475
+ }
1476
+ return out;
1477
+ }
1478
+ function flagEffect(token, shape, known) {
1479
+ if (token === "--") return NONE;
1480
+ if (/^-+$/.test(token)) return UNKNOWN;
1481
+ if (/^-\d+$/.test(token)) return NONE;
1482
+ if (token.startsWith("--")) {
1483
+ if (token.includes("=")) return NONE;
1484
+ const takes = namesFlag(token, shape.takesValue, known);
1485
+ const none = namesFlag(token, shape.noValue, known);
1486
+ if (takes && none) return UNKNOWN;
1487
+ if (takes) return { kind: "takes", flag: token };
1488
+ if (none) return NONE;
1489
+ return UNKNOWN;
1490
+ }
1491
+ const letters = token.slice(1);
1492
+ if (!letters) return NONE;
1493
+ for (let i = 0; i < letters.length; i++) {
1494
+ const name = `-${letters[i]}`;
1495
+ if (shape.takesValue.has(name)) {
1496
+ return i === letters.length - 1 ? { kind: "takes", flag: name } : NONE;
1497
+ }
1498
+ if (!shape.noValue.has(name)) return UNKNOWN;
1499
+ }
1500
+ return NONE;
1501
+ }
1502
+ function readTargets(verb, args, flags, words = [], from = 1) {
1503
+ const shape = PATTERN_VERBS[verb];
1504
+ if (!shape) return args.map((a) => a.value);
1505
+ const known = knownLongFlags(verb);
1506
+ const names = flags.flatMap((f) => flagNamesOf(f, shape));
1507
+ const patternElsewhere = names.some(
1508
+ (n) => namesFlag(n, shape.patternFlags, known) || namesFlag(n, shape.noPatternFlags, known)
1509
+ );
1510
+ const excused = /* @__PURE__ */ new Set();
1511
+ const fileFlags = FILE_OPERAND_FLAGS[verb];
1512
+ const optionsEnd = words.findIndex((w, i) => i >= from && w === "--");
1513
+ const pastOptions = (a) => optionsEnd >= 0 && a.argv > optionsEnd;
1514
+ for (const a of args) {
1515
+ if (a.afterFlag === null || pastOptions(a)) continue;
1516
+ const e = flagEffect(a.afterFlag, shape, known);
1517
+ if (e.kind !== "takes") continue;
1518
+ if (fileFlags && namesFlag(e.flag, fileFlags, known)) continue;
1519
+ excused.add(a);
1520
+ }
1521
+ const patternArgv = (() => {
1522
+ for (let i = from; i < words.length; i++) {
1523
+ const w = words[i];
1524
+ if (w === null) return -1;
1525
+ if (w === "--") return i + 1;
1526
+ if (w.startsWith("-")) {
1527
+ const e = flagEffect(w, shape, known);
1528
+ if (e.kind === "takes") i += 1;
1529
+ else if (e.kind === "unknown") return -1;
1530
+ continue;
1531
+ }
1532
+ return i;
1533
+ }
1534
+ return -1;
1535
+ })();
1536
+ if (!patternElsewhere && patternArgv >= 0) {
1537
+ const a = args.find((x) => x.argv === patternArgv);
1538
+ if (a) excused.add(a);
1539
+ }
1540
+ return args.filter((a) => !excused.has(a)).map((a) => a.value);
1541
+ }
1542
+ function flagOperandFiles(verb, words, from) {
1543
+ const flags = FILE_OPERAND_FLAGS[verb];
1544
+ if (!flags) return [];
1545
+ const known = knownLongFlags(verb);
1546
+ const out = [];
1547
+ for (let i = from; i < words.length; i++) {
1548
+ const w = words[i];
1549
+ if (w === null || !w.startsWith("-") || w === "--") continue;
1550
+ if (w.startsWith("--")) {
1551
+ const eq = w.indexOf("=");
1552
+ if (eq <= 0) continue;
1553
+ const name = w.slice(0, eq);
1554
+ const value = w.slice(eq + 1);
1555
+ if (!value) continue;
1556
+ if (namesFlag(name, flags, known)) {
1557
+ out.push(value);
1558
+ continue;
1559
+ }
1560
+ const shape2 = PATTERN_VERBS[verb];
1561
+ if (!shape2) continue;
1562
+ const recognised = namesFlag(name, shape2.takesValue, known) || namesFlag(name, shape2.noValue, known) || namesFlag(name, shape2.patternFlags, known) || namesFlag(name, shape2.noPatternFlags, known);
1563
+ if (!recognised) out.push(value);
1564
+ continue;
1565
+ }
1566
+ const shape = PATTERN_VERBS[verb];
1567
+ const letters = w.slice(1);
1568
+ for (let j = 0; j < letters.length; j++) {
1569
+ const name = `-${letters[j]}`;
1570
+ const isFileFlag = flags.has(name);
1571
+ const argTaking = isFileFlag || (shape?.takesValue.has(name) ?? false) || (READER_VALUE_LETTERS[verb] ?? []).includes(letters[j]);
1572
+ if (!argTaking) continue;
1573
+ const attached = letters.slice(j + 1);
1574
+ if (isFileFlag && attached) out.push(attached);
1575
+ break;
1576
+ }
1577
+ }
1578
+ return out;
1579
+ }
1380
1580
  function wrappedReadPaths(words, name) {
1381
1581
  if (name === "find") {
1382
1582
  const { k, starts } = findStartPoints(words, 0);
@@ -1384,7 +1584,14 @@ function wrappedReadPaths(words, name) {
1384
1584
  }
1385
1585
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
1386
1586
  const h = unwrapCommandHead(words);
1387
- return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
1587
+ if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
1588
+ const head = baseWord(words[h]);
1589
+ const rest = words.slice(h + 1);
1590
+ const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
1591
+ return [
1592
+ ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
1593
+ ...flagOperandFiles(head, words, h + 1)
1594
+ ];
1388
1595
  }
1389
1596
  function literalShellPayload(words, name) {
1390
1597
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
@@ -1729,7 +1936,7 @@ function matchCanaryArgs(args, values) {
1729
1936
  return null;
1730
1937
  }
1731
1938
  }
1732
- var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, SCP_VALUE_FLAGS, RSYNC_SKIP, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, SOURCE_COMMANDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
1939
+ var B58, B58_INDEX, XPRV_VERSIONS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, SOURCE_COMMANDS, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES;
1733
1940
  var init_dist = __esm({
1734
1941
  "packages/policy-engine/dist/index.mjs"() {
1735
1942
  "use strict";
@@ -2330,7 +2537,345 @@ var init_dist = __esm({
2330
2537
  "nl",
2331
2538
  "dd"
2332
2539
  ]);
2540
+ GREP_SHAPE = {
2541
+ takesValue: /* @__PURE__ */ new Set([
2542
+ "-A",
2543
+ "-B",
2544
+ "-C",
2545
+ "-D",
2546
+ "-d",
2547
+ "-e",
2548
+ "-f",
2549
+ "-m",
2550
+ "--after-context",
2551
+ "--before-context",
2552
+ "--binary-files",
2553
+ "--context",
2554
+ "--devices",
2555
+ "--directories",
2556
+ "--exclude",
2557
+ "--exclude-dir",
2558
+ "--exclude-from",
2559
+ "--file",
2560
+ "--include",
2561
+ "--label",
2562
+ "--max-count",
2563
+ "--regexp"
2564
+ ]),
2565
+ noValue: /* @__PURE__ */ new Set([
2566
+ "-E",
2567
+ "-F",
2568
+ "-G",
2569
+ "-P",
2570
+ "-i",
2571
+ "-y",
2572
+ "-v",
2573
+ "-V",
2574
+ "-w",
2575
+ "-x",
2576
+ "-c",
2577
+ "-l",
2578
+ "-L",
2579
+ "-o",
2580
+ "-q",
2581
+ "-s",
2582
+ "-b",
2583
+ "-H",
2584
+ "-h",
2585
+ "-n",
2586
+ "-T",
2587
+ "-Z",
2588
+ "-z",
2589
+ "-R",
2590
+ "-r",
2591
+ "-U",
2592
+ "-u",
2593
+ "-I",
2594
+ "-a",
2595
+ "--basic-regexp",
2596
+ "--binary",
2597
+ "--byte-offset",
2598
+ "--color",
2599
+ "--colour",
2600
+ "--count",
2601
+ "--dereference-recursive",
2602
+ "--extended-regexp",
2603
+ "--files-with-matches",
2604
+ "--files-without-match",
2605
+ "--fixed-strings",
2606
+ "--help",
2607
+ "--ignore-case",
2608
+ "--initial-tab",
2609
+ "--invert-match",
2610
+ "--line-buffered",
2611
+ "--line-number",
2612
+ "--line-regexp",
2613
+ "--no-filename",
2614
+ "--no-group-separator",
2615
+ "--no-ignore-case",
2616
+ "--no-messages",
2617
+ "--null",
2618
+ "--null-data",
2619
+ "--only-matching",
2620
+ "--perl-regexp",
2621
+ "--quiet",
2622
+ "--recursive",
2623
+ "--silent",
2624
+ "--text",
2625
+ "--version",
2626
+ "--with-filename",
2627
+ "--word-regexp"
2628
+ ]),
2629
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2630
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file"])
2631
+ };
2632
+ PATTERN_VERBS = {
2633
+ grep: GREP_SHAPE,
2634
+ // /usr/bin/egrep and /usr/bin/fgrep are 41-byte shell wrappers that exec
2635
+ // `grep -E` and `grep -F`. Read in full, not assumed.
2636
+ egrep: GREP_SHAPE,
2637
+ fgrep: GREP_SHAPE,
2638
+ // ripgrep 14.1.1, complete from its own --help. An earlier comment here said rg
2639
+ // was not installed on the measuring machine; it is, and that stale claim is why
2640
+ // this table shipped incomplete for two rounds, blocking ordinary searches like
2641
+ // `rg --sort-files .env src` (/code-review round 4).
2642
+ rg: {
2643
+ takesValue: /* @__PURE__ */ new Set([
2644
+ "-A",
2645
+ "-B",
2646
+ "-C",
2647
+ "-d",
2648
+ "-E",
2649
+ "-e",
2650
+ "-f",
2651
+ "-g",
2652
+ "-j",
2653
+ "-M",
2654
+ "-m",
2655
+ "-r",
2656
+ "-t",
2657
+ "-T",
2658
+ "--after-context",
2659
+ "--before-context",
2660
+ "--color",
2661
+ "--colors",
2662
+ "--context",
2663
+ "--context-separator",
2664
+ "--dfa-size-limit",
2665
+ "--encoding",
2666
+ "--engine",
2667
+ "--field-context-separator",
2668
+ "--field-match-separator",
2669
+ "--file",
2670
+ "--generate",
2671
+ "--glob",
2672
+ "--hostname-bin",
2673
+ "--hyperlink-format",
2674
+ "--iglob",
2675
+ "--ignore-file",
2676
+ "--max-columns",
2677
+ "--max-count",
2678
+ "--max-depth",
2679
+ // An ALIAS of --max-depth, and it takes a value. The round-5 extraction read
2680
+ // alias lines as plain switches and swept it into noValue, which hard-blocked
2681
+ // `rg --maxdepth 2 .env src` while `--max-depth 2` ran (/code-review round 7).
2682
+ "--maxdepth",
2683
+ "--max-filesize",
2684
+ "--path-separator",
2685
+ "--pre",
2686
+ "--pre-glob",
2687
+ "--regexp",
2688
+ "--regex-size-limit",
2689
+ "--replace",
2690
+ "--sort",
2691
+ "--sortr",
2692
+ "--threads",
2693
+ "--type",
2694
+ "--type-add",
2695
+ "--type-clear",
2696
+ "--type-not"
2697
+ ]),
2698
+ noValue: /* @__PURE__ */ new Set([
2699
+ "-.",
2700
+ "-0",
2701
+ "-a",
2702
+ "-b",
2703
+ "-c",
2704
+ "-F",
2705
+ "-h",
2706
+ "-H",
2707
+ "-i",
2708
+ "-I",
2709
+ "-l",
2710
+ "-L",
2711
+ "-n",
2712
+ "-N",
2713
+ "-o",
2714
+ "-p",
2715
+ "-P",
2716
+ "-q",
2717
+ "-s",
2718
+ "-S",
2719
+ "-u",
2720
+ "-U",
2721
+ "-v",
2722
+ "-V",
2723
+ "-w",
2724
+ "-x",
2725
+ "-z",
2726
+ "--auto-hybrid-regex",
2727
+ "--binary",
2728
+ "--block-buffered",
2729
+ "--byte-offset",
2730
+ "--case-sensitive",
2731
+ "--column",
2732
+ "--count",
2733
+ "--count-matches",
2734
+ "--crlf",
2735
+ "--debug",
2736
+ "--files",
2737
+ "--files-with-matches",
2738
+ "--files-without-match",
2739
+ "--fixed-strings",
2740
+ "--follow",
2741
+ "--glob-case-insensitive",
2742
+ "--heading",
2743
+ "--help",
2744
+ "--hidden",
2745
+ "--ignore-case",
2746
+ "--ignore-file-case-insensitive",
2747
+ "--include-zero",
2748
+ "--invert-match",
2749
+ "--json",
2750
+ "--line-buffered",
2751
+ "--line-number",
2752
+ "--line-regexp",
2753
+ "--max-columns-preview",
2754
+ "--mmap",
2755
+ "--multiline",
2756
+ "--multiline-dotall",
2757
+ "--no-column",
2758
+ "--no-config",
2759
+ "--no-context-separator",
2760
+ "--no-encoding",
2761
+ "--no-filename",
2762
+ "--no-ignore",
2763
+ "--no-ignore-dot",
2764
+ "--no-ignore-exclude",
2765
+ "--no-ignore-files",
2766
+ "--no-ignore-global",
2767
+ "--no-ignore-messages",
2768
+ "--no-ignore-parent",
2769
+ "--no-ignore-vcs",
2770
+ "--no-line-number",
2771
+ "--no-messages",
2772
+ "--no-pcre2-unicode",
2773
+ "--no-pre",
2774
+ "--no-require-git",
2775
+ "--no-unicode",
2776
+ "--null",
2777
+ "--null-data",
2778
+ "--one-file-system",
2779
+ "--only-matching",
2780
+ "--passthru",
2781
+ "--pcre2",
2782
+ "--pcre2-version",
2783
+ "--pretty",
2784
+ "--print0",
2785
+ "--quiet",
2786
+ "--search-zip",
2787
+ "--smart-case",
2788
+ "--sort-files",
2789
+ "--stats",
2790
+ "--stop-on-nonmatch",
2791
+ "--text",
2792
+ "--trace",
2793
+ "--trim",
2794
+ "--type-list",
2795
+ "--unrestricted",
2796
+ "--version",
2797
+ "--vimgrep",
2798
+ "--with-filename",
2799
+ "--word-regexp",
2800
+ // The COMPLETE remainder of `rg --help`, added after /code-review round 5
2801
+ // found 40 documented switches in neither set. The table is now the whole
2802
+ // option list: `rg --help` yields 149 long options, 35 of them value-taking.
2803
+ "--ignore",
2804
+ "--ignore-dot",
2805
+ "--ignore-exclude",
2806
+ "--ignore-files",
2807
+ "--ignore-global",
2808
+ "--ignore-messages",
2809
+ "--ignore-parent",
2810
+ "--ignore-vcs",
2811
+ "--messages",
2812
+ "--no-auto-hybrid-regex",
2813
+ "--no-binary",
2814
+ "--no-block-buffered",
2815
+ "--no-byte-offset",
2816
+ "--no-crlf",
2817
+ "--no-fixed-strings",
2818
+ "--no-follow",
2819
+ "--no-glob-case-insensitive",
2820
+ "--no-heading",
2821
+ "--no-hidden",
2822
+ "--no-ignore-file-case-insensitive",
2823
+ "--no-include-zero",
2824
+ "--no-invert-match",
2825
+ "--no-json",
2826
+ "--no-line-buffered",
2827
+ "--no-max-columns-preview",
2828
+ "--no-mmap",
2829
+ "--no-multiline",
2830
+ "--no-multiline-dotall",
2831
+ "--no-one-file-system",
2832
+ "--no-pcre2",
2833
+ "--no-search-zip",
2834
+ "--no-sort-files",
2835
+ "--no-stats",
2836
+ "--no-text",
2837
+ "--no-trim",
2838
+ "--passthrough",
2839
+ "--pcre2-unicode",
2840
+ "--require-git",
2841
+ "--unicode"
2842
+ ]),
2843
+ patternFlags: /* @__PURE__ */ new Set(["-e", "--regexp"]),
2844
+ // `--files` and `--type-list` list or enumerate without a pattern. Founder
2845
+ // decision 2026-09-13: `rg --files ~/.ssh` stays BLOCKED, which this achieves
2846
+ // by leaving the directory in the judged list.
2847
+ noPatternFlags: /* @__PURE__ */ new Set(["-f", "--file", "--files", "--type-list", "--pcre2-version"])
2848
+ }
2849
+ };
2850
+ PATTERN_VERB_NAMES = Object.keys(PATTERN_VERBS);
2851
+ GREP_FILE_OPERANDS = /* @__PURE__ */ new Set(["-f", "--file", "--exclude-from", "--include"]);
2852
+ READER_VALUE_LETTERS = {
2853
+ awk: ["F", "v", "f"],
2854
+ gawk: ["F", "v", "f", "e", "E", "i", "l", "o", "p", "D"],
2855
+ sed: ["e", "f", "i", "l"],
2856
+ sort: ["C", "k", "o", "S", "t", "T"]
2857
+ };
2858
+ FILE_OPERAND_FLAGS = {
2859
+ grep: GREP_FILE_OPERANDS,
2860
+ egrep: GREP_FILE_OPERANDS,
2861
+ fgrep: GREP_FILE_OPERANDS,
2862
+ // rg and gawk are NOT installed on the measuring machine: these two rows are
2863
+ // from the shipped documentation (ripgrep `-f/--file`, `--ignore-file`; gawk
2864
+ // `-f/--file`) and are marked as such in the spec.
2865
+ // `-g/--glob/--iglob` name which files ripgrep SEARCHES, so an operand naming
2866
+ // a credential makes rg open it -- the same reason grep's `--include` is here.
2867
+ // Measured (/code-review round 3): `rg --hidden -g .env AWS tree` opened .env
2868
+ // and printed its contents.
2869
+ rg: /* @__PURE__ */ new Set(["-f", "--file", "--ignore-file", "-g", "--glob", "--iglob"]),
2870
+ sed: /* @__PURE__ */ new Set(["-f", "--file"]),
2871
+ awk: /* @__PURE__ */ new Set(["-f", "--file"]),
2872
+ gawk: /* @__PURE__ */ new Set(["-f", "--file"]),
2873
+ sort: /* @__PURE__ */ new Set(["--files0-from"])
2874
+ };
2333
2875
  SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
2876
+ TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
2877
+ ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
2878
+ RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
2334
2879
  RSYNC_SKIP = [
2335
2880
  "e",
2336
2881
  "--rsh",
@@ -2342,21 +2887,35 @@ var init_dist = __esm({
2342
2887
  "f",
2343
2888
  "--filter"
2344
2889
  ];
2890
+ CP_VALUE_LETTERS = ["S", "t"];
2891
+ INSTALL_VALUE_LETTERS = ["S", "t", "g", "m", "o"];
2345
2892
  COPY_VERBS = {
2346
- cp: { source: "allButLast", targetDirFlag: true },
2347
- mv: { source: "allButLast", targetDirFlag: true },
2348
- install: { source: "allButLast", targetDirFlag: true },
2349
- ln: { source: "first", targetDirFlag: true },
2350
- scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
2351
- rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
2893
+ cp: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2894
+ mv: { source: "allButLast", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2895
+ install: { source: "allButLast", targetDirFlag: true, valueLetters: INSTALL_VALUE_LETTERS },
2896
+ ln: { source: "first", targetDirFlag: true, valueLetters: CP_VALUE_LETTERS },
2897
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS, valueLetters: SCP_VALUE_FLAGS },
2898
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP, valueLetters: RSYNC_VALUE_LETTERS },
2352
2899
  tar: {
2353
2900
  source: "archive",
2354
2901
  archive: "tar",
2355
- skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
2902
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"],
2903
+ valueLetters: TAR_VALUE_LETTERS
2904
+ },
2905
+ zip: {
2906
+ source: "archive",
2907
+ archive: "zip",
2908
+ skipFlags: ["x", "i", "--exclude", "--include"],
2909
+ valueLetters: ZIP_VALUE_LETTERS
2356
2910
  },
2357
- zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
2358
2911
  ar: { source: "archive", archive: "ar" },
2359
- "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
2912
+ // 7z switches are INLINE only (`-mx9`, `-px`, `-x!pat`), so no following word is
2913
+ // ever a switch's operand -- which is why the short `x` is NOT a skipFlag here
2914
+ // and valueLetters is empty. Keeping the short `x` made `7z a out.7z -mx KEY`
2915
+ // drop the credential as an exclusion operand (/code-review round 8), and the
2916
+ // derived "every skipped letter is a value letter" row in jail-copy.spec.ts is
2917
+ // what keeps the two tables honest about it.
2918
+ "7z": { source: "archive", archive: "7z", skipFlags: ["--exclude"], valueLetters: [] },
2360
2919
  gzip: { source: "all" },
2361
2920
  bzip2: { source: "all" },
2362
2921
  xz: { source: "all" },
@@ -2567,6 +3126,8 @@ var init_dist = __esm({
2567
3126
  };
2568
3127
  isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2569
3128
  positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
3129
+ NONE = { kind: "none" };
3130
+ UNKNOWN = { kind: "unknown" };
2570
3131
  SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
2571
3132
  SSRF_MAX_HOST = 253;
2572
3133
  METADATA_ADDRESSES = /* @__PURE__ */ new Set([