1use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11use torsh_core::error::{Result, TorshError};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SandboxConfig {
16 pub limits: ResourceLimits,
18 pub filesystem: FilesystemPolicy,
20 pub network: NetworkPolicy,
22 pub capabilities: CapabilitySet,
24 pub allowed_env_vars: HashSet<String>,
26 pub max_execution_time: Duration,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ResourceLimits {
33 pub max_cpu_percent: u8,
35 pub max_memory_bytes: u64,
37 pub max_disk_bytes: u64,
39 pub max_open_files: u32,
41 pub max_processes: u32,
43 pub max_threads: u32,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FilesystemPolicy {
50 pub readonly_paths: Vec<PathBuf>,
52 pub readwrite_paths: Vec<PathBuf>,
54 pub forbidden_paths: Vec<PathBuf>,
56 pub temp_dir: Option<PathBuf>,
58 pub use_overlay: bool,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct NetworkPolicy {
65 pub allowed: bool,
67 pub allowed_hosts: Vec<String>,
69 pub allowed_ports: Vec<PortRange>,
71 pub max_bandwidth: u64,
73 pub use_namespace: bool,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PortRange {
80 pub start: u16,
82 pub end: u16,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CapabilitySet {
89 pub read_files: bool,
91 pub write_files: bool,
93 pub execute: bool,
95 pub network: bool,
97 pub fork: bool,
99 pub system_info: bool,
101 pub load_libraries: bool,
103 pub custom: HashMap<String, bool>,
105}
106
107pub struct Sandbox {
109 config: SandboxConfig,
111 id: String,
113 active: bool,
115 monitor: ResourceMonitor,
117}
118
119#[derive(Debug, Clone)]
121pub struct ResourceMonitor {
122 pub cpu_usage: f64,
124 pub memory_usage: u64,
126 pub disk_usage: u64,
128 pub open_files: u32,
130 pub active_processes: u32,
132}
133
134#[derive(Debug)]
136pub struct SandboxResult<T> {
137 pub result: Result<T>,
139 pub resource_usage: ResourceUsageStats,
141 pub violations: Vec<SandboxViolation>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ResourceUsageStats {
148 pub peak_cpu: f64,
150 pub peak_memory: u64,
152 pub disk_reads: u64,
154 pub disk_writes: u64,
156 pub execution_time: Duration,
158 pub syscalls: u64,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SandboxViolation {
165 pub violation_type: ViolationType,
167 pub description: String,
169 pub severity: ViolationSeverity,
171 pub timestamp: chrono::DateTime<chrono::Utc>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub enum ViolationType {
178 ResourceLimit,
180 FileAccess,
182 NetworkAccess,
184 CapabilityDenied,
186 Timeout,
188 SuspiciousSystemCall,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub enum ViolationSeverity {
195 Low,
197 Medium,
199 High,
201 Critical,
203}
204
205impl Default for SandboxConfig {
206 fn default() -> Self {
207 Self {
208 limits: ResourceLimits::default(),
209 filesystem: FilesystemPolicy::default(),
210 network: NetworkPolicy::default(),
211 capabilities: CapabilitySet::minimal(),
212 allowed_env_vars: HashSet::new(),
213 max_execution_time: Duration::from_secs(300),
214 }
215 }
216}
217
218impl SandboxConfig {
219 pub fn restrictive() -> Self {
221 Self {
222 limits: ResourceLimits::restrictive(),
223 filesystem: FilesystemPolicy::readonly(),
224 network: NetworkPolicy::deny_all(),
225 capabilities: CapabilitySet::minimal(),
226 allowed_env_vars: HashSet::new(),
227 max_execution_time: Duration::from_secs(60),
228 }
229 }
230
231 pub fn permissive() -> Self {
233 Self {
234 limits: ResourceLimits::permissive(),
235 filesystem: FilesystemPolicy::default(),
236 network: NetworkPolicy::allow_all(),
237 capabilities: CapabilitySet::full(),
238 allowed_env_vars: std::env::vars().map(|(k, _)| k).collect(),
239 max_execution_time: Duration::from_secs(3600),
240 }
241 }
242
243 pub fn validate(&self) -> Result<()> {
245 if self.limits.max_cpu_percent > 100 {
247 return Err(TorshError::InvalidArgument(
248 "CPU limit cannot exceed 100%".to_string(),
249 ));
250 }
251
252 if self.limits.max_memory_bytes == 0 {
253 return Err(TorshError::InvalidArgument(
254 "Memory limit cannot be zero".to_string(),
255 ));
256 }
257
258 for path in &self.filesystem.forbidden_paths {
260 if !path.is_absolute() {
261 return Err(TorshError::InvalidArgument(format!(
262 "Forbidden path must be absolute: {:?}",
263 path
264 )));
265 }
266 }
267
268 Ok(())
269 }
270}
271
272impl Default for ResourceLimits {
273 fn default() -> Self {
274 Self {
275 max_cpu_percent: 80,
276 max_memory_bytes: 2 * 1024 * 1024 * 1024, max_disk_bytes: 10 * 1024 * 1024 * 1024, max_open_files: 1024,
279 max_processes: 100,
280 max_threads: 100,
281 }
282 }
283}
284
285impl ResourceLimits {
286 pub fn restrictive() -> Self {
288 Self {
289 max_cpu_percent: 50,
290 max_memory_bytes: 512 * 1024 * 1024, max_disk_bytes: 1024 * 1024 * 1024, max_open_files: 256,
293 max_processes: 10,
294 max_threads: 20,
295 }
296 }
297
298 pub fn permissive() -> Self {
300 Self {
301 max_cpu_percent: 100,
302 max_memory_bytes: 16 * 1024 * 1024 * 1024, max_disk_bytes: 100 * 1024 * 1024 * 1024, max_open_files: 4096,
305 max_processes: 1000,
306 max_threads: 1000,
307 }
308 }
309}
310
311impl Default for FilesystemPolicy {
312 fn default() -> Self {
313 Self {
314 readonly_paths: vec![],
315 readwrite_paths: vec![],
316 forbidden_paths: vec![
317 PathBuf::from("/etc/passwd"),
318 PathBuf::from("/etc/shadow"),
319 PathBuf::from("/root"),
320 ],
321 temp_dir: None,
322 use_overlay: false,
323 }
324 }
325}
326
327impl FilesystemPolicy {
328 pub fn readonly() -> Self {
330 Self {
331 readonly_paths: vec![PathBuf::from("/")],
332 readwrite_paths: vec![],
333 forbidden_paths: vec![],
334 temp_dir: Some(std::env::temp_dir()),
335 use_overlay: true,
336 }
337 }
338
339 pub fn can_read(&self, path: &Path) -> bool {
341 for forbidden in &self.forbidden_paths {
343 if path.starts_with(forbidden) {
344 return false;
345 }
346 }
347
348 for allowed in self.readonly_paths.iter().chain(&self.readwrite_paths) {
350 if path.starts_with(allowed) {
351 return true;
352 }
353 }
354
355 false
356 }
357
358 pub fn can_write(&self, path: &Path) -> bool {
360 for forbidden in &self.forbidden_paths {
362 if path.starts_with(forbidden) {
363 return false;
364 }
365 }
366
367 for allowed in &self.readwrite_paths {
369 if path.starts_with(allowed) {
370 return true;
371 }
372 }
373
374 false
375 }
376}
377
378impl Default for NetworkPolicy {
379 fn default() -> Self {
380 Self {
381 allowed: false,
382 allowed_hosts: vec![],
383 allowed_ports: vec![],
384 max_bandwidth: 10 * 1024 * 1024, use_namespace: false,
386 }
387 }
388}
389
390impl NetworkPolicy {
391 pub fn deny_all() -> Self {
393 Self {
394 allowed: false,
395 ..Default::default()
396 }
397 }
398
399 pub fn allow_all() -> Self {
401 Self {
402 allowed: true,
403 allowed_hosts: vec!["*".to_string()],
404 allowed_ports: vec![PortRange {
405 start: 1,
406 end: 65535,
407 }],
408 max_bandwidth: u64::MAX,
409 use_namespace: false,
410 }
411 }
412
413 pub fn is_host_allowed(&self, host: &str) -> bool {
415 if !self.allowed {
416 return false;
417 }
418
419 if self.allowed_hosts.contains(&"*".to_string()) {
420 return true;
421 }
422
423 self.allowed_hosts
424 .iter()
425 .any(|allowed| host == allowed || host.ends_with(&format!(".{}", allowed)))
426 }
427
428 pub fn is_port_allowed(&self, port: u16) -> bool {
430 if !self.allowed {
431 return false;
432 }
433
434 self.allowed_ports
435 .iter()
436 .any(|range| port >= range.start && port <= range.end)
437 }
438}
439
440impl CapabilitySet {
441 pub fn minimal() -> Self {
443 Self {
444 read_files: true,
445 write_files: false,
446 execute: false,
447 network: false,
448 fork: false,
449 system_info: false,
450 load_libraries: false,
451 custom: HashMap::new(),
452 }
453 }
454
455 pub fn full() -> Self {
457 Self {
458 read_files: true,
459 write_files: true,
460 execute: true,
461 network: true,
462 fork: true,
463 system_info: true,
464 load_libraries: true,
465 custom: HashMap::new(),
466 }
467 }
468
469 pub fn has_capability(&self, capability: &str) -> bool {
471 match capability {
472 "read_files" => self.read_files,
473 "write_files" => self.write_files,
474 "execute" => self.execute,
475 "network" => self.network,
476 "fork" => self.fork,
477 "system_info" => self.system_info,
478 "load_libraries" => self.load_libraries,
479 custom => self.custom.get(custom).copied().unwrap_or(false),
480 }
481 }
482}
483
484impl Sandbox {
485 pub fn new(config: SandboxConfig) -> Result<Self> {
487 config.validate()?;
488
489 let id = uuid::Uuid::new_v4().to_string();
490
491 Ok(Self {
492 config,
493 id,
494 active: false,
495 monitor: ResourceMonitor::new(),
496 })
497 }
498
499 pub fn activate(&mut self) -> Result<()> {
501 if self.active {
502 return Err(TorshError::InvalidArgument(
503 "Sandbox is already active".to_string(),
504 ));
505 }
506
507 #[cfg(target_os = "linux")]
509 self.activate_linux()?;
510
511 #[cfg(target_os = "macos")]
512 self.activate_macos()?;
513
514 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
515 self.activate_generic()?;
516
517 self.active = true;
518 Ok(())
519 }
520
521 pub fn deactivate(&mut self) -> Result<()> {
523 if !self.active {
524 return Ok(());
525 }
526
527 self.active = false;
529 Ok(())
530 }
531
532 pub fn execute<F, T>(&mut self, f: F) -> SandboxResult<T>
534 where
535 F: FnOnce() -> Result<T>,
536 {
537 let start_time = std::time::Instant::now();
538 let mut violations = Vec::new();
539
540 if let Err(e) = self.activate() {
542 return SandboxResult {
543 result: Err(e),
544 resource_usage: ResourceUsageStats::default(),
545 violations,
546 };
547 }
548
549 let result = f();
551
552 let _ = self.deactivate();
554
555 let execution_time = start_time.elapsed();
556
557 let resource_usage = ResourceUsageStats {
559 peak_cpu: self.monitor.cpu_usage,
560 peak_memory: self.monitor.memory_usage,
561 disk_reads: 0, disk_writes: 0, execution_time,
564 syscalls: 0, };
566
567 if execution_time > self.config.max_execution_time {
569 violations.push(SandboxViolation {
570 violation_type: ViolationType::Timeout,
571 description: format!(
572 "Execution time exceeded limit: {:?} > {:?}",
573 execution_time, self.config.max_execution_time
574 ),
575 severity: ViolationSeverity::High,
576 timestamp: chrono::Utc::now(),
577 });
578 }
579
580 SandboxResult {
581 result,
582 resource_usage,
583 violations,
584 }
585 }
586
587 #[cfg(target_os = "linux")]
589 fn activate_linux(&mut self) -> Result<()> {
590 Ok(())
596 }
597
598 #[cfg(target_os = "macos")]
600 fn activate_macos(&mut self) -> Result<()> {
601 Ok(())
606 }
607
608 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
610 fn activate_generic(&mut self) -> Result<()> {
611 Ok(())
613 }
614
615 pub fn id(&self) -> &str {
617 &self.id
618 }
619
620 pub fn config(&self) -> &SandboxConfig {
622 &self.config
623 }
624
625 pub fn monitor(&self) -> &ResourceMonitor {
627 &self.monitor
628 }
629}
630
631impl ResourceMonitor {
632 pub fn new() -> Self {
634 Self {
635 cpu_usage: 0.0,
636 memory_usage: 0,
637 disk_usage: 0,
638 open_files: 0,
639 active_processes: 0,
640 }
641 }
642
643 pub fn update(&mut self) {
645 }
648}
649
650impl Default for ResourceMonitor {
651 fn default() -> Self {
652 Self::new()
653 }
654}
655
656impl Default for ResourceUsageStats {
657 fn default() -> Self {
658 Self {
659 peak_cpu: 0.0,
660 peak_memory: 0,
661 disk_reads: 0,
662 disk_writes: 0,
663 execution_time: Duration::from_secs(0),
664 syscalls: 0,
665 }
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn test_sandbox_config() {
675 let config = SandboxConfig::default();
676 assert!(config.validate().is_ok());
677
678 let restrictive = SandboxConfig::restrictive();
679 assert!(restrictive.validate().is_ok());
680 assert_eq!(restrictive.limits.max_cpu_percent, 50);
681
682 let permissive = SandboxConfig::permissive();
683 assert!(permissive.validate().is_ok());
684 assert_eq!(permissive.limits.max_cpu_percent, 100);
685 }
686
687 #[test]
688 fn test_resource_limits() {
689 let limits = ResourceLimits::default();
690 assert!(limits.max_cpu_percent <= 100);
691 assert!(limits.max_memory_bytes > 0);
692
693 let restrictive = ResourceLimits::restrictive();
694 assert!(restrictive.max_memory_bytes < limits.max_memory_bytes);
695 }
696
697 #[test]
698 fn test_filesystem_policy() {
699 let policy = FilesystemPolicy::default();
700
701 assert!(!policy.can_read(Path::new("/etc/shadow")));
703 assert!(!policy.can_write(Path::new("/etc/shadow")));
704
705 let readonly = FilesystemPolicy::readonly();
706 assert!(readonly.can_read(Path::new("/usr/bin/ls")));
707 assert!(!readonly.can_write(Path::new("/usr/bin/ls")));
708 }
709
710 #[test]
711 fn test_network_policy() {
712 let deny_all = NetworkPolicy::deny_all();
713 assert!(!deny_all.allowed);
714 assert!(!deny_all.is_host_allowed("example.com"));
715 assert!(!deny_all.is_port_allowed(80));
716
717 let allow_all = NetworkPolicy::allow_all();
718 assert!(allow_all.allowed);
719 assert!(allow_all.is_host_allowed("example.com"));
720 assert!(allow_all.is_port_allowed(80));
721 }
722
723 #[test]
724 fn test_capability_set() {
725 let minimal = CapabilitySet::minimal();
726 assert!(minimal.read_files);
727 assert!(!minimal.write_files);
728 assert!(!minimal.execute);
729
730 let full = CapabilitySet::full();
731 assert!(full.read_files);
732 assert!(full.write_files);
733 assert!(full.execute);
734 }
735
736 #[test]
737 fn test_sandbox_creation() {
738 let config = SandboxConfig::default();
739 let sandbox = Sandbox::new(config);
740 assert!(sandbox.is_ok());
741 }
742
743 #[test]
744 fn test_sandbox_execution() {
745 let config = SandboxConfig::permissive();
746 let mut sandbox = Sandbox::new(config).unwrap();
747
748 let result = sandbox.execute(|| {
749 Ok(42)
751 });
752
753 assert!(result.result.is_ok());
754 assert_eq!(result.result.unwrap(), 42);
755 assert!(result.violations.is_empty());
756 }
757}