@node9/proxy 1.67.0 → 1.67.2
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 +1718 -1328
- package/dist/cli.mjs +1685 -1295
- package/dist/dashboard.mjs +188 -128
- package/dist/index.js +618 -233
- package/dist/index.mjs +618 -233
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -380,8 +380,8 @@ function sanitizeConfig(raw) {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
const lines = result.error.issues.map((issue) => {
|
|
383
|
-
const
|
|
384
|
-
return ` \u2022 ${
|
|
383
|
+
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
384
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
385
385
|
});
|
|
386
386
|
return {
|
|
387
387
|
sanitized,
|
|
@@ -1001,6 +1001,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
return null;
|
|
1003
1003
|
}
|
|
1004
|
+
var MAX_REGEX_LENGTH = 256;
|
|
1005
|
+
var REGEX_CACHE_MAX = 500;
|
|
1006
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
1007
|
+
function validateRegex(pattern) {
|
|
1008
|
+
if (!pattern) return "Pattern is required";
|
|
1009
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1010
|
+
try {
|
|
1011
|
+
new RegExp(pattern);
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
1014
|
+
}
|
|
1015
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1016
|
+
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
1020
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1021
|
+
const key = `${pattern}\0${flags}`;
|
|
1022
|
+
if (regexCache.has(key)) {
|
|
1023
|
+
const cached = regexCache.get(key);
|
|
1024
|
+
regexCache.delete(key);
|
|
1025
|
+
regexCache.set(key, cached);
|
|
1026
|
+
return cached;
|
|
1027
|
+
}
|
|
1028
|
+
if (validateRegex(pattern) !== null) return null;
|
|
1029
|
+
try {
|
|
1030
|
+
const re = new RegExp(pattern, flags);
|
|
1031
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1032
|
+
const oldest = regexCache.keys().next().value;
|
|
1033
|
+
if (oldest) regexCache.delete(oldest);
|
|
1034
|
+
}
|
|
1035
|
+
regexCache.set(key, re);
|
|
1036
|
+
return re;
|
|
1037
|
+
} catch {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function matchesPattern(text, patterns) {
|
|
1042
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
1043
|
+
if (p.length === 0) return false;
|
|
1044
|
+
const isMatch = pm(p, { nocase: true, dot: true });
|
|
1045
|
+
const target = text.toLowerCase();
|
|
1046
|
+
const directMatch = isMatch(target);
|
|
1047
|
+
if (directMatch) return true;
|
|
1048
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1049
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1050
|
+
}
|
|
1051
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1052
|
+
function getNestedValue(obj, path14) {
|
|
1053
|
+
if (!obj || typeof obj !== "object") return null;
|
|
1054
|
+
const segments = path14.split(".");
|
|
1055
|
+
for (const seg of segments) {
|
|
1056
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1057
|
+
}
|
|
1058
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
1059
|
+
}
|
|
1060
|
+
function evaluateSmartConditions(args, rule) {
|
|
1061
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
1062
|
+
const mode = rule.conditionMode ?? "all";
|
|
1063
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
1064
|
+
const resolveField = (field) => {
|
|
1065
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
1066
|
+
const rawVal = getNestedValue(args, field);
|
|
1067
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
1068
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
1069
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
1070
|
+
fieldCache.set(field, val);
|
|
1071
|
+
return val;
|
|
1072
|
+
};
|
|
1073
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
1074
|
+
const resolveFieldReadings = (field) => {
|
|
1075
|
+
const cached = readingsCache.get(field);
|
|
1076
|
+
if (cached) return cached;
|
|
1077
|
+
const primary = resolveField(field);
|
|
1078
|
+
if (primary === null) {
|
|
1079
|
+
readingsCache.set(field, []);
|
|
1080
|
+
return [];
|
|
1081
|
+
}
|
|
1082
|
+
let out = [primary];
|
|
1083
|
+
if (field === "command") {
|
|
1084
|
+
const raw = getNestedValue(args, field);
|
|
1085
|
+
if (typeof raw === "string") {
|
|
1086
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
1087
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
readingsCache.set(field, out);
|
|
1091
|
+
return out;
|
|
1092
|
+
};
|
|
1093
|
+
const results = rule.conditions.map((cond) => {
|
|
1094
|
+
const val = resolveField(cond.field);
|
|
1095
|
+
switch (cond.op) {
|
|
1096
|
+
case "exists":
|
|
1097
|
+
return val !== null && val !== "";
|
|
1098
|
+
case "notExists":
|
|
1099
|
+
return val === null || val === "";
|
|
1100
|
+
case "contains":
|
|
1101
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
1102
|
+
case "notContains":
|
|
1103
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
1104
|
+
case "matches": {
|
|
1105
|
+
if (val === null || !cond.value) return false;
|
|
1106
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1107
|
+
if (!reM) return false;
|
|
1108
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
1109
|
+
}
|
|
1110
|
+
case "notMatches": {
|
|
1111
|
+
if (!cond.value) return false;
|
|
1112
|
+
if (val === null) return true;
|
|
1113
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1114
|
+
if (!reN) return false;
|
|
1115
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
1116
|
+
}
|
|
1117
|
+
case "matchesGlob":
|
|
1118
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
1119
|
+
case "notMatchesGlob":
|
|
1120
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
1121
|
+
default:
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1126
|
+
}
|
|
1004
1127
|
var { syntax } = mvdanSh;
|
|
1005
1128
|
var sharedParser = syntax.NewParser();
|
|
1006
1129
|
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -1073,14 +1196,22 @@ function cachedNormalize(command, compute) {
|
|
|
1073
1196
|
return result;
|
|
1074
1197
|
}
|
|
1075
1198
|
function normalizeCommandForPolicy(command) {
|
|
1199
|
+
return commandReadingsImpl(command).posix;
|
|
1200
|
+
}
|
|
1201
|
+
function commandReadings(command) {
|
|
1202
|
+
const r = commandReadingsImpl(command);
|
|
1203
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
1204
|
+
}
|
|
1205
|
+
function commandReadingsImpl(command) {
|
|
1076
1206
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
1077
1207
|
}
|
|
1078
1208
|
function normalizeCommandForPolicyImpl(command) {
|
|
1079
1209
|
const f = parseShared(command);
|
|
1080
|
-
if (f === PARSE_FAIL) return command;
|
|
1210
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1081
1211
|
try {
|
|
1082
1212
|
const strips = [];
|
|
1083
1213
|
const rewrites = [];
|
|
1214
|
+
const quoteOnlyRewrites = [];
|
|
1084
1215
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
1085
1216
|
syntax.Walk(f, (node) => {
|
|
1086
1217
|
if (!node) return false;
|
|
@@ -1125,22 +1256,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1125
1256
|
if (resolved === source) continue;
|
|
1126
1257
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1127
1258
|
rewrites.push([s, e, resolved]);
|
|
1259
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
1260
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
1128
1261
|
}
|
|
1129
1262
|
return true;
|
|
1130
1263
|
});
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
...
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
return result;
|
|
1264
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
1265
|
+
const apply = (extra) => {
|
|
1266
|
+
const edits = [...stripEdits, ...extra];
|
|
1267
|
+
if (edits.length === 0) return command;
|
|
1268
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1269
|
+
let out = command;
|
|
1270
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
1271
|
+
return out;
|
|
1272
|
+
};
|
|
1273
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
1142
1274
|
} catch {
|
|
1143
|
-
return command;
|
|
1275
|
+
return { posix: command, separator: command };
|
|
1144
1276
|
}
|
|
1145
1277
|
}
|
|
1146
1278
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -1381,6 +1513,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1381
1513
|
}
|
|
1382
1514
|
return found;
|
|
1383
1515
|
}
|
|
1516
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1517
|
+
if (isBashTool(toolName)) return true;
|
|
1518
|
+
if (!toolInspection) return false;
|
|
1519
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1520
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1521
|
+
}
|
|
1522
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1523
|
+
if (!ruleTool) return true;
|
|
1524
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1525
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1526
|
+
}
|
|
1527
|
+
var INLINE_INTERPRETER = /^(python[\d.]*|perl|ruby|node|tsx|ts-node|php|lua|deno|bun|pwsh|powershell(?:\.exe)?|osascript|rscript|irb|bash|sh|zsh|script|su)$/i;
|
|
1528
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1529
|
+
"uv",
|
|
1530
|
+
"uvx",
|
|
1531
|
+
"poetry",
|
|
1532
|
+
"pipenv",
|
|
1533
|
+
"pdm",
|
|
1534
|
+
"rye",
|
|
1535
|
+
"hatch",
|
|
1536
|
+
"conda",
|
|
1537
|
+
"mamba",
|
|
1538
|
+
"micromamba",
|
|
1539
|
+
"npx",
|
|
1540
|
+
"pnpm",
|
|
1541
|
+
"yarn",
|
|
1542
|
+
"bunx",
|
|
1543
|
+
"watch",
|
|
1544
|
+
"strace",
|
|
1545
|
+
"ltrace",
|
|
1546
|
+
"chroot",
|
|
1547
|
+
"unshare",
|
|
1548
|
+
"runuser"
|
|
1549
|
+
]);
|
|
1550
|
+
function isInlineCodeFlag(interp, w) {
|
|
1551
|
+
const lw = w.toLowerCase();
|
|
1552
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1553
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1554
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1555
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1556
|
+
const body = lw.slice(1);
|
|
1557
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1558
|
+
const cut = body.search(cutAt);
|
|
1559
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1560
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1561
|
+
}
|
|
1562
|
+
var _redirStdinOps = null;
|
|
1563
|
+
function redirStdinOps() {
|
|
1564
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1565
|
+
_redirStdinOps = new Set(
|
|
1566
|
+
[
|
|
1567
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1568
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1569
|
+
deriveRedirOp("cat < f"),
|
|
1570
|
+
deriveRedirOp("cat <<< x")
|
|
1571
|
+
].filter((op) => op >= 0)
|
|
1572
|
+
);
|
|
1573
|
+
return _redirStdinOps;
|
|
1574
|
+
}
|
|
1575
|
+
function deriveBinaryOp(sample) {
|
|
1576
|
+
try {
|
|
1577
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1578
|
+
let op = -1;
|
|
1579
|
+
syntax.Walk(f, (node) => {
|
|
1580
|
+
const n = node;
|
|
1581
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1582
|
+
return true;
|
|
1583
|
+
});
|
|
1584
|
+
return op;
|
|
1585
|
+
} catch {
|
|
1586
|
+
return -1;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
var _listOps = null;
|
|
1590
|
+
function listOps() {
|
|
1591
|
+
if (_listOps) return _listOps;
|
|
1592
|
+
_listOps = new Set(
|
|
1593
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1594
|
+
);
|
|
1595
|
+
return _listOps;
|
|
1596
|
+
}
|
|
1597
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1598
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1599
|
+
function unwrapCommandHead(words) {
|
|
1600
|
+
let i = 0;
|
|
1601
|
+
while (i < words.length) {
|
|
1602
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1603
|
+
if (head === "find") {
|
|
1604
|
+
const x = words.findIndex(
|
|
1605
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1606
|
+
);
|
|
1607
|
+
if (x < 0) break;
|
|
1608
|
+
i = x + 1;
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1612
|
+
i++;
|
|
1613
|
+
let targetConsumed = false;
|
|
1614
|
+
while (i < words.length) {
|
|
1615
|
+
const t = words[i];
|
|
1616
|
+
if (t === null) {
|
|
1617
|
+
i++;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
const lt = t.toLowerCase();
|
|
1621
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1622
|
+
i++;
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
if (t.startsWith("-")) {
|
|
1626
|
+
i++;
|
|
1627
|
+
const nxt = words[i];
|
|
1628
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1629
|
+
i++;
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1633
|
+
i++;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1637
|
+
targetConsumed = true;
|
|
1638
|
+
i++;
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
break;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return i;
|
|
1645
|
+
}
|
|
1646
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1647
|
+
const cmd = stmt?.Cmd;
|
|
1648
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1649
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1650
|
+
if (words.length === 0) return false;
|
|
1651
|
+
const headIdx = unwrapCommandHead(words);
|
|
1652
|
+
const rawHead = words[headIdx];
|
|
1653
|
+
if (rawHead == null) return false;
|
|
1654
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1655
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1656
|
+
let args = words.slice(headIdx + 1);
|
|
1657
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1658
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1659
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1660
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1661
|
+
}
|
|
1662
|
+
let positionals = 0;
|
|
1663
|
+
let selectedProgram = false;
|
|
1664
|
+
for (const a of args) {
|
|
1665
|
+
if (a == null) {
|
|
1666
|
+
positionals++;
|
|
1667
|
+
selectedProgram = true;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1671
|
+
if (a === "-m") {
|
|
1672
|
+
selectedProgram = true;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1676
|
+
if (!a.startsWith("-")) {
|
|
1677
|
+
positionals++;
|
|
1678
|
+
selectedProgram = true;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1682
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1683
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1684
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1685
|
+
}
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
function detectInlineExec(command) {
|
|
1689
|
+
const f = parseShared(command);
|
|
1690
|
+
if (f === PARSE_FAIL) {
|
|
1691
|
+
return /(^|[|;&]|&&)\s*(?:[\w./-]*\/)?(python[\d.]*|perl|ruby|node|php|lua|deno|bun|pwsh|osascript|rscript|bash|sh|zsh)\s+-{1,2}[a-z]*[ceEr]/i.test(
|
|
1692
|
+
command
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
let found = false;
|
|
1696
|
+
try {
|
|
1697
|
+
syntax.Walk(f, (node) => {
|
|
1698
|
+
if (!node || found) return false;
|
|
1699
|
+
const n = node;
|
|
1700
|
+
const t = syntax.NodeType(n);
|
|
1701
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1702
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1703
|
+
found = true;
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1708
|
+
found = true;
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
return true;
|
|
1712
|
+
});
|
|
1713
|
+
} catch {
|
|
1714
|
+
return found;
|
|
1715
|
+
}
|
|
1716
|
+
return found;
|
|
1717
|
+
}
|
|
1384
1718
|
function analyzeChmod777(command) {
|
|
1385
1719
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1386
1720
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2245,150 +2579,12 @@ function extractAllSshHosts(tokens) {
|
|
|
2245
2579
|
}
|
|
2246
2580
|
return [...hosts].filter(Boolean);
|
|
2247
2581
|
}
|
|
2248
|
-
var MAX_REGEX_LENGTH = 100;
|
|
2249
|
-
var REGEX_CACHE_MAX = 500;
|
|
2250
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
2251
|
-
function validateRegex(pattern) {
|
|
2252
|
-
if (!pattern) return "Pattern is required";
|
|
2253
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2254
|
-
try {
|
|
2255
|
-
new RegExp(pattern);
|
|
2256
|
-
} catch (e) {
|
|
2257
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2258
|
-
}
|
|
2259
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2260
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2261
|
-
return null;
|
|
2262
|
-
}
|
|
2263
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2264
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2265
|
-
const key = `${pattern}\0${flags}`;
|
|
2266
|
-
if (regexCache.has(key)) {
|
|
2267
|
-
const cached = regexCache.get(key);
|
|
2268
|
-
regexCache.delete(key);
|
|
2269
|
-
regexCache.set(key, cached);
|
|
2270
|
-
return cached;
|
|
2271
|
-
}
|
|
2272
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2273
|
-
try {
|
|
2274
|
-
const re = new RegExp(pattern, flags);
|
|
2275
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2276
|
-
const oldest = regexCache.keys().next().value;
|
|
2277
|
-
if (oldest) regexCache.delete(oldest);
|
|
2278
|
-
}
|
|
2279
|
-
regexCache.set(key, re);
|
|
2280
|
-
return re;
|
|
2281
|
-
} catch {
|
|
2282
|
-
return null;
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
function matchesPattern(text, patterns) {
|
|
2286
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2287
|
-
if (p.length === 0) return false;
|
|
2288
|
-
const isMatch = pm(p, { nocase: true, dot: true });
|
|
2289
|
-
const target = text.toLowerCase();
|
|
2290
|
-
const directMatch = isMatch(target);
|
|
2291
|
-
if (directMatch) return true;
|
|
2292
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2293
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2294
|
-
}
|
|
2295
|
-
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2296
|
-
function getNestedValue(obj, path13) {
|
|
2297
|
-
if (!obj || typeof obj !== "object") return null;
|
|
2298
|
-
const segments = path13.split(".");
|
|
2299
|
-
for (const seg of segments) {
|
|
2300
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2301
|
-
}
|
|
2302
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2303
|
-
}
|
|
2304
|
-
function evaluateSmartConditions(args, rule) {
|
|
2305
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2306
|
-
const mode = rule.conditionMode ?? "all";
|
|
2307
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
2308
|
-
const resolveField = (field) => {
|
|
2309
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
2310
|
-
const rawVal = getNestedValue(args, field);
|
|
2311
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
2312
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
2313
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
2314
|
-
fieldCache.set(field, val);
|
|
2315
|
-
return val;
|
|
2316
|
-
};
|
|
2317
|
-
const results = rule.conditions.map((cond) => {
|
|
2318
|
-
const val = resolveField(cond.field);
|
|
2319
|
-
switch (cond.op) {
|
|
2320
|
-
case "exists":
|
|
2321
|
-
return val !== null && val !== "";
|
|
2322
|
-
case "notExists":
|
|
2323
|
-
return val === null || val === "";
|
|
2324
|
-
case "contains":
|
|
2325
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2326
|
-
case "notContains":
|
|
2327
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2328
|
-
case "matches": {
|
|
2329
|
-
if (val === null || !cond.value) return false;
|
|
2330
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2331
|
-
if (!reM) return false;
|
|
2332
|
-
return reM.test(val);
|
|
2333
|
-
}
|
|
2334
|
-
case "notMatches": {
|
|
2335
|
-
if (!cond.value) return false;
|
|
2336
|
-
if (val === null) return true;
|
|
2337
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2338
|
-
if (!reN) return false;
|
|
2339
|
-
return !reN.test(val);
|
|
2340
|
-
}
|
|
2341
|
-
case "matchesGlob":
|
|
2342
|
-
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
2343
|
-
case "notMatchesGlob":
|
|
2344
|
-
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
2345
|
-
default:
|
|
2346
|
-
return false;
|
|
2347
|
-
}
|
|
2348
|
-
});
|
|
2349
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2350
|
-
}
|
|
2351
2582
|
function resolveCheck(v) {
|
|
2352
2583
|
return v === "off" || v === "block" ? v : "review";
|
|
2353
2584
|
}
|
|
2354
2585
|
function resolveCheckTight(v) {
|
|
2355
2586
|
return v === "block" ? "block" : "review";
|
|
2356
2587
|
}
|
|
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
|
-
}
|
|
2392
2588
|
var VERDICT_RANK = {
|
|
2393
2589
|
allow: 0,
|
|
2394
2590
|
review: 1,
|
|
@@ -2402,6 +2598,12 @@ function resolvePinned(matches) {
|
|
|
2402
2598
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2403
2599
|
);
|
|
2404
2600
|
}
|
|
2601
|
+
function strictestVerdict(candidates) {
|
|
2602
|
+
if (candidates.length === 0) return void 0;
|
|
2603
|
+
return candidates.reduce(
|
|
2604
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2405
2607
|
function tokenize2(toolName) {
|
|
2406
2608
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2407
2609
|
}
|
|
@@ -2413,6 +2615,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2413
2615
|
const value = getNestedValue(args, fieldPath);
|
|
2414
2616
|
return typeof value === "string" ? value : null;
|
|
2415
2617
|
}
|
|
2618
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2619
|
+
const patterns = Object.keys(toolInspection);
|
|
2620
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2621
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2622
|
+
}
|
|
2416
2623
|
function isSqlTool(toolName, toolInspection) {
|
|
2417
2624
|
const patterns = Object.keys(toolInspection);
|
|
2418
2625
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2475,8 +2682,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2475
2682
|
};
|
|
2476
2683
|
}
|
|
2477
2684
|
}
|
|
2478
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2479
|
-
const
|
|
2685
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2686
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2687
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2688
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2480
2689
|
if (bashCommand !== null) {
|
|
2481
2690
|
const pipeVerdict = pipeChainVerdict(
|
|
2482
2691
|
bashCommand,
|
|
@@ -2526,8 +2735,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2526
2735
|
}
|
|
2527
2736
|
if (config.policy.smartRules.length > 0) {
|
|
2528
2737
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2738
|
+
const astSuppressed = (rule) => {
|
|
2739
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2740
|
+
const knob = rule.name === "review-drop-truncate-shell" ? resolveCheck(config.policy.commandChecks?.sqlDdl) : rule.name === "shield:filesystem:review-chmod-777" ? resolveCheck(config.policy.commandChecks?.chmod) : void 0;
|
|
2741
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2742
|
+
return true;
|
|
2743
|
+
};
|
|
2529
2744
|
const matches = config.policy.smartRules.filter(
|
|
2530
|
-
(rule) =>
|
|
2745
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2531
2746
|
);
|
|
2532
2747
|
const matchedRule = resolvePinned(matches);
|
|
2533
2748
|
if (matchedRule) {
|
|
@@ -2558,15 +2773,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2558
2773
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2559
2774
|
allTokens = analyzed.allTokens;
|
|
2560
2775
|
pathTokens = analyzed.paths;
|
|
2561
|
-
const
|
|
2562
|
-
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2563
|
-
return {
|
|
2564
|
-
decision: inlineAction === "block" ? "block" : "review",
|
|
2565
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2566
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2567
|
-
tier: 3
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2776
|
+
const candidates = [];
|
|
2570
2777
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2571
2778
|
if (evalVerdict === "block") {
|
|
2572
2779
|
return {
|
|
@@ -2577,24 +2784,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2577
2784
|
tier: 3
|
|
2578
2785
|
};
|
|
2579
2786
|
}
|
|
2787
|
+
const ptVerdict = pipeChainVerdict(
|
|
2788
|
+
shellCommand,
|
|
2789
|
+
isTrustedHost2,
|
|
2790
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2791
|
+
);
|
|
2792
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2793
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2794
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2795
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2796
|
+
candidates.push({
|
|
2797
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2798
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2799
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2800
|
+
tier: 3
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2580
2803
|
if (evalVerdict === "review") {
|
|
2581
|
-
|
|
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).
|
|
2804
|
+
candidates.push({
|
|
2585
2805
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2586
2806
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2587
2807
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2588
2808
|
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.",
|
|
2589
2809
|
tier: 3
|
|
2590
|
-
};
|
|
2810
|
+
});
|
|
2591
2811
|
}
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
isTrustedHost2,
|
|
2595
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2596
|
-
);
|
|
2597
|
-
if (ptVerdict) return ptVerdict;
|
|
2812
|
+
const builtin = strictestVerdict(candidates);
|
|
2813
|
+
if (builtin) return builtin;
|
|
2598
2814
|
if (config.policy.egress?.enabled) {
|
|
2599
2815
|
const dests = extractShellDestinations(shellCommand);
|
|
2600
2816
|
if (dests.length > 0) {
|
|
@@ -3645,28 +3861,41 @@ function readShieldOverrides() {
|
|
|
3645
3861
|
var MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
3646
3862
|
var EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
3647
3863
|
function rankIn(order, value) {
|
|
3648
|
-
return order.indexOf(value);
|
|
3864
|
+
return value === void 0 ? -1 : order.indexOf(value);
|
|
3649
3865
|
}
|
|
3650
|
-
function
|
|
3651
|
-
if (rankIn(order, cloud) === -1) return local;
|
|
3652
|
-
if (locked) return cloud;
|
|
3866
|
+
function floorValue(order, local, cloud, opts = {}) {
|
|
3867
|
+
if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
|
|
3868
|
+
if (opts.locked) return cloud;
|
|
3869
|
+
const localSet = opts.localWasSet ?? local !== void 0;
|
|
3870
|
+
if (!localSet || rankIn(order, local) === -1) return cloud;
|
|
3653
3871
|
return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
|
|
3654
3872
|
}
|
|
3873
|
+
function strictestOf(order, ...values) {
|
|
3874
|
+
let best;
|
|
3875
|
+
for (const v of values) {
|
|
3876
|
+
if (rankIn(order, v) === -1) continue;
|
|
3877
|
+
if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
|
|
3878
|
+
}
|
|
3879
|
+
return best;
|
|
3880
|
+
}
|
|
3881
|
+
function resolveByOrder(order, local, cloud, locked) {
|
|
3882
|
+
return floorValue(order, local, cloud, { locked }) ?? local;
|
|
3883
|
+
}
|
|
3655
3884
|
function resolveManagedMode(local, cloud, locked) {
|
|
3656
3885
|
return resolveByOrder(MODE_ORDER, local, cloud, locked);
|
|
3657
3886
|
}
|
|
3658
|
-
function applyManagedEgress(local, managed, locked) {
|
|
3887
|
+
function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
3659
3888
|
const next = { ...local };
|
|
3660
3889
|
if (typeof managed.enabled === "boolean") {
|
|
3661
3890
|
next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3662
3891
|
}
|
|
3663
3892
|
if (typeof managed.mode === "string") {
|
|
3664
|
-
next.mode =
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
);
|
|
3893
|
+
next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
|
|
3894
|
+
locked: locked.includes("egressMode"),
|
|
3895
|
+
// The default 'review' is seeded into egress before any merge, so absence
|
|
3896
|
+
// is invisible from `local.mode` alone — the caller tracks it for us.
|
|
3897
|
+
localWasSet: localModeUserSet
|
|
3898
|
+
}) ?? local.mode;
|
|
3670
3899
|
}
|
|
3671
3900
|
if (Array.isArray(managed.allow) && managed.allow.length > 0) {
|
|
3672
3901
|
next.allow = [...managed.allow];
|
|
@@ -3686,6 +3915,9 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3686
3915
|
if (typeof managed.enabled === "boolean") {
|
|
3687
3916
|
next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3688
3917
|
}
|
|
3918
|
+
if (managed.enabled === true) {
|
|
3919
|
+
next.scanIgnoredTools = true;
|
|
3920
|
+
}
|
|
3689
3921
|
if (typeof managed.pii === "string") {
|
|
3690
3922
|
next.pii = resolveByOrder(
|
|
3691
3923
|
DLP_PII_ORDER,
|
|
@@ -3719,12 +3951,9 @@ function applyManagedCommandChecks(local, managed, locked) {
|
|
|
3719
3951
|
const m = managed[key];
|
|
3720
3952
|
if (typeof m !== "string") continue;
|
|
3721
3953
|
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3722
|
-
const resolved =
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
m,
|
|
3726
|
-
locked.includes(lockKey)
|
|
3727
|
-
);
|
|
3954
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
3955
|
+
locked: locked.includes(lockKey)
|
|
3956
|
+
});
|
|
3728
3957
|
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3729
3958
|
next[key] = resolved;
|
|
3730
3959
|
}
|
|
@@ -3750,11 +3979,18 @@ function slug(s) {
|
|
|
3750
3979
|
var B = "[\\s/\\\\]";
|
|
3751
3980
|
var SEP = "[/\\\\]";
|
|
3752
3981
|
function pathToRegexFragment(rawPath) {
|
|
3753
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3982
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3754
3983
|
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
3755
3984
|
if (segments.length === 0) return "";
|
|
3756
3985
|
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
3757
3986
|
}
|
|
3987
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
3988
|
+
const value = pathToRegexFragment(rawPath);
|
|
3989
|
+
if (!value || !candidate) return false;
|
|
3990
|
+
const re = getCompiledRegex(value);
|
|
3991
|
+
if (!re) return false;
|
|
3992
|
+
return re.test(candidate);
|
|
3993
|
+
}
|
|
3758
3994
|
function pathRules(rawPath, verdict, reason) {
|
|
3759
3995
|
const value = pathToRegexFragment(rawPath);
|
|
3760
3996
|
if (!value) return [];
|
|
@@ -3768,12 +4004,28 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
3768
4004
|
verdict,
|
|
3769
4005
|
reason: why
|
|
3770
4006
|
},
|
|
4007
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
4008
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
3771
4009
|
{
|
|
3772
4010
|
name: `${verdict}-path-${s}-anytool`,
|
|
3773
4011
|
tool: "*",
|
|
3774
4012
|
conditions: [{ field: "file_path", op: "matches", value }],
|
|
3775
4013
|
verdict,
|
|
3776
4014
|
reason: why
|
|
4015
|
+
},
|
|
4016
|
+
{
|
|
4017
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
4018
|
+
tool: "*",
|
|
4019
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
4020
|
+
verdict,
|
|
4021
|
+
reason: why
|
|
4022
|
+
},
|
|
4023
|
+
{
|
|
4024
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
4025
|
+
tool: "*",
|
|
4026
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
4027
|
+
verdict,
|
|
4028
|
+
reason: why
|
|
3777
4029
|
}
|
|
3778
4030
|
];
|
|
3779
4031
|
}
|
|
@@ -3846,8 +4098,14 @@ var DEFAULT_CONFIG = {
|
|
|
3846
4098
|
settings: {
|
|
3847
4099
|
mode: "standard",
|
|
3848
4100
|
autoStartDaemon: true,
|
|
3849
|
-
|
|
3850
|
-
//
|
|
4101
|
+
// OFF by default. The snapshot store is a per-project bare git repo with
|
|
4102
|
+
// no size ceiling, and eviction drops the index row without deleting the
|
|
4103
|
+
// objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
|
|
4104
|
+
// from interrupted `git gc`) and filled the disk. A security tool must not
|
|
4105
|
+
// be what fills a customer's disk. Re-enable per install with
|
|
4106
|
+
// `{"settings":{"enableUndo":true}}`; the default flips back when the
|
|
4107
|
+
// bounded copy-store lands (doc/undo-v2-copy-store-design.md).
|
|
4108
|
+
enableUndo: false,
|
|
3851
4109
|
enableHookLogDebug: true,
|
|
3852
4110
|
approvalTimeoutMs: 12e4,
|
|
3853
4111
|
// 120-second auto-deny timeout
|
|
@@ -4046,10 +4304,13 @@ var DEFAULT_CONFIG = {
|
|
|
4046
4304
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
4047
4305
|
trustedHosts: [],
|
|
4048
4306
|
trustedHostsManaged: false,
|
|
4049
|
-
appPermissions: {}
|
|
4307
|
+
appPermissions: {},
|
|
4308
|
+
managedJailPaths: []
|
|
4050
4309
|
},
|
|
4051
4310
|
environments: {}
|
|
4052
4311
|
};
|
|
4312
|
+
var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
|
|
4313
|
+
var VERDICT_ORDER = ["allow", "review", "block"];
|
|
4053
4314
|
var ADVISORY_SMART_RULES = [
|
|
4054
4315
|
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
4055
4316
|
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
@@ -4061,12 +4322,7 @@ var ADVISORY_SMART_RULES = [
|
|
|
4061
4322
|
conditionMode: "all",
|
|
4062
4323
|
conditions: [
|
|
4063
4324
|
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
4064
|
-
{
|
|
4065
|
-
field: "command",
|
|
4066
|
-
op: "matches",
|
|
4067
|
-
// Matches known-safe build artifact paths in the command.
|
|
4068
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
4069
|
-
}
|
|
4325
|
+
{ field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
|
|
4070
4326
|
],
|
|
4071
4327
|
verdict: "allow",
|
|
4072
4328
|
reason: "Deleting a known-safe build artifact path"
|
|
@@ -4244,10 +4500,15 @@ function getConfig(cwd) {
|
|
|
4244
4500
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
4245
4501
|
trustedHosts: [],
|
|
4246
4502
|
trustedHostsManaged: false,
|
|
4247
|
-
appPermissions: {}
|
|
4503
|
+
appPermissions: {},
|
|
4504
|
+
managedJailPaths: []
|
|
4248
4505
|
};
|
|
4249
4506
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
4250
|
-
const
|
|
4507
|
+
const rank = (v) => {
|
|
4508
|
+
const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
|
|
4509
|
+
return i === -1 ? 1 : i;
|
|
4510
|
+
};
|
|
4511
|
+
const applyLayer = (source, isProject = false) => {
|
|
4251
4512
|
if (!source) return;
|
|
4252
4513
|
const s = source.settings || {};
|
|
4253
4514
|
const p = source.policy || {};
|
|
@@ -4307,7 +4568,9 @@ function getConfig(cwd) {
|
|
|
4307
4568
|
};
|
|
4308
4569
|
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4309
4570
|
const v = src[k];
|
|
4310
|
-
if (v
|
|
4571
|
+
if (v !== "off" && v !== "review" && v !== "block") continue;
|
|
4572
|
+
if (isProject && rank(v) < rank(cc2[k])) continue;
|
|
4573
|
+
cc2[k] = v;
|
|
4311
4574
|
}
|
|
4312
4575
|
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4313
4576
|
const v = src[k];
|
|
@@ -4317,11 +4580,22 @@ function getConfig(cwd) {
|
|
|
4317
4580
|
}
|
|
4318
4581
|
if (p.egress) {
|
|
4319
4582
|
const e = p.egress;
|
|
4320
|
-
if (e.enabled !== void 0
|
|
4321
|
-
|
|
4322
|
-
if (
|
|
4583
|
+
if (e.enabled !== void 0 && !(isProject && e.enabled === false))
|
|
4584
|
+
mergedPolicy.egress.enabled = e.enabled;
|
|
4585
|
+
if (e.mode !== void 0) {
|
|
4586
|
+
const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
|
|
4587
|
+
if (!weaker) {
|
|
4588
|
+
mergedPolicy.egress.mode = e.mode;
|
|
4589
|
+
egressModeUserSet = true;
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
|
|
4593
|
+
mergedPolicy.egress.allow.push(...e.allow);
|
|
4594
|
+
}
|
|
4595
|
+
if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
|
|
4323
4596
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4324
|
-
if (e.allowPrivate !== void 0
|
|
4597
|
+
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4598
|
+
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
4325
4599
|
}
|
|
4326
4600
|
if (p.loopDetection) {
|
|
4327
4601
|
const ld = p.loopDetection;
|
|
@@ -4363,11 +4637,21 @@ function getConfig(cwd) {
|
|
|
4363
4637
|
}
|
|
4364
4638
|
}
|
|
4365
4639
|
};
|
|
4640
|
+
let egressModeUserSet = false;
|
|
4641
|
+
let egressAllowUserSet = false;
|
|
4366
4642
|
applyLayer(globalConfig);
|
|
4367
|
-
applyLayer(
|
|
4643
|
+
applyLayer(
|
|
4644
|
+
projectConfig,
|
|
4645
|
+
/* isProject */
|
|
4646
|
+
true
|
|
4647
|
+
);
|
|
4368
4648
|
let cloudManagedShields = [];
|
|
4649
|
+
const managedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4650
|
+
const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4369
4651
|
let modeCloudControlled = false;
|
|
4370
4652
|
let modeCloudStaged = false;
|
|
4653
|
+
let cloudMandatesEnforcement = false;
|
|
4654
|
+
let cloudMandatesAppPerm = false;
|
|
4371
4655
|
{
|
|
4372
4656
|
const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
|
|
4373
4657
|
try {
|
|
@@ -4402,7 +4686,8 @@ function getConfig(cwd) {
|
|
|
4402
4686
|
deny: hosts(mc.egress.deny),
|
|
4403
4687
|
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
|
|
4404
4688
|
},
|
|
4405
|
-
locked
|
|
4689
|
+
locked,
|
|
4690
|
+
egressModeUserSet
|
|
4406
4691
|
);
|
|
4407
4692
|
}
|
|
4408
4693
|
if (mc.dlp && typeof mc.dlp === "object") {
|
|
@@ -4422,6 +4707,12 @@ function getConfig(cwd) {
|
|
|
4422
4707
|
mc.commandChecks,
|
|
4423
4708
|
locked
|
|
4424
4709
|
);
|
|
4710
|
+
for (const [key, val] of Object.entries(mc.commandChecks)) {
|
|
4711
|
+
if (typeof val !== "string") continue;
|
|
4712
|
+
managedCommandCheckKeys.add(key);
|
|
4713
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4714
|
+
if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
|
|
4715
|
+
}
|
|
4425
4716
|
}
|
|
4426
4717
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4427
4718
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
@@ -4468,12 +4759,13 @@ function getConfig(cwd) {
|
|
|
4468
4759
|
}
|
|
4469
4760
|
if (Array.isArray(mc.jailPaths)) {
|
|
4470
4761
|
for (const jp of mc.jailPaths) {
|
|
4471
|
-
const
|
|
4472
|
-
if (!
|
|
4762
|
+
const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
4763
|
+
if (!path14) continue;
|
|
4473
4764
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4474
|
-
for (const r of pathRules(
|
|
4765
|
+
for (const r of pathRules(path14, verdict, "org-managed jail")) {
|
|
4475
4766
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4476
4767
|
}
|
|
4768
|
+
mergedPolicy.managedJailPaths.push({ path: path14, verdict });
|
|
4477
4769
|
}
|
|
4478
4770
|
}
|
|
4479
4771
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -4491,7 +4783,14 @@ function getConfig(cwd) {
|
|
|
4491
4783
|
if (Object.keys(m).length) coerced[srv] = m;
|
|
4492
4784
|
}
|
|
4493
4785
|
mergedPolicy.appPermissions = coerced;
|
|
4786
|
+
cloudMandatesAppPerm = Object.values(coerced).some(
|
|
4787
|
+
(tools) => Object.values(tools).some((d) => d === "block" || d === "review")
|
|
4788
|
+
);
|
|
4494
4789
|
}
|
|
4790
|
+
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
4791
|
+
cloudMandatesEnforcement = cloudMandatesAppPerm || Array.isArray(mc.jailPaths) && mc.jailPaths.some((jp) => typeof jp?.path === "string" && jp.path.trim() !== "") || on(mc.egress) || on(mc.dlp) || on(mc.injectionScan) || on(mc.skillPinning) || on(mc.loopDetection) || !!mc.commandChecks && typeof mc.commandChecks === "object" && Object.values(mc.commandChecks).some(
|
|
4792
|
+
(v) => typeof v === "string" && v !== "off"
|
|
4793
|
+
);
|
|
4495
4794
|
}
|
|
4496
4795
|
if (raw.panicMode === true) {
|
|
4497
4796
|
mergedSettings.panicMode = true;
|
|
@@ -4533,25 +4832,45 @@ function getConfig(cwd) {
|
|
|
4533
4832
|
}
|
|
4534
4833
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4535
4834
|
const cc = mergedPolicy.commandChecks ?? {};
|
|
4536
|
-
const
|
|
4537
|
-
if (name === "review-rm") return
|
|
4538
|
-
if (name?.endsWith("-sql")) return
|
|
4835
|
+
const advisoryKnobKey = (name) => {
|
|
4836
|
+
if (name === "review-rm") return "rmAdvisory";
|
|
4837
|
+
if (name?.endsWith("-sql")) return "sqlDdl";
|
|
4539
4838
|
return void 0;
|
|
4540
4839
|
};
|
|
4541
4840
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4542
|
-
|
|
4543
|
-
const knob =
|
|
4841
|
+
const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
|
|
4842
|
+
const knob = knobKey ? cc[knobKey] : void 0;
|
|
4544
4843
|
if (knob === "off") continue;
|
|
4545
|
-
|
|
4844
|
+
const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
|
|
4845
|
+
const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
|
|
4846
|
+
const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
|
|
4847
|
+
const knobVerdict = knob === "block" ? "block" : rule.verdict;
|
|
4848
|
+
if (!managed) {
|
|
4849
|
+
if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
|
|
4850
|
+
continue;
|
|
4851
|
+
}
|
|
4852
|
+
const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
|
|
4853
|
+
if (twin) {
|
|
4854
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4855
|
+
}
|
|
4856
|
+
const injected = { ...rule, verdict: effective, pinned: true };
|
|
4857
|
+
if (rule.name === "review-rm" && effective !== "block") {
|
|
4858
|
+
injected.conditions = [
|
|
4859
|
+
...rule.conditions ?? [],
|
|
4860
|
+
{ field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
|
|
4861
|
+
];
|
|
4862
|
+
injected.conditionMode = "all";
|
|
4863
|
+
}
|
|
4864
|
+
mergedPolicy.smartRules.push(injected);
|
|
4546
4865
|
}
|
|
4547
4866
|
const envMode = process.env.NODE9_MODE;
|
|
4548
4867
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4549
4868
|
mergedSettings.mode = envMode;
|
|
4550
4869
|
}
|
|
4551
|
-
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4870
|
+
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4552
4871
|
mergedSettings.mode = "standard";
|
|
4553
4872
|
}
|
|
4554
|
-
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4873
|
+
const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4555
4874
|
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4556
4875
|
for (const name of Object.keys(mergedEnvironments)) {
|
|
4557
4876
|
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
@@ -4752,14 +5071,14 @@ function checkProvenance(cmd, cwd) {
|
|
|
4752
5071
|
}
|
|
4753
5072
|
|
|
4754
5073
|
// src/policy/index.ts
|
|
4755
|
-
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
5074
|
+
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
4756
5075
|
const config = getConfig();
|
|
4757
5076
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
4758
5077
|
return evaluatePolicy(
|
|
4759
5078
|
config,
|
|
4760
5079
|
toolName,
|
|
4761
5080
|
args,
|
|
4762
|
-
{ agent, cwd, activeEnvironment },
|
|
5081
|
+
{ agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
|
|
4763
5082
|
{
|
|
4764
5083
|
checkProvenance,
|
|
4765
5084
|
// Managed → match against the org list (frozen with the rest of managed
|
|
@@ -5611,6 +5930,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
5611
5930
|
}
|
|
5612
5931
|
}
|
|
5613
5932
|
|
|
5933
|
+
// src/shields/jail.ts
|
|
5934
|
+
import fs11 from "fs";
|
|
5935
|
+
import os10 from "os";
|
|
5936
|
+
import path13 from "path";
|
|
5937
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
5938
|
+
function jailStorePath() {
|
|
5939
|
+
return path13.join(os10.homedir(), ".node9", "jail-paths.json");
|
|
5940
|
+
}
|
|
5941
|
+
function readJailPaths() {
|
|
5942
|
+
let text;
|
|
5943
|
+
try {
|
|
5944
|
+
text = fs11.readFileSync(jailStorePath(), "utf8");
|
|
5945
|
+
} catch (err) {
|
|
5946
|
+
if (err.code === "ENOENT") return [];
|
|
5947
|
+
throw err;
|
|
5948
|
+
}
|
|
5949
|
+
let parsed;
|
|
5950
|
+
try {
|
|
5951
|
+
parsed = JSON.parse(text);
|
|
5952
|
+
} catch {
|
|
5953
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
5954
|
+
}
|
|
5955
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
5956
|
+
return parsed.paths.filter(
|
|
5957
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
5958
|
+
);
|
|
5959
|
+
}
|
|
5960
|
+
function findJailedPath(candidate) {
|
|
5961
|
+
return findJailedPathIn(candidate, readJailPaths());
|
|
5962
|
+
}
|
|
5963
|
+
function findJailedPathIn(candidate, paths) {
|
|
5964
|
+
if (!candidate) return null;
|
|
5965
|
+
for (const entry of paths) {
|
|
5966
|
+
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
5967
|
+
}
|
|
5968
|
+
return null;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5614
5971
|
// src/auth/orchestrator.ts
|
|
5615
5972
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
5616
5973
|
"write",
|
|
@@ -6086,12 +6443,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6086
6443
|
} else if (!taintWarning && !appPermReview) {
|
|
6087
6444
|
const toolLower = toolName.toLowerCase();
|
|
6088
6445
|
const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
|
|
6089
|
-
|
|
6446
|
+
const activeShields = isFileTool ? readActiveShields() : [];
|
|
6447
|
+
const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
|
|
6448
|
+
if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
|
|
6090
6449
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6091
|
-
const
|
|
6092
|
-
|
|
6093
|
-
);
|
|
6094
|
-
if (
|
|
6450
|
+
const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
|
|
6451
|
+
const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
|
|
6452
|
+
const sensitiveHit = candidates.some((c) => scanFilePath(c));
|
|
6453
|
+
if (jailHit || sensitiveHit) {
|
|
6454
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
6455
|
+
skipIgnoredFastPath: true
|
|
6456
|
+
});
|
|
6457
|
+
if (policyResult.decision === "block") {
|
|
6458
|
+
if (!isManual)
|
|
6459
|
+
appendLocalAudit(
|
|
6460
|
+
toolName,
|
|
6461
|
+
args,
|
|
6462
|
+
"deny",
|
|
6463
|
+
"smart-rule-block",
|
|
6464
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
6465
|
+
hashAuditArgs
|
|
6466
|
+
);
|
|
6467
|
+
return {
|
|
6468
|
+
approved: false,
|
|
6469
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
6470
|
+
blockedBy: "local-config",
|
|
6471
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
6472
|
+
ruleHit: policyResult.ruleName
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6095
6475
|
} else {
|
|
6096
6476
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
6097
6477
|
return { approved: true };
|
|
@@ -6154,7 +6534,12 @@ ${appPermReview}`
|
|
|
6154
6534
|
}
|
|
6155
6535
|
let cloudRequestId = null;
|
|
6156
6536
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
6157
|
-
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview ||
|
|
6537
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
|
|
6538
|
+
// a client-side heuristic the SaaS has no rule for, so without forceReview
|
|
6539
|
+
// its checkRule answers "no org rule matched" → {approved:true}, which is
|
|
6540
|
+
// not an approval of an exfiltration risk. Measured against the live BE:
|
|
6541
|
+
// {approved:true} without this flag, {pending:true} with it.
|
|
6542
|
+
!!taintWarning || void 0;
|
|
6158
6543
|
if (cloudEnforced) {
|
|
6159
6544
|
try {
|
|
6160
6545
|
const initResult = await initNode9SaaS(
|
|
@@ -6167,10 +6552,10 @@ ${appPermReview}`
|
|
|
6167
6552
|
forceReview
|
|
6168
6553
|
);
|
|
6169
6554
|
if (!initResult.pending) {
|
|
6170
|
-
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6555
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6171
6556
|
return { approved: true, checkedBy: "cloud" };
|
|
6172
6557
|
}
|
|
6173
|
-
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6558
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6174
6559
|
return {
|
|
6175
6560
|
approved: !!initResult.approved,
|
|
6176
6561
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|