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

* Improved regex for auto-execution of chained commands
This commit is contained in:
John Stearns
2024-12-17 15:15:28 -08:00
committed by GitHub
parent a982df7d1f
commit 1e83d1a835
3 changed files with 139 additions and 9 deletions

View File

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

View File

@@ -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) => {

View File

@@ -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'
})
}
})
})
})