@node9/policy-engine 1.66.0 → 1.67.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/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +68 -12
- package/dist/index.mjs +68 -12
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -336,6 +336,20 @@ interface PolicyConfig {
|
|
|
336
336
|
/** Egress / destination control (GAP-5). Optional for back-compat with
|
|
337
337
|
* callers/tests that build a PolicyConfig without it. */
|
|
338
338
|
egress?: EgressPolicy;
|
|
339
|
+
/** Command-checks governance (command-checks-governance-spec.md): admin
|
|
340
|
+
* knobs for the built-in detections' REVIEW-severity findings only.
|
|
341
|
+
* Block-severity findings (pipe-critical, eval-remote, rm-rf-home,
|
|
342
|
+
* nuclear, suspect binary) have NO key here — non-weakenable by
|
|
343
|
+
* construction. Class-B keys (evalDynamic, pipeChainHigh) exclude 'off'.
|
|
344
|
+
* rmAdvisory is consumed by the PROXY at advisory-rule injection. */
|
|
345
|
+
commandChecks?: {
|
|
346
|
+
inlineExec?: 'off' | 'review' | 'block';
|
|
347
|
+
rmAdvisory?: 'off' | 'review' | 'block';
|
|
348
|
+
chmod?: 'off' | 'review' | 'block';
|
|
349
|
+
sqlDdl?: 'off' | 'review' | 'block';
|
|
350
|
+
evalDynamic?: 'review' | 'block';
|
|
351
|
+
pipeChainHigh?: 'review' | 'block';
|
|
352
|
+
};
|
|
339
353
|
};
|
|
340
354
|
settings: {
|
|
341
355
|
mode: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -336,6 +336,20 @@ interface PolicyConfig {
|
|
|
336
336
|
/** Egress / destination control (GAP-5). Optional for back-compat with
|
|
337
337
|
* callers/tests that build a PolicyConfig without it. */
|
|
338
338
|
egress?: EgressPolicy;
|
|
339
|
+
/** Command-checks governance (command-checks-governance-spec.md): admin
|
|
340
|
+
* knobs for the built-in detections' REVIEW-severity findings only.
|
|
341
|
+
* Block-severity findings (pipe-critical, eval-remote, rm-rf-home,
|
|
342
|
+
* nuclear, suspect binary) have NO key here — non-weakenable by
|
|
343
|
+
* construction. Class-B keys (evalDynamic, pipeChainHigh) exclude 'off'.
|
|
344
|
+
* rmAdvisory is consumed by the PROXY at advisory-rule injection. */
|
|
345
|
+
commandChecks?: {
|
|
346
|
+
inlineExec?: 'off' | 'review' | 'block';
|
|
347
|
+
rmAdvisory?: 'off' | 'review' | 'block';
|
|
348
|
+
chmod?: 'off' | 'review' | 'block';
|
|
349
|
+
sqlDdl?: 'off' | 'review' | 'block';
|
|
350
|
+
evalDynamic?: 'review' | 'block';
|
|
351
|
+
pipeChainHigh?: 'review' | 'block';
|
|
352
|
+
};
|
|
339
353
|
};
|
|
340
354
|
settings: {
|
|
341
355
|
mode: string;
|
package/dist/index.js
CHANGED
|
@@ -2176,6 +2176,47 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2176
2176
|
}
|
|
2177
2177
|
|
|
2178
2178
|
// src/policy/index.ts
|
|
2179
|
+
function resolveCheck(v) {
|
|
2180
|
+
return v === "off" || v === "block" ? v : "review";
|
|
2181
|
+
}
|
|
2182
|
+
function resolveCheckTight(v) {
|
|
2183
|
+
return v === "block" ? "block" : "review";
|
|
2184
|
+
}
|
|
2185
|
+
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2186
|
+
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2187
|
+
function detectInlineExec(command) {
|
|
2188
|
+
const pipeFed = command.includes("|");
|
|
2189
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2190
|
+
for (const rawSeg of segments) {
|
|
2191
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2192
|
+
let i = 0;
|
|
2193
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2194
|
+
if (i >= tokens.length) continue;
|
|
2195
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2196
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
2197
|
+
const args = tokens.slice(i + 1);
|
|
2198
|
+
let hadRedirect = false;
|
|
2199
|
+
const positionals = [];
|
|
2200
|
+
for (let j = 0; j < args.length; j++) {
|
|
2201
|
+
const a = args[j];
|
|
2202
|
+
if (a === "-") return true;
|
|
2203
|
+
if (a.startsWith("<")) {
|
|
2204
|
+
hadRedirect = true;
|
|
2205
|
+
if (a === "<" || a === "<<") j++;
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
if (a.startsWith("-")) {
|
|
2209
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
positionals.push(a);
|
|
2213
|
+
}
|
|
2214
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2179
2220
|
var VERDICT_RANK = {
|
|
2180
2221
|
allow: 0,
|
|
2181
2222
|
review: 1,
|
|
@@ -2217,7 +2258,7 @@ function checkDangerousSql(sql) {
|
|
|
2217
2258
|
return "UPDATE without WHERE \u2014 updates every row";
|
|
2218
2259
|
return null;
|
|
2219
2260
|
}
|
|
2220
|
-
function pipeChainVerdict(command, isTrustedHost) {
|
|
2261
|
+
function pipeChainVerdict(command, isTrustedHost, highAction = "review") {
|
|
2221
2262
|
const pipeAnalysis = analyzePipeChain(command);
|
|
2222
2263
|
if (!pipeAnalysis.isPipeline) return null;
|
|
2223
2264
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -2248,7 +2289,7 @@ function pipeChainVerdict(command, isTrustedHost) {
|
|
|
2248
2289
|
};
|
|
2249
2290
|
}
|
|
2250
2291
|
return {
|
|
2251
|
-
decision:
|
|
2292
|
+
decision: highAction,
|
|
2252
2293
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2253
2294
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2254
2295
|
tier: 3
|
|
@@ -2274,7 +2315,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2274
2315
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2275
2316
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2276
2317
|
if (bashCommand !== null) {
|
|
2277
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2318
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2319
|
+
bashCommand,
|
|
2320
|
+
isTrustedHost,
|
|
2321
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2322
|
+
);
|
|
2278
2323
|
if (pipeVerdict) return pipeVerdict;
|
|
2279
2324
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2280
2325
|
if (fsVerdict) {
|
|
@@ -2289,10 +2334,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2289
2334
|
ruleDescription: fsVerdict.reason
|
|
2290
2335
|
};
|
|
2291
2336
|
}
|
|
2292
|
-
const
|
|
2337
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2338
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2293
2339
|
if (sqlVerdict) {
|
|
2294
2340
|
return {
|
|
2295
|
-
|
|
2341
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2342
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2296
2343
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2297
2344
|
reason: sqlVerdict.reason,
|
|
2298
2345
|
tier: 2,
|
|
@@ -2300,10 +2347,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2300
2347
|
ruleDescription: sqlVerdict.description
|
|
2301
2348
|
};
|
|
2302
2349
|
}
|
|
2303
|
-
const
|
|
2350
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2351
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2304
2352
|
if (chmodVerdict) {
|
|
2305
2353
|
return {
|
|
2306
|
-
|
|
2354
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2355
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2307
2356
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2308
2357
|
reason: chmodVerdict.reason,
|
|
2309
2358
|
tier: 2,
|
|
@@ -2346,10 +2395,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2346
2395
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2347
2396
|
allTokens = analyzed.allTokens;
|
|
2348
2397
|
pathTokens = analyzed.paths;
|
|
2349
|
-
const
|
|
2350
|
-
if (
|
|
2398
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2399
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2351
2400
|
return {
|
|
2352
|
-
decision: "review",
|
|
2401
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2353
2402
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2354
2403
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2355
2404
|
tier: 3
|
|
@@ -2367,14 +2416,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2367
2416
|
}
|
|
2368
2417
|
if (evalVerdict === "review") {
|
|
2369
2418
|
return {
|
|
2370
|
-
|
|
2419
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2420
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
2421
|
+
// no knob at all).
|
|
2422
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2371
2423
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2372
2424
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2373
2425
|
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
2374
2426
|
tier: 3
|
|
2375
2427
|
};
|
|
2376
2428
|
}
|
|
2377
|
-
const ptVerdict = pipeChainVerdict(
|
|
2429
|
+
const ptVerdict = pipeChainVerdict(
|
|
2430
|
+
shellCommand,
|
|
2431
|
+
isTrustedHost,
|
|
2432
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2433
|
+
);
|
|
2378
2434
|
if (ptVerdict) return ptVerdict;
|
|
2379
2435
|
if (config.policy.egress?.enabled) {
|
|
2380
2436
|
const dests = extractShellDestinations(shellCommand);
|
package/dist/index.mjs
CHANGED
|
@@ -2066,6 +2066,47 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2066
2066
|
}
|
|
2067
2067
|
|
|
2068
2068
|
// src/policy/index.ts
|
|
2069
|
+
function resolveCheck(v) {
|
|
2070
|
+
return v === "off" || v === "block" ? v : "review";
|
|
2071
|
+
}
|
|
2072
|
+
function resolveCheckTight(v) {
|
|
2073
|
+
return v === "block" ? "block" : "review";
|
|
2074
|
+
}
|
|
2075
|
+
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2076
|
+
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2077
|
+
function detectInlineExec(command) {
|
|
2078
|
+
const pipeFed = command.includes("|");
|
|
2079
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2080
|
+
for (const rawSeg of segments) {
|
|
2081
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2082
|
+
let i = 0;
|
|
2083
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2084
|
+
if (i >= tokens.length) continue;
|
|
2085
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2086
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
2087
|
+
const args = tokens.slice(i + 1);
|
|
2088
|
+
let hadRedirect = false;
|
|
2089
|
+
const positionals = [];
|
|
2090
|
+
for (let j = 0; j < args.length; j++) {
|
|
2091
|
+
const a = args[j];
|
|
2092
|
+
if (a === "-") return true;
|
|
2093
|
+
if (a.startsWith("<")) {
|
|
2094
|
+
hadRedirect = true;
|
|
2095
|
+
if (a === "<" || a === "<<") j++;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (a.startsWith("-")) {
|
|
2099
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
positionals.push(a);
|
|
2103
|
+
}
|
|
2104
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2069
2110
|
var VERDICT_RANK = {
|
|
2070
2111
|
allow: 0,
|
|
2071
2112
|
review: 1,
|
|
@@ -2107,7 +2148,7 @@ function checkDangerousSql(sql) {
|
|
|
2107
2148
|
return "UPDATE without WHERE \u2014 updates every row";
|
|
2108
2149
|
return null;
|
|
2109
2150
|
}
|
|
2110
|
-
function pipeChainVerdict(command, isTrustedHost) {
|
|
2151
|
+
function pipeChainVerdict(command, isTrustedHost, highAction = "review") {
|
|
2111
2152
|
const pipeAnalysis = analyzePipeChain(command);
|
|
2112
2153
|
if (!pipeAnalysis.isPipeline) return null;
|
|
2113
2154
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -2138,7 +2179,7 @@ function pipeChainVerdict(command, isTrustedHost) {
|
|
|
2138
2179
|
};
|
|
2139
2180
|
}
|
|
2140
2181
|
return {
|
|
2141
|
-
decision:
|
|
2182
|
+
decision: highAction,
|
|
2142
2183
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2143
2184
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2144
2185
|
tier: 3
|
|
@@ -2164,7 +2205,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2164
2205
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2165
2206
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2166
2207
|
if (bashCommand !== null) {
|
|
2167
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2208
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2209
|
+
bashCommand,
|
|
2210
|
+
isTrustedHost,
|
|
2211
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2212
|
+
);
|
|
2168
2213
|
if (pipeVerdict) return pipeVerdict;
|
|
2169
2214
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2170
2215
|
if (fsVerdict) {
|
|
@@ -2179,10 +2224,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2179
2224
|
ruleDescription: fsVerdict.reason
|
|
2180
2225
|
};
|
|
2181
2226
|
}
|
|
2182
|
-
const
|
|
2227
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2228
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2183
2229
|
if (sqlVerdict) {
|
|
2184
2230
|
return {
|
|
2185
|
-
|
|
2231
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2232
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2186
2233
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2187
2234
|
reason: sqlVerdict.reason,
|
|
2188
2235
|
tier: 2,
|
|
@@ -2190,10 +2237,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2190
2237
|
ruleDescription: sqlVerdict.description
|
|
2191
2238
|
};
|
|
2192
2239
|
}
|
|
2193
|
-
const
|
|
2240
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2241
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2194
2242
|
if (chmodVerdict) {
|
|
2195
2243
|
return {
|
|
2196
|
-
|
|
2244
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2245
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2197
2246
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2198
2247
|
reason: chmodVerdict.reason,
|
|
2199
2248
|
tier: 2,
|
|
@@ -2236,10 +2285,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2236
2285
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2237
2286
|
allTokens = analyzed.allTokens;
|
|
2238
2287
|
pathTokens = analyzed.paths;
|
|
2239
|
-
const
|
|
2240
|
-
if (
|
|
2288
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2289
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2241
2290
|
return {
|
|
2242
|
-
decision: "review",
|
|
2291
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2243
2292
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2244
2293
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2245
2294
|
tier: 3
|
|
@@ -2257,14 +2306,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2257
2306
|
}
|
|
2258
2307
|
if (evalVerdict === "review") {
|
|
2259
2308
|
return {
|
|
2260
|
-
|
|
2309
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2310
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
2311
|
+
// no knob at all).
|
|
2312
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2261
2313
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2262
2314
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2263
2315
|
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
2264
2316
|
tier: 3
|
|
2265
2317
|
};
|
|
2266
2318
|
}
|
|
2267
|
-
const ptVerdict = pipeChainVerdict(
|
|
2319
|
+
const ptVerdict = pipeChainVerdict(
|
|
2320
|
+
shellCommand,
|
|
2321
|
+
isTrustedHost,
|
|
2322
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2323
|
+
);
|
|
2268
2324
|
if (ptVerdict) return ptVerdict;
|
|
2269
2325
|
if (config.policy.egress?.enabled) {
|
|
2270
2326
|
const dests = extractShellDestinations(shellCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/policy-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.67.1",
|
|
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",
|