memory_core/store/
privacy.rs1use 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}