@node9/policy-engine 1.43.0 → 1.45.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 +73 -1
- package/dist/index.mjs +73 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1109,7 +1109,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1109
1109
|
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1110
1110
|
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1111
1111
|
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1112
|
-
"review-drop-truncate-shell"
|
|
1112
|
+
"review-drop-truncate-shell",
|
|
1113
|
+
// chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
|
|
1114
|
+
// regex smart rule is suppressed for bash — it matched `chmod 777` inside a
|
|
1115
|
+
// `node -e` / `python -c` string literal (a detection pattern, not a run
|
|
1116
|
+
// command) → false positive.
|
|
1117
|
+
"shield:filesystem:review-chmod-777"
|
|
1113
1118
|
]);
|
|
1114
1119
|
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1115
1120
|
"psql",
|
|
@@ -1134,6 +1139,62 @@ function analyzeSqlDestructive(command) {
|
|
|
1134
1139
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1135
1140
|
};
|
|
1136
1141
|
}
|
|
1142
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
|
|
1143
|
+
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1144
|
+
"sudo",
|
|
1145
|
+
"doas",
|
|
1146
|
+
"env",
|
|
1147
|
+
"xargs",
|
|
1148
|
+
"time",
|
|
1149
|
+
"nice",
|
|
1150
|
+
"ionice",
|
|
1151
|
+
"nohup",
|
|
1152
|
+
"setsid",
|
|
1153
|
+
"stdbuf",
|
|
1154
|
+
"timeout",
|
|
1155
|
+
"command",
|
|
1156
|
+
"exec"
|
|
1157
|
+
]);
|
|
1158
|
+
function chmodHasOpenPermMode(command) {
|
|
1159
|
+
const f = parseShared(command);
|
|
1160
|
+
if (f === PARSE_FAIL) return false;
|
|
1161
|
+
let found = false;
|
|
1162
|
+
try {
|
|
1163
|
+
syntax.Walk(f, (node) => {
|
|
1164
|
+
if (!node || found) return false;
|
|
1165
|
+
const n = node;
|
|
1166
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1167
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
1168
|
+
if (words.length === 0) return true;
|
|
1169
|
+
const name = (words[0] ?? "").toLowerCase();
|
|
1170
|
+
let idx = -1;
|
|
1171
|
+
if (name === "chmod") idx = 0;
|
|
1172
|
+
else if (COMMAND_WRAPPERS.has(name))
|
|
1173
|
+
idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
|
|
1174
|
+
if (idx < 0) return true;
|
|
1175
|
+
for (let i = idx + 1; i < words.length; i++) {
|
|
1176
|
+
const w = words[i];
|
|
1177
|
+
if (w !== null && w.startsWith("-")) continue;
|
|
1178
|
+
if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
return true;
|
|
1182
|
+
});
|
|
1183
|
+
} catch {
|
|
1184
|
+
return found;
|
|
1185
|
+
}
|
|
1186
|
+
return found;
|
|
1187
|
+
}
|
|
1188
|
+
function analyzeChmod777(command) {
|
|
1189
|
+
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1190
|
+
if (!chmodHasOpenPermMode(command)) return null;
|
|
1191
|
+
return {
|
|
1192
|
+
ruleName: "shield:filesystem:review-chmod-777",
|
|
1193
|
+
verdict: "review",
|
|
1194
|
+
reason: "chmod 777 requires human approval (filesystem shield)",
|
|
1195
|
+
description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1137
1198
|
function isProtectedHomePath(rawPath) {
|
|
1138
1199
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1139
1200
|
let underHome = false;
|
|
@@ -2130,6 +2191,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2130
2191
|
ruleDescription: sqlVerdict.description
|
|
2131
2192
|
};
|
|
2132
2193
|
}
|
|
2194
|
+
const chmodVerdict = analyzeChmod777(bashCommand);
|
|
2195
|
+
if (chmodVerdict) {
|
|
2196
|
+
return {
|
|
2197
|
+
decision: chmodVerdict.verdict,
|
|
2198
|
+
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2199
|
+
reason: chmodVerdict.reason,
|
|
2200
|
+
tier: 2,
|
|
2201
|
+
ruleName: chmodVerdict.ruleName,
|
|
2202
|
+
ruleDescription: chmodVerdict.description
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2133
2205
|
}
|
|
2134
2206
|
if (config.policy.smartRules.length > 0) {
|
|
2135
2207
|
const matchedRule = config.policy.smartRules.find(
|
package/dist/index.mjs
CHANGED
|
@@ -1001,7 +1001,12 @@ var AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
|
1001
1001
|
// SQL-DDL is now owned by the AST detector (analyzeSqlDestructive) so the
|
|
1002
1002
|
// raw-regex smart rule is suppressed for bash — its cond1 read a grep
|
|
1003
1003
|
// alternation's `|` as a shell pipe (`grep "…|mysql…"` → false positive).
|
|
1004
|
-
"review-drop-truncate-shell"
|
|
1004
|
+
"review-drop-truncate-shell",
|
|
1005
|
+
// chmod 777 is now owned by the AST detector (analyzeChmod777) so the raw-
|
|
1006
|
+
// regex smart rule is suppressed for bash — it matched `chmod 777` inside a
|
|
1007
|
+
// `node -e` / `python -c` string literal (a detection pattern, not a run
|
|
1008
|
+
// command) → false positive.
|
|
1009
|
+
"shield:filesystem:review-chmod-777"
|
|
1005
1010
|
]);
|
|
1006
1011
|
var SQL_DB_CLIS = /* @__PURE__ */ new Set([
|
|
1007
1012
|
"psql",
|
|
@@ -1026,6 +1031,62 @@ function analyzeSqlDestructive(command) {
|
|
|
1026
1031
|
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
1027
1032
|
};
|
|
1028
1033
|
}
|
|
1034
|
+
var CHMOD_OPEN_PERM_TOKENS = /* @__PURE__ */ new Set(["777", "0777", "a+rwx", "+x"]);
|
|
1035
|
+
var COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1036
|
+
"sudo",
|
|
1037
|
+
"doas",
|
|
1038
|
+
"env",
|
|
1039
|
+
"xargs",
|
|
1040
|
+
"time",
|
|
1041
|
+
"nice",
|
|
1042
|
+
"ionice",
|
|
1043
|
+
"nohup",
|
|
1044
|
+
"setsid",
|
|
1045
|
+
"stdbuf",
|
|
1046
|
+
"timeout",
|
|
1047
|
+
"command",
|
|
1048
|
+
"exec"
|
|
1049
|
+
]);
|
|
1050
|
+
function chmodHasOpenPermMode(command) {
|
|
1051
|
+
const f = parseShared(command);
|
|
1052
|
+
if (f === PARSE_FAIL) return false;
|
|
1053
|
+
let found = false;
|
|
1054
|
+
try {
|
|
1055
|
+
syntax.Walk(f, (node) => {
|
|
1056
|
+
if (!node || found) return false;
|
|
1057
|
+
const n = node;
|
|
1058
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1059
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
1060
|
+
if (words.length === 0) return true;
|
|
1061
|
+
const name = (words[0] ?? "").toLowerCase();
|
|
1062
|
+
let idx = -1;
|
|
1063
|
+
if (name === "chmod") idx = 0;
|
|
1064
|
+
else if (COMMAND_WRAPPERS.has(name))
|
|
1065
|
+
idx = words.findIndex((w, i) => i > 0 && w?.toLowerCase() === "chmod");
|
|
1066
|
+
if (idx < 0) return true;
|
|
1067
|
+
for (let i = idx + 1; i < words.length; i++) {
|
|
1068
|
+
const w = words[i];
|
|
1069
|
+
if (w !== null && w.startsWith("-")) continue;
|
|
1070
|
+
if (w !== null && CHMOD_OPEN_PERM_TOKENS.has(w.toLowerCase())) found = true;
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
1074
|
+
});
|
|
1075
|
+
} catch {
|
|
1076
|
+
return found;
|
|
1077
|
+
}
|
|
1078
|
+
return found;
|
|
1079
|
+
}
|
|
1080
|
+
function analyzeChmod777(command) {
|
|
1081
|
+
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1082
|
+
if (!chmodHasOpenPermMode(command)) return null;
|
|
1083
|
+
return {
|
|
1084
|
+
ruleName: "shield:filesystem:review-chmod-777",
|
|
1085
|
+
verdict: "review",
|
|
1086
|
+
reason: "chmod 777 requires human approval (filesystem shield)",
|
|
1087
|
+
description: "The AI wants to make a file world-writable/executable (chmod 777). This removes the permission protection on the file so any user or process can modify or run it."
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1029
1090
|
function isProtectedHomePath(rawPath) {
|
|
1030
1091
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
1031
1092
|
let underHome = false;
|
|
@@ -2022,6 +2083,17 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2022
2083
|
ruleDescription: sqlVerdict.description
|
|
2023
2084
|
};
|
|
2024
2085
|
}
|
|
2086
|
+
const chmodVerdict = analyzeChmod777(bashCommand);
|
|
2087
|
+
if (chmodVerdict) {
|
|
2088
|
+
return {
|
|
2089
|
+
decision: chmodVerdict.verdict,
|
|
2090
|
+
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2091
|
+
reason: chmodVerdict.reason,
|
|
2092
|
+
tier: 2,
|
|
2093
|
+
ruleName: chmodVerdict.ruleName,
|
|
2094
|
+
ruleDescription: chmodVerdict.description
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2025
2097
|
}
|
|
2026
2098
|
if (config.policy.smartRules.length > 0) {
|
|
2027
2099
|
const matchedRule = config.policy.smartRules.find(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.0",
|
|
4
4
|
"description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://node9.ai",
|