@node9/proxy 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.
@@ -184,6 +184,125 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
184
184
  }
185
185
  return null;
186
186
  }
187
+ function validateRegex(pattern) {
188
+ if (!pattern) return "Pattern is required";
189
+ if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
190
+ try {
191
+ new RegExp(pattern);
192
+ } catch (e) {
193
+ return `Invalid regex syntax: ${e.message}`;
194
+ }
195
+ if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
196
+ if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
197
+ return null;
198
+ }
199
+ function getCompiledRegex(pattern, flags = "") {
200
+ if (flags && !/^[gimsuy]+$/.test(flags)) return null;
201
+ const key = `${pattern}\0${flags}`;
202
+ if (regexCache.has(key)) {
203
+ const cached = regexCache.get(key);
204
+ regexCache.delete(key);
205
+ regexCache.set(key, cached);
206
+ return cached;
207
+ }
208
+ if (validateRegex(pattern) !== null) return null;
209
+ try {
210
+ const re = new RegExp(pattern, flags);
211
+ if (regexCache.size >= REGEX_CACHE_MAX) {
212
+ const oldest = regexCache.keys().next().value;
213
+ if (oldest) regexCache.delete(oldest);
214
+ }
215
+ regexCache.set(key, re);
216
+ return re;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ function matchesPattern(text, patterns) {
222
+ const p = Array.isArray(patterns) ? patterns : [patterns];
223
+ if (p.length === 0) return false;
224
+ const isMatch = pm(p, { nocase: true, dot: true });
225
+ const target = text.toLowerCase();
226
+ const directMatch = isMatch(target);
227
+ if (directMatch) return true;
228
+ const withoutDotSlash = text.replace(/^\.\//, "");
229
+ return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
230
+ }
231
+ function getNestedValue(obj, path11) {
232
+ if (!obj || typeof obj !== "object") return null;
233
+ const segments = path11.split(".");
234
+ for (const seg of segments) {
235
+ if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
236
+ }
237
+ return segments.reduce((prev, curr) => prev?.[curr], obj);
238
+ }
239
+ function evaluateSmartConditions(args, rule) {
240
+ if (!rule.conditions || rule.conditions.length === 0) return true;
241
+ const mode = rule.conditionMode ?? "all";
242
+ const fieldCache = /* @__PURE__ */ new Map();
243
+ const resolveField = (field) => {
244
+ if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
245
+ const rawVal = getNestedValue(args, field);
246
+ const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
247
+ const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
248
+ const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
249
+ fieldCache.set(field, val);
250
+ return val;
251
+ };
252
+ const readingsCache = /* @__PURE__ */ new Map();
253
+ const resolveFieldReadings = (field) => {
254
+ const cached = readingsCache.get(field);
255
+ if (cached) return cached;
256
+ const primary = resolveField(field);
257
+ if (primary === null) {
258
+ readingsCache.set(field, []);
259
+ return [];
260
+ }
261
+ let out = [primary];
262
+ if (field === "command") {
263
+ const raw = getNestedValue(args, field);
264
+ if (typeof raw === "string") {
265
+ const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
266
+ out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
267
+ }
268
+ }
269
+ readingsCache.set(field, out);
270
+ return out;
271
+ };
272
+ const results = rule.conditions.map((cond) => {
273
+ const val = resolveField(cond.field);
274
+ switch (cond.op) {
275
+ case "exists":
276
+ return val !== null && val !== "";
277
+ case "notExists":
278
+ return val === null || val === "";
279
+ case "contains":
280
+ return val !== null && cond.value ? val.includes(cond.value) : false;
281
+ case "notContains":
282
+ return val !== null && cond.value ? !val.includes(cond.value) : true;
283
+ case "matches": {
284
+ if (val === null || !cond.value) return false;
285
+ const reM = getCompiledRegex(cond.value, cond.flags ?? "");
286
+ if (!reM) return false;
287
+ return resolveFieldReadings(cond.field).some((v) => reM.test(v));
288
+ }
289
+ case "notMatches": {
290
+ if (!cond.value) return false;
291
+ if (val === null) return true;
292
+ const reN = getCompiledRegex(cond.value, cond.flags ?? "");
293
+ if (!reN) return false;
294
+ return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
295
+ }
296
+ case "matchesGlob":
297
+ return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
298
+ case "notMatchesGlob":
299
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
300
+ default:
301
+ return false;
302
+ }
303
+ });
304
+ return mode === "any" ? results.some((r) => r) : results.every((r) => r);
305
+ }
187
306
  function isCatHeredocOrLit(part) {
188
307
  if (!part) return false;
189
308
  const t = syntax.NodeType(part);
@@ -237,14 +356,22 @@ function cachedNormalize(command, compute) {
237
356
  return result;
238
357
  }
239
358
  function normalizeCommandForPolicy(command) {
359
+ return commandReadingsImpl(command).posix;
360
+ }
361
+ function commandReadings(command) {
362
+ const r = commandReadingsImpl(command);
363
+ return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
364
+ }
365
+ function commandReadingsImpl(command) {
240
366
  return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
241
367
  }
242
368
  function normalizeCommandForPolicyImpl(command) {
243
369
  const f = parseShared(command);
244
- if (f === PARSE_FAIL) return command;
370
+ if (f === PARSE_FAIL) return { posix: command, separator: command };
245
371
  try {
246
372
  const strips = [];
247
373
  const rewrites = [];
374
+ const quoteOnlyRewrites = [];
248
375
  const msgSpans = /* @__PURE__ */ new Set();
249
376
  syntax.Walk(f, (node) => {
250
377
  if (!node) return false;
@@ -289,22 +416,23 @@ function normalizeCommandForPolicyImpl(command) {
289
416
  if (resolved === source) continue;
290
417
  if (resolved === "" || /\s/.test(resolved)) continue;
291
418
  rewrites.push([s, e, resolved]);
419
+ const quoteOnly = source.replace(/['"]/g, "");
420
+ if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
292
421
  }
293
422
  return true;
294
423
  });
295
- const edits = [
296
- ...strips.map(([s, e]) => [s, e, '""']),
297
- ...rewrites
298
- ];
299
- if (edits.length === 0) return command;
300
- edits.sort((a, b) => b[0] - a[0]);
301
- let result = command;
302
- for (const [s, e, rep] of edits) {
303
- result = result.slice(0, s) + rep + result.slice(e);
304
- }
305
- return result;
424
+ const stripEdits = strips.map(([s, e]) => [s, e, '""']);
425
+ const apply = (extra) => {
426
+ const edits = [...stripEdits, ...extra];
427
+ if (edits.length === 0) return command;
428
+ edits.sort((a, b) => b[0] - a[0]);
429
+ let out = command;
430
+ for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
431
+ return out;
432
+ };
433
+ return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
306
434
  } catch {
307
- return command;
435
+ return { posix: command, separator: command };
308
436
  }
309
437
  }
310
438
  function scanArgsForDynamicExec(args, startIdx) {
@@ -359,6 +487,20 @@ function detectDangerousShellExec(command) {
359
487
  return null;
360
488
  }
361
489
  }
490
+ function isBashTool(toolName) {
491
+ return BASH_TOOL_NAMES.has(toolName.toLowerCase());
492
+ }
493
+ function isShellShapedTool(toolName, toolInspection) {
494
+ if (isBashTool(toolName)) return true;
495
+ if (!toolInspection) return false;
496
+ const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
497
+ return pattern !== void 0 && toolInspection[pattern] === "command";
498
+ }
499
+ function toolMatchesRule(toolName, ruleTool, toolInspection) {
500
+ if (!ruleTool) return true;
501
+ if (matchesPattern(toolName, ruleTool)) return true;
502
+ return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
503
+ }
362
504
  function isProtectedHomePath(rawPath) {
363
505
  let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
364
506
  let underHome = false;
@@ -517,105 +659,6 @@ function analyzeFsOperationImpl(command) {
517
659
  return null;
518
660
  }
519
661
  }
520
- function validateRegex(pattern) {
521
- if (!pattern) return "Pattern is required";
522
- if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
523
- try {
524
- new RegExp(pattern);
525
- } catch (e) {
526
- return `Invalid regex syntax: ${e.message}`;
527
- }
528
- if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
529
- if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
530
- return null;
531
- }
532
- function getCompiledRegex(pattern, flags = "") {
533
- if (flags && !/^[gimsuy]+$/.test(flags)) return null;
534
- const key = `${pattern}\0${flags}`;
535
- if (regexCache.has(key)) {
536
- const cached = regexCache.get(key);
537
- regexCache.delete(key);
538
- regexCache.set(key, cached);
539
- return cached;
540
- }
541
- if (validateRegex(pattern) !== null) return null;
542
- try {
543
- const re = new RegExp(pattern, flags);
544
- if (regexCache.size >= REGEX_CACHE_MAX) {
545
- const oldest = regexCache.keys().next().value;
546
- if (oldest) regexCache.delete(oldest);
547
- }
548
- regexCache.set(key, re);
549
- return re;
550
- } catch {
551
- return null;
552
- }
553
- }
554
- function matchesPattern(text, patterns) {
555
- const p = Array.isArray(patterns) ? patterns : [patterns];
556
- if (p.length === 0) return false;
557
- const isMatch = pm(p, { nocase: true, dot: true });
558
- const target = text.toLowerCase();
559
- const directMatch = isMatch(target);
560
- if (directMatch) return true;
561
- const withoutDotSlash = text.replace(/^\.\//, "");
562
- return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
563
- }
564
- function getNestedValue(obj, path11) {
565
- if (!obj || typeof obj !== "object") return null;
566
- const segments = path11.split(".");
567
- for (const seg of segments) {
568
- if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
569
- }
570
- return segments.reduce((prev, curr) => prev?.[curr], obj);
571
- }
572
- function evaluateSmartConditions(args, rule) {
573
- if (!rule.conditions || rule.conditions.length === 0) return true;
574
- const mode = rule.conditionMode ?? "all";
575
- const fieldCache = /* @__PURE__ */ new Map();
576
- const resolveField = (field) => {
577
- if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
578
- const rawVal = getNestedValue(args, field);
579
- const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
580
- const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
581
- const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
582
- fieldCache.set(field, val);
583
- return val;
584
- };
585
- const results = rule.conditions.map((cond) => {
586
- const val = resolveField(cond.field);
587
- switch (cond.op) {
588
- case "exists":
589
- return val !== null && val !== "";
590
- case "notExists":
591
- return val === null || val === "";
592
- case "contains":
593
- return val !== null && cond.value ? val.includes(cond.value) : false;
594
- case "notContains":
595
- return val !== null && cond.value ? !val.includes(cond.value) : true;
596
- case "matches": {
597
- if (val === null || !cond.value) return false;
598
- const reM = getCompiledRegex(cond.value, cond.flags ?? "");
599
- if (!reM) return false;
600
- return reM.test(val);
601
- }
602
- case "notMatches": {
603
- if (!cond.value) return false;
604
- if (val === null) return true;
605
- const reN = getCompiledRegex(cond.value, cond.flags ?? "");
606
- if (!reN) return false;
607
- return !reN.test(val);
608
- }
609
- case "matchesGlob":
610
- return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
611
- case "notMatchesGlob":
612
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
613
- default:
614
- return false;
615
- }
616
- });
617
- return mode === "any" ? results.some((r) => r) : results.every((r) => r);
618
- }
619
662
  function isShieldVerdict(v) {
620
663
  return v === "allow" || v === "review" || v === "block";
621
664
  }
@@ -669,7 +712,7 @@ function assertBuiltinShieldRegexesAreSafe() {
669
712
  }
670
713
  }
671
714
  }
672
- var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
715
+ var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
673
716
  var init_dist = __esm({
674
717
  "packages/policy-engine/dist/index.mjs"() {
675
718
  "use strict";
@@ -1159,6 +1202,10 @@ var init_dist = __esm({
1159
1202
  MAX_DEPTH = 5;
1160
1203
  MAX_STRING_BYTES = 1e5;
1161
1204
  MAX_JSON_PARSE_BYTES = 1e4;
1205
+ MAX_REGEX_LENGTH = 256;
1206
+ REGEX_CACHE_MAX = 500;
1207
+ regexCache = /* @__PURE__ */ new Map();
1208
+ FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1162
1209
  ({ syntax } = mvdanSh);
1163
1210
  sharedParser = syntax.NewParser();
1164
1211
  MESSAGE_FLAGS = /* @__PURE__ */ new Set([
@@ -1269,6 +1316,13 @@ var init_dist = __esm({
1269
1316
  )
1270
1317
  }
1271
1318
  ];
1319
+ BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
1320
+ "bash",
1321
+ "execute_bash",
1322
+ "run_shell_command",
1323
+ "shell",
1324
+ "exec_command"
1325
+ ]);
1272
1326
  AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
1273
1327
  "block-rm-rf-home",
1274
1328
  "shield:project-jail:block-read-ssh",
@@ -1292,10 +1346,6 @@ var init_dist = __esm({
1292
1346
  deriveRedirOp("cat <<X\nX"),
1293
1347
  deriveRedirOp("cat <<-X\nX")
1294
1348
  ]);
1295
- MAX_REGEX_LENGTH = 100;
1296
- REGEX_CACHE_MAX = 500;
1297
- regexCache = /* @__PURE__ */ new Map();
1298
- FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1299
1349
  aws_default = {
1300
1350
  name: "aws",
1301
1351
  description: "Protects AWS infrastructure from destructive AI operations",
@@ -2461,6 +2511,7 @@ var init_managed = __esm({
2461
2511
  var init_build = __esm({
2462
2512
  "src/shields/build.ts"() {
2463
2513
  "use strict";
2514
+ init_dist();
2464
2515
  }
2465
2516
  });
2466
2517
 
@@ -2493,8 +2544,14 @@ var init_config = __esm({
2493
2544
  settings: {
2494
2545
  mode: "standard",
2495
2546
  autoStartDaemon: true,
2496
- enableUndo: true,
2497
- // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2547
+ // OFF by default. The snapshot store is a per-project bare git repo with
2548
+ // no size ceiling, and eviction drops the index row without deleting the
2549
+ // objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
2550
+ // from interrupted `git gc`) and filled the disk. A security tool must not
2551
+ // be what fills a customer's disk. Re-enable per install with
2552
+ // `{"settings":{"enableUndo":true}}`; the default flips back when the
2553
+ // bounded copy-store lands (doc/undo-v2-copy-store-design.md).
2554
+ enableUndo: false,
2498
2555
  enableHookLogDebug: true,
2499
2556
  approvalTimeoutMs: 12e4,
2500
2557
  // 120-second auto-deny timeout
@@ -2693,7 +2750,8 @@ var init_config = __esm({
2693
2750
  skillPinning: { enabled: false, mode: "warn", roots: [] },
2694
2751
  trustedHosts: [],
2695
2752
  trustedHostsManaged: false,
2696
- appPermissions: {}
2753
+ appPermissions: {},
2754
+ managedJailPaths: []
2697
2755
  },
2698
2756
  environments: {}
2699
2757
  };
@@ -3034,6 +3092,7 @@ var init_scan_watermark = __esm({
3034
3092
  "src/daemon/scan-watermark.ts"() {
3035
3093
  "use strict";
3036
3094
  init_dlp();
3095
+ init_config();
3037
3096
  init_dist();
3038
3097
  MAX_LINE_BYTES = 2 * 1024 * 1024;
3039
3098
  }
@@ -3813,6 +3872,7 @@ var init_policy = __esm({
3813
3872
  init_trusted_hosts();
3814
3873
  init_dist();
3815
3874
  init_dist();
3875
+ init_dist();
3816
3876
  }
3817
3877
  });
3818
3878
 
@@ -4187,7 +4247,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4187
4247
  }
4188
4248
  }
4189
4249
  let astFsMatched = false;
4190
- const astRanForBash = toolNameLower === "bash" || toolNameLower === "execute_bash";
4250
+ const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
4191
4251
  if (astRanForBash) {
4192
4252
  astFsMatched = pushFsOpAstFinding(
4193
4253
  String(input.command ?? ""),
@@ -4205,7 +4265,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
4205
4265
  for (const source of ruleSources) {
4206
4266
  const { rule } = source;
4207
4267
  if (rule.verdict === "allow") continue;
4208
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
4268
+ if (!toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
4209
4269
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
4210
4270
  if (!evaluateSmartConditions(input, rule)) continue;
4211
4271
  const inputPreview = preview(input, 120);
@@ -4487,7 +4547,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4487
4547
  }
4488
4548
  }
4489
4549
  let astFsMatched = false;
4490
- const astRanForBash = toolNameLower === "run_shell_command" || toolNameLower === "shell";
4550
+ const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
4491
4551
  if (astRanForBash) {
4492
4552
  astFsMatched = pushFsOpAstFinding(
4493
4553
  String(input.command ?? ""),
@@ -4505,7 +4565,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
4505
4565
  for (const source of ruleSources) {
4506
4566
  const { rule } = source;
4507
4567
  if (rule.verdict === "allow") continue;
4508
- if (rule.tool && !matchesPattern(toolNameLower, rule.tool)) continue;
4568
+ if (!toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
4509
4569
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
4510
4570
  if (!evaluateSmartConditions(input, rule)) continue;
4511
4571
  const inputPreview = preview(input, 120);
@@ -4727,7 +4787,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4727
4787
  }
4728
4788
  }
4729
4789
  let astFsMatched = false;
4730
- const astRanForBash = toolNameLower === "exec_command" || toolNameLower === "bash";
4790
+ const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
4731
4791
  if (astRanForBash) {
4732
4792
  astFsMatched = pushFsOpAstFinding(
4733
4793
  String(input["command"] ?? ""),
@@ -4745,8 +4805,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4745
4805
  for (const source of ruleSources) {
4746
4806
  const { rule } = source;
4747
4807
  if (rule.verdict === "allow") continue;
4748
- if (rule.tool && !matchesPattern(toolNameLower === "exec_command" ? "bash" : toolNameLower, rule.tool))
4749
- continue;
4808
+ if (rule.tool && !toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
4750
4809
  if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
4751
4810
  if (!evaluateSmartConditions(input, rule)) continue;
4752
4811
  const inputPreview = preview(input, 120);
@@ -4807,7 +4866,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
4807
4866
  }
4808
4867
  return result;
4809
4868
  }
4810
- var CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
4869
+ var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
4811
4870
  var init_scan = __esm({
4812
4871
  "src/cli/commands/scan.ts"() {
4813
4872
  "use strict";
@@ -4828,6 +4887,7 @@ var init_scan = __esm({
4828
4887
  init_protection();
4829
4888
  init_scan_json();
4830
4889
  init_scan_history();
4890
+ toolInspectionMap = DEFAULT_CONFIG.policy.toolInspection;
4831
4891
  CODE_EXTENSIONS = /* @__PURE__ */ new Set([
4832
4892
  ".ts",
4833
4893
  ".tsx",