@node9/proxy 1.66.0 → 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/cli.js +167 -17
- package/dist/cli.mjs +167 -17
- package/dist/dashboard.mjs +10 -0
- package/dist/index.js +136 -13
- package/dist/index.mjs +136 -13
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -376,6 +376,16 @@ var init_config_schema = __esm({
|
|
|
376
376
|
pii: import_zod.z.enum(["off", "block"]).optional(),
|
|
377
377
|
reviewAction: import_zod.z.enum(["review", "block"]).optional()
|
|
378
378
|
}).optional(),
|
|
379
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
380
|
+
// deliberately exclude 'off' — tighten-only.
|
|
381
|
+
commandChecks: import_zod.z.object({
|
|
382
|
+
inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
383
|
+
rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
384
|
+
chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
385
|
+
sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
386
|
+
evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
|
|
387
|
+
pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
|
|
388
|
+
}).optional(),
|
|
379
389
|
egress: import_zod.z.object({
|
|
380
390
|
enabled: import_zod.z.boolean().optional(),
|
|
381
391
|
mode: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
@@ -1513,6 +1523,45 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1513
1523
|
});
|
|
1514
1524
|
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1515
1525
|
}
|
|
1526
|
+
function resolveCheck(v) {
|
|
1527
|
+
return v === "off" || v === "block" ? v : "review";
|
|
1528
|
+
}
|
|
1529
|
+
function resolveCheckTight(v) {
|
|
1530
|
+
return v === "block" ? "block" : "review";
|
|
1531
|
+
}
|
|
1532
|
+
function detectInlineExec(command) {
|
|
1533
|
+
const pipeFed = command.includes("|");
|
|
1534
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
1535
|
+
for (const rawSeg of segments) {
|
|
1536
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
1537
|
+
let i = 0;
|
|
1538
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
1539
|
+
if (i >= tokens.length) continue;
|
|
1540
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
1541
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
1542
|
+
const args = tokens.slice(i + 1);
|
|
1543
|
+
let hadRedirect = false;
|
|
1544
|
+
const positionals = [];
|
|
1545
|
+
for (let j = 0; j < args.length; j++) {
|
|
1546
|
+
const a = args[j];
|
|
1547
|
+
if (a === "-") return true;
|
|
1548
|
+
if (a.startsWith("<")) {
|
|
1549
|
+
hadRedirect = true;
|
|
1550
|
+
if (a === "<" || a === "<<") j++;
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (a.startsWith("-")) {
|
|
1554
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
positionals.push(a);
|
|
1558
|
+
}
|
|
1559
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1516
1565
|
function resolvePinned(matches) {
|
|
1517
1566
|
if (matches.length === 0) return void 0;
|
|
1518
1567
|
const pinned = matches.filter((r) => r.pinned);
|
|
@@ -1539,7 +1588,7 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
1539
1588
|
const fieldName = toolInspection[matchingPattern];
|
|
1540
1589
|
return fieldName === "sql" || fieldName === "query";
|
|
1541
1590
|
}
|
|
1542
|
-
function pipeChainVerdict(command, isTrustedHost2) {
|
|
1591
|
+
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
1543
1592
|
const pipeAnalysis = analyzePipeChain(command);
|
|
1544
1593
|
if (!pipeAnalysis.isPipeline) return null;
|
|
1545
1594
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -1570,7 +1619,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
|
|
|
1570
1619
|
};
|
|
1571
1620
|
}
|
|
1572
1621
|
return {
|
|
1573
|
-
decision:
|
|
1622
|
+
decision: highAction,
|
|
1574
1623
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1575
1624
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1576
1625
|
tier: 3
|
|
@@ -1596,7 +1645,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1596
1645
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
1597
1646
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
1598
1647
|
if (bashCommand !== null) {
|
|
1599
|
-
const pipeVerdict = pipeChainVerdict(
|
|
1648
|
+
const pipeVerdict = pipeChainVerdict(
|
|
1649
|
+
bashCommand,
|
|
1650
|
+
isTrustedHost2,
|
|
1651
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
1652
|
+
);
|
|
1600
1653
|
if (pipeVerdict) return pipeVerdict;
|
|
1601
1654
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
1602
1655
|
if (fsVerdict) {
|
|
@@ -1611,10 +1664,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1611
1664
|
ruleDescription: fsVerdict.reason
|
|
1612
1665
|
};
|
|
1613
1666
|
}
|
|
1614
|
-
const
|
|
1667
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
1668
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
1615
1669
|
if (sqlVerdict) {
|
|
1616
1670
|
return {
|
|
1617
|
-
|
|
1671
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
1672
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
1618
1673
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
1619
1674
|
reason: sqlVerdict.reason,
|
|
1620
1675
|
tier: 2,
|
|
@@ -1622,10 +1677,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1622
1677
|
ruleDescription: sqlVerdict.description
|
|
1623
1678
|
};
|
|
1624
1679
|
}
|
|
1625
|
-
const
|
|
1680
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
1681
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
1626
1682
|
if (chmodVerdict) {
|
|
1627
1683
|
return {
|
|
1628
|
-
|
|
1684
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
1685
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
1629
1686
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
1630
1687
|
reason: chmodVerdict.reason,
|
|
1631
1688
|
tier: 2,
|
|
@@ -1668,10 +1725,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1668
1725
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
1669
1726
|
allTokens = analyzed.allTokens;
|
|
1670
1727
|
pathTokens = analyzed.paths;
|
|
1671
|
-
const
|
|
1672
|
-
if (
|
|
1728
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
1729
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
1673
1730
|
return {
|
|
1674
|
-
decision: "review",
|
|
1731
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
1675
1732
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
1676
1733
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
1677
1734
|
tier: 3
|
|
@@ -1689,14 +1746,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1689
1746
|
}
|
|
1690
1747
|
if (evalVerdict === "review") {
|
|
1691
1748
|
return {
|
|
1692
|
-
|
|
1749
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
1750
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
1751
|
+
// no knob at all).
|
|
1752
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
1693
1753
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
1694
1754
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
1695
1755
|
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.",
|
|
1696
1756
|
tier: 3
|
|
1697
1757
|
};
|
|
1698
1758
|
}
|
|
1699
|
-
const ptVerdict = pipeChainVerdict(
|
|
1759
|
+
const ptVerdict = pipeChainVerdict(
|
|
1760
|
+
shellCommand,
|
|
1761
|
+
isTrustedHost2,
|
|
1762
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
1763
|
+
);
|
|
1700
1764
|
if (ptVerdict) return ptVerdict;
|
|
1701
1765
|
if (config.policy.egress?.enabled) {
|
|
1702
1766
|
const dests = extractShellDestinations(shellCommand);
|
|
@@ -2378,7 +2442,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
2378
2442
|
}
|
|
2379
2443
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
2380
2444
|
}
|
|
2381
|
-
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;
|
|
2445
|
+
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, INLINE_INTERP, INLINE_SHELL, 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;
|
|
2382
2446
|
var init_dist = __esm({
|
|
2383
2447
|
"packages/policy-engine/dist/index.mjs"() {
|
|
2384
2448
|
"use strict";
|
|
@@ -3315,6 +3379,8 @@ var init_dist = __esm({
|
|
|
3315
3379
|
REGEX_CACHE_MAX = 500;
|
|
3316
3380
|
regexCache = /* @__PURE__ */ new Map();
|
|
3317
3381
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3382
|
+
INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
3383
|
+
INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
3318
3384
|
VERDICT_RANK = {
|
|
3319
3385
|
allow: 0,
|
|
3320
3386
|
review: 1,
|
|
@@ -4382,6 +4448,23 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
4382
4448
|
}
|
|
4383
4449
|
return next;
|
|
4384
4450
|
}
|
|
4451
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
4452
|
+
const next = { ...local };
|
|
4453
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
4454
|
+
const m = managed[key];
|
|
4455
|
+
if (typeof m !== "string") continue;
|
|
4456
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4457
|
+
const resolved = resolveByOrder(
|
|
4458
|
+
COMMAND_CHECK_ORDER,
|
|
4459
|
+
local[key] ?? "review",
|
|
4460
|
+
m,
|
|
4461
|
+
locked.includes(lockKey)
|
|
4462
|
+
);
|
|
4463
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
4464
|
+
next[key] = resolved;
|
|
4465
|
+
}
|
|
4466
|
+
return next;
|
|
4467
|
+
}
|
|
4385
4468
|
function applyManagedApprovers(local, managed) {
|
|
4386
4469
|
return {
|
|
4387
4470
|
...local,
|
|
@@ -4391,7 +4474,7 @@ function applyManagedApprovers(local, managed) {
|
|
|
4391
4474
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4392
4475
|
};
|
|
4393
4476
|
}
|
|
4394
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4477
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
|
|
4395
4478
|
var init_managed = __esm({
|
|
4396
4479
|
"src/config/managed.ts"() {
|
|
4397
4480
|
"use strict";
|
|
@@ -4399,6 +4482,15 @@ var init_managed = __esm({
|
|
|
4399
4482
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4400
4483
|
DLP_PII_ORDER = ["off", "block"];
|
|
4401
4484
|
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4485
|
+
COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
4486
|
+
COMMAND_CHECK_KEYS = [
|
|
4487
|
+
"inlineExec",
|
|
4488
|
+
"rmAdvisory",
|
|
4489
|
+
"chmod",
|
|
4490
|
+
"sqlDdl",
|
|
4491
|
+
"evalDynamic",
|
|
4492
|
+
"pipeChainHigh"
|
|
4493
|
+
];
|
|
4402
4494
|
}
|
|
4403
4495
|
});
|
|
4404
4496
|
|
|
@@ -4775,6 +4867,21 @@ function getConfig(cwd) {
|
|
|
4775
4867
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4776
4868
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4777
4869
|
}
|
|
4870
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4871
|
+
const src = p.commandChecks;
|
|
4872
|
+
const cc2 = {
|
|
4873
|
+
...mergedPolicy.commandChecks
|
|
4874
|
+
};
|
|
4875
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4876
|
+
const v = src[k];
|
|
4877
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4878
|
+
}
|
|
4879
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4880
|
+
const v = src[k];
|
|
4881
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4882
|
+
}
|
|
4883
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4884
|
+
}
|
|
4778
4885
|
if (p.egress) {
|
|
4779
4886
|
const e = p.egress;
|
|
4780
4887
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4876,6 +4983,13 @@ function getConfig(cwd) {
|
|
|
4876
4983
|
locked
|
|
4877
4984
|
);
|
|
4878
4985
|
}
|
|
4986
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4987
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4988
|
+
mergedPolicy.commandChecks ?? {},
|
|
4989
|
+
mc.commandChecks,
|
|
4990
|
+
locked
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4879
4993
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4880
4994
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4881
4995
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4985,8 +5099,17 @@ function getConfig(cwd) {
|
|
|
4985
5099
|
}
|
|
4986
5100
|
}
|
|
4987
5101
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
5102
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
5103
|
+
const advisoryKnob = (name) => {
|
|
5104
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
5105
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
5106
|
+
return void 0;
|
|
5107
|
+
};
|
|
4988
5108
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4989
|
-
if (
|
|
5109
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
5110
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
5111
|
+
if (knob === "off") continue;
|
|
5112
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4990
5113
|
}
|
|
4991
5114
|
const envMode = process.env.NODE9_MODE;
|
|
4992
5115
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
@@ -18378,6 +18501,18 @@ function extractManagedConfig(body) {
|
|
|
18378
18501
|
d.reviewAction = mc.dlp.reviewAction;
|
|
18379
18502
|
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18380
18503
|
}
|
|
18504
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
18505
|
+
const cc = {};
|
|
18506
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
18507
|
+
const v = mc.commandChecks[k];
|
|
18508
|
+
if (v === "off" || v === "review" || v === "block") cc[k] = v;
|
|
18509
|
+
}
|
|
18510
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
18511
|
+
const v = mc.commandChecks[k];
|
|
18512
|
+
if (v === "review" || v === "block") cc[k] = v;
|
|
18513
|
+
}
|
|
18514
|
+
if (Object.keys(cc).length > 0) out.commandChecks = cc;
|
|
18515
|
+
}
|
|
18381
18516
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18382
18517
|
const a = {};
|
|
18383
18518
|
for (const k of ["native", "browser", "cloud", "terminal"]) {
|
|
@@ -18440,7 +18575,7 @@ function extractManagedConfig(body) {
|
|
|
18440
18575
|
}
|
|
18441
18576
|
if (Object.keys(ap).length) out.appPermissions = ap;
|
|
18442
18577
|
}
|
|
18443
|
-
return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
|
|
18578
|
+
return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.commandChecks !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
|
|
18444
18579
|
}
|
|
18445
18580
|
function sweepStaleTmp(target) {
|
|
18446
18581
|
try {
|
|
@@ -48808,6 +48943,7 @@ init_audit();
|
|
|
48808
48943
|
init_config();
|
|
48809
48944
|
init_daemon();
|
|
48810
48945
|
init_dlp();
|
|
48946
|
+
init_hasher();
|
|
48811
48947
|
|
|
48812
48948
|
// src/utils/cp-mv-parser.ts
|
|
48813
48949
|
function parseCpMvOp(command) {
|
|
@@ -48869,6 +49005,20 @@ function atLeastConfidence(c, min) {
|
|
|
48869
49005
|
function sanitize3(value) {
|
|
48870
49006
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
48871
49007
|
}
|
|
49008
|
+
function buildArgsField(rawInput) {
|
|
49009
|
+
let hashed = {};
|
|
49010
|
+
try {
|
|
49011
|
+
hashed = { argsHash: hashArgs(rawInput) };
|
|
49012
|
+
} catch {
|
|
49013
|
+
}
|
|
49014
|
+
try {
|
|
49015
|
+
const hit = scanArgs(rawInput);
|
|
49016
|
+
if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
|
|
49017
|
+
return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
|
|
49018
|
+
} catch {
|
|
49019
|
+
return hashed;
|
|
49020
|
+
}
|
|
49021
|
+
}
|
|
48872
49022
|
function registerLogCommand(program2) {
|
|
48873
49023
|
program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
|
|
48874
49024
|
"--agent <name>",
|
|
@@ -48907,7 +49057,7 @@ function registerLogCommand(program2) {
|
|
|
48907
49057
|
const entry = {
|
|
48908
49058
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48909
49059
|
tool,
|
|
48910
|
-
|
|
49060
|
+
...buildArgsField(rawInput),
|
|
48911
49061
|
decision: "allowed",
|
|
48912
49062
|
source: reviewApproved ? "inline-review-approved" : "post-hook"
|
|
48913
49063
|
};
|
package/dist/cli.mjs
CHANGED
|
@@ -380,6 +380,16 @@ var init_config_schema = __esm({
|
|
|
380
380
|
pii: z.enum(["off", "block"]).optional(),
|
|
381
381
|
reviewAction: z.enum(["review", "block"]).optional()
|
|
382
382
|
}).optional(),
|
|
383
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
384
|
+
// deliberately exclude 'off' — tighten-only.
|
|
385
|
+
commandChecks: z.object({
|
|
386
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
387
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
388
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
389
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
390
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
391
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
392
|
+
}).optional(),
|
|
383
393
|
egress: z.object({
|
|
384
394
|
enabled: z.boolean().optional(),
|
|
385
395
|
mode: z.enum(["off", "review", "block"]).optional(),
|
|
@@ -1523,6 +1533,45 @@ function evaluateSmartConditions(args, rule) {
|
|
|
1523
1533
|
});
|
|
1524
1534
|
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1525
1535
|
}
|
|
1536
|
+
function resolveCheck(v) {
|
|
1537
|
+
return v === "off" || v === "block" ? v : "review";
|
|
1538
|
+
}
|
|
1539
|
+
function resolveCheckTight(v) {
|
|
1540
|
+
return v === "block" ? "block" : "review";
|
|
1541
|
+
}
|
|
1542
|
+
function detectInlineExec(command) {
|
|
1543
|
+
const pipeFed = command.includes("|");
|
|
1544
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
1545
|
+
for (const rawSeg of segments) {
|
|
1546
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
1547
|
+
let i = 0;
|
|
1548
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
1549
|
+
if (i >= tokens.length) continue;
|
|
1550
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
1551
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
1552
|
+
const args = tokens.slice(i + 1);
|
|
1553
|
+
let hadRedirect = false;
|
|
1554
|
+
const positionals = [];
|
|
1555
|
+
for (let j = 0; j < args.length; j++) {
|
|
1556
|
+
const a = args[j];
|
|
1557
|
+
if (a === "-") return true;
|
|
1558
|
+
if (a.startsWith("<")) {
|
|
1559
|
+
hadRedirect = true;
|
|
1560
|
+
if (a === "<" || a === "<<") j++;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (a.startsWith("-")) {
|
|
1564
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
positionals.push(a);
|
|
1568
|
+
}
|
|
1569
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1526
1575
|
function resolvePinned(matches) {
|
|
1527
1576
|
if (matches.length === 0) return void 0;
|
|
1528
1577
|
const pinned = matches.filter((r) => r.pinned);
|
|
@@ -1549,7 +1598,7 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
1549
1598
|
const fieldName = toolInspection[matchingPattern];
|
|
1550
1599
|
return fieldName === "sql" || fieldName === "query";
|
|
1551
1600
|
}
|
|
1552
|
-
function pipeChainVerdict(command, isTrustedHost2) {
|
|
1601
|
+
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
1553
1602
|
const pipeAnalysis = analyzePipeChain(command);
|
|
1554
1603
|
if (!pipeAnalysis.isPipeline) return null;
|
|
1555
1604
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -1580,7 +1629,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
|
|
|
1580
1629
|
};
|
|
1581
1630
|
}
|
|
1582
1631
|
return {
|
|
1583
|
-
decision:
|
|
1632
|
+
decision: highAction,
|
|
1584
1633
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
1585
1634
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
1586
1635
|
tier: 3
|
|
@@ -1606,7 +1655,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1606
1655
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
1607
1656
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
1608
1657
|
if (bashCommand !== null) {
|
|
1609
|
-
const pipeVerdict = pipeChainVerdict(
|
|
1658
|
+
const pipeVerdict = pipeChainVerdict(
|
|
1659
|
+
bashCommand,
|
|
1660
|
+
isTrustedHost2,
|
|
1661
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
1662
|
+
);
|
|
1610
1663
|
if (pipeVerdict) return pipeVerdict;
|
|
1611
1664
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
1612
1665
|
if (fsVerdict) {
|
|
@@ -1621,10 +1674,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1621
1674
|
ruleDescription: fsVerdict.reason
|
|
1622
1675
|
};
|
|
1623
1676
|
}
|
|
1624
|
-
const
|
|
1677
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
1678
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
1625
1679
|
if (sqlVerdict) {
|
|
1626
1680
|
return {
|
|
1627
|
-
|
|
1681
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
1682
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
1628
1683
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
1629
1684
|
reason: sqlVerdict.reason,
|
|
1630
1685
|
tier: 2,
|
|
@@ -1632,10 +1687,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1632
1687
|
ruleDescription: sqlVerdict.description
|
|
1633
1688
|
};
|
|
1634
1689
|
}
|
|
1635
|
-
const
|
|
1690
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
1691
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
1636
1692
|
if (chmodVerdict) {
|
|
1637
1693
|
return {
|
|
1638
|
-
|
|
1694
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
1695
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
1639
1696
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
1640
1697
|
reason: chmodVerdict.reason,
|
|
1641
1698
|
tier: 2,
|
|
@@ -1678,10 +1735,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1678
1735
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
1679
1736
|
allTokens = analyzed.allTokens;
|
|
1680
1737
|
pathTokens = analyzed.paths;
|
|
1681
|
-
const
|
|
1682
|
-
if (
|
|
1738
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
1739
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
1683
1740
|
return {
|
|
1684
|
-
decision: "review",
|
|
1741
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
1685
1742
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
1686
1743
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
1687
1744
|
tier: 3
|
|
@@ -1699,14 +1756,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1699
1756
|
}
|
|
1700
1757
|
if (evalVerdict === "review") {
|
|
1701
1758
|
return {
|
|
1702
|
-
|
|
1759
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
1760
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
1761
|
+
// no knob at all).
|
|
1762
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
1703
1763
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
1704
1764
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
1705
1765
|
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.",
|
|
1706
1766
|
tier: 3
|
|
1707
1767
|
};
|
|
1708
1768
|
}
|
|
1709
|
-
const ptVerdict = pipeChainVerdict(
|
|
1769
|
+
const ptVerdict = pipeChainVerdict(
|
|
1770
|
+
shellCommand,
|
|
1771
|
+
isTrustedHost2,
|
|
1772
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
1773
|
+
);
|
|
1710
1774
|
if (ptVerdict) return ptVerdict;
|
|
1711
1775
|
if (config.policy.egress?.enabled) {
|
|
1712
1776
|
const dests = extractShellDestinations(shellCommand);
|
|
@@ -2388,7 +2452,7 @@ function* stringValues(obj, depth = 0) {
|
|
|
2388
2452
|
}
|
|
2389
2453
|
for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
|
|
2390
2454
|
}
|
|
2391
|
-
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;
|
|
2455
|
+
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, INLINE_INTERP, INLINE_SHELL, 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;
|
|
2392
2456
|
var init_dist = __esm({
|
|
2393
2457
|
"packages/policy-engine/dist/index.mjs"() {
|
|
2394
2458
|
"use strict";
|
|
@@ -3319,6 +3383,8 @@ var init_dist = __esm({
|
|
|
3319
3383
|
REGEX_CACHE_MAX = 500;
|
|
3320
3384
|
regexCache = /* @__PURE__ */ new Map();
|
|
3321
3385
|
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3386
|
+
INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
3387
|
+
INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
3322
3388
|
VERDICT_RANK = {
|
|
3323
3389
|
allow: 0,
|
|
3324
3390
|
review: 1,
|
|
@@ -4386,6 +4452,23 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
4386
4452
|
}
|
|
4387
4453
|
return next;
|
|
4388
4454
|
}
|
|
4455
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
4456
|
+
const next = { ...local };
|
|
4457
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
4458
|
+
const m = managed[key];
|
|
4459
|
+
if (typeof m !== "string") continue;
|
|
4460
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4461
|
+
const resolved = resolveByOrder(
|
|
4462
|
+
COMMAND_CHECK_ORDER,
|
|
4463
|
+
local[key] ?? "review",
|
|
4464
|
+
m,
|
|
4465
|
+
locked.includes(lockKey)
|
|
4466
|
+
);
|
|
4467
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
4468
|
+
next[key] = resolved;
|
|
4469
|
+
}
|
|
4470
|
+
return next;
|
|
4471
|
+
}
|
|
4389
4472
|
function applyManagedApprovers(local, managed) {
|
|
4390
4473
|
return {
|
|
4391
4474
|
...local,
|
|
@@ -4395,7 +4478,7 @@ function applyManagedApprovers(local, managed) {
|
|
|
4395
4478
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4396
4479
|
};
|
|
4397
4480
|
}
|
|
4398
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4481
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
|
|
4399
4482
|
var init_managed = __esm({
|
|
4400
4483
|
"src/config/managed.ts"() {
|
|
4401
4484
|
"use strict";
|
|
@@ -4403,6 +4486,15 @@ var init_managed = __esm({
|
|
|
4403
4486
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4404
4487
|
DLP_PII_ORDER = ["off", "block"];
|
|
4405
4488
|
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4489
|
+
COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
4490
|
+
COMMAND_CHECK_KEYS = [
|
|
4491
|
+
"inlineExec",
|
|
4492
|
+
"rmAdvisory",
|
|
4493
|
+
"chmod",
|
|
4494
|
+
"sqlDdl",
|
|
4495
|
+
"evalDynamic",
|
|
4496
|
+
"pipeChainHigh"
|
|
4497
|
+
];
|
|
4406
4498
|
}
|
|
4407
4499
|
});
|
|
4408
4500
|
|
|
@@ -4782,6 +4874,21 @@ function getConfig(cwd) {
|
|
|
4782
4874
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4783
4875
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4784
4876
|
}
|
|
4877
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4878
|
+
const src = p.commandChecks;
|
|
4879
|
+
const cc2 = {
|
|
4880
|
+
...mergedPolicy.commandChecks
|
|
4881
|
+
};
|
|
4882
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4883
|
+
const v = src[k];
|
|
4884
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4885
|
+
}
|
|
4886
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4887
|
+
const v = src[k];
|
|
4888
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4889
|
+
}
|
|
4890
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4891
|
+
}
|
|
4785
4892
|
if (p.egress) {
|
|
4786
4893
|
const e = p.egress;
|
|
4787
4894
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4883,6 +4990,13 @@ function getConfig(cwd) {
|
|
|
4883
4990
|
locked
|
|
4884
4991
|
);
|
|
4885
4992
|
}
|
|
4993
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4994
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4995
|
+
mergedPolicy.commandChecks ?? {},
|
|
4996
|
+
mc.commandChecks,
|
|
4997
|
+
locked
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
4886
5000
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4887
5001
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4888
5002
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4992,8 +5106,17 @@ function getConfig(cwd) {
|
|
|
4992
5106
|
}
|
|
4993
5107
|
}
|
|
4994
5108
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
5109
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
5110
|
+
const advisoryKnob = (name) => {
|
|
5111
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
5112
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
5113
|
+
return void 0;
|
|
5114
|
+
};
|
|
4995
5115
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4996
|
-
if (
|
|
5116
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
5117
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
5118
|
+
if (knob === "off") continue;
|
|
5119
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4997
5120
|
}
|
|
4998
5121
|
const envMode = process.env.NODE9_MODE;
|
|
4999
5122
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
@@ -18376,6 +18499,18 @@ function extractManagedConfig(body) {
|
|
|
18376
18499
|
d.reviewAction = mc.dlp.reviewAction;
|
|
18377
18500
|
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18378
18501
|
}
|
|
18502
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
18503
|
+
const cc = {};
|
|
18504
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
18505
|
+
const v = mc.commandChecks[k];
|
|
18506
|
+
if (v === "off" || v === "review" || v === "block") cc[k] = v;
|
|
18507
|
+
}
|
|
18508
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
18509
|
+
const v = mc.commandChecks[k];
|
|
18510
|
+
if (v === "review" || v === "block") cc[k] = v;
|
|
18511
|
+
}
|
|
18512
|
+
if (Object.keys(cc).length > 0) out.commandChecks = cc;
|
|
18513
|
+
}
|
|
18379
18514
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18380
18515
|
const a = {};
|
|
18381
18516
|
for (const k of ["native", "browser", "cloud", "terminal"]) {
|
|
@@ -18438,7 +18573,7 @@ function extractManagedConfig(body) {
|
|
|
18438
18573
|
}
|
|
18439
18574
|
if (Object.keys(ap).length) out.appPermissions = ap;
|
|
18440
18575
|
}
|
|
18441
|
-
return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
|
|
18576
|
+
return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.commandChecks !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
|
|
18442
18577
|
}
|
|
18443
18578
|
function sweepStaleTmp(target) {
|
|
18444
18579
|
try {
|
|
@@ -48801,6 +48936,7 @@ import path48 from "path";
|
|
|
48801
48936
|
import os45 from "os";
|
|
48802
48937
|
init_daemon();
|
|
48803
48938
|
init_dlp();
|
|
48939
|
+
init_hasher();
|
|
48804
48940
|
|
|
48805
48941
|
// src/utils/cp-mv-parser.ts
|
|
48806
48942
|
function parseCpMvOp(command) {
|
|
@@ -48862,6 +48998,20 @@ function atLeastConfidence(c, min) {
|
|
|
48862
48998
|
function sanitize3(value) {
|
|
48863
48999
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
48864
49000
|
}
|
|
49001
|
+
function buildArgsField(rawInput) {
|
|
49002
|
+
let hashed = {};
|
|
49003
|
+
try {
|
|
49004
|
+
hashed = { argsHash: hashArgs(rawInput) };
|
|
49005
|
+
} catch {
|
|
49006
|
+
}
|
|
49007
|
+
try {
|
|
49008
|
+
const hit = scanArgs(rawInput);
|
|
49009
|
+
if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
|
|
49010
|
+
return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
|
|
49011
|
+
} catch {
|
|
49012
|
+
return hashed;
|
|
49013
|
+
}
|
|
49014
|
+
}
|
|
48865
49015
|
function registerLogCommand(program2) {
|
|
48866
49016
|
program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
|
|
48867
49017
|
"--agent <name>",
|
|
@@ -48900,7 +49050,7 @@ function registerLogCommand(program2) {
|
|
|
48900
49050
|
const entry = {
|
|
48901
49051
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48902
49052
|
tool,
|
|
48903
|
-
|
|
49053
|
+
...buildArgsField(rawInput),
|
|
48904
49054
|
decision: "allowed",
|
|
48905
49055
|
source: reviewApproved ? "inline-review-approved" : "post-hook"
|
|
48906
49056
|
};
|
package/dist/dashboard.mjs
CHANGED
|
@@ -2336,6 +2336,16 @@ var init_config_schema = __esm({
|
|
|
2336
2336
|
pii: z.enum(["off", "block"]).optional(),
|
|
2337
2337
|
reviewAction: z.enum(["review", "block"]).optional()
|
|
2338
2338
|
}).optional(),
|
|
2339
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
2340
|
+
// deliberately exclude 'off' — tighten-only.
|
|
2341
|
+
commandChecks: z.object({
|
|
2342
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
2343
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
2344
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
2345
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
2346
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
2347
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
2348
|
+
}).optional(),
|
|
2339
2349
|
egress: z.object({
|
|
2340
2350
|
enabled: z.boolean().optional(),
|
|
2341
2351
|
mode: z.enum(["off", "review", "block"]).optional(),
|
package/dist/index.js
CHANGED
|
@@ -358,6 +358,16 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
358
358
|
pii: import_zod.z.enum(["off", "block"]).optional(),
|
|
359
359
|
reviewAction: import_zod.z.enum(["review", "block"]).optional()
|
|
360
360
|
}).optional(),
|
|
361
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
362
|
+
// deliberately exclude 'off' — tighten-only.
|
|
363
|
+
commandChecks: import_zod.z.object({
|
|
364
|
+
inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
365
|
+
rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
366
|
+
chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
367
|
+
sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
368
|
+
evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
|
|
369
|
+
pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
|
|
370
|
+
}).optional(),
|
|
361
371
|
egress: import_zod.z.object({
|
|
362
372
|
enabled: import_zod.z.boolean().optional(),
|
|
363
373
|
mode: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
@@ -2368,6 +2378,47 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2368
2378
|
});
|
|
2369
2379
|
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2370
2380
|
}
|
|
2381
|
+
function resolveCheck(v) {
|
|
2382
|
+
return v === "off" || v === "block" ? v : "review";
|
|
2383
|
+
}
|
|
2384
|
+
function resolveCheckTight(v) {
|
|
2385
|
+
return v === "block" ? "block" : "review";
|
|
2386
|
+
}
|
|
2387
|
+
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2388
|
+
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2389
|
+
function detectInlineExec(command) {
|
|
2390
|
+
const pipeFed = command.includes("|");
|
|
2391
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2392
|
+
for (const rawSeg of segments) {
|
|
2393
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2394
|
+
let i = 0;
|
|
2395
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2396
|
+
if (i >= tokens.length) continue;
|
|
2397
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2398
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
2399
|
+
const args = tokens.slice(i + 1);
|
|
2400
|
+
let hadRedirect = false;
|
|
2401
|
+
const positionals = [];
|
|
2402
|
+
for (let j = 0; j < args.length; j++) {
|
|
2403
|
+
const a = args[j];
|
|
2404
|
+
if (a === "-") return true;
|
|
2405
|
+
if (a.startsWith("<")) {
|
|
2406
|
+
hadRedirect = true;
|
|
2407
|
+
if (a === "<" || a === "<<") j++;
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
if (a.startsWith("-")) {
|
|
2411
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
positionals.push(a);
|
|
2415
|
+
}
|
|
2416
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2371
2422
|
var VERDICT_RANK = {
|
|
2372
2423
|
allow: 0,
|
|
2373
2424
|
review: 1,
|
|
@@ -2400,7 +2451,7 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
2400
2451
|
return fieldName === "sql" || fieldName === "query";
|
|
2401
2452
|
}
|
|
2402
2453
|
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2403
|
-
function pipeChainVerdict(command, isTrustedHost2) {
|
|
2454
|
+
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
2404
2455
|
const pipeAnalysis = analyzePipeChain(command);
|
|
2405
2456
|
if (!pipeAnalysis.isPipeline) return null;
|
|
2406
2457
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -2431,7 +2482,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
|
|
|
2431
2482
|
};
|
|
2432
2483
|
}
|
|
2433
2484
|
return {
|
|
2434
|
-
decision:
|
|
2485
|
+
decision: highAction,
|
|
2435
2486
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2436
2487
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2437
2488
|
tier: 3
|
|
@@ -2457,7 +2508,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2457
2508
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2458
2509
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2459
2510
|
if (bashCommand !== null) {
|
|
2460
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2511
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2512
|
+
bashCommand,
|
|
2513
|
+
isTrustedHost2,
|
|
2514
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2515
|
+
);
|
|
2461
2516
|
if (pipeVerdict) return pipeVerdict;
|
|
2462
2517
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2463
2518
|
if (fsVerdict) {
|
|
@@ -2472,10 +2527,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2472
2527
|
ruleDescription: fsVerdict.reason
|
|
2473
2528
|
};
|
|
2474
2529
|
}
|
|
2475
|
-
const
|
|
2530
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2531
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2476
2532
|
if (sqlVerdict) {
|
|
2477
2533
|
return {
|
|
2478
|
-
|
|
2534
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2535
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2479
2536
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2480
2537
|
reason: sqlVerdict.reason,
|
|
2481
2538
|
tier: 2,
|
|
@@ -2483,10 +2540,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2483
2540
|
ruleDescription: sqlVerdict.description
|
|
2484
2541
|
};
|
|
2485
2542
|
}
|
|
2486
|
-
const
|
|
2543
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2544
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2487
2545
|
if (chmodVerdict) {
|
|
2488
2546
|
return {
|
|
2489
|
-
|
|
2547
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2548
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2490
2549
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2491
2550
|
reason: chmodVerdict.reason,
|
|
2492
2551
|
tier: 2,
|
|
@@ -2529,10 +2588,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2529
2588
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2530
2589
|
allTokens = analyzed.allTokens;
|
|
2531
2590
|
pathTokens = analyzed.paths;
|
|
2532
|
-
const
|
|
2533
|
-
if (
|
|
2591
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2592
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2534
2593
|
return {
|
|
2535
|
-
decision: "review",
|
|
2594
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2536
2595
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2537
2596
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2538
2597
|
tier: 3
|
|
@@ -2550,14 +2609,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2550
2609
|
}
|
|
2551
2610
|
if (evalVerdict === "review") {
|
|
2552
2611
|
return {
|
|
2553
|
-
|
|
2612
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2613
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
2614
|
+
// no knob at all).
|
|
2615
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2554
2616
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2555
2617
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2556
2618
|
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.",
|
|
2557
2619
|
tier: 3
|
|
2558
2620
|
};
|
|
2559
2621
|
}
|
|
2560
|
-
const ptVerdict = pipeChainVerdict(
|
|
2622
|
+
const ptVerdict = pipeChainVerdict(
|
|
2623
|
+
shellCommand,
|
|
2624
|
+
isTrustedHost2,
|
|
2625
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2626
|
+
);
|
|
2561
2627
|
if (ptVerdict) return ptVerdict;
|
|
2562
2628
|
if (config.policy.egress?.enabled) {
|
|
2563
2629
|
const dests = extractShellDestinations(shellCommand);
|
|
@@ -3668,6 +3734,32 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3668
3734
|
}
|
|
3669
3735
|
return next;
|
|
3670
3736
|
}
|
|
3737
|
+
var COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
3738
|
+
var COMMAND_CHECK_KEYS = [
|
|
3739
|
+
"inlineExec",
|
|
3740
|
+
"rmAdvisory",
|
|
3741
|
+
"chmod",
|
|
3742
|
+
"sqlDdl",
|
|
3743
|
+
"evalDynamic",
|
|
3744
|
+
"pipeChainHigh"
|
|
3745
|
+
];
|
|
3746
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
3747
|
+
const next = { ...local };
|
|
3748
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
3749
|
+
const m = managed[key];
|
|
3750
|
+
if (typeof m !== "string") continue;
|
|
3751
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3752
|
+
const resolved = resolveByOrder(
|
|
3753
|
+
COMMAND_CHECK_ORDER,
|
|
3754
|
+
local[key] ?? "review",
|
|
3755
|
+
m,
|
|
3756
|
+
locked.includes(lockKey)
|
|
3757
|
+
);
|
|
3758
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3759
|
+
next[key] = resolved;
|
|
3760
|
+
}
|
|
3761
|
+
return next;
|
|
3762
|
+
}
|
|
3671
3763
|
function applyManagedApprovers(local, managed) {
|
|
3672
3764
|
return {
|
|
3673
3765
|
...local,
|
|
@@ -4238,6 +4330,21 @@ function getConfig(cwd) {
|
|
|
4238
4330
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4239
4331
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4240
4332
|
}
|
|
4333
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4334
|
+
const src = p.commandChecks;
|
|
4335
|
+
const cc2 = {
|
|
4336
|
+
...mergedPolicy.commandChecks
|
|
4337
|
+
};
|
|
4338
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4339
|
+
const v = src[k];
|
|
4340
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4341
|
+
}
|
|
4342
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4343
|
+
const v = src[k];
|
|
4344
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4345
|
+
}
|
|
4346
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4347
|
+
}
|
|
4241
4348
|
if (p.egress) {
|
|
4242
4349
|
const e = p.egress;
|
|
4243
4350
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4339,6 +4446,13 @@ function getConfig(cwd) {
|
|
|
4339
4446
|
locked
|
|
4340
4447
|
);
|
|
4341
4448
|
}
|
|
4449
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4450
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4451
|
+
mergedPolicy.commandChecks ?? {},
|
|
4452
|
+
mc.commandChecks,
|
|
4453
|
+
locked
|
|
4454
|
+
);
|
|
4455
|
+
}
|
|
4342
4456
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4343
4457
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4344
4458
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4448,8 +4562,17 @@ function getConfig(cwd) {
|
|
|
4448
4562
|
}
|
|
4449
4563
|
}
|
|
4450
4564
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4565
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
4566
|
+
const advisoryKnob = (name) => {
|
|
4567
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
4568
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
4569
|
+
return void 0;
|
|
4570
|
+
};
|
|
4451
4571
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4452
|
-
if (
|
|
4572
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
4573
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
4574
|
+
if (knob === "off") continue;
|
|
4575
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4453
4576
|
}
|
|
4454
4577
|
const envMode = process.env.NODE9_MODE;
|
|
4455
4578
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
package/dist/index.mjs
CHANGED
|
@@ -328,6 +328,16 @@ var ConfigFileSchema = z.object({
|
|
|
328
328
|
pii: z.enum(["off", "block"]).optional(),
|
|
329
329
|
reviewAction: z.enum(["review", "block"]).optional()
|
|
330
330
|
}).optional(),
|
|
331
|
+
// Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
|
|
332
|
+
// deliberately exclude 'off' — tighten-only.
|
|
333
|
+
commandChecks: z.object({
|
|
334
|
+
inlineExec: z.enum(["off", "review", "block"]).optional(),
|
|
335
|
+
rmAdvisory: z.enum(["off", "review", "block"]).optional(),
|
|
336
|
+
chmod: z.enum(["off", "review", "block"]).optional(),
|
|
337
|
+
sqlDdl: z.enum(["off", "review", "block"]).optional(),
|
|
338
|
+
evalDynamic: z.enum(["review", "block"]).optional(),
|
|
339
|
+
pipeChainHigh: z.enum(["review", "block"]).optional()
|
|
340
|
+
}).optional(),
|
|
331
341
|
egress: z.object({
|
|
332
342
|
enabled: z.boolean().optional(),
|
|
333
343
|
mode: z.enum(["off", "review", "block"]).optional(),
|
|
@@ -2338,6 +2348,47 @@ function evaluateSmartConditions(args, rule) {
|
|
|
2338
2348
|
});
|
|
2339
2349
|
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2340
2350
|
}
|
|
2351
|
+
function resolveCheck(v) {
|
|
2352
|
+
return v === "off" || v === "block" ? v : "review";
|
|
2353
|
+
}
|
|
2354
|
+
function resolveCheckTight(v) {
|
|
2355
|
+
return v === "block" ? "block" : "review";
|
|
2356
|
+
}
|
|
2357
|
+
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2358
|
+
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2359
|
+
function detectInlineExec(command) {
|
|
2360
|
+
const pipeFed = command.includes("|");
|
|
2361
|
+
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2362
|
+
for (const rawSeg of segments) {
|
|
2363
|
+
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2364
|
+
let i = 0;
|
|
2365
|
+
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2366
|
+
if (i >= tokens.length) continue;
|
|
2367
|
+
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2368
|
+
if (!INLINE_INTERP.test(base)) continue;
|
|
2369
|
+
const args = tokens.slice(i + 1);
|
|
2370
|
+
let hadRedirect = false;
|
|
2371
|
+
const positionals = [];
|
|
2372
|
+
for (let j = 0; j < args.length; j++) {
|
|
2373
|
+
const a = args[j];
|
|
2374
|
+
if (a === "-") return true;
|
|
2375
|
+
if (a.startsWith("<")) {
|
|
2376
|
+
hadRedirect = true;
|
|
2377
|
+
if (a === "<" || a === "<<") j++;
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
if (a.startsWith("-")) {
|
|
2381
|
+
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
positionals.push(a);
|
|
2385
|
+
}
|
|
2386
|
+
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2341
2392
|
var VERDICT_RANK = {
|
|
2342
2393
|
allow: 0,
|
|
2343
2394
|
review: 1,
|
|
@@ -2370,7 +2421,7 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
2370
2421
|
return fieldName === "sql" || fieldName === "query";
|
|
2371
2422
|
}
|
|
2372
2423
|
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2373
|
-
function pipeChainVerdict(command, isTrustedHost2) {
|
|
2424
|
+
function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
|
|
2374
2425
|
const pipeAnalysis = analyzePipeChain(command);
|
|
2375
2426
|
if (!pipeAnalysis.isPipeline) return null;
|
|
2376
2427
|
if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
|
|
@@ -2401,7 +2452,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
|
|
|
2401
2452
|
};
|
|
2402
2453
|
}
|
|
2403
2454
|
return {
|
|
2404
|
-
decision:
|
|
2455
|
+
decision: highAction,
|
|
2405
2456
|
blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
|
|
2406
2457
|
reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
|
|
2407
2458
|
tier: 3
|
|
@@ -2427,7 +2478,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2427
2478
|
if (wouldBeIgnored) return { decision: "allow" };
|
|
2428
2479
|
const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
|
|
2429
2480
|
if (bashCommand !== null) {
|
|
2430
|
-
const pipeVerdict = pipeChainVerdict(
|
|
2481
|
+
const pipeVerdict = pipeChainVerdict(
|
|
2482
|
+
bashCommand,
|
|
2483
|
+
isTrustedHost2,
|
|
2484
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2485
|
+
);
|
|
2431
2486
|
if (pipeVerdict) return pipeVerdict;
|
|
2432
2487
|
const fsVerdict = analyzeFsOperation(bashCommand);
|
|
2433
2488
|
if (fsVerdict) {
|
|
@@ -2442,10 +2497,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2442
2497
|
ruleDescription: fsVerdict.reason
|
|
2443
2498
|
};
|
|
2444
2499
|
}
|
|
2445
|
-
const
|
|
2500
|
+
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
2501
|
+
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
2446
2502
|
if (sqlVerdict) {
|
|
2447
2503
|
return {
|
|
2448
|
-
|
|
2504
|
+
// analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
|
|
2505
|
+
decision: sqlAction === "block" ? "block" : "review",
|
|
2449
2506
|
blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
|
|
2450
2507
|
reason: sqlVerdict.reason,
|
|
2451
2508
|
tier: 2,
|
|
@@ -2453,10 +2510,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2453
2510
|
ruleDescription: sqlVerdict.description
|
|
2454
2511
|
};
|
|
2455
2512
|
}
|
|
2456
|
-
const
|
|
2513
|
+
const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
|
|
2514
|
+
const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
|
|
2457
2515
|
if (chmodVerdict) {
|
|
2458
2516
|
return {
|
|
2459
|
-
|
|
2517
|
+
// analyzeChmod777 is typed review-only, so the knob maps 1:1.
|
|
2518
|
+
decision: chmodAction === "block" ? "block" : "review",
|
|
2460
2519
|
blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
|
|
2461
2520
|
reason: chmodVerdict.reason,
|
|
2462
2521
|
tier: 2,
|
|
@@ -2499,10 +2558,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2499
2558
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2500
2559
|
allTokens = analyzed.allTokens;
|
|
2501
2560
|
pathTokens = analyzed.paths;
|
|
2502
|
-
const
|
|
2503
|
-
if (
|
|
2561
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2562
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2504
2563
|
return {
|
|
2505
|
-
decision: "review",
|
|
2564
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2506
2565
|
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2507
2566
|
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2508
2567
|
tier: 3
|
|
@@ -2520,14 +2579,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2520
2579
|
}
|
|
2521
2580
|
if (evalVerdict === "review") {
|
|
2522
2581
|
return {
|
|
2523
|
-
|
|
2582
|
+
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2583
|
+
// block but can never turn this off (eval-remote above is Class A —
|
|
2584
|
+
// no knob at all).
|
|
2585
|
+
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2524
2586
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2525
2587
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2526
2588
|
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.",
|
|
2527
2589
|
tier: 3
|
|
2528
2590
|
};
|
|
2529
2591
|
}
|
|
2530
|
-
const ptVerdict = pipeChainVerdict(
|
|
2592
|
+
const ptVerdict = pipeChainVerdict(
|
|
2593
|
+
shellCommand,
|
|
2594
|
+
isTrustedHost2,
|
|
2595
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2596
|
+
);
|
|
2531
2597
|
if (ptVerdict) return ptVerdict;
|
|
2532
2598
|
if (config.policy.egress?.enabled) {
|
|
2533
2599
|
const dests = extractShellDestinations(shellCommand);
|
|
@@ -3638,6 +3704,32 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3638
3704
|
}
|
|
3639
3705
|
return next;
|
|
3640
3706
|
}
|
|
3707
|
+
var COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
3708
|
+
var COMMAND_CHECK_KEYS = [
|
|
3709
|
+
"inlineExec",
|
|
3710
|
+
"rmAdvisory",
|
|
3711
|
+
"chmod",
|
|
3712
|
+
"sqlDdl",
|
|
3713
|
+
"evalDynamic",
|
|
3714
|
+
"pipeChainHigh"
|
|
3715
|
+
];
|
|
3716
|
+
function applyManagedCommandChecks(local, managed, locked) {
|
|
3717
|
+
const next = { ...local };
|
|
3718
|
+
for (const key of COMMAND_CHECK_KEYS) {
|
|
3719
|
+
const m = managed[key];
|
|
3720
|
+
if (typeof m !== "string") continue;
|
|
3721
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3722
|
+
const resolved = resolveByOrder(
|
|
3723
|
+
COMMAND_CHECK_ORDER,
|
|
3724
|
+
local[key] ?? "review",
|
|
3725
|
+
m,
|
|
3726
|
+
locked.includes(lockKey)
|
|
3727
|
+
);
|
|
3728
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3729
|
+
next[key] = resolved;
|
|
3730
|
+
}
|
|
3731
|
+
return next;
|
|
3732
|
+
}
|
|
3641
3733
|
function applyManagedApprovers(local, managed) {
|
|
3642
3734
|
return {
|
|
3643
3735
|
...local,
|
|
@@ -4208,6 +4300,21 @@ function getConfig(cwd) {
|
|
|
4208
4300
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4209
4301
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4210
4302
|
}
|
|
4303
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4304
|
+
const src = p.commandChecks;
|
|
4305
|
+
const cc2 = {
|
|
4306
|
+
...mergedPolicy.commandChecks
|
|
4307
|
+
};
|
|
4308
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4309
|
+
const v = src[k];
|
|
4310
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4311
|
+
}
|
|
4312
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4313
|
+
const v = src[k];
|
|
4314
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4315
|
+
}
|
|
4316
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4317
|
+
}
|
|
4211
4318
|
if (p.egress) {
|
|
4212
4319
|
const e = p.egress;
|
|
4213
4320
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4309,6 +4416,13 @@ function getConfig(cwd) {
|
|
|
4309
4416
|
locked
|
|
4310
4417
|
);
|
|
4311
4418
|
}
|
|
4419
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4420
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4421
|
+
mergedPolicy.commandChecks ?? {},
|
|
4422
|
+
mc.commandChecks,
|
|
4423
|
+
locked
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
4312
4426
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4313
4427
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4314
4428
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4418,8 +4532,17 @@ function getConfig(cwd) {
|
|
|
4418
4532
|
}
|
|
4419
4533
|
}
|
|
4420
4534
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4535
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
4536
|
+
const advisoryKnob = (name) => {
|
|
4537
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
4538
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
4539
|
+
return void 0;
|
|
4540
|
+
};
|
|
4421
4541
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4422
|
-
if (
|
|
4542
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
4543
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
4544
|
+
if (knob === "off") continue;
|
|
4545
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4423
4546
|
}
|
|
4424
4547
|
const envMode = process.env.NODE9_MODE;
|
|
4425
4548
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.67.0",
|
|
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",
|