Skip to main content

torsh_package/
sandbox.rs

1//! Sandboxing system for safe package execution
2//!
3//! This module provides a secure execution environment for untrusted packages
4//! with resource limits, file system restrictions, and network isolation.
5
6use 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/// Sandbox configuration
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SandboxConfig {
16    /// Resource limits
17    pub limits: ResourceLimits,
18    /// File system access policy
19    pub filesystem: FilesystemPolicy,
20    /// Network access policy
21    pub network: NetworkPolicy,
22    /// Capability restrictions
23    pub capabilities: CapabilitySet,
24    /// Environment variables allowed
25    pub allowed_env_vars: HashSet<String>,
26    /// Maximum execution time
27    pub max_execution_time: Duration,
28}
29
30/// Resource limits for sandboxed execution
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ResourceLimits {
33    /// Maximum CPU usage (percentage, 0-100)
34    pub max_cpu_percent: u8,
35    /// Maximum memory in bytes
36    pub max_memory_bytes: u64,
37    /// Maximum disk space in bytes
38    pub max_disk_bytes: u64,
39    /// Maximum number of open files
40    pub max_open_files: u32,
41    /// Maximum number of processes
42    pub max_processes: u32,
43    /// Maximum number of threads
44    pub max_threads: u32,
45}
46
47/// File system access policy
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct FilesystemPolicy {
50    /// Read-only paths
51    pub readonly_paths: Vec<PathBuf>,
52    /// Read-write paths
53    pub readwrite_paths: Vec<PathBuf>,
54    /// Forbidden paths (blacklist)
55    pub forbidden_paths: Vec<PathBuf>,
56    /// Temporary directory for sandbox
57    pub temp_dir: Option<PathBuf>,
58    /// Whether to use virtual filesystem overlay
59    pub use_overlay: bool,
60}
61
62/// Network access policy
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct NetworkPolicy {
65    /// Whether network access is allowed at all
66    pub allowed: bool,
67    /// Allowed hostnames (whitelist)
68    pub allowed_hosts: Vec<String>,
69    /// Allowed port ranges
70    pub allowed_ports: Vec<PortRange>,
71    /// Maximum bandwidth in bytes/sec
72    pub max_bandwidth: u64,
73    /// Whether to use network namespace isolation
74    pub use_namespace: bool,
75}
76
77/// Port range for network policy
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PortRange {
80    /// Start port (inclusive)
81    pub start: u16,
82    /// End port (inclusive)
83    pub end: u16,
84}
85
86/// Capability set for fine-grained permissions
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CapabilitySet {
89    /// Can read from filesystem
90    pub read_files: bool,
91    /// Can write to filesystem
92    pub write_files: bool,
93    /// Can execute programs
94    pub execute: bool,
95    /// Can access network
96    pub network: bool,
97    /// Can create processes
98    pub fork: bool,
99    /// Can access system information
100    pub system_info: bool,
101    /// Can load dynamic libraries
102    pub load_libraries: bool,
103    /// Custom capabilities
104    pub custom: HashMap<String, bool>,
105}
106
107/// Sandbox execution context
108pub struct Sandbox {
109    /// Sandbox configuration
110    config: SandboxConfig,
111    /// Sandbox ID for tracking
112    id: String,
113    /// Whether sandbox is active
114    active: bool,
115    /// Resource usage monitor
116    monitor: ResourceMonitor,
117}
118
119/// Resource usage monitor
120#[derive(Debug, Clone)]
121pub struct ResourceMonitor {
122    /// Current CPU usage percentage
123    pub cpu_usage: f64,
124    /// Current memory usage in bytes
125    pub memory_usage: u64,
126    /// Current disk usage in bytes
127    pub disk_usage: u64,
128    /// Number of open files
129    pub open_files: u32,
130    /// Number of active processes
131    pub active_processes: u32,
132}
133
134/// Sandbox execution result
135#[derive(Debug)]
136pub struct SandboxResult<T> {
137    /// Execution result
138    pub result: Result<T>,
139    /// Resource usage statistics
140    pub resource_usage: ResourceUsageStats,
141    /// Sandbox violations detected
142    pub violations: Vec<SandboxViolation>,
143}
144
145/// Resource usage statistics after execution
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ResourceUsageStats {
148    /// Peak CPU usage percentage
149    pub peak_cpu: f64,
150    /// Peak memory usage in bytes
151    pub peak_memory: u64,
152    /// Total disk reads in bytes
153    pub disk_reads: u64,
154    /// Total disk writes in bytes
155    pub disk_writes: u64,
156    /// Total execution time
157    pub execution_time: Duration,
158    /// Number of system calls made
159    pub syscalls: u64,
160}
161
162/// Sandbox violation record
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SandboxViolation {
165    /// Type of violation
166    pub violation_type: ViolationType,
167    /// Description of the violation
168    pub description: String,
169    /// Severity level
170    pub severity: ViolationSeverity,
171    /// When the violation occurred
172    pub timestamp: chrono::DateTime<chrono::Utc>,
173}
174
175/// Type of sandbox violation
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub enum ViolationType {
178    /// Resource limit exceeded
179    ResourceLimit,
180    /// Forbidden file access
181    FileAccess,
182    /// Forbidden network access
183    NetworkAccess,
184    /// Missing capability
185    CapabilityDenied,
186    /// Execution timeout
187    Timeout,
188    /// Suspicious system call
189    SuspiciousSystemCall,
190}
191
192/// Violation severity level
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub enum ViolationSeverity {
195    /// Low severity (logged but allowed)
196    Low,
197    /// Medium severity (warned)
198    Medium,
199    /// High severity (blocked)
200    High,
201    /// Critical severity (sandbox terminated)
202    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    /// Create a restrictive sandbox configuration
220    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    /// Create a permissive sandbox configuration (for trusted packages)
232    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    /// Validate sandbox configuration
244    pub fn validate(&self) -> Result<()> {
245        // Check resource limits
246        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        // Check filesystem policy
259        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, // 2GB
277            max_disk_bytes: 10 * 1024 * 1024 * 1024,  // 10GB
278            max_open_files: 1024,
279            max_processes: 100,
280            max_threads: 100,
281        }
282    }
283}
284
285impl ResourceLimits {
286    /// Create restrictive resource limits
287    pub fn restrictive() -> Self {
288        Self {
289            max_cpu_percent: 50,
290            max_memory_bytes: 512 * 1024 * 1024, // 512MB
291            max_disk_bytes: 1024 * 1024 * 1024,  // 1GB
292            max_open_files: 256,
293            max_processes: 10,
294            max_threads: 20,
295        }
296    }
297
298    /// Create permissive resource limits
299    pub fn permissive() -> Self {
300        Self {
301            max_cpu_percent: 100,
302            max_memory_bytes: 16 * 1024 * 1024 * 1024, // 16GB
303            max_disk_bytes: 100 * 1024 * 1024 * 1024,  // 100GB
304            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    /// Create readonly filesystem policy
329    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    /// Check if path is allowed for reading
340    pub fn can_read(&self, path: &Path) -> bool {
341        // Check if in forbidden paths
342        for forbidden in &self.forbidden_paths {
343            if path.starts_with(forbidden) {
344                return false;
345            }
346        }
347
348        // Check if in readonly or readwrite paths
349        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    /// Check if path is allowed for writing
359    pub fn can_write(&self, path: &Path) -> bool {
360        // Check if in forbidden paths
361        for forbidden in &self.forbidden_paths {
362            if path.starts_with(forbidden) {
363                return false;
364            }
365        }
366
367        // Only readwrite paths are writable
368        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, // 10 MB/s
385            use_namespace: false,
386        }
387    }
388}
389
390impl NetworkPolicy {
391    /// Create deny-all network policy
392    pub fn deny_all() -> Self {
393        Self {
394            allowed: false,
395            ..Default::default()
396        }
397    }
398
399    /// Create allow-all network policy
400    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    /// Check if host is allowed
414    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    /// Check if port is allowed
429    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    /// Create minimal capability set (very restrictive)
442    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    /// Create full capability set (permissive)
456    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    /// Check if a capability is granted
470    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    /// Create a new sandbox
486    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    /// Activate the sandbox
500    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        // Platform-specific sandbox activation
508        #[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    /// Deactivate the sandbox
522    pub fn deactivate(&mut self) -> Result<()> {
523        if !self.active {
524            return Ok(());
525        }
526
527        // Platform-specific sandbox deactivation
528        self.active = false;
529        Ok(())
530    }
531
532    /// Execute a function in the sandbox
533    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        // Activate sandbox
541        if let Err(e) = self.activate() {
542            return SandboxResult {
543                result: Err(e),
544                resource_usage: ResourceUsageStats::default(),
545                violations,
546            };
547        }
548
549        // Execute function
550        let result = f();
551
552        // Deactivate sandbox
553        let _ = self.deactivate();
554
555        let execution_time = start_time.elapsed();
556
557        // Collect resource usage
558        let resource_usage = ResourceUsageStats {
559            peak_cpu: self.monitor.cpu_usage,
560            peak_memory: self.monitor.memory_usage,
561            disk_reads: 0,  // Would be collected from OS
562            disk_writes: 0, // Would be collected from OS
563            execution_time,
564            syscalls: 0, // Would be collected from tracing
565        };
566
567        // Check for violations
568        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    /// Linux-specific sandbox activation
588    #[cfg(target_os = "linux")]
589    fn activate_linux(&mut self) -> Result<()> {
590        // In production, would use:
591        // - seccomp for system call filtering
592        // - namespaces for isolation
593        // - cgroups for resource limits
594        // For now, this is a placeholder
595        Ok(())
596    }
597
598    /// macOS-specific sandbox activation
599    #[cfg(target_os = "macos")]
600    fn activate_macos(&mut self) -> Result<()> {
601        // In production, would use:
602        // - sandbox-exec for sandboxing
603        // - process sandboxing APIs
604        // For now, this is a placeholder
605        Ok(())
606    }
607
608    /// Generic sandbox activation (fallback)
609    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
610    fn activate_generic(&mut self) -> Result<()> {
611        // Limited sandboxing capabilities on other platforms
612        Ok(())
613    }
614
615    /// Get sandbox ID
616    pub fn id(&self) -> &str {
617        &self.id
618    }
619
620    /// Get sandbox configuration
621    pub fn config(&self) -> &SandboxConfig {
622        &self.config
623    }
624
625    /// Get resource monitor
626    pub fn monitor(&self) -> &ResourceMonitor {
627        &self.monitor
628    }
629}
630
631impl ResourceMonitor {
632    /// Create a new resource monitor
633    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    /// Update resource usage
644    pub fn update(&mut self) {
645        // In production, would query actual system metrics
646        // For now, placeholder values
647    }
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        // Forbidden paths should not be readable
702        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            // Simple test function
750            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}