@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.mjs
CHANGED
|
@@ -696,6 +696,139 @@ function redactText(text) {
|
|
|
696
696
|
|
|
697
697
|
// src/shell/index.ts
|
|
698
698
|
import mvdanSh from "mvdan-sh";
|
|
699
|
+
|
|
700
|
+
// src/rules/index.ts
|
|
701
|
+
import pm from "picomatch";
|
|
702
|
+
|
|
703
|
+
// src/utils/regex.ts
|
|
704
|
+
import safeRegex2 from "safe-regex2";
|
|
705
|
+
var MAX_REGEX_LENGTH = 256;
|
|
706
|
+
var REGEX_CACHE_MAX = 500;
|
|
707
|
+
var regexCache = /* @__PURE__ */ new Map();
|
|
708
|
+
function validateRegex(pattern) {
|
|
709
|
+
if (!pattern) return "Pattern is required";
|
|
710
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
711
|
+
try {
|
|
712
|
+
new RegExp(pattern);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
715
|
+
}
|
|
716
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
717
|
+
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
721
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
722
|
+
const key = `${pattern}\0${flags}`;
|
|
723
|
+
if (regexCache.has(key)) {
|
|
724
|
+
const cached = regexCache.get(key);
|
|
725
|
+
regexCache.delete(key);
|
|
726
|
+
regexCache.set(key, cached);
|
|
727
|
+
return cached;
|
|
728
|
+
}
|
|
729
|
+
if (validateRegex(pattern) !== null) return null;
|
|
730
|
+
try {
|
|
731
|
+
const re = new RegExp(pattern, flags);
|
|
732
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
733
|
+
const oldest = regexCache.keys().next().value;
|
|
734
|
+
if (oldest) regexCache.delete(oldest);
|
|
735
|
+
}
|
|
736
|
+
regexCache.set(key, re);
|
|
737
|
+
return re;
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/rules/index.ts
|
|
744
|
+
function matchesPattern(text, patterns) {
|
|
745
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
746
|
+
if (p.length === 0) return false;
|
|
747
|
+
const isMatch = pm(p, { nocase: true, dot: true });
|
|
748
|
+
const target = text.toLowerCase();
|
|
749
|
+
const directMatch = isMatch(target);
|
|
750
|
+
if (directMatch) return true;
|
|
751
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
752
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
753
|
+
}
|
|
754
|
+
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
755
|
+
function getNestedValue(obj, path) {
|
|
756
|
+
if (!obj || typeof obj !== "object") return null;
|
|
757
|
+
const segments = path.split(".");
|
|
758
|
+
for (const seg of segments) {
|
|
759
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
760
|
+
}
|
|
761
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
762
|
+
}
|
|
763
|
+
function evaluateSmartConditions(args, rule) {
|
|
764
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
765
|
+
const mode = rule.conditionMode ?? "all";
|
|
766
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
767
|
+
const resolveField = (field) => {
|
|
768
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
769
|
+
const rawVal = getNestedValue(args, field);
|
|
770
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
771
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
772
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
773
|
+
fieldCache.set(field, val);
|
|
774
|
+
return val;
|
|
775
|
+
};
|
|
776
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
777
|
+
const resolveFieldReadings = (field) => {
|
|
778
|
+
const cached = readingsCache.get(field);
|
|
779
|
+
if (cached) return cached;
|
|
780
|
+
const primary = resolveField(field);
|
|
781
|
+
if (primary === null) {
|
|
782
|
+
readingsCache.set(field, []);
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
let out = [primary];
|
|
786
|
+
if (field === "command") {
|
|
787
|
+
const raw = getNestedValue(args, field);
|
|
788
|
+
if (typeof raw === "string") {
|
|
789
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
790
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
readingsCache.set(field, out);
|
|
794
|
+
return out;
|
|
795
|
+
};
|
|
796
|
+
const results = rule.conditions.map((cond) => {
|
|
797
|
+
const val = resolveField(cond.field);
|
|
798
|
+
switch (cond.op) {
|
|
799
|
+
case "exists":
|
|
800
|
+
return val !== null && val !== "";
|
|
801
|
+
case "notExists":
|
|
802
|
+
return val === null || val === "";
|
|
803
|
+
case "contains":
|
|
804
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
805
|
+
case "notContains":
|
|
806
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
807
|
+
case "matches": {
|
|
808
|
+
if (val === null || !cond.value) return false;
|
|
809
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
810
|
+
if (!reM) return false;
|
|
811
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
812
|
+
}
|
|
813
|
+
case "notMatches": {
|
|
814
|
+
if (!cond.value) return false;
|
|
815
|
+
if (val === null) return true;
|
|
816
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
817
|
+
if (!reN) return false;
|
|
818
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
819
|
+
}
|
|
820
|
+
case "matchesGlob":
|
|
821
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
822
|
+
case "notMatchesGlob":
|
|
823
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
824
|
+
default:
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/shell/index.ts
|
|
699
832
|
var { syntax } = mvdanSh;
|
|
700
833
|
var sharedParser = syntax.NewParser();
|
|
701
834
|
var MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -768,14 +901,22 @@ function cachedNormalize(command, compute) {
|
|
|
768
901
|
return result;
|
|
769
902
|
}
|
|
770
903
|
function normalizeCommandForPolicy(command) {
|
|
904
|
+
return commandReadingsImpl(command).posix;
|
|
905
|
+
}
|
|
906
|
+
function commandReadings(command) {
|
|
907
|
+
const r = commandReadingsImpl(command);
|
|
908
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
909
|
+
}
|
|
910
|
+
function commandReadingsImpl(command) {
|
|
771
911
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
772
912
|
}
|
|
773
913
|
function normalizeCommandForPolicyImpl(command) {
|
|
774
914
|
const f = parseShared(command);
|
|
775
|
-
if (f === PARSE_FAIL) return command;
|
|
915
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
776
916
|
try {
|
|
777
917
|
const strips = [];
|
|
778
918
|
const rewrites = [];
|
|
919
|
+
const quoteOnlyRewrites = [];
|
|
779
920
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
780
921
|
syntax.Walk(f, (node) => {
|
|
781
922
|
if (!node) return false;
|
|
@@ -820,22 +961,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
820
961
|
if (resolved === source) continue;
|
|
821
962
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
822
963
|
rewrites.push([s, e, resolved]);
|
|
964
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
965
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
823
966
|
}
|
|
824
967
|
return true;
|
|
825
968
|
});
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
...
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
return result;
|
|
969
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
970
|
+
const apply = (extra) => {
|
|
971
|
+
const edits = [...stripEdits, ...extra];
|
|
972
|
+
if (edits.length === 0) return command;
|
|
973
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
974
|
+
let out = command;
|
|
975
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
976
|
+
return out;
|
|
977
|
+
};
|
|
978
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
837
979
|
} catch {
|
|
838
|
-
return command;
|
|
980
|
+
return { posix: command, separator: command };
|
|
839
981
|
}
|
|
840
982
|
}
|
|
841
983
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -905,9 +1047,34 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
905
1047
|
"vi",
|
|
906
1048
|
"emacs",
|
|
907
1049
|
"code",
|
|
908
|
-
"type"
|
|
1050
|
+
"type",
|
|
1051
|
+
// — the 22 that were missing —
|
|
1052
|
+
"grep",
|
|
1053
|
+
"egrep",
|
|
1054
|
+
"fgrep",
|
|
1055
|
+
"rg",
|
|
1056
|
+
"ag",
|
|
1057
|
+
"ack",
|
|
1058
|
+
"awk",
|
|
1059
|
+
"gawk",
|
|
1060
|
+
"sed",
|
|
1061
|
+
"cut",
|
|
1062
|
+
"tr",
|
|
1063
|
+
"jq",
|
|
1064
|
+
"yq",
|
|
1065
|
+
"od",
|
|
1066
|
+
"xxd",
|
|
1067
|
+
"hexdump",
|
|
1068
|
+
"strings",
|
|
1069
|
+
"sort",
|
|
1070
|
+
"uniq",
|
|
1071
|
+
"tac",
|
|
1072
|
+
"nl",
|
|
1073
|
+
"dd"
|
|
909
1074
|
]);
|
|
910
|
-
var FS_OP_PRESCREEN_RE =
|
|
1075
|
+
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1076
|
+
`(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
|
|
1077
|
+
);
|
|
911
1078
|
var HOME_CACHE_ALLOWLIST = [
|
|
912
1079
|
".cache",
|
|
913
1080
|
".npm/_npx",
|
|
@@ -946,9 +1113,37 @@ var SENSITIVE_PATH_RULES = [
|
|
|
946
1113
|
// for the canonical test-asserted contract.
|
|
947
1114
|
rule: "shield:project-jail:block-read-env",
|
|
948
1115
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1116
|
+
// Structural, not a list. The previous form enumerated seven suffixes and
|
|
1117
|
+
// anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
|
|
1118
|
+
// gitignored, all routinely holding real secrets — were never covered. A
|
|
1119
|
+
// hand-written list of what to protect is only ever as complete as the day
|
|
1120
|
+
// it was typed; this says "`.env` plus any suffix chain" and then names the
|
|
1121
|
+
// exceptions, which is the direction that fails safe.
|
|
1122
|
+
//
|
|
1123
|
+
// \.env the segment itself
|
|
1124
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1125
|
+
// files. Without it a flat suffix class swallows both.
|
|
1126
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1127
|
+
// [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
|
|
1128
|
+
// `(\.[\w-]+)*` reads the same but is rejected by
|
|
1129
|
+
// safe-regex2, and this pattern runs on the hook hot path.
|
|
1130
|
+
//
|
|
1131
|
+
// The two exclusions are NOT the same shape, because the words do not mean
|
|
1132
|
+
// the same thing:
|
|
1133
|
+
//
|
|
1134
|
+
// (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
|
|
1135
|
+
// stays a fixture whatever follows, so `.env.example.md` is allowed too.
|
|
1136
|
+
// These are checked into git by convention: already public, so blocking
|
|
1137
|
+
// them buys nothing and costs the most common legitimate agent read.
|
|
1138
|
+
//
|
|
1139
|
+
// (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
|
|
1140
|
+
// fixture. `.env.test` is the committed template and stays allowed, but
|
|
1141
|
+
// `.env.test.local` is gitignored by the `.env*.local` convention and
|
|
1142
|
+
// holds real values, so it must block. Using `\b` here — the obvious
|
|
1143
|
+
// symmetry — silently exempts every `.env.test.*` file.
|
|
1144
|
+
//
|
|
1145
|
+
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1146
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
|
|
952
1147
|
},
|
|
953
1148
|
{
|
|
954
1149
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1077,6 +1272,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1077
1272
|
}
|
|
1078
1273
|
return found;
|
|
1079
1274
|
}
|
|
1275
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1276
|
+
if (isBashTool(toolName)) return true;
|
|
1277
|
+
if (!toolInspection) return false;
|
|
1278
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1279
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1280
|
+
}
|
|
1281
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1282
|
+
if (!ruleTool) return true;
|
|
1283
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1284
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1285
|
+
}
|
|
1286
|
+
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;
|
|
1287
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1288
|
+
"uv",
|
|
1289
|
+
"uvx",
|
|
1290
|
+
"poetry",
|
|
1291
|
+
"pipenv",
|
|
1292
|
+
"pdm",
|
|
1293
|
+
"rye",
|
|
1294
|
+
"hatch",
|
|
1295
|
+
"conda",
|
|
1296
|
+
"mamba",
|
|
1297
|
+
"micromamba",
|
|
1298
|
+
"npx",
|
|
1299
|
+
"pnpm",
|
|
1300
|
+
"yarn",
|
|
1301
|
+
"bunx",
|
|
1302
|
+
"watch",
|
|
1303
|
+
"strace",
|
|
1304
|
+
"ltrace",
|
|
1305
|
+
"chroot",
|
|
1306
|
+
"unshare",
|
|
1307
|
+
"runuser"
|
|
1308
|
+
]);
|
|
1309
|
+
function isInlineCodeFlag(interp, w) {
|
|
1310
|
+
const lw = w.toLowerCase();
|
|
1311
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1312
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1313
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1314
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1315
|
+
const body = lw.slice(1);
|
|
1316
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1317
|
+
const cut = body.search(cutAt);
|
|
1318
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1319
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1320
|
+
}
|
|
1321
|
+
var _redirStdinOps = null;
|
|
1322
|
+
function redirStdinOps() {
|
|
1323
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1324
|
+
_redirStdinOps = new Set(
|
|
1325
|
+
[
|
|
1326
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1327
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1328
|
+
deriveRedirOp("cat < f"),
|
|
1329
|
+
deriveRedirOp("cat <<< x")
|
|
1330
|
+
].filter((op) => op >= 0)
|
|
1331
|
+
);
|
|
1332
|
+
return _redirStdinOps;
|
|
1333
|
+
}
|
|
1334
|
+
function deriveBinaryOp(sample) {
|
|
1335
|
+
try {
|
|
1336
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1337
|
+
let op = -1;
|
|
1338
|
+
syntax.Walk(f, (node) => {
|
|
1339
|
+
const n = node;
|
|
1340
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1341
|
+
return true;
|
|
1342
|
+
});
|
|
1343
|
+
return op;
|
|
1344
|
+
} catch {
|
|
1345
|
+
return -1;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
var _listOps = null;
|
|
1349
|
+
function listOps() {
|
|
1350
|
+
if (_listOps) return _listOps;
|
|
1351
|
+
_listOps = new Set(
|
|
1352
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1353
|
+
);
|
|
1354
|
+
return _listOps;
|
|
1355
|
+
}
|
|
1356
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1357
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1358
|
+
function unwrapCommandHead(words) {
|
|
1359
|
+
let i = 0;
|
|
1360
|
+
while (i < words.length) {
|
|
1361
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1362
|
+
if (head === "find") {
|
|
1363
|
+
const x = words.findIndex(
|
|
1364
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1365
|
+
);
|
|
1366
|
+
if (x < 0) break;
|
|
1367
|
+
i = x + 1;
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1371
|
+
i++;
|
|
1372
|
+
let targetConsumed = false;
|
|
1373
|
+
while (i < words.length) {
|
|
1374
|
+
const t = words[i];
|
|
1375
|
+
if (t === null) {
|
|
1376
|
+
i++;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const lt = t.toLowerCase();
|
|
1380
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1381
|
+
i++;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (t.startsWith("-")) {
|
|
1385
|
+
i++;
|
|
1386
|
+
const nxt = words[i];
|
|
1387
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1388
|
+
i++;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1392
|
+
i++;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1396
|
+
targetConsumed = true;
|
|
1397
|
+
i++;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return i;
|
|
1404
|
+
}
|
|
1405
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1406
|
+
const cmd = stmt?.Cmd;
|
|
1407
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1408
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1409
|
+
if (words.length === 0) return false;
|
|
1410
|
+
const headIdx = unwrapCommandHead(words);
|
|
1411
|
+
const rawHead = words[headIdx];
|
|
1412
|
+
if (rawHead == null) return false;
|
|
1413
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1414
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1415
|
+
let args = words.slice(headIdx + 1);
|
|
1416
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1417
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1418
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1419
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1420
|
+
}
|
|
1421
|
+
let positionals = 0;
|
|
1422
|
+
let selectedProgram = false;
|
|
1423
|
+
for (const a of args) {
|
|
1424
|
+
if (a == null) {
|
|
1425
|
+
positionals++;
|
|
1426
|
+
selectedProgram = true;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1430
|
+
if (a === "-m") {
|
|
1431
|
+
selectedProgram = true;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1435
|
+
if (!a.startsWith("-")) {
|
|
1436
|
+
positionals++;
|
|
1437
|
+
selectedProgram = true;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1441
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1442
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1443
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1444
|
+
}
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
function detectInlineExec(command) {
|
|
1448
|
+
const f = parseShared(command);
|
|
1449
|
+
if (f === PARSE_FAIL) {
|
|
1450
|
+
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(
|
|
1451
|
+
command
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
let found = false;
|
|
1455
|
+
try {
|
|
1456
|
+
syntax.Walk(f, (node) => {
|
|
1457
|
+
if (!node || found) return false;
|
|
1458
|
+
const n = node;
|
|
1459
|
+
const t = syntax.NodeType(n);
|
|
1460
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1461
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1462
|
+
found = true;
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1467
|
+
found = true;
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
return true;
|
|
1471
|
+
});
|
|
1472
|
+
} catch {
|
|
1473
|
+
return found;
|
|
1474
|
+
}
|
|
1475
|
+
return found;
|
|
1476
|
+
}
|
|
1080
1477
|
function analyzeChmod777(command) {
|
|
1081
1478
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1082
1479
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -1954,117 +2351,6 @@ function parseAllSshHostsFromCommand(command) {
|
|
|
1954
2351
|
return extractAllSshHosts(tokens.slice(1));
|
|
1955
2352
|
}
|
|
1956
2353
|
|
|
1957
|
-
// src/rules/index.ts
|
|
1958
|
-
import pm from "picomatch";
|
|
1959
|
-
|
|
1960
|
-
// src/utils/regex.ts
|
|
1961
|
-
import safeRegex2 from "safe-regex2";
|
|
1962
|
-
var MAX_REGEX_LENGTH = 100;
|
|
1963
|
-
var REGEX_CACHE_MAX = 500;
|
|
1964
|
-
var regexCache = /* @__PURE__ */ new Map();
|
|
1965
|
-
function validateRegex(pattern) {
|
|
1966
|
-
if (!pattern) return "Pattern is required";
|
|
1967
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
1968
|
-
try {
|
|
1969
|
-
new RegExp(pattern);
|
|
1970
|
-
} catch (e) {
|
|
1971
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
1972
|
-
}
|
|
1973
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
1974
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
1975
|
-
return null;
|
|
1976
|
-
}
|
|
1977
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
1978
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
1979
|
-
const key = `${pattern}\0${flags}`;
|
|
1980
|
-
if (regexCache.has(key)) {
|
|
1981
|
-
const cached = regexCache.get(key);
|
|
1982
|
-
regexCache.delete(key);
|
|
1983
|
-
regexCache.set(key, cached);
|
|
1984
|
-
return cached;
|
|
1985
|
-
}
|
|
1986
|
-
if (validateRegex(pattern) !== null) return null;
|
|
1987
|
-
try {
|
|
1988
|
-
const re = new RegExp(pattern, flags);
|
|
1989
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
1990
|
-
const oldest = regexCache.keys().next().value;
|
|
1991
|
-
if (oldest) regexCache.delete(oldest);
|
|
1992
|
-
}
|
|
1993
|
-
regexCache.set(key, re);
|
|
1994
|
-
return re;
|
|
1995
|
-
} catch {
|
|
1996
|
-
return null;
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// src/rules/index.ts
|
|
2001
|
-
function matchesPattern(text, patterns) {
|
|
2002
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
2003
|
-
if (p.length === 0) return false;
|
|
2004
|
-
const isMatch = pm(p, { nocase: true, dot: true });
|
|
2005
|
-
const target = text.toLowerCase();
|
|
2006
|
-
const directMatch = isMatch(target);
|
|
2007
|
-
if (directMatch) return true;
|
|
2008
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2009
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2010
|
-
}
|
|
2011
|
-
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2012
|
-
function getNestedValue(obj, path) {
|
|
2013
|
-
if (!obj || typeof obj !== "object") return null;
|
|
2014
|
-
const segments = path.split(".");
|
|
2015
|
-
for (const seg of segments) {
|
|
2016
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2017
|
-
}
|
|
2018
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
2019
|
-
}
|
|
2020
|
-
function evaluateSmartConditions(args, rule) {
|
|
2021
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
2022
|
-
const mode = rule.conditionMode ?? "all";
|
|
2023
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
2024
|
-
const resolveField = (field) => {
|
|
2025
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
2026
|
-
const rawVal = getNestedValue(args, field);
|
|
2027
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
2028
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
2029
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
2030
|
-
fieldCache.set(field, val);
|
|
2031
|
-
return val;
|
|
2032
|
-
};
|
|
2033
|
-
const results = rule.conditions.map((cond) => {
|
|
2034
|
-
const val = resolveField(cond.field);
|
|
2035
|
-
switch (cond.op) {
|
|
2036
|
-
case "exists":
|
|
2037
|
-
return val !== null && val !== "";
|
|
2038
|
-
case "notExists":
|
|
2039
|
-
return val === null || val === "";
|
|
2040
|
-
case "contains":
|
|
2041
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
2042
|
-
case "notContains":
|
|
2043
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
2044
|
-
case "matches": {
|
|
2045
|
-
if (val === null || !cond.value) return false;
|
|
2046
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2047
|
-
if (!reM) return false;
|
|
2048
|
-
return reM.test(val);
|
|
2049
|
-
}
|
|
2050
|
-
case "notMatches": {
|
|
2051
|
-
if (!cond.value) return false;
|
|
2052
|
-
if (val === null) return true;
|
|
2053
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
2054
|
-
if (!reN) return false;
|
|
2055
|
-
return !reN.test(val);
|
|
2056
|
-
}
|
|
2057
|
-
case "matchesGlob":
|
|
2058
|
-
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
2059
|
-
case "notMatchesGlob":
|
|
2060
|
-
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
2061
|
-
default:
|
|
2062
|
-
return false;
|
|
2063
|
-
}
|
|
2064
|
-
});
|
|
2065
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
2354
|
// src/policy/index.ts
|
|
2069
2355
|
function resolveCheck(v) {
|
|
2070
2356
|
return v === "off" || v === "block" ? v : "review";
|
|
@@ -2072,41 +2358,6 @@ function resolveCheck(v) {
|
|
|
2072
2358
|
function resolveCheckTight(v) {
|
|
2073
2359
|
return v === "block" ? "block" : "review";
|
|
2074
2360
|
}
|
|
2075
|
-
var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
|
|
2076
|
-
var INLINE_SHELL = /^(bash|sh|zsh)$/i;
|
|
2077
|
-
function detectInlineExec(command) {
|
|
2078
|
-
const pipeFed = command.includes("|");
|
|
2079
|
-
const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
|
|
2080
|
-
for (const rawSeg of segments) {
|
|
2081
|
-
const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
|
|
2082
|
-
let i = 0;
|
|
2083
|
-
while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
|
|
2084
|
-
if (i >= tokens.length) continue;
|
|
2085
|
-
const base = tokens[i].split("/").pop() ?? tokens[i];
|
|
2086
|
-
if (!INLINE_INTERP.test(base)) continue;
|
|
2087
|
-
const args = tokens.slice(i + 1);
|
|
2088
|
-
let hadRedirect = false;
|
|
2089
|
-
const positionals = [];
|
|
2090
|
-
for (let j = 0; j < args.length; j++) {
|
|
2091
|
-
const a = args[j];
|
|
2092
|
-
if (a === "-") return true;
|
|
2093
|
-
if (a.startsWith("<")) {
|
|
2094
|
-
hadRedirect = true;
|
|
2095
|
-
if (a === "<" || a === "<<") j++;
|
|
2096
|
-
continue;
|
|
2097
|
-
}
|
|
2098
|
-
if (a.startsWith("-")) {
|
|
2099
|
-
if (/^-(c|e|eval)$/i.test(a)) return true;
|
|
2100
|
-
continue;
|
|
2101
|
-
}
|
|
2102
|
-
positionals.push(a);
|
|
2103
|
-
}
|
|
2104
|
-
if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
|
|
2105
|
-
return true;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return false;
|
|
2109
|
-
}
|
|
2110
2361
|
var VERDICT_RANK = {
|
|
2111
2362
|
allow: 0,
|
|
2112
2363
|
review: 1,
|
|
@@ -2120,6 +2371,12 @@ function resolvePinned(matches) {
|
|
|
2120
2371
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2121
2372
|
);
|
|
2122
2373
|
}
|
|
2374
|
+
function strictestVerdict(candidates) {
|
|
2375
|
+
if (candidates.length === 0) return void 0;
|
|
2376
|
+
return candidates.reduce(
|
|
2377
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2123
2380
|
function tokenize2(toolName) {
|
|
2124
2381
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2125
2382
|
}
|
|
@@ -2131,6 +2388,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2131
2388
|
const value = getNestedValue(args, fieldPath);
|
|
2132
2389
|
return typeof value === "string" ? value : null;
|
|
2133
2390
|
}
|
|
2391
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2392
|
+
const patterns = Object.keys(toolInspection);
|
|
2393
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2394
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2395
|
+
}
|
|
2134
2396
|
function isSqlTool(toolName, toolInspection) {
|
|
2135
2397
|
const patterns = Object.keys(toolInspection);
|
|
2136
2398
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2202,8 +2464,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2202
2464
|
};
|
|
2203
2465
|
}
|
|
2204
2466
|
}
|
|
2205
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2206
|
-
const
|
|
2467
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2468
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2469
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2470
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2207
2471
|
if (bashCommand !== null) {
|
|
2208
2472
|
const pipeVerdict = pipeChainVerdict(
|
|
2209
2473
|
bashCommand,
|
|
@@ -2253,8 +2517,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2253
2517
|
}
|
|
2254
2518
|
if (config.policy.smartRules.length > 0) {
|
|
2255
2519
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2520
|
+
const astSuppressed = (rule) => {
|
|
2521
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2522
|
+
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;
|
|
2523
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2524
|
+
return true;
|
|
2525
|
+
};
|
|
2256
2526
|
const matches = config.policy.smartRules.filter(
|
|
2257
|
-
(rule) =>
|
|
2527
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2258
2528
|
);
|
|
2259
2529
|
const matchedRule = resolvePinned(matches);
|
|
2260
2530
|
if (matchedRule) {
|
|
@@ -2285,15 +2555,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2285
2555
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2286
2556
|
allTokens = analyzed.allTokens;
|
|
2287
2557
|
pathTokens = analyzed.paths;
|
|
2288
|
-
const
|
|
2289
|
-
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2290
|
-
return {
|
|
2291
|
-
decision: inlineAction === "block" ? "block" : "review",
|
|
2292
|
-
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2293
|
-
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2294
|
-
tier: 3
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2558
|
+
const candidates = [];
|
|
2297
2559
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2298
2560
|
if (evalVerdict === "block") {
|
|
2299
2561
|
return {
|
|
@@ -2304,24 +2566,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2304
2566
|
tier: 3
|
|
2305
2567
|
};
|
|
2306
2568
|
}
|
|
2569
|
+
const ptVerdict = pipeChainVerdict(
|
|
2570
|
+
shellCommand,
|
|
2571
|
+
isTrustedHost,
|
|
2572
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2573
|
+
);
|
|
2574
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2575
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2576
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2577
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2578
|
+
candidates.push({
|
|
2579
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2580
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2581
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2582
|
+
tier: 3
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2307
2585
|
if (evalVerdict === "review") {
|
|
2308
|
-
|
|
2309
|
-
// Class B tighten-only: commandChecks.evalDynamic may upgrade to
|
|
2310
|
-
// block but can never turn this off (eval-remote above is Class A —
|
|
2311
|
-
// no knob at all).
|
|
2586
|
+
candidates.push({
|
|
2312
2587
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2313
2588
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2314
2589
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2315
2590
|
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.",
|
|
2316
2591
|
tier: 3
|
|
2317
|
-
};
|
|
2592
|
+
});
|
|
2318
2593
|
}
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
isTrustedHost,
|
|
2322
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2323
|
-
);
|
|
2324
|
-
if (ptVerdict) return ptVerdict;
|
|
2594
|
+
const builtin = strictestVerdict(candidates);
|
|
2595
|
+
if (builtin) return builtin;
|
|
2325
2596
|
if (config.policy.egress?.enabled) {
|
|
2326
2597
|
const dests = extractShellDestinations(shellCommand);
|
|
2327
2598
|
if (dests.length > 0) {
|
|
@@ -3586,15 +3857,15 @@ function detectArgsPii(args) {
|
|
|
3586
3857
|
|
|
3587
3858
|
// src/scan/canonical.ts
|
|
3588
3859
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3589
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
3590
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
3860
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v9";
|
|
3861
|
+
var CANONICAL_EXTRACTOR_HASH = "4ebf40dfe1d7c0a1";
|
|
3591
3862
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
3592
3863
|
function extractCanonicalFindings(call, ctx) {
|
|
3593
3864
|
const out = [];
|
|
3594
3865
|
const ts = call.timestamp;
|
|
3595
3866
|
const toolNameLower = call.toolName.toLowerCase();
|
|
3596
3867
|
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
3597
|
-
const
|
|
3868
|
+
const isShell = isShellShapedTool(call.toolName, ctx.toolInspection) && command !== null;
|
|
3598
3869
|
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
3599
3870
|
out.push(
|
|
3600
3871
|
makeFinding({
|
|
@@ -3669,7 +3940,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3669
3940
|
);
|
|
3670
3941
|
}
|
|
3671
3942
|
}
|
|
3672
|
-
if (!
|
|
3943
|
+
if (!isShell || command === null) {
|
|
3673
3944
|
return out;
|
|
3674
3945
|
}
|
|
3675
3946
|
const fsVerdict = analyzeFsOperation(command);
|
|
@@ -3695,7 +3966,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3695
3966
|
for (const source of ctx.rules) {
|
|
3696
3967
|
const r = source.rule;
|
|
3697
3968
|
if (r.verdict === "allow") continue;
|
|
3698
|
-
if (
|
|
3969
|
+
if (!toolMatchesRule(toolNameLower, r.tool, ctx.toolInspection)) continue;
|
|
3699
3970
|
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
3700
3971
|
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
3701
3972
|
out.push(
|
|
@@ -3971,6 +4242,7 @@ export {
|
|
|
3971
4242
|
detectArgsPii,
|
|
3972
4243
|
detectDangerousEval,
|
|
3973
4244
|
detectDangerousShellExec,
|
|
4245
|
+
detectInlineExec,
|
|
3974
4246
|
detectPii,
|
|
3975
4247
|
evaluateEgress,
|
|
3976
4248
|
evaluateLoopWindow,
|
|
@@ -3989,6 +4261,7 @@ export {
|
|
|
3989
4261
|
isIgnoredTool,
|
|
3990
4262
|
isPrivateHost,
|
|
3991
4263
|
isProtectedHomePath,
|
|
4264
|
+
isShellShapedTool,
|
|
3992
4265
|
isShieldVerdict,
|
|
3993
4266
|
matchSensitivePath,
|
|
3994
4267
|
matchesPattern,
|
|
@@ -4006,6 +4279,7 @@ export {
|
|
|
4006
4279
|
summarizeBlast,
|
|
4007
4280
|
summarizeScan,
|
|
4008
4281
|
toScanFinding,
|
|
4282
|
+
toolMatchesRule,
|
|
4009
4283
|
truncateBlastPath,
|
|
4010
4284
|
validateOverrides,
|
|
4011
4285
|
validateRegex,
|