b4cf6dcf4d739521bbbe9ef99b96ab9bded10ec8

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Allow static grep include globs

Diff

 1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
 2index e476c427090ede47d492a601676b6218377f4883..9ed19c526c8c0cd4eb6d8fcbc803ad943b32001f 100644
 3--- a/src/core/deterministic.ts
 4+++ b/src/core/deterministic.ts
 5@@ -113,7 +113,12 @@ function isStaticOptionAssignment(prefix: string): boolean {
 6   return /^--[A-Za-z0-9][A-Za-z0-9_-]*=$/.test(prefix);
 7 }
 8 
 9-function hasUnsafeWordSyntax(command: string, allowStaticGlobs = false): boolean {
10+function isSafeGrepIncludeGlob(command: string, wordStart: number): boolean {
11+  const word = command.slice(wordStart).split(/\s/, 1)[0] ?? "";
12+  return /^--include=\*\.(?!(?:env[A-Za-z0-9._-]*|pem|key|p12|pfx)$)[A-Za-z0-9]+$/i.test(word);
13+}
14+
15+function hasUnsafeWordSyntax(command: string, allowStaticGlobs = false, program?: string): boolean {
16   let quote: "'" | '"' | undefined;
17   let wordStarted = false;
18   let quotedWord = false;
19@@ -198,7 +203,12 @@ function hasUnsafeWordSyntax(command: string, allowStaticGlobs = false): boolean
20       wordStarted = true;
21       continue;
22     }
23-    if (!allowStaticGlobs && (character === "*" || character === "?" || character === "[")) return true;
24+    if (
25+      !allowStaticGlobs &&
26+      (character === "*" || character === "?" || character === "[") &&
27+      !(program === "grep" && isSafeGrepIncludeGlob(command, wordStart))
28+    )
29+      return true;
30     if (character === "~") {
31       if (wordStarted || (nextCharacter !== undefined && nextCharacter !== "/" && !/\s/.test(nextCharacter))) {
32         return true;
33@@ -570,7 +580,7 @@ function checkBash(command: string, redirects: readonly BashRedirect[] = [], par
34     checkSystemPathMutation(cleaned);
35   if (askDecision) return askDecision;
36   const [program, ...arguments_] = commandWords(cleaned);
37-  if (hasUnsafeWordSyntax(source, program === "ls")) return undefined;
38+  if (hasUnsafeWordSyntax(source, program === "ls", program)) return undefined;
39   if (program && hasInlineInterpreterCode(program, arguments_)) return undefined;
40   return checkDocker(cleaned, parsed) ?? checkAllow(cleaned, parsed);
41 }
42diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
43index 0c48cd2720c29b41843482a88bd1b3cf7caa50d2..4c7af4f3990f727694bd133325c8806bed3b2337 100644
44--- a/test/core/deterministic.test.ts
45+++ b/test/core/deterministic.test.ts
46@@ -97,18 +97,20 @@ test("allows static pathname globs only for ls", () => {
47   }
48 });
49 
50-test("allows static quoted option values while rejecting mixed quoted paths", () => {
51+test("allows static grep include globs while rejecting mixed quoted paths", () => {
52   for (const command of [
53     'grep -rn "NewOrchestrator\\|ScratchModel\\|scratch" --include="*.go"',
54     "grep -rn 'NewOrchestrator\\|ScratchModel\\|scratch' --include='*.go'",
55     'grep --include="*.go"',
56+    'grep -rn "WaitReady" --include=*.go .',
57+    'grep -rn "8080" --include=*.go --include=*.json --include=Makefile .',
58   ]) {
59     expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
60   }
61 
62-  expect(checkDeterministic("bash", { command: 'grep -rn scratch --include=".env"' })).toMatchObject({
63-    decision: "ask",
64-  });
65+  for (const command of ['grep -rn scratch --include=".env"', "grep -rn scratch --include=*.env ."]) {
66+    expect(checkDeterministic("bash", { command })?.decision).not.toBe("allow");
67+  }
68 
69   for (const command of ['cat .e"nv"', 'echo > /e"tc"/x', 'rm -rf "$(echo -n /)"']) {
70     expect(checkDeterministic("bash", { command })?.decision).not.toBe("allow");