Skip to main content

memory_core/store/
privacy.rs

1use std::sync::{Mutex, OnceLock};
2
3use regex_lite::Regex;
4
5use crate::config::PrivacyConfig;
6
7static PRIVATE_TAG_RE: OnceLock<Regex> = OnceLock::new();
8static SECRET_PATTERNS_CACHE: OnceLock<Mutex<CachedPatterns>> = OnceLock::new();
9
10struct CachedPatterns {
11    source: Vec<String>,
12    compiled: Vec<Regex>,
13}
14
15pub fn strip_private_tags(content: &str) -> String {
16    let re = PRIVATE_TAG_RE.get_or_init(|| {
17        Regex::new(r"(?si)<private>.*?</private>").unwrap()
18    });
19    re.replace_all(content, "[REDACTED]").to_string()
20}
21
22pub fn strip_secrets(content: &str, config: &PrivacyConfig) -> String {
23    let compiled = get_or_compile_patterns(config);
24    let mut result = content.to_string();
25    for re in &compiled {
26        result = re.replace_all(&result, "[SECRET_REDACTED]").to_string();
27    }
28    result
29}
30
31fn get_or_compile_patterns(config: &PrivacyConfig) -> Vec<Regex> {
32    let current = effective_patterns(config);
33    let cache = SECRET_PATTERNS_CACHE.get_or_init(|| {
34        Mutex::new(CachedPatterns {
35            source: Vec::new(),
36            compiled: Vec::new(),
37        })
38    });
39    let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
40    if guard.source == current {
41        return guard.compiled.clone();
42    }
43    let compiled: Vec<Regex> = current
44        .iter()
45        .filter_map(|p| Regex::new(p).ok())
46        .collect();
47    guard.source = current;
48    guard.compiled = compiled.clone();
49    compiled
50}
51
52fn effective_patterns(config: &PrivacyConfig) -> Vec<String> {
53    if config.replace_defaults {
54        config.extra_patterns.clone()
55    } else {
56        let mut patterns = config.secret_patterns.clone();
57        patterns.extend(config.extra_patterns.clone());
58        patterns
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::config::PrivacyConfig;
66
67    #[test]
68    fn strips_private_tags() {
69        let input = "public info <private>secret stuff</private> more public";
70        assert_eq!(strip_private_tags(input), "public info [REDACTED] more public");
71    }
72
73    #[test]
74    fn strips_multiline_private_tags() {
75        let input = "before <private>\nline1\nline2\n</private> after";
76        assert_eq!(strip_private_tags(input), "before [REDACTED] after");
77    }
78
79    #[test]
80    fn detects_aws_key() {
81        let config = PrivacyConfig::default();
82        let input = "my key is AKIAIOSFODNN7EXAMPLE";
83        assert!(strip_secrets(input, &config).contains("[SECRET_REDACTED]"));
84    }
85
86    #[test]
87    fn detects_private_key_block() {
88        let config = PrivacyConfig::default();
89        let input = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIB...";
90        assert!(strip_secrets(input, &config).contains("[SECRET_REDACTED]"));
91    }
92
93    #[test]
94    fn detects_api_key_assignment() {
95        let config = PrivacyConfig::default();
96        let input = "config: api_key=abc123xyz";
97        assert!(strip_secrets(input, &config).contains("[SECRET_REDACTED]"));
98    }
99
100    #[test]
101    fn detects_connection_strings() {
102        let config = PrivacyConfig::default();
103        for input in [
104            "mongodb://user:pass@host:27017/db",
105            "postgres://user:pass@host/db",
106            "mysql://user:pass@host/db",
107            "redis://user:pass@host:6379",
108        ] {
109            assert!(
110                strip_secrets(input, &config).contains("[SECRET_REDACTED]"),
111                "Failed to detect: {input}"
112            );
113        }
114    }
115
116    #[test]
117    fn detects_github_token() {
118        let config = PrivacyConfig::default();
119        let input = "token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
120        assert!(strip_secrets(input, &config).contains("[SECRET_REDACTED]"));
121    }
122
123    #[test]
124    fn extra_patterns_additive() {
125        let mut config = PrivacyConfig::default();
126        config.extra_patterns.push(r"CUSTOM_[0-9]{6}".into());
127        let input = "custom: CUSTOM_123456";
128        assert!(strip_secrets(input, &config).contains("[SECRET_REDACTED]"));
129    }
130
131    #[test]
132    fn replace_defaults_removes_builtins() {
133        let mut config = PrivacyConfig::default();
134        config.replace_defaults = true;
135        config.extra_patterns.push(r"ONLY_THIS".into());
136        let input = "AKIAIOSFODNN7EXAMPLE should not be caught";
137        let result = strip_secrets(input, &config);
138        assert!(!result.contains("[SECRET_REDACTED]"));
139
140        let input2 = "ONLY_THIS should be caught";
141        assert!(strip_secrets(input2, &config).contains("[SECRET_REDACTED]"));
142    }
143}