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)]
31 pub allow_unix_sockets: Option<Vec<String>>,
32
33 #[serde(default)]
35 pub allow_all_unix_sockets: Option<bool>,
36
37 #[serde(default)]
39 pub allow_local_binding: Option<bool>,
40
41 #[serde(default)]
43 pub http_proxy_port: Option<u16>,
44
45 #[serde(default)]
47 pub socks_proxy_port: Option<u16>,
48
49 #[serde(default)]
51 pub mitm_proxy: Option<MitmProxyConfig>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56#[serde(rename_all = "camelCase")]
57pub struct FilesystemConfig {
58 #[serde(default)]
60 pub deny_read: Vec<String>,
61
62 #[serde(default)]
64 pub allow_write: Vec<String>,
65
66 #[serde(default)]
68 pub deny_write: Vec<String>,
69
70 #[serde(default)]
72 pub allow_git_config: Option<bool>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct RipgrepConfig {
79 pub command: String,
81 #[serde(default)]
83 pub args: Option<Vec<String>>,
84}
85
86impl Default for RipgrepConfig {
87 fn default() -> Self {
88 Self {
89 command: "rg".to_string(),
90 args: None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97#[serde(rename_all = "camelCase")]
98pub struct SeccompConfig {
99 pub bpf_path: Option<String>,
101 pub apply_path: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
107#[serde(rename_all = "camelCase")]
108pub struct SandboxRuntimeConfig {
109 #[serde(default)]
111 pub network: NetworkConfig,
112
113 #[serde(default)]
115 pub filesystem: FilesystemConfig,
116
117 #[serde(default)]
119 pub ignore_violations: Option<std::collections::HashMap<String, Vec<String>>>,
120
121 #[serde(default)]
123 pub enable_weaker_nested_sandbox: Option<bool>,
124
125 #[serde(default)]
127 pub ripgrep: Option<RipgrepConfig>,
128
129 #[serde(default)]
131 pub mandatory_deny_search_depth: Option<u32>,
132
133 #[serde(default)]
135 pub allow_pty: Option<bool>,
136
137 #[serde(default)]
139 pub seccomp: Option<SeccompConfig>,
140}
141
142pub const DANGEROUS_FILES: &[&str] = &[
144 ".gitconfig",
145 ".bashrc",
146 ".bash_profile",
147 ".bash_login",
148 ".profile",
149 ".zshrc",
150 ".zprofile",
151 ".zshenv",
152 ".zlogin",
153 ".mcp.json",
154 ".mcp-settings.json",
155 ".npmrc",
156 ".yarnrc",
157 ".yarnrc.yml",
158];
159
160pub const DANGEROUS_DIRECTORIES: &[&str] = &[
162 ".git/hooks",
163 ".git",
164 ".vscode",
165 ".idea",
166 ".claude/commands",
167];
168
169impl SandboxRuntimeConfig {
170 pub fn validate(&self) -> Result<(), SandboxError> {
172 for domain in &self.network.allowed_domains {
174 validate_domain_pattern(domain)?;
175 }
176
177 for domain in &self.network.denied_domains {
179 validate_domain_pattern(domain)?;
180 }
181
182 if let Some(ref mitm) = self.network.mitm_proxy {
184 for domain in &mitm.domains {
185 validate_domain_pattern(domain)?;
186 }
187 }
188
189 Ok(())
190 }
191}
192
193fn validate_domain_pattern(pattern: &str) -> Result<(), SandboxError> {
195 if pattern.is_empty() {
197 return Err(ConfigError::InvalidDomainPattern {
198 pattern: pattern.to_string(),
199 reason: "domain pattern cannot be empty".to_string(),
200 }
201 .into());
202 }
203
204 if pattern == "*" {
206 return Err(ConfigError::InvalidDomainPattern {
207 pattern: pattern.to_string(),
208 reason: "wildcard-only patterns are not allowed".to_string(),
209 }
210 .into());
211 }
212
213 if pattern.starts_with("*.") {
215 let suffix = &pattern[2..];
216 if !suffix.contains('.') && suffix.len() <= 4 {
218 return Err(ConfigError::InvalidDomainPattern {
219 pattern: pattern.to_string(),
220 reason: "pattern is too broad (matches entire TLD)".to_string(),
221 }
222 .into());
223 }
224 }
225
226 if pattern.contains(':') {
228 return Err(ConfigError::InvalidDomainPattern {
229 pattern: pattern.to_string(),
230 reason: "domain patterns cannot include port numbers".to_string(),
231 }
232 .into());
233 }
234
235 let check_part = if pattern.starts_with("*.") {
237 &pattern[2..]
238 } else {
239 pattern
240 };
241
242 for ch in check_part.chars() {
243 if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '-' && ch != '_' {
244 return Err(ConfigError::InvalidDomainPattern {
245 pattern: pattern.to_string(),
246 reason: format!("invalid character '{}' in domain pattern", ch),
247 }
248 .into());
249 }
250 }
251
252 Ok(())
253}
254
255pub fn matches_domain_pattern(hostname: &str, pattern: &str) -> bool {
257 let hostname_lower = hostname.to_lowercase();
258 let pattern_lower = pattern.to_lowercase();
259
260 if pattern_lower.starts_with("*.") {
261 let base_domain = &pattern_lower[2..];
263 hostname_lower.ends_with(&format!(".{}", base_domain))
264 } else {
265 hostname_lower == pattern_lower
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_domain_pattern_matching() {
276 assert!(matches_domain_pattern("example.com", "example.com"));
278 assert!(matches_domain_pattern("EXAMPLE.COM", "example.com"));
279 assert!(!matches_domain_pattern("api.example.com", "example.com"));
280
281 assert!(matches_domain_pattern("api.example.com", "*.example.com"));
283 assert!(matches_domain_pattern("deep.api.example.com", "*.example.com"));
284 assert!(!matches_domain_pattern("example.com", "*.example.com"));
285
286 assert!(matches_domain_pattern("API.EXAMPLE.COM", "*.example.com"));
288 }
289
290 #[test]
291 fn test_domain_pattern_validation() {
292 assert!(validate_domain_pattern("example.com").is_ok());
294 assert!(validate_domain_pattern("*.example.com").is_ok());
295 assert!(validate_domain_pattern("localhost").is_ok());
296 assert!(validate_domain_pattern("api.github.com").is_ok());
297
298 assert!(validate_domain_pattern("").is_err());
300 assert!(validate_domain_pattern("*").is_err());
301 assert!(validate_domain_pattern("*.com").is_err());
302 assert!(validate_domain_pattern("example.com:8080").is_err());
303 }
304}