File size: 4,802 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * Escapes a string for use in a regular expression.
 */
export function escapeRegex(text: string): string {
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s"]/g, '\\$&');
}

/**
 * Basic validation for regular expressions to prevent common ReDoS patterns.
 * This is a heuristic check and not a substitute for a full ReDoS scanner.
 */
export function isSafeRegExp(pattern: string): boolean {
  try {
    // 1. Ensure it's a valid regex
    new RegExp(pattern);
  } catch {
    return false;
  }

  // 2. Limit length to prevent extremely long regexes
  if (pattern.length > 2048) {
    return false;
  }

  // 3. Heuristic: Check for nested quantifiers which are a primary source of ReDoS.
  // Examples: (a+)+, (a|b)*, (.*)*, ([a-z]+)+
  // We look for a group (...) followed by a quantifier (+, *, or {n,m})
  // where the group itself contains a quantifier.
  // This matches a '(' followed by some content including a quantifier, then ')',
  // followed by another quantifier.
  const nestedQuantifierPattern = /\([^)]*[*+?{].*\)[*+?{]/;
  if (nestedQuantifierPattern.test(pattern)) {
    return false;
  }

  return true;
}

/**
 * Builds a list of args patterns for policy matching.
 *
 * This function handles the transformation of command prefixes and regexes into
 * the internal argsPattern representation used by the PolicyEngine.
 *
 * @param argsPattern An optional raw regex string for arguments.
 * @param commandPrefix An optional command prefix (or list of prefixes) to allow.
 * @param commandRegex An optional command regex string to allow.
 * @returns An array of string patterns (or undefined) for the PolicyEngine.
 */
export function buildArgsPatterns(
  argsPattern?: string,
  commandPrefix?: string | string[],
  commandRegex?: string,
): Array<string | undefined> {
  if (commandPrefix) {
    const prefixes = Array.isArray(commandPrefix)
      ? commandPrefix
      : [commandPrefix];

    return prefixes.map((prefix) => {
      // JSON.stringify safely encodes the prefix in quotes.
      // We remove ONLY the trailing quote to match it as an open prefix string.
      const encodedPrefix = JSON.stringify(prefix);
      const openQuotePrefix = encodedPrefix.substring(
        0,
        encodedPrefix.length - 1,
      );

      // Escape the exact JSON literal segment we expect to see
      const matchSegment = escapeRegex(`"command":${openQuotePrefix}`);

      // We allow [\s], ["], or the specific sequence [\"] (for escaped quotes
      // in JSON). We do NOT allow generic [\\], which would match "git\status"
      // -> "gitstatus".
      return `${matchSegment}(?:[\\s"]|\\\\")`;
    });
  }

  if (commandRegex) {
    return [`"command":"${commandRegex}`];
  }

  return [argsPattern];
}

/**
 * Builds a regex pattern to match a specific parameter and value in tool arguments.
 * This is used to narrow tool approvals to specific parameters.
 *
 * @param paramName The name of the parameter.
 * @param value The value to match.
 * @returns A regex string that matches "<paramName>":<value> in a JSON string.
 */
export function buildParamArgsPattern(
  paramName: string,
  value: unknown,
): string {
  const encodedValue = JSON.stringify(value);
  // We wrap the JSON string in escapeRegex and prepend/append \\0 to explicitly
  // match top-level JSON properties generated by stableStringify, preventing
  // argument injection bypass attacks.
  return `\\\\0${escapeRegex(`"${paramName}":${encodedValue}`)}\\\\0`;
}

/**
 * Builds a regex pattern to match a specific file path in tool arguments.
 * This is used to narrow tool approvals for edit tools to specific files.
 *
 * @param filePath The relative path to the file.
 * @returns A regex string that matches "file_path":"<path>" in a JSON string.
 */
export function buildFilePathArgsPattern(filePath: string): string {
  return buildParamArgsPattern('file_path', filePath);
}

/**
 * Builds a regex pattern to match a specific directory path in tool arguments.
 * This is used to narrow tool approvals for list_directory tool.
 *
 * @param dirPath The path to the directory.
 * @returns A regex string that matches "dir_path":"<path>" in a JSON string.
 */
export function buildDirPathArgsPattern(dirPath: string): string {
  return buildParamArgsPattern('dir_path', dirPath);
}

/**
 * Builds a regex pattern to match a specific "pattern" in tool arguments.
 * This is used to narrow tool approvals for search tools like glob/grep to specific patterns.
 *
 * @param pattern The pattern to match.
 * @returns A regex string that matches "pattern":"<pattern>" in a JSON string.
 */
export function buildPatternArgsPattern(pattern: string): string {
  return buildParamArgsPattern('pattern', pattern);
}