security_events/
mobile_redaction.rs1use crate::event::{EventValue, SecurityEvent};
14use std::collections::BTreeMap;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum LogLevel {
21 Trace = 0,
23 Debug = 1,
25 Info = 2,
27 Warn = 3,
29 Error = 4,
31}
32
33#[derive(Clone, Debug)]
38pub struct LogLevelEnforcer {
39 min_level: LogLevel,
40}
41
42impl LogLevelEnforcer {
43 #[must_use]
45 pub fn release() -> Self {
46 Self {
47 min_level: LogLevel::Info,
48 }
49 }
50
51 #[must_use]
53 pub fn debug() -> Self {
54 Self {
55 min_level: LogLevel::Trace,
56 }
57 }
58
59 #[must_use]
61 pub fn with_min_level(min_level: LogLevel) -> Self {
62 Self { min_level }
63 }
64
65 #[must_use]
68 pub fn should_emit(&self, level: LogLevel) -> bool {
69 level >= self.min_level
70 }
71}
72
73const NON_DEVICE_UUID_KEYS: &[&str] = &[
75 "event_id",
76 "parent_event_id",
77 "request_id",
78 "trace_id",
79 "correlation_id",
80 "session_id",
81 "transaction_id",
82 "span_id",
83];
84
85#[derive(Clone, Debug)]
95pub struct MobileRedactionEngine {
96 _private: (),
97}
98
99impl MobileRedactionEngine {
100 #[must_use]
102 pub fn new() -> Self {
103 Self { _private: () }
104 }
105
106 #[must_use]
109 pub fn scrub_event(&self, mut event: SecurityEvent) -> SecurityEvent {
110 let mut new_labels = BTreeMap::new();
111 for (key, value) in event.labels {
112 match value {
113 EventValue::Classified {
114 value: v,
115 classification,
116 } => {
117 let scrubbed = self.scrub_value(&key, &v);
118 new_labels.insert(
119 key,
120 EventValue::Classified {
121 value: scrubbed,
122 classification,
123 },
124 );
125 }
126 }
127 }
128 event.labels = new_labels;
129 event
130 }
131
132 fn scrub_value(&self, key: &str, value: &str) -> String {
134 let trimmed = value.trim();
135
136 if is_imei(trimmed) {
138 return "[DEVICE_ID_REDACTED]".to_string();
139 }
140
141 if is_mac_address(trimmed) {
143 return "[DEVICE_ID_REDACTED]".to_string();
144 }
145
146 if is_gps_coordinates(trimmed) {
148 return "[LOCATION_REDACTED]".to_string();
149 }
150
151 if is_uuid(trimmed) && is_device_id_key(key) {
153 return if is_advertising_id_key(key) {
154 "[AD_ID_REDACTED]".to_string()
155 } else {
156 "[DEVICE_ID_REDACTED]".to_string()
157 };
158 }
159
160 value.to_string()
161 }
162}
163
164impl Default for MobileRedactionEngine {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170fn is_imei(s: &str) -> bool {
172 s.len() == 15 && s.bytes().all(|b| b.is_ascii_digit())
173}
174
175fn is_mac_address(s: &str) -> bool {
178 if s.len() != 17 {
179 return false;
180 }
181 let bytes = s.as_bytes();
182 let separator = bytes[2];
183 if separator != b':' && separator != b'-' {
184 return false;
185 }
186 for (i, &b) in bytes.iter().enumerate() {
187 let pos_in_group = i % 3;
188 if pos_in_group == 2 {
189 if i == 17 - 1 {
190 if !b.is_ascii_hexdigit() {
192 return false;
193 }
194 } else if b != separator {
195 return false;
196 }
197 } else if !b.is_ascii_hexdigit() {
198 return false;
199 }
200 }
201 true
202}
203
204fn is_gps_coordinates(s: &str) -> bool {
207 let parts: Vec<&str> = s.split(',').collect();
208 if parts.len() != 2 {
209 return false;
210 }
211 is_decimal_coordinate(parts[0].trim()) && is_decimal_coordinate(parts[1].trim())
212}
213
214fn is_decimal_coordinate(s: &str) -> bool {
217 let s = s.strip_prefix('-').unwrap_or(s);
218 let dot_parts: Vec<&str> = s.split('.').collect();
219 if dot_parts.len() != 2 {
220 return false;
221 }
222 let integer = dot_parts[0];
223 let fraction = dot_parts[1];
224 if integer.is_empty() || fraction.is_empty() {
225 return false;
226 }
227 if integer.len() > 3 || integer.is_empty() {
229 return false;
230 }
231 integer.bytes().all(|b| b.is_ascii_digit()) && fraction.bytes().all(|b| b.is_ascii_digit())
232}
233
234fn is_uuid(s: &str) -> bool {
237 if s.len() != 36 {
238 return false;
239 }
240 let bytes = s.as_bytes();
241 if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
243 return false;
244 }
245 for (i, &b) in bytes.iter().enumerate() {
246 if i == 8 || i == 13 || i == 18 || i == 23 {
247 continue;
248 }
249 if !b.is_ascii_hexdigit() {
250 return false;
251 }
252 }
253 true
254}
255
256fn is_device_id_key(key: &str) -> bool {
258 let lower = key.to_ascii_lowercase();
259 if NON_DEVICE_UUID_KEYS.iter().any(|&k| lower == k) {
261 return false;
262 }
263 lower.contains("idfv")
264 || lower.contains("idfa")
265 || lower.contains("gaid")
266 || lower.contains("ad_id")
267 || lower.contains("advertising")
268 || lower.contains("device")
269 || lower.contains("vendor")
270 || lower.contains("hardware")
271}
272
273fn is_advertising_id_key(key: &str) -> bool {
275 let lower = key.to_ascii_lowercase();
276 lower.contains("ad_id")
277 || lower.contains("idfa")
278 || lower.contains("gaid")
279 || lower.contains("advertising")
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_is_imei() {
288 assert!(is_imei("353456789012345"));
289 assert!(!is_imei("12345678901234")); assert!(!is_imei("1234567890123456")); assert!(!is_imei("35345678901234a")); }
293
294 #[test]
295 fn test_is_mac_address() {
296 assert!(is_mac_address("AA:BB:CC:DD:EE:FF"));
297 assert!(is_mac_address("aa:bb:cc:dd:ee:ff"));
298 assert!(is_mac_address("AA-BB-CC-DD-EE-FF"));
299 assert!(!is_mac_address("AA:BB:CC:DD:EE")); assert!(!is_mac_address("AABBCCDDEEFF")); assert!(!is_mac_address("GG:BB:CC:DD:EE:FF")); }
303
304 #[test]
305 fn test_is_gps_coordinates() {
306 assert!(is_gps_coordinates("37.7749, -122.4194"));
307 assert!(is_gps_coordinates("-33.8688, 151.2093"));
308 assert!(is_gps_coordinates("0.0, 0.0"));
309 assert!(!is_gps_coordinates("hello, world"));
310 assert!(!is_gps_coordinates("37.7749"));
311 assert!(!is_gps_coordinates(""));
312 }
313
314 #[test]
315 fn test_is_uuid() {
316 assert!(is_uuid("E621E1F8-C36C-495A-93FC-0C247A3E6E5F"));
317 assert!(is_uuid("38400000-8cf0-11bd-b23e-10b96e40000d"));
318 assert!(!is_uuid("not-a-uuid"));
319 assert!(!is_uuid("E621E1F8C36C495A93FC0C247A3E6E5F")); }
321
322 #[test]
323 fn test_log_level_ordering() {
324 assert!(LogLevel::Error > LogLevel::Warn);
325 assert!(LogLevel::Warn > LogLevel::Info);
326 assert!(LogLevel::Info > LogLevel::Debug);
327 assert!(LogLevel::Debug > LogLevel::Trace);
328 }
329}