@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.js
CHANGED
|
@@ -410,8 +410,8 @@ function sanitizeConfig(raw) {
|
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
const lines = result.error.issues.map((issue) => {
|
|
413
|
-
const
|
|
414
|
-
return ` \u2022 ${
|
|
413
|
+
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
414
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
415
415
|
});
|
|
416
416
|
return {
|
|
417
417
|
sanitized,
|
|
@@ -1031,6 +1031,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1031
1031
|
}
|
|
1032
1032
|
return null;
|
|
1033
1033
|
}
|
|
1034
|
+
var MAX_REGEX_LENGTH = 256;
|
|
1035
|
+
var REGEX_CACHE_MAX = 500;
|
|
1036
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
1037
|
+
function validateRegex(pattern) {
|
|
1038
|
+
if (!pattern) return "Pattern is required";
|
|
1039
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1040
|
+
try {
|
|
1041
|
+
new RegExp(pattern);
|
|
1042
|
+
} catch (e) {
|
|
1043
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
1044
|
+
}
|
|
1045
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1046
|
+
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
1050
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1051
|
+
const key = `${pattern}\0${flags}`;
|
|
1052
|
+
if (regexCache.has(key)) {
|
|
1053
|
+
const cached = regexCache.get(key);
|
|
1054
|
+
regexCache.delete(key);
|
|
1055
|
+
regexCache.set(key, cached);
|
|
1056
|
+
return cached;
|
|
1057
|
+
}
|
|
1058
|
+
if (validateRegex(pattern) !== null) return null;
|
|
1059
|
+
try {
|
|
1060
|
+
const re = new RegExp(pattern, flags);
|
|
1061
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1062
|
+
const oldest = regexCache.keys().next().value;
|
|
1063
|
+
if (oldest) regexCache.delete(oldest);
|
|
1064
|
+
}
|
|
1065
|
+
regexCache.set(key, re);
|
|
1066
|
+
return re;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function matchesPattern(text, patterns) {
|
|
1072
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
1073
|
+
if (p.length === 0) return false;
|
|
1074
|
+
const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
|
|
1075
|
+
const target = text.toLowerCase();
|
|
1076
|
+
const directMatch = isMatch(target);
|
|
1077
|
+
if (directMatch) return true;
|
|
1078
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1079
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1080
|
+
}
|
|
1081
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1082
|
+
function getNestedValue(obj, path14) {
|
|
1083
|
+
if (!obj || typeof obj !== "object") return null;
|
|
1084
|
+
const segments = path14.split(".");
|
|
1085
|
+
for (const seg of segments) {
|
|
1086
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1087
|
+
}
|
|
1088
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
1089
|
+
}
|
|
1090
|
+
function evaluateSmartConditions(args, rule) {
|
|
1091
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
1092
|
+
const mode = rule.conditionMode ?? "all";
|
|
1093
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
1094
|
+
const resolveField = (field) => {
|
|
1095
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
1096
|
+
const rawVal = getNestedValue(args, field);
|
|
1097
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
1098
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
1099
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
1100
|
+
fieldCache.set(field, val);
|
|
1101
|
+
return val;
|
|
1102
|
+
};
|
|
1103
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
1104
|
+
const resolveFieldReadings = (field) => {
|
|
1105
|
+
const cached = readingsCache.get(field);
|
|
1106
|
+
if (cached) return cached;
|
|
1107
|
+
const primary = resolveField(field);
|
|
1108
|
+
if (primary === null) {
|
|
1109
|
+
readingsCache.set(field, []);
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
let out = [primary];
|
|
1113
|
+
if (field === "command") {
|
|
1114
|
+
const raw = getNestedValue(args, field);
|
|
1115
|
+
if (typeof raw === "string") {
|
|
1116
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
1117
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
readingsCache.set(field, out);
|
|
1121
|
+
return out;
|
|
1122
|
+
};
|
|
1123
|
+
const results = rule.conditions.map((cond) => {
|
|
1124
|
+
const val = resolveField(cond.field);
|
|
1125
|
+
switch (cond.op) {
|
|
1126
|
+
case "exists":
|
|
1127
|
+
return val !== null && val !== "";
|
|
1128
|
+
case "notExists":
|
|
1129
|
+
return val === null || val === "";
|
|
1130
|
+
case "contains":
|
|
1131
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
1132
|
+
case "notContains":
|
|
1133
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
1134
|
+
case "matches": {
|
|
1135
|
+
if (val === null || !cond.value) return false;
|
|
1136
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1137
|
+
if (!reM) return false;
|
|
1138
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
1139
|
+
}
|
|
1140
|
+
case "notMatches": {
|
|
1141
|
+
if (!cond.value) return false;
|
|
1142
|
+
if (val === null) return true;
|
|
1143
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1144
|
+
if (!reN) return false;
|
|
1145
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
1146
|
+
}
|
|
1147
|
+
case "matchesGlob":
|
|
1148
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1149
|
+
case "notMatchesGlob":
|
|
1150
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
1151
|
+
default:
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1156
|
+
}
|
|
1034
1157
|
var { syntax } = import_mvdan_sh.default;
|
|
1035
1158
|
var sharedParser = syntax.NewParser();
|
|
1036
1159
|
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -1103,14 +1226,22 @@ function cachedNormalize(command, compute) {
|
|
|
1103
1226
|
return result;
|
|
1104
1227
|
}
|
|
1105
1228
|
function normalizeCommandForPolicy(command) {
|
|
1229
|
+
return commandReadingsImpl(command).posix;
|
|
1230
|
+
}
|
|
1231
|
+
function commandReadings(command) {
|
|
1232
|
+
const r = commandReadingsImpl(command);
|
|
1233
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
1234
|
+
}
|
|
1235
|
+
function commandReadingsImpl(command) {
|
|
1106
1236
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
1107
1237
|
}
|
|
1108
1238
|
function normalizeCommandForPolicyImpl(command) {
|
|
1109
1239
|
const f = parseShared(command);
|
|
1110
|
-
if (f === PARSE_FAIL) return command;
|
|
1240
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1111
1241
|
try {
|
|
1112
1242
|
const strips = [];
|
|
1113
1243
|
const rewrites = [];
|
|
1244
|
+
const quoteOnlyRewrites = [];
|
|
1114
1245
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
1115
1246
|
syntax.Walk(f, (node) => {
|
|
1116
1247
|
if (!node) return false;
|
|
@@ -1155,22 +1286,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1155
1286
|
if (resolved === source) continue;
|
|
1156
1287
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1157
1288
|
rewrites.push([s, e, resolved]);
|
|
1289
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
1290
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
1158
1291
|
}
|
|
1159
1292
|
return true;
|
|
1160
1293
|
});
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
...
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
return result;
|
|
1294
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
1295
|
+
const apply = (extra) => {
|
|
1296
|
+
const edits = [...stripEdits, ...extra];
|
|
1297
|
+
if (edits.length === 0) return command;
|
|
1298
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1299
|
+
let out = command;
|
|
1300
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
1301
|
+
return out;
|
|
1302
|
+
};
|
|
1303
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
1172
1304
|
} catch {
|
|
1173
|
-
return command;
|
|
1305
|
+
return { posix: command, separator: command };
|
|
1174
1306
|
}
|
|
1175
1307
|
}
|
|
1176
1308
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -1411,6 +1543,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1411
1543
|
}
|
|
1412
1544
|
return found;
|
|
1413
1545
|
}
|
|
1546
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1547
|
+
if (isBashTool(toolName)) return true;
|
|
1548
|
+
if (!toolInspection) return false;
|
|
1549
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1550
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1551
|
+
}
|
|
1552
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1553
|
+
if (!ruleTool) return true;
|
|
1554
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1555
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1556
|
+
}
|
|
1557
|
+
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;
|
|
1558
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1559
|
+
"uv",
|
|
1560
|
+
"uvx",
|
|
1561
|
+
"poetry",
|
|
1562
|
+
"pipenv",
|
|
1563
|
+
"pdm",
|
|
1564
|
+
"rye",
|
|
1565
|
+
"hatch",
|
|
1566
|
+
"conda",
|
|
1567
|
+
"mamba",
|
|
1568
|
+
"micromamba",
|
|
1569
|
+
"npx",
|
|
1570
|
+
"pnpm",
|
|
1571
|
+
"yarn",
|
|
1572
|
+
"bunx",
|
|
1573
|
+
"watch",
|
|
1574
|
+
"strace",
|
|
1575
|
+
"ltrace",
|
|
1576
|
+
"chroot",
|
|
1577
|
+
"unshare",
|
|
1578
|
+
"runuser"
|
|
1579
|
+
]);
|
|
1580
|
+
function isInlineCodeFlag(interp, w) {
|
|
1581
|
+
const lw = w.toLowerCase();
|
|
1582
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1583
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1584
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1585
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1586
|
+
const body = lw.slice(1);
|
|
1587
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1588
|
+
const cut = body.search(cutAt);
|
|
1589
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1590
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1591
|
+
}
|
|
1592
|
+
var _redirStdinOps = null;
|
|
1593
|
+
function redirStdinOps() {
|
|
1594
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1595
|
+
_redirStdinOps = new Set(
|
|
1596
|
+
[
|
|
1597
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1598
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1599
|
+
deriveRedirOp("cat < f"),
|
|
1600
|
+
deriveRedirOp("cat <<< x")
|
|
1601
|
+
].filter((op) => op >= 0)
|
|
1602
|
+
);
|
|
1603
|
+
return _redirStdinOps;
|
|
1604
|
+
}
|
|
1605
|
+
function deriveBinaryOp(sample) {
|
|
1606
|
+
try {
|
|
1607
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1608
|
+
let op = -1;
|
|
1609
|
+
syntax.Walk(f, (node) => {
|
|
1610
|
+
const n = node;
|
|
1611
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1612
|
+
return true;
|
|
1613
|
+
});
|
|
1614
|
+
return op;
|
|
1615
|
+
} catch {
|
|
1616
|
+
return -1;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
var _listOps = null;
|
|
1620
|
+
function listOps() {
|
|
1621
|
+
if (_listOps) return _listOps;
|
|
1622
|
+
_listOps = new Set(
|
|
1623
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1624
|
+
);
|
|
1625
|
+
return _listOps;
|
|
1626
|
+
}
|
|
1627
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1628
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1629
|
+
function unwrapCommandHead(words) {
|
|
1630
|
+
let i = 0;
|
|
1631
|
+
while (i < words.length) {
|
|
1632
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1633
|
+
if (head === "find") {
|
|
1634
|
+
const x = words.findIndex(
|
|
1635
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1636
|
+
);
|
|
1637
|
+
if (x < 0) break;
|
|
1638
|
+
i = x + 1;
|
|
1639
|
+
continue;
|
|
1640
|
+
}
|
|
1641
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1642
|
+
i++;
|
|
1643
|
+
let targetConsumed = false;
|
|
1644
|
+
while (i < words.length) {
|
|
1645
|
+
const t = words[i];
|
|
1646
|
+
if (t === null) {
|
|
1647
|
+
i++;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
const lt = t.toLowerCase();
|
|
1651
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1652
|
+
i++;
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (t.startsWith("-")) {
|
|
1656
|
+
i++;
|
|
1657
|
+
const nxt = words[i];
|
|
1658
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1659
|
+
i++;
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1663
|
+
i++;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1667
|
+
targetConsumed = true;
|
|
1668
|
+
i++;
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return i;
|
|
1675
|
+
}
|
|
1676
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1677
|
+
const cmd = stmt?.Cmd;
|
|
1678
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1679
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1680
|
+
if (words.length === 0) return false;
|
|
1681
|
+
const headIdx = unwrapCommandHead(words);
|
|
1682
|
+
const rawHead = words[headIdx];
|
|
1683
|
+
if (rawHead == null) return false;
|
|
1684
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1685
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1686
|
+
let args = words.slice(headIdx + 1);
|
|
1687
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1688
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1689
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1690
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1691
|
+
}
|
|
1692
|
+
let positionals = 0;
|
|
1693
|
+
let selectedProgram = false;
|
|
1694
|
+
for (const a of args) {
|
|
1695
|
+
if (a == null) {
|
|
1696
|
+
positionals++;
|
|
1697
|
+
selectedProgram = true;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1701
|
+
if (a === "-m") {
|
|
1702
|
+
selectedProgram = true;
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1706
|
+
if (!a.startsWith("-")) {
|
|
1707
|
+
positionals++;
|
|
1708
|
+
selectedProgram = true;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1712
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1713
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1714
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1715
|
+
}
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
function detectInlineExec(command) {
|
|
1719
|
+
const f = parseShared(command);
|
|
1720
|
+
if (f === PARSE_FAIL) {
|
|
1721
|
+
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(
|
|
1722
|
+
command
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
let found = false;
|
|
1726
|
+
try {
|
|
1727
|
+
syntax.Walk(f, (node) => {
|
|
1728
|
+
if (!node || found) return false;
|
|
1729
|
+
const n = node;
|
|
1730
|
+
const t = syntax.NodeType(n);
|
|
1731
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1732
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1733
|
+
found = true;
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1738
|
+
found = true;
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
return true;
|
|
1742
|
+
});
|
|
1743
|
+
} catch {
|
|
1744
|
+
return found;
|
|
1745
|
+
}
|
|
1746
|
+
return found;
|
|
1747
|
+
}
|
|
1414
1748
|
function analyzeChmod777(command) {
|
|
1415
1749
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1416
1750
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2275,150 +2609,12 @@ function extractAllSshHosts(tokens) {
|
|
|
2275
2609
|
}
|
|
2276
2610
|
return [...hosts].filter(Boolean);
|
|
2277
2611
|
}
|
|
2278
|
-
var MAX_REGEX_LENGTH = 100;
|
|
2279
|
-
var REGEX_CACHE_MAX = 500;
|
|
2280
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
2281
|
-
function validateRegex(pattern) {
|
|
2282
|
-
if (!pattern) return "Pattern is required";
|
|
2283
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2284
|
-
try {
|
|
2285
|
-
new RegExp(pattern);
|
|
2286
|
-
} catch (e) {
|
|
2287
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2288
|
-
}
|
|
2289
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2290
|
-
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2291
|
-
return null;
|
|
2292
|
-
}
|
|
2293
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2294
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2295
|
-
const key = `${pattern}\0${flags}`;
|
|
2296
|
-
if (regexCache.has(key)) {
|
|
2297
|
-
const cached = regexCache.get(key);
|
|
2298
|
-
regexCache.delete(key);
|
|
2299
|
-
regexCache.set(key, cached);
|
|
2300
|
-
return cached;
|
|
2301
|
-
}
|
|
2302
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2303
|
-
try {
|
|
2304
|
-
const re = new RegExp(pattern, flags);
|
|
2305
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2306
|
-
const oldest = regexCache.keys().next().value;
|
|
2307
|
-
if (oldest) regexCache.delete(oldest);
|
|
2308
|
-
}
|
|
2309
|
-
regexCache.set(key, re);
|
|
2310
|
-
return re;
|
|
2311
|
-
} catch {
|
|
2312
|
-
return null;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
function matchesPattern(text, patterns) {
|
|
2316
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2317
|
-
if (p.length === 0) return false;
|
|
2318
|
-
const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
|
|
2319
|
-
const target = text.toLowerCase();
|
|
2320
|
-
const directMatch = isMatch(target);
|
|
2321
|
-
if (directMatch) return true;
|
|
2322
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2323
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2324
|
-
}
|
|
2325
|
-
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2326
|
-
function getNestedValue(obj, path13) {
|
|
2327
|
-
if (!obj || typeof obj !== "object") return null;
|
|
2328
|
-
const segments = path13.split(".");
|
|
2329
|
-
for (const seg of segments) {
|
|
2330
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2331
|
-
}
|
|
2332
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2333
|
-
}
|
|
2334
|
-
function evaluateSmartConditions(args, rule) {
|
|
2335
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2336
|
-
const mode = rule.conditionMode ?? "all";
|
|
2337
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
2338
|
-
const resolveField = (field) => {
|
|
2339
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
2340
|
-
const rawVal = getNestedValue(args, field);
|
|
2341
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
2342
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
2343
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
2344
|
-
fieldCache.set(field, val);
|
|
2345
|
-
return val;
|
|
2346
|
-
};
|
|
2347
|
-
const results = rule.conditions.map((cond) => {
|
|
2348
|
-
const val = resolveField(cond.field);
|
|
2349
|
-
switch (cond.op) {
|
|
2350
|
-
case "exists":
|
|
2351
|
-
return val !== null && val !== "";
|
|
2352
|
-
case "notExists":
|
|
2353
|
-
return val === null || val === "";
|
|
2354
|
-
case "contains":
|
|
2355
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2356
|
-
case "notContains":
|
|
2357
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2358
|
-
case "matches": {
|
|
2359
|
-
if (val === null || !cond.value) return false;
|
|
2360
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2361
|
-
if (!reM) return false;
|
|
2362
|
-
return reM.test(val);
|
|
2363
|
-
}
|
|
2364
|
-
case "notMatches": {
|
|
2365
|
-
if (!cond.value) return false;
|
|
2366
|
-
if (val === null) return true;
|
|
2367
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2368
|
-
if (!reN) return false;
|
|
2369
|
-
return !reN.test(val);
|
|
2370
|
-
}
|
|
2371
|
-
case "matchesGlob":
|
|
2372
|
-
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2373
|
-
case "notMatchesGlob":
|
|
2374
|
-
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2375
|
-
default:
|
|
2376
|
-
return false;
|
|
2377
|
-
}
|
|
2378
|
-
});
|
|
2379
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2380
|
-
}
|
|
2381
2612
|
function resolveCheck(v) {
|
|
2382
2613
|
return v === "off" || v === "block" ? v : "review";
|
|
2383
2614
|
}
|
|
2384
2615
|
function resolveCheckTight(v) {
|
|
2385
2616
|
return v === "block" ? "block" : "review";
|
|
2386
2617
|
}
|
|
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
|
-
}
|
|
2422
2618
|
var VERDICT_RANK = {
|
|
2423
2619
|
allow: 0,
|
|
2424
2620
|
review: 1,
|
|
@@ -2432,6 +2628,12 @@ function resolvePinned(matches) {
|
|
|
2432
2628
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2433
2629
|
);
|
|
2434
2630
|
}
|
|
2631
|
+
function strictestVerdict(candidates) {
|
|
2632
|
+
if (candidates.length === 0) return void 0;
|
|
2633
|
+
return candidates.reduce(
|
|
2634
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2435
2637
|
function tokenize2(toolName) {
|
|
2436
2638
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2437
2639
|
}
|
|
@@ -2443,6 +2645,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2443
2645
|
const value = getNestedValue(args, fieldPath);
|
|
2444
2646
|
return typeof value === "string" ? value : null;
|
|
2445
2647
|
}
|
|
2648
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2649
|
+
const patterns = Object.keys(toolInspection);
|
|
2650
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2651
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2652
|
+
}
|
|
2446
2653
|
function isSqlTool(toolName, toolInspection) {
|
|
2447
2654
|
const patterns = Object.keys(toolInspection);
|
|
2448
2655
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2505,8 +2712,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2505
2712
|
};
|
|
2506
2713
|
}
|
|
2507
2714
|
}
|
|
2508
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2509
|
-
const
|
|
2715
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2716
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2717
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2718
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2510
2719
|
if (bashCommand !== null) {
|
|
2511
2720
|
const pipeVerdict = pipeChainVerdict(
|
|
2512
2721
|
bashCommand,
|
|
@@ -2556,8 +2765,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2556
2765
|
}
|
|
2557
2766
|
if (config.policy.smartRules.length > 0) {
|
|
2558
2767
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2768
|
+
const astSuppressed = (rule) => {
|
|
2769
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2770
|
+
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;
|
|
2771
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2772
|
+
return true;
|
|
2773
|
+
};
|
|
2559
2774
|
const matches = config.policy.smartRules.filter(
|
|
2560
|
-
(rule) =>
|
|
2775
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2561
2776
|
);
|
|
2562
2777
|
const matchedRule = resolvePinned(matches);
|
|
2563
2778
|
if (matchedRule) {
|
|
@@ -2588,15 +2803,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2588
2803
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2589
2804
|
allTokens = analyzed.allTokens;
|
|
2590
2805
|
pathTokens = analyzed.paths;
|
|
2591
|
-
const
|
|
2592
|
-
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2593
|
-
return {
|
|
2594
|
-
decision: inlineAction === "block" ? "block" : "review",
|
|
2595
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2596
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2597
|
-
tier: 3
|
|
2598
|
-
};
|
|
2599
|
-
}
|
|
2806
|
+
const candidates = [];
|
|
2600
2807
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2601
2808
|
if (evalVerdict === "block") {
|
|
2602
2809
|
return {
|
|
@@ -2607,24 +2814,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2607
2814
|
tier: 3
|
|
2608
2815
|
};
|
|
2609
2816
|
}
|
|
2817
|
+
const ptVerdict = pipeChainVerdict(
|
|
2818
|
+
shellCommand,
|
|
2819
|
+
isTrustedHost2,
|
|
2820
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2821
|
+
);
|
|
2822
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2823
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2824
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2825
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2826
|
+
candidates.push({
|
|
2827
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2828
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2829
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2830
|
+
tier: 3
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2610
2833
|
if (evalVerdict === "review") {
|
|
2611
|
-
|
|
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).
|
|
2834
|
+
candidates.push({
|
|
2615
2835
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2616
2836
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2617
2837
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2618
2838
|
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.",
|
|
2619
2839
|
tier: 3
|
|
2620
|
-
};
|
|
2840
|
+
});
|
|
2621
2841
|
}
|
|
2622
|
-
const
|
|
2623
|
-
|
|
2624
|
-
isTrustedHost2,
|
|
2625
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2626
|
-
);
|
|
2627
|
-
if (ptVerdict) return ptVerdict;
|
|
2842
|
+
const builtin = strictestVerdict(candidates);
|
|
2843
|
+
if (builtin) return builtin;
|
|
2628
2844
|
if (config.policy.egress?.enabled) {
|
|
2629
2845
|
const dests = extractShellDestinations(shellCommand);
|
|
2630
2846
|
if (dests.length > 0) {
|
|
@@ -3675,28 +3891,41 @@ function readShieldOverrides() {
|
|
|
3675
3891
|
var MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
3676
3892
|
var EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
3677
3893
|
function rankIn(order, value) {
|
|
3678
|
-
return order.indexOf(value);
|
|
3894
|
+
return value === void 0 ? -1 : order.indexOf(value);
|
|
3679
3895
|
}
|
|
3680
|
-
function
|
|
3681
|
-
if (rankIn(order, cloud) === -1) return local;
|
|
3682
|
-
if (locked) return cloud;
|
|
3896
|
+
function floorValue(order, local, cloud, opts = {}) {
|
|
3897
|
+
if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
|
|
3898
|
+
if (opts.locked) return cloud;
|
|
3899
|
+
const localSet = opts.localWasSet ?? local !== void 0;
|
|
3900
|
+
if (!localSet || rankIn(order, local) === -1) return cloud;
|
|
3683
3901
|
return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
|
|
3684
3902
|
}
|
|
3903
|
+
function strictestOf(order, ...values) {
|
|
3904
|
+
let best;
|
|
3905
|
+
for (const v of values) {
|
|
3906
|
+
if (rankIn(order, v) === -1) continue;
|
|
3907
|
+
if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
|
|
3908
|
+
}
|
|
3909
|
+
return best;
|
|
3910
|
+
}
|
|
3911
|
+
function resolveByOrder(order, local, cloud, locked) {
|
|
3912
|
+
return floorValue(order, local, cloud, { locked }) ?? local;
|
|
3913
|
+
}
|
|
3685
3914
|
function resolveManagedMode(local, cloud, locked) {
|
|
3686
3915
|
return resolveByOrder(MODE_ORDER, local, cloud, locked);
|
|
3687
3916
|
}
|
|
3688
|
-
function applyManagedEgress(local, managed, locked) {
|
|
3917
|
+
function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
3689
3918
|
const next = { ...local };
|
|
3690
3919
|
if (typeof managed.enabled === "boolean") {
|
|
3691
3920
|
next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3692
3921
|
}
|
|
3693
3922
|
if (typeof managed.mode === "string") {
|
|
3694
|
-
next.mode =
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
);
|
|
3923
|
+
next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
|
|
3924
|
+
locked: locked.includes("egressMode"),
|
|
3925
|
+
// The default 'review' is seeded into egress before any merge, so absence
|
|
3926
|
+
// is invisible from `local.mode` alone — the caller tracks it for us.
|
|
3927
|
+
localWasSet: localModeUserSet
|
|
3928
|
+
}) ?? local.mode;
|
|
3700
3929
|
}
|
|
3701
3930
|
if (Array.isArray(managed.allow) && managed.allow.length > 0) {
|
|
3702
3931
|
next.allow = [...managed.allow];
|
|
@@ -3716,6 +3945,9 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3716
3945
|
if (typeof managed.enabled === "boolean") {
|
|
3717
3946
|
next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3718
3947
|
}
|
|
3948
|
+
if (managed.enabled === true) {
|
|
3949
|
+
next.scanIgnoredTools = true;
|
|
3950
|
+
}
|
|
3719
3951
|
if (typeof managed.pii === "string") {
|
|
3720
3952
|
next.pii = resolveByOrder(
|
|
3721
3953
|
DLP_PII_ORDER,
|
|
@@ -3749,12 +3981,9 @@ function applyManagedCommandChecks(local, managed, locked) {
|
|
|
3749
3981
|
const m = managed[key];
|
|
3750
3982
|
if (typeof m !== "string") continue;
|
|
3751
3983
|
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3752
|
-
const resolved =
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
m,
|
|
3756
|
-
locked.includes(lockKey)
|
|
3757
|
-
);
|
|
3984
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
3985
|
+
locked: locked.includes(lockKey)
|
|
3986
|
+
});
|
|
3758
3987
|
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3759
3988
|
next[key] = resolved;
|
|
3760
3989
|
}
|
|
@@ -3780,11 +4009,18 @@ function slug(s) {
|
|
|
3780
4009
|
var B = "[\\s/\\\\]";
|
|
3781
4010
|
var SEP = "[/\\\\]";
|
|
3782
4011
|
function pathToRegexFragment(rawPath) {
|
|
3783
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
4012
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3784
4013
|
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
3785
4014
|
if (segments.length === 0) return "";
|
|
3786
4015
|
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
3787
4016
|
}
|
|
4017
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
4018
|
+
const value = pathToRegexFragment(rawPath);
|
|
4019
|
+
if (!value || !candidate) return false;
|
|
4020
|
+
const re = getCompiledRegex(value);
|
|
4021
|
+
if (!re) return false;
|
|
4022
|
+
return re.test(candidate);
|
|
4023
|
+
}
|
|
3788
4024
|
function pathRules(rawPath, verdict, reason) {
|
|
3789
4025
|
const value = pathToRegexFragment(rawPath);
|
|
3790
4026
|
if (!value) return [];
|
|
@@ -3798,12 +4034,28 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
3798
4034
|
verdict,
|
|
3799
4035
|
reason: why
|
|
3800
4036
|
},
|
|
4037
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
4038
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
3801
4039
|
{
|
|
3802
4040
|
name: `${verdict}-path-${s}-anytool`,
|
|
3803
4041
|
tool: "*",
|
|
3804
4042
|
conditions: [{ field: "file_path", op: "matches", value }],
|
|
3805
4043
|
verdict,
|
|
3806
4044
|
reason: why
|
|
4045
|
+
},
|
|
4046
|
+
{
|
|
4047
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
4048
|
+
tool: "*",
|
|
4049
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
4050
|
+
verdict,
|
|
4051
|
+
reason: why
|
|
4052
|
+
},
|
|
4053
|
+
{
|
|
4054
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
4055
|
+
tool: "*",
|
|
4056
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
4057
|
+
verdict,
|
|
4058
|
+
reason: why
|
|
3807
4059
|
}
|
|
3808
4060
|
];
|
|
3809
4061
|
}
|
|
@@ -3876,8 +4128,14 @@ var DEFAULT_CONFIG = {
|
|
|
3876
4128
|
settings: {
|
|
3877
4129
|
mode: "standard",
|
|
3878
4130
|
autoStartDaemon: true,
|
|
3879
|
-
|
|
3880
|
-
//
|
|
4131
|
+
// OFF by default. The snapshot store is a per-project bare git repo with
|
|
4132
|
+
// no size ceiling, and eviction drops the index row without deleting the
|
|
4133
|
+
// objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
|
|
4134
|
+
// from interrupted `git gc`) and filled the disk. A security tool must not
|
|
4135
|
+
// be what fills a customer's disk. Re-enable per install with
|
|
4136
|
+
// `{"settings":{"enableUndo":true}}`; the default flips back when the
|
|
4137
|
+
// bounded copy-store lands (doc/undo-v2-copy-store-design.md).
|
|
4138
|
+
enableUndo: false,
|
|
3881
4139
|
enableHookLogDebug: true,
|
|
3882
4140
|
approvalTimeoutMs: 12e4,
|
|
3883
4141
|
// 120-second auto-deny timeout
|
|
@@ -4076,10 +4334,13 @@ var DEFAULT_CONFIG = {
|
|
|
4076
4334
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
4077
4335
|
trustedHosts: [],
|
|
4078
4336
|
trustedHostsManaged: false,
|
|
4079
|
-
appPermissions: {}
|
|
4337
|
+
appPermissions: {},
|
|
4338
|
+
managedJailPaths: []
|
|
4080
4339
|
},
|
|
4081
4340
|
environments: {}
|
|
4082
4341
|
};
|
|
4342
|
+
var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
|
|
4343
|
+
var VERDICT_ORDER = ["allow", "review", "block"];
|
|
4083
4344
|
var ADVISORY_SMART_RULES = [
|
|
4084
4345
|
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
4085
4346
|
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
@@ -4091,12 +4352,7 @@ var ADVISORY_SMART_RULES = [
|
|
|
4091
4352
|
conditionMode: "all",
|
|
4092
4353
|
conditions: [
|
|
4093
4354
|
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
4094
|
-
{
|
|
4095
|
-
field: "command",
|
|
4096
|
-
op: "matches",
|
|
4097
|
-
// Matches known-safe build artifact paths in the command.
|
|
4098
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
4099
|
-
}
|
|
4355
|
+
{ field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
|
|
4100
4356
|
],
|
|
4101
4357
|
verdict: "allow",
|
|
4102
4358
|
reason: "Deleting a known-safe build artifact path"
|
|
@@ -4274,10 +4530,15 @@ function getConfig(cwd) {
|
|
|
4274
4530
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
4275
4531
|
trustedHosts: [],
|
|
4276
4532
|
trustedHostsManaged: false,
|
|
4277
|
-
appPermissions: {}
|
|
4533
|
+
appPermissions: {},
|
|
4534
|
+
managedJailPaths: []
|
|
4278
4535
|
};
|
|
4279
4536
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
4280
|
-
const
|
|
4537
|
+
const rank = (v) => {
|
|
4538
|
+
const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
|
|
4539
|
+
return i === -1 ? 1 : i;
|
|
4540
|
+
};
|
|
4541
|
+
const applyLayer = (source, isProject = false) => {
|
|
4281
4542
|
if (!source) return;
|
|
4282
4543
|
const s = source.settings || {};
|
|
4283
4544
|
const p = source.policy || {};
|
|
@@ -4337,7 +4598,9 @@ function getConfig(cwd) {
|
|
|
4337
4598
|
};
|
|
4338
4599
|
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4339
4600
|
const v = src[k];
|
|
4340
|
-
if (v
|
|
4601
|
+
if (v !== "off" && v !== "review" && v !== "block") continue;
|
|
4602
|
+
if (isProject && rank(v) < rank(cc2[k])) continue;
|
|
4603
|
+
cc2[k] = v;
|
|
4341
4604
|
}
|
|
4342
4605
|
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4343
4606
|
const v = src[k];
|
|
@@ -4347,11 +4610,22 @@ function getConfig(cwd) {
|
|
|
4347
4610
|
}
|
|
4348
4611
|
if (p.egress) {
|
|
4349
4612
|
const e = p.egress;
|
|
4350
|
-
if (e.enabled !== void 0
|
|
4351
|
-
|
|
4352
|
-
if (
|
|
4613
|
+
if (e.enabled !== void 0 && !(isProject && e.enabled === false))
|
|
4614
|
+
mergedPolicy.egress.enabled = e.enabled;
|
|
4615
|
+
if (e.mode !== void 0) {
|
|
4616
|
+
const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
|
|
4617
|
+
if (!weaker) {
|
|
4618
|
+
mergedPolicy.egress.mode = e.mode;
|
|
4619
|
+
egressModeUserSet = true;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
|
|
4623
|
+
mergedPolicy.egress.allow.push(...e.allow);
|
|
4624
|
+
}
|
|
4625
|
+
if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
|
|
4353
4626
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4354
|
-
if (e.allowPrivate !== void 0
|
|
4627
|
+
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4628
|
+
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
4355
4629
|
}
|
|
4356
4630
|
if (p.loopDetection) {
|
|
4357
4631
|
const ld = p.loopDetection;
|
|
@@ -4393,11 +4667,21 @@ function getConfig(cwd) {
|
|
|
4393
4667
|
}
|
|
4394
4668
|
}
|
|
4395
4669
|
};
|
|
4670
|
+
let egressModeUserSet = false;
|
|
4671
|
+
let egressAllowUserSet = false;
|
|
4396
4672
|
applyLayer(globalConfig);
|
|
4397
|
-
applyLayer(
|
|
4673
|
+
applyLayer(
|
|
4674
|
+
projectConfig,
|
|
4675
|
+
/* isProject */
|
|
4676
|
+
true
|
|
4677
|
+
);
|
|
4398
4678
|
let cloudManagedShields = [];
|
|
4679
|
+
const managedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4680
|
+
const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4399
4681
|
let modeCloudControlled = false;
|
|
4400
4682
|
let modeCloudStaged = false;
|
|
4683
|
+
let cloudMandatesEnforcement = false;
|
|
4684
|
+
let cloudMandatesAppPerm = false;
|
|
4401
4685
|
{
|
|
4402
4686
|
const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
|
|
4403
4687
|
try {
|
|
@@ -4432,7 +4716,8 @@ function getConfig(cwd) {
|
|
|
4432
4716
|
deny: hosts(mc.egress.deny),
|
|
4433
4717
|
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
|
|
4434
4718
|
},
|
|
4435
|
-
locked
|
|
4719
|
+
locked,
|
|
4720
|
+
egressModeUserSet
|
|
4436
4721
|
);
|
|
4437
4722
|
}
|
|
4438
4723
|
if (mc.dlp && typeof mc.dlp === "object") {
|
|
@@ -4452,6 +4737,12 @@ function getConfig(cwd) {
|
|
|
4452
4737
|
mc.commandChecks,
|
|
4453
4738
|
locked
|
|
4454
4739
|
);
|
|
4740
|
+
for (const [key, val] of Object.entries(mc.commandChecks)) {
|
|
4741
|
+
if (typeof val !== "string") continue;
|
|
4742
|
+
managedCommandCheckKeys.add(key);
|
|
4743
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4744
|
+
if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
|
|
4745
|
+
}
|
|
4455
4746
|
}
|
|
4456
4747
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4457
4748
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
@@ -4498,12 +4789,13 @@ function getConfig(cwd) {
|
|
|
4498
4789
|
}
|
|
4499
4790
|
if (Array.isArray(mc.jailPaths)) {
|
|
4500
4791
|
for (const jp of mc.jailPaths) {
|
|
4501
|
-
const
|
|
4502
|
-
if (!
|
|
4792
|
+
const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
4793
|
+
if (!path14) continue;
|
|
4503
4794
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4504
|
-
for (const r of pathRules(
|
|
4795
|
+
for (const r of pathRules(path14, verdict, "org-managed jail")) {
|
|
4505
4796
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4506
4797
|
}
|
|
4798
|
+
mergedPolicy.managedJailPaths.push({ path: path14, verdict });
|
|
4507
4799
|
}
|
|
4508
4800
|
}
|
|
4509
4801
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -4521,7 +4813,14 @@ function getConfig(cwd) {
|
|
|
4521
4813
|
if (Object.keys(m).length) coerced[srv] = m;
|
|
4522
4814
|
}
|
|
4523
4815
|
mergedPolicy.appPermissions = coerced;
|
|
4816
|
+
cloudMandatesAppPerm = Object.values(coerced).some(
|
|
4817
|
+
(tools) => Object.values(tools).some((d) => d === "block" || d === "review")
|
|
4818
|
+
);
|
|
4524
4819
|
}
|
|
4820
|
+
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
4821
|
+
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(
|
|
4822
|
+
(v) => typeof v === "string" && v !== "off"
|
|
4823
|
+
);
|
|
4525
4824
|
}
|
|
4526
4825
|
if (raw.panicMode === true) {
|
|
4527
4826
|
mergedSettings.panicMode = true;
|
|
@@ -4563,25 +4862,45 @@ function getConfig(cwd) {
|
|
|
4563
4862
|
}
|
|
4564
4863
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4565
4864
|
const cc = mergedPolicy.commandChecks ?? {};
|
|
4566
|
-
const
|
|
4567
|
-
if (name === "review-rm") return
|
|
4568
|
-
if (name?.endsWith("-sql")) return
|
|
4865
|
+
const advisoryKnobKey = (name) => {
|
|
4866
|
+
if (name === "review-rm") return "rmAdvisory";
|
|
4867
|
+
if (name?.endsWith("-sql")) return "sqlDdl";
|
|
4569
4868
|
return void 0;
|
|
4570
4869
|
};
|
|
4571
4870
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4572
|
-
|
|
4573
|
-
const knob =
|
|
4871
|
+
const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
|
|
4872
|
+
const knob = knobKey ? cc[knobKey] : void 0;
|
|
4574
4873
|
if (knob === "off") continue;
|
|
4575
|
-
|
|
4874
|
+
const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
|
|
4875
|
+
const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
|
|
4876
|
+
const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
|
|
4877
|
+
const knobVerdict = knob === "block" ? "block" : rule.verdict;
|
|
4878
|
+
if (!managed) {
|
|
4879
|
+
if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
|
|
4880
|
+
continue;
|
|
4881
|
+
}
|
|
4882
|
+
const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
|
|
4883
|
+
if (twin) {
|
|
4884
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4885
|
+
}
|
|
4886
|
+
const injected = { ...rule, verdict: effective, pinned: true };
|
|
4887
|
+
if (rule.name === "review-rm" && effective !== "block") {
|
|
4888
|
+
injected.conditions = [
|
|
4889
|
+
...rule.conditions ?? [],
|
|
4890
|
+
{ field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
|
|
4891
|
+
];
|
|
4892
|
+
injected.conditionMode = "all";
|
|
4893
|
+
}
|
|
4894
|
+
mergedPolicy.smartRules.push(injected);
|
|
4576
4895
|
}
|
|
4577
4896
|
const envMode = process.env.NODE9_MODE;
|
|
4578
4897
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4579
4898
|
mergedSettings.mode = envMode;
|
|
4580
4899
|
}
|
|
4581
|
-
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4900
|
+
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4582
4901
|
mergedSettings.mode = "standard";
|
|
4583
4902
|
}
|
|
4584
|
-
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4903
|
+
const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4585
4904
|
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4586
4905
|
for (const name of Object.keys(mergedEnvironments)) {
|
|
4587
4906
|
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
@@ -4782,14 +5101,14 @@ function checkProvenance(cmd, cwd) {
|
|
|
4782
5101
|
}
|
|
4783
5102
|
|
|
4784
5103
|
// src/policy/index.ts
|
|
4785
|
-
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
5104
|
+
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
4786
5105
|
const config = getConfig();
|
|
4787
5106
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
4788
5107
|
return evaluatePolicy(
|
|
4789
5108
|
config,
|
|
4790
5109
|
toolName,
|
|
4791
5110
|
args,
|
|
4792
|
-
{ agent, cwd, activeEnvironment },
|
|
5111
|
+
{ agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
|
|
4793
5112
|
{
|
|
4794
5113
|
checkProvenance,
|
|
4795
5114
|
// Managed → match against the org list (frozen with the rest of managed
|
|
@@ -5641,6 +5960,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
5641
5960
|
}
|
|
5642
5961
|
}
|
|
5643
5962
|
|
|
5963
|
+
// src/shields/jail.ts
|
|
5964
|
+
var import_fs11 = __toESM(require("fs"));
|
|
5965
|
+
var import_os10 = __toESM(require("os"));
|
|
5966
|
+
var import_path13 = __toESM(require("path"));
|
|
5967
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
5968
|
+
function jailStorePath() {
|
|
5969
|
+
return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
|
|
5970
|
+
}
|
|
5971
|
+
function readJailPaths() {
|
|
5972
|
+
let text;
|
|
5973
|
+
try {
|
|
5974
|
+
text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
|
|
5975
|
+
} catch (err) {
|
|
5976
|
+
if (err.code === "ENOENT") return [];
|
|
5977
|
+
throw err;
|
|
5978
|
+
}
|
|
5979
|
+
let parsed;
|
|
5980
|
+
try {
|
|
5981
|
+
parsed = JSON.parse(text);
|
|
5982
|
+
} catch {
|
|
5983
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
5984
|
+
}
|
|
5985
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
5986
|
+
return parsed.paths.filter(
|
|
5987
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
5988
|
+
);
|
|
5989
|
+
}
|
|
5990
|
+
function findJailedPath(candidate) {
|
|
5991
|
+
return findJailedPathIn(candidate, readJailPaths());
|
|
5992
|
+
}
|
|
5993
|
+
function findJailedPathIn(candidate, paths) {
|
|
5994
|
+
if (!candidate) return null;
|
|
5995
|
+
for (const entry of paths) {
|
|
5996
|
+
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
5997
|
+
}
|
|
5998
|
+
return null;
|
|
5999
|
+
}
|
|
6000
|
+
|
|
5644
6001
|
// src/auth/orchestrator.ts
|
|
5645
6002
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
5646
6003
|
"write",
|
|
@@ -6116,12 +6473,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6116
6473
|
} else if (!taintWarning && !appPermReview) {
|
|
6117
6474
|
const toolLower = toolName.toLowerCase();
|
|
6118
6475
|
const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
|
|
6119
|
-
|
|
6476
|
+
const activeShields = isFileTool ? readActiveShields() : [];
|
|
6477
|
+
const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
|
|
6478
|
+
if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
|
|
6120
6479
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6121
|
-
const
|
|
6122
|
-
|
|
6123
|
-
);
|
|
6124
|
-
if (
|
|
6480
|
+
const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
|
|
6481
|
+
const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
|
|
6482
|
+
const sensitiveHit = candidates.some((c) => scanFilePath(c));
|
|
6483
|
+
if (jailHit || sensitiveHit) {
|
|
6484
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
6485
|
+
skipIgnoredFastPath: true
|
|
6486
|
+
});
|
|
6487
|
+
if (policyResult.decision === "block") {
|
|
6488
|
+
if (!isManual)
|
|
6489
|
+
appendLocalAudit(
|
|
6490
|
+
toolName,
|
|
6491
|
+
args,
|
|
6492
|
+
"deny",
|
|
6493
|
+
"smart-rule-block",
|
|
6494
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
6495
|
+
hashAuditArgs
|
|
6496
|
+
);
|
|
6497
|
+
return {
|
|
6498
|
+
approved: false,
|
|
6499
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
6500
|
+
blockedBy: "local-config",
|
|
6501
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
6502
|
+
ruleHit: policyResult.ruleName
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6125
6505
|
} else {
|
|
6126
6506
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
6127
6507
|
return { approved: true };
|
|
@@ -6184,7 +6564,12 @@ ${appPermReview}`
|
|
|
6184
6564
|
}
|
|
6185
6565
|
let cloudRequestId = null;
|
|
6186
6566
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
6187
|
-
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview ||
|
|
6567
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
|
|
6568
|
+
// a client-side heuristic the SaaS has no rule for, so without forceReview
|
|
6569
|
+
// its checkRule answers "no org rule matched" → {approved:true}, which is
|
|
6570
|
+
// not an approval of an exfiltration risk. Measured against the live BE:
|
|
6571
|
+
// {approved:true} without this flag, {pending:true} with it.
|
|
6572
|
+
!!taintWarning || void 0;
|
|
6188
6573
|
if (cloudEnforced) {
|
|
6189
6574
|
try {
|
|
6190
6575
|
const initResult = await initNode9SaaS(
|
|
@@ -6197,10 +6582,10 @@ ${appPermReview}`
|
|
|
6197
6582
|
forceReview
|
|
6198
6583
|
);
|
|
6199
6584
|
if (!initResult.pending) {
|
|
6200
|
-
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6585
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6201
6586
|
return { approved: true, checkedBy: "cloud" };
|
|
6202
6587
|
}
|
|
6203
|
-
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6588
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6204
6589
|
return {
|
|
6205
6590
|
approved: !!initResult.approved,
|
|
6206
6591
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|