@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.mjs
CHANGED
|
@@ -380,8 +380,8 @@ function sanitizeConfig(raw) {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
const lines = result.error.issues.map((issue) => {
|
|
383
|
-
const
|
|
384
|
-
return ` \u2022 ${
|
|
383
|
+
const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
384
|
+
return ` \u2022 ${path14}: ${issue.message}`;
|
|
385
385
|
});
|
|
386
386
|
return {
|
|
387
387
|
sanitized,
|
|
@@ -1001,6 +1001,129 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1001
1001
|
}
|
|
1002
1002
|
return null;
|
|
1003
1003
|
}
|
|
1004
|
+
var MAX_REGEX_LENGTH = 256;
|
|
1005
|
+
var REGEX_CACHE_MAX = 500;
|
|
1006
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
1007
|
+
function validateRegex(pattern) {
|
|
1008
|
+
if (!pattern) return "Pattern is required";
|
|
1009
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1010
|
+
try {
|
|
1011
|
+
new RegExp(pattern);
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
1014
|
+
}
|
|
1015
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1016
|
+
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
1020
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1021
|
+
const key = `${pattern}\0${flags}`;
|
|
1022
|
+
if (regexCache.has(key)) {
|
|
1023
|
+
const cached = regexCache.get(key);
|
|
1024
|
+
regexCache.delete(key);
|
|
1025
|
+
regexCache.set(key, cached);
|
|
1026
|
+
return cached;
|
|
1027
|
+
}
|
|
1028
|
+
if (validateRegex(pattern) !== null) return null;
|
|
1029
|
+
try {
|
|
1030
|
+
const re = new RegExp(pattern, flags);
|
|
1031
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1032
|
+
const oldest = regexCache.keys().next().value;
|
|
1033
|
+
if (oldest) regexCache.delete(oldest);
|
|
1034
|
+
}
|
|
1035
|
+
regexCache.set(key, re);
|
|
1036
|
+
return re;
|
|
1037
|
+
} catch {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function matchesPattern(text, patterns) {
|
|
1042
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
1043
|
+
if (p.length === 0) return false;
|
|
1044
|
+
const isMatch = pm(p, { nocase: true, dot: true });
|
|
1045
|
+
const target = text.toLowerCase();
|
|
1046
|
+
const directMatch = isMatch(target);
|
|
1047
|
+
if (directMatch) return true;
|
|
1048
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1049
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1050
|
+
}
|
|
1051
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1052
|
+
function getNestedValue(obj, path14) {
|
|
1053
|
+
if (!obj || typeof obj !== "object") return null;
|
|
1054
|
+
const segments = path14.split(".");
|
|
1055
|
+
for (const seg of segments) {
|
|
1056
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1057
|
+
}
|
|
1058
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
1059
|
+
}
|
|
1060
|
+
function evaluateSmartConditions(args, rule) {
|
|
1061
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
1062
|
+
const mode = rule.conditionMode ?? "all";
|
|
1063
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
1064
|
+
const resolveField = (field) => {
|
|
1065
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
1066
|
+
const rawVal = getNestedValue(args, field);
|
|
1067
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
1068
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
1069
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
1070
|
+
fieldCache.set(field, val);
|
|
1071
|
+
return val;
|
|
1072
|
+
};
|
|
1073
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
1074
|
+
const resolveFieldReadings = (field) => {
|
|
1075
|
+
const cached = readingsCache.get(field);
|
|
1076
|
+
if (cached) return cached;
|
|
1077
|
+
const primary = resolveField(field);
|
|
1078
|
+
if (primary === null) {
|
|
1079
|
+
readingsCache.set(field, []);
|
|
1080
|
+
return [];
|
|
1081
|
+
}
|
|
1082
|
+
let out = [primary];
|
|
1083
|
+
if (field === "command") {
|
|
1084
|
+
const raw = getNestedValue(args, field);
|
|
1085
|
+
if (typeof raw === "string") {
|
|
1086
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
1087
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
readingsCache.set(field, out);
|
|
1091
|
+
return out;
|
|
1092
|
+
};
|
|
1093
|
+
const results = rule.conditions.map((cond) => {
|
|
1094
|
+
const val = resolveField(cond.field);
|
|
1095
|
+
switch (cond.op) {
|
|
1096
|
+
case "exists":
|
|
1097
|
+
return val !== null && val !== "";
|
|
1098
|
+
case "notExists":
|
|
1099
|
+
return val === null || val === "";
|
|
1100
|
+
case "contains":
|
|
1101
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
1102
|
+
case "notContains":
|
|
1103
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
1104
|
+
case "matches": {
|
|
1105
|
+
if (val === null || !cond.value) return false;
|
|
1106
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1107
|
+
if (!reM) return false;
|
|
1108
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
1109
|
+
}
|
|
1110
|
+
case "notMatches": {
|
|
1111
|
+
if (!cond.value) return false;
|
|
1112
|
+
if (val === null) return true;
|
|
1113
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
1114
|
+
if (!reN) return false;
|
|
1115
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
1116
|
+
}
|
|
1117
|
+
case "matchesGlob":
|
|
1118
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
1119
|
+
case "notMatchesGlob":
|
|
1120
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
1121
|
+
default:
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
1126
|
+
}
|
|
1004
1127
|
var { syntax } = mvdanSh;
|
|
1005
1128
|
var sharedParser = syntax.NewParser();
|
|
1006
1129
|
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -1073,14 +1196,22 @@ function cachedNormalize(command, compute) {
|
|
|
1073
1196
|
return result;
|
|
1074
1197
|
}
|
|
1075
1198
|
function normalizeCommandForPolicy(command) {
|
|
1199
|
+
return commandReadingsImpl(command).posix;
|
|
1200
|
+
}
|
|
1201
|
+
function commandReadings(command) {
|
|
1202
|
+
const r = commandReadingsImpl(command);
|
|
1203
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
1204
|
+
}
|
|
1205
|
+
function commandReadingsImpl(command) {
|
|
1076
1206
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
1077
1207
|
}
|
|
1078
1208
|
function normalizeCommandForPolicyImpl(command) {
|
|
1079
1209
|
const f = parseShared(command);
|
|
1080
|
-
if (f === PARSE_FAIL) return command;
|
|
1210
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
1081
1211
|
try {
|
|
1082
1212
|
const strips = [];
|
|
1083
1213
|
const rewrites = [];
|
|
1214
|
+
const quoteOnlyRewrites = [];
|
|
1084
1215
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
1085
1216
|
syntax.Walk(f, (node) => {
|
|
1086
1217
|
if (!node) return false;
|
|
@@ -1125,22 +1256,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
1125
1256
|
if (resolved === source) continue;
|
|
1126
1257
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
1127
1258
|
rewrites.push([s, e, resolved]);
|
|
1259
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
1260
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
1128
1261
|
}
|
|
1129
1262
|
return true;
|
|
1130
1263
|
});
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
...
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
return result;
|
|
1264
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
1265
|
+
const apply = (extra) => {
|
|
1266
|
+
const edits = [...stripEdits, ...extra];
|
|
1267
|
+
if (edits.length === 0) return command;
|
|
1268
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1269
|
+
let out = command;
|
|
1270
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
1271
|
+
return out;
|
|
1272
|
+
};
|
|
1273
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
1142
1274
|
} catch {
|
|
1143
|
-
return command;
|
|
1275
|
+
return { posix: command, separator: command };
|
|
1144
1276
|
}
|
|
1145
1277
|
}
|
|
1146
1278
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -1209,9 +1341,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1209
1341
|
"vi",
|
|
1210
1342
|
"emacs",
|
|
1211
1343
|
"code",
|
|
1212
|
-
"type"
|
|
1344
|
+
"type",
|
|
1345
|
+
// — the 22 that were missing —
|
|
1346
|
+
"grep",
|
|
1347
|
+
"egrep",
|
|
1348
|
+
"fgrep",
|
|
1349
|
+
"rg",
|
|
1350
|
+
"ag",
|
|
1351
|
+
"ack",
|
|
1352
|
+
"awk",
|
|
1353
|
+
"gawk",
|
|
1354
|
+
"sed",
|
|
1355
|
+
"cut",
|
|
1356
|
+
"tr",
|
|
1357
|
+
"jq",
|
|
1358
|
+
"yq",
|
|
1359
|
+
"od",
|
|
1360
|
+
"xxd",
|
|
1361
|
+
"hexdump",
|
|
1362
|
+
"strings",
|
|
1363
|
+
"sort",
|
|
1364
|
+
"uniq",
|
|
1365
|
+
"tac",
|
|
1366
|
+
"nl",
|
|
1367
|
+
"dd"
|
|
1213
1368
|
]);
|
|
1214
|
-
var FS_OP_PRESCREEN_RE =
|
|
1369
|
+
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1370
|
+
`(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
|
|
1371
|
+
);
|
|
1215
1372
|
var HOME_CACHE_ALLOWLIST = [
|
|
1216
1373
|
".cache",
|
|
1217
1374
|
".npm/_npx",
|
|
@@ -1250,9 +1407,37 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1250
1407
|
// for the canonical test-asserted contract.
|
|
1251
1408
|
rule: "shield:project-jail:block-read-env",
|
|
1252
1409
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1410
|
+
// Structural, not a list. The previous form enumerated seven suffixes and
|
|
1411
|
+
// anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
|
|
1412
|
+
// gitignored, all routinely holding real secrets — were never covered. A
|
|
1413
|
+
// hand-written list of what to protect is only ever as complete as the day
|
|
1414
|
+
// it was typed; this says "`.env` plus any suffix chain" and then names the
|
|
1415
|
+
// exceptions, which is the direction that fails safe.
|
|
1416
|
+
//
|
|
1417
|
+
// \.env the segment itself
|
|
1418
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1419
|
+
// files. Without it a flat suffix class swallows both.
|
|
1420
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1421
|
+
// [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
|
|
1422
|
+
// `(\.[\w-]+)*` reads the same but is rejected by
|
|
1423
|
+
// safe-regex2, and this pattern runs on the hook hot path.
|
|
1424
|
+
//
|
|
1425
|
+
// The two exclusions are NOT the same shape, because the words do not mean
|
|
1426
|
+
// the same thing:
|
|
1427
|
+
//
|
|
1428
|
+
// (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
|
|
1429
|
+
// stays a fixture whatever follows, so `.env.example.md` is allowed too.
|
|
1430
|
+
// These are checked into git by convention: already public, so blocking
|
|
1431
|
+
// them buys nothing and costs the most common legitimate agent read.
|
|
1432
|
+
//
|
|
1433
|
+
// (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
|
|
1434
|
+
// fixture. `.env.test` is the committed template and stays allowed, but
|
|
1435
|
+
// `.env.test.local` is gitignored by the `.env*.local` convention and
|
|
1436
|
+
// holds real values, so it must block. Using `\b` here — the obvious
|
|
1437
|
+
// symmetry — silently exempts every `.env.test.*` file.
|
|
1438
|
+
//
|
|
1439
|
+
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1440
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
|
|
1256
1441
|
},
|
|
1257
1442
|
{
|
|
1258
1443
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1381,6 +1566,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1381
1566
|
}
|
|
1382
1567
|
return found;
|
|
1383
1568
|
}
|
|
1569
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1570
|
+
if (isBashTool(toolName)) return true;
|
|
1571
|
+
if (!toolInspection) return false;
|
|
1572
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1573
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1574
|
+
}
|
|
1575
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1576
|
+
if (!ruleTool) return true;
|
|
1577
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1578
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1579
|
+
}
|
|
1580
|
+
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;
|
|
1581
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1582
|
+
"uv",
|
|
1583
|
+
"uvx",
|
|
1584
|
+
"poetry",
|
|
1585
|
+
"pipenv",
|
|
1586
|
+
"pdm",
|
|
1587
|
+
"rye",
|
|
1588
|
+
"hatch",
|
|
1589
|
+
"conda",
|
|
1590
|
+
"mamba",
|
|
1591
|
+
"micromamba",
|
|
1592
|
+
"npx",
|
|
1593
|
+
"pnpm",
|
|
1594
|
+
"yarn",
|
|
1595
|
+
"bunx",
|
|
1596
|
+
"watch",
|
|
1597
|
+
"strace",
|
|
1598
|
+
"ltrace",
|
|
1599
|
+
"chroot",
|
|
1600
|
+
"unshare",
|
|
1601
|
+
"runuser"
|
|
1602
|
+
]);
|
|
1603
|
+
function isInlineCodeFlag(interp, w) {
|
|
1604
|
+
const lw = w.toLowerCase();
|
|
1605
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1606
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1607
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1608
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1609
|
+
const body = lw.slice(1);
|
|
1610
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1611
|
+
const cut = body.search(cutAt);
|
|
1612
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1613
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1614
|
+
}
|
|
1615
|
+
var _redirStdinOps = null;
|
|
1616
|
+
function redirStdinOps() {
|
|
1617
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1618
|
+
_redirStdinOps = new Set(
|
|
1619
|
+
[
|
|
1620
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1621
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1622
|
+
deriveRedirOp("cat < f"),
|
|
1623
|
+
deriveRedirOp("cat <<< x")
|
|
1624
|
+
].filter((op) => op >= 0)
|
|
1625
|
+
);
|
|
1626
|
+
return _redirStdinOps;
|
|
1627
|
+
}
|
|
1628
|
+
function deriveBinaryOp(sample) {
|
|
1629
|
+
try {
|
|
1630
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1631
|
+
let op = -1;
|
|
1632
|
+
syntax.Walk(f, (node) => {
|
|
1633
|
+
const n = node;
|
|
1634
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1635
|
+
return true;
|
|
1636
|
+
});
|
|
1637
|
+
return op;
|
|
1638
|
+
} catch {
|
|
1639
|
+
return -1;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
var _listOps = null;
|
|
1643
|
+
function listOps() {
|
|
1644
|
+
if (_listOps) return _listOps;
|
|
1645
|
+
_listOps = new Set(
|
|
1646
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1647
|
+
);
|
|
1648
|
+
return _listOps;
|
|
1649
|
+
}
|
|
1650
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1651
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1652
|
+
function unwrapCommandHead(words) {
|
|
1653
|
+
let i = 0;
|
|
1654
|
+
while (i < words.length) {
|
|
1655
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1656
|
+
if (head === "find") {
|
|
1657
|
+
const x = words.findIndex(
|
|
1658
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1659
|
+
);
|
|
1660
|
+
if (x < 0) break;
|
|
1661
|
+
i = x + 1;
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1665
|
+
i++;
|
|
1666
|
+
let targetConsumed = false;
|
|
1667
|
+
while (i < words.length) {
|
|
1668
|
+
const t = words[i];
|
|
1669
|
+
if (t === null) {
|
|
1670
|
+
i++;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
const lt = t.toLowerCase();
|
|
1674
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1675
|
+
i++;
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (t.startsWith("-")) {
|
|
1679
|
+
i++;
|
|
1680
|
+
const nxt = words[i];
|
|
1681
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1682
|
+
i++;
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1686
|
+
i++;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1690
|
+
targetConsumed = true;
|
|
1691
|
+
i++;
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
break;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return i;
|
|
1698
|
+
}
|
|
1699
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1700
|
+
const cmd = stmt?.Cmd;
|
|
1701
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1702
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1703
|
+
if (words.length === 0) return false;
|
|
1704
|
+
const headIdx = unwrapCommandHead(words);
|
|
1705
|
+
const rawHead = words[headIdx];
|
|
1706
|
+
if (rawHead == null) return false;
|
|
1707
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1708
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1709
|
+
let args = words.slice(headIdx + 1);
|
|
1710
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1711
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1712
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1713
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1714
|
+
}
|
|
1715
|
+
let positionals = 0;
|
|
1716
|
+
let selectedProgram = false;
|
|
1717
|
+
for (const a of args) {
|
|
1718
|
+
if (a == null) {
|
|
1719
|
+
positionals++;
|
|
1720
|
+
selectedProgram = true;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1724
|
+
if (a === "-m") {
|
|
1725
|
+
selectedProgram = true;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1729
|
+
if (!a.startsWith("-")) {
|
|
1730
|
+
positionals++;
|
|
1731
|
+
selectedProgram = true;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1735
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1736
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1737
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1738
|
+
}
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
function detectInlineExec(command) {
|
|
1742
|
+
const f = parseShared(command);
|
|
1743
|
+
if (f === PARSE_FAIL) {
|
|
1744
|
+
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(
|
|
1745
|
+
command
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
let found = false;
|
|
1749
|
+
try {
|
|
1750
|
+
syntax.Walk(f, (node) => {
|
|
1751
|
+
if (!node || found) return false;
|
|
1752
|
+
const n = node;
|
|
1753
|
+
const t = syntax.NodeType(n);
|
|
1754
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1755
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1756
|
+
found = true;
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1761
|
+
found = true;
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
return true;
|
|
1765
|
+
});
|
|
1766
|
+
} catch {
|
|
1767
|
+
return found;
|
|
1768
|
+
}
|
|
1769
|
+
return found;
|
|
1770
|
+
}
|
|
1384
1771
|
function analyzeChmod777(command) {
|
|
1385
1772
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1386
1773
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2245,150 +2632,12 @@ function extractAllSshHosts(tokens) {
|
|
|
2245
2632
|
}
|
|
2246
2633
|
return [...hosts].filter(Boolean);
|
|
2247
2634
|
}
|
|
2248
|
-
var MAX_REGEX_LENGTH = 100;
|
|
2249
|
-
var REGEX_CACHE_MAX = 500;
|
|
2250
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
2251
|
-
function validateRegex(pattern) {
|
|
2252
|
-
if (!pattern) return "Pattern is required";
|
|
2253
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2254
|
-
try {
|
|
2255
|
-
new RegExp(pattern);
|
|
2256
|
-
} catch (e) {
|
|
2257
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2258
|
-
}
|
|
2259
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2260
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2261
|
-
return null;
|
|
2262
|
-
}
|
|
2263
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2264
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2265
|
-
const key = `${pattern}\0${flags}`;
|
|
2266
|
-
if (regexCache.has(key)) {
|
|
2267
|
-
const cached = regexCache.get(key);
|
|
2268
|
-
regexCache.delete(key);
|
|
2269
|
-
regexCache.set(key, cached);
|
|
2270
|
-
return cached;
|
|
2271
|
-
}
|
|
2272
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2273
|
-
try {
|
|
2274
|
-
const re = new RegExp(pattern, flags);
|
|
2275
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2276
|
-
const oldest = regexCache.keys().next().value;
|
|
2277
|
-
if (oldest) regexCache.delete(oldest);
|
|
2278
|
-
}
|
|
2279
|
-
regexCache.set(key, re);
|
|
2280
|
-
return re;
|
|
2281
|
-
} catch {
|
|
2282
|
-
return null;
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
function matchesPattern(text, patterns) {
|
|
2286
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2287
|
-
if (p.length === 0) return false;
|
|
2288
|
-
const isMatch = pm(p, { nocase: true, dot: true });
|
|
2289
|
-
const target = text.toLowerCase();
|
|
2290
|
-
const directMatch = isMatch(target);
|
|
2291
|
-
if (directMatch) return true;
|
|
2292
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2293
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2294
|
-
}
|
|
2295
|
-
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2296
|
-
function getNestedValue(obj, path13) {
|
|
2297
|
-
if (!obj || typeof obj !== "object") return null;
|
|
2298
|
-
const segments = path13.split(".");
|
|
2299
|
-
for (const seg of segments) {
|
|
2300
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2301
|
-
}
|
|
2302
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2303
|
-
}
|
|
2304
|
-
function evaluateSmartConditions(args, rule) {
|
|
2305
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2306
|
-
const mode = rule.conditionMode ?? "all";
|
|
2307
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
2308
|
-
const resolveField = (field) => {
|
|
2309
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
2310
|
-
const rawVal = getNestedValue(args, field);
|
|
2311
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
2312
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
2313
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
2314
|
-
fieldCache.set(field, val);
|
|
2315
|
-
return val;
|
|
2316
|
-
};
|
|
2317
|
-
const results = rule.conditions.map((cond) => {
|
|
2318
|
-
const val = resolveField(cond.field);
|
|
2319
|
-
switch (cond.op) {
|
|
2320
|
-
case "exists":
|
|
2321
|
-
return val !== null && val !== "";
|
|
2322
|
-
case "notExists":
|
|
2323
|
-
return val === null || val === "";
|
|
2324
|
-
case "contains":
|
|
2325
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2326
|
-
case "notContains":
|
|
2327
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2328
|
-
case "matches": {
|
|
2329
|
-
if (val === null || !cond.value) return false;
|
|
2330
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2331
|
-
if (!reM) return false;
|
|
2332
|
-
return reM.test(val);
|
|
2333
|
-
}
|
|
2334
|
-
case "notMatches": {
|
|
2335
|
-
if (!cond.value) return false;
|
|
2336
|
-
if (val === null) return true;
|
|
2337
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2338
|
-
if (!reN) return false;
|
|
2339
|
-
return !reN.test(val);
|
|
2340
|
-
}
|
|
2341
|
-
case "matchesGlob":
|
|
2342
|
-
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
2343
|
-
case "notMatchesGlob":
|
|
2344
|
-
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
2345
|
-
default:
|
|
2346
|
-
return false;
|
|
2347
|
-
}
|
|
2348
|
-
});
|
|
2349
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2350
|
-
}
|
|
2351
2635
|
function resolveCheck(v) {
|
|
2352
2636
|
return v === "off" || v === "block" ? v : "review";
|
|
2353
2637
|
}
|
|
2354
2638
|
function resolveCheckTight(v) {
|
|
2355
2639
|
return v === "block" ? "block" : "review";
|
|
2356
2640
|
}
|
|
2357
|
-
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2358
|
-
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2359
|
-
function detectInlineExec(command) {
|
|
2360
|
-
const pipeFed = command.includes("|");
|
|
2361
|
-
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2362
|
-
for (const rawSeg of segments) {
|
|
2363
|
-
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2364
|
-
let i = 0;
|
|
2365
|
-
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2366
|
-
if (i >= tokens.length) continue;
|
|
2367
|
-
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2368
|
-
if (!INLINE_INTERP.test(base)) continue;
|
|
2369
|
-
const args = tokens.slice(i + 1);
|
|
2370
|
-
let hadRedirect = false;
|
|
2371
|
-
const positionals = [];
|
|
2372
|
-
for (let j = 0; j < args.length; j++) {
|
|
2373
|
-
const a = args[j];
|
|
2374
|
-
if (a === "-") return true;
|
|
2375
|
-
if (a.startsWith("<")) {
|
|
2376
|
-
hadRedirect = true;
|
|
2377
|
-
if (a === "<" || a === "<<") j++;
|
|
2378
|
-
continue;
|
|
2379
|
-
}
|
|
2380
|
-
if (a.startsWith("-")) {
|
|
2381
|
-
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2382
|
-
continue;
|
|
2383
|
-
}
|
|
2384
|
-
positionals.push(a);
|
|
2385
|
-
}
|
|
2386
|
-
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2387
|
-
return true;
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
return false;
|
|
2391
|
-
}
|
|
2392
2641
|
var VERDICT_RANK = {
|
|
2393
2642
|
allow: 0,
|
|
2394
2643
|
review: 1,
|
|
@@ -2402,6 +2651,12 @@ function resolvePinned(matches) {
|
|
|
2402
2651
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2403
2652
|
);
|
|
2404
2653
|
}
|
|
2654
|
+
function strictestVerdict(candidates) {
|
|
2655
|
+
if (candidates.length === 0) return void 0;
|
|
2656
|
+
return candidates.reduce(
|
|
2657
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2405
2660
|
function tokenize2(toolName) {
|
|
2406
2661
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2407
2662
|
}
|
|
@@ -2413,6 +2668,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2413
2668
|
const value = getNestedValue(args, fieldPath);
|
|
2414
2669
|
return typeof value === "string" ? value : null;
|
|
2415
2670
|
}
|
|
2671
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2672
|
+
const patterns = Object.keys(toolInspection);
|
|
2673
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2674
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2675
|
+
}
|
|
2416
2676
|
function isSqlTool(toolName, toolInspection) {
|
|
2417
2677
|
const patterns = Object.keys(toolInspection);
|
|
2418
2678
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2475,8 +2735,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2475
2735
|
};
|
|
2476
2736
|
}
|
|
2477
2737
|
}
|
|
2478
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2479
|
-
const
|
|
2738
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2739
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2740
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2741
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2480
2742
|
if (bashCommand !== null) {
|
|
2481
2743
|
const pipeVerdict = pipeChainVerdict(
|
|
2482
2744
|
bashCommand,
|
|
@@ -2526,8 +2788,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2526
2788
|
}
|
|
2527
2789
|
if (config.policy.smartRules.length > 0) {
|
|
2528
2790
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2791
|
+
const astSuppressed = (rule) => {
|
|
2792
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2793
|
+
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;
|
|
2794
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2795
|
+
return true;
|
|
2796
|
+
};
|
|
2529
2797
|
const matches = config.policy.smartRules.filter(
|
|
2530
|
-
(rule) =>
|
|
2798
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2531
2799
|
);
|
|
2532
2800
|
const matchedRule = resolvePinned(matches);
|
|
2533
2801
|
if (matchedRule) {
|
|
@@ -2558,15 +2826,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2558
2826
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2559
2827
|
allTokens = analyzed.allTokens;
|
|
2560
2828
|
pathTokens = analyzed.paths;
|
|
2561
|
-
const
|
|
2562
|
-
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2563
|
-
return {
|
|
2564
|
-
decision: inlineAction === "block" ? "block" : "review",
|
|
2565
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2566
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2567
|
-
tier: 3
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2829
|
+
const candidates = [];
|
|
2570
2830
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2571
2831
|
if (evalVerdict === "block") {
|
|
2572
2832
|
return {
|
|
@@ -2577,24 +2837,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2577
2837
|
tier: 3
|
|
2578
2838
|
};
|
|
2579
2839
|
}
|
|
2840
|
+
const ptVerdict = pipeChainVerdict(
|
|
2841
|
+
shellCommand,
|
|
2842
|
+
isTrustedHost2,
|
|
2843
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2844
|
+
);
|
|
2845
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2846
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2847
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2848
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2849
|
+
candidates.push({
|
|
2850
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2851
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2852
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2853
|
+
tier: 3
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2580
2856
|
if (evalVerdict === "review") {
|
|
2581
|
-
|
|
2582
|
-
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2583
|
-
// block but can never turn this off (eval-remote above is Class A —
|
|
2584
|
-
// no knob at all).
|
|
2857
|
+
candidates.push({
|
|
2585
2858
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2586
2859
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2587
2860
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2588
2861
|
ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
|
|
2589
2862
|
tier: 3
|
|
2590
|
-
};
|
|
2863
|
+
});
|
|
2591
2864
|
}
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
isTrustedHost2,
|
|
2595
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2596
|
-
);
|
|
2597
|
-
if (ptVerdict) return ptVerdict;
|
|
2865
|
+
const builtin = strictestVerdict(candidates);
|
|
2866
|
+
if (builtin) return builtin;
|
|
2598
2867
|
if (config.policy.egress?.enabled) {
|
|
2599
2868
|
const dests = extractShellDestinations(shellCommand);
|
|
2600
2869
|
if (dests.length > 0) {
|
|
@@ -3645,28 +3914,41 @@ function readShieldOverrides() {
|
|
|
3645
3914
|
var MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
3646
3915
|
var EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
3647
3916
|
function rankIn(order, value) {
|
|
3648
|
-
return order.indexOf(value);
|
|
3917
|
+
return value === void 0 ? -1 : order.indexOf(value);
|
|
3649
3918
|
}
|
|
3650
|
-
function
|
|
3651
|
-
if (rankIn(order, cloud) === -1) return local;
|
|
3652
|
-
if (locked) return cloud;
|
|
3919
|
+
function floorValue(order, local, cloud, opts = {}) {
|
|
3920
|
+
if (cloud === void 0 || rankIn(order, cloud) === -1) return local;
|
|
3921
|
+
if (opts.locked) return cloud;
|
|
3922
|
+
const localSet = opts.localWasSet ?? local !== void 0;
|
|
3923
|
+
if (!localSet || rankIn(order, local) === -1) return cloud;
|
|
3653
3924
|
return rankIn(order, local) > rankIn(order, cloud) ? local : cloud;
|
|
3654
3925
|
}
|
|
3926
|
+
function strictestOf(order, ...values) {
|
|
3927
|
+
let best;
|
|
3928
|
+
for (const v of values) {
|
|
3929
|
+
if (rankIn(order, v) === -1) continue;
|
|
3930
|
+
if (best === void 0 || rankIn(order, v) > rankIn(order, best)) best = v;
|
|
3931
|
+
}
|
|
3932
|
+
return best;
|
|
3933
|
+
}
|
|
3934
|
+
function resolveByOrder(order, local, cloud, locked) {
|
|
3935
|
+
return floorValue(order, local, cloud, { locked }) ?? local;
|
|
3936
|
+
}
|
|
3655
3937
|
function resolveManagedMode(local, cloud, locked) {
|
|
3656
3938
|
return resolveByOrder(MODE_ORDER, local, cloud, locked);
|
|
3657
3939
|
}
|
|
3658
|
-
function applyManagedEgress(local, managed, locked) {
|
|
3940
|
+
function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
3659
3941
|
const next = { ...local };
|
|
3660
3942
|
if (typeof managed.enabled === "boolean") {
|
|
3661
3943
|
next.enabled = locked.includes("egressEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3662
3944
|
}
|
|
3663
3945
|
if (typeof managed.mode === "string") {
|
|
3664
|
-
next.mode =
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
);
|
|
3946
|
+
next.mode = floorValue(EGRESS_MODE_ORDER, local.mode, managed.mode, {
|
|
3947
|
+
locked: locked.includes("egressMode"),
|
|
3948
|
+
// The default 'review' is seeded into egress before any merge, so absence
|
|
3949
|
+
// is invisible from `local.mode` alone — the caller tracks it for us.
|
|
3950
|
+
localWasSet: localModeUserSet
|
|
3951
|
+
}) ?? local.mode;
|
|
3670
3952
|
}
|
|
3671
3953
|
if (Array.isArray(managed.allow) && managed.allow.length > 0) {
|
|
3672
3954
|
next.allow = [...managed.allow];
|
|
@@ -3686,6 +3968,9 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3686
3968
|
if (typeof managed.enabled === "boolean") {
|
|
3687
3969
|
next.enabled = locked.includes("dlpEnabled") ? managed.enabled : local.enabled || managed.enabled;
|
|
3688
3970
|
}
|
|
3971
|
+
if (managed.enabled === true) {
|
|
3972
|
+
next.scanIgnoredTools = true;
|
|
3973
|
+
}
|
|
3689
3974
|
if (typeof managed.pii === "string") {
|
|
3690
3975
|
next.pii = resolveByOrder(
|
|
3691
3976
|
DLP_PII_ORDER,
|
|
@@ -3719,13 +4004,9 @@ function applyManagedCommandChecks(local, managed, locked) {
|
|
|
3719
4004
|
const m = managed[key];
|
|
3720
4005
|
if (typeof m !== "string") continue;
|
|
3721
4006
|
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
localValue,
|
|
3726
|
-
m,
|
|
3727
|
-
locked.includes(lockKey)
|
|
3728
|
-
);
|
|
4007
|
+
const resolved = floorValue(COMMAND_CHECK_ORDER, local[key], m, {
|
|
4008
|
+
locked: locked.includes(lockKey)
|
|
4009
|
+
});
|
|
3729
4010
|
if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
|
|
3730
4011
|
next[key] = resolved;
|
|
3731
4012
|
}
|
|
@@ -3751,11 +4032,18 @@ function slug(s) {
|
|
|
3751
4032
|
var B = "[\\s/\\\\]";
|
|
3752
4033
|
var SEP = "[/\\\\]";
|
|
3753
4034
|
function pathToRegexFragment(rawPath) {
|
|
3754
|
-
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
4035
|
+
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
3755
4036
|
const segments = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
3756
4037
|
if (segments.length === 0) return "";
|
|
3757
4038
|
return `(^|${B})${segments.join(SEP)}(${B}|$)`;
|
|
3758
4039
|
}
|
|
4040
|
+
function pathMatchesFragment(candidate, rawPath) {
|
|
4041
|
+
const value = pathToRegexFragment(rawPath);
|
|
4042
|
+
if (!value || !candidate) return false;
|
|
4043
|
+
const re = getCompiledRegex(value);
|
|
4044
|
+
if (!re) return false;
|
|
4045
|
+
return re.test(candidate);
|
|
4046
|
+
}
|
|
3759
4047
|
function pathRules(rawPath, verdict, reason) {
|
|
3760
4048
|
const value = pathToRegexFragment(rawPath);
|
|
3761
4049
|
if (!value) return [];
|
|
@@ -3769,12 +4057,28 @@ function pathRules(rawPath, verdict, reason) {
|
|
|
3769
4057
|
verdict,
|
|
3770
4058
|
reason: why
|
|
3771
4059
|
},
|
|
4060
|
+
// Keep the historical `-anytool` name for the file_path rule: the
|
|
4061
|
+
// rule→shield attribution maps (Report SHIELDS panel) key on rule names.
|
|
3772
4062
|
{
|
|
3773
4063
|
name: `${verdict}-path-${s}-anytool`,
|
|
3774
4064
|
tool: "*",
|
|
3775
4065
|
conditions: [{ field: "file_path", op: "matches", value }],
|
|
3776
4066
|
verdict,
|
|
3777
4067
|
reason: why
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
name: `${verdict}-path-${s}-anytool-path`,
|
|
4071
|
+
tool: "*",
|
|
4072
|
+
conditions: [{ field: "path", op: "matches", value }],
|
|
4073
|
+
verdict,
|
|
4074
|
+
reason: why
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
name: `${verdict}-path-${s}-anytool-pattern`,
|
|
4078
|
+
tool: "*",
|
|
4079
|
+
conditions: [{ field: "pattern", op: "matches", value }],
|
|
4080
|
+
verdict,
|
|
4081
|
+
reason: why
|
|
3778
4082
|
}
|
|
3779
4083
|
];
|
|
3780
4084
|
}
|
|
@@ -3847,8 +4151,14 @@ var DEFAULT_CONFIG = {
|
|
|
3847
4151
|
settings: {
|
|
3848
4152
|
mode: "standard",
|
|
3849
4153
|
autoStartDaemon: true,
|
|
3850
|
-
|
|
3851
|
-
//
|
|
4154
|
+
// OFF by default. The snapshot store is a per-project bare git repo with
|
|
4155
|
+
// no size ceiling, and eviction drops the index row without deleting the
|
|
4156
|
+
// objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
|
|
4157
|
+
// from interrupted `git gc`) and filled the disk. A security tool must not
|
|
4158
|
+
// be what fills a customer's disk. Re-enable per install with
|
|
4159
|
+
// `{"settings":{"enableUndo":true}}`; the default flips back when the
|
|
4160
|
+
// bounded copy-store lands (doc/undo-v2-copy-store-design.md).
|
|
4161
|
+
enableUndo: false,
|
|
3852
4162
|
enableHookLogDebug: true,
|
|
3853
4163
|
approvalTimeoutMs: 12e4,
|
|
3854
4164
|
// 120-second auto-deny timeout
|
|
@@ -4047,10 +4357,13 @@ var DEFAULT_CONFIG = {
|
|
|
4047
4357
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
4048
4358
|
trustedHosts: [],
|
|
4049
4359
|
trustedHostsManaged: false,
|
|
4050
|
-
appPermissions: {}
|
|
4360
|
+
appPermissions: {},
|
|
4361
|
+
managedJailPaths: []
|
|
4051
4362
|
},
|
|
4052
4363
|
environments: {}
|
|
4053
4364
|
};
|
|
4365
|
+
var RM_SAFE_PATH_PATTERN = "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)";
|
|
4366
|
+
var VERDICT_ORDER = ["allow", "review", "block"];
|
|
4054
4367
|
var ADVISORY_SMART_RULES = [
|
|
4055
4368
|
// ── rm safety ─────────────────────────────────────────────────────────────
|
|
4056
4369
|
// tool: '*' so they cover bash, shell, run_shell_command, and Gemini's Shell.
|
|
@@ -4062,12 +4375,7 @@ var ADVISORY_SMART_RULES = [
|
|
|
4062
4375
|
conditionMode: "all",
|
|
4063
4376
|
conditions: [
|
|
4064
4377
|
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
4065
|
-
{
|
|
4066
|
-
field: "command",
|
|
4067
|
-
op: "matches",
|
|
4068
|
-
// Matches known-safe build artifact paths in the command.
|
|
4069
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
4070
|
-
}
|
|
4378
|
+
{ field: "command", op: "matches", value: RM_SAFE_PATH_PATTERN }
|
|
4071
4379
|
],
|
|
4072
4380
|
verdict: "allow",
|
|
4073
4381
|
reason: "Deleting a known-safe build artifact path"
|
|
@@ -4245,10 +4553,15 @@ function getConfig(cwd) {
|
|
|
4245
4553
|
// here. A managed list fills this below and flips trustedHostsManaged.
|
|
4246
4554
|
trustedHosts: [],
|
|
4247
4555
|
trustedHostsManaged: false,
|
|
4248
|
-
appPermissions: {}
|
|
4556
|
+
appPermissions: {},
|
|
4557
|
+
managedJailPaths: []
|
|
4249
4558
|
};
|
|
4250
4559
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
4251
|
-
const
|
|
4560
|
+
const rank = (v) => {
|
|
4561
|
+
const i = COMMAND_CHECK_ORDER.indexOf(v ?? "");
|
|
4562
|
+
return i === -1 ? 1 : i;
|
|
4563
|
+
};
|
|
4564
|
+
const applyLayer = (source, isProject = false) => {
|
|
4252
4565
|
if (!source) return;
|
|
4253
4566
|
const s = source.settings || {};
|
|
4254
4567
|
const p = source.policy || {};
|
|
@@ -4308,7 +4621,9 @@ function getConfig(cwd) {
|
|
|
4308
4621
|
};
|
|
4309
4622
|
for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
|
|
4310
4623
|
const v = src[k];
|
|
4311
|
-
if (v
|
|
4624
|
+
if (v !== "off" && v !== "review" && v !== "block") continue;
|
|
4625
|
+
if (isProject && rank(v) < rank(cc2[k])) continue;
|
|
4626
|
+
cc2[k] = v;
|
|
4312
4627
|
}
|
|
4313
4628
|
for (const k of ["evalDynamic", "pipeChainHigh"]) {
|
|
4314
4629
|
const v = src[k];
|
|
@@ -4318,11 +4633,22 @@ function getConfig(cwd) {
|
|
|
4318
4633
|
}
|
|
4319
4634
|
if (p.egress) {
|
|
4320
4635
|
const e = p.egress;
|
|
4321
|
-
if (e.enabled !== void 0
|
|
4322
|
-
|
|
4323
|
-
if (
|
|
4636
|
+
if (e.enabled !== void 0 && !(isProject && e.enabled === false))
|
|
4637
|
+
mergedPolicy.egress.enabled = e.enabled;
|
|
4638
|
+
if (e.mode !== void 0) {
|
|
4639
|
+
const weaker = isProject && rank(e.mode) < rank(mergedPolicy.egress.mode);
|
|
4640
|
+
if (!weaker) {
|
|
4641
|
+
mergedPolicy.egress.mode = e.mode;
|
|
4642
|
+
egressModeUserSet = true;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
if (Array.isArray(e.allow) && (!isProject || !egressAllowUserSet)) {
|
|
4646
|
+
mergedPolicy.egress.allow.push(...e.allow);
|
|
4647
|
+
}
|
|
4648
|
+
if (Array.isArray(e.allow) && !isProject) egressAllowUserSet = true;
|
|
4324
4649
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4325
|
-
if (e.allowPrivate !== void 0
|
|
4650
|
+
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4651
|
+
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
4326
4652
|
}
|
|
4327
4653
|
if (p.loopDetection) {
|
|
4328
4654
|
const ld = p.loopDetection;
|
|
@@ -4364,11 +4690,21 @@ function getConfig(cwd) {
|
|
|
4364
4690
|
}
|
|
4365
4691
|
}
|
|
4366
4692
|
};
|
|
4693
|
+
let egressModeUserSet = false;
|
|
4694
|
+
let egressAllowUserSet = false;
|
|
4367
4695
|
applyLayer(globalConfig);
|
|
4368
|
-
applyLayer(
|
|
4696
|
+
applyLayer(
|
|
4697
|
+
projectConfig,
|
|
4698
|
+
/* isProject */
|
|
4699
|
+
true
|
|
4700
|
+
);
|
|
4369
4701
|
let cloudManagedShields = [];
|
|
4702
|
+
const managedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4703
|
+
const lockedCommandCheckKeys = /* @__PURE__ */ new Set();
|
|
4370
4704
|
let modeCloudControlled = false;
|
|
4371
4705
|
let modeCloudStaged = false;
|
|
4706
|
+
let cloudMandatesEnforcement = false;
|
|
4707
|
+
let cloudMandatesAppPerm = false;
|
|
4372
4708
|
{
|
|
4373
4709
|
const cacheFile = path4.join(os4.homedir(), ".node9", "rules-cache.json");
|
|
4374
4710
|
try {
|
|
@@ -4403,7 +4739,8 @@ function getConfig(cwd) {
|
|
|
4403
4739
|
deny: hosts(mc.egress.deny),
|
|
4404
4740
|
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
|
|
4405
4741
|
},
|
|
4406
|
-
locked
|
|
4742
|
+
locked,
|
|
4743
|
+
egressModeUserSet
|
|
4407
4744
|
);
|
|
4408
4745
|
}
|
|
4409
4746
|
if (mc.dlp && typeof mc.dlp === "object") {
|
|
@@ -4423,6 +4760,12 @@ function getConfig(cwd) {
|
|
|
4423
4760
|
mc.commandChecks,
|
|
4424
4761
|
locked
|
|
4425
4762
|
);
|
|
4763
|
+
for (const [key, val] of Object.entries(mc.commandChecks)) {
|
|
4764
|
+
if (typeof val !== "string") continue;
|
|
4765
|
+
managedCommandCheckKeys.add(key);
|
|
4766
|
+
const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
|
|
4767
|
+
if (locked.includes(lockKey)) lockedCommandCheckKeys.add(key);
|
|
4768
|
+
}
|
|
4426
4769
|
}
|
|
4427
4770
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
4428
4771
|
const bool = (v) => typeof v === "boolean" ? v : void 0;
|
|
@@ -4469,12 +4812,13 @@ function getConfig(cwd) {
|
|
|
4469
4812
|
}
|
|
4470
4813
|
if (Array.isArray(mc.jailPaths)) {
|
|
4471
4814
|
for (const jp of mc.jailPaths) {
|
|
4472
|
-
const
|
|
4473
|
-
if (!
|
|
4815
|
+
const path14 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
4816
|
+
if (!path14) continue;
|
|
4474
4817
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4475
|
-
for (const r of pathRules(
|
|
4818
|
+
for (const r of pathRules(path14, verdict, "org-managed jail")) {
|
|
4476
4819
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4477
4820
|
}
|
|
4821
|
+
mergedPolicy.managedJailPaths.push({ path: path14, verdict });
|
|
4478
4822
|
}
|
|
4479
4823
|
}
|
|
4480
4824
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -4492,7 +4836,14 @@ function getConfig(cwd) {
|
|
|
4492
4836
|
if (Object.keys(m).length) coerced[srv] = m;
|
|
4493
4837
|
}
|
|
4494
4838
|
mergedPolicy.appPermissions = coerced;
|
|
4839
|
+
cloudMandatesAppPerm = Object.values(coerced).some(
|
|
4840
|
+
(tools) => Object.values(tools).some((d) => d === "block" || d === "review")
|
|
4841
|
+
);
|
|
4495
4842
|
}
|
|
4843
|
+
const on = (v) => !!v && typeof v === "object" && v.enabled === true;
|
|
4844
|
+
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(
|
|
4845
|
+
(v) => typeof v === "string" && v !== "off"
|
|
4846
|
+
);
|
|
4496
4847
|
}
|
|
4497
4848
|
if (raw.panicMode === true) {
|
|
4498
4849
|
mergedSettings.panicMode = true;
|
|
@@ -4534,25 +4885,45 @@ function getConfig(cwd) {
|
|
|
4534
4885
|
}
|
|
4535
4886
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
4536
4887
|
const cc = mergedPolicy.commandChecks ?? {};
|
|
4537
|
-
const
|
|
4538
|
-
if (name === "review-rm") return
|
|
4539
|
-
if (name?.endsWith("-sql")) return
|
|
4888
|
+
const advisoryKnobKey = (name) => {
|
|
4889
|
+
if (name === "review-rm") return "rmAdvisory";
|
|
4890
|
+
if (name?.endsWith("-sql")) return "sqlDdl";
|
|
4540
4891
|
return void 0;
|
|
4541
4892
|
};
|
|
4542
4893
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
4543
|
-
|
|
4544
|
-
const knob =
|
|
4894
|
+
const knobKey = rule.verdict === "review" ? advisoryKnobKey(rule.name) : void 0;
|
|
4895
|
+
const knob = knobKey ? cc[knobKey] : void 0;
|
|
4545
4896
|
if (knob === "off") continue;
|
|
4546
|
-
|
|
4897
|
+
const managed = knobKey ? managedCommandCheckKeys.has(knobKey) : false;
|
|
4898
|
+
const locked = knobKey ? lockedCommandCheckKeys.has(knobKey) : false;
|
|
4899
|
+
const twin = existingAdvisoryNames.has(rule.name) ? mergedPolicy.smartRules.find((r) => r.name === rule.name) : void 0;
|
|
4900
|
+
const knobVerdict = knob === "block" ? "block" : rule.verdict;
|
|
4901
|
+
if (!managed) {
|
|
4902
|
+
if (!twin) mergedPolicy.smartRules.push({ ...rule, verdict: knobVerdict });
|
|
4903
|
+
continue;
|
|
4904
|
+
}
|
|
4905
|
+
const effective = locked ? knobVerdict : strictestOf(VERDICT_ORDER, knobVerdict, twin?.verdict) ?? knobVerdict;
|
|
4906
|
+
if (twin) {
|
|
4907
|
+
mergedPolicy.smartRules = mergedPolicy.smartRules.filter((r) => r.name !== rule.name);
|
|
4908
|
+
}
|
|
4909
|
+
const injected = { ...rule, verdict: effective, pinned: true };
|
|
4910
|
+
if (rule.name === "review-rm" && effective !== "block") {
|
|
4911
|
+
injected.conditions = [
|
|
4912
|
+
...rule.conditions ?? [],
|
|
4913
|
+
{ field: "command", op: "notMatches", value: RM_SAFE_PATH_PATTERN }
|
|
4914
|
+
];
|
|
4915
|
+
injected.conditionMode = "all";
|
|
4916
|
+
}
|
|
4917
|
+
mergedPolicy.smartRules.push(injected);
|
|
4547
4918
|
}
|
|
4548
4919
|
const envMode = process.env.NODE9_MODE;
|
|
4549
4920
|
if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
|
|
4550
4921
|
mergedSettings.mode = envMode;
|
|
4551
4922
|
}
|
|
4552
|
-
if (cloudManagedShields.length > 0 && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4923
|
+
if ((cloudManagedShields.length > 0 || cloudMandatesEnforcement) && !modeCloudControlled && !modeCloudStaged && (mergedSettings.mode === "observe" || mergedSettings.mode === "audit")) {
|
|
4553
4924
|
mergedSettings.mode = "standard";
|
|
4554
4925
|
}
|
|
4555
|
-
const managedFloorActive = cloudManagedShields.length > 0 || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4926
|
+
const managedFloorActive = cloudManagedShields.length > 0 || cloudMandatesEnforcement || modeCloudControlled && mergedSettings.mode === "strict";
|
|
4556
4927
|
if (modeCloudControlled && mergedSettings.mode === "strict") {
|
|
4557
4928
|
for (const name of Object.keys(mergedEnvironments)) {
|
|
4558
4929
|
if (mergedEnvironments[name]?.requireApproval === false) {
|
|
@@ -4753,14 +5124,14 @@ function checkProvenance(cmd, cwd) {
|
|
|
4753
5124
|
}
|
|
4754
5125
|
|
|
4755
5126
|
// src/policy/index.ts
|
|
4756
|
-
async function evaluatePolicy2(toolName, args, agent, cwd) {
|
|
5127
|
+
async function evaluatePolicy2(toolName, args, agent, cwd, opts) {
|
|
4757
5128
|
const config = getConfig();
|
|
4758
5129
|
const activeEnvironment = getActiveEnvironment(config) ?? void 0;
|
|
4759
5130
|
return evaluatePolicy(
|
|
4760
5131
|
config,
|
|
4761
5132
|
toolName,
|
|
4762
5133
|
args,
|
|
4763
|
-
{ agent, cwd, activeEnvironment },
|
|
5134
|
+
{ agent, cwd, activeEnvironment, skipIgnoredFastPath: opts?.skipIgnoredFastPath },
|
|
4764
5135
|
{
|
|
4765
5136
|
checkProvenance,
|
|
4766
5137
|
// Managed → match against the org list (frozen with the rest of managed
|
|
@@ -5612,6 +5983,44 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
5612
5983
|
}
|
|
5613
5984
|
}
|
|
5614
5985
|
|
|
5986
|
+
// src/shields/jail.ts
|
|
5987
|
+
import fs11 from "fs";
|
|
5988
|
+
import os10 from "os";
|
|
5989
|
+
import path13 from "path";
|
|
5990
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
5991
|
+
function jailStorePath() {
|
|
5992
|
+
return path13.join(os10.homedir(), ".node9", "jail-paths.json");
|
|
5993
|
+
}
|
|
5994
|
+
function readJailPaths() {
|
|
5995
|
+
let text;
|
|
5996
|
+
try {
|
|
5997
|
+
text = fs11.readFileSync(jailStorePath(), "utf8");
|
|
5998
|
+
} catch (err) {
|
|
5999
|
+
if (err.code === "ENOENT") return [];
|
|
6000
|
+
throw err;
|
|
6001
|
+
}
|
|
6002
|
+
let parsed;
|
|
6003
|
+
try {
|
|
6004
|
+
parsed = JSON.parse(text);
|
|
6005
|
+
} catch {
|
|
6006
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
6007
|
+
}
|
|
6008
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
6009
|
+
return parsed.paths.filter(
|
|
6010
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
6011
|
+
);
|
|
6012
|
+
}
|
|
6013
|
+
function findJailedPath(candidate) {
|
|
6014
|
+
return findJailedPathIn(candidate, readJailPaths());
|
|
6015
|
+
}
|
|
6016
|
+
function findJailedPathIn(candidate, paths) {
|
|
6017
|
+
if (!candidate) return null;
|
|
6018
|
+
for (const entry of paths) {
|
|
6019
|
+
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
6020
|
+
}
|
|
6021
|
+
return null;
|
|
6022
|
+
}
|
|
6023
|
+
|
|
5615
6024
|
// src/auth/orchestrator.ts
|
|
5616
6025
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
5617
6026
|
"write",
|
|
@@ -6087,12 +6496,35 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6087
6496
|
} else if (!taintWarning && !appPermReview) {
|
|
6088
6497
|
const toolLower = toolName.toLowerCase();
|
|
6089
6498
|
const isFileTool = toolLower === "read" || toolLower === "grep" || toolLower === "glob" || toolLower === "read_file" || toolLower === "grep_search" || toolLower === "list_files";
|
|
6090
|
-
|
|
6499
|
+
const activeShields = isFileTool ? readActiveShields() : [];
|
|
6500
|
+
const managedJail = isFileTool ? config.policy.managedJailPaths ?? [] : [];
|
|
6501
|
+
if (isFileTool && (activeShields.includes("project-jail") || activeShields.includes(USER_JAIL_SHIELD) || managedJail.length > 0)) {
|
|
6091
6502
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
);
|
|
6095
|
-
if (
|
|
6503
|
+
const candidates = ["file_path", "path", "pattern", "filename"].map((k) => argsObj[k]).filter((v) => typeof v === "string" && v.length > 0);
|
|
6504
|
+
const jailHit = (candidates.map(findJailedPath).find(Boolean) ?? candidates.map((c) => findJailedPathIn(c, managedJail)).find(Boolean)) || null;
|
|
6505
|
+
const sensitiveHit = candidates.some((c) => scanFilePath(c));
|
|
6506
|
+
if (jailHit || sensitiveHit) {
|
|
6507
|
+
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
6508
|
+
skipIgnoredFastPath: true
|
|
6509
|
+
});
|
|
6510
|
+
if (policyResult.decision === "block") {
|
|
6511
|
+
if (!isManual)
|
|
6512
|
+
appendLocalAudit(
|
|
6513
|
+
toolName,
|
|
6514
|
+
args,
|
|
6515
|
+
"deny",
|
|
6516
|
+
"smart-rule-block",
|
|
6517
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
6518
|
+
hashAuditArgs
|
|
6519
|
+
);
|
|
6520
|
+
return {
|
|
6521
|
+
approved: false,
|
|
6522
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
6523
|
+
blockedBy: "local-config",
|
|
6524
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
6525
|
+
ruleHit: policyResult.ruleName
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6096
6528
|
} else {
|
|
6097
6529
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta, hashAuditArgs);
|
|
6098
6530
|
return { approved: true };
|
|
@@ -6155,7 +6587,12 @@ ${appPermReview}`
|
|
|
6155
6587
|
}
|
|
6156
6588
|
let cloudRequestId = null;
|
|
6157
6589
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
6158
|
-
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview ||
|
|
6590
|
+
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || !!appPermReview || // Task #16 vector C: a taint review needs a GENUINE pending entry. Taint is
|
|
6591
|
+
// a client-side heuristic the SaaS has no rule for, so without forceReview
|
|
6592
|
+
// its checkRule answers "no org rule matched" → {approved:true}, which is
|
|
6593
|
+
// not an approval of an exfiltration risk. Measured against the live BE:
|
|
6594
|
+
// {approved:true} without this flag, {pending:true} with it.
|
|
6595
|
+
!!taintWarning || void 0;
|
|
6159
6596
|
if (cloudEnforced) {
|
|
6160
6597
|
try {
|
|
6161
6598
|
const initResult = await initNode9SaaS(
|
|
@@ -6168,10 +6605,10 @@ ${appPermReview}`
|
|
|
6168
6605
|
forceReview
|
|
6169
6606
|
);
|
|
6170
6607
|
if (!initResult.pending) {
|
|
6171
|
-
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6608
|
+
if (initResult.shadowMode && !localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6172
6609
|
return { approved: true, checkedBy: "cloud" };
|
|
6173
6610
|
}
|
|
6174
|
-
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview) {
|
|
6611
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched && !appPermReview && !taintWarning) {
|
|
6175
6612
|
return {
|
|
6176
6613
|
approved: !!initResult.approved,
|
|
6177
6614
|
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|