@node9/policy-engine 1.65.1 → 1.67.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.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +72 -13
- package/dist/index.mjs +72 -13
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -329,10 +329,27 @@ interface PolicyConfig {
|
|
|
329
329
|
dlp: {
|
|
330
330
|
enabled: boolean;
|
|
331
331
|
scanIgnoredTools: boolean;
|
|
332
|
+
/** Inline-ask v2: 'block' upgrades review-severity DLP matches to a hard
|
|
333
|
+
* block. Optional for back-compat with callers that don't set it. */
|
|
334
|
+
reviewAction?: 'review' | 'block';
|
|
332
335
|
};
|
|
333
336
|
/** Egress / destination control (GAP-5). Optional for back-compat with
|
|
334
337
|
* callers/tests that build a PolicyConfig without it. */
|
|
335
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
|
+
};
|
|
336
353
|
};
|
|
337
354
|
settings: {
|
|
338
355
|
mode: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -329,10 +329,27 @@ interface PolicyConfig {
|
|
|
329
329
|
dlp: {
|
|
330
330
|
enabled: boolean;
|
|
331
331
|
scanIgnoredTools: boolean;
|
|
332
|
+
/** Inline-ask v2: 'block' upgrades review-severity DLP matches to a hard
|
|
333
|
+
* block. Optional for back-compat with callers that don't set it. */
|
|
334
|
+
reviewAction?: 'review' | 'block';
|
|
332
335
|
};
|
|
333
336
|
/** Egress / destination control (GAP-5). Optional for back-compat with
|
|
334
337
|
* callers/tests that build a PolicyConfig without it. */
|
|
335
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
|
+
};
|
|
336
353
|
};
|
|
337
354
|
settings: {
|
|
338
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
|
|
@@ -2262,7 +2303,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2262
2303
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2263
2304
|
if (dlpMatch) {
|
|
2264
2305
|
return {
|
|
2265
|
-
|
|
2306
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
2307
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
2308
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
2309
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
2266
2310
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2267
2311
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2268
2312
|
};
|
|
@@ -2271,7 +2315,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2271
2315
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2272
2316
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2273
2317
|
if (bashCommand !== null) {
|
|
2274
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2318
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2319
|
+
bashCommand,
|
|
2320
|
+
isTrustedHost,
|
|
2321
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2322
|
+
);
|
|
2275
2323
|
if (pipeVerdict) return pipeVerdict;
|
|
2276
2324
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2277
2325
|
if (fsVerdict) {
|
|
@@ -2286,10 +2334,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2286
2334
|
ruleDescription: fsVerdict.reason
|
|
2287
2335
|
};
|
|
2288
2336
|
}
|
|
2289
|
-
const
|
|
2337
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2338
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2290
2339
|
if (sqlVerdict) {
|
|
2291
2340
|
return {
|
|
2292
|
-
|
|
2341
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2342
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2293
2343
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2294
2344
|
reason: sqlVerdict.reason,
|
|
2295
2345
|
tier: 2,
|
|
@@ -2297,10 +2347,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2297
2347
|
ruleDescription: sqlVerdict.description
|
|
2298
2348
|
};
|
|
2299
2349
|
}
|
|
2300
|
-
const
|
|
2350
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2351
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2301
2352
|
if (chmodVerdict) {
|
|
2302
2353
|
return {
|
|
2303
|
-
|
|
2354
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2355
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2304
2356
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2305
2357
|
reason: chmodVerdict.reason,
|
|
2306
2358
|
tier: 2,
|
|
@@ -2343,10 +2395,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2343
2395
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2344
2396
|
allTokens = analyzed.allTokens;
|
|
2345
2397
|
pathTokens = analyzed.paths;
|
|
2346
|
-
const
|
|
2347
|
-
if (
|
|
2398
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2399
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2348
2400
|
return {
|
|
2349
|
-
decision: "review",
|
|
2401
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2350
2402
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2351
2403
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2352
2404
|
tier: 3
|
|
@@ -2364,14 +2416,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2364
2416
|
}
|
|
2365
2417
|
if (evalVerdict === "review") {
|
|
2366
2418
|
return {
|
|
2367
|
-
|
|
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),
|
|
2368
2423
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2369
2424
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2370
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.",
|
|
2371
2426
|
tier: 3
|
|
2372
2427
|
};
|
|
2373
2428
|
}
|
|
2374
|
-
const ptVerdict = pipeChainVerdict(
|
|
2429
|
+
const ptVerdict = pipeChainVerdict(
|
|
2430
|
+
shellCommand,
|
|
2431
|
+
isTrustedHost,
|
|
2432
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2433
|
+
);
|
|
2375
2434
|
if (ptVerdict) return ptVerdict;
|
|
2376
2435
|
if (config.policy.egress?.enabled) {
|
|
2377
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
|
|
@@ -2152,7 +2193,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2152
2193
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2153
2194
|
if (dlpMatch) {
|
|
2154
2195
|
return {
|
|
2155
|
-
|
|
2196
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
2197
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
2198
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
2199
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
2156
2200
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2157
2201
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2158
2202
|
};
|
|
@@ -2161,7 +2205,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2161
2205
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2162
2206
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2163
2207
|
if (bashCommand !== null) {
|
|
2164
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2208
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2209
|
+
bashCommand,
|
|
2210
|
+
isTrustedHost,
|
|
2211
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2212
|
+
);
|
|
2165
2213
|
if (pipeVerdict) return pipeVerdict;
|
|
2166
2214
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2167
2215
|
if (fsVerdict) {
|
|
@@ -2176,10 +2224,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2176
2224
|
ruleDescription: fsVerdict.reason
|
|
2177
2225
|
};
|
|
2178
2226
|
}
|
|
2179
|
-
const
|
|
2227
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2228
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2180
2229
|
if (sqlVerdict) {
|
|
2181
2230
|
return {
|
|
2182
|
-
|
|
2231
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2232
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2183
2233
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2184
2234
|
reason: sqlVerdict.reason,
|
|
2185
2235
|
tier: 2,
|
|
@@ -2187,10 +2237,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2187
2237
|
ruleDescription: sqlVerdict.description
|
|
2188
2238
|
};
|
|
2189
2239
|
}
|
|
2190
|
-
const
|
|
2240
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2241
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2191
2242
|
if (chmodVerdict) {
|
|
2192
2243
|
return {
|
|
2193
|
-
|
|
2244
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2245
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2194
2246
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2195
2247
|
reason: chmodVerdict.reason,
|
|
2196
2248
|
tier: 2,
|
|
@@ -2233,10 +2285,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2233
2285
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2234
2286
|
allTokens = analyzed.allTokens;
|
|
2235
2287
|
pathTokens = analyzed.paths;
|
|
2236
|
-
const
|
|
2237
|
-
if (
|
|
2288
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2289
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2238
2290
|
return {
|
|
2239
|
-
decision: "review",
|
|
2291
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2240
2292
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2241
2293
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2242
2294
|
tier: 3
|
|
@@ -2254,14 +2306,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2254
2306
|
}
|
|
2255
2307
|
if (evalVerdict === "review") {
|
|
2256
2308
|
return {
|
|
2257
|
-
|
|
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),
|
|
2258
2313
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2259
2314
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2260
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.",
|
|
2261
2316
|
tier: 3
|
|
2262
2317
|
};
|
|
2263
2318
|
}
|
|
2264
|
-
const ptVerdict = pipeChainVerdict(
|
|
2319
|
+
const ptVerdict = pipeChainVerdict(
|
|
2320
|
+
shellCommand,
|
|
2321
|
+
isTrustedHost,
|
|
2322
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2323
|
+
);
|
|
2265
2324
|
if (ptVerdict) return ptVerdict;
|
|
2266
2325
|
if (config.policy.egress?.enabled) {
|
|
2267
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.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",
|