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