Просмотр исходного кода

Improved regex for auto-execution of chained commands (#158)

* Improved regex for auto-execution of chained commands
John Stearns 1 год назад
Родитель
Сommit
1e83d1a835

+ 5 - 0
.changeset/large-crews-hunt.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Improved regex for auto-execution of chained commands

+ 1 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -523,7 +523,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 			}
 
 			// Split command by chaining operators
-			const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
+			const commands = command.split(/&&|\|\||;|(?<!"[^"]*)\|(?![^"]*")|\$\(|`/).map(cmd => cmd.trim())
 
 			// Check if all individual commands are allowed
 			return commands.every((cmd) => {

+ 133 - 8
webview-ui/src/components/chat/__tests__/ChatView.test.tsx

@@ -415,7 +415,7 @@ describe('ChatView - Auto Approval Tests', () => {
     it('auto-approves chained commands when all parts are allowed', async () => {
       render(
         <ExtensionStateContextProvider>
-          <ChatView 
+          <ChatView
             isHidden={false}
             showAnnouncement={false}
             hideAnnouncement={() => {}}
@@ -429,7 +429,12 @@ describe('ChatView - Auto Approval Tests', () => {
         'npm test && npm run build',
         'npm test; npm run build',
         'npm test || npm run build',
-        'npm test | npm run build'
+        'npm test | npm run build',
+        // Add test for quoted pipes which should be treated as part of the command, not as a chain operator
+        'echo "hello | world"',
+        'npm test "param with | inside" && npm run build',
+        // PowerShell command with Select-String
+        'npm test 2>&1 | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'
       ]
 
       for (const command of allowedChainedCommands) {
@@ -438,7 +443,7 @@ describe('ChatView - Auto Approval Tests', () => {
         // First hydrate state with initial task
         mockPostMessage({
           alwaysAllowExecute: true,
-          allowedCommands: ['npm test', 'npm run build'],
+          allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
           clineMessages: [
             {
               type: 'say',
@@ -452,7 +457,7 @@ describe('ChatView - Auto Approval Tests', () => {
         // Then send the chained command ask message
         mockPostMessage({
           alwaysAllowExecute: true,
-          allowedCommands: ['npm test', 'npm run build'],
+          allowedCommands: ['npm test', 'npm run build', 'echo', 'Select-String'],
           clineMessages: [
             {
               type: 'say',
@@ -483,7 +488,7 @@ describe('ChatView - Auto Approval Tests', () => {
     it('does not auto-approve chained commands when any part is disallowed', () => {
       render(
         <ExtensionStateContextProvider>
-          <ChatView 
+          <ChatView
             isHidden={false}
             showAnnouncement={false}
             hideAnnouncement={() => {}}
@@ -498,15 +503,20 @@ describe('ChatView - Auto Approval Tests', () => {
         'npm test; rm -rf /',
         'npm test || rm -rf /',
         'npm test | rm -rf /',
+        // Test subshell execution using $() and backticks
         'npm test $(echo dangerous)',
-        'npm test `echo dangerous`'
+        'npm test `echo dangerous`',
+        // Test unquoted pipes with disallowed commands
+        'npm test | rm -rf /',
+        // Test PowerShell command with disallowed parts
+        'npm test 2>&1 | Select-String -NotMatch "node_modules" | rm -rf /'
       ]
 
       disallowedChainedCommands.forEach(command => {
         // First hydrate state with initial task
         mockPostMessage({
           alwaysAllowExecute: true,
-          allowedCommands: ['npm test'],
+          allowedCommands: ['npm test', 'Select-String'],
           clineMessages: [
             {
               type: 'say',
@@ -520,7 +530,7 @@ describe('ChatView - Auto Approval Tests', () => {
         // Then send the chained command ask message
         mockPostMessage({
           alwaysAllowExecute: true,
-          allowedCommands: ['npm test'],
+          allowedCommands: ['npm test', 'Select-String'],
           clineMessages: [
             {
               type: 'say',
@@ -545,6 +555,121 @@ describe('ChatView - Auto Approval Tests', () => {
         })
       })
     })
+
+    it('handles complex PowerShell command chains correctly', async () => {
+      render(
+        <ExtensionStateContextProvider>
+          <ChatView
+            isHidden={false}
+            showAnnouncement={false}
+            hideAnnouncement={() => {}}
+            showHistoryView={() => {}}
+          />
+        </ExtensionStateContextProvider>
+      )
+
+      // Test PowerShell specific command chains
+      const powershellCommands = {
+        allowed: [
+          'npm test 2>&1 | Select-String -NotMatch "node_modules"',
+          'npm test 2>&1 | Select-String "FAIL|Error"',
+          'npm test 2>&1 | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"'
+        ],
+        disallowed: [
+          'npm test 2>&1 | Select-String -NotMatch "node_modules" | rm -rf /',
+          'npm test 2>&1 | Select-String "FAIL|Error" && del /F /Q *',
+          'npm test 2>&1 | Select-String -NotMatch "node_modules" | Remove-Item -Recurse'
+        ]
+      }
+
+      // Test allowed PowerShell commands
+      for (const command of powershellCommands.allowed) {
+        jest.clearAllMocks()
+
+        mockPostMessage({
+          alwaysAllowExecute: true,
+          allowedCommands: ['npm test', 'Select-String'],
+          clineMessages: [
+            {
+              type: 'say',
+              say: 'task',
+              ts: Date.now() - 2000,
+              text: 'Initial task'
+            }
+          ]
+        })
+
+        mockPostMessage({
+          alwaysAllowExecute: true,
+          allowedCommands: ['npm test', 'Select-String'],
+          clineMessages: [
+            {
+              type: 'say',
+              say: 'task',
+              ts: Date.now() - 2000,
+              text: 'Initial task'
+            },
+            {
+              type: 'ask',
+              ask: 'command',
+              ts: Date.now(),
+              text: command,
+              partial: false
+            }
+          ]
+        })
+
+        await waitFor(() => {
+          expect(vscode.postMessage).toHaveBeenCalledWith({
+            type: 'askResponse',
+            askResponse: 'yesButtonClicked'
+          })
+        })
+      }
+
+      // Test disallowed PowerShell commands
+      for (const command of powershellCommands.disallowed) {
+        jest.clearAllMocks()
+
+        mockPostMessage({
+          alwaysAllowExecute: true,
+          allowedCommands: ['npm test', 'Select-String'],
+          clineMessages: [
+            {
+              type: 'say',
+              say: 'task',
+              ts: Date.now() - 2000,
+              text: 'Initial task'
+            }
+          ]
+        })
+
+        mockPostMessage({
+          alwaysAllowExecute: true,
+          allowedCommands: ['npm test', 'Select-String'],
+          clineMessages: [
+            {
+              type: 'say',
+              say: 'task',
+              ts: Date.now() - 2000,
+              text: 'Initial task'
+            },
+            {
+              type: 'ask',
+              ask: 'command',
+              ts: Date.now(),
+              text: command,
+              partial: false
+            }
+          ]
+        })
+
+        expect(vscode.postMessage).not.toHaveBeenCalledWith({
+          type: 'askResponse',
+          askResponse: 'yesButtonClicked'
+        })
+      }
+    })
   })
 })