1use crate::error::BuildError;
7use crate::security::{SecurityConfig, RateLimiter, OutputSanitizer};
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(&mut self, operation: &str, identifier: &str, payload_size: usize) -> Result<(), BuildError> {
35 self.rate_limiter.check_rate_limit(identifier)?;
37
38 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 self.batch_monitor.track_operation(identifier, operation)?;
47
48 Ok(())
49 }
50
51 pub fn sanitize_response(&self, response: &str) -> Result<String, BuildError> {
53 self.output_sanitizer.sanitize_xml_output(response)
54 }
55
56 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 pub fn get_wasm_security_headers(&self) -> IndexMap<String, String> {
63 let mut headers = IndexMap::new();
64
65 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 headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
73
74 headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
76
77 headers.insert("X-XSS-Protection".to_string(), "1; mode=block".to_string());
79
80 headers.insert("Referrer-Policy".to_string(), "strict-origin-when-cross-origin".to_string());
82
83 headers.insert(
85 "Permissions-Policy".to_string(),
86 "camera=(), microphone=(), location=(), interest-cohort=()".to_string(),
87 );
88
89 headers
90 }
91
92 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#[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 pub fn new(config: SecurityConfig) -> Self {
128 Self {
129 operations: IndexMap::new(),
130 config,
131 }
132 }
133
134 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 records.retain(|record| now.duration_since(record.timestamp) <= Duration::from_secs(60));
141
142 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 records.push(OperationRecord {
151 operation: operation.to_string(),
152 timestamp: now,
153 resource_usage: 1, });
155
156 Ok(())
157 }
158
159 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#[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#[derive(Debug)]
191pub struct FfiValidator {
192 config: SecurityConfig,
193}
194
195#[derive(Debug, Clone, Copy)]
197pub enum FfiDataType {
198 Xml,
199 Json,
200 Binary,
201 Utf8String,
202}
203
204impl FfiValidator {
205 pub fn new(config: SecurityConfig) -> Self {
207 Self { config }
208 }
209
210 pub fn validate_input(&self, data: &[u8], expected_type: FfiDataType) -> Result<(), BuildError> {
212 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 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 }
243 }
244
245 Ok(())
246 }
247
248 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 { 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#[derive(Debug, Clone)]
286pub struct ApiSecurityConfig {
287 pub enabled: bool,
289 pub max_concurrent_requests: u32,
291 pub request_timeout_seconds: u64,
293 pub detailed_errors: bool,
295 pub enable_cors: bool,
297 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, 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 assert!(manager.validate_request("parse", "user1", 1000).is_ok());
325
326 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 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 assert!(monitor.track_operation("user1", "parse").is_err());
346
347 assert!(monitor.track_operation("user2", "parse").is_ok());
349
350 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 let valid_string = "Hello, world!".as_bytes();
363 assert!(validator.validate_input(valid_string, FfiDataType::Utf8String).is_ok());
364
365 let valid_xml = "<root><child>content</child></root>".as_bytes();
367 assert!(validator.validate_input(valid_xml, FfiDataType::Xml).is_ok());
368
369 let valid_json = r#"{"key": "value"}"#.as_bytes();
371 assert!(validator.validate_input(valid_json, FfiDataType::Json).is_ok());
372
373 let invalid_utf8 = &[0xff, 0xfe, 0xfd];
375 assert!(validator.validate_input(invalid_utf8, FfiDataType::Utf8String).is_err());
376
377 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 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 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 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}