1use crate::error::BuildError;
7use crate::security::{OutputSanitizer, RateLimiter, SecurityConfig};
8use indexmap::{IndexMap, IndexSet};
9use std::time::{Duration, Instant};
10
11#[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 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 pub fn validate_request(
35 &mut self,
36 operation: &str,
37 identifier: &str,
38 payload_size: usize,
39 ) -> Result<(), BuildError> {
40 self.rate_limiter.check_rate_limit(identifier)?;
42
43 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 self.batch_monitor.track_operation(identifier, operation)?;
53
54 Ok(())
55 }
56
57 pub fn sanitize_response(&self, response: &str) -> Result<String, BuildError> {
59 self.output_sanitizer.sanitize_xml_output(response)
60 }
61
62 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 pub fn get_wasm_security_headers(&self) -> IndexMap<String, String> {
73 let mut headers = IndexMap::new();
74
75 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 headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
83
84 headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
86
87 headers.insert("X-XSS-Protection".to_string(), "1; mode=block".to_string());
89
90 headers.insert(
92 "Referrer-Policy".to_string(),
93 "strict-origin-when-cross-origin".to_string(),
94 );
95
96 headers.insert(
98 "Permissions-Policy".to_string(),
99 "camera=(), microphone=(), location=(), interest-cohort=()".to_string(),
100 );
101
102 headers
103 }
104
105 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#[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 pub fn new(config: SecurityConfig) -> Self {
142 Self {
143 operations: IndexMap::new(),
144 config,
145 }
146 }
147
148 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 records.retain(|record| now.duration_since(record.timestamp) <= Duration::from_secs(60));
155
156 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 records.push(OperationRecord {
165 operation: operation.to_string(),
166 timestamp: now,
167 resource_usage: 1, });
169
170 Ok(())
171 }
172
173 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#[derive(Debug)]
197pub struct RateLimitInfo {
198 pub total_operations: usize,
200 pub unique_operations: usize,
202 pub time_window_seconds: u64,
204}
205
206#[derive(Debug, Clone, Copy)]
208pub enum ContentType {
209 Xml,
211 Json,
213 Binary,
215 Utf8String,
217}
218
219#[derive(Debug, Clone)]
221pub struct BatchStats {
222 pub total_operations: usize,
224 pub unique_operations: usize,
226 pub time_window_seconds: u64,
228}
229
230#[derive(Debug)]
232pub struct FfiValidator {
233 config: SecurityConfig,
234}
235
236#[derive(Debug, Clone, Copy)]
238pub enum FfiDataType {
239 Xml,
241 Json,
243 Binary,
245 Utf8String,
247}
248
249impl FfiValidator {
250 pub fn new(config: SecurityConfig) -> Self {
252 Self { config }
253 }
254
255 pub fn validate_input(
257 &self,
258 data: &[u8],
259 expected_type: FfiDataType,
260 ) -> Result<(), BuildError> {
261 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 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 }
291 }
292
293 Ok(())
294 }
295
296 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 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#[derive(Debug, Clone)]
336pub struct ApiSecurityConfig {
337 pub enabled: bool,
339 pub max_concurrent_requests: u32,
341 pub request_timeout_seconds: u64,
343 pub detailed_errors: bool,
345 pub enable_cors: bool,
347 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, 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 assert!(manager.validate_request("parse", "user1", 1000).is_ok());
375
376 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 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 assert!(monitor.track_operation("user1", "parse").is_err());
400
401 assert!(monitor.track_operation("user2", "parse").is_ok());
403
404 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 let valid_string = "Hello, world!".as_bytes();
417 assert!(validator
418 .validate_input(valid_string, FfiDataType::Utf8String)
419 .is_ok());
420
421 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 let valid_json = r#"{"key": "value"}"#.as_bytes();
429 assert!(validator
430 .validate_input(valid_json, FfiDataType::Json)
431 .is_ok());
432
433 let invalid_utf8 = &[0xff, 0xfe, 0xfd];
435 assert!(validator
436 .validate_input(invalid_utf8, FfiDataType::Utf8String)
437 .is_err());
438
439 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 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 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 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}