Skip to main content

claude_agent/security/
mod.rs

1//! Security sandbox system providing TOCTOU-safe file operations and process isolation.
2//!
3//! This module provides comprehensive security controls:
4//! - TOCTOU-safe path resolution using `openat()` with `O_NOFOLLOW`
5//! - Symlink attack prevention with depth limiting
6//! - AST-based bash command analysis
7//! - Environment variable sanitization
8//! - Process resource limits via `setrlimit`
9//! - OS-level sandboxing (Landlock on Linux, Seatbelt on macOS)
10
11pub mod bash;
12pub mod fs;
13pub mod guard;
14pub mod limits;
15pub mod path;
16pub mod policy;
17pub mod sandbox;
18
19mod error;
20
21pub use error::SecurityError;
22pub use fs::{SecureFileHandle, SecureFs};
23pub use guard::SecurityGuard;
24pub use limits::ResourceLimits;
25pub use path::SafePath;
26pub use policy::SecurityPolicy;
27pub use sandbox::{DomainCheck, NetworkConfig, NetworkSandbox, Sandbox, SandboxConfig};
28
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31
32#[derive(Clone)]
33pub struct SecurityContext {
34    pub fs: SecureFs,
35    pub bash: bash::BashAnalyzer,
36    pub limits: ResourceLimits,
37    pub policy: SecurityPolicy,
38    pub network: Arc<NetworkSandbox>,
39    pub sandbox: Arc<Sandbox>,
40}
41
42impl SecurityContext {
43    pub fn new(root: impl AsRef<Path>) -> Result<Self, SecurityError> {
44        Self::builder().root(root).build()
45    }
46
47    pub fn builder() -> SecurityContextBuilder {
48        SecurityContextBuilder::default()
49    }
50
51    /// Create a permissive SecurityContext that allows all operations.
52    ///
53    /// # Panics
54    /// Panics if the root filesystem cannot be accessed.
55    pub fn permissive() -> Self {
56        Self {
57            fs: SecureFs::permissive(),
58            bash: bash::BashAnalyzer::new(bash::BashPolicy::default()),
59            limits: ResourceLimits::none(),
60            policy: SecurityPolicy::permissive(),
61            network: Arc::new(NetworkSandbox::permissive()),
62            sandbox: Arc::new(Sandbox::disabled()),
63        }
64    }
65
66    pub fn root(&self) -> &Path {
67        self.fs.root()
68    }
69
70    pub fn is_sandboxed(&self) -> bool {
71        self.sandbox.is_enabled()
72    }
73
74    pub fn should_auto_allow_bash(&self) -> bool {
75        self.sandbox.should_auto_allow_bash()
76    }
77}
78
79#[derive(Default)]
80pub struct SecurityContextBuilder {
81    root: Option<PathBuf>,
82    allowed_paths: Vec<PathBuf>,
83    denied_patterns: Vec<String>,
84    limits: Option<ResourceLimits>,
85    bash_policy: Option<bash::BashPolicy>,
86    max_symlink_depth: Option<u8>,
87    network: Option<NetworkSandbox>,
88    sandbox_config: Option<SandboxConfig>,
89}
90
91impl SecurityContextBuilder {
92    pub fn root(mut self, path: impl AsRef<Path>) -> Self {
93        self.root = Some(path.as_ref().to_path_buf());
94        self
95    }
96
97    pub fn allowed_paths(mut self, paths: Vec<PathBuf>) -> Self {
98        self.allowed_paths = paths;
99        self
100    }
101
102    pub fn denied_patterns(mut self, patterns: Vec<String>) -> Self {
103        self.denied_patterns = patterns;
104        self
105    }
106
107    pub fn limits(mut self, limits: ResourceLimits) -> Self {
108        self.limits = Some(limits);
109        self
110    }
111
112    pub fn bash_policy(mut self, policy: bash::BashPolicy) -> Self {
113        self.bash_policy = Some(policy);
114        self
115    }
116
117    pub fn max_symlink_depth(mut self, depth: u8) -> Self {
118        self.max_symlink_depth = Some(depth);
119        self
120    }
121
122    pub fn network(mut self, sandbox: NetworkSandbox) -> Self {
123        self.network = Some(sandbox);
124        self
125    }
126
127    pub fn sandbox(mut self, config: SandboxConfig) -> Self {
128        self.sandbox_config = Some(config);
129        self
130    }
131
132    pub fn sandbox_enabled(mut self, enabled: bool) -> Self {
133        let root = self
134            .root
135            .clone()
136            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
137        self.sandbox_config = Some(if enabled {
138            SandboxConfig::new(root)
139        } else {
140            SandboxConfig::disabled()
141        });
142        self
143    }
144
145    pub fn auto_allow_bash_if_sandboxed(mut self, auto_allow: bool) -> Self {
146        let root = self
147            .root
148            .clone()
149            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
150        let config = self
151            .sandbox_config
152            .take()
153            .unwrap_or_else(|| SandboxConfig::new(root));
154        self.sandbox_config = Some(config.with_auto_allow_bash(auto_allow));
155        self
156    }
157
158    pub fn build(self) -> Result<SecurityContext, SecurityError> {
159        let root = self
160            .root
161            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
162
163        let fs = SecureFs::new(
164            &root,
165            self.allowed_paths.clone(),
166            &self.denied_patterns,
167            self.max_symlink_depth
168                .unwrap_or(crate::security::path::DEFAULT_MAX_SYMLINK_DEPTH),
169        )?;
170
171        let sandbox_config = self.sandbox_config.unwrap_or_else(|| {
172            SandboxConfig::disabled()
173                .with_working_dir(root)
174                .with_allowed_paths(self.allowed_paths)
175                .with_denied_paths(self.denied_patterns)
176        });
177
178        let network = self
179            .network
180            .unwrap_or_else(|| sandbox_config.to_network_sandbox());
181
182        Ok(SecurityContext {
183            fs,
184            bash: bash::BashAnalyzer::new(self.bash_policy.unwrap_or_default()),
185            limits: self.limits.unwrap_or_default(),
186            policy: SecurityPolicy::default(),
187            network: Arc::new(network),
188            sandbox: Arc::new(Sandbox::new(sandbox_config)),
189        })
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use tempfile::tempdir;
197
198    #[test]
199    fn test_security_context_new() {
200        let dir = tempdir().unwrap();
201        let security = SecurityContext::new(dir.path()).unwrap();
202        assert_eq!(security.root(), std::fs::canonicalize(dir.path()).unwrap());
203    }
204
205    #[test]
206    fn test_security_context_permissive() {
207        let security = SecurityContext::permissive();
208        assert_eq!(security.root(), Path::new("/"));
209    }
210
211    #[test]
212    fn test_builder() {
213        let dir = tempdir().unwrap();
214        let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
215        let security = SecurityContext::builder()
216            .root(&canonical_dir)
217            .max_symlink_depth(5)
218            .limits(ResourceLimits::default())
219            .build()
220            .unwrap();
221
222        assert_eq!(security.root(), canonical_dir);
223    }
224}