@node9/proxy 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/cli.js +168 -17
- package/dist/cli.mjs +168 -17
- package/dist/dashboard.mjs +10 -0
- package/dist/index.js +137 -13
- package/dist/index.mjs +137 -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,24 @@ 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 localValue = local[key];
|
|
4458
|
+
const resolved = localValue === void 0 ? m : resolveByOrder(
|
|
4459
|
+
COMMAND_CHECK_ORDER,
|
|
4460
|
+
localValue,
|
|
4461
|
+
m,
|
|
4462
|
+
locked.includes(lockKey)
|
|
4463
|
+
);
|
|
4464
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
4465
|
+
next[key] = resolved;
|
|
4466
|
+
}
|
|
4467
|
+
return next;
|
|
4468
|
+
}
|
|
4385
4469
|
function applyManagedApprovers(local, managed) {
|
|
4386
4470
|
return {
|
|
4387
4471
|
...local,
|
|
@@ -4391,7 +4475,7 @@ function applyManagedApprovers(local, managed) {
|
|
|
4391
4475
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4392
4476
|
};
|
|
4393
4477
|
}
|
|
4394
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4478
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
|
|
4395
4479
|
var init_managed = __esm({
|
|
4396
4480
|
"src/config/managed.ts"() {
|
|
4397
4481
|
"use strict";
|
|
@@ -4399,6 +4483,15 @@ var init_managed = __esm({
|
|
|
4399
4483
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4400
4484
|
DLP_PII_ORDER = ["off", "block"];
|
|
4401
4485
|
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4486
|
+
COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
4487
|
+
COMMAND_CHECK_KEYS = [
|
|
4488
|
+
"inlineExec",
|
|
4489
|
+
"rmAdvisory",
|
|
4490
|
+
"chmod",
|
|
4491
|
+
"sqlDdl",
|
|
4492
|
+
"evalDynamic",
|
|
4493
|
+
"pipeChainHigh"
|
|
4494
|
+
];
|
|
4402
4495
|
}
|
|
4403
4496
|
});
|
|
4404
4497
|
|
|
@@ -4775,6 +4868,21 @@ function getConfig(cwd) {
|
|
|
4775
4868
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4776
4869
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4777
4870
|
}
|
|
4871
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4872
|
+
const src = p.commandChecks;
|
|
4873
|
+
const cc2 = {
|
|
4874
|
+
...mergedPolicy.commandChecks
|
|
4875
|
+
};
|
|
4876
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4877
|
+
const v = src[k];
|
|
4878
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4879
|
+
}
|
|
4880
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4881
|
+
const v = src[k];
|
|
4882
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4883
|
+
}
|
|
4884
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4885
|
+
}
|
|
4778
4886
|
if (p.egress) {
|
|
4779
4887
|
const e = p.egress;
|
|
4780
4888
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4876,6 +4984,13 @@ function getConfig(cwd) {
|
|
|
4876
4984
|
locked
|
|
4877
4985
|
);
|
|
4878
4986
|
}
|
|
4987
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4988
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4989
|
+
mergedPolicy.commandChecks ?? {},
|
|
4990
|
+
mc.commandChecks,
|
|
4991
|
+
locked
|
|
4992
|
+
);
|
|
4993
|
+
}
|
|
4879
4994
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4880
4995
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4881
4996
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4985,8 +5100,17 @@ function getConfig(cwd) {
|
|
|
4985
5100
|
}
|
|
4986
5101
|
}
|
|
4987
5102
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
5103
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
5104
|
+
const advisoryKnob = (name) => {
|
|
5105
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
5106
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
5107
|
+
return void 0;
|
|
5108
|
+
};
|
|
4988
5109
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4989
|
-
if (
|
|
5110
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
5111
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
5112
|
+
if (knob === "off") continue;
|
|
5113
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4990
5114
|
}
|
|
4991
5115
|
const envMode = process.env.NODE9_MODE;
|
|
4992
5116
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
@@ -18378,6 +18502,18 @@ function extractManagedConfig(body) {
|
|
|
18378
18502
|
d.reviewAction = mc.dlp.reviewAction;
|
|
18379
18503
|
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18380
18504
|
}
|
|
18505
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
18506
|
+
const cc = {};
|
|
18507
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
18508
|
+
const v = mc.commandChecks[k];
|
|
18509
|
+
if (v === "off" || v === "review" || v === "block") cc[k] = v;
|
|
18510
|
+
}
|
|
18511
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
18512
|
+
const v = mc.commandChecks[k];
|
|
18513
|
+
if (v === "review" || v === "block") cc[k] = v;
|
|
18514
|
+
}
|
|
18515
|
+
if (Object.keys(cc).length > 0) out.commandChecks = cc;
|
|
18516
|
+
}
|
|
18381
18517
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18382
18518
|
const a = {};
|
|
18383
18519
|
for (const k of ["native", "browser", "cloud", "terminal"]) {
|
|
@@ -18440,7 +18576,7 @@ function extractManagedConfig(body) {
|
|
|
18440
18576
|
}
|
|
18441
18577
|
if (Object.keys(ap).length) out.appPermissions = ap;
|
|
18442
18578
|
}
|
|
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;
|
|
18579
|
+
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
18580
|
}
|
|
18445
18581
|
function sweepStaleTmp(target) {
|
|
18446
18582
|
try {
|
|
@@ -48808,6 +48944,7 @@ init_audit();
|
|
|
48808
48944
|
init_config();
|
|
48809
48945
|
init_daemon();
|
|
48810
48946
|
init_dlp();
|
|
48947
|
+
init_hasher();
|
|
48811
48948
|
|
|
48812
48949
|
// src/utils/cp-mv-parser.ts
|
|
48813
48950
|
function parseCpMvOp(command) {
|
|
@@ -48869,6 +49006,20 @@ function atLeastConfidence(c, min) {
|
|
|
48869
49006
|
function sanitize3(value) {
|
|
48870
49007
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
48871
49008
|
}
|
|
49009
|
+
function buildArgsField(rawInput) {
|
|
49010
|
+
let hashed = {};
|
|
49011
|
+
try {
|
|
49012
|
+
hashed = { argsHash: hashArgs(rawInput) };
|
|
49013
|
+
} catch {
|
|
49014
|
+
}
|
|
49015
|
+
try {
|
|
49016
|
+
const hit = scanArgs(rawInput);
|
|
49017
|
+
if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
|
|
49018
|
+
return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
|
|
49019
|
+
} catch {
|
|
49020
|
+
return hashed;
|
|
49021
|
+
}
|
|
49022
|
+
}
|
|
48872
49023
|
function registerLogCommand(program2) {
|
|
48873
49024
|
program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
|
|
48874
49025
|
"--agent <name>",
|
|
@@ -48907,7 +49058,7 @@ function registerLogCommand(program2) {
|
|
|
48907
49058
|
const entry = {
|
|
48908
49059
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48909
49060
|
tool,
|
|
48910
|
-
|
|
49061
|
+
...buildArgsField(rawInput),
|
|
48911
49062
|
decision: "allowed",
|
|
48912
49063
|
source: reviewApproved ? "inline-review-approved" : "post-hook"
|
|
48913
49064
|
};
|
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,24 @@ 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 localValue = local[key];
|
|
4462
|
+
const resolved = localValue === void 0 ? m : resolveByOrder(
|
|
4463
|
+
COMMAND_CHECK_ORDER,
|
|
4464
|
+
localValue,
|
|
4465
|
+
m,
|
|
4466
|
+
locked.includes(lockKey)
|
|
4467
|
+
);
|
|
4468
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
4469
|
+
next[key] = resolved;
|
|
4470
|
+
}
|
|
4471
|
+
return next;
|
|
4472
|
+
}
|
|
4389
4473
|
function applyManagedApprovers(local, managed) {
|
|
4390
4474
|
return {
|
|
4391
4475
|
...local,
|
|
@@ -4395,7 +4479,7 @@ function applyManagedApprovers(local, managed) {
|
|
|
4395
4479
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4396
4480
|
};
|
|
4397
4481
|
}
|
|
4398
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4482
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
|
|
4399
4483
|
var init_managed = __esm({
|
|
4400
4484
|
"src/config/managed.ts"() {
|
|
4401
4485
|
"use strict";
|
|
@@ -4403,6 +4487,15 @@ var init_managed = __esm({
|
|
|
4403
4487
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4404
4488
|
DLP_PII_ORDER = ["off", "block"];
|
|
4405
4489
|
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4490
|
+
COMMAND_CHECK_ORDER = ["off", "review", "block"];
|
|
4491
|
+
COMMAND_CHECK_KEYS = [
|
|
4492
|
+
"inlineExec",
|
|
4493
|
+
"rmAdvisory",
|
|
4494
|
+
"chmod",
|
|
4495
|
+
"sqlDdl",
|
|
4496
|
+
"evalDynamic",
|
|
4497
|
+
"pipeChainHigh"
|
|
4498
|
+
];
|
|
4406
4499
|
}
|
|
4407
4500
|
});
|
|
4408
4501
|
|
|
@@ -4782,6 +4875,21 @@ function getConfig(cwd) {
|
|
|
4782
4875
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4783
4876
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4784
4877
|
}
|
|
4878
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4879
|
+
const src = p.commandChecks;
|
|
4880
|
+
const cc2 = {
|
|
4881
|
+
...mergedPolicy.commandChecks
|
|
4882
|
+
};
|
|
4883
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4884
|
+
const v = src[k];
|
|
4885
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4886
|
+
}
|
|
4887
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4888
|
+
const v = src[k];
|
|
4889
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4890
|
+
}
|
|
4891
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4892
|
+
}
|
|
4785
4893
|
if (p.egress) {
|
|
4786
4894
|
const e = p.egress;
|
|
4787
4895
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4883,6 +4991,13 @@ function getConfig(cwd) {
|
|
|
4883
4991
|
locked
|
|
4884
4992
|
);
|
|
4885
4993
|
}
|
|
4994
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4995
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4996
|
+
mergedPolicy.commandChecks ?? {},
|
|
4997
|
+
mc.commandChecks,
|
|
4998
|
+
locked
|
|
4999
|
+
);
|
|
5000
|
+
}
|
|
4886
5001
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4887
5002
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4888
5003
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4992,8 +5107,17 @@ function getConfig(cwd) {
|
|
|
4992
5107
|
}
|
|
4993
5108
|
}
|
|
4994
5109
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
5110
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
5111
|
+
const advisoryKnob = (name) => {
|
|
5112
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
5113
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
5114
|
+
return void 0;
|
|
5115
|
+
};
|
|
4995
5116
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4996
|
-
if (
|
|
5117
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
5118
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
5119
|
+
if (knob === "off") continue;
|
|
5120
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4997
5121
|
}
|
|
4998
5122
|
const envMode = process.env.NODE9_MODE;
|
|
4999
5123
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
@@ -18376,6 +18500,18 @@ function extractManagedConfig(body) {
|
|
|
18376
18500
|
d.reviewAction = mc.dlp.reviewAction;
|
|
18377
18501
|
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18378
18502
|
}
|
|
18503
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
18504
|
+
const cc = {};
|
|
18505
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
18506
|
+
const v = mc.commandChecks[k];
|
|
18507
|
+
if (v === "off" || v === "review" || v === "block") cc[k] = v;
|
|
18508
|
+
}
|
|
18509
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
18510
|
+
const v = mc.commandChecks[k];
|
|
18511
|
+
if (v === "review" || v === "block") cc[k] = v;
|
|
18512
|
+
}
|
|
18513
|
+
if (Object.keys(cc).length > 0) out.commandChecks = cc;
|
|
18514
|
+
}
|
|
18379
18515
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18380
18516
|
const a = {};
|
|
18381
18517
|
for (const k of ["native", "browser", "cloud", "terminal"]) {
|
|
@@ -18438,7 +18574,7 @@ function extractManagedConfig(body) {
|
|
|
18438
18574
|
}
|
|
18439
18575
|
if (Object.keys(ap).length) out.appPermissions = ap;
|
|
18440
18576
|
}
|
|
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;
|
|
18577
|
+
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
18578
|
}
|
|
18443
18579
|
function sweepStaleTmp(target) {
|
|
18444
18580
|
try {
|
|
@@ -48801,6 +48937,7 @@ import path48 from "path";
|
|
|
48801
48937
|
import os45 from "os";
|
|
48802
48938
|
init_daemon();
|
|
48803
48939
|
init_dlp();
|
|
48940
|
+
init_hasher();
|
|
48804
48941
|
|
|
48805
48942
|
// src/utils/cp-mv-parser.ts
|
|
48806
48943
|
function parseCpMvOp(command) {
|
|
@@ -48862,6 +48999,20 @@ function atLeastConfidence(c, min) {
|
|
|
48862
48999
|
function sanitize3(value) {
|
|
48863
49000
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
48864
49001
|
}
|
|
49002
|
+
function buildArgsField(rawInput) {
|
|
49003
|
+
let hashed = {};
|
|
49004
|
+
try {
|
|
49005
|
+
hashed = { argsHash: hashArgs(rawInput) };
|
|
49006
|
+
} catch {
|
|
49007
|
+
}
|
|
49008
|
+
try {
|
|
49009
|
+
const hit = scanArgs(rawInput);
|
|
49010
|
+
if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
|
|
49011
|
+
return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
|
|
49012
|
+
} catch {
|
|
49013
|
+
return hashed;
|
|
49014
|
+
}
|
|
49015
|
+
}
|
|
48865
49016
|
function registerLogCommand(program2) {
|
|
48866
49017
|
program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
|
|
48867
49018
|
"--agent <name>",
|
|
@@ -48900,7 +49051,7 @@ function registerLogCommand(program2) {
|
|
|
48900
49051
|
const entry = {
|
|
48901
49052
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48902
49053
|
tool,
|
|
48903
|
-
|
|
49054
|
+
...buildArgsField(rawInput),
|
|
48904
49055
|
decision: "allowed",
|
|
48905
49056
|
source: reviewApproved ? "inline-review-approved" : "post-hook"
|
|
48906
49057
|
};
|
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,33 @@ 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 localValue = local[key];
|
|
3753
|
+
const resolved = localValue === void 0 ? m : resolveByOrder(
|
|
3754
|
+
COMMAND_CHECK_ORDER,
|
|
3755
|
+
localValue,
|
|
3756
|
+
m,
|
|
3757
|
+
locked.includes(lockKey)
|
|
3758
|
+
);
|
|
3759
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3760
|
+
next[key] = resolved;
|
|
3761
|
+
}
|
|
3762
|
+
return next;
|
|
3763
|
+
}
|
|
3671
3764
|
function applyManagedApprovers(local, managed) {
|
|
3672
3765
|
return {
|
|
3673
3766
|
...local,
|
|
@@ -4238,6 +4331,21 @@ function getConfig(cwd) {
|
|
|
4238
4331
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4239
4332
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4240
4333
|
}
|
|
4334
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4335
|
+
const src = p.commandChecks;
|
|
4336
|
+
const cc2 = {
|
|
4337
|
+
...mergedPolicy.commandChecks
|
|
4338
|
+
};
|
|
4339
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4340
|
+
const v = src[k];
|
|
4341
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4342
|
+
}
|
|
4343
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4344
|
+
const v = src[k];
|
|
4345
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4346
|
+
}
|
|
4347
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4348
|
+
}
|
|
4241
4349
|
if (p.egress) {
|
|
4242
4350
|
const e = p.egress;
|
|
4243
4351
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4339,6 +4447,13 @@ function getConfig(cwd) {
|
|
|
4339
4447
|
locked
|
|
4340
4448
|
);
|
|
4341
4449
|
}
|
|
4450
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4451
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4452
|
+
mergedPolicy.commandChecks ?? {},
|
|
4453
|
+
mc.commandChecks,
|
|
4454
|
+
locked
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4342
4457
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4343
4458
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4344
4459
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4448,8 +4563,17 @@ function getConfig(cwd) {
|
|
|
4448
4563
|
}
|
|
4449
4564
|
}
|
|
4450
4565
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4566
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
4567
|
+
const advisoryKnob = (name) => {
|
|
4568
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
4569
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
4570
|
+
return void 0;
|
|
4571
|
+
};
|
|
4451
4572
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4452
|
-
if (
|
|
4573
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
4574
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
4575
|
+
if (knob === "off") continue;
|
|
4576
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4453
4577
|
}
|
|
4454
4578
|
const envMode = process.env.NODE9_MODE;
|
|
4455
4579
|
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,33 @@ 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 localValue = local[key];
|
|
3723
|
+
const resolved = localValue === void 0 ? m : resolveByOrder(
|
|
3724
|
+
COMMAND_CHECK_ORDER,
|
|
3725
|
+
localValue,
|
|
3726
|
+
m,
|
|
3727
|
+
locked.includes(lockKey)
|
|
3728
|
+
);
|
|
3729
|
+
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3730
|
+
next[key] = resolved;
|
|
3731
|
+
}
|
|
3732
|
+
return next;
|
|
3733
|
+
}
|
|
3641
3734
|
function applyManagedApprovers(local, managed) {
|
|
3642
3735
|
return {
|
|
3643
3736
|
...local,
|
|
@@ -4208,6 +4301,21 @@ function getConfig(cwd) {
|
|
|
4208
4301
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4209
4302
|
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4210
4303
|
}
|
|
4304
|
+
if (p.commandChecks && typeof p.commandChecks === "object") {
|
|
4305
|
+
const src = p.commandChecks;
|
|
4306
|
+
const cc2 = {
|
|
4307
|
+
...mergedPolicy.commandChecks
|
|
4308
|
+
};
|
|
4309
|
+
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4310
|
+
const v = src[k];
|
|
4311
|
+
if (v === "off" || v === "review" || v === "block") cc2[k] = v;
|
|
4312
|
+
}
|
|
4313
|
+
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4314
|
+
const v = src[k];
|
|
4315
|
+
if (v === "review" || v === "block") cc2[k] = v;
|
|
4316
|
+
}
|
|
4317
|
+
if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
|
|
4318
|
+
}
|
|
4211
4319
|
if (p.egress) {
|
|
4212
4320
|
const e = p.egress;
|
|
4213
4321
|
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
@@ -4309,6 +4417,13 @@ function getConfig(cwd) {
|
|
|
4309
4417
|
locked
|
|
4310
4418
|
);
|
|
4311
4419
|
}
|
|
4420
|
+
if (mc.commandChecks && typeof mc.commandChecks === "object") {
|
|
4421
|
+
mergedPolicy.commandChecks = applyManagedCommandChecks(
|
|
4422
|
+
mergedPolicy.commandChecks ?? {},
|
|
4423
|
+
mc.commandChecks,
|
|
4424
|
+
locked
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4312
4427
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4313
4428
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
4314
4429
|
mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
|
|
@@ -4418,8 +4533,17 @@ function getConfig(cwd) {
|
|
|
4418
4533
|
}
|
|
4419
4534
|
}
|
|
4420
4535
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4536
|
+
const cc = mergedPolicy.commandChecks ?? {};
|
|
4537
|
+
const advisoryKnob = (name) => {
|
|
4538
|
+
if (name === "review-rm") return cc.rmAdvisory;
|
|
4539
|
+
if (name?.endsWith("-sql")) return cc.sqlDdl;
|
|
4540
|
+
return void 0;
|
|
4541
|
+
};
|
|
4421
4542
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4422
|
-
if (
|
|
4543
|
+
if (existingAdvisoryNames.has(rule.name)) continue;
|
|
4544
|
+
const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
|
|
4545
|
+
if (knob === "off") continue;
|
|
4546
|
+
mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
|
|
4423
4547
|
}
|
|
4424
4548
|
const envMode = process.env.NODE9_MODE;
|
|
4425
4549
|
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.1",
|
|
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",
|