@node9/proxy 1.18.3 → 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
@@ -1045,6 +1045,202 @@ function detectDangerousShellExec(command) {
1045
1045
  return null;
1046
1046
  }
1047
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
+ }
1048
1244
  function analyzeShellCommand(command) {
1049
1245
  const actions = [];
1050
1246
  const paths = [];
@@ -1529,6 +1725,43 @@ function isSqlTool(toolName, toolInspection) {
1529
1725
  return fieldName === "sql" || fieldName === "query";
1530
1726
  }
1531
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
+ }
1532
1765
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1533
1766
  const { agent, cwd, activeEnvironment } = context;
1534
1767
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -1544,9 +1777,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1544
1777
  }
1545
1778
  }
1546
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
+ }
1547
1798
  if (config.policy.smartRules.length > 0) {
1548
1799
  const matchedRule = config.policy.smartRules.find(
1549
- (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)
1550
1801
  );
1551
1802
  if (matchedRule) {
1552
1803
  if (matchedRule.verdict === "allow")
@@ -1604,41 +1855,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1604
1855
  tier: 3
1605
1856
  };
1606
1857
  }
1607
- const pipeAnalysis = analyzePipeChain(shellCommand);
1608
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1609
- const sinks = pipeAnalysis.sinkTargets;
1610
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1611
- if (pipeAnalysis.risk === "critical") {
1612
- if (allTrusted) {
1613
- return {
1614
- decision: "review",
1615
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1616
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1617
- tier: 3
1618
- };
1619
- }
1620
- return {
1621
- decision: "block",
1622
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1623
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1624
- tier: 3
1625
- };
1626
- }
1627
- if (allTrusted) {
1628
- return {
1629
- decision: "allow",
1630
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1631
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1632
- tier: 3
1633
- };
1634
- }
1635
- return {
1636
- decision: "review",
1637
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1638
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1639
- tier: 3
1640
- };
1641
- }
1858
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1859
+ if (ptVerdict) return ptVerdict;
1642
1860
  const firstToken = analyzed.actions[0] ?? "";
1643
1861
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1644
1862
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2504,6 +2722,7 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2504
2722
  const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
2505
2723
  return { nextRecords, count, looping: count >= threshold };
2506
2724
  }
2725
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2507
2726
 
2508
2727
  // src/shields.ts
2509
2728
  var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
package/dist/index.mjs CHANGED
@@ -1015,6 +1015,202 @@ function detectDangerousShellExec(command) {
1015
1015
  return null;
1016
1016
  }
1017
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
+ }
1018
1214
  function analyzeShellCommand(command) {
1019
1215
  const actions = [];
1020
1216
  const paths = [];
@@ -1499,6 +1695,43 @@ function isSqlTool(toolName, toolInspection) {
1499
1695
  return fieldName === "sql" || fieldName === "query";
1500
1696
  }
1501
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
+ }
1502
1735
  async function evaluatePolicy(config, toolName, args, context = {}, hooks = {}) {
1503
1736
  const { agent, cwd, activeEnvironment } = context;
1504
1737
  const { checkProvenance: checkProvenance2, isTrustedHost: isTrustedHost2 } = hooks;
@@ -1514,9 +1747,27 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1514
1747
  }
1515
1748
  }
1516
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
+ }
1517
1768
  if (config.policy.smartRules.length > 0) {
1518
1769
  const matchedRule = config.policy.smartRules.find(
1519
- (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)
1520
1771
  );
1521
1772
  if (matchedRule) {
1522
1773
  if (matchedRule.verdict === "allow")
@@ -1574,41 +1825,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1574
1825
  tier: 3
1575
1826
  };
1576
1827
  }
1577
- const pipeAnalysis = analyzePipeChain(shellCommand);
1578
- if (pipeAnalysis.isPipeline && (pipeAnalysis.risk === "critical" || pipeAnalysis.risk === "high")) {
1579
- const sinks = pipeAnalysis.sinkTargets;
1580
- const allTrusted = sinks.length > 0 && sinks.every((host) => isTrustedHost2 ? isTrustedHost2(host) : false);
1581
- if (pipeAnalysis.risk === "critical") {
1582
- if (allTrusted) {
1583
- return {
1584
- decision: "review",
1585
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host (obfuscated)",
1586
- reason: `Obfuscated pipe to trusted host(s): ${sinks.join(", ")} \u2014 requires approval`,
1587
- tier: 3
1588
- };
1589
- }
1590
- return {
1591
- decision: "block",
1592
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (critical)",
1593
- reason: `Sensitive file piped through obfuscator to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1594
- tier: 3
1595
- };
1596
- }
1597
- if (allTrusted) {
1598
- return {
1599
- decision: "allow",
1600
- blockedByLabel: "Node9: Pipe-Chain to Trusted Host",
1601
- reason: `Sensitive file piped to trusted host(s): ${sinks.join(", ")}`,
1602
- tier: 3
1603
- };
1604
- }
1605
- return {
1606
- decision: "review",
1607
- blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1608
- reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1609
- tier: 3
1610
- };
1611
- }
1828
+ const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1829
+ if (ptVerdict) return ptVerdict;
1612
1830
  const firstToken = analyzed.actions[0] ?? "";
1613
1831
  if (["ssh", "scp", "rsync"].includes(firstToken)) {
1614
1832
  const rawTokens = shellCommand.trim().split(/\s+/);
@@ -2474,6 +2692,7 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2474
2692
  const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
2475
2693
  return { nextRecords, count, looping: count >= threshold };
2476
2694
  }
2695
+ var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
2477
2696
 
2478
2697
  // src/shields.ts
2479
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.3",
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",