1use super::tool::RiskLevel;
2use std::path::{Path, PathBuf};
3
4#[allow(dead_code)]
5pub const PROTECTED_FILES: &[&str] = &[
6 "C:\\Windows",
8 "C:\\Program Files",
9 "C:\\$Recycle.Bin",
10 "System Volume Information",
11 "C:\\Users\\Default",
12 "/etc",
14 "/dev",
15 "/proc",
16 "/sys",
17 "/root",
18 "/var/log",
19 "/boot",
20 ".bashrc",
22 ".zshrc",
23 ".bash_history",
24 ".gitconfig",
25 ".ssh/",
26 ".aws/",
27 ".env",
28 "credentials.json",
29 "auth.json",
30 "id_rsa",
31 ".mcp.json",
33 "hematite_memory.db",
34];
35
36#[allow(dead_code)]
39pub fn path_is_safe(workspace_root: &Path, target: &Path) -> Result<PathBuf, String> {
40 let mut target_str = target.to_string_lossy().to_string().to_lowercase();
42 target_str = target_str
43 .replace("\\", "/")
44 .replace("\u{005c}", "/")
45 .replace("%5c", "/");
46
47 for protected in PROTECTED_FILES {
49 let prot_lower = protected.to_lowercase().replace("\\", "/");
50 if target_str.contains(&prot_lower) {
51 return Err(format!(
52 "AccessDenied: Path {} hits the Hematite Security Blacklist natively: {}",
53 target_str, protected
54 ));
55 }
56 }
57
58 let resolved_path = match std::fs::canonicalize(target) {
60 Ok(p) => p,
61 Err(_) => {
62 let parent = target.parent().unwrap_or(Path::new(""));
64 let mut resolved_parent = std::fs::canonicalize(parent)
65 .map_err(|_| "AccessDenied: Invalid directory ancestry inside sandbox root. Path traversing halted!".to_string())?;
66 if let Some(name) = target.file_name() {
67 resolved_parent.push(name);
68 }
69 resolved_parent
70 }
71 };
72
73 let resolved_str = resolved_path
75 .to_string_lossy()
76 .to_string()
77 .to_lowercase()
78 .replace("\\", "/");
79 for protected in PROTECTED_FILES {
80 let prot_lower = protected.to_lowercase().replace("\\", "/");
81 if resolved_str.contains(&prot_lower) {
82 return Err(format!(
83 "AccessDenied: Canonicalized Sandbox resolution natively hits Blacklist bounds: {}",
84 protected
85 ));
86 }
87 }
88
89 let resolved_workspace = std::fs::canonicalize(workspace_root).unwrap_or_default();
90
91 if !resolved_path.starts_with(&resolved_workspace) {
93 if target.is_absolute() {
95 return Ok(resolved_path);
96 }
97 return Err(format!("AccessDenied: ⛔ SANDBOX BREACHED ⛔ Attempted directory traversal outside project bounds: {:?}", resolved_path));
98 }
99
100 Ok(resolved_path)
101}
102
103#[allow(dead_code)]
105pub fn bash_is_safe(cmd: &str) -> Result<(), String> {
106 let lower = cmd
107 .to_lowercase()
108 .replace("\\", "/")
109 .replace("\u{005c}", "/")
110 .replace("%5c", "/");
111 for protected in PROTECTED_FILES {
112 let prot_lower = protected.to_lowercase().replace("\\", "/");
113 if lower.contains(&prot_lower) {
114 return Err(format!("AccessDenied: Bash command structurally attempts to manipulate blacklisted system area: {}", protected));
115 }
116 }
117
118 let sandbox_redirects = [
121 "deno run",
122 "deno --version",
123 "deno -v",
124 "python -c ",
125 "python3 -c ",
126 "node -e ",
127 "node --eval",
128 ];
129 for pattern in sandbox_redirects {
130 if lower.contains(pattern) {
131 return Err(format!(
132 "Use the run_code tool instead of shell for executing {} code. \
133 Shell is blocked for sandbox-style execution.",
134 pattern.split_whitespace().next().unwrap_or("code")
135 ));
136 }
137 }
138
139 let diagnostic_redirects = [
140 "nvidia-smi",
141 "wmic path win32_videocontroller",
142 "wmic path win32_perfformatteddata_gpu",
143 ];
144 for pattern in diagnostic_redirects {
145 if lower.contains(pattern) {
146 return Err(format!(
147 "Use the inspect_host tool with the relevant topic (e.g., topic=\"overclocker\" or topic=\"hardware\") \
148 instead of shell for executing {} diagnostics. \
149 Shell is blocked for raw hardware vitals to ensure high-fidelity bitmask decoding and session-wide history tracking.",
150 pattern.split_whitespace().next().unwrap_or("hardware")
151 ));
152 }
153 }
154
155 Ok(())
156}
157
158pub fn classify_bash_risk(cmd: &str) -> RiskLevel {
164 let lower = cmd.to_lowercase();
165
166 let high = [
168 "rm -",
170 "rm /",
171 "del /",
172 "del /f",
173 "rmdir /s",
174 "remove-item -r",
175 "curl ",
177 "wget ",
178 "invoke-webrequest",
179 "invoke-restmethod",
180 "fetch ",
181 "sudo ",
183 "runas ",
184 "su -",
185 "git push",
187 "git force",
188 "git reset --hard",
189 "git clean -f",
190 "shutdown",
192 "restart-computer",
193 "taskkill",
194 "format-volume",
195 "diskpart",
196 "format c",
197 "del c:\\",
198 ".ssh/",
200 ".aws/",
201 "credentials.json",
202 ];
203 if high.iter().any(|p| lower.contains(p)) {
204 return RiskLevel::High;
205 }
206
207 let safe_prefixes = [
209 "cargo check",
210 "cargo build",
211 "cargo test",
212 "cargo fmt",
213 "cargo clippy",
214 "cargo run",
215 "cargo doc",
216 "cargo tree",
217 "rustc ",
218 "rustfmt ",
219 "git status",
220 "git log",
221 "git diff",
222 "git branch",
223 "git show",
224 "git stash list",
225 "git remote -v",
226 "ls ",
227 "ls\n",
228 "dir ",
229 "dir\n",
230 "echo ",
231 "pwd",
232 "whoami",
233 "cat ",
234 "type ",
235 "head ",
236 "tail ",
237 "get-childitem",
238 "get-content",
239 "get-location",
240 "cargo --version",
241 "rustc --version",
242 "git --version",
243 "node --version",
244 "npm --version",
245 "python --version",
246 "grep ",
248 "grep\n",
249 "rg ",
250 "rg\n",
251 "find ",
252 "find\n",
253 "select-string",
254 "select-object",
255 "where-object",
256 "sort ",
257 "sort\n",
258 "wc ",
259 "uniq ",
260 "cut ",
261 "file ",
262 "stat ",
263 "du ",
264 "df ",
265 "powershell -command \"select-string",
267 "powershell -command \"get-childitem",
268 "powershell -command \"get-content",
269 "powershell -command \"get-counter",
270 "powershell -command 'select-string",
271 "powershell -command 'get-childitem",
272 "powershell -command 'get-counter",
273 "get-counter",
274 "get-item",
275 "test-path",
276 "select-object",
277 "powershell -command \"get-item",
278 "powershell -command \"test-path",
279 "powershell -command \"select-object",
280 "powershell -command 'get-item",
281 "powershell -command 'test-path",
282 "powershell -command 'select-object",
283 "get-smbencryptionstatus",
284 "get-smbshare",
285 "get-smbsession",
286 "get-netlanmanagerconnection",
287 ];
288 if safe_prefixes
289 .iter()
290 .any(|p| lower.starts_with(p) || lower == p.trim())
291 {
292 return RiskLevel::Safe;
293 }
294
295 RiskLevel::Moderate
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use std::path::Path;
303
304 #[test]
305 fn test_blacklist_windows_system() {
306 let root = Path::new("C:\\Users\\ocean\\Project");
308 let target = Path::new("C:\\Windows\\System32\\cmd.exe");
309 let result = path_is_safe(root, target);
310 assert!(
311 result.is_err(),
312 "Windows System directory should be blocked!"
313 );
314 assert!(result.unwrap_err().contains("Security Blacklist"));
315 }
316
317 #[test]
318 fn test_relative_parent_traversal_is_blocked() {
319 let root = std::env::current_dir().unwrap();
320 let result = path_is_safe(&root, Path::new(".."));
321 assert!(
322 result.is_err(),
323 "Relative traversal outside of workspace root should be blocked!"
324 );
325 assert!(result.unwrap_err().contains("SANDBOX BREACHED"));
326 }
327
328 #[test]
329 fn test_absolute_outside_path_is_allowed_when_not_blacklisted() {
330 let root = std::env::current_dir().unwrap();
331 if let Some(parent) = root.parent() {
332 let result = path_is_safe(&root, parent);
333 assert!(
334 result.is_ok(),
335 "Absolute non-blacklisted paths should follow the relaxed sandbox policy."
336 );
337 }
338 }
339
340 #[test]
341 fn test_bash_blacklist() {
342 let cmd = "ls C:\\Windows";
343 let result = bash_is_safe(cmd);
344 assert!(
345 result.is_err(),
346 "Bash command touching Windows should be blocked!"
347 );
348 assert!(result.unwrap_err().contains("blacklisted system area"));
349 }
350
351 #[test]
352 fn test_risk_classification() {
353 assert_eq!(classify_bash_risk("cargo check"), RiskLevel::Safe);
354 assert_eq!(classify_bash_risk("rm -rf /"), RiskLevel::High);
355 assert_eq!(classify_bash_risk("mkdir new_dir"), RiskLevel::Moderate);
356 assert_eq!(
357 classify_bash_risk("get-counter '\\PhysicalDisk(_Total)\\Avg. Disk Queue Length'"),
358 RiskLevel::Safe
359 );
360 assert_eq!(classify_bash_risk("powershell -command \"get-counter '\\PhysicalDisk(_Total)\\Avg. Disk Queue Length'\""), RiskLevel::Safe);
361 }
362}