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    pub fn permissive() -> Self {
52        Self {
53            fs: SecureFs::permissive(),
54            bash: bash::BashAnalyzer::new(bash::BashPolicy::default()),
55            limits: ResourceLimits::none(),
56            policy: SecurityPolicy::permissive(),
57            network: Arc::new(NetworkSandbox::permissive()),
58            sandbox: Arc::new(Sandbox::disabled()),
59        }
60    }
61
62    pub fn root(&self) -> &Path {
63        self.fs.root()
64    }
65
66    pub fn is_sandboxed(&self) -> bool {
67        self.sandbox.is_enabled()
68    }
69
70    pub fn should_auto_allow_bash(&self) -> bool {
71        self.sandbox.should_auto_allow_bash()
72    }
73}
74
75#[derive(Default)]
76pub struct SecurityContextBuilder {
77    root: Option<PathBuf>,
78    allowed_paths: Vec<PathBuf>,
79    denied_patterns: Vec<String>,
80    limits: Option<ResourceLimits>,
81    bash_policy: Option<bash::BashPolicy>,
82    max_symlink_depth: Option<u8>,
83    network: Option<NetworkSandbox>,
84    sandbox_config: Option<SandboxConfig>,
85}
86
87impl SecurityContextBuilder {
88    pub fn root(mut self, path: impl AsRef<Path>) -> Self {
89        self.root = Some(path.as_ref().to_path_buf());
90        self
91    }
92
93    pub fn allowed_paths(mut self, paths: Vec<PathBuf>) -> Self {
94        self.allowed_paths = paths;
95        self
96    }
97
98    pub fn denied_patterns(mut self, patterns: Vec<String>) -> Self {
99        self.denied_patterns = patterns;
100        self
101    }
102
103    pub fn limits(mut self, limits: ResourceLimits) -> Self {
104        self.limits = Some(limits);
105        self
106    }
107
108    pub fn bash_policy(mut self, policy: bash::BashPolicy) -> Self {
109        self.bash_policy = Some(policy);
110        self
111    }
112
113    pub fn max_symlink_depth(mut self, depth: u8) -> Self {
114        self.max_symlink_depth = Some(depth);
115        self
116    }
117
118    pub fn network(mut self, sandbox: NetworkSandbox) -> Self {
119        self.network = Some(sandbox);
120        self
121    }
122
123    pub fn sandbox(mut self, config: SandboxConfig) -> Self {
124        self.sandbox_config = Some(config);
125        self
126    }
127
128    pub fn sandbox_enabled(mut self, enabled: bool) -> Self {
129        let root = self
130            .root
131            .clone()
132            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
133        self.sandbox_config = Some(if enabled {
134            SandboxConfig::new(root)
135        } else {
136            SandboxConfig::disabled()
137        });
138        self
139    }
140
141    pub fn auto_allow_bash_if_sandboxed(mut self, auto_allow: bool) -> Self {
142        let root = self
143            .root
144            .clone()
145            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
146        let config = self
147            .sandbox_config
148            .take()
149            .unwrap_or_else(|| SandboxConfig::new(root));
150        self.sandbox_config = Some(config.with_auto_allow_bash(auto_allow));
151        self
152    }
153
154    pub fn build(self) -> Result<SecurityContext, SecurityError> {
155        let root = self
156            .root
157            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
158
159        let fs = SecureFs::new(
160            root.clone(),
161            self.allowed_paths.clone(),
162            self.denied_patterns.clone(),
163            self.max_symlink_depth.unwrap_or(10),
164        )?;
165
166        let sandbox_config = self.sandbox_config.unwrap_or_else(|| {
167            SandboxConfig::disabled()
168                .with_working_dir(root)
169                .with_allowed_paths(self.allowed_paths)
170                .with_denied_paths(self.denied_patterns)
171        });
172
173        let network = self
174            .network
175            .unwrap_or_else(|| sandbox_config.to_network_sandbox());
176
177        Ok(SecurityContext {
178            fs,
179            bash: bash::BashAnalyzer::new(self.bash_policy.unwrap_or_default()),
180            limits: self.limits.unwrap_or_default(),
181            policy: SecurityPolicy::default(),
182            network: Arc::new(network),
183            sandbox: Arc::new(Sandbox::new(sandbox_config)),
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use tempfile::tempdir;
192
193    #[test]
194    fn test_security_context_new() {
195        let dir = tempdir().unwrap();
196        let security = SecurityContext::new(dir.path()).unwrap();
197        assert_eq!(security.root(), std::fs::canonicalize(dir.path()).unwrap());
198    }
199
200    #[test]
201    fn test_security_context_permissive() {
202        let security = SecurityContext::permissive();
203        assert_eq!(security.root(), Path::new("/"));
204    }
205
206    #[test]
207    fn test_builder() {
208        let dir = tempdir().unwrap();
209        let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
210        let security = SecurityContext::builder()
211            .root(&canonical_dir)
212            .max_symlink_depth(5)
213            .limits(ResourceLimits::default())
214            .build()
215            .unwrap();
216
217        assert_eq!(security.root(), canonical_dir);
218    }
219}