@node9/policy-engine 1.42.0 → 1.44.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
@@ -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.42.0",
3
+ "version": "1.44.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",