Skip to main content

mocopr_core/
security.rs

1// Security validation and hardening implementation
2// This addresses the failing integration test by implementing actual security checks
3
4use crate::Error;
5use crate::utils::Utils;
6use anyhow::Result;
7use std::fs;
8use std::path::{Path, PathBuf};
9use tracing::warn;
10use url::Url;
11
12/// Comprehensive security validator for MCP operations
13pub struct SecurityValidator {
14    /// Allowed URI schemes
15    pub allowed_schemes: Vec<String>,
16    /// Maximum file size for operations
17    pub max_file_size: u64,
18    /// Allowed file extensions
19    pub allowed_extensions: Vec<String>,
20    /// Root directory for file operations
21    pub root_directory: Option<PathBuf>,
22}
23
24impl Default for SecurityValidator {
25    fn default() -> Self {
26        Self {
27            allowed_schemes: vec!["file".to_string(), "http".to_string(), "https".to_string()],
28            max_file_size: 10 * 1024 * 1024, // 10MB
29            allowed_extensions: vec![
30                "txt".to_string(),
31                "md".to_string(),
32                "json".to_string(),
33                "yml".to_string(),
34                "yaml".to_string(),
35                "xml".to_string(),
36                "csv".to_string(),
37                "log".to_string(),
38            ],
39            root_directory: None,
40        }
41    }
42}
43
44impl SecurityValidator {
45    /// Create a new security validator with custom settings
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set allowed URI schemes
51    pub fn with_allowed_schemes(mut self, schemes: Vec<String>) -> Self {
52        self.allowed_schemes = schemes;
53        self
54    }
55
56    /// Set maximum file size
57    pub fn with_max_file_size(mut self, size: u64) -> Self {
58        self.max_file_size = size;
59        self
60    }
61
62    /// Set allowed file extensions
63    pub fn with_allowed_extensions(mut self, extensions: Vec<String>) -> Self {
64        self.allowed_extensions = extensions;
65        self
66    }
67
68    /// Set root directory for file operations
69    pub fn with_root_directory(mut self, root: PathBuf) -> Self {
70        self.root_directory = Some(root);
71        self
72    }
73
74    /// Validate a URI for security compliance
75    pub fn validate_uri(&self, uri: &Url) -> Result<()> {
76        // Check scheme
77        if !self.allowed_schemes.contains(&uri.scheme().to_string()) {
78            return Err(Error::security(format!(
79                "URI scheme '{}' is not allowed. Allowed schemes: {:?}",
80                uri.scheme(),
81                self.allowed_schemes
82            ))
83            .into());
84        }
85
86        // Additional validation for file URIs
87        if uri.scheme() == "file"
88            && let Ok(path) = uri.to_file_path()
89        {
90            self.validate_file_path(&path)?;
91        }
92
93        Ok(())
94    }
95
96    /// Validate a file path for security compliance
97    pub fn validate_file_path(&self, path: &Path) -> Result<()> {
98        // Sanitize path to prevent directory traversal
99        let sanitized = Utils::sanitize_path(path);
100
101        // Check if path is within allowed root directory
102        if let Some(root) = &self.root_directory {
103            let canonical_root = fs::canonicalize(root).map_err(|e| {
104                Error::security(format!("Failed to canonicalize root directory: {}", e))
105            })?;
106
107            let canonical_path = fs::canonicalize(&sanitized)
108                .map_err(|e| Error::security(format!("Failed to canonicalize file path: {}", e)))?;
109
110            if !canonical_path.starts_with(&canonical_root) {
111                return Err(Error::security(format!(
112                    "Path '{}' is outside of allowed directory '{}'",
113                    canonical_path.display(),
114                    canonical_root.display()
115                ))
116                .into());
117            }
118        }
119
120        // Check file extension
121        if let Some(extension) = path.extension() {
122            let ext_str = extension.to_string_lossy().to_lowercase();
123            if !self.allowed_extensions.contains(&ext_str) {
124                warn!(
125                    "File extension '{}' is not in allowed list: {:?}",
126                    ext_str, self.allowed_extensions
127                );
128                return Err(Error::security(format!(
129                    "File extension '{}' is not allowed. Allowed extensions: {:?}",
130                    ext_str, self.allowed_extensions
131                ))
132                .into());
133            }
134        }
135
136        Ok(())
137    }
138
139    /// Validate a file path from string for security compliance
140    pub fn validate_file_path_str(&self, path: &str) -> Result<()> {
141        self.validate_file_path(Path::new(path))
142    }
143
144    /// Validate a PathBuf for security compliance
145    pub fn validate_file_path_buf(&self, path: &Path) -> Result<()> {
146        self.validate_file_path(path)
147    }
148
149    /// Validate file size
150    pub fn validate_file_size(&self, size: u64) -> Result<()> {
151        Ok(Utils::validate_file_size(size, self.max_file_size)?)
152    }
153
154    /// Validate string input for safety
155    pub fn validate_string_input(&self, input: &str) -> Result<()> {
156        Ok(Utils::validate_safe_string(input)?)
157    }
158
159    /// Comprehensive resource validation
160    pub fn validate_resource_access(&self, uri: &Url) -> Result<()> {
161        // Basic URI validation
162        self.validate_uri(uri)?;
163
164        // For file URIs, perform additional checks
165        if uri.scheme() == "file"
166            && let Ok(path) = uri.to_file_path()
167        {
168            // Check if file exists
169            if !path.exists() {
170                return Err(
171                    Error::not_found(format!("File does not exist: {}", path.display())).into(),
172                );
173            }
174
175            // Check file size
176            if let Ok(metadata) = fs::metadata(&path) {
177                self.validate_file_size(metadata.len())?;
178            } else {
179                return Err(Error::security(format!(
180                    "Cannot read file metadata: {}",
181                    path.display()
182                ))
183                .into());
184            }
185        }
186        Ok(())
187    }
188
189    /// Validate tool parameters
190    pub fn validate_tool_parameters(&self, params: &serde_json::Value) -> Result<()> {
191        // Recursively validate all string values in the parameter object
192        match params {
193            serde_json::Value::String(s) => {
194                self.validate_string_input(s)?;
195            }
196            serde_json::Value::Object(obj) => {
197                for (key, value) in obj {
198                    self.validate_string_input(key)?;
199                    self.validate_tool_parameters(value)?;
200                }
201            }
202            serde_json::Value::Array(arr) => {
203                for value in arr {
204                    self.validate_tool_parameters(value)?;
205                }
206            }
207            _ => {} // Numbers, booleans, null are safe
208        }
209
210        Ok(())
211    }
212}
213
214/// Error recovery and resilience system
215pub struct ErrorRecoverySystem {
216    /// Maximum retry attempts
217    pub max_retries: u32,
218    /// Retry delay in milliseconds
219    pub retry_delay_ms: u64,
220    /// Whether to log errors
221    pub log_errors: bool,
222}
223
224impl Default for ErrorRecoverySystem {
225    fn default() -> Self {
226        Self {
227            max_retries: 3,
228            retry_delay_ms: 1000,
229            log_errors: true,
230        }
231    }
232}
233
234impl ErrorRecoverySystem {
235    /// Create a new error recovery system
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    /// Execute an operation with retry logic
241    pub async fn execute_with_retry<F, T, E>(&self, mut operation: F) -> Result<T>
242    where
243        F: FnMut() -> Result<T, E> + Send + Sync,
244        E: std::error::Error + Send + Sync + 'static,
245        T: Send + Sync,
246    {
247        let mut attempts = 0;
248
249        loop {
250            match operation() {
251                Ok(result) => return Ok(result),
252                Err(e) => {
253                    attempts += 1;
254
255                    if self.log_errors {
256                        warn!(
257                            "Operation failed (attempt {}/{}): {}",
258                            attempts, self.max_retries, e
259                        );
260                    }
261
262                    if attempts >= self.max_retries {
263                        return Err(Error::operation_failed(format!(
264                            "Operation failed after {} attempts: {}",
265                            self.max_retries, e
266                        ))
267                        .into());
268                    }
269
270                    // Wait before retry
271                    tokio::time::sleep(tokio::time::Duration::from_millis(self.retry_delay_ms))
272                        .await;
273                }
274            }
275        }
276    }
277
278    /// Handle invalid method calls gracefully
279    pub fn handle_invalid_method(&self, method: &str) -> Error {
280        if self.log_errors {
281            warn!("Invalid method called: {}", method);
282        }
283
284        Error::method_not_found(format!(
285            "Method '{}' is not supported. Available methods should be checked through capability negotiation.",
286            method
287        ))
288    }
289
290    /// Handle invalid parameters gracefully
291    pub fn handle_invalid_parameters(&self, method: &str, error: &str) -> Error {
292        if self.log_errors {
293            warn!("Invalid parameters for method '{}': {}", method, error);
294        }
295
296        Error::invalid_params(format!(
297            "Invalid parameters for method '{}': {}. Please check the method signature and required parameters.",
298            method, error
299        ))
300    }
301
302    /// Handle resource access errors gracefully
303    pub fn handle_resource_error(&self, uri: &str, error: &str) -> Error {
304        if self.log_errors {
305            warn!("Resource access error for '{}': {}", uri, error);
306        }
307
308        Error::resource_error(format!(
309            "Failed to access resource '{}': {}. Please check the resource exists and is accessible.",
310            uri, error
311        ))
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use std::fs;
319    use tempfile::TempDir;
320
321    #[test]
322    fn test_security_validator_path_traversal() {
323        let temp_dir = TempDir::new().unwrap();
324        let validator = SecurityValidator::new().with_root_directory(temp_dir.path().to_path_buf());
325
326        // Create a test file within the allowed directory
327        let allowed_file = temp_dir.path().join("allowed.txt");
328        fs::write(&allowed_file, "test content").unwrap();
329
330        // Test allowed access
331        assert!(validator.validate_file_path(&allowed_file).is_ok());
332
333        // Test path traversal attempts
334        let traversal_attempts = vec![
335            temp_dir.path().join("../../../etc/passwd"),
336            temp_dir.path().join("../../sensitive.txt"),
337            temp_dir.path().join("../outside/file.txt"),
338        ];
339
340        for malicious_path in traversal_attempts {
341            assert!(validator.validate_file_path(&malicious_path).is_err());
342        }
343    }
344
345    #[test]
346    fn test_security_validator_file_extensions() {
347        let validator = SecurityValidator::new()
348            .with_allowed_extensions(vec!["txt".to_string(), "md".to_string()]);
349
350        // Test allowed extensions
351        assert!(validator.validate_file_path(Path::new("test.txt")).is_ok());
352        assert!(validator.validate_file_path(Path::new("test.md")).is_ok());
353
354        // Test disallowed extensions
355        assert!(validator.validate_file_path(Path::new("test.exe")).is_err());
356        assert!(validator.validate_file_path(Path::new("test.sh")).is_err());
357    }
358
359    #[tokio::test]
360    async fn test_error_recovery_system() {
361        let recovery = ErrorRecoverySystem::new();
362
363        // Test successful operation
364        let result = recovery
365            .execute_with_retry(|| -> Result<i32, std::io::Error> { Ok(42) })
366            .await;
367
368        assert!(result.is_ok());
369        assert_eq!(result.unwrap(), 42);
370
371        // Test operation that fails then succeeds
372        let attempts = std::sync::Arc::new(std::sync::Mutex::new(0));
373        let attempts_clone = attempts.clone();
374        let result = recovery
375            .execute_with_retry(move || -> Result<i32, std::io::Error> {
376                let mut attempts_ref = attempts_clone.lock().unwrap();
377                *attempts_ref += 1;
378                if *attempts_ref < 3 {
379                    Err(std::io::Error::other("temporary failure"))
380                } else {
381                    Ok(42)
382                }
383            })
384            .await;
385
386        assert!(result.is_ok());
387        assert_eq!(result.unwrap(), 42);
388    }
389}