1pub mod entity_classifier;
11pub mod error_sanitizer;
12pub mod path_validator;
13
14pub use self::entity_classifier::{
16    create_entity, create_external_entity, create_parameter_entity, AttackType, ClassifierConfig,
17    Entity, EntityClass, EntityClassifier, EntityMetrics, ValidationResult,
18};
19
20pub use self::path_validator::{PathValidationConfig, PathValidator, ValidatedPath};
22
23pub use self::error_sanitizer::{
25    sanitize_build_error, sanitize_error, sanitize_io_error, sanitize_parse_error,
26    sanitize_security_error, ErrorContext, ErrorLevel, ErrorMode, ErrorSanitizer, RedactionRule,
27    SanitizedError, SanitizerConfig, SanitizerStatistics, SecureError,
28};
29
30use crate::error::BuildError;
31use once_cell::sync::Lazy;
32use quick_xml::events::Event;
33use quick_xml::Reader;
34use regex::Regex;
35use std::io::BufRead;
36use std::path::{Path, PathBuf};
37use std::time::{Duration, Instant};
38use tracing::{debug, warn};
39use url::Url;
40
41const MAX_XML_SIZE: usize = 100 * 1024 * 1024;
43
44const MAX_JSON_SIZE: usize = 50 * 1024 * 1024;
46
47const MAX_STRING_SIZE: usize = 1024 * 1024;
49
50const MAX_XML_DEPTH: usize = 100;
52
53const MAX_ATTRIBUTES_PER_ELEMENT: usize = 100;
55
56const MAX_CHILD_ELEMENTS: usize = 10000;
58
59const MAX_REQUESTS_PER_MINUTE: u32 = 100;
61const RATE_LIMIT_WINDOW: Duration = Duration::from_secs(60);
62
63static DANGEROUS_ENTITY_REGEX: Lazy<Regex> = Lazy::new(|| {
65    Regex::new(r"<!ENTITY\s+[^>]*>").unwrap()
67});
68
69fn contains_only_safe_entities(input: &str) -> bool {
71    let re = Regex::new(r"&([a-zA-Z_][a-zA-Z0-9._-]*|#[0-9]+|#x[0-9a-fA-F]+);").unwrap();
73    for cap in re.captures_iter(input) {
74        let entity = &cap[1];
75        match entity {
77            "lt" | "gt" | "amp" | "quot" | "apos" => continue,
78            _ if entity.starts_with('#') => continue, _ => return false,                        }
81    }
82    true
83}
84
85static EXTERNAL_REF_REGEX: Lazy<Regex> =
87    Lazy::new(|| Regex::new(r#"(SYSTEM|PUBLIC)\s+['"][^'"]*['"]"#).unwrap());
88
89#[allow(dead_code)]
91static DANGEROUS_PATH_REGEX: Lazy<Regex> =
92    Lazy::new(|| Regex::new(r"\.\./|\\\.\\\|/etc/|/proc/|/sys/|/dev/|/tmp/|C:\\|\\\\").unwrap());
93
94static SQL_INJECTION_REGEX: Lazy<Regex> = Lazy::new(|| {
96    Regex::new(r"(?i)(union|select|insert|update|delete|drop|exec|script|javascript|vbscript|onload|onerror)").unwrap()
97});
98
99static XML_BOMB_REGEX: Lazy<Regex> =
101    Lazy::new(|| Regex::new(r#"<!ENTITY\s+\w+\s+['"](&\w+;)+['"]"#).unwrap());
102
103#[derive(Debug, Clone)]
105pub struct SecurityConfig {
106    pub max_xml_size: usize,
108    pub max_json_size: usize,
110    pub max_string_size: usize,
112    pub max_xml_depth: usize,
114    pub max_attributes_per_element: usize,
116    pub max_child_elements: usize,
118    pub allow_external_entities: bool,
120    pub allow_dtd: bool,
122    pub rate_limiting_enabled: bool,
124    pub max_requests_per_minute: u32,
126    pub enable_entity_classification: bool,
128    pub max_entity_expansion_ratio: f64,
130    pub max_entity_depth: usize,
132}
133
134impl Default for SecurityConfig {
135    fn default() -> Self {
136        Self {
137            max_xml_size: MAX_XML_SIZE,
138            max_json_size: MAX_JSON_SIZE,
139            max_string_size: MAX_STRING_SIZE,
140            max_xml_depth: MAX_XML_DEPTH,
141            max_attributes_per_element: MAX_ATTRIBUTES_PER_ELEMENT,
142            max_child_elements: MAX_CHILD_ELEMENTS,
143            allow_external_entities: false, allow_dtd: false,               rate_limiting_enabled: true,
146            max_requests_per_minute: MAX_REQUESTS_PER_MINUTE,
147            enable_entity_classification: true, max_entity_expansion_ratio: 10.0,   max_entity_depth: 3,                }
151    }
152}
153
154pub struct SecureXmlReader<R: BufRead> {
156    reader: Reader<R>,
157    config: SecurityConfig,
158    current_depth: usize,
159    element_count: usize,
160    start_time: Instant,
161}
162
163impl<R: BufRead> SecureXmlReader<R> {
164    pub fn new(reader: R, config: SecurityConfig) -> Self {
166        let mut xml_reader = Reader::from_reader(reader);
167
168        xml_reader.config_mut().check_comments = false;
170        xml_reader.config_mut().check_end_names = true;
171        xml_reader.config_mut().trim_text_start = true;
172        xml_reader.config_mut().trim_text_end = true;
173        xml_reader.config_mut().expand_empty_elements = false;
174
175        Self {
176            reader: xml_reader,
177            config,
178            current_depth: 0,
179            element_count: 0,
180            start_time: Instant::now(),
181        }
182    }
183
184    pub fn read_event<'a>(&mut self, buf: &'a mut Vec<u8>) -> Result<Event<'a>, BuildError> {
186        if self.start_time.elapsed() > Duration::from_secs(30) {
188            return Err(BuildError::Security("XML processing timeout".to_string()));
189        }
190
191        let event = self
192            .reader
193            .read_event_into(buf)
194            .map_err(|e| BuildError::Security(format!("XML parsing error: {}", e)))?;
195
196        match &event {
197            Event::Start(_) => {
198                self.current_depth += 1;
199                self.element_count += 1;
200
201                if self.current_depth > self.config.max_xml_depth {
203                    return Err(BuildError::Security(format!(
204                        "XML nesting too deep: {} > {}",
205                        self.current_depth, self.config.max_xml_depth
206                    )));
207                }
208
209                if self.element_count > self.config.max_child_elements {
211                    return Err(BuildError::Security(format!(
212                        "Too many XML elements: {} > {}",
213                        self.element_count, self.config.max_child_elements
214                    )));
215                }
216            }
217            Event::End(_) => {
218                self.current_depth = self.current_depth.saturating_sub(1);
219            }
220            Event::DocType(dt) => {
221                if !self.config.allow_dtd {
222                    return Err(BuildError::Security(
223                        "DTD processing not allowed".to_string(),
224                    ));
225                }
226
227                let dtd_str = String::from_utf8_lossy(dt.as_ref());
229                if DANGEROUS_ENTITY_REGEX.is_match(&dtd_str) {
230                    return Err(BuildError::Security(
231                        "Dangerous entity detected in DTD".to_string(),
232                    ));
233                }
234
235                if EXTERNAL_REF_REGEX.is_match(&dtd_str) {
236                    return Err(BuildError::Security(
237                        "External reference detected in DTD".to_string(),
238                    ));
239                }
240
241                if XML_BOMB_REGEX.is_match(&dtd_str) {
242                    return Err(BuildError::Security(
243                        "Potential XML bomb detected".to_string(),
244                    ));
245                }
246            }
247            _ => {}
248        }
249
250        Ok(event)
251    }
252
253    pub fn into_inner(self) -> Reader<R> {
255        self.reader
256    }
257}
258
259pub struct InputValidator {
261    config: SecurityConfig,
262    entity_classifier: Option<EntityClassifier>,
263}
264
265impl InputValidator {
266    pub fn new(config: SecurityConfig) -> Self {
268        let entity_classifier = if config.enable_entity_classification {
269            let mut classifier_config = entity_classifier::ClassifierConfig::default();
270            classifier_config.max_expansion_ratio = config.max_entity_expansion_ratio;
271            classifier_config.max_depth = config.max_entity_depth;
272            classifier_config.allow_external_entities = config.allow_external_entities;
273            Some(EntityClassifier::with_config(classifier_config))
274        } else {
275            None
276        };
277
278        Self {
279            config,
280            entity_classifier,
281        }
282    }
283
284    pub fn validate_string(&self, input: &str, field_name: &str) -> Result<String, BuildError> {
286        if input.len() > self.config.max_string_size {
288            return Err(BuildError::InputSanitization(format!(
289                "String too long for field '{}': {} > {}",
290                field_name,
291                input.len(),
292                self.config.max_string_size
293            )));
294        }
295
296        if input.contains('\0') {
298            return Err(BuildError::InputSanitization(format!(
299                "Null byte detected in field '{}'",
300                field_name
301            )));
302        }
303
304        if SQL_INJECTION_REGEX.is_match(input) {
306            return Err(BuildError::InputSanitization(format!(
307                "Potential injection attack detected in field '{}'",
308                field_name
309            )));
310        }
311
312        if !contains_only_safe_entities(input) {
314            return Err(BuildError::InputSanitization(format!(
315                "Dangerous entity reference detected in field '{}'",
316                field_name
317            )));
318        }
319
320        if input.contains("../")
322            || input.contains("..\\")
323            || input.contains("/etc/")
324            || input.contains("C:\\")
325        {
326            return Err(BuildError::InputSanitization(format!(
327                "Path traversal pattern detected in field '{}'",
328                field_name
329            )));
330        }
331
332        let sanitized = input
334            .chars()
335            .filter(|&c| !c.is_control() || c == '\n' || c == '\r' || c == '\t')
336            .collect::<String>()
337            .trim()
338            .to_string();
339
340        Ok(sanitized)
341    }
342
343    pub fn validate_path(&self, path: &str) -> Result<PathBuf, BuildError> {
345        let mut config = PathValidationConfig::default();
347        config.allow_relative_outside_base = true; config.check_existence = false; let path_validator = PathValidator::with_config(config);
351        let validated_path = path_validator.validate(path)?;
352
353        if !validated_path.warnings.is_empty() {
355            tracing::debug!(
356                "Path validation warnings for '{}': {:?}",
357                path,
358                validated_path.warnings
359            );
360        }
361
362        Ok(validated_path.normalized)
363    }
364
365    pub fn validate_path_with_config(
367        &self,
368        path: &str,
369        config: PathValidationConfig,
370    ) -> Result<PathBuf, BuildError> {
371        let path_validator = PathValidator::with_config(config);
372        let validated_path = path_validator.validate(path)?;
373
374        if !validated_path.warnings.is_empty() {
376            tracing::debug!(
377                "Path validation warnings for '{}': {:?}",
378                path,
379                validated_path.warnings
380            );
381        }
382
383        Ok(validated_path.normalized)
384    }
385
386    pub fn validate_url(&self, url_str: &str) -> Result<Url, BuildError> {
388        let url = Url::parse(url_str)
390            .map_err(|e| BuildError::InputSanitization(format!("Invalid URL: {}", e)))?;
391
392        match url.scheme() {
394            "http" | "https" => {}
395            _ => {
396                return Err(BuildError::InputSanitization(format!(
397                    "Unsafe URL scheme: {}",
398                    url.scheme()
399                )));
400            }
401        }
402
403        if let Some(host_str) = url.host_str() {
405            if host_str == "localhost"
406                || host_str == "127.0.0.1"
407                || host_str == "::1"
408                || host_str.starts_with("192.168.")
409                || host_str.starts_with("10.")
410                || host_str.starts_with("172.")
411            {
412                return Err(BuildError::InputSanitization(
413                    "Private or local URLs not allowed".to_string(),
414                ));
415            }
416        }
417
418        Ok(url)
419    }
420
421    pub fn validate_xml_content(&self, xml: &str) -> Result<(), BuildError> {
423        if xml.len() > self.config.max_xml_size {
425            return Err(BuildError::InputSanitization(format!(
426                "XML too large: {} > {}",
427                xml.len(),
428                self.config.max_xml_size
429            )));
430        }
431
432        if DANGEROUS_ENTITY_REGEX.is_match(xml) {
434            return Err(BuildError::Security(
435                "XML entity declaration detected".to_string(),
436            ));
437        }
438
439        if !contains_only_safe_entities(xml) {
441            return Err(BuildError::Security(
442                "Custom entity reference detected".to_string(),
443            ));
444        }
445
446        if EXTERNAL_REF_REGEX.is_match(xml) {
447            return Err(BuildError::Security(
448                "External reference detected".to_string(),
449            ));
450        }
451
452        if XML_BOMB_REGEX.is_match(xml) {
453            return Err(BuildError::Security(
454                "Potential XML bomb detected".to_string(),
455            ));
456        }
457
458        let entity_count = xml.matches("&").count();
460        if entity_count > 1000 {
461            return Err(BuildError::Security(
462                "Excessive entity usage detected".to_string(),
463            ));
464        }
465
466        Ok(())
467    }
468
469    pub fn validate_entities(&mut self, entities: &[Entity]) -> Result<(), BuildError> {
471        if let Some(ref mut classifier) = self.entity_classifier {
472            let result = classifier.validate_entity_chain(entities);
473
474            if !result.is_safe {
475                let error_msg = if !result.errors.is_empty() {
476                    result.errors.join("; ")
477                } else {
478                    format!("Entity validation failed: {:?}", result.classification)
479                };
480
481                return Err(BuildError::Security(error_msg));
482            }
483
484            if !result.warnings.is_empty() {
486                warn!("Entity validation warnings: {}", result.warnings.join("; "));
487            }
488
489            debug!(
491                "Entity validation metrics: {} entities, {:.2}x expansion, {}ms processing",
492                result.metrics.entity_count,
493                result.metrics.expansion_ratio,
494                result.metrics.processing_time_ms
495            );
496        }
497
498        Ok(())
499    }
500
501    pub fn classify_entity(&mut self, name: &str, value: &str) -> EntityClass {
503        if let Some(ref mut classifier) = self.entity_classifier {
504            classifier.classify_entity(name, value)
505        } else {
506            if contains_only_safe_entities(&format!("&{};", name)) {
508                EntityClass::SafeBuiltin
509            } else {
510                EntityClass::CustomLocal
511            }
512        }
513    }
514
515    pub fn get_entity_metrics(&self) -> Option<Vec<EntityMetrics>> {
517        self.entity_classifier
518            .as_ref()
519            .map(|classifier| classifier.get_metrics_history().iter().cloned().collect())
520    }
521
522    pub fn validate_json_content(&self, json: &str) -> Result<(), BuildError> {
524        if json.len() > self.config.max_json_size {
526            return Err(BuildError::InputSanitization(format!(
527                "JSON too large: {} > {}",
528                json.len(),
529                self.config.max_json_size
530            )));
531        }
532
533        if SQL_INJECTION_REGEX.is_match(json) {
535            return Err(BuildError::InputSanitization(
536                "Potential injection in JSON".to_string(),
537            ));
538        }
539
540        let depth = json
542            .chars()
543            .fold((0i32, 0i32), |(max_depth, current_depth), c| match c {
544                '{' | '[' => (max_depth.max(current_depth + 1), current_depth + 1),
545                '}' | ']' => (max_depth, current_depth.saturating_sub(1)),
546                _ => (max_depth, current_depth),
547            })
548            .0;
549
550        if depth > self.config.max_xml_depth as i32 {
551            return Err(BuildError::InputSanitization(format!(
552                "JSON nesting too deep: {}",
553                depth
554            )));
555        }
556
557        Ok(())
558    }
559}
560
561#[derive(Debug)]
563pub struct RateLimiter {
564    requests: indexmap::IndexMap<String, Vec<Instant>>,
565    config: SecurityConfig,
566}
567
568impl RateLimiter {
569    pub fn new(config: SecurityConfig) -> Self {
571        Self {
572            requests: indexmap::IndexMap::new(),
573            config,
574        }
575    }
576
577    pub fn check_rate_limit(&mut self, identifier: &str) -> Result<(), BuildError> {
579        if !self.config.rate_limiting_enabled {
580            return Ok(());
581        }
582
583        let now = Instant::now();
584        let requests = self.requests.entry(identifier.to_string()).or_default();
585
586        requests.retain(|&req_time| now.duration_since(req_time) <= RATE_LIMIT_WINDOW);
588
589        if requests.len() >= self.config.max_requests_per_minute as usize {
591            return Err(BuildError::Security(format!(
592                "Rate limit exceeded for {}",
593                identifier
594            )));
595        }
596
597        requests.push(now);
599
600        Ok(())
601    }
602
603    pub fn cleanup(&mut self) {
605        let now = Instant::now();
606
607        self.requests.retain(|_, requests| {
608            requests.retain(|&req_time| now.duration_since(req_time) <= RATE_LIMIT_WINDOW);
609            !requests.is_empty()
610        });
611    }
612}
613
614#[derive(Debug)]
616pub struct OutputSanitizer {
617    #[allow(dead_code)]
618    config: SecurityConfig,
619}
620
621impl OutputSanitizer {
622    pub fn new(config: SecurityConfig) -> Self {
624        Self { config }
625    }
626
627    pub fn sanitize_xml_output(&self, xml: &str) -> Result<String, BuildError> {
629        self.check_for_sensitive_data(xml)?;
631
632        self.validate_xml_structure(xml)?;
634
635        let sanitized = self.escape_xml_entities(xml);
637
638        Ok(sanitized)
639    }
640
641    fn check_for_sensitive_data(&self, content: &str) -> Result<(), BuildError> {
643        let sensitive_patterns = [
645            r"<password[^>]*>[^<]+</password>",
646            r"<secret[^>]*>[^<]+</secret>",
647            r"<key[^>]*>[^<]+</key>",
648            r"<token[^>]*>[^<]+</token>",
649            r"password\s*[:=]\s*[^\s<]+",
650            r"secret\s*[:=]\s*[^\s<]+",
651            r"key\s*[:=]\s*[^\s<]+",
652            r"token\s*[:=]\s*[^\s<]+",
653            r"[A-Za-z0-9+/]{40,}={0,2}", ];
655
656        for pattern in &sensitive_patterns {
657            if let Ok(regex) = regex::Regex::new(pattern) {
658                if regex.is_match(content) {
659                    return Err(BuildError::Security(
660                        "Potential sensitive data detected in output".to_string(),
661                    ));
662                }
663            }
664        }
665
666        Ok(())
667    }
668
669    fn escape_xml_entities(&self, xml: &str) -> String {
671        html_escape::encode_text(xml).to_string()
672    }
673
674    fn validate_xml_structure(&self, xml: &str) -> Result<(), BuildError> {
676        let mut reader = quick_xml::Reader::from_str(xml);
677        reader.config_mut().expand_empty_elements = false;
678        reader.config_mut().trim_text(true);
679
680        let mut buf = Vec::new();
681        let mut depth = 0;
682
683        loop {
684            match reader.read_event_into(&mut buf) {
685                Ok(quick_xml::events::Event::Start(_)) => {
686                    depth += 1;
687                    if depth > MAX_XML_DEPTH {
688                        return Err(BuildError::Security(
689                            "XML depth limit exceeded in output".to_string(),
690                        ));
691                    }
692                }
693                Ok(quick_xml::events::Event::End(_)) => {
694                    depth = depth.saturating_sub(1);
695                }
696                Ok(quick_xml::events::Event::Eof) => break,
697                Ok(_) => {}
698                Err(e) => {
699                    return Err(BuildError::Security(format!(
700                        "Invalid XML structure in output: {}",
701                        e
702                    )));
703                }
704            }
705            buf.clear();
706        }
707
708        Ok(())
709    }
710
711    pub fn create_secure_log_message(
713        &self,
714        operation: &str,
715        success: bool,
716        details: Option<&str>,
717    ) -> String {
718        let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
719        let status = if success { "SUCCESS" } else { "FAILED" };
720
721        match details {
722            Some(detail) if detail.len() < 100 => {
723                let sanitized_detail = self.sanitize_log_detail(detail);
725                format!(
726                    "[{}] {} - {}: {}",
727                    timestamp, operation, status, sanitized_detail
728                )
729            }
730            _ => {
731                format!("[{}] {} - {}", timestamp, operation, status)
732            }
733        }
734    }
735
736    fn sanitize_log_detail(&self, detail: &str) -> String {
738        let sensitive_patterns = [
740            (r"password\s*[:=]\s*[^\s]+", "password=[REDACTED]"),
741            (r"secret\s*[:=]\s*[^\s]+", "secret=[REDACTED]"),
742            (r"key\s*[:=]\s*[^\s]+", "key=[REDACTED]"),
743            (r"token\s*[:=]\s*[^\s]+", "token=[REDACTED]"),
744        ];
745
746        let mut sanitized = detail.to_string();
747        for (pattern, replacement) in &sensitive_patterns {
748            if let Ok(regex) = regex::Regex::new(pattern) {
749                sanitized = regex.replace_all(&sanitized, *replacement).to_string();
750            }
751        }
752
753        if sanitized.len() > 200 {
755            sanitized.truncate(197);
756            sanitized.push_str("...");
757        }
758
759        sanitized
760    }
761}
762
763pub struct SecureTempFile {
765    path: PathBuf,
766    file: std::fs::File,
767}
768
769impl SecureTempFile {
770    pub fn new() -> Result<Self, BuildError> {
772        use std::fs::OpenOptions;
773        #[cfg(unix)]
774        use std::os::unix::fs::OpenOptionsExt;
775
776        let temp_dir = std::env::temp_dir();
777        let file_name = format!("ddex_builder_{}", uuid::Uuid::new_v4());
778        let path = temp_dir.join(file_name);
779
780        #[cfg(unix)]
782        let file = OpenOptions::new()
783            .create_new(true)
784            .write(true)
785            .read(true)
786            .mode(0o600) .open(&path)
788            .map_err(|e| BuildError::Io(format!("Failed to create secure temp file: {}", e)))?;
789
790        #[cfg(not(unix))]
791        let file = OpenOptions::new()
792            .create_new(true)
793            .write(true)
794            .read(true)
795            .open(&path)
796            .map_err(|e| BuildError::Io(format!("Failed to create secure temp file: {}", e)))?;
797
798        Ok(Self { path, file })
799    }
800
801    pub fn file(&mut self) -> &mut std::fs::File {
803        &mut self.file
804    }
805
806    pub fn path(&self) -> &Path {
808        &self.path
809    }
810}
811
812impl Drop for SecureTempFile {
813    fn drop(&mut self) {
814        let _ = std::fs::remove_file(&self.path);
816    }
817}
818
819pub mod utils {
821
822    pub fn sanitize_filename(filename: &str) -> String {
824        filename
825            .chars()
826            .filter(|c| c.is_alphanumeric() || *c == '.' || *c == '-' || *c == '_')
827            .take(255) .collect::<String>()
829            .replace("..", "") }
831
832    pub fn generate_secure_id() -> String {
834        uuid::Uuid::new_v4().to_string()
835    }
836
837    pub fn constant_time_compare(a: &str, b: &str) -> bool {
839        if a.len() != b.len() {
840            return false;
841        }
842
843        let mut result = 0u8;
844        for (byte_a, byte_b) in a.bytes().zip(b.bytes()) {
845            result |= byte_a ^ byte_b;
846        }
847
848        result == 0
849    }
850
851    pub fn hash_for_logging(data: &str) -> String {
853        use sha2::{Digest, Sha256};
854        let hash = Sha256::digest(data.as_bytes());
855        format!("{:.8}", hex::encode(hash))
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use std::io::Cursor;
863
864    #[test]
865    fn test_input_validation() {
866        let config = SecurityConfig::default();
867        let validator = InputValidator::new(config);
868
869        assert!(validator.validate_string("Valid input", "test").is_ok());
871
872        assert!(validator.validate_string("Invalid\0input", "test").is_err());
874
875        assert!(validator
877            .validate_string("'; DROP TABLE users; --", "test")
878            .is_err());
879
880        assert!(validator.validate_string("&dangerous;", "test").is_err());
882    }
883
884    #[test]
885    fn test_path_validation() {
886        let config = SecurityConfig::default();
887        let validator = InputValidator::new(config);
888
889        assert!(validator.validate_path("safe/path/file.xml").is_ok());
891
892        assert!(validator.validate_path("../../../etc/passwd").is_err());
894
895        assert!(validator.validate_path("/etc/passwd").is_err());
897    }
898
899    #[test]
900    fn test_xml_security() {
901        let config = SecurityConfig::default();
902        let validator = InputValidator::new(config);
903
904        assert!(validator
906            .validate_xml_content("<root><child>content</child></root>")
907            .is_ok());
908
909        assert!(validator
911            .validate_xml_content(
912                "<!DOCTYPE test [<!ENTITY xxe SYSTEM 'file:///etc/passwd'>]><root>&xxe;</root>"
913            )
914            .is_err());
915
916        assert!(validator.validate_xml_content(
918            "<!DOCTYPE bomb [<!ENTITY a '&b;&b;'><!ENTITY b '&c;&c;'><!ENTITY c 'boom'>]><root>&a;</root>"
919        ).is_err());
920    }
921
922    #[test]
923    fn test_secure_xml_reader() {
924        let config = SecurityConfig::default();
925        let xml = b"<root><child>content</child></root>";
926        let cursor = Cursor::new(xml);
927        let mut reader = SecureXmlReader::new(cursor, config);
928
929        let mut buf = Vec::new();
931        loop {
932            match reader.read_event(&mut buf) {
933                Ok(Event::Eof) => break,
934                Ok(_) => {
935                    buf.clear();
936                    continue;
937                }
938                Err(e) => panic!("Unexpected error: {}", e),
939            }
940        }
941    }
942
943    #[test]
944    fn test_rate_limiter() {
945        let config = SecurityConfig {
946            rate_limiting_enabled: true,
947            max_requests_per_minute: 2,
948            ..SecurityConfig::default()
949        };
950        let mut limiter = RateLimiter::new(config);
951
952        assert!(limiter.check_rate_limit("user1").is_ok());
954        assert!(limiter.check_rate_limit("user1").is_ok());
955
956        assert!(limiter.check_rate_limit("user1").is_err());
958
959        assert!(limiter.check_rate_limit("user2").is_ok());
961    }
962
963    #[test]
964    fn test_url_validation() {
965        let config = SecurityConfig::default();
966        let validator = InputValidator::new(config);
967
968        assert!(validator.validate_url("https://example.com/path").is_ok());
970
971        assert!(validator.validate_url("http://192.168.1.1/").is_err());
973
974        assert!(validator.validate_url("http://localhost:8080/").is_err());
976
977        assert!(validator.validate_url("file:///etc/passwd").is_err());
979    }
980
981    #[test]
982    fn test_output_sanitizer() {
983        let config = SecurityConfig::default();
984        let sanitizer = OutputSanitizer::new(config);
985
986        let safe_xml = "<root><child>content</child></root>";
988        assert!(sanitizer.sanitize_xml_output(safe_xml).is_ok());
989
990        let sensitive_xml = "<root><password>secret123</password></root>";
992        let result = sanitizer.sanitize_xml_output(sensitive_xml);
993        assert!(
994            result.is_err(),
995            "Expected sensitive data to be detected, but got: {:?}",
996            result
997        );
998
999        let malformed_xml = "<root><child>content</child><"; let result = sanitizer.sanitize_xml_output(malformed_xml);
1002        assert!(
1003            result.is_err(),
1004            "Expected malformed XML to be rejected, but got: {:?}",
1005            result
1006        );
1007    }
1008
1009    #[test]
1010    fn test_secure_logging() {
1011        let config = SecurityConfig::default();
1012        let sanitizer = OutputSanitizer::new(config);
1013
1014        let log_msg = sanitizer.create_secure_log_message("BUILD", true, Some("file.xml"));
1016        assert!(log_msg.contains("BUILD"));
1017        assert!(log_msg.contains("SUCCESS"));
1018        assert!(log_msg.contains("file.xml"));
1019
1020        let sensitive_detail = "password=secret123 key=abc";
1022        let log_msg = sanitizer.create_secure_log_message("LOGIN", false, Some(sensitive_detail));
1023        assert!(log_msg.contains("[REDACTED]"));
1024        assert!(!log_msg.contains("secret123"));
1025        assert!(!log_msg.contains("abc"));
1026    }
1027
1028    #[test]
1029    fn test_security_utils() {
1030        let clean_name = utils::sanitize_filename("../../../etc/passwd");
1032        assert!(!clean_name.contains(".."));
1033        assert!(!clean_name.contains("/"));
1034
1035        let id1 = utils::generate_secure_id();
1037        let id2 = utils::generate_secure_id();
1038        assert_ne!(id1, id2);
1039        assert_eq!(id1.len(), 36); assert!(utils::constant_time_compare("test", "test"));
1043        assert!(!utils::constant_time_compare("test", "other"));
1044        assert!(!utils::constant_time_compare("test", "testing"));
1045
1046        let hash = utils::hash_for_logging("sensitive_data");
1048        assert_eq!(hash.len(), 8);
1049        assert!(!hash.contains("sensitive"));
1050    }
1051}