@node9/proxy 1.18.2 → 1.19.0

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.mjs CHANGED
@@ -858,9 +858,70 @@ var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
858
858
  ]);
859
859
  var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
860
860
  var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
861
+ function isCatHeredocOrLit(part) {
862
+ if (!part) return false;
863
+ const t = syntax.NodeType(part);
864
+ if (t === "Lit") return true;
865
+ if (t !== "CmdSubst") return false;
866
+ const stmts = part.Stmts || [];
867
+ if (stmts.length !== 1) return false;
868
+ const stmt = stmts[0];
869
+ const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
870
+ const hasHeredoc = redirs.some((r) => r && r.Hdoc);
871
+ if (!hasHeredoc) return false;
872
+ const cmd = stmt.Cmd;
873
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
874
+ const firstArg = cmd.Args?.[0]?.Parts || [];
875
+ if (firstArg.length !== 1 || syntax.NodeType(firstArg[0]) !== "Lit") return false;
876
+ return (firstArg[0].Value || "").toLowerCase() === "cat";
877
+ }
878
+ var NORMALIZE_CACHE_MAX = 5e3;
879
+ var normalizeCache = /* @__PURE__ */ new Map();
880
+ var AST_CACHE_MAX = 5e3;
881
+ var astCache = /* @__PURE__ */ new Map();
882
+ var PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
883
+ function parseShared(command) {
884
+ const cached = astCache.get(command);
885
+ if (cached !== void 0) {
886
+ astCache.delete(command);
887
+ astCache.set(command, cached);
888
+ return cached;
889
+ }
890
+ let parsed;
891
+ try {
892
+ parsed = sharedParser.Parse(command, "cmd");
893
+ } catch {
894
+ parsed = PARSE_FAIL;
895
+ }
896
+ if (astCache.size >= AST_CACHE_MAX) {
897
+ const oldest = astCache.keys().next().value;
898
+ if (oldest !== void 0) astCache.delete(oldest);
899
+ }
900
+ astCache.set(command, parsed);
901
+ return parsed;
902
+ }
903
+ function cachedNormalize(command, compute) {
904
+ const hit = normalizeCache.get(command);
905
+ if (hit !== void 0) {
906
+ normalizeCache.delete(command);
907
+ normalizeCache.set(command, hit);
908
+ return hit;
909
+ }
910
+ const result = compute();
911
+ if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
912
+ const oldest = normalizeCache.keys().next().value;
913
+ if (oldest !== void 0) normalizeCache.delete(oldest);
914
+ }
915
+ normalizeCache.set(command, result);
916
+ return result;
917
+ }
861
918
  function normalizeCommandForPolicy(command) {
919
+ return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
920
+ }
921
+ function normalizeCommandForPolicyImpl(command) {
922
+ const f = parseShared(command);
923
+ if (f === PARSE_FAIL) return command;
862
924
  try {
863
- const f = sharedParser.Parse(command, "cmd");
864
925
  const strips = [];
865
926
  syntax.Walk(f, (node) => {
866
927
  if (!node) return false;
@@ -882,7 +943,11 @@ function normalizeCommandForPolicy(command) {
882
943
  } else if (nt === "DblQuoted") {
883
944
  const innerParts = quotedNode.Parts || [];
884
945
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
885
- if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
946
+ if (allLit) {
947
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
948
+ } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
949
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
950
+ }
886
951
  }
887
952
  }
888
953
  return true;
@@ -950,6 +1015,202 @@ function detectDangerousShellExec(command) {
950
1015
  return null;
951
1016
  }
952
1017
  }
1018
+ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1019
+ "cat",
1020
+ "less",
1021
+ "head",
1022
+ "tail",
1023
+ "bat",
1024
+ "more",
1025
+ "open",
1026
+ "print",
1027
+ "nano",
1028
+ "vim",
1029
+ "vi",
1030
+ "emacs",
1031
+ "code",
1032
+ "type"
1033
+ ]);
1034
+ var FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1035
+ var HOME_CACHE_ALLOWLIST = [
1036
+ ".cache",
1037
+ ".npm/_npx",
1038
+ ".npm/_cacache",
1039
+ ".cargo/registry",
1040
+ ".gradle/caches",
1041
+ ".gradle/.tmp",
1042
+ ".m2/repository",
1043
+ ".pnpm-store",
1044
+ ".yarn/cache",
1045
+ ".yarn/.cache",
1046
+ ".cache/pip",
1047
+ ".local/share/Trash",
1048
+ ".rustup/downloads"
1049
+ ];
1050
+ var SENSITIVE_PATH_RULES = [
1051
+ {
1052
+ rule: "shield:project-jail:block-read-ssh",
1053
+ reason: "Reading SSH private keys is blocked by project-jail shield",
1054
+ match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
1055
+ },
1056
+ {
1057
+ rule: "shield:project-jail:block-read-aws",
1058
+ reason: "Reading AWS credentials is blocked by project-jail shield",
1059
+ match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
1060
+ },
1061
+ {
1062
+ rule: "shield:project-jail:block-read-env",
1063
+ reason: "Reading .env files is blocked by project-jail shield",
1064
+ match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
1065
+ },
1066
+ {
1067
+ rule: "shield:project-jail:block-read-credentials",
1068
+ reason: "Reading credential files is blocked by project-jail shield",
1069
+ match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
1070
+ p
1071
+ )
1072
+ }
1073
+ ];
1074
+ var BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
1075
+ "bash",
1076
+ "execute_bash",
1077
+ "run_shell_command",
1078
+ "shell",
1079
+ "exec_command"
1080
+ ]);
1081
+ function isBashTool(toolName) {
1082
+ return BASH_TOOL_NAMES.has(toolName.toLowerCase());
1083
+ }
1084
+ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1085
+ "block-rm-rf-home",
1086
+ "shield:project-jail:block-read-ssh",
1087
+ "shield:project-jail:block-read-aws",
1088
+ "shield:project-jail:block-read-env",
1089
+ "shield:project-jail:block-read-credentials"
1090
+ ]);
1091
+ function isProtectedHomePath(rawPath) {
1092
+ let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
1093
+ let underHome = false;
1094
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
1095
+ p = p.replace(/^~[\\/]?/, "");
1096
+ underHome = true;
1097
+ } else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
1098
+ p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
1099
+ underHome = true;
1100
+ }
1101
+ if (!underHome) return false;
1102
+ if (p === "" || p === "." || p === "./") return true;
1103
+ for (const safe of HOME_CACHE_ALLOWLIST) {
1104
+ if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
1105
+ return false;
1106
+ }
1107
+ }
1108
+ return true;
1109
+ }
1110
+ function extractLiteralArgs(callExpr) {
1111
+ const args = callExpr.Args || [];
1112
+ if (args.length === 0) return { name: "", flags: [], paths: [] };
1113
+ const litFromWord = (w) => {
1114
+ const parts = w?.Parts || [];
1115
+ let s = "";
1116
+ for (const p of parts) {
1117
+ const t = syntax.NodeType(p);
1118
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1119
+ else if (t === "SglQuoted") s += p.Value ?? "";
1120
+ else if (t === "DblQuoted") {
1121
+ const inner = p.Parts || [];
1122
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1123
+ s += inner.map((ip) => ip.Value ?? "").join("");
1124
+ } else {
1125
+ return null;
1126
+ }
1127
+ }
1128
+ return s;
1129
+ };
1130
+ const name = (litFromWord(args[0]) || "").toLowerCase();
1131
+ const flags = [];
1132
+ const paths = [];
1133
+ for (let i = 1; i < args.length; i++) {
1134
+ const v = litFromWord(args[i]);
1135
+ if (v === null) continue;
1136
+ if (v.startsWith("-")) flags.push(v);
1137
+ else paths.push(v);
1138
+ }
1139
+ return { name, flags, paths };
1140
+ }
1141
+ var FS_OP_CACHE_MAX = 5e3;
1142
+ var fsOpCache = /* @__PURE__ */ new Map();
1143
+ function analyzeFsOperation(command) {
1144
+ if (!FS_OP_PRESCREEN_RE.test(command)) return null;
1145
+ if (fsOpCache.has(command)) {
1146
+ const hit = fsOpCache.get(command) ?? null;
1147
+ fsOpCache.delete(command);
1148
+ fsOpCache.set(command, hit);
1149
+ return hit;
1150
+ }
1151
+ const computed = analyzeFsOperationImpl(command);
1152
+ if (fsOpCache.size >= FS_OP_CACHE_MAX) {
1153
+ const oldest = fsOpCache.keys().next().value;
1154
+ if (oldest !== void 0) fsOpCache.delete(oldest);
1155
+ }
1156
+ fsOpCache.set(command, computed);
1157
+ return computed;
1158
+ }
1159
+ function analyzeFsOperationImpl(command) {
1160
+ const f = parseShared(command);
1161
+ if (f === PARSE_FAIL) return null;
1162
+ let result = null;
1163
+ try {
1164
+ syntax.Walk(f, (node) => {
1165
+ if (!node || result) return false;
1166
+ const n = node;
1167
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1168
+ const { name, flags, paths } = extractLiteralArgs(n);
1169
+ if (!name) return true;
1170
+ if (name === "rm") {
1171
+ const flagStr = flags.join("").toLowerCase();
1172
+ const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
1173
+ const hasF = /[f]/.test(flagStr) || flags.includes("--force");
1174
+ if (hasR && hasF) {
1175
+ for (const p of paths) {
1176
+ if (isProtectedHomePath(p)) {
1177
+ result = {
1178
+ ruleName: "block-rm-rf-home",
1179
+ verdict: "block",
1180
+ reason: "Recursive delete of home directory is irreversible",
1181
+ path: p
1182
+ };
1183
+ return false;
1184
+ }
1185
+ if (p === "/" || /^\/+$/.test(p)) {
1186
+ result = {
1187
+ ruleName: "block-rm-rf-home",
1188
+ verdict: "block",
1189
+ reason: "Recursive delete of root is catastrophic",
1190
+ path: p
1191
+ };
1192
+ return false;
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ if (FS_READ_TOOLS.has(name)) {
1198
+ for (const p of paths) {
1199
+ for (const sp of SENSITIVE_PATH_RULES) {
1200
+ if (sp.match(p)) {
1201
+ result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
1202
+ return false;
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ return true;
1208
+ });
1209
+ return result;
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ }
953
1214
  function analyzeShellCommand(command) {
954
1215
  const actions = [];
955
1216
  const paths = [];
@@ -1371,10 +1632,18 @@ function getNestedValue(obj, path13) {
1371
1632
  function evaluateSmartConditions(args, rule) {
1372
1633
  if (!rule.conditions || rule.conditions.length === 0) return true;
1373
1634
  const mode = rule.conditionMode ?? "all";
1635
+ const fieldCache = /* @__PURE__ */ new Map();
1636
+ const resolveField = (field) => {
1637
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1638
+ const rawVal = getNestedValue(args, field);
1639
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1640
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1641
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1642
+ fieldCache.set(field, val);
1643
+ return val;
1644
+ };
1374
1645
  const results = rule.conditions.map((cond) => {
1375
- const rawVal = getNestedValue(args, cond.field);
1376
- const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1377
- const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1646
+ const val = resolveField(cond.field);
1378
1647
  switch (cond.op) {
1379
1648
  case "exists":
1380
1649
  return val !== null && val !== "";
@@ -1426,6 +1695,43 @@ function isSqlTool(toolName, toolInspection) {
1426
1695
  return fieldName === "sql" || fieldName === "query";
1427
1696
  }
1428
1697
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
1698
+ function pipeChainVerdict(command, isTrustedHost2) {
1699
+ const pipeAnalysis = analyzePipeChain(command);
1700
+ if (!pipeAnalysis.isPipeline) return null;
1701
+ if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
1702
+ const sinks = pipeAnalysis.sinkTargets;
1703
+ const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1704
+ if (pipeAnalysis.risk === "critical") {
1705
+ if (allTrusted) {
1706
+ return {
1707
+ decision: "review",
1708
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1709
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1710
+ tier: 3
1711
+ };
1712
+ }
1713
+ return {
1714
+ decision: "block",
1715
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1716
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1717
+ tier: 3
1718
+ };
1719
+ }
1720
+ if (allTrusted) {
1721
+ return {
1722
+ decision: "allow",
1723
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1724
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1725
+ tier: 3
1726
+ };
1727
+ }
1728
+ return {
1729
+ decision: "review",
1730
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1731
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1732
+ tier: 3
1733
+ };
1734
+ }
1429
1735
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1430
1736
  const { agent, cwd, activeEnvironment } = context;
1431
1737
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -1441,9 +1747,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1441
1747
  }
1442
1748
  }
1443
1749
  if (wouldBeIgnored) return { decision: "allow" };
1750
+ const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1751
+ if (bashCommand !== null) {
1752
+ const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1753
+ if (pipeVerdict) return pipeVerdict;
1754
+ const fsVerdict = analyzeFsOperation(bashCommand);
1755
+ if (fsVerdict) {
1756
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
1757
+ const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
1758
+ return {
1759
+ decision: fsVerdict.verdict,
1760
+ blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
1761
+ reason: fsVerdict.reason,
1762
+ tier: 2,
1763
+ ruleName: fsVerdict.ruleName,
1764
+ ruleDescription: fsVerdict.reason
1765
+ };
1766
+ }
1767
+ }
1444
1768
  if (config.policy.smartRules.length > 0) {
1445
1769
  const matchedRule = config.policy.smartRules.find(
1446
- (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
1770
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1447
1771
  );
1448
1772
  if (matchedRule) {
1449
1773
  if (matchedRule.verdict === "allow")
@@ -1501,41 +1825,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1501
1825
  tier: 3
1502
1826
  };
1503
1827
  }
1504
- const pipeAnalysis = analyzePipeChain(shellCommand);
1505
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1506
- const sinks = pipeAnalysis.sinkTargets;
1507
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1508
- if (pipeAnalysis.risk === "critical") {
1509
- if (allTrusted) {
1510
- return {
1511
- decision: "review",
1512
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1513
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1514
- tier: 3
1515
- };
1516
- }
1517
- return {
1518
- decision: "block",
1519
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1520
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1521
- tier: 3
1522
- };
1523
- }
1524
- if (allTrusted) {
1525
- return {
1526
- decision: "allow",
1527
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1528
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1529
- tier: 3
1530
- };
1531
- }
1532
- return {
1533
- decision: "review",
1534
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1535
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1536
- tier: 3
1537
- };
1538
- }
1828
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1829
+ if (ptVerdict) return ptVerdict;
1539
1830
  const firstToken = analyzed.actions[0] ?? "";
1540
1831
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1541
1832
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2401,6 +2692,7 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2401
2692
  const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
2402
2693
  return { nextRecords, count, looping: count >= threshold };
2403
2694
  }
2695
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2404
2696
 
2405
2697
  // src/shields.ts
2406
2698
  var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.18.2",
3
+ "version": "1.19.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -59,6 +59,8 @@
59
59
  "lint:fix": "eslint . --fix",
60
60
  "format": "prettier --write .",
61
61
  "format:check": "prettier --check .",
62
+ "check:extractor-version": "node scripts/check-extractor-version.mjs",
63
+ "bump-extractor-version": "node scripts/check-extractor-version.mjs --bump",
62
64
  "fix": "npm run format && npm run lint:fix",
63
65
  "validate": "npm run format && npm run lint && npm run typecheck && npm run test && npm run test:e2e && npm run build",
64
66
  "test:e2e": "cross-env NODE9_TESTING=1 bash scripts/e2e.sh",