@node9/policy-engine 1.67.0 → 1.67.2

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.js CHANGED
@@ -66,6 +66,7 @@ __export(src_exports, {
66
66
  detectArgsPii: () => detectArgsPii,
67
67
  detectDangerousEval: () => detectDangerousEval,
68
68
  detectDangerousShellExec: () => detectDangerousShellExec,
69
+ detectInlineExec: () => detectInlineExec,
69
70
  detectPii: () => detectPii,
70
71
  evaluateEgress: () => evaluateEgress,
71
72
  evaluateLoopWindow: () => evaluateLoopWindow,
@@ -84,6 +85,7 @@ __export(src_exports, {
84
85
  isIgnoredTool: () => isIgnoredTool,
85
86
  isPrivateHost: () => isPrivateHost,
86
87
  isProtectedHomePath: () => isProtectedHomePath,
88
+ isShellShapedTool: () => isShellShapedTool,
87
89
  isShieldVerdict: () => isShieldVerdict,
88
90
  matchSensitivePath: () => matchSensitivePath,
89
91
  matchesPattern: () => matchesPattern,
@@ -101,6 +103,7 @@ __export(src_exports, {
101
103
  summarizeBlast: () => summarizeBlast,
102
104
  summarizeScan: () => summarizeScan,
103
105
  toScanFinding: () => toScanFinding,
106
+ toolMatchesRule: () => toolMatchesRule,
104
107
  truncateBlastPath: () => truncateBlastPath,
105
108
  validateOverrides: () => validateOverrides,
106
109
  validateRegex: () => validateRegex,
@@ -806,6 +809,139 @@ function redactText(text) {
806
809
 
807
810
  // src/shell/index.ts
808
811
  var import_mvdan_sh = __toESM(require("mvdan-sh"));
812
+
813
+ // src/rules/index.ts
814
+ var import_picomatch = __toESM(require("picomatch"));
815
+
816
+ // src/utils/regex.ts
817
+ var import_safe_regex22 = __toESM(require("safe-regex2"));
818
+ var MAX_REGEX_LENGTH = 256;
819
+ var REGEX_CACHE_MAX = 500;
820
+ var regexCache = /* @__PURE__ */ new Map();
821
+ function validateRegex(pattern) {
822
+ if (!pattern) return "Pattern is required";
823
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
824
+ try {
825
+ new RegExp(pattern);
826
+ } catch (e) {
827
+ return `Invalid regex syntax: ${e.message}`;
828
+ }
829
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
830
+ if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
831
+ return null;
832
+ }
833
+ function getCompiledRegex(pattern, flags = "") {
834
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
835
+ const key = `${pattern}\0${flags}`;
836
+ if (regexCache.has(key)) {
837
+ const cached = regexCache.get(key);
838
+ regexCache.delete(key);
839
+ regexCache.set(key, cached);
840
+ return cached;
841
+ }
842
+ if (validateRegex(pattern) !== null) return null;
843
+ try {
844
+ const re = new RegExp(pattern, flags);
845
+ if (regexCache.size >= REGEX_CACHE_MAX) {
846
+ const oldest = regexCache.keys().next().value;
847
+ if (oldest) regexCache.delete(oldest);
848
+ }
849
+ regexCache.set(key, re);
850
+ return re;
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+
856
+ // src/rules/index.ts
857
+ function matchesPattern(text, patterns) {
858
+ const p = Array.isArray(patterns) ? patterns : [patterns];
859
+ if (p.length === 0) return false;
860
+ const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
861
+ const target = text.toLowerCase();
862
+ const directMatch = isMatch(target);
863
+ if (directMatch) return true;
864
+ const withoutDotSlash = text.replace(/^\.\//, "");
865
+ return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
866
+ }
867
+ var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
868
+ function getNestedValue(obj, path) {
869
+ if (!obj || typeof obj !== "object") return null;
870
+ const segments = path.split(".");
871
+ for (const seg of segments) {
872
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
873
+ }
874
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
875
+ }
876
+ function evaluateSmartConditions(args, rule) {
877
+ if (!rule.conditions || rule.conditions.length === 0) return true;
878
+ const mode = rule.conditionMode ?? "all";
879
+ const fieldCache = /* @__PURE__ */ new Map();
880
+ const resolveField = (field) => {
881
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
882
+ const rawVal = getNestedValue(args, field);
883
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
884
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
885
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
886
+ fieldCache.set(field, val);
887
+ return val;
888
+ };
889
+ const readingsCache = /* @__PURE__ */ new Map();
890
+ const resolveFieldReadings = (field) => {
891
+ const cached = readingsCache.get(field);
892
+ if (cached) return cached;
893
+ const primary = resolveField(field);
894
+ if (primary === null) {
895
+ readingsCache.set(field, []);
896
+ return [];
897
+ }
898
+ let out = [primary];
899
+ if (field === "command") {
900
+ const raw = getNestedValue(args, field);
901
+ if (typeof raw === "string") {
902
+ const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
903
+ out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
904
+ }
905
+ }
906
+ readingsCache.set(field, out);
907
+ return out;
908
+ };
909
+ const results = rule.conditions.map((cond) => {
910
+ const val = resolveField(cond.field);
911
+ switch (cond.op) {
912
+ case "exists":
913
+ return val !== null && val !== "";
914
+ case "notExists":
915
+ return val === null || val === "";
916
+ case "contains":
917
+ return val !== null && cond.value ? val.includes(cond.value) : false;
918
+ case "notContains":
919
+ return val !== null && cond.value ? !val.includes(cond.value) : true;
920
+ case "matches": {
921
+ if (val === null || !cond.value) return false;
922
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
923
+ if (!reM) return false;
924
+ return resolveFieldReadings(cond.field).some((v) => reM.test(v));
925
+ }
926
+ case "notMatches": {
927
+ if (!cond.value) return false;
928
+ if (val === null) return true;
929
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
930
+ if (!reN) return false;
931
+ return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
932
+ }
933
+ case "matchesGlob":
934
+ return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
935
+ case "notMatchesGlob":
936
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
937
+ default:
938
+ return false;
939
+ }
940
+ });
941
+ return mode === "any" ? results.some((r) => r) : results.every((r) => r);
942
+ }
943
+
944
+ // src/shell/index.ts
809
945
  var { syntax } = import_mvdan_sh.default;
810
946
  var sharedParser = syntax.NewParser();
811
947
  var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -878,14 +1014,22 @@ function cachedNormalize(command, compute) {
878
1014
  return result;
879
1015
  }
880
1016
  function normalizeCommandForPolicy(command) {
1017
+ return commandReadingsImpl(command).posix;
1018
+ }
1019
+ function commandReadings(command) {
1020
+ const r = commandReadingsImpl(command);
1021
+ return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
1022
+ }
1023
+ function commandReadingsImpl(command) {
881
1024
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
882
1025
  }
883
1026
  function normalizeCommandForPolicyImpl(command) {
884
1027
  const f = parseShared(command);
885
- if (f === PARSE_FAIL) return command;
1028
+ if (f === PARSE_FAIL) return { posix: command, separator: command };
886
1029
  try {
887
1030
  const strips = [];
888
1031
  const rewrites = [];
1032
+ const quoteOnlyRewrites = [];
889
1033
  const msgSpans = /* @__PURE__ */ new Set();
890
1034
  syntax.Walk(f, (node) => {
891
1035
  if (!node) return false;
@@ -930,22 +1074,23 @@ function normalizeCommandForPolicyImpl(command) {
930
1074
  if (resolved === source) continue;
931
1075
  if (resolved === "" || /\s/.test(resolved)) continue;
932
1076
  rewrites.push([s, e, resolved]);
1077
+ const quoteOnly = source.replace(/['"]/g, "");
1078
+ if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
933
1079
  }
934
1080
  return true;
935
1081
  });
936
- const edits = [
937
- ...strips.map(([s, e]) => [s, e, '""']),
938
- ...rewrites
939
- ];
940
- if (edits.length === 0) return command;
941
- edits.sort((a, b) => b[0] - a[0]);
942
- let result = command;
943
- for (const [s, e, rep] of edits) {
944
- result = result.slice(0, s) + rep + result.slice(e);
945
- }
946
- return result;
1082
+ const stripEdits = strips.map(([s, e]) => [s, e, '""']);
1083
+ const apply = (extra) => {
1084
+ const edits = [...stripEdits, ...extra];
1085
+ if (edits.length === 0) return command;
1086
+ edits.sort((a, b) => b[0] - a[0]);
1087
+ let out = command;
1088
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
1089
+ return out;
1090
+ };
1091
+ return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
947
1092
  } catch {
948
- return command;
1093
+ return { posix: command, separator: command };
949
1094
  }
950
1095
  }
951
1096
  function scanArgsForDynamicExec(args, startIdx) {
@@ -1187,6 +1332,208 @@ function chmodHasOpenPermMode(command) {
1187
1332
  }
1188
1333
  return found;
1189
1334
  }
1335
+ function isShellShapedTool(toolName, toolInspection) {
1336
+ if (isBashTool(toolName)) return true;
1337
+ if (!toolInspection) return false;
1338
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
1339
+ return pattern !== void 0 && toolInspection[pattern] === "command";
1340
+ }
1341
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
1342
+ if (!ruleTool) return true;
1343
+ if (matchesPattern(toolName, ruleTool)) return true;
1344
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
1345
+ }
1346
+ var INLINE_INTERPRETER = /^(python[\d.]*|perl|ruby|node|tsx|ts-node|php|lua|deno|bun|pwsh|powershell(?:\.exe)?|osascript|rscript|irb|bash|sh|zsh|script|su)$/i;
1347
+ var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
1348
+ "uv",
1349
+ "uvx",
1350
+ "poetry",
1351
+ "pipenv",
1352
+ "pdm",
1353
+ "rye",
1354
+ "hatch",
1355
+ "conda",
1356
+ "mamba",
1357
+ "micromamba",
1358
+ "npx",
1359
+ "pnpm",
1360
+ "yarn",
1361
+ "bunx",
1362
+ "watch",
1363
+ "strace",
1364
+ "ltrace",
1365
+ "chroot",
1366
+ "unshare",
1367
+ "runuser"
1368
+ ]);
1369
+ function isInlineCodeFlag(interp, w) {
1370
+ const lw = w.toLowerCase();
1371
+ if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
1372
+ if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
1373
+ if (!w.startsWith("-") || w.startsWith("--")) return false;
1374
+ const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
1375
+ const body = lw.slice(1);
1376
+ const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
1377
+ const cut = body.search(cutAt);
1378
+ const bundle = cut >= 0 ? body.slice(0, cut) : body;
1379
+ return [...codeLetters].some((l) => bundle.includes(l));
1380
+ }
1381
+ var _redirStdinOps = null;
1382
+ function redirStdinOps() {
1383
+ if (_redirStdinOps) return _redirStdinOps;
1384
+ _redirStdinOps = new Set(
1385
+ [
1386
+ deriveRedirOp("cat <<X\nX"),
1387
+ deriveRedirOp("cat <<-X\nX"),
1388
+ deriveRedirOp("cat < f"),
1389
+ deriveRedirOp("cat <<< x")
1390
+ ].filter((op) => op >= 0)
1391
+ );
1392
+ return _redirStdinOps;
1393
+ }
1394
+ function deriveBinaryOp(sample) {
1395
+ try {
1396
+ const f = sharedParser.Parse(sample, "cmd");
1397
+ let op = -1;
1398
+ syntax.Walk(f, (node) => {
1399
+ const n = node;
1400
+ if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
1401
+ return true;
1402
+ });
1403
+ return op;
1404
+ } catch {
1405
+ return -1;
1406
+ }
1407
+ }
1408
+ var _listOps = null;
1409
+ function listOps() {
1410
+ if (_listOps) return _listOps;
1411
+ _listOps = new Set(
1412
+ [deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
1413
+ );
1414
+ return _listOps;
1415
+ }
1416
+ var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
1417
+ var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
1418
+ function unwrapCommandHead(words) {
1419
+ let i = 0;
1420
+ while (i < words.length) {
1421
+ const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
1422
+ if (head === "find") {
1423
+ const x = words.findIndex(
1424
+ (w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
1425
+ );
1426
+ if (x < 0) break;
1427
+ i = x + 1;
1428
+ continue;
1429
+ }
1430
+ if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
1431
+ i++;
1432
+ let targetConsumed = false;
1433
+ while (i < words.length) {
1434
+ const t = words[i];
1435
+ if (t === null) {
1436
+ i++;
1437
+ continue;
1438
+ }
1439
+ const lt = t.toLowerCase();
1440
+ if (/^[A-Za-z_]\w*=/.test(t)) {
1441
+ i++;
1442
+ continue;
1443
+ }
1444
+ if (t.startsWith("-")) {
1445
+ i++;
1446
+ const nxt = words[i];
1447
+ if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
1448
+ i++;
1449
+ continue;
1450
+ }
1451
+ if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
1452
+ i++;
1453
+ continue;
1454
+ }
1455
+ if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
1456
+ targetConsumed = true;
1457
+ i++;
1458
+ continue;
1459
+ }
1460
+ break;
1461
+ }
1462
+ }
1463
+ return i;
1464
+ }
1465
+ function inlineExecStmt(stmt, pipeFed) {
1466
+ const cmd = stmt?.Cmd;
1467
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
1468
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
1469
+ if (words.length === 0) return false;
1470
+ const headIdx = unwrapCommandHead(words);
1471
+ const rawHead = words[headIdx];
1472
+ if (rawHead == null) return false;
1473
+ const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
1474
+ if (!INLINE_INTERPRETER.test(interp)) return false;
1475
+ let args = words.slice(headIdx + 1);
1476
+ if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
1477
+ if (INTERP_LEADING_TARGET.has(interp)) {
1478
+ const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
1479
+ args = firstFlag >= 0 ? args.slice(firstFlag) : [];
1480
+ }
1481
+ let positionals = 0;
1482
+ let selectedProgram = false;
1483
+ for (const a of args) {
1484
+ if (a == null) {
1485
+ positionals++;
1486
+ selectedProgram = true;
1487
+ continue;
1488
+ }
1489
+ if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
1490
+ if (a === "-m") {
1491
+ selectedProgram = true;
1492
+ continue;
1493
+ }
1494
+ if (a === "-" && !selectedProgram) return true;
1495
+ if (!a.startsWith("-")) {
1496
+ positionals++;
1497
+ selectedProgram = true;
1498
+ }
1499
+ }
1500
+ const redirs = stmt.Redirs || cmd.Redirs || [];
1501
+ const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
1502
+ if (positionals === 0 && (stdinFed || pipeFed)) {
1503
+ if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
1504
+ }
1505
+ return false;
1506
+ }
1507
+ function detectInlineExec(command) {
1508
+ const f = parseShared(command);
1509
+ if (f === PARSE_FAIL) {
1510
+ return /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i.test(
1511
+ command
1512
+ );
1513
+ }
1514
+ let found = false;
1515
+ try {
1516
+ syntax.Walk(f, (node) => {
1517
+ if (!node || found) return false;
1518
+ const n = node;
1519
+ const t = syntax.NodeType(n);
1520
+ if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
1521
+ if (inlineExecStmt(n.Y, true)) {
1522
+ found = true;
1523
+ return false;
1524
+ }
1525
+ }
1526
+ if (t === "Stmt" && inlineExecStmt(n, false)) {
1527
+ found = true;
1528
+ return false;
1529
+ }
1530
+ return true;
1531
+ });
1532
+ } catch {
1533
+ return found;
1534
+ }
1535
+ return found;
1536
+ }
1190
1537
  function analyzeChmod777(command) {
1191
1538
  if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
1192
1539
  if (!chmodHasOpenPermMode(command)) return null;
@@ -2064,117 +2411,6 @@ function parseAllSshHostsFromCommand(command) {
2064
2411
  return extractAllSshHosts(tokens.slice(1));
2065
2412
  }
2066
2413
 
2067
- // src/rules/index.ts
2068
- var import_picomatch = __toESM(require("picomatch"));
2069
-
2070
- // src/utils/regex.ts
2071
- var import_safe_regex22 = __toESM(require("safe-regex2"));
2072
- var MAX_REGEX_LENGTH = 100;
2073
- var REGEX_CACHE_MAX = 500;
2074
- var regexCache = /* @__PURE__ */ new Map();
2075
- function validateRegex(pattern) {
2076
- if (!pattern) return "Pattern is required";
2077
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
2078
- try {
2079
- new RegExp(pattern);
2080
- } catch (e) {
2081
- return `Invalid regex syntax: ${e.message}`;
2082
- }
2083
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
2084
- if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
2085
- return null;
2086
- }
2087
- function getCompiledRegex(pattern, flags = "") {
2088
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
2089
- const key = `${pattern}\0${flags}`;
2090
- if (regexCache.has(key)) {
2091
- const cached = regexCache.get(key);
2092
- regexCache.delete(key);
2093
- regexCache.set(key, cached);
2094
- return cached;
2095
- }
2096
- if (validateRegex(pattern) !== null) return null;
2097
- try {
2098
- const re = new RegExp(pattern, flags);
2099
- if (regexCache.size >= REGEX_CACHE_MAX) {
2100
- const oldest = regexCache.keys().next().value;
2101
- if (oldest) regexCache.delete(oldest);
2102
- }
2103
- regexCache.set(key, re);
2104
- return re;
2105
- } catch {
2106
- return null;
2107
- }
2108
- }
2109
-
2110
- // src/rules/index.ts
2111
- function matchesPattern(text, patterns) {
2112
- const p = Array.isArray(patterns) ? patterns : [patterns];
2113
- if (p.length === 0) return false;
2114
- const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
2115
- const target = text.toLowerCase();
2116
- const directMatch = isMatch(target);
2117
- if (directMatch) return true;
2118
- const withoutDotSlash = text.replace(/^\.\//, "");
2119
- return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
2120
- }
2121
- var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2122
- function getNestedValue(obj, path) {
2123
- if (!obj || typeof obj !== "object") return null;
2124
- const segments = path.split(".");
2125
- for (const seg of segments) {
2126
- if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
2127
- }
2128
- return segments.reduce((prev, curr) => prev?.[curr], obj);
2129
- }
2130
- function evaluateSmartConditions(args, rule) {
2131
- if (!rule.conditions || rule.conditions.length === 0) return true;
2132
- const mode = rule.conditionMode ?? "all";
2133
- const fieldCache = /* @__PURE__ */ new Map();
2134
- const resolveField = (field) => {
2135
- if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
2136
- const rawVal = getNestedValue(args, field);
2137
- const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
2138
- const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
2139
- const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
2140
- fieldCache.set(field, val);
2141
- return val;
2142
- };
2143
- const results = rule.conditions.map((cond) => {
2144
- const val = resolveField(cond.field);
2145
- switch (cond.op) {
2146
- case "exists":
2147
- return val !== null && val !== "";
2148
- case "notExists":
2149
- return val === null || val === "";
2150
- case "contains":
2151
- return val !== null && cond.value ? val.includes(cond.value) : false;
2152
- case "notContains":
2153
- return val !== null && cond.value ? !val.includes(cond.value) : true;
2154
- case "matches": {
2155
- if (val === null || !cond.value) return false;
2156
- const reM = getCompiledRegex(cond.value, cond.flags ?? "");
2157
- if (!reM) return false;
2158
- return reM.test(val);
2159
- }
2160
- case "notMatches": {
2161
- if (!cond.value) return false;
2162
- if (val === null) return true;
2163
- const reN = getCompiledRegex(cond.value, cond.flags ?? "");
2164
- if (!reN) return false;
2165
- return !reN.test(val);
2166
- }
2167
- case "matchesGlob":
2168
- return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
2169
- case "notMatchesGlob":
2170
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
2171
- default:
2172
- return false;
2173
- }
2174
- });
2175
- return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2176
- }
2177
-
2178
2414
  // src/policy/index.ts
2179
2415
  function resolveCheck(v) {
2180
2416
  return v === "off" || v === "block" ? v : "review";
@@ -2182,41 +2418,6 @@ function resolveCheck(v) {
2182
2418
  function resolveCheckTight(v) {
2183
2419
  return v === "block" ? "block" : "review";
2184
2420
  }
2185
- var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2186
- var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2187
- function detectInlineExec(command) {
2188
- const pipeFed = command.includes("|");
2189
- const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2190
- for (const rawSeg of segments) {
2191
- const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2192
- let i = 0;
2193
- while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2194
- if (i >= tokens.length) continue;
2195
- const base = tokens[i].split("/").pop() ?? tokens[i];
2196
- if (!INLINE_INTERP.test(base)) continue;
2197
- const args = tokens.slice(i + 1);
2198
- let hadRedirect = false;
2199
- const positionals = [];
2200
- for (let j = 0; j < args.length; j++) {
2201
- const a = args[j];
2202
- if (a === "-") return true;
2203
- if (a.startsWith("<")) {
2204
- hadRedirect = true;
2205
- if (a === "<" || a === "<<") j++;
2206
- continue;
2207
- }
2208
- if (a.startsWith("-")) {
2209
- if (/^-(c|e|eval)$/i.test(a)) return true;
2210
- continue;
2211
- }
2212
- positionals.push(a);
2213
- }
2214
- if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2215
- return true;
2216
- }
2217
- }
2218
- return false;
2219
- }
2220
2421
  var VERDICT_RANK = {
2221
2422
  allow: 0,
2222
2423
  review: 1,
@@ -2230,6 +2431,12 @@ function resolvePinned(matches) {
2230
2431
  (best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
2231
2432
  );
2232
2433
  }
2434
+ function strictestVerdict(candidates) {
2435
+ if (candidates.length === 0) return void 0;
2436
+ return candidates.reduce(
2437
+ (best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
2438
+ );
2439
+ }
2233
2440
  function tokenize2(toolName) {
2234
2441
  return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
2235
2442
  }
@@ -2241,6 +2448,11 @@ function extractShellCommand(toolName, args, toolInspection) {
2241
2448
  const value = getNestedValue(args, fieldPath);
2242
2449
  return typeof value === "string" ? value : null;
2243
2450
  }
2451
+ function inspectsShellCommand(toolName, toolInspection) {
2452
+ const patterns = Object.keys(toolInspection);
2453
+ const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
2454
+ return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
2455
+ }
2244
2456
  function isSqlTool(toolName, toolInspection) {
2245
2457
  const patterns = Object.keys(toolInspection);
2246
2458
  const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
@@ -2312,8 +2524,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2312
2524
  };
2313
2525
  }
2314
2526
  }
2315
- if (wouldBeIgnored) return { decision: "allow" };
2316
- const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2527
+ if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
2528
+ const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
2529
+ const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
2530
+ const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
2317
2531
  if (bashCommand !== null) {
2318
2532
  const pipeVerdict = pipeChainVerdict(
2319
2533
  bashCommand,
@@ -2363,8 +2577,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2363
2577
  }
2364
2578
  if (config.policy.smartRules.length > 0) {
2365
2579
  const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
2580
+ const astSuppressed = (rule) => {
2581
+ if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
2582
+ const knob = rule.name === "review-drop-truncate-shell" ? resolveCheck(config.policy.commandChecks?.sqlDdl) : rule.name === "shield:filesystem:review-chmod-777" ? resolveCheck(config.policy.commandChecks?.chmod) : void 0;
2583
+ if (knob === "off" && rule.pinned) return false;
2584
+ return true;
2585
+ };
2366
2586
  const matches = config.policy.smartRules.filter(
2367
- (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2587
+ (rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
2368
2588
  );
2369
2589
  const matchedRule = resolvePinned(matches);
2370
2590
  if (matchedRule) {
@@ -2395,15 +2615,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2395
2615
  const analyzed = analyzeShellCommand(shellCommand);
2396
2616
  allTokens = analyzed.allTokens;
2397
2617
  pathTokens = analyzed.paths;
2398
- const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2399
- if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2400
- return {
2401
- decision: inlineAction === "block" ? "block" : "review",
2402
- blockedByLabel: "Node9 Standard (Inline Execution)",
2403
- ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2404
- tier: 3
2405
- };
2406
- }
2618
+ const candidates = [];
2407
2619
  const evalVerdict = detectDangerousShellExec(shellCommand);
2408
2620
  if (evalVerdict === "block") {
2409
2621
  return {
@@ -2414,24 +2626,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2414
2626
  tier: 3
2415
2627
  };
2416
2628
  }
2629
+ const ptVerdict = pipeChainVerdict(
2630
+ shellCommand,
2631
+ isTrustedHost,
2632
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2633
+ );
2634
+ if (ptVerdict?.decision === "allow") return ptVerdict;
2635
+ if (ptVerdict) candidates.push(ptVerdict);
2636
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2637
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2638
+ candidates.push({
2639
+ decision: inlineAction === "block" ? "block" : "review",
2640
+ blockedByLabel: "Node9 Standard (Inline Execution)",
2641
+ ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2642
+ tier: 3
2643
+ });
2644
+ }
2417
2645
  if (evalVerdict === "review") {
2418
- return {
2419
- // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2420
- // block but can never turn this off (eval-remote above is Class A —
2421
- // no knob at all).
2646
+ candidates.push({
2422
2647
  decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2423
2648
  blockedByLabel: "Node9: Eval Dynamic Content",
2424
2649
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2425
2650
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
2426
2651
  tier: 3
2427
- };
2652
+ });
2428
2653
  }
2429
- const ptVerdict = pipeChainVerdict(
2430
- shellCommand,
2431
- isTrustedHost,
2432
- resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2433
- );
2434
- if (ptVerdict) return ptVerdict;
2654
+ const builtin = strictestVerdict(candidates);
2655
+ if (builtin) return builtin;
2435
2656
  if (config.policy.egress?.enabled) {
2436
2657
  const dests = extractShellDestinations(shellCommand);
2437
2658
  if (dests.length > 0) {
@@ -3696,15 +3917,15 @@ function detectArgsPii(args) {
3696
3917
 
3697
3918
  // src/scan/canonical.ts
3698
3919
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
3699
- var CANONICAL_EXTRACTOR_VERSION = "canonical-v6";
3700
- var CANONICAL_EXTRACTOR_HASH = "a0e2bb339fe67e19";
3920
+ var CANONICAL_EXTRACTOR_VERSION = "canonical-v8";
3921
+ var CANONICAL_EXTRACTOR_HASH = "80f40f974263b281";
3701
3922
  var DEDUPE_PREVIEW_LEN = 120;
3702
3923
  function extractCanonicalFindings(call, ctx) {
3703
3924
  const out = [];
3704
3925
  const ts = call.timestamp;
3705
3926
  const toolNameLower = call.toolName.toLowerCase();
3706
3927
  const command = typeof call.args.command === "string" ? call.args.command : null;
3707
- const isBash = isBashTool(call.toolName) && command !== null;
3928
+ const isShell = isShellShapedTool(call.toolName, ctx.toolInspection) && command !== null;
3708
3929
  if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
3709
3930
  out.push(
3710
3931
  makeFinding({
@@ -3779,7 +4000,7 @@ function extractCanonicalFindings(call, ctx) {
3779
4000
  );
3780
4001
  }
3781
4002
  }
3782
- if (!isBash || command === null) {
4003
+ if (!isShell || command === null) {
3783
4004
  return out;
3784
4005
  }
3785
4006
  const fsVerdict = analyzeFsOperation(command);
@@ -3805,7 +4026,7 @@ function extractCanonicalFindings(call, ctx) {
3805
4026
  for (const source of ctx.rules) {
3806
4027
  const r = source.rule;
3807
4028
  if (r.verdict === "allow") continue;
3808
- if (r.tool && !matchesPattern(toolNameLower, r.tool)) continue;
4029
+ if (!toolMatchesRule(toolNameLower, r.tool, ctx.toolInspection)) continue;
3809
4030
  if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
3810
4031
  if (!evaluateSmartConditions(call.args, r)) continue;
3811
4032
  out.push(
@@ -4082,6 +4303,7 @@ var ENGINE_VERSION = "1.4.0";
4082
4303
  detectArgsPii,
4083
4304
  detectDangerousEval,
4084
4305
  detectDangerousShellExec,
4306
+ detectInlineExec,
4085
4307
  detectPii,
4086
4308
  evaluateEgress,
4087
4309
  evaluateLoopWindow,
@@ -4100,6 +4322,7 @@ var ENGINE_VERSION = "1.4.0";
4100
4322
  isIgnoredTool,
4101
4323
  isPrivateHost,
4102
4324
  isProtectedHomePath,
4325
+ isShellShapedTool,
4103
4326
  isShieldVerdict,
4104
4327
  matchSensitivePath,
4105
4328
  matchesPattern,
@@ -4117,6 +4340,7 @@ var ENGINE_VERSION = "1.4.0";
4117
4340
  summarizeBlast,
4118
4341
  summarizeScan,
4119
4342
  toScanFinding,
4343
+ toolMatchesRule,
4120
4344
  truncateBlastPath,
4121
4345
  validateOverrides,
4122
4346
  validateRegex,