Skip to main content

memory_core/store/
privacy.rs

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