Skip to main content

driven/security/
sandbox.rs

1//! Rule Sandbox
2//!
3//! Isolated execution environment for rule processing.
4
5use super::{Capability, CapabilityManifest};
6use crate::Result;
7use std::collections::HashSet;
8use std::path::{Path, PathBuf};
9
10/// Sandbox configuration
11#[derive(Debug, Clone)]
12pub struct SandboxConfig {
13    /// Allowed read paths
14    pub read_paths: HashSet<PathBuf>,
15    /// Allowed write paths
16    pub write_paths: HashSet<PathBuf>,
17    /// Allowed environment variables
18    pub env_vars: HashSet<String>,
19    /// Maximum memory (bytes)
20    pub max_memory: usize,
21    /// Maximum execution time (ms)
22    pub max_time_ms: u64,
23    /// Allow network access
24    pub allow_network: bool,
25    /// Allow process spawning
26    pub allow_spawn: bool,
27}
28
29impl SandboxConfig {
30    /// Create a restrictive sandbox
31    pub fn restricted() -> Self {
32        Self {
33            read_paths: HashSet::new(),
34            write_paths: HashSet::new(),
35            env_vars: HashSet::new(),
36            max_memory: 64 * 1024 * 1024, // 64MB
37            max_time_ms: 5000,            // 5 seconds
38            allow_network: false,
39            allow_spawn: false,
40        }
41    }
42
43    /// Create a permissive sandbox
44    pub fn permissive() -> Self {
45        Self {
46            read_paths: HashSet::new(), // Empty = allow all
47            write_paths: HashSet::new(),
48            env_vars: HashSet::new(),
49            max_memory: 512 * 1024 * 1024, // 512MB
50            max_time_ms: 30000,            // 30 seconds
51            allow_network: true,
52            allow_spawn: true,
53        }
54    }
55
56    /// Allow reading from a path
57    pub fn allow_read(mut self, path: impl Into<PathBuf>) -> Self {
58        self.read_paths.insert(path.into());
59        self
60    }
61
62    /// Allow writing to a path
63    pub fn allow_write(mut self, path: impl Into<PathBuf>) -> Self {
64        self.write_paths.insert(path.into());
65        self
66    }
67
68    /// Allow access to environment variable
69    pub fn allow_env(mut self, var: impl Into<String>) -> Self {
70        self.env_vars.insert(var.into());
71        self
72    }
73
74    /// Set memory limit
75    pub fn with_memory_limit(mut self, bytes: usize) -> Self {
76        self.max_memory = bytes;
77        self
78    }
79
80    /// Set time limit
81    pub fn with_time_limit(mut self, ms: u64) -> Self {
82        self.max_time_ms = ms;
83        self
84    }
85
86    /// Convert to capability manifest
87    pub fn to_manifest(&self) -> CapabilityManifest {
88        let mut manifest = CapabilityManifest::new();
89
90        if !self.read_paths.is_empty() {
91            manifest.require(Capability::FileRead);
92        }
93
94        if !self.write_paths.is_empty() {
95            manifest.require(Capability::FileWrite);
96        }
97
98        if !self.env_vars.is_empty() {
99            manifest.require(Capability::Environment);
100        }
101
102        if self.allow_network {
103            manifest.require(Capability::Network);
104        } else {
105            manifest.deny(Capability::Network);
106        }
107
108        if self.allow_spawn {
109            manifest.require(Capability::Process);
110        } else {
111            manifest.deny(Capability::Process);
112        }
113
114        manifest
115    }
116}
117
118impl Default for SandboxConfig {
119    fn default() -> Self {
120        Self::restricted()
121    }
122}
123
124/// Sandbox for isolated rule execution
125#[derive(Debug)]
126pub struct Sandbox {
127    /// Configuration
128    config: SandboxConfig,
129    /// Execution started
130    started: Option<std::time::Instant>,
131    /// Memory used
132    memory_used: usize,
133    /// Violations detected
134    violations: Vec<SandboxViolation>,
135}
136
137/// Sandbox violation
138#[derive(Debug, Clone)]
139pub struct SandboxViolation {
140    /// Violation type
141    pub kind: ViolationKind,
142    /// Description
143    pub message: String,
144    /// Timestamp
145    pub timestamp: std::time::Instant,
146}
147
148/// Violation types
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum ViolationKind {
151    /// Unauthorized file read
152    UnauthorizedRead,
153    /// Unauthorized file write
154    UnauthorizedWrite,
155    /// Memory limit exceeded
156    MemoryExceeded,
157    /// Time limit exceeded
158    TimeExceeded,
159    /// Network access denied
160    NetworkDenied,
161    /// Process spawn denied
162    SpawnDenied,
163    /// Environment access denied
164    EnvDenied,
165}
166
167impl Sandbox {
168    /// Create a new sandbox
169    pub fn new(config: SandboxConfig) -> Self {
170        Self {
171            config,
172            started: None,
173            memory_used: 0,
174            violations: Vec::new(),
175        }
176    }
177
178    /// Start sandbox execution
179    pub fn start(&mut self) {
180        self.started = Some(std::time::Instant::now());
181        self.memory_used = 0;
182        self.violations.clear();
183    }
184
185    /// Check if execution is within time limit
186    pub fn check_time(&mut self) -> Result<bool> {
187        if let Some(started) = self.started {
188            if started.elapsed().as_millis() as u64 > self.config.max_time_ms {
189                self.record_violation(ViolationKind::TimeExceeded, "Time limit exceeded");
190                return Ok(false);
191            }
192        }
193        Ok(true)
194    }
195
196    /// Check if memory allocation is allowed
197    pub fn check_memory(&mut self, bytes: usize) -> Result<bool> {
198        if self.memory_used + bytes > self.config.max_memory {
199            self.record_violation(ViolationKind::MemoryExceeded, "Memory limit exceeded");
200            return Ok(false);
201        }
202        self.memory_used += bytes;
203        Ok(true)
204    }
205
206    /// Check if file read is allowed
207    pub fn check_read(&mut self, path: &Path) -> bool {
208        // If no specific paths, deny all
209        if self.config.read_paths.is_empty() {
210            self.record_violation(
211                ViolationKind::UnauthorizedRead,
212                &format!("Read denied: {}", path.display()),
213            );
214            return false;
215        }
216
217        // Check if path is under any allowed path
218        let allowed = self
219            .config
220            .read_paths
221            .iter()
222            .any(|allowed| path.starts_with(allowed) || path == allowed);
223
224        if !allowed {
225            self.record_violation(
226                ViolationKind::UnauthorizedRead,
227                &format!("Read denied: {}", path.display()),
228            );
229        }
230
231        allowed
232    }
233
234    /// Check if file write is allowed
235    pub fn check_write(&mut self, path: &Path) -> bool {
236        if self.config.write_paths.is_empty() {
237            self.record_violation(
238                ViolationKind::UnauthorizedWrite,
239                &format!("Write denied: {}", path.display()),
240            );
241            return false;
242        }
243
244        let allowed = self
245            .config
246            .write_paths
247            .iter()
248            .any(|allowed| path.starts_with(allowed) || path == allowed);
249
250        if !allowed {
251            self.record_violation(
252                ViolationKind::UnauthorizedWrite,
253                &format!("Write denied: {}", path.display()),
254            );
255        }
256
257        allowed
258    }
259
260    /// Check if environment variable access is allowed
261    pub fn check_env(&mut self, var: &str) -> bool {
262        if self.config.env_vars.is_empty() {
263            self.record_violation(
264                ViolationKind::EnvDenied,
265                &format!("Env access denied: {}", var),
266            );
267            return false;
268        }
269
270        let allowed = self.config.env_vars.contains(var);
271
272        if !allowed {
273            self.record_violation(
274                ViolationKind::EnvDenied,
275                &format!("Env access denied: {}", var),
276            );
277        }
278
279        allowed
280    }
281
282    /// Check if network access is allowed
283    pub fn check_network(&mut self) -> bool {
284        if !self.config.allow_network {
285            self.record_violation(ViolationKind::NetworkDenied, "Network access denied");
286            return false;
287        }
288        true
289    }
290
291    /// Check if process spawning is allowed
292    pub fn check_spawn(&mut self) -> bool {
293        if !self.config.allow_spawn {
294            self.record_violation(ViolationKind::SpawnDenied, "Process spawn denied");
295            return false;
296        }
297        true
298    }
299
300    /// Record a violation
301    fn record_violation(&mut self, kind: ViolationKind, message: &str) {
302        self.violations.push(SandboxViolation {
303            kind,
304            message: message.to_string(),
305            timestamp: std::time::Instant::now(),
306        });
307    }
308
309    /// Get all violations
310    pub fn violations(&self) -> &[SandboxViolation] {
311        &self.violations
312    }
313
314    /// Check if any violations occurred
315    pub fn has_violations(&self) -> bool {
316        !self.violations.is_empty()
317    }
318
319    /// Get memory used
320    pub fn memory_used(&self) -> usize {
321        self.memory_used
322    }
323
324    /// Get elapsed time
325    pub fn elapsed(&self) -> Option<std::time::Duration> {
326        self.started.map(|s| s.elapsed())
327    }
328
329    /// Reset sandbox state
330    pub fn reset(&mut self) {
331        self.started = None;
332        self.memory_used = 0;
333        self.violations.clear();
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_sandbox_config() {
343        let config = SandboxConfig::restricted()
344            .allow_read("/home/user")
345            .allow_write("/tmp")
346            .with_memory_limit(128 * 1024 * 1024);
347
348        assert!(config.read_paths.contains(Path::new("/home/user")));
349        assert!(config.write_paths.contains(Path::new("/tmp")));
350        assert_eq!(config.max_memory, 128 * 1024 * 1024);
351    }
352
353    #[test]
354    fn test_sandbox_file_access() {
355        let config = SandboxConfig::restricted().allow_read("/home/user/project");
356
357        let mut sandbox = Sandbox::new(config);
358        sandbox.start();
359
360        assert!(sandbox.check_read(Path::new("/home/user/project/src/main.rs")));
361        assert!(!sandbox.check_read(Path::new("/etc/passwd")));
362        assert!(sandbox.has_violations());
363    }
364
365    #[test]
366    fn test_sandbox_memory() {
367        let config = SandboxConfig::restricted().with_memory_limit(1024);
368
369        let mut sandbox = Sandbox::new(config);
370        sandbox.start();
371
372        assert!(sandbox.check_memory(512).unwrap());
373        assert!(sandbox.check_memory(512).unwrap());
374        assert!(!sandbox.check_memory(1).unwrap()); // Over limit
375    }
376}