1use std::fmt;
34
35use crate::security::SecurityProfile;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum FieldSensitivity {
40 Public,
42 Sensitive,
44 PII,
46 Secret,
48}
49
50impl fmt::Display for FieldSensitivity {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Public => write!(f, "public"),
54 Self::Sensitive => write!(f, "sensitive"),
55 Self::PII => write!(f, "pii"),
56 Self::Secret => write!(f, "secret"),
57 }
58 }
59}
60
61#[derive(Debug)]
63pub struct FieldMasker;
64
65impl FieldMasker {
66 #[must_use]
68 pub fn detect_sensitivity(field_name: &str) -> FieldSensitivity {
69 let lower = field_name.to_lowercase();
70
71 if lower.starts_with("password")
73 || lower.starts_with("secret")
74 || lower.starts_with("token")
75 || lower.contains("token") || lower.starts_with("key")
77 || lower.starts_with("api_key")
78 || lower.starts_with("api_secret")
79 || lower.starts_with("auth")
80 || lower.starts_with("oauth")
81 || lower == "hash"
82 || lower == "signature"
83 || lower.contains("webhook_secret")
84 || lower.contains("private_key")
85 || lower.contains("certificate")
86 || lower.contains("tls_secret")
87 || lower.contains("encryption_key")
88 || lower.contains("database_url")
89 || lower.contains("connection_string")
90 || lower.contains("access_token")
91 {
92 return FieldSensitivity::Secret;
93 }
94
95 if lower == "ssn"
97 || lower == "social_security_number"
98 || lower.contains("credit_card")
99 || lower.contains("card_number")
100 || lower == "cvv"
101 || lower == "cvc"
102 || lower.contains("bank_account")
103 || lower == "pin"
104 || lower.contains("driver_license")
105 || lower.contains("driver's_license")
106 || lower.contains("passport")
107 || lower == "date_of_birth"
108 || lower == "dob"
109 || lower.contains("maiden_name")
110 || lower.contains("mother's_name")
111 || lower.contains("routing_number")
112 || lower.contains("swift_code")
113 || lower == "iban"
114 || lower.contains("health_record")
115 || lower.contains("medical_record")
116 || lower.contains("state_id")
117 || lower.contains("drivers_license_number")
118 {
119 return FieldSensitivity::PII;
120 }
121
122 if lower == "email"
124 || lower.starts_with("email_")
125 || lower.ends_with("_email")
126 || lower == "phone"
127 || lower == "phone_number"
128 || lower.starts_with("phone_")
129 || lower == "mobile"
130 || lower.starts_with("mobile_")
131 || lower == "telephone"
132 || lower == "fax"
133 || lower.contains("ip_address")
134 || lower.contains("ipaddress")
135 || lower == "mac_address"
136 || lower == "macaddress"
137 || lower == "username"
138 || lower.starts_with("username_")
139 || lower.contains("login_name")
140 || lower.contains("im_handle")
141 || lower.contains("slack_id")
142 || lower.contains("twitter_handle")
143 || lower.contains("billing_address")
144 || lower.contains("shipping_address")
145 || lower.contains("home_address")
146 || lower.contains("work_address")
147 || lower.contains("zip_code")
148 || lower.contains("postal_code")
149 || lower.contains("ssn_last_four")
150 {
151 return FieldSensitivity::Sensitive;
152 }
153
154 FieldSensitivity::Public
156 }
157
158 #[must_use]
160 pub fn mask_value(value: &str, sensitivity: FieldSensitivity) -> String {
161 match sensitivity {
162 FieldSensitivity::Public => value.to_string(),
163 FieldSensitivity::Sensitive => Self::mask_sensitive(value),
164 FieldSensitivity::PII => Self::mask_pii(value),
165 FieldSensitivity::Secret => Self::mask_secret(value),
166 }
167 }
168
169 fn mask_sensitive(value: &str) -> String {
171 if value.is_empty() {
172 "***".to_string()
173 } else {
174 let first_char = value.chars().next().unwrap_or('*');
175 format!("{first_char}***")
176 }
177 }
178
179 fn mask_pii(_value: &str) -> String {
181 "[PII]".to_string()
182 }
183
184 fn mask_secret(_value: &str) -> String {
186 "****".to_string()
187 }
188
189 #[must_use]
191 pub fn should_mask(sensitivity: FieldSensitivity, profile: &SecurityProfile) -> bool {
192 match profile {
193 SecurityProfile::Standard => false,
194 SecurityProfile::Regulated => sensitivity != FieldSensitivity::Public,
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
208 fn test_id_is_public() {
209 assert_eq!(FieldMasker::detect_sensitivity("id"), FieldSensitivity::Public);
210 }
211
212 #[test]
213 fn test_name_is_public() {
214 assert_eq!(FieldMasker::detect_sensitivity("name"), FieldSensitivity::Public);
215 }
216
217 #[test]
218 fn test_title_is_public() {
219 assert_eq!(FieldMasker::detect_sensitivity("title"), FieldSensitivity::Public);
220 }
221
222 #[test]
223 fn test_description_is_public() {
224 assert_eq!(FieldMasker::detect_sensitivity("description"), FieldSensitivity::Public);
225 }
226
227 #[test]
228 fn test_created_at_is_public() {
229 assert_eq!(FieldMasker::detect_sensitivity("created_at"), FieldSensitivity::Public);
230 }
231
232 #[test]
237 fn test_email_is_sensitive() {
238 assert_eq!(FieldMasker::detect_sensitivity("email"), FieldSensitivity::Sensitive);
239 }
240
241 #[test]
242 fn test_email_address_is_sensitive() {
243 assert_eq!(FieldMasker::detect_sensitivity("email_address"), FieldSensitivity::Sensitive);
244 }
245
246 #[test]
247 fn test_user_email_is_sensitive() {
248 assert_eq!(FieldMasker::detect_sensitivity("user_email"), FieldSensitivity::Sensitive);
249 }
250
251 #[test]
252 fn test_phone_is_sensitive() {
253 assert_eq!(FieldMasker::detect_sensitivity("phone"), FieldSensitivity::Sensitive);
254 }
255
256 #[test]
257 fn test_phone_number_is_sensitive() {
258 assert_eq!(FieldMasker::detect_sensitivity("phone_number"), FieldSensitivity::Sensitive);
259 }
260
261 #[test]
262 fn test_mobile_is_sensitive() {
263 assert_eq!(FieldMasker::detect_sensitivity("mobile"), FieldSensitivity::Sensitive);
264 }
265
266 #[test]
267 fn test_mobile_phone_is_sensitive() {
268 assert_eq!(FieldMasker::detect_sensitivity("mobile_phone"), FieldSensitivity::Sensitive);
269 }
270
271 #[test]
272 fn test_ip_address_is_sensitive() {
273 assert_eq!(FieldMasker::detect_sensitivity("ip_address"), FieldSensitivity::Sensitive);
274 }
275
276 #[test]
277 fn test_mac_address_is_sensitive() {
278 assert_eq!(FieldMasker::detect_sensitivity("mac_address"), FieldSensitivity::Sensitive);
279 }
280
281 #[test]
286 fn test_ssn_is_pii() {
287 assert_eq!(FieldMasker::detect_sensitivity("ssn"), FieldSensitivity::PII);
288 }
289
290 #[test]
291 fn test_social_security_is_pii() {
292 assert_eq!(
293 FieldMasker::detect_sensitivity("social_security_number"),
294 FieldSensitivity::PII
295 );
296 }
297
298 #[test]
299 fn test_credit_card_is_pii() {
300 assert_eq!(FieldMasker::detect_sensitivity("credit_card"), FieldSensitivity::PII);
301 }
302
303 #[test]
304 fn test_card_number_is_pii() {
305 assert_eq!(FieldMasker::detect_sensitivity("card_number"), FieldSensitivity::PII);
306 }
307
308 #[test]
309 fn test_cvv_is_pii() {
310 assert_eq!(FieldMasker::detect_sensitivity("cvv"), FieldSensitivity::PII);
311 }
312
313 #[test]
314 fn test_cvc_is_pii() {
315 assert_eq!(FieldMasker::detect_sensitivity("cvc"), FieldSensitivity::PII);
316 }
317
318 #[test]
319 fn test_bank_account_is_pii() {
320 assert_eq!(FieldMasker::detect_sensitivity("bank_account"), FieldSensitivity::PII);
321 }
322
323 #[test]
324 fn test_pin_is_pii() {
325 assert_eq!(FieldMasker::detect_sensitivity("pin"), FieldSensitivity::PII);
326 }
327
328 #[test]
329 fn test_driver_license_is_pii() {
330 assert_eq!(FieldMasker::detect_sensitivity("driver_license"), FieldSensitivity::PII);
331 }
332
333 #[test]
334 fn test_passport_is_pii() {
335 assert_eq!(FieldMasker::detect_sensitivity("passport"), FieldSensitivity::PII);
336 }
337
338 #[test]
343 fn test_password_is_secret() {
344 assert_eq!(FieldMasker::detect_sensitivity("password"), FieldSensitivity::Secret);
345 }
346
347 #[test]
348 fn test_password_hash_is_secret() {
349 assert_eq!(FieldMasker::detect_sensitivity("password_hash"), FieldSensitivity::Secret);
350 }
351
352 #[test]
353 fn test_secret_is_secret() {
354 assert_eq!(FieldMasker::detect_sensitivity("secret"), FieldSensitivity::Secret);
355 }
356
357 #[test]
358 fn test_secret_key_is_secret() {
359 assert_eq!(FieldMasker::detect_sensitivity("secret_key"), FieldSensitivity::Secret);
360 }
361
362 #[test]
363 fn test_token_is_secret() {
364 assert_eq!(FieldMasker::detect_sensitivity("token"), FieldSensitivity::Secret);
365 }
366
367 #[test]
368 fn test_refresh_token_is_secret() {
369 assert_eq!(FieldMasker::detect_sensitivity("refresh_token"), FieldSensitivity::Secret);
370 }
371
372 #[test]
373 fn test_api_key_is_secret() {
374 assert_eq!(FieldMasker::detect_sensitivity("api_key"), FieldSensitivity::Secret);
375 }
376
377 #[test]
378 fn test_auth_token_is_secret() {
379 assert_eq!(FieldMasker::detect_sensitivity("auth_token"), FieldSensitivity::Secret);
380 }
381
382 #[test]
383 fn test_hash_is_secret() {
384 assert_eq!(FieldMasker::detect_sensitivity("hash"), FieldSensitivity::Secret);
385 }
386
387 #[test]
388 fn test_signature_is_secret() {
389 assert_eq!(FieldMasker::detect_sensitivity("signature"), FieldSensitivity::Secret);
390 }
391
392 #[test]
397 fn test_case_insensitive_email() {
398 assert_eq!(FieldMasker::detect_sensitivity("EMAIL"), FieldSensitivity::Sensitive);
399 }
400
401 #[test]
402 fn test_case_insensitive_password() {
403 assert_eq!(FieldMasker::detect_sensitivity("PASSWORD"), FieldSensitivity::Secret);
404 }
405
406 #[test]
407 fn test_mixed_case_ssn() {
408 assert_eq!(FieldMasker::detect_sensitivity("SSN"), FieldSensitivity::PII);
409 }
410
411 #[test]
416 fn test_public_value_unmasked() {
417 let result = FieldMasker::mask_value("value", FieldSensitivity::Public);
418 assert_eq!(result, "value");
419 }
420
421 #[test]
422 fn test_public_empty_string_unmasked() {
423 let result = FieldMasker::mask_value("", FieldSensitivity::Public);
424 assert_eq!(result, "");
425 }
426
427 #[test]
432 fn test_sensitive_email_masked() {
433 let result = FieldMasker::mask_value("user@example.com", FieldSensitivity::Sensitive);
434 assert_eq!(result, "u***");
435 }
436
437 #[test]
438 fn test_sensitive_phone_masked() {
439 let result = FieldMasker::mask_value("555-1234", FieldSensitivity::Sensitive);
440 assert_eq!(result, "5***");
441 }
442
443 #[test]
444 fn test_sensitive_single_char_masked() {
445 let result = FieldMasker::mask_value("a", FieldSensitivity::Sensitive);
446 assert_eq!(result, "a***");
447 }
448
449 #[test]
450 fn test_sensitive_empty_masked() {
451 let result = FieldMasker::mask_value("", FieldSensitivity::Sensitive);
452 assert_eq!(result, "***");
453 }
454
455 #[test]
460 fn test_pii_ssn_masked() {
461 let result = FieldMasker::mask_value("123-45-6789", FieldSensitivity::PII);
462 assert_eq!(result, "[PII]");
463 }
464
465 #[test]
466 fn test_pii_credit_card_masked() {
467 let result = FieldMasker::mask_value("4111-1111-1111-1111", FieldSensitivity::PII);
468 assert_eq!(result, "[PII]");
469 }
470
471 #[test]
472 fn test_pii_empty_masked() {
473 let result = FieldMasker::mask_value("", FieldSensitivity::PII);
474 assert_eq!(result, "[PII]");
475 }
476
477 #[test]
482 fn test_secret_password_masked() {
483 let result = FieldMasker::mask_value("mypassword123", FieldSensitivity::Secret);
484 assert_eq!(result, "****");
485 }
486
487 #[test]
488 fn test_secret_token_masked() {
489 let result = FieldMasker::mask_value("token_abc123xyz", FieldSensitivity::Secret);
490 assert_eq!(result, "****");
491 }
492
493 #[test]
494 fn test_secret_empty_masked() {
495 let result = FieldMasker::mask_value("", FieldSensitivity::Secret);
496 assert_eq!(result, "****");
497 }
498
499 #[test]
500 fn test_secret_any_value_masked() {
501 let result = FieldMasker::mask_value("anything", FieldSensitivity::Secret);
502 assert_eq!(result, "****");
503 }
504
505 #[test]
510 fn test_standard_profile_no_masking() {
511 let standard = SecurityProfile::standard();
512 assert!(!FieldMasker::should_mask(FieldSensitivity::Public, &standard));
513 assert!(!FieldMasker::should_mask(FieldSensitivity::Sensitive, &standard));
514 assert!(!FieldMasker::should_mask(FieldSensitivity::PII, &standard));
515 assert!(!FieldMasker::should_mask(FieldSensitivity::Secret, &standard));
516 }
517
518 #[test]
519 fn test_regulated_profile_public_no_masking() {
520 let regulated = SecurityProfile::regulated();
521 assert!(!FieldMasker::should_mask(FieldSensitivity::Public, ®ulated));
522 }
523
524 #[test]
525 fn test_regulated_profile_sensitive_masked() {
526 let regulated = SecurityProfile::regulated();
527 assert!(FieldMasker::should_mask(FieldSensitivity::Sensitive, ®ulated));
528 }
529
530 #[test]
531 fn test_regulated_profile_pii_masked() {
532 let regulated = SecurityProfile::regulated();
533 assert!(FieldMasker::should_mask(FieldSensitivity::PII, ®ulated));
534 }
535
536 #[test]
537 fn test_regulated_profile_secret_masked() {
538 let regulated = SecurityProfile::regulated();
539 assert!(FieldMasker::should_mask(FieldSensitivity::Secret, ®ulated));
540 }
541
542 #[test]
547 fn test_very_long_email_masked() {
548 let long_email = "a".repeat(1000) + "@example.com";
549 let result = FieldMasker::mask_value(&long_email, FieldSensitivity::Sensitive);
550 assert_eq!(result, "a***");
551 assert!(result.len() < long_email.len());
552 }
553
554 #[test]
555 fn test_unicode_email_masked() {
556 let result = FieldMasker::mask_value("émail@example.com", FieldSensitivity::Sensitive);
557 assert_eq!(result, "é***");
558 }
559
560 #[test]
561 fn test_sensitivity_display() {
562 assert_eq!(FieldSensitivity::Public.to_string(), "public");
563 assert_eq!(FieldSensitivity::Sensitive.to_string(), "sensitive");
564 assert_eq!(FieldSensitivity::PII.to_string(), "pii");
565 assert_eq!(FieldSensitivity::Secret.to_string(), "secret");
566 }
567}