@node9/proxy 1.61.0 → 1.61.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 +119 -13
- package/dist/cli.mjs +119 -13
- package/dist/dashboard.mjs +20 -1
- package/dist/index.js +94 -2
- package/dist/index.mjs +94 -2
- package/dist/scan-ink.mjs +19 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -973,6 +973,91 @@ function analyzeFsOperation(command) {
|
|
|
973
973
|
fsOpCache.set(normalized, computed);
|
|
974
974
|
return computed;
|
|
975
975
|
}
|
|
976
|
+
function isSensitiveCleanupName(p) {
|
|
977
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
978
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
979
|
+
}
|
|
980
|
+
function isWaivableCleanupTarget(p) {
|
|
981
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
982
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
983
|
+
if (/[*?[{]/.test(p)) return false;
|
|
984
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
function deriveRedirOp(sample) {
|
|
988
|
+
try {
|
|
989
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
990
|
+
let op = -1;
|
|
991
|
+
syntax.Walk(f, (node) => {
|
|
992
|
+
const n = node;
|
|
993
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
994
|
+
return true;
|
|
995
|
+
});
|
|
996
|
+
return op;
|
|
997
|
+
} catch {
|
|
998
|
+
return -1;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function collectSameCommandCreations(f) {
|
|
1002
|
+
const created = /* @__PURE__ */ new Set();
|
|
1003
|
+
try {
|
|
1004
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1005
|
+
for (const stmt of stmts) {
|
|
1006
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1007
|
+
const redirs = stmt.Redirs || [];
|
|
1008
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1009
|
+
for (const r of redirs) {
|
|
1010
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1011
|
+
const w = resolveWordLiteral(r.Word);
|
|
1012
|
+
if (w) created.add(stripDotSlash(w));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
} catch {
|
|
1017
|
+
return created;
|
|
1018
|
+
}
|
|
1019
|
+
return created;
|
|
1020
|
+
}
|
|
1021
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1022
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1023
|
+
const f = parseShared(command);
|
|
1024
|
+
if (f === PARSE_FAIL) return false;
|
|
1025
|
+
const created = collectSameCommandCreations(f);
|
|
1026
|
+
if (created.size === 0) return false;
|
|
1027
|
+
let sawRm = false;
|
|
1028
|
+
let ok2 = true;
|
|
1029
|
+
try {
|
|
1030
|
+
syntax.Walk(f, (node) => {
|
|
1031
|
+
if (!node || !ok2) return false;
|
|
1032
|
+
const n = node;
|
|
1033
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1034
|
+
const args = n.Args || [];
|
|
1035
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1036
|
+
if (name !== "rm") return true;
|
|
1037
|
+
sawRm = true;
|
|
1038
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1039
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1040
|
+
ok2 = false;
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (paths.length === 0) {
|
|
1044
|
+
ok2 = false;
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
for (const p of paths) {
|
|
1048
|
+
const np = stripDotSlash(p);
|
|
1049
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1050
|
+
ok2 = false;
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return true;
|
|
1055
|
+
});
|
|
1056
|
+
} catch {
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
return sawRm && ok2;
|
|
1060
|
+
}
|
|
976
1061
|
function analyzeFsOperationImpl(command) {
|
|
977
1062
|
const f = parseShared(command);
|
|
978
1063
|
if (f === PARSE_FAIL) return null;
|
|
@@ -1543,8 +1628,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1543
1628
|
}
|
|
1544
1629
|
}
|
|
1545
1630
|
if (config.policy.smartRules.length > 0) {
|
|
1631
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
1546
1632
|
const matches = config.policy.smartRules.filter(
|
|
1547
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
1633
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
1548
1634
|
);
|
|
1549
1635
|
const matchedRule = resolvePinned(matches);
|
|
1550
1636
|
if (matchedRule) {
|
|
@@ -2285,7 +2371,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
2285
2371
|
}
|
|
2286
2372
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
2287
2373
|
}
|
|
2288
|
-
var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
2374
|
+
var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
2289
2375
|
var init_dist = __esm({
|
|
2290
2376
|
"packages/policy-engine/dist/index.mjs"() {
|
|
2291
2377
|
"use strict";
|
|
@@ -2962,7 +3048,7 @@ var init_dist = __esm({
|
|
|
2962
3048
|
"mongosh"
|
|
2963
3049
|
]);
|
|
2964
3050
|
SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
2965
|
-
CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
3051
|
+
CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
2966
3052
|
COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
2967
3053
|
"sudo",
|
|
2968
3054
|
"doas",
|
|
@@ -3065,6 +3151,12 @@ var init_dist = __esm({
|
|
|
3065
3151
|
};
|
|
3066
3152
|
FS_OP_CACHE_MAX = 5e3;
|
|
3067
3153
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
3154
|
+
stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
3155
|
+
REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
3156
|
+
REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
3157
|
+
deriveRedirOp("cat <<X\nX"),
|
|
3158
|
+
deriveRedirOp("cat <<-X\nX")
|
|
3159
|
+
]);
|
|
3068
3160
|
DEFAULT_EGRESS_ALLOWLIST = [
|
|
3069
3161
|
"*.github.com",
|
|
3070
3162
|
"*.githubusercontent.com",
|
|
@@ -50835,7 +50927,7 @@ function normalizeClientName(name) {
|
|
|
50835
50927
|
const sanitized = sanitize4(name).slice(0, 40);
|
|
50836
50928
|
return sanitized.length > 0 ? sanitized : void 0;
|
|
50837
50929
|
}
|
|
50838
|
-
function reportPinMismatchToCloud(serverKey, agent) {
|
|
50930
|
+
function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
|
|
50839
50931
|
try {
|
|
50840
50932
|
const creds = getCredentials();
|
|
50841
50933
|
if (!creds) return;
|
|
@@ -50844,18 +50936,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
|
|
|
50844
50936
|
{ serverKey, reason: "tool-pin-mismatch" },
|
|
50845
50937
|
"mcp-pin-mismatch",
|
|
50846
50938
|
creds,
|
|
50847
|
-
{ mcpServer:
|
|
50939
|
+
{ mcpServer: serverLabel, agent },
|
|
50848
50940
|
void 0,
|
|
50849
50941
|
false,
|
|
50850
50942
|
{
|
|
50851
50943
|
ruleName: "MCP tool definitions changed (possible rug pull)",
|
|
50852
|
-
ruleDescription: `The MCP server "${
|
|
50944
|
+
ruleDescription: `The MCP server "${serverLabel}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
|
|
50853
50945
|
}
|
|
50854
50946
|
);
|
|
50855
50947
|
} catch {
|
|
50856
50948
|
}
|
|
50857
50949
|
}
|
|
50858
|
-
function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
50950
|
+
function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
|
|
50859
50951
|
try {
|
|
50860
50952
|
const creds = getCredentials();
|
|
50861
50953
|
if (!creds) return;
|
|
@@ -50864,7 +50956,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
|
50864
50956
|
{ serverKey, toolCount },
|
|
50865
50957
|
"mcp-discovered",
|
|
50866
50958
|
creds,
|
|
50867
|
-
{ mcpServer:
|
|
50959
|
+
{ mcpServer: serverLabel, agent },
|
|
50868
50960
|
void 0,
|
|
50869
50961
|
false,
|
|
50870
50962
|
{ mcpToolCount: toolCount }
|
|
@@ -50872,7 +50964,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
|
50872
50964
|
} catch {
|
|
50873
50965
|
}
|
|
50874
50966
|
}
|
|
50875
|
-
function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
|
|
50967
|
+
function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
|
|
50876
50968
|
try {
|
|
50877
50969
|
const creds = getCredentials();
|
|
50878
50970
|
if (!creds) return;
|
|
@@ -50881,7 +50973,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
|
|
|
50881
50973
|
{ serverKey, responseBytes },
|
|
50882
50974
|
"mcp-large-response",
|
|
50883
50975
|
creds,
|
|
50884
|
-
{ mcpServer:
|
|
50976
|
+
{ mcpServer: serverLabel, agent },
|
|
50885
50977
|
void 0,
|
|
50886
50978
|
false,
|
|
50887
50979
|
{ mcpResponseBytes: responseBytes }
|
|
@@ -51118,7 +51210,12 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51118
51210
|
const currentHash = hashToolDefinitions(tools);
|
|
51119
51211
|
const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
|
|
51120
51212
|
const token = getInternalToken();
|
|
51121
|
-
reportInventoryToCloud(
|
|
51213
|
+
reportInventoryToCloud(
|
|
51214
|
+
serverKey,
|
|
51215
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51216
|
+
tools.length,
|
|
51217
|
+
clientName
|
|
51218
|
+
);
|
|
51122
51219
|
if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
|
|
51123
51220
|
const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
|
|
51124
51221
|
fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
|
|
@@ -51190,7 +51287,11 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51190
51287
|
console.error(import_chalk21.default.red(" Session quarantined \u2014 all tool calls blocked."));
|
|
51191
51288
|
console.error(import_chalk21.default.yellow(` Run: node9 mcp pin update ${serverKey}
|
|
51192
51289
|
`));
|
|
51193
|
-
reportPinMismatchToCloud(
|
|
51290
|
+
reportPinMismatchToCloud(
|
|
51291
|
+
serverKey,
|
|
51292
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51293
|
+
clientName
|
|
51294
|
+
);
|
|
51194
51295
|
const errorResponse = {
|
|
51195
51296
|
jsonrpc: "2.0",
|
|
51196
51297
|
id: parsed.id,
|
|
@@ -51236,7 +51337,12 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51236
51337
|
`\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
|
|
51237
51338
|
)
|
|
51238
51339
|
);
|
|
51239
|
-
reportLargeResponseToCloud(
|
|
51340
|
+
reportLargeResponseToCloud(
|
|
51341
|
+
serverKey,
|
|
51342
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51343
|
+
line.length,
|
|
51344
|
+
clientName
|
|
51345
|
+
);
|
|
51240
51346
|
if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
|
|
51241
51347
|
const token = getInternalToken();
|
|
51242
51348
|
fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
|
package/dist/cli.mjs
CHANGED
|
@@ -983,6 +983,91 @@ function analyzeFsOperation(command) {
|
|
|
983
983
|
fsOpCache.set(normalized, computed);
|
|
984
984
|
return computed;
|
|
985
985
|
}
|
|
986
|
+
function isSensitiveCleanupName(p) {
|
|
987
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
988
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
989
|
+
}
|
|
990
|
+
function isWaivableCleanupTarget(p) {
|
|
991
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
992
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
993
|
+
if (/[*?[{]/.test(p)) return false;
|
|
994
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
function deriveRedirOp(sample) {
|
|
998
|
+
try {
|
|
999
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1000
|
+
let op = -1;
|
|
1001
|
+
syntax.Walk(f, (node) => {
|
|
1002
|
+
const n = node;
|
|
1003
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
1004
|
+
return true;
|
|
1005
|
+
});
|
|
1006
|
+
return op;
|
|
1007
|
+
} catch {
|
|
1008
|
+
return -1;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function collectSameCommandCreations(f) {
|
|
1012
|
+
const created = /* @__PURE__ */ new Set();
|
|
1013
|
+
try {
|
|
1014
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1015
|
+
for (const stmt of stmts) {
|
|
1016
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1017
|
+
const redirs = stmt.Redirs || [];
|
|
1018
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1019
|
+
for (const r of redirs) {
|
|
1020
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1021
|
+
const w = resolveWordLiteral(r.Word);
|
|
1022
|
+
if (w) created.add(stripDotSlash(w));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} catch {
|
|
1027
|
+
return created;
|
|
1028
|
+
}
|
|
1029
|
+
return created;
|
|
1030
|
+
}
|
|
1031
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1032
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1033
|
+
const f = parseShared(command);
|
|
1034
|
+
if (f === PARSE_FAIL) return false;
|
|
1035
|
+
const created = collectSameCommandCreations(f);
|
|
1036
|
+
if (created.size === 0) return false;
|
|
1037
|
+
let sawRm = false;
|
|
1038
|
+
let ok2 = true;
|
|
1039
|
+
try {
|
|
1040
|
+
syntax.Walk(f, (node) => {
|
|
1041
|
+
if (!node || !ok2) return false;
|
|
1042
|
+
const n = node;
|
|
1043
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1044
|
+
const args = n.Args || [];
|
|
1045
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1046
|
+
if (name !== "rm") return true;
|
|
1047
|
+
sawRm = true;
|
|
1048
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1049
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1050
|
+
ok2 = false;
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
if (paths.length === 0) {
|
|
1054
|
+
ok2 = false;
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
for (const p of paths) {
|
|
1058
|
+
const np = stripDotSlash(p);
|
|
1059
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1060
|
+
ok2 = false;
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return true;
|
|
1065
|
+
});
|
|
1066
|
+
} catch {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
return sawRm && ok2;
|
|
1070
|
+
}
|
|
986
1071
|
function analyzeFsOperationImpl(command) {
|
|
987
1072
|
const f = parseShared(command);
|
|
988
1073
|
if (f === PARSE_FAIL) return null;
|
|
@@ -1553,8 +1638,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1553
1638
|
}
|
|
1554
1639
|
}
|
|
1555
1640
|
if (config.policy.smartRules.length > 0) {
|
|
1641
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
1556
1642
|
const matches = config.policy.smartRules.filter(
|
|
1557
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
1643
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
1558
1644
|
);
|
|
1559
1645
|
const matchedRule = resolvePinned(matches);
|
|
1560
1646
|
if (matchedRule) {
|
|
@@ -2295,7 +2381,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
2295
2381
|
}
|
|
2296
2382
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
2297
2383
|
}
|
|
2298
|
-
var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
2384
|
+
var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
|
|
2299
2385
|
var init_dist = __esm({
|
|
2300
2386
|
"packages/policy-engine/dist/index.mjs"() {
|
|
2301
2387
|
"use strict";
|
|
@@ -2966,7 +3052,7 @@ var init_dist = __esm({
|
|
|
2966
3052
|
"mongosh"
|
|
2967
3053
|
]);
|
|
2968
3054
|
SQL_DDL_RE = /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i;
|
|
2969
|
-
CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
3055
|
+
CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
2970
3056
|
COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
2971
3057
|
"sudo",
|
|
2972
3058
|
"doas",
|
|
@@ -3069,6 +3155,12 @@ var init_dist = __esm({
|
|
|
3069
3155
|
};
|
|
3070
3156
|
FS_OP_CACHE_MAX = 5e3;
|
|
3071
3157
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
3158
|
+
stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
3159
|
+
REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
3160
|
+
REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
3161
|
+
deriveRedirOp("cat <<X\nX"),
|
|
3162
|
+
deriveRedirOp("cat <<-X\nX")
|
|
3163
|
+
]);
|
|
3072
3164
|
DEFAULT_EGRESS_ALLOWLIST = [
|
|
3073
3165
|
"*.github.com",
|
|
3074
3166
|
"*.githubusercontent.com",
|
|
@@ -50828,7 +50920,7 @@ function normalizeClientName(name) {
|
|
|
50828
50920
|
const sanitized = sanitize4(name).slice(0, 40);
|
|
50829
50921
|
return sanitized.length > 0 ? sanitized : void 0;
|
|
50830
50922
|
}
|
|
50831
|
-
function reportPinMismatchToCloud(serverKey, agent) {
|
|
50923
|
+
function reportPinMismatchToCloud(serverKey, serverLabel, agent) {
|
|
50832
50924
|
try {
|
|
50833
50925
|
const creds = getCredentials();
|
|
50834
50926
|
if (!creds) return;
|
|
@@ -50837,18 +50929,18 @@ function reportPinMismatchToCloud(serverKey, agent) {
|
|
|
50837
50929
|
{ serverKey, reason: "tool-pin-mismatch" },
|
|
50838
50930
|
"mcp-pin-mismatch",
|
|
50839
50931
|
creds,
|
|
50840
|
-
{ mcpServer:
|
|
50932
|
+
{ mcpServer: serverLabel, agent },
|
|
50841
50933
|
void 0,
|
|
50842
50934
|
false,
|
|
50843
50935
|
{
|
|
50844
50936
|
ruleName: "MCP tool definitions changed (possible rug pull)",
|
|
50845
|
-
ruleDescription: `The MCP server "${
|
|
50937
|
+
ruleDescription: `The MCP server "${serverLabel}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
|
|
50846
50938
|
}
|
|
50847
50939
|
);
|
|
50848
50940
|
} catch {
|
|
50849
50941
|
}
|
|
50850
50942
|
}
|
|
50851
|
-
function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
50943
|
+
function reportInventoryToCloud(serverKey, serverLabel, toolCount, agent) {
|
|
50852
50944
|
try {
|
|
50853
50945
|
const creds = getCredentials();
|
|
50854
50946
|
if (!creds) return;
|
|
@@ -50857,7 +50949,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
|
50857
50949
|
{ serverKey, toolCount },
|
|
50858
50950
|
"mcp-discovered",
|
|
50859
50951
|
creds,
|
|
50860
|
-
{ mcpServer:
|
|
50952
|
+
{ mcpServer: serverLabel, agent },
|
|
50861
50953
|
void 0,
|
|
50862
50954
|
false,
|
|
50863
50955
|
{ mcpToolCount: toolCount }
|
|
@@ -50865,7 +50957,7 @@ function reportInventoryToCloud(serverKey, toolCount, agent) {
|
|
|
50865
50957
|
} catch {
|
|
50866
50958
|
}
|
|
50867
50959
|
}
|
|
50868
|
-
function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
|
|
50960
|
+
function reportLargeResponseToCloud(serverKey, serverLabel, responseBytes, agent) {
|
|
50869
50961
|
try {
|
|
50870
50962
|
const creds = getCredentials();
|
|
50871
50963
|
if (!creds) return;
|
|
@@ -50874,7 +50966,7 @@ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
|
|
|
50874
50966
|
{ serverKey, responseBytes },
|
|
50875
50967
|
"mcp-large-response",
|
|
50876
50968
|
creds,
|
|
50877
|
-
{ mcpServer:
|
|
50969
|
+
{ mcpServer: serverLabel, agent },
|
|
50878
50970
|
void 0,
|
|
50879
50971
|
false,
|
|
50880
50972
|
{ mcpResponseBytes: responseBytes }
|
|
@@ -51111,7 +51203,12 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51111
51203
|
const currentHash = hashToolDefinitions(tools);
|
|
51112
51204
|
const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
|
|
51113
51205
|
const token = getInternalToken();
|
|
51114
|
-
reportInventoryToCloud(
|
|
51206
|
+
reportInventoryToCloud(
|
|
51207
|
+
serverKey,
|
|
51208
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51209
|
+
tools.length,
|
|
51210
|
+
clientName
|
|
51211
|
+
);
|
|
51115
51212
|
if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
|
|
51116
51213
|
const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
|
|
51117
51214
|
fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
|
|
@@ -51183,7 +51280,11 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51183
51280
|
console.error(chalk21.red(" Session quarantined \u2014 all tool calls blocked."));
|
|
51184
51281
|
console.error(chalk21.yellow(` Run: node9 mcp pin update ${serverKey}
|
|
51185
51282
|
`));
|
|
51186
|
-
reportPinMismatchToCloud(
|
|
51283
|
+
reportPinMismatchToCloud(
|
|
51284
|
+
serverKey,
|
|
51285
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51286
|
+
clientName
|
|
51287
|
+
);
|
|
51187
51288
|
const errorResponse = {
|
|
51188
51289
|
jsonrpc: "2.0",
|
|
51189
51290
|
id: parsed.id,
|
|
@@ -51229,7 +51330,12 @@ async function runMcpGateway(upstreamCommand, configName) {
|
|
|
51229
51330
|
`\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
|
|
51230
51331
|
)
|
|
51231
51332
|
);
|
|
51232
|
-
reportLargeResponseToCloud(
|
|
51333
|
+
reportLargeResponseToCloud(
|
|
51334
|
+
serverKey,
|
|
51335
|
+
resolveServerLabel("", serverKey, upstreamCommand, configName),
|
|
51336
|
+
line.length,
|
|
51337
|
+
clientName
|
|
51338
|
+
);
|
|
51233
51339
|
if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
|
|
51234
51340
|
const token = getInternalToken();
|
|
51235
51341
|
fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
|
package/dist/dashboard.mjs
CHANGED
|
@@ -443,6 +443,20 @@ function analyzeFsOperation(command) {
|
|
|
443
443
|
fsOpCache.set(normalized, computed);
|
|
444
444
|
return computed;
|
|
445
445
|
}
|
|
446
|
+
function deriveRedirOp(sample) {
|
|
447
|
+
try {
|
|
448
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
449
|
+
let op = -1;
|
|
450
|
+
syntax.Walk(f, (node) => {
|
|
451
|
+
const n = node;
|
|
452
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
453
|
+
return true;
|
|
454
|
+
});
|
|
455
|
+
return op;
|
|
456
|
+
} catch {
|
|
457
|
+
return -1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
446
460
|
function analyzeFsOperationImpl(command) {
|
|
447
461
|
const f = parseShared(command);
|
|
448
462
|
if (f === PARSE_FAIL) return null;
|
|
@@ -655,7 +669,7 @@ function assertBuiltinShieldRegexesAreSafe() {
|
|
|
655
669
|
}
|
|
656
670
|
}
|
|
657
671
|
}
|
|
658
|
-
var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
672
|
+
var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
659
673
|
var init_dist = __esm({
|
|
660
674
|
"packages/policy-engine/dist/index.mjs"() {
|
|
661
675
|
"use strict";
|
|
@@ -1273,6 +1287,11 @@ var init_dist = __esm({
|
|
|
1273
1287
|
]);
|
|
1274
1288
|
FS_OP_CACHE_MAX = 5e3;
|
|
1275
1289
|
fsOpCache = /* @__PURE__ */ new Map();
|
|
1290
|
+
REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
1291
|
+
REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
1292
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1293
|
+
deriveRedirOp("cat <<-X\nX")
|
|
1294
|
+
]);
|
|
1276
1295
|
MAX_REGEX_LENGTH = 100;
|
|
1277
1296
|
REGEX_CACHE_MAX = 500;
|
|
1278
1297
|
regexCache = /* @__PURE__ */ new Map();
|
package/dist/index.js
CHANGED
|
@@ -1351,7 +1351,7 @@ function analyzeSqlDestructive(command) {
|
|
|
1351
1351
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1352
1352
|
};
|
|
1353
1353
|
}
|
|
1354
|
-
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
1354
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
1355
1355
|
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1356
1356
|
"sudo",
|
|
1357
1357
|
"doas",
|
|
@@ -1671,6 +1671,97 @@ function analyzeFsOperation(command) {
|
|
|
1671
1671
|
fsOpCache.set(normalized, computed);
|
|
1672
1672
|
return computed;
|
|
1673
1673
|
}
|
|
1674
|
+
var stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
1675
|
+
function isSensitiveCleanupName(p) {
|
|
1676
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
1677
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
1678
|
+
}
|
|
1679
|
+
function isWaivableCleanupTarget(p) {
|
|
1680
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
1681
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
1682
|
+
if (/[*?[{]/.test(p)) return false;
|
|
1683
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
1684
|
+
return true;
|
|
1685
|
+
}
|
|
1686
|
+
function deriveRedirOp(sample) {
|
|
1687
|
+
try {
|
|
1688
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1689
|
+
let op = -1;
|
|
1690
|
+
syntax.Walk(f, (node) => {
|
|
1691
|
+
const n = node;
|
|
1692
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
1693
|
+
return true;
|
|
1694
|
+
});
|
|
1695
|
+
return op;
|
|
1696
|
+
} catch {
|
|
1697
|
+
return -1;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
1701
|
+
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
1702
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1703
|
+
deriveRedirOp("cat <<-X\nX")
|
|
1704
|
+
]);
|
|
1705
|
+
function collectSameCommandCreations(f) {
|
|
1706
|
+
const created = /* @__PURE__ */ new Set();
|
|
1707
|
+
try {
|
|
1708
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1709
|
+
for (const stmt of stmts) {
|
|
1710
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1711
|
+
const redirs = stmt.Redirs || [];
|
|
1712
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1713
|
+
for (const r of redirs) {
|
|
1714
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1715
|
+
const w = resolveWordLiteral(r.Word);
|
|
1716
|
+
if (w) created.add(stripDotSlash(w));
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
} catch {
|
|
1721
|
+
return created;
|
|
1722
|
+
}
|
|
1723
|
+
return created;
|
|
1724
|
+
}
|
|
1725
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1726
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1727
|
+
const f = parseShared(command);
|
|
1728
|
+
if (f === PARSE_FAIL) return false;
|
|
1729
|
+
const created = collectSameCommandCreations(f);
|
|
1730
|
+
if (created.size === 0) return false;
|
|
1731
|
+
let sawRm = false;
|
|
1732
|
+
let ok = true;
|
|
1733
|
+
try {
|
|
1734
|
+
syntax.Walk(f, (node) => {
|
|
1735
|
+
if (!node || !ok) return false;
|
|
1736
|
+
const n = node;
|
|
1737
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1738
|
+
const args = n.Args || [];
|
|
1739
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1740
|
+
if (name !== "rm") return true;
|
|
1741
|
+
sawRm = true;
|
|
1742
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1743
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1744
|
+
ok = false;
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
if (paths.length === 0) {
|
|
1748
|
+
ok = false;
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
for (const p of paths) {
|
|
1752
|
+
const np = stripDotSlash(p);
|
|
1753
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1754
|
+
ok = false;
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return true;
|
|
1759
|
+
});
|
|
1760
|
+
} catch {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
return sawRm && ok;
|
|
1764
|
+
}
|
|
1674
1765
|
function analyzeFsOperationImpl(command) {
|
|
1675
1766
|
const f = parseShared(command);
|
|
1676
1767
|
if (f === PARSE_FAIL) return null;
|
|
@@ -2398,8 +2489,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2398
2489
|
}
|
|
2399
2490
|
}
|
|
2400
2491
|
if (config.policy.smartRules.length > 0) {
|
|
2492
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2401
2493
|
const matches = config.policy.smartRules.filter(
|
|
2402
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
2494
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2403
2495
|
);
|
|
2404
2496
|
const matchedRule = resolvePinned(matches);
|
|
2405
2497
|
if (matchedRule) {
|
package/dist/index.mjs
CHANGED
|
@@ -1321,7 +1321,7 @@ function analyzeSqlDestructive(command) {
|
|
|
1321
1321
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1322
1322
|
};
|
|
1323
1323
|
}
|
|
1324
|
-
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"
|
|
1324
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx"]);
|
|
1325
1325
|
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1326
1326
|
"sudo",
|
|
1327
1327
|
"doas",
|
|
@@ -1641,6 +1641,97 @@ function analyzeFsOperation(command) {
|
|
|
1641
1641
|
fsOpCache.set(normalized, computed);
|
|
1642
1642
|
return computed;
|
|
1643
1643
|
}
|
|
1644
|
+
var stripDotSlash = (p) => p.replace(/^\.\//, "");
|
|
1645
|
+
function isSensitiveCleanupName(p) {
|
|
1646
|
+
const base = p.replace(/^.*[\\/]/, "");
|
|
1647
|
+
return /^\.env(\.|$)/i.test(base) || /(?:^|[\\/])\.(?:ssh|aws|gnupg|git)(?:[\\/]|$)/i.test(p) || /\.(?:pem|key|p12|pfx|crt)$/i.test(base) || /^\.?(?:netrc|npmrc|pgpass|htpasswd)$/i.test(base) || /^id_(?:rsa|dsa|ecdsa|ed25519)/i.test(base) || /credential/i.test(p) || /secret/i.test(base);
|
|
1648
|
+
}
|
|
1649
|
+
function isWaivableCleanupTarget(p) {
|
|
1650
|
+
if (/^[/~]/.test(p) || /^\$/.test(p)) return false;
|
|
1651
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(p)) return false;
|
|
1652
|
+
if (/[*?[{]/.test(p)) return false;
|
|
1653
|
+
if (isSensitiveCleanupName(p)) return false;
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
function deriveRedirOp(sample) {
|
|
1657
|
+
try {
|
|
1658
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1659
|
+
let op = -1;
|
|
1660
|
+
syntax.Walk(f, (node) => {
|
|
1661
|
+
const n = node;
|
|
1662
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
1663
|
+
return true;
|
|
1664
|
+
});
|
|
1665
|
+
return op;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return -1;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
1671
|
+
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
1672
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1673
|
+
deriveRedirOp("cat <<-X\nX")
|
|
1674
|
+
]);
|
|
1675
|
+
function collectSameCommandCreations(f) {
|
|
1676
|
+
const created = /* @__PURE__ */ new Set();
|
|
1677
|
+
try {
|
|
1678
|
+
const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
|
|
1679
|
+
for (const stmt of stmts) {
|
|
1680
|
+
if (!stmt || !stmt.Cmd || syntax.NodeType(stmt.Cmd) !== "CallExpr") continue;
|
|
1681
|
+
const redirs = stmt.Redirs || [];
|
|
1682
|
+
if (!redirs.some((r) => r && REDIR_HEREDOC_OPS.has(r.Op))) continue;
|
|
1683
|
+
for (const r of redirs) {
|
|
1684
|
+
if (r && REDIR_TRUNCATE_OPS.has(r.Op) && r.N == null) {
|
|
1685
|
+
const w = resolveWordLiteral(r.Word);
|
|
1686
|
+
if (w) created.add(stripDotSlash(w));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
} catch {
|
|
1691
|
+
return created;
|
|
1692
|
+
}
|
|
1693
|
+
return created;
|
|
1694
|
+
}
|
|
1695
|
+
function isRmCreatedInCommandCleanup(command) {
|
|
1696
|
+
if (!/\brm\b/.test(command)) return false;
|
|
1697
|
+
const f = parseShared(command);
|
|
1698
|
+
if (f === PARSE_FAIL) return false;
|
|
1699
|
+
const created = collectSameCommandCreations(f);
|
|
1700
|
+
if (created.size === 0) return false;
|
|
1701
|
+
let sawRm = false;
|
|
1702
|
+
let ok = true;
|
|
1703
|
+
try {
|
|
1704
|
+
syntax.Walk(f, (node) => {
|
|
1705
|
+
if (!node || !ok) return false;
|
|
1706
|
+
const n = node;
|
|
1707
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1708
|
+
const args = n.Args || [];
|
|
1709
|
+
const name = (resolveWordLiteral(args[0]) ?? "").toLowerCase();
|
|
1710
|
+
if (name !== "rm") return true;
|
|
1711
|
+
sawRm = true;
|
|
1712
|
+
const { flags, paths } = extractLiteralArgs(n);
|
|
1713
|
+
if (args.length - 1 > flags.length + paths.length) {
|
|
1714
|
+
ok = false;
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
if (paths.length === 0) {
|
|
1718
|
+
ok = false;
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
for (const p of paths) {
|
|
1722
|
+
const np = stripDotSlash(p);
|
|
1723
|
+
if (!created.has(np) || !isWaivableCleanupTarget(np)) {
|
|
1724
|
+
ok = false;
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return true;
|
|
1729
|
+
});
|
|
1730
|
+
} catch {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
return sawRm && ok;
|
|
1734
|
+
}
|
|
1644
1735
|
function analyzeFsOperationImpl(command) {
|
|
1645
1736
|
const f = parseShared(command);
|
|
1646
1737
|
if (f === PARSE_FAIL) return null;
|
|
@@ -2368,8 +2459,9 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2368
2459
|
}
|
|
2369
2460
|
}
|
|
2370
2461
|
if (config.policy.smartRules.length > 0) {
|
|
2462
|
+
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2371
2463
|
const matches = config.policy.smartRules.filter(
|
|
2372
|
-
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && evaluateSmartConditions(args, rule)
|
|
2464
|
+
(rule) => matchesPattern(toolName, rule.tool) && !(bashCommand !== null && rule.name && AST_FS_REGEX_RULES.has(rule.name)) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2373
2465
|
);
|
|
2374
2466
|
const matchedRule = resolvePinned(matches);
|
|
2375
2467
|
if (matchedRule) {
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -818,6 +818,25 @@ function assertBuiltinPatternsAreSafe() {
|
|
|
818
818
|
assertBuiltinPatternsAreSafe();
|
|
819
819
|
var { syntax } = mvdanSh;
|
|
820
820
|
var sharedParser = syntax.NewParser();
|
|
821
|
+
function deriveRedirOp(sample) {
|
|
822
|
+
try {
|
|
823
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
824
|
+
let op = -1;
|
|
825
|
+
syntax.Walk(f, (node) => {
|
|
826
|
+
const n = node;
|
|
827
|
+
if (n && syntax.NodeType(n) === "Redirect" && op < 0) op = n.Op;
|
|
828
|
+
return true;
|
|
829
|
+
});
|
|
830
|
+
return op;
|
|
831
|
+
} catch {
|
|
832
|
+
return -1;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
836
|
+
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
837
|
+
deriveRedirOp("cat <<X\nX"),
|
|
838
|
+
deriveRedirOp("cat <<-X\nX")
|
|
839
|
+
]);
|
|
821
840
|
var aws_default = {
|
|
822
841
|
name: "aws",
|
|
823
842
|
description: "Protects AWS infrastructure from destructive AI operations",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.1",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|