use super::tool::RiskLevel;
use std::path::{Path, PathBuf};
#[allow(dead_code)]
pub const PROTECTED_FILES: &[&str] = &[
"C:\\Windows",
"C:\\Program Files",
"C:\\$Recycle.Bin",
"System Volume Information",
"C:\\Users\\Default",
"/etc",
"/dev",
"/proc",
"/sys",
"/root",
"/var/log",
"/boot",
".bashrc",
".zshrc",
".bash_history",
".gitconfig",
".ssh/",
".aws/",
".env",
"credentials.json",
"auth.json",
"id_rsa",
".mcp.json",
"hematite_memory.db",
];
#[allow(dead_code)]
pub fn path_is_safe(workspace_root: &Path, target: &Path) -> Result<PathBuf, String> {
let mut target_str = target.to_string_lossy().to_string().to_lowercase();
target_str = target_str
.replace("\\", "/")
.replace("\u{005c}", "/")
.replace("%5c", "/");
for protected in PROTECTED_FILES {
let prot_lower = protected.to_lowercase().replace("\\", "/");
if target_str.contains(&prot_lower) {
return Err(format!(
"AccessDenied: Path {} hits the Hematite Security Blacklist natively: {}",
target_str, protected
));
}
}
let resolved_path = match std::fs::canonicalize(target) {
Ok(p) => p,
Err(_) => {
let parent = target.parent().unwrap_or(Path::new(""));
let mut resolved_parent = std::fs::canonicalize(parent)
.map_err(|_| "AccessDenied: Invalid directory ancestry inside sandbox root. Path traversing halted!".to_string())?;
if let Some(name) = target.file_name() {
resolved_parent.push(name);
}
resolved_parent
}
};
let resolved_str = resolved_path
.to_string_lossy()
.to_string()
.to_lowercase()
.replace("\\", "/");
for protected in PROTECTED_FILES {
let prot_lower = protected.to_lowercase().replace("\\", "/");
if resolved_str.contains(&prot_lower) {
return Err(format!(
"AccessDenied: Canonicalized Sandbox resolution natively hits Blacklist bounds: {}",
protected
));
}
}
let resolved_workspace = std::fs::canonicalize(workspace_root).unwrap_or_default();
if !resolved_path.starts_with(&resolved_workspace) {
if target.is_absolute() {
return Ok(resolved_path);
}
return Err(format!("AccessDenied: ⛔ SANDBOX BREACHED ⛔ Attempted directory traversal outside project bounds: {:?}", resolved_path));
}
Ok(resolved_path)
}
#[allow(dead_code)]
pub fn bash_is_safe(cmd: &str) -> Result<(), String> {
let lower = cmd
.to_lowercase()
.replace("\\", "/")
.replace("\u{005c}", "/")
.replace("%5c", "/");
for protected in PROTECTED_FILES {
let prot_lower = protected.to_lowercase().replace("\\", "/");
if lower.contains(&prot_lower) {
return Err(format!("AccessDenied: Bash command structurally attempts to manipulate blacklisted system area: {}", protected));
}
}
let sandbox_redirects = [
"deno run",
"deno --version",
"deno -v",
"python -c ",
"python3 -c ",
"node -e ",
"node --eval",
];
for pattern in sandbox_redirects {
if lower.contains(pattern) {
return Err(format!(
"Use the run_code tool instead of shell for executing {} code. \
Shell is blocked for sandbox-style execution.",
pattern.split_whitespace().next().unwrap_or("code")
));
}
}
Ok(())
}
pub fn classify_bash_risk(cmd: &str) -> RiskLevel {
let lower = cmd.to_lowercase();
let high = [
"rm -",
"rm /",
"del /",
"del /f",
"rmdir /s",
"remove-item -r",
"curl ",
"wget ",
"invoke-webrequest",
"invoke-restmethod",
"fetch ",
"sudo ",
"runas ",
"su -",
"git push",
"git force",
"git reset --hard",
"git clean -f",
"shutdown",
"restart-computer",
"taskkill",
"format-volume",
"diskpart",
"format c",
"del c:\\",
".ssh/",
".aws/",
"credentials.json",
];
if high.iter().any(|p| lower.contains(p)) {
return RiskLevel::High;
}
let safe_prefixes = [
"cargo check",
"cargo build",
"cargo test",
"cargo fmt",
"cargo clippy",
"cargo run",
"cargo doc",
"cargo tree",
"rustc ",
"rustfmt ",
"git status",
"git log",
"git diff",
"git branch",
"git show",
"git stash list",
"git remote -v",
"ls ",
"ls\n",
"dir ",
"dir\n",
"echo ",
"pwd",
"whoami",
"cat ",
"type ",
"head ",
"tail ",
"get-childitem",
"get-content",
"get-location",
"cargo --version",
"rustc --version",
"git --version",
"node --version",
"npm --version",
"python --version",
"grep ",
"grep\n",
"rg ",
"rg\n",
"find ",
"find\n",
"select-string",
"select-object",
"where-object",
"sort ",
"sort\n",
"wc ",
"uniq ",
"cut ",
"file ",
"stat ",
"du ",
"df ",
"powershell -command \"select-string",
"powershell -command \"get-childitem",
"powershell -command \"get-content",
"powershell -command \"get-counter",
"powershell -command 'select-string",
"powershell -command 'get-childitem",
"powershell -command 'get-counter",
"get-counter",
"get-item",
"test-path",
"select-object",
"powershell -command \"get-item",
"powershell -command \"test-path",
"powershell -command \"select-object",
"powershell -command 'get-item",
"powershell -command 'test-path",
"powershell -command 'select-object",
];
if safe_prefixes
.iter()
.any(|p| lower.starts_with(p) || lower == p.trim())
{
return RiskLevel::Safe;
}
RiskLevel::Moderate
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_blacklist_windows_system() {
let root = Path::new("C:\\Users\\ocean\\Project");
let target = Path::new("C:\\Windows\\System32\\cmd.exe");
let result = path_is_safe(root, target);
assert!(
result.is_err(),
"Windows System directory should be blocked!"
);
assert!(result.unwrap_err().contains("Security Blacklist"));
}
#[test]
fn test_relative_parent_traversal_is_blocked() {
let root = std::env::current_dir().unwrap();
let result = path_is_safe(&root, Path::new(".."));
assert!(
result.is_err(),
"Relative traversal outside of workspace root should be blocked!"
);
assert!(result.unwrap_err().contains("SANDBOX BREACHED"));
}
#[test]
fn test_absolute_outside_path_is_allowed_when_not_blacklisted() {
let root = std::env::current_dir().unwrap();
if let Some(parent) = root.parent() {
let result = path_is_safe(&root, parent);
assert!(
result.is_ok(),
"Absolute non-blacklisted paths should follow the relaxed sandbox policy."
);
}
}
#[test]
fn test_bash_blacklist() {
let cmd = "ls C:\\Windows";
let result = bash_is_safe(cmd);
assert!(
result.is_err(),
"Bash command touching Windows should be blocked!"
);
assert!(result.unwrap_err().contains("blacklisted system area"));
}
#[test]
fn test_risk_classification() {
assert_eq!(classify_bash_risk("cargo check"), RiskLevel::Safe);
assert_eq!(classify_bash_risk("rm -rf /"), RiskLevel::High);
assert_eq!(classify_bash_risk("mkdir new_dir"), RiskLevel::Moderate);
assert_eq!(
classify_bash_risk("get-counter '\\PhysicalDisk(_Total)\\Avg. Disk Queue Length'"),
RiskLevel::Safe
);
assert_eq!(classify_bash_risk("powershell -command \"get-counter '\\PhysicalDisk(_Total)\\Avg. Disk Queue Length'\""), RiskLevel::Safe);
}
}