@node9/policy-engine 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/index.d.mts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +469 -192
- package/dist/index.mjs +466 -192
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ __export(src_exports, {
|
|
|
66
66
|
detectArgsPii: () => detectArgsPii,
|
|
67
67
|
detectDangerousEval: () => detectDangerousEval,
|
|
68
68
|
detectDangerousShellExec: () => detectDangerousShellExec,
|
|
69
|
+
detectInlineExec: () => detectInlineExec,
|
|
69
70
|
detectPii: () => detectPii,
|
|
70
71
|
evaluateEgress: () => evaluateEgress,
|
|
71
72
|
evaluateLoopWindow: () => evaluateLoopWindow,
|
|
@@ -84,6 +85,7 @@ __export(src_exports, {
|
|
|
84
85
|
isIgnoredTool: () => isIgnoredTool,
|
|
85
86
|
isPrivateHost: () => isPrivateHost,
|
|
86
87
|
isProtectedHomePath: () => isProtectedHomePath,
|
|
88
|
+
isShellShapedTool: () => isShellShapedTool,
|
|
87
89
|
isShieldVerdict: () => isShieldVerdict,
|
|
88
90
|
matchSensitivePath: () => matchSensitivePath,
|
|
89
91
|
matchesPattern: () => matchesPattern,
|
|
@@ -101,6 +103,7 @@ __export(src_exports, {
|
|
|
101
103
|
summarizeBlast: () => summarizeBlast,
|
|
102
104
|
summarizeScan: () => summarizeScan,
|
|
103
105
|
toScanFinding: () => toScanFinding,
|
|
106
|
+
toolMatchesRule: () => toolMatchesRule,
|
|
104
107
|
truncateBlastPath: () => truncateBlastPath,
|
|
105
108
|
validateOverrides: () => validateOverrides,
|
|
106
109
|
validateRegex: () => validateRegex,
|
|
@@ -806,6 +809,139 @@ function redactText(text) {
|
|
|
806
809
|
|
|
807
810
|
// src/shell/index.ts
|
|
808
811
|
var import_mvdan_sh = __toESM(require("mvdan-sh"));
|
|
812
|
+
|
|
813
|
+
// src/rules/index.ts
|
|
814
|
+
var import_picomatch = __toESM(require("picomatch"));
|
|
815
|
+
|
|
816
|
+
// src/utils/regex.ts
|
|
817
|
+
var import_safe_regex22 = __toESM(require("safe-regex2"));
|
|
818
|
+
var MAX_REGEX_LENGTH = 256;
|
|
819
|
+
var REGEX_CACHE_MAX = 500;
|
|
820
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
821
|
+
function validateRegex(pattern) {
|
|
822
|
+
if (!pattern) return "Pattern is required";
|
|
823
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
824
|
+
try {
|
|
825
|
+
new RegExp(pattern);
|
|
826
|
+
} catch (e) {
|
|
827
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
828
|
+
}
|
|
829
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
830
|
+
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
834
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
835
|
+
const key = `${pattern}\0${flags}`;
|
|
836
|
+
if (regexCache.has(key)) {
|
|
837
|
+
const cached = regexCache.get(key);
|
|
838
|
+
regexCache.delete(key);
|
|
839
|
+
regexCache.set(key, cached);
|
|
840
|
+
return cached;
|
|
841
|
+
}
|
|
842
|
+
if (validateRegex(pattern) !== null) return null;
|
|
843
|
+
try {
|
|
844
|
+
const re = new RegExp(pattern, flags);
|
|
845
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
846
|
+
const oldest = regexCache.keys().next().value;
|
|
847
|
+
if (oldest) regexCache.delete(oldest);
|
|
848
|
+
}
|
|
849
|
+
regexCache.set(key, re);
|
|
850
|
+
return re;
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/rules/index.ts
|
|
857
|
+
function matchesPattern(text, patterns) {
|
|
858
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
859
|
+
if (p.length === 0) return false;
|
|
860
|
+
const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
|
|
861
|
+
const target = text.toLowerCase();
|
|
862
|
+
const directMatch = isMatch(target);
|
|
863
|
+
if (directMatch) return true;
|
|
864
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
865
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
866
|
+
}
|
|
867
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
868
|
+
function getNestedValue(obj, path) {
|
|
869
|
+
if (!obj || typeof obj !== "object") return null;
|
|
870
|
+
const segments = path.split(".");
|
|
871
|
+
for (const seg of segments) {
|
|
872
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
873
|
+
}
|
|
874
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
875
|
+
}
|
|
876
|
+
function evaluateSmartConditions(args, rule) {
|
|
877
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
878
|
+
const mode = rule.conditionMode ?? "all";
|
|
879
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
880
|
+
const resolveField = (field) => {
|
|
881
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
882
|
+
const rawVal = getNestedValue(args, field);
|
|
883
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
884
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
885
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
886
|
+
fieldCache.set(field, val);
|
|
887
|
+
return val;
|
|
888
|
+
};
|
|
889
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
890
|
+
const resolveFieldReadings = (field) => {
|
|
891
|
+
const cached = readingsCache.get(field);
|
|
892
|
+
if (cached) return cached;
|
|
893
|
+
const primary = resolveField(field);
|
|
894
|
+
if (primary === null) {
|
|
895
|
+
readingsCache.set(field, []);
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
let out = [primary];
|
|
899
|
+
if (field === "command") {
|
|
900
|
+
const raw = getNestedValue(args, field);
|
|
901
|
+
if (typeof raw === "string") {
|
|
902
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
903
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
readingsCache.set(field, out);
|
|
907
|
+
return out;
|
|
908
|
+
};
|
|
909
|
+
const results = rule.conditions.map((cond) => {
|
|
910
|
+
const val = resolveField(cond.field);
|
|
911
|
+
switch (cond.op) {
|
|
912
|
+
case "exists":
|
|
913
|
+
return val !== null && val !== "";
|
|
914
|
+
case "notExists":
|
|
915
|
+
return val === null || val === "";
|
|
916
|
+
case "contains":
|
|
917
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
918
|
+
case "notContains":
|
|
919
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
920
|
+
case "matches": {
|
|
921
|
+
if (val === null || !cond.value) return false;
|
|
922
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
923
|
+
if (!reM) return false;
|
|
924
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
925
|
+
}
|
|
926
|
+
case "notMatches": {
|
|
927
|
+
if (!cond.value) return false;
|
|
928
|
+
if (val === null) return true;
|
|
929
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
930
|
+
if (!reN) return false;
|
|
931
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
932
|
+
}
|
|
933
|
+
case "matchesGlob":
|
|
934
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
935
|
+
case "notMatchesGlob":
|
|
936
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
937
|
+
default:
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/shell/index.ts
|
|
809
945
|
var { syntax } = import_mvdan_sh.default;
|
|
810
946
|
var sharedParser = syntax.NewParser();
|
|
811
947
|
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -878,14 +1014,22 @@ function cachedNormalize(command, compute) {
|
|
|
878
1014
|
return result;
|
|
879
1015
|
}
|
|
880
1016
|
function normalizeCommandForPolicy(command) {
|
|
1017
|
+
return commandReadingsImpl(command).posix;
|
|
1018
|
+
}
|
|
1019
|
+
function commandReadings(command) {
|
|
1020
|
+
const r = commandReadingsImpl(command);
|
|
1021
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
1022
|
+
}
|
|
1023
|
+
function commandReadingsImpl(command) {
|
|
881
1024
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
882
1025
|
}
|
|
883
1026
|
function normalizeCommandForPolicyImpl(command) {
|
|
884
1027
|
const f = parseShared(command);
|
|
885
|
-
if (f === PARSE_FAIL) return command;
|
|
1028
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
886
1029
|
try {
|
|
887
1030
|
const strips = [];
|
|
888
1031
|
const rewrites = [];
|
|
1032
|
+
const quoteOnlyRewrites = [];
|
|
889
1033
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
890
1034
|
syntax.Walk(f, (node) => {
|
|
891
1035
|
if (!node) return false;
|
|
@@ -930,22 +1074,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
930
1074
|
if (resolved === source) continue;
|
|
931
1075
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
932
1076
|
rewrites.push([s, e, resolved]);
|
|
1077
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
1078
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
933
1079
|
}
|
|
934
1080
|
return true;
|
|
935
1081
|
});
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
...
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
return result;
|
|
1082
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
1083
|
+
const apply = (extra) => {
|
|
1084
|
+
const edits = [...stripEdits, ...extra];
|
|
1085
|
+
if (edits.length === 0) return command;
|
|
1086
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
1087
|
+
let out = command;
|
|
1088
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
1089
|
+
return out;
|
|
1090
|
+
};
|
|
1091
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
947
1092
|
} catch {
|
|
948
|
-
return command;
|
|
1093
|
+
return { posix: command, separator: command };
|
|
949
1094
|
}
|
|
950
1095
|
}
|
|
951
1096
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -1015,9 +1160,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1015
1160
|
"vi",
|
|
1016
1161
|
"emacs",
|
|
1017
1162
|
"code",
|
|
1018
|
-
"type"
|
|
1163
|
+
"type",
|
|
1164
|
+
// — the 22 that were missing —
|
|
1165
|
+
"grep",
|
|
1166
|
+
"egrep",
|
|
1167
|
+
"fgrep",
|
|
1168
|
+
"rg",
|
|
1169
|
+
"ag",
|
|
1170
|
+
"ack",
|
|
1171
|
+
"awk",
|
|
1172
|
+
"gawk",
|
|
1173
|
+
"sed",
|
|
1174
|
+
"cut",
|
|
1175
|
+
"tr",
|
|
1176
|
+
"jq",
|
|
1177
|
+
"yq",
|
|
1178
|
+
"od",
|
|
1179
|
+
"xxd",
|
|
1180
|
+
"hexdump",
|
|
1181
|
+
"strings",
|
|
1182
|
+
"sort",
|
|
1183
|
+
"uniq",
|
|
1184
|
+
"tac",
|
|
1185
|
+
"nl",
|
|
1186
|
+
"dd"
|
|
1019
1187
|
]);
|
|
1020
|
-
var FS_OP_PRESCREEN_RE =
|
|
1188
|
+
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1189
|
+
`(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
|
|
1190
|
+
);
|
|
1021
1191
|
var HOME_CACHE_ALLOWLIST = [
|
|
1022
1192
|
".cache",
|
|
1023
1193
|
".npm/_npx",
|
|
@@ -1056,9 +1226,37 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1056
1226
|
// for the canonical test-asserted contract.
|
|
1057
1227
|
rule: "shield:project-jail:block-read-env",
|
|
1058
1228
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1229
|
+
// Structural, not a list. The previous form enumerated seven suffixes and
|
|
1230
|
+
// anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
|
|
1231
|
+
// gitignored, all routinely holding real secrets — were never covered. A
|
|
1232
|
+
// hand-written list of what to protect is only ever as complete as the day
|
|
1233
|
+
// it was typed; this says "`.env` plus any suffix chain" and then names the
|
|
1234
|
+
// exceptions, which is the direction that fails safe.
|
|
1235
|
+
//
|
|
1236
|
+
// \.env the segment itself
|
|
1237
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1238
|
+
// files. Without it a flat suffix class swallows both.
|
|
1239
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1240
|
+
// [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
|
|
1241
|
+
// `(\.[\w-]+)*` reads the same but is rejected by
|
|
1242
|
+
// safe-regex2, and this pattern runs on the hook hot path.
|
|
1243
|
+
//
|
|
1244
|
+
// The two exclusions are NOT the same shape, because the words do not mean
|
|
1245
|
+
// the same thing:
|
|
1246
|
+
//
|
|
1247
|
+
// (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
|
|
1248
|
+
// stays a fixture whatever follows, so `.env.example.md` is allowed too.
|
|
1249
|
+
// These are checked into git by convention: already public, so blocking
|
|
1250
|
+
// them buys nothing and costs the most common legitimate agent read.
|
|
1251
|
+
//
|
|
1252
|
+
// (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
|
|
1253
|
+
// fixture. `.env.test` is the committed template and stays allowed, but
|
|
1254
|
+
// `.env.test.local` is gitignored by the `.env*.local` convention and
|
|
1255
|
+
// holds real values, so it must block. Using `\b` here — the obvious
|
|
1256
|
+
// symmetry — silently exempts every `.env.test.*` file.
|
|
1257
|
+
//
|
|
1258
|
+
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1259
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
|
|
1062
1260
|
},
|
|
1063
1261
|
{
|
|
1064
1262
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1187,6 +1385,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1187
1385
|
}
|
|
1188
1386
|
return found;
|
|
1189
1387
|
}
|
|
1388
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1389
|
+
if (isBashTool(toolName)) return true;
|
|
1390
|
+
if (!toolInspection) return false;
|
|
1391
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1392
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1393
|
+
}
|
|
1394
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1395
|
+
if (!ruleTool) return true;
|
|
1396
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1397
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1398
|
+
}
|
|
1399
|
+
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;
|
|
1400
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1401
|
+
"uv",
|
|
1402
|
+
"uvx",
|
|
1403
|
+
"poetry",
|
|
1404
|
+
"pipenv",
|
|
1405
|
+
"pdm",
|
|
1406
|
+
"rye",
|
|
1407
|
+
"hatch",
|
|
1408
|
+
"conda",
|
|
1409
|
+
"mamba",
|
|
1410
|
+
"micromamba",
|
|
1411
|
+
"npx",
|
|
1412
|
+
"pnpm",
|
|
1413
|
+
"yarn",
|
|
1414
|
+
"bunx",
|
|
1415
|
+
"watch",
|
|
1416
|
+
"strace",
|
|
1417
|
+
"ltrace",
|
|
1418
|
+
"chroot",
|
|
1419
|
+
"unshare",
|
|
1420
|
+
"runuser"
|
|
1421
|
+
]);
|
|
1422
|
+
function isInlineCodeFlag(interp, w) {
|
|
1423
|
+
const lw = w.toLowerCase();
|
|
1424
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1425
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1426
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1427
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1428
|
+
const body = lw.slice(1);
|
|
1429
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1430
|
+
const cut = body.search(cutAt);
|
|
1431
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1432
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1433
|
+
}
|
|
1434
|
+
var _redirStdinOps = null;
|
|
1435
|
+
function redirStdinOps() {
|
|
1436
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1437
|
+
_redirStdinOps = new Set(
|
|
1438
|
+
[
|
|
1439
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1440
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1441
|
+
deriveRedirOp("cat < f"),
|
|
1442
|
+
deriveRedirOp("cat <<< x")
|
|
1443
|
+
].filter((op) => op >= 0)
|
|
1444
|
+
);
|
|
1445
|
+
return _redirStdinOps;
|
|
1446
|
+
}
|
|
1447
|
+
function deriveBinaryOp(sample) {
|
|
1448
|
+
try {
|
|
1449
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1450
|
+
let op = -1;
|
|
1451
|
+
syntax.Walk(f, (node) => {
|
|
1452
|
+
const n = node;
|
|
1453
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1454
|
+
return true;
|
|
1455
|
+
});
|
|
1456
|
+
return op;
|
|
1457
|
+
} catch {
|
|
1458
|
+
return -1;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
var _listOps = null;
|
|
1462
|
+
function listOps() {
|
|
1463
|
+
if (_listOps) return _listOps;
|
|
1464
|
+
_listOps = new Set(
|
|
1465
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1466
|
+
);
|
|
1467
|
+
return _listOps;
|
|
1468
|
+
}
|
|
1469
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1470
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1471
|
+
function unwrapCommandHead(words) {
|
|
1472
|
+
let i = 0;
|
|
1473
|
+
while (i < words.length) {
|
|
1474
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1475
|
+
if (head === "find") {
|
|
1476
|
+
const x = words.findIndex(
|
|
1477
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1478
|
+
);
|
|
1479
|
+
if (x < 0) break;
|
|
1480
|
+
i = x + 1;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1484
|
+
i++;
|
|
1485
|
+
let targetConsumed = false;
|
|
1486
|
+
while (i < words.length) {
|
|
1487
|
+
const t = words[i];
|
|
1488
|
+
if (t === null) {
|
|
1489
|
+
i++;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const lt = t.toLowerCase();
|
|
1493
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1494
|
+
i++;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (t.startsWith("-")) {
|
|
1498
|
+
i++;
|
|
1499
|
+
const nxt = words[i];
|
|
1500
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1501
|
+
i++;
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1505
|
+
i++;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1509
|
+
targetConsumed = true;
|
|
1510
|
+
i++;
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return i;
|
|
1517
|
+
}
|
|
1518
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1519
|
+
const cmd = stmt?.Cmd;
|
|
1520
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1521
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1522
|
+
if (words.length === 0) return false;
|
|
1523
|
+
const headIdx = unwrapCommandHead(words);
|
|
1524
|
+
const rawHead = words[headIdx];
|
|
1525
|
+
if (rawHead == null) return false;
|
|
1526
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1527
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1528
|
+
let args = words.slice(headIdx + 1);
|
|
1529
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1530
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1531
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1532
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1533
|
+
}
|
|
1534
|
+
let positionals = 0;
|
|
1535
|
+
let selectedProgram = false;
|
|
1536
|
+
for (const a of args) {
|
|
1537
|
+
if (a == null) {
|
|
1538
|
+
positionals++;
|
|
1539
|
+
selectedProgram = true;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1543
|
+
if (a === "-m") {
|
|
1544
|
+
selectedProgram = true;
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1548
|
+
if (!a.startsWith("-")) {
|
|
1549
|
+
positionals++;
|
|
1550
|
+
selectedProgram = true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1554
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1555
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1556
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1557
|
+
}
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
function detectInlineExec(command) {
|
|
1561
|
+
const f = parseShared(command);
|
|
1562
|
+
if (f === PARSE_FAIL) {
|
|
1563
|
+
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(
|
|
1564
|
+
command
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
let found = false;
|
|
1568
|
+
try {
|
|
1569
|
+
syntax.Walk(f, (node) => {
|
|
1570
|
+
if (!node || found) return false;
|
|
1571
|
+
const n = node;
|
|
1572
|
+
const t = syntax.NodeType(n);
|
|
1573
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1574
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1575
|
+
found = true;
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1580
|
+
found = true;
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
return true;
|
|
1584
|
+
});
|
|
1585
|
+
} catch {
|
|
1586
|
+
return found;
|
|
1587
|
+
}
|
|
1588
|
+
return found;
|
|
1589
|
+
}
|
|
1190
1590
|
function analyzeChmod777(command) {
|
|
1191
1591
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1192
1592
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2064,117 +2464,6 @@ function parseAllSshHostsFromCommand(command) {
|
|
|
2064
2464
|
return extractAllSshHosts(tokens.slice(1));
|
|
2065
2465
|
}
|
|
2066
2466
|
|
|
2067
|
-
// src/rules/index.ts
|
|
2068
|
-
var import_picomatch = __toESM(require("picomatch"));
|
|
2069
|
-
|
|
2070
|
-
// src/utils/regex.ts
|
|
2071
|
-
var import_safe_regex22 = __toESM(require("safe-regex2"));
|
|
2072
|
-
var MAX_REGEX_LENGTH = 100;
|
|
2073
|
-
var REGEX_CACHE_MAX = 500;
|
|
2074
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
2075
|
-
function validateRegex(pattern) {
|
|
2076
|
-
if (!pattern) return "Pattern is required";
|
|
2077
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
2078
|
-
try {
|
|
2079
|
-
new RegExp(pattern);
|
|
2080
|
-
} catch (e) {
|
|
2081
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
2082
|
-
}
|
|
2083
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
2084
|
-
if (!(0, import_safe_regex22.default)(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
2085
|
-
return null;
|
|
2086
|
-
}
|
|
2087
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
2088
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
2089
|
-
const key = `${pattern}\0${flags}`;
|
|
2090
|
-
if (regexCache.has(key)) {
|
|
2091
|
-
const cached = regexCache.get(key);
|
|
2092
|
-
regexCache.delete(key);
|
|
2093
|
-
regexCache.set(key, cached);
|
|
2094
|
-
return cached;
|
|
2095
|
-
}
|
|
2096
|
-
if (validateRegex(pattern) !== null) return null;
|
|
2097
|
-
try {
|
|
2098
|
-
const re = new RegExp(pattern, flags);
|
|
2099
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
2100
|
-
const oldest = regexCache.keys().next().value;
|
|
2101
|
-
if (oldest) regexCache.delete(oldest);
|
|
2102
|
-
}
|
|
2103
|
-
regexCache.set(key, re);
|
|
2104
|
-
return re;
|
|
2105
|
-
} catch {
|
|
2106
|
-
return null;
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
// src/rules/index.ts
|
|
2111
|
-
function matchesPattern(text, patterns) {
|
|
2112
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2113
|
-
if (p.length === 0) return false;
|
|
2114
|
-
const isMatch = (0, import_picomatch.default)(p, { nocase: true, dot: true });
|
|
2115
|
-
const target = text.toLowerCase();
|
|
2116
|
-
const directMatch = isMatch(target);
|
|
2117
|
-
if (directMatch) return true;
|
|
2118
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2119
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2120
|
-
}
|
|
2121
|
-
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2122
|
-
function getNestedValue(obj, path) {
|
|
2123
|
-
if (!obj || typeof obj !== "object") return null;
|
|
2124
|
-
const segments = path.split(".");
|
|
2125
|
-
for (const seg of segments) {
|
|
2126
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2127
|
-
}
|
|
2128
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2129
|
-
}
|
|
2130
|
-
function evaluateSmartConditions(args, rule) {
|
|
2131
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2132
|
-
const mode = rule.conditionMode ?? "all";
|
|
2133
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
2134
|
-
const resolveField = (field) => {
|
|
2135
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
2136
|
-
const rawVal = getNestedValue(args, field);
|
|
2137
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
2138
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
2139
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
2140
|
-
fieldCache.set(field, val);
|
|
2141
|
-
return val;
|
|
2142
|
-
};
|
|
2143
|
-
const results = rule.conditions.map((cond) => {
|
|
2144
|
-
const val = resolveField(cond.field);
|
|
2145
|
-
switch (cond.op) {
|
|
2146
|
-
case "exists":
|
|
2147
|
-
return val !== null && val !== "";
|
|
2148
|
-
case "notExists":
|
|
2149
|
-
return val === null || val === "";
|
|
2150
|
-
case "contains":
|
|
2151
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2152
|
-
case "notContains":
|
|
2153
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2154
|
-
case "matches": {
|
|
2155
|
-
if (val === null || !cond.value) return false;
|
|
2156
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2157
|
-
if (!reM) return false;
|
|
2158
|
-
return reM.test(val);
|
|
2159
|
-
}
|
|
2160
|
-
case "notMatches": {
|
|
2161
|
-
if (!cond.value) return false;
|
|
2162
|
-
if (val === null) return true;
|
|
2163
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2164
|
-
if (!reN) return false;
|
|
2165
|
-
return !reN.test(val);
|
|
2166
|
-
}
|
|
2167
|
-
case "matchesGlob":
|
|
2168
|
-
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2169
|
-
case "notMatchesGlob":
|
|
2170
|
-
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
2171
|
-
default:
|
|
2172
|
-
return false;
|
|
2173
|
-
}
|
|
2174
|
-
});
|
|
2175
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
2467
|
// src/policy/index.ts
|
|
2179
2468
|
function resolveCheck(v) {
|
|
2180
2469
|
return v === "off" || v === "block" ? v : "review";
|
|
@@ -2182,41 +2471,6 @@ function resolveCheck(v) {
|
|
|
2182
2471
|
function resolveCheckTight(v) {
|
|
2183
2472
|
return v === "block" ? "block" : "review";
|
|
2184
2473
|
}
|
|
2185
|
-
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2186
|
-
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2187
|
-
function detectInlineExec(command) {
|
|
2188
|
-
const pipeFed = command.includes("|");
|
|
2189
|
-
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2190
|
-
for (const rawSeg of segments) {
|
|
2191
|
-
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2192
|
-
let i = 0;
|
|
2193
|
-
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2194
|
-
if (i >= tokens.length) continue;
|
|
2195
|
-
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2196
|
-
if (!INLINE_INTERP.test(base)) continue;
|
|
2197
|
-
const args = tokens.slice(i + 1);
|
|
2198
|
-
let hadRedirect = false;
|
|
2199
|
-
const positionals = [];
|
|
2200
|
-
for (let j = 0; j < args.length; j++) {
|
|
2201
|
-
const a = args[j];
|
|
2202
|
-
if (a === "-") return true;
|
|
2203
|
-
if (a.startsWith("<")) {
|
|
2204
|
-
hadRedirect = true;
|
|
2205
|
-
if (a === "<" || a === "<<") j++;
|
|
2206
|
-
continue;
|
|
2207
|
-
}
|
|
2208
|
-
if (a.startsWith("-")) {
|
|
2209
|
-
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
positionals.push(a);
|
|
2213
|
-
}
|
|
2214
|
-
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2215
|
-
return true;
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
return false;
|
|
2219
|
-
}
|
|
2220
2474
|
var VERDICT_RANK = {
|
|
2221
2475
|
allow: 0,
|
|
2222
2476
|
review: 1,
|
|
@@ -2230,6 +2484,12 @@ function resolvePinned(matches) {
|
|
|
2230
2484
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2231
2485
|
);
|
|
2232
2486
|
}
|
|
2487
|
+
function strictestVerdict(candidates) {
|
|
2488
|
+
if (candidates.length === 0) return void 0;
|
|
2489
|
+
return candidates.reduce(
|
|
2490
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2233
2493
|
function tokenize2(toolName) {
|
|
2234
2494
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2235
2495
|
}
|
|
@@ -2241,6 +2501,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2241
2501
|
const value = getNestedValue(args, fieldPath);
|
|
2242
2502
|
return typeof value === "string" ? value : null;
|
|
2243
2503
|
}
|
|
2504
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2505
|
+
const patterns = Object.keys(toolInspection);
|
|
2506
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2507
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2508
|
+
}
|
|
2244
2509
|
function isSqlTool(toolName, toolInspection) {
|
|
2245
2510
|
const patterns = Object.keys(toolInspection);
|
|
2246
2511
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2312,8 +2577,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2312
2577
|
};
|
|
2313
2578
|
}
|
|
2314
2579
|
}
|
|
2315
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2316
|
-
const
|
|
2580
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2581
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2582
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2583
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2317
2584
|
if (bashCommand !== null) {
|
|
2318
2585
|
const pipeVerdict = pipeChainVerdict(
|
|
2319
2586
|
bashCommand,
|
|
@@ -2363,8 +2630,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2363
2630
|
}
|
|
2364
2631
|
if (config.policy.smartRules.length > 0) {
|
|
2365
2632
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2633
|
+
const astSuppressed = (rule) => {
|
|
2634
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2635
|
+
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;
|
|
2636
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2637
|
+
return true;
|
|
2638
|
+
};
|
|
2366
2639
|
const matches = config.policy.smartRules.filter(
|
|
2367
|
-
(rule) =>
|
|
2640
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2368
2641
|
);
|
|
2369
2642
|
const matchedRule = resolvePinned(matches);
|
|
2370
2643
|
if (matchedRule) {
|
|
@@ -2395,15 +2668,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2395
2668
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2396
2669
|
allTokens = analyzed.allTokens;
|
|
2397
2670
|
pathTokens = analyzed.paths;
|
|
2398
|
-
const
|
|
2399
|
-
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2400
|
-
return {
|
|
2401
|
-
decision: inlineAction === "block" ? "block" : "review",
|
|
2402
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2403
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2404
|
-
tier: 3
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2671
|
+
const candidates = [];
|
|
2407
2672
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2408
2673
|
if (evalVerdict === "block") {
|
|
2409
2674
|
return {
|
|
@@ -2414,24 +2679,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2414
2679
|
tier: 3
|
|
2415
2680
|
};
|
|
2416
2681
|
}
|
|
2682
|
+
const ptVerdict = pipeChainVerdict(
|
|
2683
|
+
shellCommand,
|
|
2684
|
+
isTrustedHost,
|
|
2685
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2686
|
+
);
|
|
2687
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2688
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2689
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2690
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2691
|
+
candidates.push({
|
|
2692
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2693
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2694
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2695
|
+
tier: 3
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2417
2698
|
if (evalVerdict === "review") {
|
|
2418
|
-
|
|
2419
|
-
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2420
|
-
// block but can never turn this off (eval-remote above is Class A —
|
|
2421
|
-
// no knob at all).
|
|
2699
|
+
candidates.push({
|
|
2422
2700
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2423
2701
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2424
2702
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2425
2703
|
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.",
|
|
2426
2704
|
tier: 3
|
|
2427
|
-
};
|
|
2705
|
+
});
|
|
2428
2706
|
}
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
isTrustedHost,
|
|
2432
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2433
|
-
);
|
|
2434
|
-
if (ptVerdict) return ptVerdict;
|
|
2707
|
+
const builtin = strictestVerdict(candidates);
|
|
2708
|
+
if (builtin) return builtin;
|
|
2435
2709
|
if (config.policy.egress?.enabled) {
|
|
2436
2710
|
const dests = extractShellDestinations(shellCommand);
|
|
2437
2711
|
if (dests.length > 0) {
|
|
@@ -3696,15 +3970,15 @@ function detectArgsPii(args) {
|
|
|
3696
3970
|
|
|
3697
3971
|
// src/scan/canonical.ts
|
|
3698
3972
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3699
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
3700
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
3973
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v9";
|
|
3974
|
+
var CANONICAL_EXTRACTOR_HASH = "4ebf40dfe1d7c0a1";
|
|
3701
3975
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
3702
3976
|
function extractCanonicalFindings(call, ctx) {
|
|
3703
3977
|
const out = [];
|
|
3704
3978
|
const ts = call.timestamp;
|
|
3705
3979
|
const toolNameLower = call.toolName.toLowerCase();
|
|
3706
3980
|
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
3707
|
-
const
|
|
3981
|
+
const isShell = isShellShapedTool(call.toolName, ctx.toolInspection) && command !== null;
|
|
3708
3982
|
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
3709
3983
|
out.push(
|
|
3710
3984
|
makeFinding({
|
|
@@ -3779,7 +4053,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3779
4053
|
);
|
|
3780
4054
|
}
|
|
3781
4055
|
}
|
|
3782
|
-
if (!
|
|
4056
|
+
if (!isShell || command === null) {
|
|
3783
4057
|
return out;
|
|
3784
4058
|
}
|
|
3785
4059
|
const fsVerdict = analyzeFsOperation(command);
|
|
@@ -3805,7 +4079,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3805
4079
|
for (const source of ctx.rules) {
|
|
3806
4080
|
const r = source.rule;
|
|
3807
4081
|
if (r.verdict === "allow") continue;
|
|
3808
|
-
if (
|
|
4082
|
+
if (!toolMatchesRule(toolNameLower, r.tool, ctx.toolInspection)) continue;
|
|
3809
4083
|
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
3810
4084
|
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
3811
4085
|
out.push(
|
|
@@ -4082,6 +4356,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4082
4356
|
detectArgsPii,
|
|
4083
4357
|
detectDangerousEval,
|
|
4084
4358
|
detectDangerousShellExec,
|
|
4359
|
+
detectInlineExec,
|
|
4085
4360
|
detectPii,
|
|
4086
4361
|
evaluateEgress,
|
|
4087
4362
|
evaluateLoopWindow,
|
|
@@ -4100,6 +4375,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4100
4375
|
isIgnoredTool,
|
|
4101
4376
|
isPrivateHost,
|
|
4102
4377
|
isProtectedHomePath,
|
|
4378
|
+
isShellShapedTool,
|
|
4103
4379
|
isShieldVerdict,
|
|
4104
4380
|
matchSensitivePath,
|
|
4105
4381
|
matchesPattern,
|
|
@@ -4117,6 +4393,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4117
4393
|
summarizeBlast,
|
|
4118
4394
|
summarizeScan,
|
|
4119
4395
|
toScanFinding,
|
|
4396
|
+
toolMatchesRule,
|
|
4120
4397
|
truncateBlastPath,
|
|
4121
4398
|
validateOverrides,
|
|
4122
4399
|
validateRegex,
|