ddex_builder/
api_security.rs

1//! API Security Features for DDEX Builder
2//! 
3//! This module provides comprehensive security features for API boundaries,
4//! including FFI validation, WASM security headers, and batch operation protection.
5
6use crate::error::BuildError;
7use crate::security::{SecurityConfig, RateLimiter, OutputSanitizer};
8use indexmap::{IndexMap, IndexSet};
9use std::time::{Duration, Instant};
10
11/// API security manager for coordinating security features
12#[derive(Debug)]
13pub struct ApiSecurityManager {
14    rate_limiter: RateLimiter,
15    output_sanitizer: OutputSanitizer,
16    batch_monitor: BatchOperationMonitor,
17    ffi_validator: FfiValidator,
18    config: SecurityConfig,
19}
20
21impl ApiSecurityManager {
22    /// Create new API security manager
23    pub fn new(config: SecurityConfig) -> Self {
24        Self {
25            rate_limiter: RateLimiter::new(config.clone()),
26            output_sanitizer: OutputSanitizer::new(config.clone()),
27            batch_monitor: BatchOperationMonitor::new(config.clone()),
28            ffi_validator: FfiValidator::new(config.clone()),
29            config,
30        }
31    }
32    
33    /// Validate API request before processing
34    pub fn validate_request(&mut self, operation: &str, identifier: &str, payload_size: usize) -> Result<(), BuildError> {
35        // Check rate limits
36        self.rate_limiter.check_rate_limit(identifier)?;
37        
38        // Validate payload size
39        if payload_size > self.config.max_xml_size {
40            return Err(BuildError::Security(
41                format!("Payload too large: {} bytes", payload_size)
42            ));
43        }
44        
45        // Track batch operations
46        self.batch_monitor.track_operation(identifier, operation)?;
47        
48        Ok(())
49    }
50    
51    /// Sanitize API response before returning
52    pub fn sanitize_response(&self, response: &str) -> Result<String, BuildError> {
53        self.output_sanitizer.sanitize_xml_output(response)
54    }
55    
56    /// Validate FFI boundary data
57    pub fn validate_ffi_input(&self, data: &[u8], expected_type: FfiDataType) -> Result<(), BuildError> {
58        self.ffi_validator.validate_input(data, expected_type)
59    }
60    
61    /// Get security headers for WASM builds
62    pub fn get_wasm_security_headers(&self) -> IndexMap<String, String> {
63        let mut headers = IndexMap::new();
64        
65        // Content Security Policy
66        headers.insert(
67            "Content-Security-Policy".to_string(),
68            "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'".to_string(),
69        );
70        
71        // Prevent MIME type sniffing
72        headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
73        
74        // Frame options
75        headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
76        
77        // XSS Protection
78        headers.insert("X-XSS-Protection".to_string(), "1; mode=block".to_string());
79        
80        // Referrer policy
81        headers.insert("Referrer-Policy".to_string(), "strict-origin-when-cross-origin".to_string());
82        
83        // Permissions policy
84        headers.insert(
85            "Permissions-Policy".to_string(),
86            "camera=(), microphone=(), location=(), interest-cohort=()".to_string(),
87        );
88        
89        headers
90    }
91    
92    /// Create secure error response (without internal details)
93    pub fn create_secure_error_response(&self, error: &BuildError, request_id: &str) -> String {
94        let sanitized_message = match error {
95            BuildError::Security(_) => "Security validation failed",
96            BuildError::InvalidFormat { .. } => "Invalid input format",
97            BuildError::Validation(..) => "Validation error",
98            BuildError::Io(_) => "I/O operation failed",
99            _ => "Internal error occurred",
100        };
101        
102        let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
103        
104        format!(
105            r#"{{"error": "{}", "request_id": "{}", "timestamp": "{}"}}"#,
106            sanitized_message, request_id, timestamp
107        )
108    }
109}
110
111/// Monitor batch operations to prevent resource exhaustion
112#[derive(Debug)]
113pub struct BatchOperationMonitor {
114    operations: IndexMap<String, Vec<OperationRecord>>,
115    config: SecurityConfig,
116}
117
118#[derive(Debug, Clone)]
119struct OperationRecord {
120    operation: String,
121    timestamp: Instant,
122    resource_usage: usize,
123}
124
125impl BatchOperationMonitor {
126    /// Create new batch operation monitor
127    pub fn new(config: SecurityConfig) -> Self {
128        Self {
129            operations: IndexMap::new(),
130            config,
131        }
132    }
133    
134    /// Track a batch operation
135    pub fn track_operation(&mut self, identifier: &str, operation: &str) -> Result<(), BuildError> {
136        let now = Instant::now();
137        let records = self.operations.entry(identifier.to_string()).or_default();
138        
139        // Clean up old records
140        records.retain(|record| now.duration_since(record.timestamp) <= Duration::from_secs(60));
141        
142        // Check batch limits
143        if records.len() >= self.config.max_requests_per_minute as usize {
144            return Err(BuildError::Security(
145                "Batch operation limit exceeded".to_string()
146            ));
147        }
148        
149        // Add new record
150        records.push(OperationRecord {
151            operation: operation.to_string(),
152            timestamp: now,
153            resource_usage: 1, // Could be made more sophisticated
154        });
155        
156        Ok(())
157    }
158    
159    /// Get operation statistics
160    pub fn get_stats(&self, identifier: &str) -> Option<BatchStats> {
161        let records = self.operations.get(identifier)?;
162        let now = Instant::now();
163        
164        let recent_records: Vec<_> = records
165            .iter()
166            .filter(|r| now.duration_since(r.timestamp) <= Duration::from_secs(60))
167            .collect();
168        
169        Some(BatchStats {
170            total_operations: recent_records.len(),
171            unique_operations: recent_records
172                .iter()
173                .map(|r| r.operation.as_str())
174                .collect::<IndexSet<_>>()
175                .len(),
176            time_window_seconds: 60,
177        })
178    }
179}
180
181/// Statistics for batch operations
182#[derive(Debug, Clone)]
183pub struct BatchStats {
184    pub total_operations: usize,
185    pub unique_operations: usize,
186    pub time_window_seconds: u64,
187}
188
189/// FFI boundary validator
190#[derive(Debug)]
191pub struct FfiValidator {
192    config: SecurityConfig,
193}
194
195/// Expected data types for FFI validation
196#[derive(Debug, Clone, Copy)]
197pub enum FfiDataType {
198    Xml,
199    Json,
200    Binary,
201    Utf8String,
202}
203
204impl FfiValidator {
205    /// Create new FFI validator
206    pub fn new(config: SecurityConfig) -> Self {
207        Self { config }
208    }
209    
210    /// Validate FFI input data
211    pub fn validate_input(&self, data: &[u8], expected_type: FfiDataType) -> Result<(), BuildError> {
212        // Check size limits
213        if data.len() > self.config.max_xml_size {
214            return Err(BuildError::Security(
215                format!("FFI input too large: {} bytes", data.len())
216            ));
217        }
218        
219        // Validate data format
220        match expected_type {
221            FfiDataType::Utf8String => {
222                std::str::from_utf8(data).map_err(|_| {
223                    BuildError::Security("Invalid UTF-8 in FFI input".to_string())
224                })?;
225            }
226            FfiDataType::Xml => {
227                let xml_str = std::str::from_utf8(data).map_err(|_| {
228                    BuildError::Security("Invalid UTF-8 in XML FFI input".to_string())
229                })?;
230                self.validate_xml_structure(xml_str)?;
231            }
232            FfiDataType::Json => {
233                let json_str = std::str::from_utf8(data).map_err(|_| {
234                    BuildError::Security("Invalid UTF-8 in JSON FFI input".to_string())
235                })?;
236                serde_json::from_str::<serde_json::Value>(json_str).map_err(|_| {
237                    BuildError::Security("Invalid JSON in FFI input".to_string())
238                })?;
239            }
240            FfiDataType::Binary => {
241                // For binary data, just check size - no format validation needed
242            }
243        }
244        
245        Ok(())
246    }
247    
248    /// Validate XML structure in FFI input
249    fn validate_xml_structure(&self, xml: &str) -> Result<(), BuildError> {
250        let mut reader = quick_xml::Reader::from_str(xml);
251        reader.config_mut().expand_empty_elements = false;
252        
253        let mut buf = Vec::new();
254        let mut depth: i32 = 0;
255        
256        loop {
257            match reader.read_event_into(&mut buf) {
258                Ok(quick_xml::events::Event::Start(_)) => {
259                    depth += 1;
260                    if depth > 100 { // Reasonable depth limit for FFI
261                        return Err(BuildError::Security(
262                            "XML depth limit exceeded in FFI input".to_string()
263                        ));
264                    }
265                }
266                Ok(quick_xml::events::Event::End(_)) => {
267                    depth = depth.saturating_sub(1);
268                }
269                Ok(quick_xml::events::Event::Eof) => break,
270                Ok(_) => {}
271                Err(e) => {
272                    return Err(BuildError::Security(
273                        format!("Invalid XML structure in FFI input: {}", e)
274                    ));
275                }
276            }
277            buf.clear();
278        }
279        
280        Ok(())
281    }
282}
283
284/// API security configuration specifically for different API boundaries
285#[derive(Debug, Clone)]
286pub struct ApiSecurityConfig {
287    /// Enable API security features
288    pub enabled: bool,
289    /// Maximum concurrent requests per client
290    pub max_concurrent_requests: u32,
291    /// Request timeout in seconds
292    pub request_timeout_seconds: u64,
293    /// Enable detailed error messages (disable in production)
294    pub detailed_errors: bool,
295    /// Enable CORS headers for WASM
296    pub enable_cors: bool,
297    /// Allowed origins for CORS
298    pub allowed_origins: Vec<String>,
299}
300
301impl Default for ApiSecurityConfig {
302    fn default() -> Self {
303        Self {
304            enabled: true,
305            max_concurrent_requests: 10,
306            request_timeout_seconds: 30,
307            detailed_errors: false, // Secure default
308            enable_cors: false,
309            allowed_origins: vec!["https://localhost".to_string()],
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    
318    #[test]
319    fn test_api_security_manager() {
320        let config = SecurityConfig::default();
321        let mut manager = ApiSecurityManager::new(config);
322        
323        // Test valid request
324        assert!(manager.validate_request("parse", "user1", 1000).is_ok());
325        
326        // Test oversized payload (should exceed max_xml_size of 100MB)
327        let result = manager.validate_request("parse", "user1", 200_000_000);
328        assert!(result.is_err(), "Expected oversized payload to be rejected, but got: {:?}", result);
329    }
330    
331    #[test]
332    fn test_batch_operation_monitor() {
333        let config = SecurityConfig {
334            max_requests_per_minute: 3,
335            ..SecurityConfig::default()
336        };
337        let mut monitor = BatchOperationMonitor::new(config);
338        
339        // First few operations should succeed
340        assert!(monitor.track_operation("user1", "parse").is_ok());
341        assert!(monitor.track_operation("user1", "build").is_ok());
342        assert!(monitor.track_operation("user1", "validate").is_ok());
343        
344        // Fourth operation should fail
345        assert!(monitor.track_operation("user1", "parse").is_err());
346        
347        // Different user should work
348        assert!(monitor.track_operation("user2", "parse").is_ok());
349        
350        // Check stats
351        let stats = monitor.get_stats("user1").unwrap();
352        assert_eq!(stats.total_operations, 3);
353        assert_eq!(stats.unique_operations, 3);
354    }
355    
356    #[test]
357    fn test_ffi_validator() {
358        let config = SecurityConfig::default();
359        let validator = FfiValidator::new(config);
360        
361        // Test valid UTF-8 string
362        let valid_string = "Hello, world!".as_bytes();
363        assert!(validator.validate_input(valid_string, FfiDataType::Utf8String).is_ok());
364        
365        // Test valid XML
366        let valid_xml = "<root><child>content</child></root>".as_bytes();
367        assert!(validator.validate_input(valid_xml, FfiDataType::Xml).is_ok());
368        
369        // Test valid JSON
370        let valid_json = r#"{"key": "value"}"#.as_bytes();
371        assert!(validator.validate_input(valid_json, FfiDataType::Json).is_ok());
372        
373        // Test invalid UTF-8
374        let invalid_utf8 = &[0xff, 0xfe, 0xfd];
375        assert!(validator.validate_input(invalid_utf8, FfiDataType::Utf8String).is_err());
376        
377        // Test invalid JSON
378        let invalid_json = "{broken json".as_bytes();
379        assert!(validator.validate_input(invalid_json, FfiDataType::Json).is_err());
380    }
381    
382    #[test]
383    fn test_wasm_security_headers() {
384        let config = SecurityConfig::default();
385        let manager = ApiSecurityManager::new(config);
386        
387        let headers = manager.get_wasm_security_headers();
388        
389        // Check that essential security headers are present
390        assert!(headers.contains_key("Content-Security-Policy"));
391        assert!(headers.contains_key("X-Content-Type-Options"));
392        assert!(headers.contains_key("X-Frame-Options"));
393        assert!(headers.contains_key("X-XSS-Protection"));
394        
395        // Check header values
396        assert_eq!(headers.get("X-Content-Type-Options").unwrap(), "nosniff");
397        assert_eq!(headers.get("X-Frame-Options").unwrap(), "DENY");
398    }
399    
400    #[test]
401    fn test_secure_error_response() {
402        let config = SecurityConfig::default();
403        let manager = ApiSecurityManager::new(config);
404        
405        let error = BuildError::Security("Internal security details".to_string());
406        let response = manager.create_secure_error_response(&error, "req-123");
407        
408        // Should not contain internal details
409        assert!(!response.contains("Internal security details"));
410        assert!(response.contains("Security validation failed"));
411        assert!(response.contains("req-123"));
412        assert!(response.contains("error"));
413    }
414}