@node9/proxy 1.18.3 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +637 -156
- package/dist/cli.mjs +637 -156
- package/dist/index.js +255 -36
- package/dist/index.mjs +255 -36
- package/package.json +3 -1
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
|
|
1608
|
-
if (
|
|
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
|
|
1578
|
-
if (
|
|
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.
|
|
3
|
+
"version": "1.19.1",
|
|
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",
|