//! Redaction policy: the catalogue of patterns and structural rules //! the redactor applies. use once_cell::sync::Lazy; use regex::Regex; /// A single regex-driven rule. #[derive(Debug, Clone)] pub struct RedactionRule { pub id: &'static str, pub description: &'static str, pub pattern: Regex, /// If `Some(n)`, the rule replaces capture-group `n` rather than /// the whole match. Useful for "ts3server://host?password=XXXX" /// where the host should be preserved but `XXXX` redacted. pub redact_group: Option, } impl RedactionRule { pub fn new( id: &'static str, description: &'static str, pattern: &str, ) -> Self { Self { id, description, pattern: Regex::new(pattern).expect("static rule regex must compile"), redact_group: None, } } pub fn new_group( id: &'static str, description: &'static str, pattern: &str, group: usize, ) -> Self { Self { id, description, pattern: Regex::new(pattern).expect("static rule regex must compile"), redact_group: Some(group), } } } /// The full redaction policy. #[derive(Debug, Clone)] pub struct RedactionPolicy { pub rules: Vec, /// Structured-field redaction: any key whose lower-cased name /// contains one of these substrings is fully redacted in /// diagnostic bundles. Mirrors the bundle policy in /// `diagnostic-redaction-audit-report.md` §5. pub redact_field_substrings: Vec<&'static str>, /// Bundle-level: include chat messages? Audit-report default = No. pub include_chat: bool, /// Bundle-level: include channel tree details? pub include_channel_tree: bool, } /// Hard upper bound on protocol-string lengths kept in diagnostics. /// /// REDACT-TC-009 ("long hostile protocol string"): truncate or safely /// escape. The PoC truncates with a clear marker. pub const MAX_PROTOCOL_STRING_LEN: usize = 256; impl RedactionPolicy { /// The default policy used by the PoC. Mirrors the audit-report /// catalogue. Production code (`chanora_diagnostics`) will own the /// canonical version of this list. pub fn default_policy() -> Self { Self { rules: DEFAULT_RULES.clone(), redact_field_substrings: vec![ "password", "secret", "private_key", "private-key", "identity_key", "identity-key", "token", "credential", "passphrase", ], include_chat: false, include_channel_tree: false, } } } static DEFAULT_RULES: Lazy> = Lazy::new(|| { vec![ // Identity secret as it appears in `tsclientlib` style: // long base64-ish run preceded by a known marker. // We err on the side of catching base64 blobs >= 64 chars. RedactionRule::new( "identity-base64-blob", "Long base64-like run; matches identity secrets and tokens.", r"\b[A-Za-z0-9+/]{64,}={0,2}\b", ), // `password = "..."` / `password: "..."` / `password=...` // Captures the value in group 1 so the *key* name is kept. // The value excludes separators commonly found in URL query // strings and config lines (`& , ; " whitespace`). RedactionRule::new_group( "password-kv", "key=value style password assignment.", r#"(?i)\b(?:password|passwd|pass|pwd)\s*[:=]\s*"?([^"\s,;&]+)"?"#, 1, ), // ts3server://host?password=XXXX&... RedactionRule::new_group( "ts3server-url-password", "Password embedded in a ts3server:// URL query string.", r"(?i)(?:[?&])password=([^&\s]+)", 1, ), // Generic Authorization: Bearer ... RedactionRule::new_group( "authorization-bearer", "Authorization header bearer token.", r"(?i)Authorization:\s*Bearer\s+(\S+)", 1, ), // Linux user home: /home//... → minimize the username // segment. Captures group 1 = username. RedactionRule::new_group( "linux-home-path", "Linux home-directory path; minimizes the username segment.", r"(/home/)([^/\s]+)", 2, ), // Windows user: C:\Users\\... RedactionRule::new_group( "windows-user-path", "Windows user-profile path; minimizes the username segment.", r"(?i)([A-Z]:\\Users\\)([^\\\s]+)", 2, ), // macOS user: /Users//... RedactionRule::new_group( "macos-user-path", "macOS user-profile path; minimizes the username segment.", r"(/Users/)([^/\s]+)", 2, ), ] });