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