Skip to content

Commit f7031f2

Browse files
committed
feat(chat): Add warning UI for high risk bash command
1 parent eed4803 commit f7031f2

File tree

9 files changed

+2120
-3552
lines changed

9 files changed

+2120
-3552
lines changed

package-lock.json

Lines changed: 1927 additions & 3478 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,8 @@
492492
"umd-compat-loader": "^2.1.2",
493493
"vue-loader": "^17.2.2",
494494
"vue-style-loader": "^4.1.3",
495-
"webfont": "^11.2.26"
495+
"webfont": "^11.2.26",
496+
"shlex": "^2.1.2"
496497
},
497498
"dependencies": {
498499
"@amzn/amazon-q-developer-streaming-client": "file:../../src.gen/@amzn/amazon-q-developer-streaming-client",

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,10 @@ export class ChatController {
662662
try {
663663
await ToolUtils.validate(tool)
664664

665-
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse)
665+
const chatStream = new ChatStream(this.messenger, tabID, triggerID, toolUse, {
666+
requiresAcceptance: false,
667+
isHighRisk: false,
668+
})
666669
const output = await ToolUtils.invoke(tool, chatStream)
667670

668671
toolResults.push({

packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { ToolType, ToolUtils } from '../../../tools/toolUtils'
4545
import { ChatStream } from '../../../tools/chatStream'
4646
import path from 'path'
4747
import { getWorkspaceForFile } from '../../../../shared/utilities/workspaceUtils'
48+
import { CommandValidation } from '../../../tools/executeBash'
4849

4950
export type StaticTextResponseType = 'quick-action-help' | 'onboarding-help' | 'transform' | 'help'
5051

@@ -220,11 +221,12 @@ export class Messenger {
220221
if (tool.type === ToolType.FsWrite) {
221222
session.setShowDiffOnFileWrite(true)
222223
}
223-
const requiresAcceptance = ToolUtils.requiresAcceptance(tool)
224-
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, requiresAcceptance)
224+
const validation = ToolUtils.requiresAcceptance(tool)
225+
226+
const chatStream = new ChatStream(this, tabID, triggerID, toolUse, validation)
225227
ToolUtils.queueDescription(tool, chatStream)
226228

227-
if (!requiresAcceptance) {
229+
if (!validation.requiresAcceptance) {
228230
// Need separate id for read tool and safe bash command execution as 'confirm-tool-use' id is required to change button status from `Confirm` to `Confirmed` state in cwChatConnector.ts which will impact generic tool execution.
229231
this.dispatcher.sendCustomFormActionMessage(
230232
new CustomFormActionMessage(tabID, {
@@ -432,17 +434,21 @@ export class Messenger {
432434
tabID: string,
433435
triggerID: string,
434436
toolUse: ToolUse | undefined,
435-
requiresAcceptance = false
437+
validation: CommandValidation
436438
) {
437439
const buttons: ChatItemButton[] = []
438440
let fileList: ChatItemContent['fileList'] = undefined
439-
if (requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) {
441+
if (validation.requiresAcceptance && toolUse?.name === ToolType.ExecuteBash) {
440442
buttons.push({
441443
id: 'confirm-tool-use',
442444
text: 'Confirm',
443445
position: 'outside',
444446
status: 'info',
445447
})
448+
449+
if (validation.isHighRisk) {
450+
message = '⚠️ WARNING: High risk command detected:\n\n' + message
451+
}
446452
} else if (toolUse?.name === ToolType.FsWrite) {
447453
// FileList
448454
const absoluteFilePath = (toolUse?.input as any).path
@@ -471,7 +477,7 @@ export class Messenger {
471477
this.dispatcher.sendChatMessage(
472478
new ChatMessage(
473479
{
474-
message,
480+
message: message,
475481
messageType: 'answer-part',
476482
followUps: undefined,
477483
followUpsHeader: undefined,

packages/core/src/codewhispererChat/tools/chatStream.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Writable } from 'stream'
77
import { getLogger } from '../../shared/logger/logger'
88
import { Messenger } from '../controllers/chat/messenger/messenger'
99
import { ToolUse } from '@amzn/codewhisperer-streaming'
10+
import { CommandValidation } from './executeBash'
1011

1112
/**
1213
* A writable stream that feeds each chunk/line to the chat UI.
@@ -20,7 +21,7 @@ export class ChatStream extends Writable {
2021
private readonly tabID: string,
2122
private readonly triggerID: string,
2223
private readonly toolUse: ToolUse | undefined,
23-
private readonly requiresAcceptance = false,
24+
private readonly validation: CommandValidation,
2425
private readonly logger = getLogger('chatStream')
2526
) {
2627
super()
@@ -37,7 +38,7 @@ export class ChatStream extends Writable {
3738
this.tabID,
3839
this.triggerID,
3940
this.toolUse,
40-
this.requiresAcceptance
41+
this.validation
4142
)
4243
callback()
4344
}
@@ -49,7 +50,7 @@ export class ChatStream extends Writable {
4950
this.tabID,
5051
this.triggerID,
5152
this.toolUse,
52-
this.requiresAcceptance
53+
this.validation
5354
)
5455
}
5556
callback()

packages/core/src/codewhispererChat/tools/executeBash.ts

Lines changed: 157 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,127 @@
55

66
import { Writable } from 'stream'
77
import { getLogger } from '../../shared/logger/logger'
8-
import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir()
8+
import { fs } from '../../shared/fs/fs'
99
import { ChildProcess, ChildProcessOptions } from '../../shared/utilities/processUtils'
1010
import { 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+
])
13115
export const maxBashToolResponseSize: number = 1024 * 1024 // 1MB
14116
export const lineCount: number = 1024
15-
export const dangerousPatterns: string[] = ['|', '<(', '$(', '`', '>', '&&', '||']
16117

17118
export 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+
22129
export 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```')

packages/core/src/codewhispererChat/tools/toolUtils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { Writable } from 'stream'
66
import { FsRead, FsReadParams } from './fsRead'
77
import { FsWrite, FsWriteParams } from './fsWrite'
8-
import { ExecuteBash, ExecuteBashParams } from './executeBash'
8+
import { CommandValidation, ExecuteBash, ExecuteBashParams } from './executeBash'
99
import { ToolResult, ToolResultContentBlock, ToolResultStatus, ToolUse } from '@amzn/codewhisperer-streaming'
1010
import { InvokeOutput } from './toolShared'
1111
import { ListDirectory, ListDirectoryParams } from './listDirectory'
@@ -37,16 +37,16 @@ export class ToolUtils {
3737
}
3838
}
3939

40-
static requiresAcceptance(tool: Tool) {
40+
static requiresAcceptance(tool: Tool): CommandValidation {
4141
switch (tool.type) {
4242
case ToolType.FsRead:
43-
return false
43+
return { requiresAcceptance: false, isHighRisk: false }
4444
case ToolType.FsWrite:
45-
return true
45+
return { requiresAcceptance: true, isHighRisk: false }
4646
case ToolType.ExecuteBash:
4747
return tool.tool.requiresAcceptance()
4848
case ToolType.ListDirectory:
49-
return false
49+
return { requiresAcceptance: false, isHighRisk: false }
5050
}
5151
}
5252

0 commit comments

Comments
 (0)