@node9/proxy 1.67.1 → 1.67.3
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 +1776 -1334
- package/dist/cli.mjs +1743 -1301
- package/dist/dashboard.mjs +246 -133
- package/dist/index.js +676 -239
- package/dist/index.mjs +676 -239
- package/dist/scan-ink.mjs +42 -0
- 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) {
|
|
@@ -1239,9 +1371,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1239
1371
|
"vi",
|
|
1240
1372
|
"emacs",
|
|
1241
1373
|
"code",
|
|
1242
|
-
"type"
|
|
1374
|
+
"type",
|
|
1375
|
+
// — the 22 that were missing —
|
|
1376
|
+
"grep",
|
|
1377
|
+
"egrep",
|
|
1378
|
+
"fgrep",
|
|
1379
|
+
"rg",
|
|
1380
|
+
"ag",
|
|
1381
|
+
"ack",
|
|
1382
|
+
"awk",
|
|
1383
|
+
"gawk",
|
|
1384
|
+
"sed",
|
|
1385
|
+
"cut",
|
|
1386
|
+
"tr",
|
|
1387
|
+
"jq",
|
|
1388
|
+
"yq",
|
|
1389
|
+
"od",
|
|
1390
|
+
"xxd",
|
|
1391
|
+
"hexdump",
|
|
1392
|
+
"strings",
|
|
1393
|
+
"sort",
|
|
1394
|
+
"uniq",
|
|
1395
|
+
"tac",
|
|
1396
|
+
"nl",
|
|
1397
|
+
"dd"
|
|
1243
1398
|
]);
|
|
1244
|
-
var FS_OP_PRESCREEN_RE =
|
|
1399
|
+
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1400
|
+
`(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
|
|
1401
|
+
);
|
|
1245
1402
|
var HOME_CACHE_ALLOWLIST = [
|
|
1246
1403
|
".cache",
|
|
1247
1404
|
".npm/_npx",
|
|
@@ -1280,9 +1437,37 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1280
1437
|
// for the canonical test-asserted contract.
|
|
1281
1438
|
rule: "shield:project-jail:block-read-env",
|
|
1282
1439
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1440
|
+
// Structural, not a list. The previous form enumerated seven suffixes and
|
|
1441
|
+
// anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
|
|
1442
|
+
// gitignored, all routinely holding real secrets — were never covered. A
|
|
1443
|
+
// hand-written list of what to protect is only ever as complete as the day
|
|
1444
|
+
// it was typed; this says "`.env` plus any suffix chain" and then names the
|
|
1445
|
+
// exceptions, which is the direction that fails safe.
|
|
1446
|
+
//
|
|
1447
|
+
// \.env the segment itself
|
|
1448
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1449
|
+
// files. Without it a flat suffix class swallows both.
|
|
1450
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1451
|
+
// [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
|
|
1452
|
+
// `(\.[\w-]+)*` reads the same but is rejected by
|
|
1453
|
+
// safe-regex2, and this pattern runs on the hook hot path.
|
|
1454
|
+
//
|
|
1455
|
+
// The two exclusions are NOT the same shape, because the words do not mean
|
|
1456
|
+
// the same thing:
|
|
1457
|
+
//
|
|
1458
|
+
// (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
|
|
1459
|
+
// stays a fixture whatever follows, so `.env.example.md` is allowed too.
|
|
1460
|
+
// These are checked into git by convention: already public, so blocking
|
|
1461
|
+
// them buys nothing and costs the most common legitimate agent read.
|
|
1462
|
+
//
|
|
1463
|
+
// (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
|
|
1464
|
+
// fixture. `.env.test` is the committed template and stays allowed, but
|
|
1465
|
+
// `.env.test.local` is gitignored by the `.env*.local` convention and
|
|
1466
|
+
// holds real values, so it must block. Using `\b` here — the obvious
|
|
1467
|
+
// symmetry — silently exempts every `.env.test.*` file.
|
|
1468
|
+
//
|
|
1469
|
+
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1470
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
|
|
1286
1471
|
},
|
|
1287
1472
|
{
|
|
1288
1473
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1411,6 +1596,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1411
1596
|
}
|
|
1412
1597
|
return found;
|
|
1413
1598
|
}
|
|
1599
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1600
|
+
if (isBashTool(toolName)) return true;
|
|
1601
|
+
if (!toolInspection) return false;
|
|
1602
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1603
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1604
|
+
}
|
|
1605
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1606
|
+
if (!ruleTool) return true;
|
|
1607
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1608
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1609
|
+
}
|
|
1610
|
+
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;
|
|
1611
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1612
|
+
"uv",
|
|
1613
|
+
"uvx",
|
|
1614
|
+
"poetry",
|
|
1615
|
+
"pipenv",
|
|
1616
|
+
"pdm",
|
|
1617
|
+
"rye",
|
|
1618
|
+
"hatch",
|
|
1619
|
+
"conda",
|
|
1620
|
+
"mamba",
|
|
1621
|
+
"micromamba",
|
|
1622
|
+
"npx",
|
|
1623
|
+
"pnpm",
|
|
1624
|
+
"yarn",
|
|
1625
|
+
"bunx",
|
|
1626
|
+
"watch",
|
|
1627
|
+
"strace",
|
|
1628
|
+
"ltrace",
|
|
1629
|
+
"chroot",
|
|
1630
|
+
"unshare",
|
|
1631
|
+
"runuser"
|
|
1632
|
+
]);
|
|
1633
|
+
function isInlineCodeFlag(interp, w) {
|
|
1634
|
+
const lw = w.toLowerCase();
|
|
1635
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1636
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1637
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1638
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1639
|
+
const body = lw.slice(1);
|
|
1640
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1641
|
+
const cut = body.search(cutAt);
|
|
1642
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1643
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1644
|
+
}
|
|
1645
|
+
var _redirStdinOps = null;
|
|
1646
|
+
function redirStdinOps() {
|
|
1647
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1648
|
+
_redirStdinOps = new Set(
|
|
1649
|
+
[
|
|
1650
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1651
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1652
|
+
deriveRedirOp("cat < f"),
|
|
1653
|
+
deriveRedirOp("cat <<< x")
|
|
1654
|
+
].filter((op) => op >= 0)
|
|
1655
|
+
);
|
|
1656
|
+
return _redirStdinOps;
|
|
1657
|
+
}
|
|
1658
|
+
function deriveBinaryOp(sample) {
|
|
1659
|
+
try {
|
|
1660
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1661
|
+
let op = -1;
|
|
1662
|
+
syntax.Walk(f, (node) => {
|
|
1663
|
+
const n = node;
|
|
1664
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1665
|
+
return true;
|
|
1666
|
+
});
|
|
1667
|
+
return op;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return -1;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
var _listOps = null;
|
|
1673
|
+
function listOps() {
|
|
1674
|
+
if (_listOps) return _listOps;
|
|
1675
|
+
_listOps = new Set(
|
|
1676
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1677
|
+
);
|
|
1678
|
+
return _listOps;
|
|
1679
|
+
}
|
|
1680
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1681
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1682
|
+
function unwrapCommandHead(words) {
|
|
1683
|
+
let i = 0;
|
|
1684
|
+
while (i < words.length) {
|
|
1685
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1686
|
+
if (head === "find") {
|
|
1687
|
+
const x = words.findIndex(
|
|
1688
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1689
|
+
);
|
|
1690
|
+
if (x < 0) break;
|
|
1691
|
+
i = x + 1;
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1695
|
+
i++;
|
|
1696
|
+
let targetConsumed = false;
|
|
1697
|
+
while (i < words.length) {
|
|
1698
|
+
const t = words[i];
|
|
1699
|
+
if (t === null) {
|
|
1700
|
+
i++;
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
const lt = t.toLowerCase();
|
|
1704
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1705
|
+
i++;
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
if (t.startsWith("-")) {
|
|
1709
|
+
i++;
|
|
1710
|
+
const nxt = words[i];
|
|
1711
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1712
|
+
i++;
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1716
|
+
i++;
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1720
|
+
targetConsumed = true;
|
|
1721
|
+
i++;
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return i;
|
|
1728
|
+
}
|
|
1729
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1730
|
+
const cmd = stmt?.Cmd;
|
|
1731
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1732
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1733
|
+
if (words.length === 0) return false;
|
|
1734
|
+
const headIdx = unwrapCommandHead(words);
|
|
1735
|
+
const rawHead = words[headIdx];
|
|
1736
|
+
if (rawHead == null) return false;
|
|
1737
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1738
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1739
|
+
let args = words.slice(headIdx + 1);
|
|
1740
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1741
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1742
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1743
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1744
|
+
}
|
|
1745
|
+
let positionals = 0;
|
|
1746
|
+
let selectedProgram = false;
|
|
1747
|
+
for (const a of args) {
|
|
1748
|
+
if (a == null) {
|
|
1749
|
+
positionals++;
|
|
1750
|
+
selectedProgram = true;
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1754
|
+
if (a === "-m") {
|
|
1755
|
+
selectedProgram = true;
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1759
|
+
if (!a.startsWith("-")) {
|
|
1760
|
+
positionals++;
|
|
1761
|
+
selectedProgram = true;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1765
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1766
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1767
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1768
|
+
}
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
function detectInlineExec(command) {
|
|
1772
|
+
const f = parseShared(command);
|
|
1773
|
+
if (f === PARSE_FAIL) {
|
|
1774
|
+
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(
|
|
1775
|
+
command
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
let found = false;
|
|
1779
|
+
try {
|
|
1780
|
+
syntax.Walk(f, (node) => {
|
|
1781
|
+
if (!node || found) return false;
|
|
1782
|
+
const n = node;
|
|
1783
|
+
const t = syntax.NodeType(n);
|
|
1784
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1785
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1786
|
+
found = true;
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1791
|
+
found = true;
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
return true;
|
|
1795
|
+
});
|
|
1796
|
+
} catch {
|
|
1797
|
+
return found;
|
|
1798
|
+
}
|
|
1799
|
+
return found;
|
|
1800
|
+
}
|
|
1414
1801
|
function analyzeChmod777(command) {
|
|
1415
1802
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1416
1803
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2275,150 +2662,12 @@ function extractAllSshHosts(tokens) {
|
|
|
2275
2662
|
}
|
|
2276
2663
|
return [...hosts].filter(Boolean);
|
|
2277
2664
|
}
|
|
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
2665
|
function resolveCheck(v) {
|
|
2382
2666
|
return v === "off" || v === "block" ? v : "review";
|
|
2383
2667
|
}
|
|
2384
2668
|
function resolveCheckTight(v) {
|
|
2385
2669
|
return v === "block" ? "block" : "review";
|
|
2386
2670
|
}
|
|
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
2671
|
var VERDICT_RANK = {
|
|
2423
2672
|
allow: 0,
|
|
2424
2673
|
review: 1,
|
|
@@ -2432,6 +2681,12 @@ function resolvePinned(matches) {
|
|
|
2432
2681
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2433
2682
|
);
|
|
2434
2683
|
}
|
|
2684
|
+
function strictestVerdict(candidates) {
|
|
2685
|
+
if (candidates.length === 0) return void 0;
|
|
2686
|
+
return candidates.reduce(
|
|
2687
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2435
2690
|
function tokenize2(toolName) {
|
|
2436
2691
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2437
2692
|
}
|
|
@@ -2443,6 +2698,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2443
2698
|
const value = getNestedValue(args, fieldPath);
|
|
2444
2699
|
return typeof value === "string" ? value : null;
|
|
2445
2700
|
}
|
|
2701
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2702
|
+
const patterns = Object.keys(toolInspection);
|
|
2703
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2704
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2705
|
+
}
|
|
2446
2706
|
function isSqlTool(toolName, toolInspection) {
|
|
2447
2707
|
const patterns = Object.keys(toolInspection);
|
|
2448
2708
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2505,8 +2765,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2505
2765
|
};
|
|
2506
2766
|
}
|
|
2507
2767
|
}
|
|
2508
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2509
|
-
const
|
|
2768
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2769
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2770
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2771
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2510
2772
|
if (bashCommand !== null) {
|
|
2511
2773
|
const pipeVerdict = pipeChainVerdict(
|
|
2512
2774
|
bashCommand,
|
|
@@ -2556,8 +2818,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2556
2818
|
}
|
|
2557
2819
|
if (config.policy.smartRules.length > 0) {
|
|
2558
2820
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2821
|
+
const astSuppressed = (rule) => {
|
|
2822
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2823
|
+
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;
|
|
2824
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2825
|
+
return true;
|
|
2826
|
+
};
|
|
2559
2827
|
const matches = config.policy.smartRules.filter(
|
|
2560
|
-
(rule) =>
|
|
2828
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2561
2829
|
);
|
|
2562
2830
|
const matchedRule = resolvePinned(matches);
|
|
2563
2831
|
if (matchedRule) {
|
|
@@ -2588,15 +2856,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2588
2856
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2589
2857
|
allTokens = analyzed.allTokens;
|
|
2590
2858
|
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
|
-
}
|
|
2859
|
+
const candidates = [];
|
|
2600
2860
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2601
2861
|
if (evalVerdict === "block") {
|
|
2602
2862
|
return {
|
|
@@ -2607,24 +2867,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2607
2867
|
tier: 3
|
|
2608
2868
|
};
|
|
2609
2869
|
}
|
|
2870
|
+
const ptVerdict = pipeChainVerdict(
|
|
2871
|
+
shellCommand,
|
|
2872
|
+
isTrustedHost2,
|
|
2873
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2874
|
+
);
|
|
2875
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2876
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2877
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2878
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2879
|
+
candidates.push({
|
|
2880
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2881
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2882
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2883
|
+
tier: 3
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2610
2886
|
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).
|
|
2887
|
+
candidates.push({
|
|
2615
2888
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2616
2889
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2617
2890
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2618
2891
|
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
2892
|
tier: 3
|
|
2620
|
-
};
|
|
2893
|
+
});
|
|
2621
2894
|
}
|
|
2622
|
-
const
|
|
2623
|
-
|
|
2624
|
-
isTrustedHost2,
|
|
2625
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2626
|
-
);
|
|
2627
|
-
if (ptVerdict) return ptVerdict;
|
|
2895
|
+
const builtin = strictestVerdict(candidates);
|
|
2896
|
+
if (builtin) return builtin;
|
|
2628
2897
|
if (config.policy.egress?.enabled) {
|
|
2629
2898
|
const dests = extractShellDestinations(shellCommand);
|
|
2630
2899
|
if (dests.length > 0) {
|
|
@@ -3675,28 +3944,41 @@ function readShieldOverrides() {
|
|
|
3675
3944
|
var MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
3676
3945
|
var EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
3677
3946
|
function rankIn(order, value) {
|
|
3678
|
-
return order.indexOf(value);
|
|
3947
|
+
return value === void 0 ? -1 : order.indexOf(value);
|
|
3679
3948
|
}
|
|
3680
|
-
function
|
|
3681
|
-
if (rankIn(order, cloud) === -1) return local;
|
|
3682
|
-
if (locked) return cloud;
|
|
3949
|
+
function floorValue(order, local, cloud, opts = {}) {
|
|
3950
|
+
if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
|
|
3951
|
+
if (opts.locked) return cloud;
|
|
3952
|
+
const localSet = opts.localWasSet ?? local !== void 0;
|
|
3953
|
+
if (!localSet || rankIn(order, local) === -1) return cloud;
|
|
3683
3954
|
return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
|
|
3684
3955
|
}
|
|
3956
|
+
function strictestOf(order, ...values) {
|
|
3957
|
+
let best;
|
|
3958
|
+
for (const v of values) {
|
|
3959
|
+
if (rankIn(order, v) === -1) continue;
|
|
3960
|
+
if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
|
|
3961
|
+
}
|
|
3962
|
+
return best;
|
|
3963
|
+
}
|
|
3964
|
+
function resolveByOrder(order, local, cloud, locked) {
|
|
3965
|
+
return floorValue(order, local, cloud, { locked }) ?? local;
|
|
3966
|
+
}
|
|
3685
3967
|
function resolveManagedMode(local, cloud, locked) {
|
|
3686
3968
|
return resolveByOrder(MODE_ORDER, local, cloud, locked);
|
|
3687
3969
|
}
|
|
3688
|
-
function applyManagedEgress(local, managed, locked) {
|
|
3970
|
+
function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
3689
3971
|
const next = { ...local };
|
|
3690
3972
|
if (typeof managed.enabled === "boolean") {
|
|
3691
3973
|
next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3692
3974
|
}
|
|
3693
3975
|
if (typeof managed.mode === "string") {
|
|
3694
|
-
next.mode =
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
);
|
|
3976
|
+
next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
|
|
3977
|
+
locked: locked.includes("egressMode"),
|
|
3978
|
+
// The default 'review' is seeded into egress before any merge, so absence
|
|
3979
|
+
// is invisible from `local.mode` alone — the caller tracks it for us.
|
|
3980
|
+
localWasSet: localModeUserSet
|
|
3981
|
+
}) ?? local.mode;
|
|
3700
3982
|
}
|
|
3701
3983
|
if (Array.isArray(managed.allow) && managed.allow.length > 0) {
|
|
3702
3984
|
next.allow = [...managed.allow];
|
|
@@ -3716,6 +3998,9 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3716
3998
|
if (typeof managed.enabled === "boolean") {
|
|
3717
3999
|
next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3718
4000
|
}
|
|
4001
|
+
if (managed.enabled === true) {
|
|
4002
|
+
next.scanIgnoredTools = true;
|
|
4003
|
+
}
|
|
3719
4004
|
if (typeof managed.pii === "string") {
|
|
3720
4005
|
next.pii = resolveByOrder(
|
|
3721
4006
|
DLP_PII_ORDER,
|
|
@@ -3749,13 +4034,9 @@ function applyManagedCommandChecks(local, managed, locked) {
|
|
|
3749
4034
|
const m = managed[key];
|
|
3750
4035
|
if (typeof m !== "string") continue;
|
|
3751
4036
|
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
localValue,
|
|
3756
|
-
m,
|
|
3757
|
-
locked.includes(lockKey)
|
|
3758
|
-
);
|
|
4037
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
4038
|
+
locked: locked.includes(lockKey)
|
|
4039
|
+
});
|
|
3759
4040
|
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3760
4041
|
next[key] = resolved;
|
|
3761
4042
|
}
|
|
@@ -3781,11 +4062,18 @@ function slug(s) {
|
|
|
3781
4062
|
var B = "[\\s/\\\\]";
|
|
3782
4063
|
var SEP = "[/\\\\]";
|
|
3783
4064
|
function pathToRegexFragment(rawPath) {
|
|
3784
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
4065
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3785
4066
|
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
3786
4067
|
if (segments.length === 0) return "";
|
|
3787
4068
|
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
3788
4069
|
}
|
|
4070
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
4071
|
+
const value = pathToRegexFragment(rawPath);
|
|
4072
|
+
if (!value || !candidate) return false;
|
|
4073
|
+
const re = getCompiledRegex(value);
|
|
4074
|
+
if (!re) return false;
|
|
4075
|
+
return re.test(candidate);
|
|
4076
|
+
}
|
|
3789
4077
|
function pathRules(rawPath, verdict, reason) {
|
|
3790
4078
|
const value = pathToRegexFragment(rawPath);
|
|
3791
4079
|
if (!value) return [];
|
|
@@ -3799,12 +4087,28 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
3799
4087
|
verdict,
|
|
3800
4088
|
reason: why
|
|
3801
4089
|
},
|
|
4090
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
4091
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
3802
4092
|
{
|
|
3803
4093
|
name: `${verdict}-path-${s}-anytool`,
|
|
3804
4094
|
tool: "*",
|
|
3805
4095
|
conditions: [{ field: "file_path", op: "matches", value }],
|
|
3806
4096
|
verdict,
|
|
3807
4097
|
reason: why
|
|
4098
|
+
},
|
|
4099
|
+
{
|
|
4100
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
4101
|
+
tool: "*",
|
|
4102
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
4103
|
+
verdict,
|
|
4104
|
+
reason: why
|
|
4105
|
+
},
|
|
4106
|
+
{
|
|
4107
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
4108
|
+
tool: "*",
|
|
4109
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
4110
|
+
verdict,
|
|
4111
|
+
reason: why
|
|
3808
4112
|
}
|
|
3809
4113
|
];
|
|
3810
4114
|
}
|
|
@@ -3877,8 +4181,14 @@ var DEFAULT_CONFIG = {
|
|
|
3877
4181
|
settings: {
|
|
3878
4182
|
mode: "standard",
|
|
3879
4183
|
autoStartDaemon: true,
|
|
3880
|
-
|
|
3881
|
-
//
|
|
4184
|
+
// OFF by default. The snapshot store is a per-project bare git repo with
|
|
4185
|
+
// no size ceiling, and eviction drops the index row without deleting the
|
|
4186
|
+
// objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
|
|
4187
|
+
// from interrupted `git gc`) and filled the disk. A security tool must not
|
|
4188
|
+
// be what fills a customer's disk. Re-enable per install with
|
|
4189
|
+
// `{"settings":{"enableUndo":true}}`; the default flips back when the
|
|
4190
|
+
// bounded copy-store lands (doc/undo-v2-copy-store-design.md).
|
|
4191
|
+
enableUndo: false,
|
|
3882
4192
|
enableHookLogDebug: true,
|
|
3883
4193
|
approvalTimeoutMs: 12e4,
|
|
3884
4194
|
// 120-second auto-deny timeout
|
|
@@ -4077,10 +4387,13 @@ var DEFAULT_CONFIG = {
|
|
|
4077
4387
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
4078
4388
|
trustedHosts: [],
|
|
4079
4389
|
trustedHostsManaged: false,
|
|
4080
|
-
appPermissions: {}
|
|
4390
|
+
appPermissions: {},
|
|
4391
|
+
managedJailPaths: []
|
|
4081
4392
|
},
|
|
4082
4393
|
environments: {}
|
|
4083
4394
|
};
|
|
4395
|
+
var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
|
|
4396
|
+
var VERDICT_ORDER = ["allow", "review", "block"];
|
|
4084
4397
|
var ADVISORY_SMART_RULES = [
|
|
4085
4398
|
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
4086
4399
|
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
@@ -4092,12 +4405,7 @@ var ADVISORY_SMART_RULES = [
|
|
|
4092
4405
|
conditionMode: "all",
|
|
4093
4406
|
conditions: [
|
|
4094
4407
|
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
4095
|
-
{
|
|
4096
|
-
field: "command",
|
|
4097
|
-
op: "matches",
|
|
4098
|
-
// Matches known-safe build artifact paths in the command.
|
|
4099
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
4100
|
-
}
|
|
4408
|
+
{ field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
|
|
4101
4409
|
],
|
|
4102
4410
|
verdict: "allow",
|
|
4103
4411
|
reason: "Deleting a known-safe build artifact path"
|
|
@@ -4275,10 +4583,15 @@ function getConfig(cwd) {
|
|
|
4275
4583
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
4276
4584
|
trustedHosts: [],
|
|
4277
4585
|
trustedHostsManaged: false,
|
|
4278
|
-
appPermissions: {}
|
|
4586
|
+
appPermissions: {},
|
|
4587
|
+
managedJailPaths: []
|
|
4279
4588
|
};
|
|
4280
4589
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
4281
|
-
const
|
|
4590
|
+
const rank = (v) => {
|
|
4591
|
+
const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
|
|
4592
|
+
return i === -1 ? 1 : i;
|
|
4593
|
+
};
|
|
4594
|
+
const applyLayer = (source, isProject = false) => {
|
|
4282
4595
|
if (!source) return;
|
|
4283
4596
|
const s = source.settings || {};
|
|
4284
4597
|
const p = source.policy || {};
|
|
@@ -4338,7 +4651,9 @@ function getConfig(cwd) {
|
|
|
4338
4651
|
};
|
|
4339
4652
|
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4340
4653
|
const v = src[k];
|
|
4341
|
-
if (v
|
|
4654
|
+
if (v !== "off" && v !== "review" && v !== "block") continue;
|
|
4655
|
+
if (isProject && rank(v) < rank(cc2[k])) continue;
|
|
4656
|
+
cc2[k] = v;
|
|
4342
4657
|
}
|
|
4343
4658
|
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4344
4659
|
const v = src[k];
|
|
@@ -4348,11 +4663,22 @@ function getConfig(cwd) {
|
|
|
4348
4663
|
}
|
|
4349
4664
|
if (p.egress) {
|
|
4350
4665
|
const e = p.egress;
|
|
4351
|
-
if (e.enabled !== void 0
|
|
4352
|
-
|
|
4353
|
-
if (
|
|
4666
|
+
if (e.enabled !== void 0 && !(isProject && e.enabled === false))
|
|
4667
|
+
mergedPolicy.egress.enabled = e.enabled;
|
|
4668
|
+
if (e.mode !== void 0) {
|
|
4669
|
+
const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
|
|
4670
|
+
if (!weaker) {
|
|
4671
|
+
mergedPolicy.egress.mode = e.mode;
|
|
4672
|
+
egressModeUserSet = true;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
|
|
4676
|
+
mergedPolicy.egress.allow.push(...e.allow);
|
|
4677
|
+
}
|
|
4678
|
+
if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
|
|
4354
4679
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4355
|
-
if (e.allowPrivate !== void 0
|
|
4680
|
+
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4681
|
+
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
4356
4682
|
}
|
|
4357
4683
|
if (p.loopDetection) {
|
|
4358
4684
|
const ld = p.loopDetection;
|
|
@@ -4394,11 +4720,21 @@ function getConfig(cwd) {
|
|
|
4394
4720
|
}
|
|
4395
4721
|
}
|
|
4396
4722
|
};
|
|
4723
|
+
let egressModeUserSet = false;
|
|
4724
|
+
let egressAllowUserSet = false;
|
|
4397
4725
|
applyLayer(globalConfig);
|
|
4398
|
-
applyLayer(
|
|
4726
|
+
applyLayer(
|
|
4727
|
+
projectConfig,
|
|
4728
|
+
/* isProject */
|
|
4729
|
+
true
|
|
4730
|
+
);
|
|
4399
4731
|
let cloudManagedShields = [];
|
|
4732
|
+
const managedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4733
|
+
const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4400
4734
|
let modeCloudControlled = false;
|
|
4401
4735
|
let modeCloudStaged = false;
|
|
4736
|
+
let cloudMandatesEnforcement = false;
|
|
4737
|
+
let cloudMandatesAppPerm = false;
|
|
4402
4738
|
{
|
|
4403
4739
|
const cacheFile = import_path4.default.join(import_os4.default.homedir(), ".node9", "rules-cache.json");
|
|
4404
4740
|
try {
|
|
@@ -4433,7 +4769,8 @@ function getConfig(cwd) {
|
|
|
4433
4769
|
deny: hosts(mc.egress.deny),
|
|
4434
4770
|
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
|
|
4435
4771
|
},
|
|
4436
|
-
locked
|
|
4772
|
+
locked,
|
|
4773
|
+
egressModeUserSet
|
|
4437
4774
|
);
|
|
4438
4775
|
}
|
|
4439
4776
|
if (mc.dlp && typeof mc.dlp === "object") {
|
|
@@ -4453,6 +4790,12 @@ function getConfig(cwd) {
|
|
|
4453
4790
|
mc.commandChecks,
|
|
4454
4791
|
locked
|
|
4455
4792
|
);
|
|
4793
|
+
for (const [key, val] of Object.entries(mc.commandChecks)) {
|
|
4794
|
+
if (typeof val !== "string") continue;
|
|
4795
|
+
managedCommandCheckKeys.add(key);
|
|
4796
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4797
|
+
if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
|
|
4798
|
+
}
|
|
4456
4799
|
}
|
|
4457
4800
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4458
4801
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
@@ -4499,12 +4842,13 @@ function getConfig(cwd) {
|
|
|
4499
4842
|
}
|
|
4500
4843
|
if (Array.isArray(mc.jailPaths)) {
|
|
4501
4844
|
for (const jp of mc.jailPaths) {
|
|
4502
|
-
const
|
|
4503
|
-
if (!
|
|
4845
|
+
const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
4846
|
+
if (!path14) continue;
|
|
4504
4847
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4505
|
-
for (const r of pathRules(
|
|
4848
|
+
for (const r of pathRules(path14, verdict, "org-managed jail")) {
|
|
4506
4849
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4507
4850
|
}
|
|
4851
|
+
mergedPolicy.managedJailPaths.push({ path: path14, verdict });
|
|
4508
4852
|
}
|
|
4509
4853
|
}
|
|
4510
4854
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -4522,7 +4866,14 @@ function getConfig(cwd) {
|
|
|
4522
4866
|
if (Object.keys(m).length) coerced[srv] = m;
|
|
4523
4867
|
}
|
|
4524
4868
|
mergedPolicy.appPermissions = coerced;
|
|
4869
|
+
cloudMandatesAppPerm = Object.values(coerced).some(
|
|
4870
|
+
(tools) => Object.values(tools).some((d) => d === "block" || d === "review")
|
|
4871
|
+
);
|
|
4525
4872
|
}
|
|
4873
|
+
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
4874
|
+
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(
|
|
4875
|
+
(v) => typeof v === "string" && v !== "off"
|
|
4876
|
+
);
|
|
4526
4877
|
}
|
|
4527
4878
|
if (raw.panicMode === true) {
|
|
4528
4879
|
mergedSettings.panicMode = true;
|
|
@@ -4564,25 +4915,45 @@ function getConfig(cwd) {
|
|
|
4564
4915
|
}
|
|
4565
4916
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4566
4917
|
const cc = mergedPolicy.commandChecks ?? {};
|
|
4567
|
-
const
|
|
4568
|
-
if (name === "review-rm") return
|
|
4569
|
-
if (name?.endsWith("-sql")) return
|
|
4918
|
+
const advisoryKnobKey = (name) => {
|
|
4919
|
+
if (name === "review-rm") return "rmAdvisory";
|
|
4920
|
+
if (name?.endsWith("-sql")) return "sqlDdl";
|
|
4570
4921
|
return void 0;
|
|
4571
4922
|
};
|
|
4572
4923
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4573
|
-
|
|
4574
|
-
const knob =
|
|
4924
|
+
const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
|
|
4925
|
+
const knob = knobKey ? cc[knobKey] : void 0;
|
|
4575
4926
|
if (knob === "off") continue;
|
|
4576
|
-
|
|
4927
|
+
const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
|
|
4928
|
+
const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
|
|
4929
|
+
const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
|
|
4930
|
+
const knobVerdict = knob === "block" ? "block" : rule.verdict;
|
|
4931
|
+
if (!managed) {
|
|
4932
|
+
if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
|
|
4933
|
+
continue;
|
|
4934
|
+
}
|
|
4935
|
+
const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
|
|
4936
|
+
if (twin) {
|
|
4937
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4938
|
+
}
|
|
4939
|
+
const injected = { ...rule, verdict: effective, pinned: true };
|
|
4940
|
+
if (rule.name === "review-rm" && effective !== "block") {
|
|
4941
|
+
injected.conditions = [
|
|
4942
|
+
...rule.conditions ?? [],
|
|
4943
|
+
{ field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
|
|
4944
|
+
];
|
|
4945
|
+
injected.conditionMode = "all";
|
|
4946
|
+
}
|
|
4947
|
+
mergedPolicy.smartRules.push(injected);
|
|
4577
4948
|
}
|
|
4578
4949
|
const envMode = process.env.NODE9_MODE;
|
|
4579
4950
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4580
4951
|
mergedSettings.mode = envMode;
|
|
4581
4952
|
}
|
|
4582
|
-
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4953
|
+
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4583
4954
|
mergedSettings.mode = "standard";
|
|
4584
4955
|
}
|
|
4585
|
-
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4956
|
+
const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4586
4957
|
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4587
4958
|
for (const name of Object.keys(mergedEnvironments)) {
|
|
4588
4959
|
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
@@ -4783,14 +5154,14 @@ function checkProvenance(cmd, cwd) {
|
|
|
4783
5154
|
}
|
|
4784
5155
|
|
|
4785
5156
|
// src/policy/index.ts
|
|
4786
|
-
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
5157
|
+
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
4787
5158
|
const config = getConfig();
|
|
4788
5159
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
4789
5160
|
return evaluatePolicy(
|
|
4790
5161
|
config,
|
|
4791
5162
|
toolName,
|
|
4792
5163
|
args,
|
|
4793
|
-
{ agent, cwd, activeEnvironment },
|
|
5164
|
+
{ agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
|
|
4794
5165
|
{
|
|
4795
5166
|
checkProvenance,
|
|
4796
5167
|
// Managed → match against the org list (frozen with the rest of managed
|
|
@@ -5642,6 +6013,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
5642
6013
|
}
|
|
5643
6014
|
}
|
|
5644
6015
|
|
|
6016
|
+
// src/shields/jail.ts
|
|
6017
|
+
var import_fs11 = __toESM(require("fs"));
|
|
6018
|
+
var import_os10 = __toESM(require("os"));
|
|
6019
|
+
var import_path13 = __toESM(require("path"));
|
|
6020
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
6021
|
+
function jailStorePath() {
|
|
6022
|
+
return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
|
|
6023
|
+
}
|
|
6024
|
+
function readJailPaths() {
|
|
6025
|
+
let text;
|
|
6026
|
+
try {
|
|
6027
|
+
text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
|
|
6028
|
+
} catch (err) {
|
|
6029
|
+
if (err.code === "ENOENT") return [];
|
|
6030
|
+
throw err;
|
|
6031
|
+
}
|
|
6032
|
+
let parsed;
|
|
6033
|
+
try {
|
|
6034
|
+
parsed = JSON.parse(text);
|
|
6035
|
+
} catch {
|
|
6036
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
6037
|
+
}
|
|
6038
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
6039
|
+
return parsed.paths.filter(
|
|
6040
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
6041
|
+
);
|
|
6042
|
+
}
|
|
6043
|
+
function findJailedPath(candidate) {
|
|
6044
|
+
return findJailedPathIn(candidate, readJailPaths());
|
|
6045
|
+
}
|
|
6046
|
+
function findJailedPathIn(candidate, paths) {
|
|
6047
|
+
if (!candidate) return null;
|
|
6048
|
+
for (const entry of paths) {
|
|
6049
|
+
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
6050
|
+
}
|
|
6051
|
+
return null;
|
|
6052
|
+
}
|
|
6053
|
+
|
|
5645
6054
|
// src/auth/orchestrator.ts
|
|
5646
6055
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
5647
6056
|
"write",
|
|
@@ -6117,12 +6526,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6117
6526
|
} else if (!taintWarning && !appPermReview) {
|
|
6118
6527
|
const toolLower = toolName.toLowerCase();
|
|
6119
6528
|
const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
|
|
6120
|
-
|
|
6529
|
+
const activeShields = isFileTool ? readActiveShields() : [];
|
|
6530
|
+
const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
|
|
6531
|
+
if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
|
|
6121
6532
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6122
|
-
const
|
|
6123
|
-
|
|
6124
|
-
);
|
|
6125
|
-
if (
|
|
6533
|
+
const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
|
|
6534
|
+
const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
|
|
6535
|
+
const sensitiveHit = candidates.some((c) => scanFilePath(c));
|
|
6536
|
+
if (jailHit || sensitiveHit) {
|
|
6537
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
6538
|
+
skipIgnoredFastPath: true
|
|
6539
|
+
});
|
|
6540
|
+
if (policyResult.decision === "block") {
|
|
6541
|
+
if (!isManual)
|
|
6542
|
+
appendLocalAudit(
|
|
6543
|
+
toolName,
|
|
6544
|
+
args,
|
|
6545
|
+
"deny",
|
|
6546
|
+
"smart-rule-block",
|
|
6547
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
6548
|
+
hashAuditArgs
|
|
6549
|
+
);
|
|
6550
|
+
return {
|
|
6551
|
+
approved: false,
|
|
6552
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
6553
|
+
blockedBy: "local-config",
|
|
6554
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
6555
|
+
ruleHit: policyResult.ruleName
|
|
6556
|
+
};
|
|
6557
|
+
}
|
|
6126
6558
|
} else {
|
|
6127
6559
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
6128
6560
|
return { approved: true };
|
|
@@ -6185,7 +6617,12 @@ ${appPermReview}`
|
|
|
6185
6617
|
}
|
|
6186
6618
|
let cloudRequestId = null;
|
|
6187
6619
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
6188
|
-
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview ||
|
|
6620
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
|
|
6621
|
+
// a client-side heuristic the SaaS has no rule for, so without forceReview
|
|
6622
|
+
// its checkRule answers "no org rule matched" → {approved:true}, which is
|
|
6623
|
+
// not an approval of an exfiltration risk. Measured against the live BE:
|
|
6624
|
+
// {approved:true} without this flag, {pending:true} with it.
|
|
6625
|
+
!!taintWarning || void 0;
|
|
6189
6626
|
if (cloudEnforced) {
|
|
6190
6627
|
try {
|
|
6191
6628
|
const initResult = await initNode9SaaS(
|
|
@@ -6198,10 +6635,10 @@ ${appPermReview}`
|
|
|
6198
6635
|
forceReview
|
|
6199
6636
|
);
|
|
6200
6637
|
if (!initResult.pending) {
|
|
6201
|
-
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6638
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6202
6639
|
return { approved: true, checkedBy: "cloud" };
|
|
6203
6640
|
}
|
|
6204
|
-
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6641
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6205
6642
|
return {
|
|
6206
6643
|
approved: !!initResult.approved,
|
|
6207
6644
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|