55
66import { Writable } from 'stream'
77import { getLogger } from '../../shared/logger/logger'
8- import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir()
8+ import { fs } from '../../shared/fs/fs'
99import { ChildProcess , ChildProcessOptions } from '../../shared/utilities/processUtils'
1010import { InvokeOutput , OutputKind , sanitizePath } from './toolShared'
11+ import { split } from 'shlex'
1112
12- export const readOnlyCommands : string [ ] = [ 'ls' , 'cat' , 'echo' , 'pwd' , 'which' , 'head' , 'tail' ]
13+ export const readOnlyCommands = new Set < string > ( [
14+ 'ls' ,
15+ 'cat' ,
16+ 'bat' ,
17+ 'pwd' ,
18+ 'echo' ,
19+ 'file' ,
20+ 'less' ,
21+ 'more' ,
22+ 'tree' ,
23+ 'find' ,
24+ 'top' ,
25+ 'htop' ,
26+ 'ps' ,
27+ 'df' ,
28+ 'du' ,
29+ 'free' ,
30+ 'uname' ,
31+ 'date' ,
32+ 'whoami' ,
33+ 'which' ,
34+ 'ping' ,
35+ 'ifconfig' ,
36+ 'ip' ,
37+ 'netstat' ,
38+ 'ss' ,
39+ 'dig' ,
40+ 'grep' ,
41+ 'wc' ,
42+ 'sort' ,
43+ 'diff' ,
44+ 'head' ,
45+ 'tail' ,
46+ ] )
47+ export const dangerousPatterns = new Set ( [ '<(' , '$(' , '`' , '>' , '&&' , '||' ] )
48+ const highRiskCommands = new Map < string , string > ( [
49+ [ 'rm' , 'This command can delete files and directories' ] ,
50+ [ 'chmod' , 'This command can modify file permissions' ] ,
51+ [ 'chown' , 'This command can change file ownership' ] ,
52+ [ 'dd' , 'This command can overwrite disk data' ] ,
53+ [ 'sudo' , 'This command can execute with elevated privileges' ] ,
54+ [ 'su' , 'This command can switch user accounts' ] ,
55+ [ 'mv' , 'This command can move or rename files' ] ,
56+ [ 'cp' , 'This command can copy files, potentially overwriting existing ones' ] ,
57+ [ 'ln' , 'This command can create links, potentially to sensitive files' ] ,
58+ [ 'mkfs' , 'This command can format filesystems' ] ,
59+ [ 'fdisk' , 'This command can modify disk partitions' ] ,
60+ [ 'mount' , 'This command can mount filesystems' ] ,
61+ [ 'umount' , 'This command can unmount filesystems' ] ,
62+ [ 'shutdown' , 'This command can shut down the system' ] ,
63+ [ 'reboot' , 'This command can restart the system' ] ,
64+ [ 'poweroff' , 'This command can power off the system' ] ,
65+ [ 'kill' , 'This command can terminate processes' ] ,
66+ [ 'killall' , 'This command can terminate multiple processes' ] ,
67+ [ 'pkill' , 'This command can terminate processes by name' ] ,
68+ [ 'iptables' , 'This command can modify firewall rules' ] ,
69+ [ 'ifconfig' , 'This command can configure network interfaces' ] ,
70+ [ 'route' , 'This command can modify network routing tables' ] ,
71+ [ 'useradd' , 'This command can create new user accounts' ] ,
72+ [ 'userdel' , 'This command can delete user accounts' ] ,
73+ [ 'passwd' , 'This command can change user passwords' ] ,
74+ [ 'visudo' , 'This command can modify sudo privileges' ] ,
75+ [ 'crontab' , 'This command can schedule tasks to run automatically' ] ,
76+ [ 'at' , 'This command can schedule one-time tasks' ] ,
77+ [ 'systemctl' , 'This command can control system services' ] ,
78+ [ 'service' , 'This command can start, stop, or restart system services' ] ,
79+ [ 'insmod' , 'This command can insert a module into the Linux kernel' ] ,
80+ [ 'rmmod' , 'This command can remove a module from the Linux kernel' ] ,
81+ [ 'modprobe' , 'This command can add or remove modules from the Linux kernel' ] ,
82+ [ 'apt' , 'This command can install, update, or remove software packages' ] ,
83+ [ 'yum' , 'This command can install, update, or remove software packages' ] ,
84+ [ 'dnf' , 'This command can install, update, or remove software packages' ] ,
85+ [ 'pacman' , 'This command can install, update, or remove software packages' ] ,
86+ [ 'tar' , 'This command can create or extract archives, potentially overwriting files' ] ,
87+ [ 'find' , 'This command can search for files and execute commands on them' ] ,
88+ [ 'grep' , 'This command can search file contents and potentially reveal sensitive information' ] ,
89+ [ 'awk' , 'This command can process and modify text files' ] ,
90+ [ 'sed' , 'This command can modify file contents' ] ,
91+ [ 'perl' , 'This command can execute Perl scripts' ] ,
92+ [ 'python' , 'This command can execute Python scripts' ] ,
93+ [ 'bash' , 'This command can execute bash scripts' ] ,
94+ [ 'sh' , 'This command can execute shell scripts' ] ,
95+ [ 'exec' , 'This command can execute commands replacing the current process' ] ,
96+ [ 'eval' , 'This command can execute arguments as a shell command' ] ,
97+ [ 'xargs' , 'This command can build and execute command lines from standard input' ] ,
98+ [ 'nohup' , 'This command can run commands immune to hangups' ] ,
99+ [ 'wget' , 'This command can download files from the web' ] ,
100+ [ 'curl' , 'This command can transfer data from or to a server' ] ,
101+ [ 'nc' , 'This command can arbitrarily read and write network connections' ] ,
102+ [ 'ssh' , 'This command can remotely access other systems' ] ,
103+ [ 'scp' , 'This command can copy files between hosts on a network' ] ,
104+ [ 'ftp' , 'This command can transfer files to and from a remote network' ] ,
105+ [ 'sftp' , 'This command can securely transfer files over SSH' ] ,
106+ [ 'rsync' , 'This command can synchronize files and directories' ] ,
107+ [ 'chroot' , 'This command can change the root directory' ] ,
108+ [ 'mount' , 'This command can mount file systems' ] ,
109+ [ 'umount' , 'This command can unmount file systems' ] ,
110+ [ 'lsof' , 'This command can list open files and the processes that opened them' ] ,
111+ [ 'strace' , 'This command can trace system calls and signals' ] ,
112+ [ 'gdb' , 'This command can debug programs' ] ,
113+ [ 'strings' , 'This command can print printable characters in files' ] ,
114+ ] )
13115export const maxBashToolResponseSize : number = 1024 * 1024 // 1MB
14116export const lineCount : number = 1024
15- export const dangerousPatterns : string [ ] = [ '|' , '<(' , '$(' , '`' , '>' , '&&' , '||' ]
16117
17118export interface ExecuteBashParams {
18119 command : string
19120 cwd ?: string
20121}
21122
123+ export interface CommandValidation {
124+ requiresAcceptance : boolean
125+ isHighRisk : boolean
126+ reason ?: string
127+ }
128+
22129export class ExecuteBash {
23130 private readonly command : string
24131 private readonly workingDirectory ?: string
@@ -34,7 +141,7 @@ export class ExecuteBash {
34141 throw new Error ( 'Bash command cannot be empty.' )
35142 }
36143
37- const args = ExecuteBash . parseCommand ( this . command )
144+ const args = split ( this . command )
38145 if ( ! args || args . length === 0 ) {
39146 throw new Error ( 'No command found.' )
40147 }
@@ -46,22 +153,60 @@ export class ExecuteBash {
46153 }
47154 }
48155
49- public requiresAcceptance ( ) : boolean {
156+ public requiresAcceptance ( ) : CommandValidation {
50157 try {
51- const args = ExecuteBash . parseCommand ( this . command )
158+ const args = split ( this . command )
52159 if ( ! args || args . length === 0 ) {
53- return true
160+ return { requiresAcceptance : true , isHighRisk : false }
161+ }
162+
163+ // Split commands by pipe and process each segment
164+ let currentCmd : string [ ] = [ ]
165+ const allCommands : string [ ] [ ] = [ ]
166+
167+ for ( const arg of args ) {
168+ if ( arg === '|' ) {
169+ if ( currentCmd . length > 0 ) {
170+ allCommands . push ( currentCmd )
171+ }
172+ currentCmd = [ ]
173+ } else if ( arg . includes ( '|' ) ) {
174+ return { requiresAcceptance : true , isHighRisk : false }
175+ } else {
176+ currentCmd . push ( arg )
177+ }
54178 }
55179
56- if ( args . some ( ( arg ) => dangerousPatterns . some ( ( pattern ) => arg . includes ( pattern ) ) ) ) {
57- return true
180+ if ( currentCmd . length > 0 ) {
181+ allCommands . push ( currentCmd )
58182 }
59183
60- const command = args [ 0 ]
61- return ! readOnlyCommands . includes ( command )
184+ for ( const cmdArgs of allCommands ) {
185+ if ( cmdArgs . length === 0 ) {
186+ return { requiresAcceptance : true , isHighRisk : false }
187+ }
188+
189+ if ( cmdArgs . some ( ( arg ) => Array . from ( dangerousPatterns ) . some ( ( pattern ) => arg . includes ( pattern ) ) ) ) {
190+ return { requiresAcceptance : true , isHighRisk : false }
191+ }
192+
193+ const command = cmdArgs [ 0 ]
194+
195+ if ( highRiskCommands . has ( command ) ) {
196+ const reason = highRiskCommands . get ( command )
197+ this . logger . warn ( `WARNING: High risk command detected: ${ command } ` )
198+ this . logger . warn ( `Reason: ${ reason } ` )
199+ return { requiresAcceptance : true , isHighRisk : true , reason : reason }
200+ }
201+
202+ if ( ! readOnlyCommands . has ( command ) ) {
203+ return { requiresAcceptance : true , isHighRisk : false }
204+ }
205+ }
206+ return { requiresAcceptance : false , isHighRisk : false }
62207 } catch ( error ) {
63208 this . logger . warn ( `Error while checking acceptance: ${ ( error as Error ) . message } ` )
64- return true
209+ return { requiresAcceptance : true , isHighRisk : false }
65210 }
66211 }
67212
@@ -167,43 +312,6 @@ export class ExecuteBash {
167312 return output
168313 }
169314
170- private static parseCommand ( command : string ) : string [ ] | undefined {
171- const result : string [ ] = [ ]
172- let current = ''
173- let inQuote : string | undefined
174- let escaped = false
175-
176- for ( const char of command ) {
177- if ( escaped ) {
178- current += char
179- escaped = false
180- } else if ( char === '\\' ) {
181- escaped = true
182- } else if ( inQuote ) {
183- if ( char === inQuote ) {
184- inQuote = undefined
185- } else {
186- current += char
187- }
188- } else if ( char === '"' || char === "'" ) {
189- inQuote = char
190- } else if ( char === ' ' || char === '\t' ) {
191- if ( current ) {
192- result . push ( current )
193- current = ''
194- }
195- } else {
196- current += char
197- }
198- }
199-
200- if ( current ) {
201- result . push ( current )
202- }
203-
204- return result
205- }
206-
207315 public queueDescription ( updates : Writable ) : void {
208316 updates . write ( `I will run the following shell command:\n` )
209317 updates . write ( '```bash\n' + this . command + '\n```' )
0 commit comments