@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.js CHANGED
@@ -888,9 +888,70 @@ var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
888
888
  ]);
889
889
  var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish", "dash", "ksh"]);
890
890
  var DOWNLOAD_CMDS = /* @__PURE__ */ new Set(["curl", "wget"]);
891
+ function isCatHeredocOrLit(part) {
892
+ if (!part) return false;
893
+ const t = syntax.NodeType(part);
894
+ if (t === "Lit") return true;
895
+ if (t !== "CmdSubst") return false;
896
+ const stmts = part.Stmts || [];
897
+ if (stmts.length !== 1) return false;
898
+ const stmt = stmts[0];
899
+ const redirs = stmt.Redirs || stmt.Cmd?.Redirs || [];
900
+ const hasHeredoc = redirs.some((r) => r && r.Hdoc);
901
+ if (!hasHeredoc) return false;
902
+ const cmd = stmt.Cmd;
903
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
904
+ const firstArg = cmd.Args?.[0]?.Parts || [];
905
+ if (firstArg.length !== 1 || syntax.NodeType(firstArg[0]) !== "Lit") return false;
906
+ return (firstArg[0].Value || "").toLowerCase() === "cat";
907
+ }
908
+ var NORMALIZE_CACHE_MAX = 5e3;
909
+ var normalizeCache = /* @__PURE__ */ new Map();
910
+ var AST_CACHE_MAX = 5e3;
911
+ var astCache = /* @__PURE__ */ new Map();
912
+ var PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
913
+ function parseShared(command) {
914
+ const cached = astCache.get(command);
915
+ if (cached !== void 0) {
916
+ astCache.delete(command);
917
+ astCache.set(command, cached);
918
+ return cached;
919
+ }
920
+ let parsed;
921
+ try {
922
+ parsed = sharedParser.Parse(command, "cmd");
923
+ } catch {
924
+ parsed = PARSE_FAIL;
925
+ }
926
+ if (astCache.size >= AST_CACHE_MAX) {
927
+ const oldest = astCache.keys().next().value;
928
+ if (oldest !== void 0) astCache.delete(oldest);
929
+ }
930
+ astCache.set(command, parsed);
931
+ return parsed;
932
+ }
933
+ function cachedNormalize(command, compute) {
934
+ const hit = normalizeCache.get(command);
935
+ if (hit !== void 0) {
936
+ normalizeCache.delete(command);
937
+ normalizeCache.set(command, hit);
938
+ return hit;
939
+ }
940
+ const result = compute();
941
+ if (normalizeCache.size >= NORMALIZE_CACHE_MAX) {
942
+ const oldest = normalizeCache.keys().next().value;
943
+ if (oldest !== void 0) normalizeCache.delete(oldest);
944
+ }
945
+ normalizeCache.set(command, result);
946
+ return result;
947
+ }
891
948
  function normalizeCommandForPolicy(command) {
949
+ return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
950
+ }
951
+ function normalizeCommandForPolicyImpl(command) {
952
+ const f = parseShared(command);
953
+ if (f === PARSE_FAIL) return command;
892
954
  try {
893
- const f = sharedParser.Parse(command, "cmd");
894
955
  const strips = [];
895
956
  syntax.Walk(f, (node) => {
896
957
  if (!node) return false;
@@ -912,7 +973,11 @@ function normalizeCommandForPolicy(command) {
912
973
  } else if (nt === "DblQuoted") {
913
974
  const innerParts = quotedNode.Parts || [];
914
975
  const allLit = innerParts.length === 0 || innerParts.every((p) => syntax.NodeType(p) === "Lit");
915
- if (allLit) strips.push([next.Pos().Offset(), next.End().Offset()]);
976
+ if (allLit) {
977
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
978
+ } else if (innerParts.every((p) => isCatHeredocOrLit(p))) {
979
+ strips.push([next.Pos().Offset(), next.End().Offset()]);
980
+ }
916
981
  }
917
982
  }
918
983
  return true;
@@ -980,6 +1045,202 @@ function detectDangerousShellExec(command) {
980
1045
  return null;
981
1046
  }
982
1047
  }
1048
+ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1049
+ "cat",
1050
+ "less",
1051
+ "head",
1052
+ "tail",
1053
+ "bat",
1054
+ "more",
1055
+ "open",
1056
+ "print",
1057
+ "nano",
1058
+ "vim",
1059
+ "vi",
1060
+ "emacs",
1061
+ "code",
1062
+ "type"
1063
+ ]);
1064
+ var FS_OP_PRESCREEN_RE = /(?:^|[\s|;&(`\n])(?:rm|cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type)\b/;
1065
+ var HOME_CACHE_ALLOWLIST = [
1066
+ ".cache",
1067
+ ".npm/_npx",
1068
+ ".npm/_cacache",
1069
+ ".cargo/registry",
1070
+ ".gradle/caches",
1071
+ ".gradle/.tmp",
1072
+ ".m2/repository",
1073
+ ".pnpm-store",
1074
+ ".yarn/cache",
1075
+ ".yarn/.cache",
1076
+ ".cache/pip",
1077
+ ".local/share/Trash",
1078
+ ".rustup/downloads"
1079
+ ];
1080
+ var SENSITIVE_PATH_RULES = [
1081
+ {
1082
+ rule: "shield:project-jail:block-read-ssh",
1083
+ reason: "Reading SSH private keys is blocked by project-jail shield",
1084
+ match: (p) => /(^|[\\/])\.ssh[\\/]/i.test(p)
1085
+ },
1086
+ {
1087
+ rule: "shield:project-jail:block-read-aws",
1088
+ reason: "Reading AWS credentials is blocked by project-jail shield",
1089
+ match: (p) => /(^|[\\/])\.aws[\\/]/i.test(p)
1090
+ },
1091
+ {
1092
+ rule: "shield:project-jail:block-read-env",
1093
+ reason: "Reading .env files is blocked by project-jail shield",
1094
+ match: (p) => /(?:^|[\\/])\.env(?:\.local|\.production|\.staging)?$/i.test(p)
1095
+ },
1096
+ {
1097
+ rule: "shield:project-jail:block-read-credentials",
1098
+ reason: "Reading credential files is blocked by project-jail shield",
1099
+ match: (p) => /(?:credentials\.json|\.netrc|\.npmrc|\.docker[\\/]config\.json|gcloud[\\/]credentials)$/i.test(
1100
+ p
1101
+ )
1102
+ }
1103
+ ];
1104
+ var BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
1105
+ "bash",
1106
+ "execute_bash",
1107
+ "run_shell_command",
1108
+ "shell",
1109
+ "exec_command"
1110
+ ]);
1111
+ function isBashTool(toolName) {
1112
+ return BASH_TOOL_NAMES.has(toolName.toLowerCase());
1113
+ }
1114
+ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1115
+ "block-rm-rf-home",
1116
+ "shield:project-jail:block-read-ssh",
1117
+ "shield:project-jail:block-read-aws",
1118
+ "shield:project-jail:block-read-env",
1119
+ "shield:project-jail:block-read-credentials"
1120
+ ]);
1121
+ function isProtectedHomePath(rawPath) {
1122
+ let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
1123
+ let underHome = false;
1124
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
1125
+ p = p.replace(/^~[\\/]?/, "");
1126
+ underHome = true;
1127
+ } else if (/^\/home\/[^/]+/.test(p) || /^\/root(\/|$)/.test(p)) {
1128
+ p = p.replace(/^\/home\/[^/]+[\\/]?|^\/root[\\/]?/, "");
1129
+ underHome = true;
1130
+ }
1131
+ if (!underHome) return false;
1132
+ if (p === "" || p === "." || p === "./") return true;
1133
+ for (const safe of HOME_CACHE_ALLOWLIST) {
1134
+ if (p === safe || p.startsWith(safe + "/") || p.startsWith(safe + "\\")) {
1135
+ return false;
1136
+ }
1137
+ }
1138
+ return true;
1139
+ }
1140
+ function extractLiteralArgs(callExpr) {
1141
+ const args = callExpr.Args || [];
1142
+ if (args.length === 0) return { name: "", flags: [], paths: [] };
1143
+ const litFromWord = (w) => {
1144
+ const parts = w?.Parts || [];
1145
+ let s = "";
1146
+ for (const p of parts) {
1147
+ const t = syntax.NodeType(p);
1148
+ if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
1149
+ else if (t === "SglQuoted") s += p.Value ?? "";
1150
+ else if (t === "DblQuoted") {
1151
+ const inner = p.Parts || [];
1152
+ if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
1153
+ s += inner.map((ip) => ip.Value ?? "").join("");
1154
+ } else {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ return s;
1159
+ };
1160
+ const name = (litFromWord(args[0]) || "").toLowerCase();
1161
+ const flags = [];
1162
+ const paths = [];
1163
+ for (let i = 1; i < args.length; i++) {
1164
+ const v = litFromWord(args[i]);
1165
+ if (v === null) continue;
1166
+ if (v.startsWith("-")) flags.push(v);
1167
+ else paths.push(v);
1168
+ }
1169
+ return { name, flags, paths };
1170
+ }
1171
+ var FS_OP_CACHE_MAX = 5e3;
1172
+ var fsOpCache = /* @__PURE__ */ new Map();
1173
+ function analyzeFsOperation(command) {
1174
+ if (!FS_OP_PRESCREEN_RE.test(command)) return null;
1175
+ if (fsOpCache.has(command)) {
1176
+ const hit = fsOpCache.get(command) ?? null;
1177
+ fsOpCache.delete(command);
1178
+ fsOpCache.set(command, hit);
1179
+ return hit;
1180
+ }
1181
+ const computed = analyzeFsOperationImpl(command);
1182
+ if (fsOpCache.size >= FS_OP_CACHE_MAX) {
1183
+ const oldest = fsOpCache.keys().next().value;
1184
+ if (oldest !== void 0) fsOpCache.delete(oldest);
1185
+ }
1186
+ fsOpCache.set(command, computed);
1187
+ return computed;
1188
+ }
1189
+ function analyzeFsOperationImpl(command) {
1190
+ const f = parseShared(command);
1191
+ if (f === PARSE_FAIL) return null;
1192
+ let result = null;
1193
+ try {
1194
+ syntax.Walk(f, (node) => {
1195
+ if (!node || result) return false;
1196
+ const n = node;
1197
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1198
+ const { name, flags, paths } = extractLiteralArgs(n);
1199
+ if (!name) return true;
1200
+ if (name === "rm") {
1201
+ const flagStr = flags.join("").toLowerCase();
1202
+ const hasR = /[r]/.test(flagStr) || flags.includes("--recursive");
1203
+ const hasF = /[f]/.test(flagStr) || flags.includes("--force");
1204
+ if (hasR && hasF) {
1205
+ for (const p of paths) {
1206
+ if (isProtectedHomePath(p)) {
1207
+ result = {
1208
+ ruleName: "block-rm-rf-home",
1209
+ verdict: "block",
1210
+ reason: "Recursive delete of home directory is irreversible",
1211
+ path: p
1212
+ };
1213
+ return false;
1214
+ }
1215
+ if (p === "/" || /^\/+$/.test(p)) {
1216
+ result = {
1217
+ ruleName: "block-rm-rf-home",
1218
+ verdict: "block",
1219
+ reason: "Recursive delete of root is catastrophic",
1220
+ path: p
1221
+ };
1222
+ return false;
1223
+ }
1224
+ }
1225
+ }
1226
+ }
1227
+ if (FS_READ_TOOLS.has(name)) {
1228
+ for (const p of paths) {
1229
+ for (const sp of SENSITIVE_PATH_RULES) {
1230
+ if (sp.match(p)) {
1231
+ result = { ruleName: sp.rule, verdict: "block", reason: sp.reason, path: p };
1232
+ return false;
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ return true;
1238
+ });
1239
+ return result;
1240
+ } catch {
1241
+ return null;
1242
+ }
1243
+ }
983
1244
  function analyzeShellCommand(command) {
984
1245
  const actions = [];
985
1246
  const paths = [];
@@ -1401,10 +1662,18 @@ function getNestedValue(obj, path13) {
1401
1662
  function evaluateSmartConditions(args, rule) {
1402
1663
  if (!rule.conditions || rule.conditions.length === 0) return true;
1403
1664
  const mode = rule.conditionMode ?? "all";
1665
+ const fieldCache = /* @__PURE__ */ new Map();
1666
+ const resolveField = (field) => {
1667
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
1668
+ const rawVal = getNestedValue(args, field);
1669
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
1670
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
1671
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
1672
+ fieldCache.set(field, val);
1673
+ return val;
1674
+ };
1404
1675
  const results = rule.conditions.map((cond) => {
1405
- const rawVal = getNestedValue(args, cond.field);
1406
- const normalized = rawVal !== null && rawVal !== void 0 ? String(rawVal).replace(/\s+/g, " ").trim() : null;
1407
- const val = cond.field === "command" && normalized !== null ? normalizeCommandForPolicy(normalized) : normalized;
1676
+ const val = resolveField(cond.field);
1408
1677
  switch (cond.op) {
1409
1678
  case "exists":
1410
1679
  return val !== null && val !== "";
@@ -1456,6 +1725,43 @@ function isSqlTool(toolName, toolInspection) {
1456
1725
  return fieldName === "sql" || fieldName === "query";
1457
1726
  }
1458
1727
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
1728
+ function pipeChainVerdict(command, isTrustedHost2) {
1729
+ const pipeAnalysis = analyzePipeChain(command);
1730
+ if (!pipeAnalysis.isPipeline) return null;
1731
+ if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
1732
+ const sinks = pipeAnalysis.sinkTargets;
1733
+ const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1734
+ if (pipeAnalysis.risk === "critical") {
1735
+ if (allTrusted) {
1736
+ return {
1737
+ decision: "review",
1738
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1739
+ reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1740
+ tier: 3
1741
+ };
1742
+ }
1743
+ return {
1744
+ decision: "block",
1745
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1746
+ reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1747
+ tier: 3
1748
+ };
1749
+ }
1750
+ if (allTrusted) {
1751
+ return {
1752
+ decision: "allow",
1753
+ blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1754
+ reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1755
+ tier: 3
1756
+ };
1757
+ }
1758
+ return {
1759
+ decision: "review",
1760
+ blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1761
+ reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1762
+ tier: 3
1763
+ };
1764
+ }
1459
1765
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1460
1766
  const { agent, cwd, activeEnvironment } = context;
1461
1767
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -1471,9 +1777,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1471
1777
  }
1472
1778
  }
1473
1779
  if (wouldBeIgnored) return { decision: "allow" };
1780
+ const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1781
+ if (bashCommand !== null) {
1782
+ const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1783
+ if (pipeVerdict) return pipeVerdict;
1784
+ const fsVerdict = analyzeFsOperation(bashCommand);
1785
+ if (fsVerdict) {
1786
+ const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
1787
+ const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
1788
+ return {
1789
+ decision: fsVerdict.verdict,
1790
+ blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
1791
+ reason: fsVerdict.reason,
1792
+ tier: 2,
1793
+ ruleName: fsVerdict.ruleName,
1794
+ ruleDescription: fsVerdict.reason
1795
+ };
1796
+ }
1797
+ }
1474
1798
  if (config.policy.smartRules.length > 0) {
1475
1799
  const matchedRule = config.policy.smartRules.find(
1476
- (rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
1800
+ (rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
1477
1801
  );
1478
1802
  if (matchedRule) {
1479
1803
  if (matchedRule.verdict === "allow")
@@ -1531,41 +1855,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1531
1855
  tier: 3
1532
1856
  };
1533
1857
  }
1534
- const pipeAnalysis = analyzePipeChain(shellCommand);
1535
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1536
- const sinks = pipeAnalysis.sinkTargets;
1537
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1538
- if (pipeAnalysis.risk === "critical") {
1539
- if (allTrusted) {
1540
- return {
1541
- decision: "review",
1542
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1543
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1544
- tier: 3
1545
- };
1546
- }
1547
- return {
1548
- decision: "block",
1549
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1550
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1551
- tier: 3
1552
- };
1553
- }
1554
- if (allTrusted) {
1555
- return {
1556
- decision: "allow",
1557
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1558
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1559
- tier: 3
1560
- };
1561
- }
1562
- return {
1563
- decision: "review",
1564
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1565
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1566
- tier: 3
1567
- };
1568
- }
1858
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1859
+ if (ptVerdict) return ptVerdict;
1569
1860
  const firstToken = analyzed.actions[0] ?? "";
1570
1861
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1571
1862
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2431,6 +2722,7 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2431
2722
  const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
2432
2723
  return { nextRecords, count, looping: count >= threshold };
2433
2724
  }
2725
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2434
2726
 
2435
2727
  // src/shields.ts
2436
2728
  var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");