1use serde::{Deserialize, Serialize};
4
5use crate::error::{ConfigError, SandboxError};
6
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9#[serde(rename_all = "camelCase")]
10pub struct MitmProxyConfig {
11 pub socket_path: String,
13 pub domains: Vec<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
19#[serde(rename_all = "camelCase")]
20pub struct NetworkConfig {
21 #[serde(default)]
23 pub allowed_domains: Vec<String>,
24
25 #[serde(default)]
27 pub denied_domains: Vec<String>,
28
29 #[serde(default)]
32 pub allow_unix_sockets: Option<Vec<String>>,
33
34 #[serde(default)]
38 pub allow_all_unix_sockets: Option<bool>,
39
40 #[serde(default)]
42 pub allow_local_binding: Option<bool>,
43
44 #[serde(default)]
46 pub http_proxy_port: Option<u16>,
47
48 #[serde(default)]
50 pub socks_proxy_port: Option<u16>,
51
52 #[serde(default)]
54 pub mitm_proxy: Option<MitmProxyConfig>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, Default)]
59#[serde(rename_all = "camelCase")]
60pub struct FilesystemConfig {
61 #[serde(default)]
63 pub deny_read: Vec<String>,
64
65 #[serde(default)]
67 pub allow_write: Vec<String>,
68
69 #[serde(default)]
71 pub deny_write: Vec<String>,
72
73 #[serde(default)]
75 pub allow_git_config: Option<bool>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct RipgrepConfig {
82 pub command: String,
84 #[serde(default)]
86 pub args: Option<Vec<String>>,
87}
88
89impl Default for RipgrepConfig {
90 fn default() -> Self {
91 Self {
92 command: "rg".to_string(),
93 args: None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, Default)]
100#[serde(rename_all = "camelCase")]
101pub struct SeccompConfig {
102 pub bpf_path: Option<String>,
104 pub apply_path: Option<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110#[serde(rename_all = "camelCase")]
111pub struct SandboxRuntimeConfig {
112 #[serde(default)]
114 pub network: NetworkConfig,
115
116 #[serde(default)]
118 pub filesystem: FilesystemConfig,
119
120 #[serde(default)]
122 pub ignore_violations: Option<std::collections::HashMap<String, Vec<String>>>,
123
124 #[serde(default)]
126 pub enable_weaker_nested_sandbox: Option<bool>,
127
128 #[serde(default)]
130 pub ripgrep: Option<RipgrepConfig>,
131
132 #[serde(default)]
134 pub mandatory_deny_search_depth: Option<u32>,
135
136 #[serde(default)]
138 pub allow_pty: Option<bool>,
139
140 #[serde(default)]
142 pub seccomp: Option<SeccompConfig>,
143}
144
145pub const DANGEROUS_FILES: &[&str] = &[
147 ".gitconfig",
148 ".bashrc",
149 ".bash_profile",
150 ".bash_login",
151 ".profile",
152 ".zshrc",
153 ".zprofile",
154 ".zshenv",
155 ".zlogin",
156 ".mcp.json",
157 ".mcp-settings.json",
158 ".npmrc",
159 ".yarnrc",
160 ".yarnrc.yml",
161];
162
163pub const DANGEROUS_DIRECTORIES: &[&str] = &[
165 ".git/hooks",
166 ".git",
167 ".vscode",
168 ".idea",
169 ".claude/commands",
170];
171
172impl SandboxRuntimeConfig {
173 pub fn validate(&self) -> Result<(), SandboxError> {
175 for domain in &self.network.allowed_domains {
177 validate_domain_pattern(domain)?;
178 }
179
180 for domain in &self.network.denied_domains {
182 validate_domain_pattern(domain)?;
183 }
184
185 if let Some(ref mitm) = self.network.mitm_proxy {
187 for domain in &mitm.domains {
188 validate_domain_pattern(domain)?;
189 }
190 }
191
192 Ok(())
193 }
194}
195
196fn validate_domain_pattern(pattern: &str) -> Result<(), SandboxError> {
198 if pattern.is_empty() {
200 return Err(ConfigError::InvalidDomainPattern {
201 pattern: pattern.to_string(),
202 reason: "domain pattern cannot be empty".to_string(),
203 }
204 .into());
205 }
206
207 if pattern == "*" {
209 return Err(ConfigError::InvalidDomainPattern {
210 pattern: pattern.to_string(),
211 reason: "wildcard-only patterns are not allowed".to_string(),
212 }
213 .into());
214 }
215
216 if pattern.starts_with("*.") {
218 let suffix = &pattern[2..];
219 if !suffix.contains('.') && suffix.len() <= 4 {
221 return Err(ConfigError::InvalidDomainPattern {
222 pattern: pattern.to_string(),
223 reason: "pattern is too broad (matches entire TLD)".to_string(),
224 }
225 .into());
226 }
227 }
228
229 if pattern.contains(':') {
231 return Err(ConfigError::InvalidDomainPattern {
232 pattern: pattern.to_string(),
233 reason: "domain patterns cannot include port numbers".to_string(),
234 }
235 .into());
236 }
237
238 let check_part = if pattern.starts_with("*.") {
240 &pattern[2..]
241 } else {
242 pattern
243 };
244
245 for ch in check_part.chars() {
246 if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '-' && ch != '_' {
247 return Err(ConfigError::InvalidDomainPattern {
248 pattern: pattern.to_string(),
249 reason: format!("invalid character '{}' in domain pattern", ch),
250 }
251 .into());
252 }
253 }
254
255 Ok(())
256}
257
258pub fn matches_domain_pattern(hostname: &str, pattern: &str) -> bool {
260 let hostname_lower = hostname.to_lowercase();
261 let pattern_lower = pattern.to_lowercase();
262
263 if pattern_lower.starts_with("*.") {
264 let base_domain = &pattern_lower[2..];
266 hostname_lower.ends_with(&format!(".{}", base_domain))
267 } else {
268 hostname_lower == pattern_lower
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_domain_pattern_matching() {
279 assert!(matches_domain_pattern("example.com", "example.com"));
281 assert!(matches_domain_pattern("EXAMPLE.COM", "example.com"));
282 assert!(!matches_domain_pattern("api.example.com", "example.com"));
283
284 assert!(matches_domain_pattern("api.example.com", "*.example.com"));
286 assert!(matches_domain_pattern("deep.api.example.com", "*.example.com"));
287 assert!(!matches_domain_pattern("example.com", "*.example.com"));
288
289 assert!(matches_domain_pattern("API.EXAMPLE.COM", "*.example.com"));
291 }
292
293 #[test]
294 fn test_domain_pattern_validation() {
295 assert!(validate_domain_pattern("example.com").is_ok());
297 assert!(validate_domain_pattern("*.example.com").is_ok());
298 assert!(validate_domain_pattern("localhost").is_ok());
299 assert!(validate_domain_pattern("api.github.com").is_ok());
300
301 assert!(validate_domain_pattern("").is_err());
303 assert!(validate_domain_pattern("*").is_err());
304 assert!(validate_domain_pattern("*.com").is_err());
305 assert!(validate_domain_pattern("example.com:8080").is_err());
306 }
307}