Skip to content

Commit 18f32bd

Browse files
committed
add reject reasons for the restricted bash commands
1 parent 453ab5b commit 18f32bd

1 file changed

Lines changed: 220 additions & 1 deletion

File tree

src/tools/bashexec.rs

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ impl BashExecutor {
1616
pub fn execute(&self, command: &str) -> Result<String> {
1717
if !self.is_safe_command(command) {
1818
return Err(SofosError::ToolExecution(
19-
"Command is not allowed. Only read-only commands are permitted.".to_string(),
19+
self.get_rejection_reason(command)
2020
));
2121
}
2222

@@ -259,6 +259,194 @@ impl BashExecutor {
259259
// git remote -v (view only), git config --list, git ls-files, etc.
260260
true
261261
}
262+
263+
fn get_rejection_reason(&self, command: &str) -> String {
264+
let command_lower = command.to_lowercase();
265+
266+
// Sudo check
267+
if command_lower.starts_with("sudo") || command_lower.contains(" sudo ") {
268+
return format!(
269+
"Command blocked: '{}'\nReason: Contains 'sudo' which requires privilege escalation.\nSofos only allows read-only operations for security.",
270+
command
271+
);
272+
}
273+
274+
// Parent directory traversal
275+
if command.contains("..") {
276+
return format!(
277+
"Command blocked: '{}'\nReason: Contains '..' (parent directory traversal).\nAll operations must stay within the current workspace directory.",
278+
command
279+
);
280+
}
281+
282+
// Directory change commands
283+
let directory_commands = ["cd", "pushd", "popd"];
284+
for cmd in &directory_commands {
285+
if command_lower.starts_with(cmd)
286+
|| command_lower.contains(&format!(" {}", cmd))
287+
|| command_lower.contains(&format!(";{}", cmd))
288+
|| command_lower.contains(&format!("&&{}", cmd))
289+
|| command_lower.contains(&format!("||{}", cmd))
290+
|| command_lower.contains(&format!("|{}", cmd))
291+
{
292+
return format!(
293+
"Command blocked: '{}'\nReason: Contains '{}' which changes the working directory.\nDirectory changes are not allowed for security. Use absolute paths from the workspace root instead.",
294+
command, cmd
295+
);
296+
}
297+
}
298+
299+
// Absolute paths
300+
if command.starts_with('/') || command.contains(" /")
301+
|| command.contains("|/") || command.contains(";/")
302+
|| command.contains("&&/") || command.contains("||/")
303+
{
304+
return format!(
305+
"Command blocked: '{}'\nReason: Contains absolute paths (starting with '/').\nOnly relative paths within the workspace are allowed.",
306+
command
307+
);
308+
}
309+
310+
// Git-specific blocking
311+
if !self.is_safe_git_command(&command_lower) {
312+
return self.get_git_rejection_reason(command);
313+
}
314+
315+
// Output redirection
316+
if command.contains('>') || command.contains(">>") {
317+
return format!(
318+
"Command blocked: '{}'\nReason: Contains output redirection ('>' or '>>').\nUse the write_file tool to create or modify files instead.",
319+
command
320+
);
321+
}
322+
323+
// Here-doc
324+
if command.contains("<<") {
325+
return format!(
326+
"Command blocked: '{}'\nReason: Contains here-doc ('<<').\nUse the write_file tool to create files instead.",
327+
command
328+
);
329+
}
330+
331+
// Forbidden commands
332+
let forbidden_commands = [
333+
("rm", "delete files", "Use the delete_file tool"),
334+
("mv", "move/rename files", "Use the move_file tool"),
335+
("cp", "copy files", "Use the copy_file tool"),
336+
("chmod", "change permissions", "File permissions cannot be modified"),
337+
("mkdir", "create directories", "Use the create_directory tool"),
338+
("rmdir", "remove directories", "Use the delete_directory tool"),
339+
("touch", "create/modify files", "Use the write_file tool"),
340+
];
341+
342+
for (cmd, action, alternative) in &forbidden_commands {
343+
if command_lower.starts_with(cmd)
344+
|| command_lower.starts_with(&format!("{} ", cmd))
345+
|| command_lower.contains(&format!(" {}", cmd))
346+
|| command_lower.contains(&format!("|{}", cmd))
347+
|| command_lower.contains(&format!(";{}", cmd))
348+
|| command_lower.contains(&format!("&&{}", cmd))
349+
|| command_lower.contains(&format!("||{}", cmd))
350+
{
351+
return format!(
352+
"Command blocked: '{}'\nReason: Contains '{}' which would {} (modification operation).\n{}.",
353+
command, cmd, action, alternative
354+
);
355+
}
356+
}
357+
358+
// Catch-all for other dangerous commands
359+
format!(
360+
"Command blocked: '{}'\nReason: Command contains potentially unsafe operations.\nSofos only allows read-only commands for security.",
361+
command
362+
)
363+
}
364+
365+
fn get_git_rejection_reason(&self, command: &str) -> String {
366+
let command_lower = command.to_lowercase();
367+
368+
// Check for specific dangerous git operations and provide helpful feedback
369+
if command_lower.contains("git push") {
370+
return format!(
371+
"Command blocked: '{}'\nReason: 'git push' sends data to remote repositories (network operation).\nAllowed: Use 'git status', 'git log', 'git diff' to view changes.",
372+
command
373+
);
374+
}
375+
376+
if command_lower.contains("git pull") || command_lower.contains("git fetch") {
377+
return format!(
378+
"Command blocked: '{}'\nReason: '{}' fetches data from remote repositories (network operation).\nAllowed: Use 'git status', 'git log', 'git diff' to view local changes.",
379+
command,
380+
if command_lower.contains("git pull") { "git pull" } else { "git fetch" }
381+
);
382+
}
383+
384+
if command_lower.contains("git clone") {
385+
return format!(
386+
"Command blocked: '{}'\nReason: 'git clone' downloads repositories (network operation and creates directories).\nClone repositories manually outside of Sofos.",
387+
command
388+
);
389+
}
390+
391+
if command_lower.contains("git commit") || command_lower.contains("git add") {
392+
return format!(
393+
"Command blocked: '{}'\nReason: '{}' modifies the git repository.\nAllowed: Use 'git status', 'git diff' to view changes. Create commits manually.",
394+
command,
395+
if command_lower.contains("git commit") { "git commit" } else { "git add" }
396+
);
397+
}
398+
399+
if command_lower.contains("git reset") || command_lower.contains("git clean") {
400+
return format!(
401+
"Command blocked: '{}'\nReason: '{}' is a destructive operation that discards changes.\nAllowed: Use 'git status', 'git log', 'git diff' to view repository state.",
402+
command,
403+
if command_lower.contains("git reset") { "git reset" } else { "git clean" }
404+
);
405+
}
406+
407+
if command_lower.contains("git checkout") || command_lower.contains("git switch") {
408+
return format!(
409+
"Command blocked: '{}'\nReason: '{}' changes branches or modifies working directory.\nAllowed: Use 'git branch' to list branches, 'git status' to see current branch.",
410+
command,
411+
if command_lower.contains("git checkout") { "git checkout" } else { "git switch" }
412+
);
413+
}
414+
415+
if command_lower.contains("git merge") || command_lower.contains("git rebase") {
416+
return format!(
417+
"Command blocked: '{}'\nReason: '{}' modifies git history and repository state.\nPerform merges/rebases manually outside of Sofos.",
418+
command,
419+
if command_lower.contains("git merge") { "git merge" } else { "git rebase" }
420+
);
421+
}
422+
423+
if command_lower.contains("git stash") && !command_lower.contains("git stash list") && !command_lower.contains("git stash show") {
424+
return format!(
425+
"Command blocked: '{}'\nReason: 'git stash' (without list/show) modifies repository state.\nAllowed: Use 'git stash list' or 'git stash show' to view stashed changes.",
426+
command
427+
);
428+
}
429+
430+
if command_lower.contains("git remote add") || command_lower.contains("git remote set-url") {
431+
return format!(
432+
"Command blocked: '{}'\nReason: Modifying git remotes could redirect pushes to unauthorized servers.\nAllowed: Use 'git remote -v' to view configured remotes.",
433+
command
434+
);
435+
}
436+
437+
if command_lower.contains("git submodule") {
438+
return format!(
439+
"Command blocked: '{}'\nReason: 'git submodule' can fetch from remote repositories (network operation).\nManage submodules manually outside of Sofos.",
440+
command
441+
);
442+
}
443+
444+
// Generic git rejection
445+
format!(
446+
"Command blocked: '{}'\nReason: Contains git operation that modifies repository or accesses network.\nAllowed git commands: status, log, diff, show, branch, remote -v, grep, blame, stash list/show",
447+
command
448+
)
449+
}
262450
}
263451

264452
#[cfg(test)]
@@ -452,4 +640,35 @@ mod tests {
452640
assert!(!executor.is_safe_command("echo test; git add ."));
453641
assert!(!executor.is_safe_command("git status || git pull"));
454642
}
643+
644+
#[test]
645+
fn test_error_messages_are_informative() {
646+
let executor = BashExecutor::new(PathBuf::from(".")).unwrap();
647+
648+
// Test git push error message
649+
let result = executor.execute("git push origin main");
650+
assert!(result.is_err());
651+
if let Err(crate::error::SofosError::ToolExecution(msg)) = result {
652+
assert!(msg.contains("git push origin main"));
653+
assert!(msg.contains("network operation"));
654+
assert!(msg.contains("git status"));
655+
}
656+
657+
// Test rm error message
658+
let result = executor.execute("rm file.txt");
659+
assert!(result.is_err());
660+
if let Err(crate::error::SofosError::ToolExecution(msg)) = result {
661+
assert!(msg.contains("rm file.txt"));
662+
assert!(msg.contains("delete files"));
663+
assert!(msg.contains("delete_file tool"));
664+
}
665+
666+
// Test cd error message
667+
let result = executor.execute("cd /tmp");
668+
assert!(result.is_err());
669+
if let Err(crate::error::SofosError::ToolExecution(msg)) = result {
670+
assert!(msg.contains("cd /tmp"));
671+
assert!(msg.contains("changes the working directory"));
672+
}
673+
}
455674
}

0 commit comments

Comments
 (0)