@node9/policy-engine 1.67.0 → 1.67.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +41 -3
- package/dist/index.d.ts +41 -3
- package/dist/index.js +411 -187
- package/dist/index.mjs +408 -187
- 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) {
|
|
@@ -1187,6 +1332,208 @@ function chmodHasOpenPermMode(command) {
|
|
|
1187
1332
|
}
|
|
1188
1333
|
return found;
|
|
1189
1334
|
}
|
|
1335
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
1336
|
+
if (isBashTool(toolName)) return true;
|
|
1337
|
+
if (!toolInspection) return false;
|
|
1338
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
1339
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
1340
|
+
}
|
|
1341
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
1342
|
+
if (!ruleTool) return true;
|
|
1343
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
1344
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
1345
|
+
}
|
|
1346
|
+
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;
|
|
1347
|
+
var RUNNER_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1348
|
+
"uv",
|
|
1349
|
+
"uvx",
|
|
1350
|
+
"poetry",
|
|
1351
|
+
"pipenv",
|
|
1352
|
+
"pdm",
|
|
1353
|
+
"rye",
|
|
1354
|
+
"hatch",
|
|
1355
|
+
"conda",
|
|
1356
|
+
"mamba",
|
|
1357
|
+
"micromamba",
|
|
1358
|
+
"npx",
|
|
1359
|
+
"pnpm",
|
|
1360
|
+
"yarn",
|
|
1361
|
+
"bunx",
|
|
1362
|
+
"watch",
|
|
1363
|
+
"strace",
|
|
1364
|
+
"ltrace",
|
|
1365
|
+
"chroot",
|
|
1366
|
+
"unshare",
|
|
1367
|
+
"runuser"
|
|
1368
|
+
]);
|
|
1369
|
+
function isInlineCodeFlag(interp, w) {
|
|
1370
|
+
const lw = w.toLowerCase();
|
|
1371
|
+
if (lw === "--eval" || lw === "--command" || lw === "--print") return true;
|
|
1372
|
+
if (/^(pwsh|powershell)/.test(interp)) return /^-(c|command|e|ec|enc|encodedcommand)$/i.test(lw);
|
|
1373
|
+
if (!w.startsWith("-") || w.startsWith("--")) return false;
|
|
1374
|
+
const codeLetters = /^(perl|ruby|lua|bun|osascript|rscript|irb)$/.test(interp) ? "e" : /^(node|tsx|ts-node)$/.test(interp) ? "ep" : interp === "php" ? "r" : "c";
|
|
1375
|
+
const body = lw.slice(1);
|
|
1376
|
+
const cutAt = /^(perl|ruby|node|tsx|ts-node)$/.test(interp) ? /[mirw]|[^a-z0-9]/ : /[^a-z0-9]/;
|
|
1377
|
+
const cut = body.search(cutAt);
|
|
1378
|
+
const bundle = cut >= 0 ? body.slice(0, cut) : body;
|
|
1379
|
+
return [...codeLetters].some((l) => bundle.includes(l));
|
|
1380
|
+
}
|
|
1381
|
+
var _redirStdinOps = null;
|
|
1382
|
+
function redirStdinOps() {
|
|
1383
|
+
if (_redirStdinOps) return _redirStdinOps;
|
|
1384
|
+
_redirStdinOps = new Set(
|
|
1385
|
+
[
|
|
1386
|
+
deriveRedirOp("cat <<X\nX"),
|
|
1387
|
+
deriveRedirOp("cat <<-X\nX"),
|
|
1388
|
+
deriveRedirOp("cat < f"),
|
|
1389
|
+
deriveRedirOp("cat <<< x")
|
|
1390
|
+
].filter((op) => op >= 0)
|
|
1391
|
+
);
|
|
1392
|
+
return _redirStdinOps;
|
|
1393
|
+
}
|
|
1394
|
+
function deriveBinaryOp(sample) {
|
|
1395
|
+
try {
|
|
1396
|
+
const f = sharedParser.Parse(sample, "cmd");
|
|
1397
|
+
let op = -1;
|
|
1398
|
+
syntax.Walk(f, (node) => {
|
|
1399
|
+
const n = node;
|
|
1400
|
+
if (n && syntax.NodeType(n) === "BinaryCmd" && op < 0) op = n.Op;
|
|
1401
|
+
return true;
|
|
1402
|
+
});
|
|
1403
|
+
return op;
|
|
1404
|
+
} catch {
|
|
1405
|
+
return -1;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
var _listOps = null;
|
|
1409
|
+
function listOps() {
|
|
1410
|
+
if (_listOps) return _listOps;
|
|
1411
|
+
_listOps = new Set(
|
|
1412
|
+
[deriveBinaryOp("a && b"), deriveBinaryOp("a || b")].filter((o) => o >= 0)
|
|
1413
|
+
);
|
|
1414
|
+
return _listOps;
|
|
1415
|
+
}
|
|
1416
|
+
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1417
|
+
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1418
|
+
function unwrapCommandHead(words) {
|
|
1419
|
+
let i = 0;
|
|
1420
|
+
while (i < words.length) {
|
|
1421
|
+
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1422
|
+
if (head === "find") {
|
|
1423
|
+
const x = words.findIndex(
|
|
1424
|
+
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1425
|
+
);
|
|
1426
|
+
if (x < 0) break;
|
|
1427
|
+
i = x + 1;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (!COMMAND_WRAPPERS.has(head) && !RUNNER_WRAPPERS.has(head)) break;
|
|
1431
|
+
i++;
|
|
1432
|
+
let targetConsumed = false;
|
|
1433
|
+
while (i < words.length) {
|
|
1434
|
+
const t = words[i];
|
|
1435
|
+
if (t === null) {
|
|
1436
|
+
i++;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const lt = t.toLowerCase();
|
|
1440
|
+
if (/^[A-Za-z_]\w*=/.test(t)) {
|
|
1441
|
+
i++;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (t.startsWith("-")) {
|
|
1445
|
+
i++;
|
|
1446
|
+
const nxt = words[i];
|
|
1447
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()))
|
|
1448
|
+
i++;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (lt === "run" || lt === "exec" || lt === "dlx" || /^\d+(\.\d+)?[smhd]?$/.test(lt)) {
|
|
1452
|
+
i++;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
if (!targetConsumed && WRAPPER_TAKES_TARGET.has(head)) {
|
|
1456
|
+
targetConsumed = true;
|
|
1457
|
+
i++;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return i;
|
|
1464
|
+
}
|
|
1465
|
+
function inlineExecStmt(stmt, pipeFed) {
|
|
1466
|
+
const cmd = stmt?.Cmd;
|
|
1467
|
+
if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return false;
|
|
1468
|
+
const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
|
|
1469
|
+
if (words.length === 0) return false;
|
|
1470
|
+
const headIdx = unwrapCommandHead(words);
|
|
1471
|
+
const rawHead = words[headIdx];
|
|
1472
|
+
if (rawHead == null) return false;
|
|
1473
|
+
const interp = (rawHead.split("/").pop() ?? "").toLowerCase();
|
|
1474
|
+
if (!INLINE_INTERPRETER.test(interp)) return false;
|
|
1475
|
+
let args = words.slice(headIdx + 1);
|
|
1476
|
+
if (interp === "deno" && (args[0] ?? "").toLowerCase() === "eval") return true;
|
|
1477
|
+
if (INTERP_LEADING_TARGET.has(interp)) {
|
|
1478
|
+
const firstFlag = args.findIndex((a) => a == null || a.startsWith("-"));
|
|
1479
|
+
args = firstFlag >= 0 ? args.slice(firstFlag) : [];
|
|
1480
|
+
}
|
|
1481
|
+
let positionals = 0;
|
|
1482
|
+
let selectedProgram = false;
|
|
1483
|
+
for (const a of args) {
|
|
1484
|
+
if (a == null) {
|
|
1485
|
+
positionals++;
|
|
1486
|
+
selectedProgram = true;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) return true;
|
|
1490
|
+
if (a === "-m") {
|
|
1491
|
+
selectedProgram = true;
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (a === "-" && !selectedProgram) return true;
|
|
1495
|
+
if (!a.startsWith("-")) {
|
|
1496
|
+
positionals++;
|
|
1497
|
+
selectedProgram = true;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const redirs = stmt.Redirs || cmd.Redirs || [];
|
|
1501
|
+
const stdinFed = redirs.some((r) => r && redirStdinOps().has(r.Op));
|
|
1502
|
+
if (positionals === 0 && (stdinFed || pipeFed)) {
|
|
1503
|
+
if (!/^(bash|sh|zsh)$/i.test(interp)) return true;
|
|
1504
|
+
}
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
function detectInlineExec(command) {
|
|
1508
|
+
const f = parseShared(command);
|
|
1509
|
+
if (f === PARSE_FAIL) {
|
|
1510
|
+
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(
|
|
1511
|
+
command
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
let found = false;
|
|
1515
|
+
try {
|
|
1516
|
+
syntax.Walk(f, (node) => {
|
|
1517
|
+
if (!node || found) return false;
|
|
1518
|
+
const n = node;
|
|
1519
|
+
const t = syntax.NodeType(n);
|
|
1520
|
+
if (t === "BinaryCmd" && n.Y && !listOps().has(n.Op)) {
|
|
1521
|
+
if (inlineExecStmt(n.Y, true)) {
|
|
1522
|
+
found = true;
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (t === "Stmt" && inlineExecStmt(n, false)) {
|
|
1527
|
+
found = true;
|
|
1528
|
+
return false;
|
|
1529
|
+
}
|
|
1530
|
+
return true;
|
|
1531
|
+
});
|
|
1532
|
+
} catch {
|
|
1533
|
+
return found;
|
|
1534
|
+
}
|
|
1535
|
+
return found;
|
|
1536
|
+
}
|
|
1190
1537
|
function analyzeChmod777(command) {
|
|
1191
1538
|
if (!/chmod/i.test(command.replace(/[\\'"]/g, ""))) return null;
|
|
1192
1539
|
if (!chmodHasOpenPermMode(command)) return null;
|
|
@@ -2064,117 +2411,6 @@ function parseAllSshHostsFromCommand(command) {
|
|
|
2064
2411
|
return extractAllSshHosts(tokens.slice(1));
|
|
2065
2412
|
}
|
|
2066
2413
|
|
|
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
2414
|
// src/policy/index.ts
|
|
2179
2415
|
function resolveCheck(v) {
|
|
2180
2416
|
return v === "off" || v === "block" ? v : "review";
|
|
@@ -2182,41 +2418,6 @@ function resolveCheck(v) {
|
|
|
2182
2418
|
function resolveCheckTight(v) {
|
|
2183
2419
|
return v === "block" ? "block" : "review";
|
|
2184
2420
|
}
|
|
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
2421
|
var VERDICT_RANK = {
|
|
2221
2422
|
allow: 0,
|
|
2222
2423
|
review: 1,
|
|
@@ -2230,6 +2431,12 @@ function resolvePinned(matches) {
|
|
|
2230
2431
|
(best, r) => VERDICT_RANK[r.verdict] > VERDICT_RANK[best.verdict] ? r : best
|
|
2231
2432
|
);
|
|
2232
2433
|
}
|
|
2434
|
+
function strictestVerdict(candidates) {
|
|
2435
|
+
if (candidates.length === 0) return void 0;
|
|
2436
|
+
return candidates.reduce(
|
|
2437
|
+
(best, c) => VERDICT_RANK[c.decision] > VERDICT_RANK[best.decision] ? c : best
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2233
2440
|
function tokenize2(toolName) {
|
|
2234
2441
|
return toolName.toLowerCase().split(/[_.\-\s]+/).filter(Boolean);
|
|
2235
2442
|
}
|
|
@@ -2241,6 +2448,11 @@ function extractShellCommand(toolName, args, toolInspection) {
|
|
|
2241
2448
|
const value = getNestedValue(args, fieldPath);
|
|
2242
2449
|
return typeof value === "string" ? value : null;
|
|
2243
2450
|
}
|
|
2451
|
+
function inspectsShellCommand(toolName, toolInspection) {
|
|
2452
|
+
const patterns = Object.keys(toolInspection);
|
|
2453
|
+
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
2454
|
+
return matchingPattern !== void 0 && toolInspection[matchingPattern] === "command";
|
|
2455
|
+
}
|
|
2244
2456
|
function isSqlTool(toolName, toolInspection) {
|
|
2245
2457
|
const patterns = Object.keys(toolInspection);
|
|
2246
2458
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -2312,8 +2524,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2312
2524
|
};
|
|
2313
2525
|
}
|
|
2314
2526
|
}
|
|
2315
|
-
if (wouldBeIgnored) return { decision: "allow" };
|
|
2316
|
-
const
|
|
2527
|
+
if (wouldBeIgnored && !context.skipIgnoredFastPath) return { decision: "allow" };
|
|
2528
|
+
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
2529
|
+
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
2530
|
+
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
2317
2531
|
if (bashCommand !== null) {
|
|
2318
2532
|
const pipeVerdict = pipeChainVerdict(
|
|
2319
2533
|
bashCommand,
|
|
@@ -2363,8 +2577,14 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2363
2577
|
}
|
|
2364
2578
|
if (config.policy.smartRules.length > 0) {
|
|
2365
2579
|
const rmCleanupWaiver = bashCommand !== null && isRmCreatedInCommandCleanup(bashCommand);
|
|
2580
|
+
const astSuppressed = (rule) => {
|
|
2581
|
+
if (bashCommand === null || !rule.name || !AST_FS_REGEX_RULES.has(rule.name)) return false;
|
|
2582
|
+
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;
|
|
2583
|
+
if (knob === "off" && rule.pinned) return false;
|
|
2584
|
+
return true;
|
|
2585
|
+
};
|
|
2366
2586
|
const matches = config.policy.smartRules.filter(
|
|
2367
|
-
(rule) =>
|
|
2587
|
+
(rule) => toolMatchesRule(toolName, rule.tool, config.policy.toolInspection) && !astSuppressed(rule) && !(rmCleanupWaiver && rule.name === "review-rm" && rule.verdict === "review") && evaluateSmartConditions(args, rule)
|
|
2368
2588
|
);
|
|
2369
2589
|
const matchedRule = resolvePinned(matches);
|
|
2370
2590
|
if (matchedRule) {
|
|
@@ -2395,15 +2615,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2395
2615
|
const analyzed = analyzeShellCommand(shellCommand);
|
|
2396
2616
|
allTokens = analyzed.allTokens;
|
|
2397
2617
|
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
|
-
}
|
|
2618
|
+
const candidates = [];
|
|
2407
2619
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
2408
2620
|
if (evalVerdict === "block") {
|
|
2409
2621
|
return {
|
|
@@ -2414,24 +2626,33 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2414
2626
|
tier: 3
|
|
2415
2627
|
};
|
|
2416
2628
|
}
|
|
2629
|
+
const ptVerdict = pipeChainVerdict(
|
|
2630
|
+
shellCommand,
|
|
2631
|
+
isTrustedHost,
|
|
2632
|
+
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2633
|
+
);
|
|
2634
|
+
if (ptVerdict?.decision === "allow") return ptVerdict;
|
|
2635
|
+
if (ptVerdict) candidates.push(ptVerdict);
|
|
2636
|
+
const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
|
|
2637
|
+
if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
|
|
2638
|
+
candidates.push({
|
|
2639
|
+
decision: inlineAction === "block" ? "block" : "review",
|
|
2640
|
+
blockedByLabel: "Node9 Standard (Inline Execution)",
|
|
2641
|
+
ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
|
|
2642
|
+
tier: 3
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2417
2645
|
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).
|
|
2646
|
+
candidates.push({
|
|
2422
2647
|
decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
|
|
2423
2648
|
blockedByLabel: "Node9: Eval Dynamic Content",
|
|
2424
2649
|
reason: "eval of dynamic content (variable or subshell expansion) requires approval",
|
|
2425
2650
|
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
2651
|
tier: 3
|
|
2427
|
-
};
|
|
2652
|
+
});
|
|
2428
2653
|
}
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
isTrustedHost,
|
|
2432
|
-
resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
|
|
2433
|
-
);
|
|
2434
|
-
if (ptVerdict) return ptVerdict;
|
|
2654
|
+
const builtin = strictestVerdict(candidates);
|
|
2655
|
+
if (builtin) return builtin;
|
|
2435
2656
|
if (config.policy.egress?.enabled) {
|
|
2436
2657
|
const dests = extractShellDestinations(shellCommand);
|
|
2437
2658
|
if (dests.length > 0) {
|
|
@@ -3696,15 +3917,15 @@ function detectArgsPii(args) {
|
|
|
3696
3917
|
|
|
3697
3918
|
// src/scan/canonical.ts
|
|
3698
3919
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3699
|
-
var CANONICAL_EXTRACTOR_VERSION = "canonical-
|
|
3700
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
3920
|
+
var CANONICAL_EXTRACTOR_VERSION = "canonical-v8";
|
|
3921
|
+
var CANONICAL_EXTRACTOR_HASH = "80f40f974263b281";
|
|
3701
3922
|
var DEDUPE_PREVIEW_LEN = 120;
|
|
3702
3923
|
function extractCanonicalFindings(call, ctx) {
|
|
3703
3924
|
const out = [];
|
|
3704
3925
|
const ts = call.timestamp;
|
|
3705
3926
|
const toolNameLower = call.toolName.toLowerCase();
|
|
3706
3927
|
const command = typeof call.args.command === "string" ? call.args.command : null;
|
|
3707
|
-
const
|
|
3928
|
+
const isShell = isShellShapedTool(call.toolName, ctx.toolInspection) && command !== null;
|
|
3708
3929
|
if (call.outputBytes !== void 0 && call.outputBytes > LONG_OUTPUT_THRESHOLD_BYTES) {
|
|
3709
3930
|
out.push(
|
|
3710
3931
|
makeFinding({
|
|
@@ -3779,7 +4000,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3779
4000
|
);
|
|
3780
4001
|
}
|
|
3781
4002
|
}
|
|
3782
|
-
if (!
|
|
4003
|
+
if (!isShell || command === null) {
|
|
3783
4004
|
return out;
|
|
3784
4005
|
}
|
|
3785
4006
|
const fsVerdict = analyzeFsOperation(command);
|
|
@@ -3805,7 +4026,7 @@ function extractCanonicalFindings(call, ctx) {
|
|
|
3805
4026
|
for (const source of ctx.rules) {
|
|
3806
4027
|
const r = source.rule;
|
|
3807
4028
|
if (r.verdict === "allow") continue;
|
|
3808
|
-
if (
|
|
4029
|
+
if (!toolMatchesRule(toolNameLower, r.tool, ctx.toolInspection)) continue;
|
|
3809
4030
|
if (r.name && AST_FS_REGEX_RULES.has(r.name)) continue;
|
|
3810
4031
|
if (!evaluateSmartConditions(call.args, r)) continue;
|
|
3811
4032
|
out.push(
|
|
@@ -4082,6 +4303,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4082
4303
|
detectArgsPii,
|
|
4083
4304
|
detectDangerousEval,
|
|
4084
4305
|
detectDangerousShellExec,
|
|
4306
|
+
detectInlineExec,
|
|
4085
4307
|
detectPii,
|
|
4086
4308
|
evaluateEgress,
|
|
4087
4309
|
evaluateLoopWindow,
|
|
@@ -4100,6 +4322,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4100
4322
|
isIgnoredTool,
|
|
4101
4323
|
isPrivateHost,
|
|
4102
4324
|
isProtectedHomePath,
|
|
4325
|
+
isShellShapedTool,
|
|
4103
4326
|
isShieldVerdict,
|
|
4104
4327
|
matchSensitivePath,
|
|
4105
4328
|
matchesPattern,
|
|
@@ -4117,6 +4340,7 @@ var ENGINE_VERSION = "1.4.0";
|
|
|
4117
4340
|
summarizeBlast,
|
|
4118
4341
|
summarizeScan,
|
|
4119
4342
|
toScanFinding,
|
|
4343
|
+
toolMatchesRule,
|
|
4120
4344
|
truncateBlastPath,
|
|
4121
4345
|
validateOverrides,
|
|
4122
4346
|
validateRegex,
|