use regex_lite::Regex; use std::sync::LazyLock; static URL_RE: LazyLock = LazyLock::new(|| { Regex::new(r#"(?i)\b[a-z][a-z0-9+.-]{1,20}://[^\s<>"']+"#).expect("valid URL regex") }); static PRIVATE_KEY_RE: LazyLock = LazyLock::new(|| { Regex::new(r"(?is)-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----") .expect("valid private-key regex") }); static BEARER_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+").expect("valid bearer regex")); static SECRET_RE: LazyLock = LazyLock::new(|| { Regex::new( r"(?i)\b(api[_-]?key|authorization|password|passwd|private[_-]?key|proxy[_-]?(password|username)|refresh[_-]?token|secret|token|username)\b\s*[:=]\s*[^\s,;]+", ) .expect("valid secret regex") }); static EMAIL_RE: LazyLock = LazyLock::new(|| { Regex::new(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b").expect("valid email regex") }); static UNIX_HOME_RE: LazyLock = LazyLock::new(|| Regex::new(r"/(Users|home)/[^/\s]+").expect("valid Unix home regex")); static WINDOWS_HOME_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)\b[A-Z]:\\Users\\[^\\\s]+").expect("valid Windows home regex")); static IPV4_RE: LazyLock = LazyLock::new(|| Regex::new(r"\b([0-9]{1,3}\.){3}[0-9]{1,3}\b").expect("valid IPv4 regex")); static DOMAIN_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?i)\b([a-z0-9-]+\.)+[a-z]{2,}\b").expect("valid domain regex")); static UUID_RE: LazyLock = LazyLock::new(|| { Regex::new(r"(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b") .expect("valid UUID regex") }); pub fn url_label(value: &str) -> String { url::Url::parse(value) .map(|parsed| format!("{}://", parsed.scheme())) .unwrap_or_else(|_| "".to_string()) } pub fn text(value: &str) -> String { let redacted = PRIVATE_KEY_RE.replace_all(value, ""); let redacted = URL_RE.replace_all(&redacted, ""); let redacted = BEARER_RE.replace_all(&redacted, "Bearer "); let redacted = SECRET_RE.replace_all(&redacted, ""); let redacted = EMAIL_RE.replace_all(&redacted, ""); let redacted = UNIX_HOME_RE.replace_all(&redacted, "/"); let redacted = WINDOWS_HOME_RE.replace_all(&redacted, ""); let redacted = IPV4_RE.replace_all(&redacted, ""); let redacted = DOMAIN_RE.replace_all(&redacted, ""); UUID_RE .replace_all(&redacted, "") .into_owned() } #[cfg(test)] mod tests { use super::*; #[test] fn redacts_sensitive_log_content() { let input = format!( concat!( "URL https://user:pass@example.com/callback?code=private\n", "Authorization: Bearer secret-token\n", "password=hunter2\n", "user@example.com /Users/alice/Library C:\\Users\\alice\\AppData\n", "exit 203.0.113.42\n", "-----BEGIN {0} KEY-----\nprivate-material\n-----END {0} KEY-----\n", ), "PRIVATE" ); let output = text(&input); for sensitive in [ "user:pass", "example.com", "private-material", "secret-token", "hunter2", "user@example.com", "alice", "203.0.113.42", ] { assert!(!output.contains(sensitive), "log output leaked {sensitive}"); } } #[test] fn url_labels_retain_only_the_scheme() { assert_eq!( url_label("https://user:pass@example.com/path?token=value"), "https://" ); assert_eq!(url_label("not a URL"), ""); } }