ddex_builder/security/
error_sanitizer.rs

1//! Error message sanitization system for preventing information disclosure
2//!
3//! This module provides a comprehensive error sanitization system that ensures
4//! sensitive information is not leaked through error messages while maintaining
5//! useful debugging capabilities for developers.
6
7use once_cell::sync::Lazy;
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fmt;
12use std::sync::Mutex;
13use tracing::error;
14use uuid::Uuid;
15
16/// Operating mode for error sanitization
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum ErrorMode {
19    /// Production mode - maximum sanitization, minimal information disclosure
20    Production,
21    /// Development mode - balanced sanitization, more details for debugging
22    Development,
23    /// Testing mode - minimal sanitization, full details for test validation
24    Testing,
25}
26
27/// Error classification levels for secure error handling
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ErrorLevel {
30    /// Safe for external users - no sensitive information
31    Public,
32    /// For internal logging only - may contain sensitive details
33    Internal,
34    /// Development only - full details, stripped in release builds
35    Debug,
36}
37
38/// Context where error occurred
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ErrorContext {
41    /// File open operation
42    FileOpen,
43    /// File read operation
44    FileRead,
45    /// File write operation
46    FileWrite,
47    /// Network request
48    NetworkRequest,
49    /// XML parsing
50    XmlParsing,
51    /// XML building
52    XmlBuilding,
53    /// Security validation
54    SecurityValidation,
55    /// Entity classification
56    EntityClassification,
57    /// Path validation
58    PathValidation,
59    /// Memory allocation
60    MemoryAllocation,
61    /// Database connection
62    DatabaseConnection,
63    /// Authentication check
64    Authentication,
65    /// Authorization check
66    Authorization,
67}
68
69/// Trait for secure error handling with multiple disclosure levels
70pub trait SecureError: fmt::Display + fmt::Debug {
71    /// Get the public-safe error message
72    fn public_message(&self) -> String;
73
74    /// Get the internal error message for logging
75    fn internal_message(&self) -> String;
76
77    /// Get the debug error message (development only)
78    fn debug_message(&self) -> String;
79
80    /// Get the error classification level
81    fn error_level(&self) -> ErrorLevel;
82
83    /// Get the error context
84    fn error_context(&self) -> ErrorContext;
85
86    /// Generate a unique error ID for correlation
87    fn error_id(&self) -> String {
88        Uuid::new_v4().to_string()
89    }
90}
91
92/// Rule for redacting sensitive information from error messages
93#[derive(Debug, Clone)]
94pub struct RedactionRule {
95    /// Name of the rule for identification
96    pub name: String,
97    /// Regex pattern to match sensitive data
98    pub pattern: Regex,
99    /// Replacement text (may include capture groups)
100    pub replacement: String,
101    /// Whether this rule applies in production mode
102    pub production: bool,
103    /// Whether this rule applies in development mode
104    pub development: bool,
105    /// Whether this rule applies in testing mode
106    pub testing: bool,
107}
108
109impl RedactionRule {
110    /// Create a new redaction rule
111    pub fn new(
112        name: &str,
113        pattern: &str,
114        replacement: &str,
115        production: bool,
116        development: bool,
117        testing: bool,
118    ) -> Result<Self, regex::Error> {
119        Ok(RedactionRule {
120            name: name.to_string(),
121            pattern: Regex::new(pattern)?,
122            replacement: replacement.to_string(),
123            production,
124            development,
125            testing,
126        })
127    }
128
129    /// Check if this rule applies in the given mode
130    pub fn applies_to_mode(&self, mode: ErrorMode) -> bool {
131        match mode {
132            ErrorMode::Production => self.production,
133            ErrorMode::Development => self.development,
134            ErrorMode::Testing => self.testing,
135        }
136    }
137
138    /// Apply this rule to a message
139    pub fn apply(&self, message: &str) -> String {
140        self.pattern
141            .replace_all(message, self.replacement.as_str())
142            .to_string()
143    }
144}
145
146/// Configuration for error sanitization behavior
147#[derive(Debug, Clone)]
148pub struct SanitizerConfig {
149    /// Operating mode
150    pub mode: ErrorMode,
151    /// Whether to generate correlation IDs
152    pub generate_correlation_ids: bool,
153    /// Whether to log internal details
154    pub log_internal_details: bool,
155    /// Maximum error message length
156    pub max_message_length: usize,
157    /// Whether to include error codes
158    pub include_error_codes: bool,
159}
160
161impl Default for SanitizerConfig {
162    fn default() -> Self {
163        SanitizerConfig {
164            mode: if cfg!(debug_assertions) {
165                ErrorMode::Development
166            } else {
167                ErrorMode::Production
168            },
169            generate_correlation_ids: true,
170            log_internal_details: true,
171            max_message_length: 256,
172            include_error_codes: true,
173        }
174    }
175}
176
177/// Main error sanitization engine
178pub struct ErrorSanitizer {
179    config: SanitizerConfig,
180    redaction_rules: Vec<RedactionRule>,
181    error_code_map: HashMap<ErrorContext, &'static str>,
182    correlation_store: HashMap<String, String>,
183}
184
185/// Sanitized error result with correlation ID
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SanitizedError {
188    /// Correlation ID for internal tracking
189    pub correlation_id: String,
190    /// Public-safe error message
191    pub message: String,
192    /// Error code for programmatic handling
193    pub code: Option<String>,
194    /// Additional context that's safe to expose
195    pub context: Option<String>,
196}
197
198impl fmt::Display for SanitizedError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        if let Some(code) = &self.code {
201            write!(f, "[{}] {}", code, self.message)?;
202        } else {
203            write!(f, "{}", self.message)?;
204        }
205
206        if let Some(context) = &self.context {
207            write!(f, " ({})", context)?;
208        }
209
210        write!(f, " [ID: {}]", &self.correlation_id[0..8])
211    }
212}
213
214/// Pre-defined redaction rules for common sensitive data patterns
215static DEFAULT_REDACTION_RULES: Lazy<Vec<RedactionRule>> = Lazy::new(|| {
216    let mut rules = Vec::new();
217
218    // File system paths - most aggressive in production
219    if let Ok(rule) = RedactionRule::new(
220        "filesystem_paths",
221        r"(/[^/\s]+)+(/[^/\s]*\.[^/\s]+)?|([A-Z]:\\[^\\]+\\[^\\]*)",
222        "<file path>",
223        true,  // production
224        false, // development
225        false, // testing
226    ) {
227        rules.push(rule);
228    }
229
230    // Development-friendly path redaction (keep filename)
231    if let Ok(rule) = RedactionRule::new(
232        "filesystem_paths_dev",
233        r"(/[^/\s]+)+/([^/\s]*\.[^/\s]+)|([A-Z]:\\[^\\]+\\[^\\]*)\\([^\\]*)",
234        "<path>/$2$4",
235        false, // production
236        true,  // development
237        false, // testing
238    ) {
239        rules.push(rule);
240    }
241
242    // IP addresses
243    if let Ok(rule) = RedactionRule::new(
244        "ip_addresses",
245        r"\b(?:\d{1,3}\.){3}\d{1,3}\b|\b[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}\b",
246        "<ip address>",
247        true,  // production
248        true,  // development
249        false, // testing
250    ) {
251        rules.push(rule);
252    }
253
254    // Hostnames and URLs
255    if let Ok(rule) = RedactionRule::new(
256        "hostnames",
257        r"https?://[^\s/$.?#].[^\s]*|[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(?:[/\s]|$)",
258        "<hostname>",
259        true,  // production
260        true,  // development
261        false, // testing
262    ) {
263        rules.push(rule);
264    }
265
266    // Memory addresses
267    if let Ok(rule) = RedactionRule::new(
268        "memory_addresses",
269        r"0x[0-9a-fA-F]+|[0-9a-fA-F]{8,16}",
270        "<memory address>",
271        true,  // production
272        true,  // development
273        false, // testing
274    ) {
275        rules.push(rule);
276    }
277
278    // Stack traces (line numbers and function names)
279    if let Ok(rule) = RedactionRule::new(
280        "stack_traces",
281        r"at [^:]+:\d+:\d+|in `[^`]+`",
282        "<stack trace>",
283        true,  // production
284        false, // development
285        false, // testing
286    ) {
287        rules.push(rule);
288    }
289
290    // API keys and tokens (basic patterns)
291    if let Ok(rule) = RedactionRule::new(
292        "api_keys",
293        r#"(?i)(api_?key|token|secret|password|auth)[\s]*[:=][\s]*"?([a-zA-Z0-9\-_]{16,})"?"#,
294        "$1=<redacted>",
295        true, // production
296        true, // development
297        true, // testing (even in testing, don't leak real keys)
298    ) {
299        rules.push(rule);
300    }
301
302    // User-specific paths (home directories)
303    if let Ok(rule) = RedactionRule::new(
304        "user_paths",
305        r"/Users/[^/\s]+|/home/[^/\s]+|C:\\Users\\[^\\\\]+",
306        "<user directory>",
307        true,  // production
308        true,  // development
309        false, // testing
310    ) {
311        rules.push(rule);
312    }
313
314    // Database connection strings
315    if let Ok(rule) = RedactionRule::new(
316        "db_connections",
317        r"(?i)(mysql|postgres|mongodb)://[^@\s]+@[^/\s]+/[^\s]*",
318        "$1://<connection>",
319        true, // production
320        true, // development
321        true, // testing
322    ) {
323        rules.push(rule);
324    }
325
326    rules
327});
328
329impl ErrorSanitizer {
330    /// Create a new error sanitizer with default configuration
331    pub fn new() -> Self {
332        Self::with_config(SanitizerConfig::default())
333    }
334
335    /// Create a new error sanitizer with custom configuration
336    pub fn with_config(config: SanitizerConfig) -> Self {
337        let error_code_map = Self::create_error_code_map();
338
339        ErrorSanitizer {
340            config,
341            redaction_rules: DEFAULT_REDACTION_RULES.clone(),
342            error_code_map,
343            correlation_store: HashMap::new(),
344        }
345    }
346
347    /// Add a custom redaction rule
348    pub fn add_redaction_rule(&mut self, rule: RedactionRule) {
349        self.redaction_rules.push(rule);
350    }
351
352    /// Sanitize an error message based on context and mode
353    pub fn sanitize<E>(&mut self, error: E, context: ErrorContext) -> SanitizedError
354    where
355        E: std::error::Error + fmt::Display + fmt::Debug,
356    {
357        let correlation_id = if self.config.generate_correlation_ids {
358            Uuid::new_v4().to_string()
359        } else {
360            "none".to_string()
361        };
362
363        // Get the raw error message
364        let raw_message = error.to_string();
365        let debug_message = format!("{:?}", error);
366
367        // Log full details internally if enabled
368        if self.config.log_internal_details {
369            error!(
370                correlation_id = %correlation_id,
371                context = ?context,
372                raw_message = %raw_message,
373                debug_info = %debug_message,
374                "Internal error details"
375            );
376
377            // Store full details for potential debugging
378            if self.config.generate_correlation_ids {
379                self.correlation_store.insert(
380                    correlation_id.clone(),
381                    format!(
382                        "Context: {:?}, Error: {}, Debug: {}",
383                        context, raw_message, debug_message
384                    ),
385                );
386            }
387        }
388
389        // Apply sanitization based on mode and context
390        let sanitized_message = self.apply_sanitization(&raw_message, context);
391
392        // Truncate if too long
393        let final_message = if sanitized_message.len() > self.config.max_message_length {
394            format!(
395                "{}...",
396                &sanitized_message[0..self.config.max_message_length.saturating_sub(3)]
397            )
398        } else {
399            sanitized_message
400        };
401
402        // Get error code
403        let error_code = if self.config.include_error_codes {
404            self.error_code_map.get(&context).map(|&s| s.to_string())
405        } else {
406            None
407        };
408
409        SanitizedError {
410            correlation_id,
411            message: final_message,
412            code: error_code,
413            context: Some(self.get_safe_context_description(context)),
414        }
415    }
416
417    /// Apply sanitization rules to a message
418    fn apply_sanitization(&self, message: &str, context: ErrorContext) -> String {
419        let mut sanitized = message.to_string();
420
421        // Apply context-specific sanitization first
422        sanitized = self.apply_context_specific_sanitization(sanitized, context);
423
424        // Apply general redaction rules
425        for rule in &self.redaction_rules {
426            if rule.applies_to_mode(self.config.mode) {
427                sanitized = rule.apply(&sanitized);
428            }
429        }
430
431        sanitized
432    }
433
434    /// Apply context-specific sanitization logic
435    fn apply_context_specific_sanitization(
436        &self,
437        message: String,
438        context: ErrorContext,
439    ) -> String {
440        match (context, self.config.mode) {
441            (
442                ErrorContext::FileOpen | ErrorContext::FileRead | ErrorContext::FileWrite,
443                ErrorMode::Production,
444            ) => "File operation failed".to_string(),
445            (
446                ErrorContext::FileOpen | ErrorContext::FileRead | ErrorContext::FileWrite,
447                ErrorMode::Development,
448            ) => {
449                // Keep operation type but redact full paths
450                let operation = match context {
451                    ErrorContext::FileOpen => "open",
452                    ErrorContext::FileRead => "read",
453                    ErrorContext::FileWrite => "write",
454                    _ => "access",
455                };
456                format!("Failed to {} file", operation)
457            }
458            (ErrorContext::NetworkRequest, ErrorMode::Production) => {
459                "Network operation failed".to_string()
460            }
461            (ErrorContext::XmlParsing, ErrorMode::Production) => {
462                "Invalid XML structure".to_string()
463            }
464            (ErrorContext::XmlBuilding, ErrorMode::Production) => {
465                "XML generation failed".to_string()
466            }
467            (ErrorContext::SecurityValidation, ErrorMode::Production) => {
468                "Security validation failed".to_string()
469            }
470            (ErrorContext::EntityClassification, ErrorMode::Production) => {
471                "Entity validation failed".to_string()
472            }
473            (ErrorContext::PathValidation, ErrorMode::Production) => {
474                "Path validation failed".to_string()
475            }
476            (ErrorContext::MemoryAllocation, ErrorMode::Production) => {
477                "Memory allocation failed".to_string()
478            }
479            (ErrorContext::DatabaseConnection, ErrorMode::Production) => {
480                "Database connection failed".to_string()
481            }
482            (ErrorContext::Authentication, ErrorMode::Production) => {
483                "Authentication failed".to_string()
484            }
485            (ErrorContext::Authorization, ErrorMode::Production) => "Access denied".to_string(),
486            // In development and testing modes, allow more detail
487            _ => message,
488        }
489    }
490
491    /// Create error code mapping
492    fn create_error_code_map() -> HashMap<ErrorContext, &'static str> {
493        let mut map = HashMap::new();
494        map.insert(ErrorContext::FileOpen, "E1001");
495        map.insert(ErrorContext::FileRead, "E1002");
496        map.insert(ErrorContext::FileWrite, "E1003");
497        map.insert(ErrorContext::NetworkRequest, "E2001");
498        map.insert(ErrorContext::XmlParsing, "E3001");
499        map.insert(ErrorContext::XmlBuilding, "E3002");
500        map.insert(ErrorContext::SecurityValidation, "E4001");
501        map.insert(ErrorContext::EntityClassification, "E4002");
502        map.insert(ErrorContext::PathValidation, "E4003");
503        map.insert(ErrorContext::MemoryAllocation, "E5001");
504        map.insert(ErrorContext::DatabaseConnection, "E6001");
505        map.insert(ErrorContext::Authentication, "E7001");
506        map.insert(ErrorContext::Authorization, "E7002");
507        map
508    }
509
510    /// Get a safe description of the error context
511    fn get_safe_context_description(&self, context: ErrorContext) -> String {
512        match context {
513            ErrorContext::FileOpen => "file access".to_string(),
514            ErrorContext::FileRead => "file reading".to_string(),
515            ErrorContext::FileWrite => "file writing".to_string(),
516            ErrorContext::NetworkRequest => "network operation".to_string(),
517            ErrorContext::XmlParsing => "XML parsing".to_string(),
518            ErrorContext::XmlBuilding => "XML generation".to_string(),
519            ErrorContext::SecurityValidation => "security check".to_string(),
520            ErrorContext::EntityClassification => "entity validation".to_string(),
521            ErrorContext::PathValidation => "path validation".to_string(),
522            ErrorContext::MemoryAllocation => "memory management".to_string(),
523            ErrorContext::DatabaseConnection => "database access".to_string(),
524            ErrorContext::Authentication => "authentication".to_string(),
525            ErrorContext::Authorization => "authorization".to_string(),
526        }
527    }
528
529    /// Retrieve stored error details by correlation ID (for debugging)
530    pub fn get_error_details(&self, correlation_id: &str) -> Option<&String> {
531        self.correlation_store.get(correlation_id)
532    }
533
534    /// Clear stored error details (for memory management)
535    pub fn clear_error_store(&mut self) {
536        self.correlation_store.clear();
537    }
538
539    /// Get statistics about sanitization
540    pub fn get_statistics(&self) -> SanitizerStatistics {
541        SanitizerStatistics {
542            mode: self.config.mode,
543            active_rules: self
544                .redaction_rules
545                .iter()
546                .filter(|r| r.applies_to_mode(self.config.mode))
547                .count(),
548            stored_errors: self.correlation_store.len(),
549        }
550    }
551}
552
553/// Statistics about the error sanitizer
554#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct SanitizerStatistics {
556    /// Current error handling mode
557    pub mode: ErrorMode,
558    /// Number of active sanitization rules
559    pub active_rules: usize,
560    /// Number of errors stored for analysis
561    pub stored_errors: usize,
562}
563
564impl Default for ErrorSanitizer {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570/// Convenience functions for common error types
571impl ErrorSanitizer {
572    /// Sanitize an I/O error
573    pub fn sanitize_io_error<E>(&mut self, error: E, context: ErrorContext) -> SanitizedError
574    where
575        E: std::error::Error + fmt::Display + fmt::Debug,
576    {
577        self.sanitize(error, context)
578    }
579
580    /// Sanitize a parsing error
581    pub fn sanitize_parse_error<E>(&mut self, error: E) -> SanitizedError
582    where
583        E: std::error::Error + fmt::Display + fmt::Debug,
584    {
585        self.sanitize(error, ErrorContext::XmlParsing)
586    }
587
588    /// Sanitize a build error
589    pub fn sanitize_build_error<E>(&mut self, error: E) -> SanitizedError
590    where
591        E: std::error::Error + fmt::Display + fmt::Debug,
592    {
593        self.sanitize(error, ErrorContext::XmlBuilding)
594    }
595
596    /// Sanitize a security error
597    pub fn sanitize_security_error<E>(&mut self, error: E) -> SanitizedError
598    where
599        E: std::error::Error + fmt::Display + fmt::Debug,
600    {
601        self.sanitize(error, ErrorContext::SecurityValidation)
602    }
603}
604
605/// Global error sanitizer instance - thread-safe and no unsafe code required
606static GLOBAL_SANITIZER: Lazy<Mutex<ErrorSanitizer>> =
607    Lazy::new(|| Mutex::new(ErrorSanitizer::with_config(SanitizerConfig::default())));
608
609/// Initialize the global error sanitizer with custom configuration
610pub fn init_global_sanitizer(config: SanitizerConfig) {
611    // Replace the default sanitizer with one using the provided config
612    *GLOBAL_SANITIZER.lock().unwrap() = ErrorSanitizer::with_config(config);
613}
614
615/// Get access to the global error sanitizer
616pub fn with_global_sanitizer<F, R>(f: F) -> R
617where
618    F: FnOnce(&mut ErrorSanitizer) -> R,
619{
620    let mut sanitizer = GLOBAL_SANITIZER.lock().unwrap();
621    f(&mut *sanitizer)
622}
623
624/// Quick sanitization functions using global sanitizer
625pub fn sanitize_error<E>(error: E, context: ErrorContext) -> SanitizedError
626where
627    E: std::error::Error + fmt::Display + fmt::Debug,
628{
629    with_global_sanitizer(|sanitizer| sanitizer.sanitize(error, context))
630}
631
632/// Sanitize IO error for safe external reporting
633pub fn sanitize_io_error<E>(error: E, context: ErrorContext) -> SanitizedError
634where
635    E: std::error::Error + fmt::Display + fmt::Debug,
636{
637    with_global_sanitizer(|sanitizer| sanitizer.sanitize_io_error(error, context))
638}
639
640/// Sanitize parse error for safe external reporting
641pub fn sanitize_parse_error<E>(error: E) -> SanitizedError
642where
643    E: std::error::Error + fmt::Display + fmt::Debug,
644{
645    with_global_sanitizer(|sanitizer| sanitizer.sanitize_parse_error(error))
646}
647
648/// Sanitize build error for safe external reporting
649pub fn sanitize_build_error<E>(error: E) -> SanitizedError
650where
651    E: std::error::Error + fmt::Display + fmt::Debug,
652{
653    with_global_sanitizer(|sanitizer| sanitizer.sanitize_build_error(error))
654}
655
656/// Sanitize security error for safe external reporting
657pub fn sanitize_security_error<E>(error: E) -> SanitizedError
658where
659    E: std::error::Error + fmt::Display + fmt::Debug,
660{
661    with_global_sanitizer(|sanitizer| sanitizer.sanitize_security_error(error))
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use std::io::{Error, ErrorKind};
668
669    #[test]
670    fn test_secure_error_trait() {
671        struct TestError {
672            message: String,
673            context: ErrorContext,
674        }
675
676        impl fmt::Display for TestError {
677            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678                write!(f, "{}", self.message)
679            }
680        }
681
682        impl fmt::Debug for TestError {
683            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684                write!(
685                    f,
686                    "TestError {{ message: {:?}, context: {:?} }}",
687                    self.message, self.context
688                )
689            }
690        }
691
692        impl std::error::Error for TestError {}
693
694        impl SecureError for TestError {
695            fn public_message(&self) -> String {
696                "Operation failed".to_string()
697            }
698
699            fn internal_message(&self) -> String {
700                self.message.clone()
701            }
702
703            fn debug_message(&self) -> String {
704                format!("{:?}", self)
705            }
706
707            fn error_level(&self) -> ErrorLevel {
708                ErrorLevel::Internal
709            }
710
711            fn error_context(&self) -> ErrorContext {
712                self.context
713            }
714        }
715
716        let error = TestError {
717            message: "Detailed error with /path/to/file.txt".to_string(),
718            context: ErrorContext::FileRead,
719        };
720
721        assert_eq!(error.public_message(), "Operation failed");
722        assert!(error.internal_message().contains("/path/to/file.txt"));
723        assert_eq!(error.error_level(), ErrorLevel::Internal);
724        assert_eq!(error.error_context(), ErrorContext::FileRead);
725    }
726
727    #[test]
728    fn test_redaction_rules() {
729        let rule = RedactionRule::new(
730            "test_paths",
731            r"/[^/\s]+/[^/\s]+",
732            "<redacted path>",
733            true,
734            true,
735            false,
736        )
737        .unwrap();
738
739        let message = "Failed to open /home/user/secret.txt";
740        let redacted = rule.apply(message);
741        assert_eq!(redacted, "Failed to open <redacted path>/secret.txt");
742
743        assert!(rule.applies_to_mode(ErrorMode::Production));
744        assert!(rule.applies_to_mode(ErrorMode::Development));
745        assert!(!rule.applies_to_mode(ErrorMode::Testing));
746    }
747
748    #[test]
749    fn test_error_sanitizer_production_mode() {
750        let config = SanitizerConfig {
751            mode: ErrorMode::Production,
752            generate_correlation_ids: true,
753            log_internal_details: false, // Don't spam logs in tests
754            max_message_length: 100,
755            include_error_codes: true,
756        };
757
758        let mut sanitizer = ErrorSanitizer::with_config(config);
759        let io_error = Error::new(
760            ErrorKind::NotFound,
761            "File not found: /home/user/secrets.txt",
762        );
763
764        let sanitized = sanitizer.sanitize_io_error(io_error, ErrorContext::FileOpen);
765
766        assert_eq!(sanitized.message, "File operation failed");
767        assert_eq!(sanitized.code, Some("E1001".to_string()));
768        assert!(sanitized.context.is_some());
769        assert!(!sanitized.correlation_id.is_empty());
770    }
771
772    #[test]
773    fn test_error_sanitizer_development_mode() {
774        let config = SanitizerConfig {
775            mode: ErrorMode::Development,
776            generate_correlation_ids: true,
777            log_internal_details: false,
778            max_message_length: 200,
779            include_error_codes: true,
780        };
781
782        let mut sanitizer = ErrorSanitizer::with_config(config);
783        let io_error = Error::new(
784            ErrorKind::PermissionDenied,
785            "Permission denied: /etc/shadow",
786        );
787
788        let sanitized = sanitizer.sanitize_io_error(io_error, ErrorContext::FileRead);
789
790        // Should be more descriptive in development mode
791        assert!(sanitized.message.contains("file"));
792        assert_eq!(sanitized.code, Some("E1002".to_string()));
793        assert!(sanitized.context.is_some());
794    }
795
796    #[test]
797    fn test_path_redaction() {
798        let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
799            mode: ErrorMode::Production,
800            ..SanitizerConfig::default()
801        });
802
803        let error = Error::new(
804            ErrorKind::NotFound,
805            "Cannot find /Users/john/Documents/secret.pdf",
806        );
807        let sanitized = sanitizer.sanitize_io_error(error, ErrorContext::FileOpen);
808
809        // In production mode, should get generic message
810        assert_eq!(sanitized.message, "File operation failed");
811    }
812
813    #[test]
814    fn test_ip_address_redaction() {
815        let rule = RedactionRule::new(
816            "test_ips",
817            r"\b(?:\d{1,3}\.){3}\d{1,3}\b",
818            "<ip>",
819            true,
820            true,
821            true,
822        )
823        .unwrap();
824
825        let message = "Connection failed to 192.168.1.1:8080";
826        let redacted = rule.apply(message);
827        assert_eq!(redacted, "Connection failed to <ip>:8080");
828    }
829
830    #[test]
831    fn test_memory_address_redaction() {
832        let rule = RedactionRule::new(
833            "test_memory",
834            r"0x[0-9a-fA-F]+",
835            "<addr>",
836            true,
837            true,
838            false,
839        )
840        .unwrap();
841
842        let message = "Segfault at address 0x7fff5fbff000";
843        let redacted = rule.apply(message);
844        assert_eq!(redacted, "Segfault at address <addr>");
845    }
846
847    #[test]
848    fn test_api_key_redaction() {
849        let rule = RedactionRule::new(
850            "test_keys",
851            r#"(?i)(api_?key|token)[\s]*[:=][\s]*"?[a-zA-Z0-9\-_]{16,}"?"#,
852            "$1=<redacted>",
853            true,
854            true,
855            true,
856        )
857        .unwrap();
858
859        let message = r#"Authentication failed: api_key="sk_test_123456789abcdefghij""#;
860        let redacted = rule.apply(message);
861        assert!(redacted.contains("api_key=<redacted>"));
862        assert!(!redacted.contains("sk_test_123456789abcdefghij"));
863    }
864
865    #[test]
866    fn test_context_specific_sanitization() {
867        let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
868            mode: ErrorMode::Production,
869            ..SanitizerConfig::default()
870        });
871
872        // Test different contexts
873        let contexts = vec![
874            (ErrorContext::XmlParsing, "Invalid XML structure"),
875            (ErrorContext::XmlBuilding, "XML generation failed"),
876            (
877                ErrorContext::SecurityValidation,
878                "Security validation failed",
879            ),
880            (ErrorContext::Authentication, "Authentication failed"),
881            (ErrorContext::Authorization, "Access denied"),
882        ];
883
884        for (context, expected) in contexts {
885            let error = Error::new(
886                ErrorKind::InvalidInput,
887                "Detailed error message with /path/to/file.txt",
888            );
889            let sanitized = sanitizer.sanitize_io_error(error, context);
890            assert_eq!(sanitized.message, expected);
891        }
892    }
893
894    #[test]
895    fn test_message_length_truncation() {
896        let config = SanitizerConfig {
897            mode: ErrorMode::Testing, // Allow full message to test truncation
898            max_message_length: 20,
899            ..SanitizerConfig::default()
900        };
901
902        let mut sanitizer = ErrorSanitizer::with_config(config);
903        let long_error = Error::new(
904            ErrorKind::Other,
905            "This is a very long error message that should be truncated.",
906        );
907
908        let sanitized = sanitizer.sanitize_io_error(long_error, ErrorContext::FileRead);
909        assert!(sanitized.message.len() <= 20);
910        assert!(sanitized.message.ends_with("..."));
911    }
912
913    #[test]
914    fn test_correlation_id_generation() {
915        let mut sanitizer = ErrorSanitizer::with_config(SanitizerConfig {
916            generate_correlation_ids: true,
917            ..SanitizerConfig::default()
918        });
919
920        let error1 = Error::new(ErrorKind::NotFound, "Error 1");
921        let error2 = Error::new(ErrorKind::NotFound, "Error 2");
922
923        let sanitized1 = sanitizer.sanitize_io_error(error1, ErrorContext::FileOpen);
924        let sanitized2 = sanitizer.sanitize_io_error(error2, ErrorContext::FileOpen);
925
926        assert_ne!(sanitized1.correlation_id, sanitized2.correlation_id);
927        assert!(!sanitized1.correlation_id.is_empty());
928        assert!(!sanitized2.correlation_id.is_empty());
929    }
930
931    #[test]
932    fn test_error_codes() {
933        let sanitizer = ErrorSanitizer::new();
934        let stats = sanitizer.get_statistics();
935
936        assert_eq!(
937            stats.mode,
938            if cfg!(debug_assertions) {
939                ErrorMode::Development
940            } else {
941                ErrorMode::Production
942            }
943        );
944        assert!(stats.active_rules > 0);
945        assert_eq!(stats.stored_errors, 0);
946    }
947
948    #[test]
949    fn test_global_sanitizer() {
950        let error = Error::new(
951            ErrorKind::PermissionDenied,
952            "Access denied to /secret/file.txt",
953        );
954        let sanitized = sanitize_io_error(error, ErrorContext::FileRead);
955
956        assert!(!sanitized.correlation_id.is_empty());
957        assert!(!sanitized.message.is_empty());
958        assert!(sanitized.code.is_some());
959    }
960}