1use std::fmt;
34
35use crate::security::SecurityProfile;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40pub enum FieldSensitivity {
41 Public,
43 Sensitive,
45 PII,
47 Secret,
49}
50
51impl fmt::Display for FieldSensitivity {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Public => write!(f, "public"),
55 Self::Sensitive => write!(f, "sensitive"),
56 Self::PII => write!(f, "pii"),
57 Self::Secret => write!(f, "secret"),
58 }
59 }
60}
61
62#[derive(Debug)]
64pub struct FieldMasker;
65
66impl FieldMasker {
67 #[must_use]
69 pub fn detect_sensitivity(field_name: &str) -> FieldSensitivity {
70 let lower = field_name.to_lowercase();
71
72 if lower.starts_with("password")
74 || lower.starts_with("secret")
75 || lower.starts_with("token")
76 || lower.contains("token") || lower.starts_with("key")
78 || lower.starts_with("api_key")
79 || lower.starts_with("api_secret")
80 || lower.starts_with("auth")
81 || lower.starts_with("oauth")
82 || lower == "hash"
83 || lower == "signature"
84 || lower.contains("webhook_secret")
85 || lower.contains("private_key")
86 || lower.contains("certificate")
87 || lower.contains("tls_secret")
88 || lower.contains("encryption_key")
89 || lower.contains("database_url")
90 || lower.contains("connection_string")
91 || lower.contains("access_token")
92 || lower == "jwt"
94 || lower.starts_with("jwt_")
95 || lower.ends_with("_jwt")
96 || lower == "nonce"
97 || lower.starts_with("nonce_")
98 || lower.ends_with("_nonce")
99 || lower == "bearer"
100 || lower.starts_with("bearer_")
101 || lower == "client_secret"
102 || lower.contains("client_secret")
103 {
104 return FieldSensitivity::Secret;
105 }
106
107 if lower == "ssn"
109 || lower == "social_security_number"
110 || lower.contains("credit_card")
111 || lower.contains("card_number")
112 || lower == "cvv"
113 || lower == "cvc"
114 || lower.contains("bank_account")
115 || lower == "pin"
116 || lower.contains("driver_license")
117 || lower.contains("driver's_license")
118 || lower.contains("passport")
119 || lower == "date_of_birth"
120 || lower == "dob"
121 || lower.contains("maiden_name")
122 || lower.contains("mother's_name")
123 || lower.contains("routing_number")
124 || lower.contains("swift_code")
125 || lower == "iban"
126 || lower.contains("health_record")
127 || lower.contains("medical_record")
128 || lower.contains("state_id")
129 || lower.contains("drivers_license_number")
130 {
131 return FieldSensitivity::PII;
132 }
133
134 if lower == "email"
136 || lower.starts_with("email_")
137 || lower.ends_with("_email")
138 || lower == "phone"
139 || lower == "phone_number"
140 || lower.starts_with("phone_")
141 || lower == "mobile"
142 || lower.starts_with("mobile_")
143 || lower == "telephone"
144 || lower == "fax"
145 || lower.contains("ip_address")
146 || lower.contains("ipaddress")
147 || lower == "mac_address"
148 || lower == "macaddress"
149 || lower == "username"
150 || lower.starts_with("username_")
151 || lower.contains("login_name")
152 || lower.contains("im_handle")
153 || lower.contains("slack_id")
154 || lower.contains("twitter_handle")
155 || lower.contains("billing_address")
156 || lower.contains("shipping_address")
157 || lower.contains("home_address")
158 || lower.contains("work_address")
159 || lower.contains("zip_code")
160 || lower.contains("postal_code")
161 || lower.contains("ssn_last_four")
162 {
163 return FieldSensitivity::Sensitive;
164 }
165
166 FieldSensitivity::Public
168 }
169
170 #[must_use]
172 pub fn mask_value(value: &str, sensitivity: FieldSensitivity) -> String {
173 match sensitivity {
174 FieldSensitivity::Public => value.to_string(),
175 FieldSensitivity::Sensitive => Self::mask_sensitive(value),
176 FieldSensitivity::PII => Self::mask_pii(value),
177 FieldSensitivity::Secret => Self::mask_secret(value),
178 }
179 }
180
181 fn mask_sensitive(value: &str) -> String {
183 if value.is_empty() {
184 "***".to_string()
185 } else {
186 let first_char = value.chars().next().unwrap_or('*');
187 format!("{first_char}***")
188 }
189 }
190
191 fn mask_pii(_value: &str) -> String {
193 "[PII]".to_string()
194 }
195
196 fn mask_secret(_value: &str) -> String {
198 "****".to_string()
199 }
200
201 #[must_use]
203 pub fn should_mask(sensitivity: FieldSensitivity, profile: &SecurityProfile) -> bool {
204 match profile {
205 SecurityProfile::Standard => false,
206 SecurityProfile::Regulated => sensitivity != FieldSensitivity::Public,
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
220 fn test_id_is_public() {
221 assert_eq!(FieldMasker::detect_sensitivity("id"), FieldSensitivity::Public);
222 }
223
224 #[test]
225 fn test_name_is_public() {
226 assert_eq!(FieldMasker::detect_sensitivity("name"), FieldSensitivity::Public);
227 }
228
229 #[test]
230 fn test_title_is_public() {
231 assert_eq!(FieldMasker::detect_sensitivity("title"), FieldSensitivity::Public);
232 }
233
234 #[test]
235 fn test_description_is_public() {
236 assert_eq!(FieldMasker::detect_sensitivity("description"), FieldSensitivity::Public);
237 }
238
239 #[test]
240 fn test_created_at_is_public() {
241 assert_eq!(FieldMasker::detect_sensitivity("created_at"), FieldSensitivity::Public);
242 }
243
244 #[test]
249 fn test_email_is_sensitive() {
250 assert_eq!(FieldMasker::detect_sensitivity("email"), FieldSensitivity::Sensitive);
251 }
252
253 #[test]
254 fn test_email_address_is_sensitive() {
255 assert_eq!(FieldMasker::detect_sensitivity("email_address"), FieldSensitivity::Sensitive);
256 }
257
258 #[test]
259 fn test_user_email_is_sensitive() {
260 assert_eq!(FieldMasker::detect_sensitivity("user_email"), FieldSensitivity::Sensitive);
261 }
262
263 #[test]
264 fn test_phone_is_sensitive() {
265 assert_eq!(FieldMasker::detect_sensitivity("phone"), FieldSensitivity::Sensitive);
266 }
267
268 #[test]
269 fn test_phone_number_is_sensitive() {
270 assert_eq!(FieldMasker::detect_sensitivity("phone_number"), FieldSensitivity::Sensitive);
271 }
272
273 #[test]
274 fn test_mobile_is_sensitive() {
275 assert_eq!(FieldMasker::detect_sensitivity("mobile"), FieldSensitivity::Sensitive);
276 }
277
278 #[test]
279 fn test_mobile_phone_is_sensitive() {
280 assert_eq!(FieldMasker::detect_sensitivity("mobile_phone"), FieldSensitivity::Sensitive);
281 }
282
283 #[test]
284 fn test_ip_address_is_sensitive() {
285 assert_eq!(FieldMasker::detect_sensitivity("ip_address"), FieldSensitivity::Sensitive);
286 }
287
288 #[test]
289 fn test_mac_address_is_sensitive() {
290 assert_eq!(FieldMasker::detect_sensitivity("mac_address"), FieldSensitivity::Sensitive);
291 }
292
293 #[test]
298 fn test_ssn_is_pii() {
299 assert_eq!(FieldMasker::detect_sensitivity("ssn"), FieldSensitivity::PII);
300 }
301
302 #[test]
303 fn test_social_security_is_pii() {
304 assert_eq!(
305 FieldMasker::detect_sensitivity("social_security_number"),
306 FieldSensitivity::PII
307 );
308 }
309
310 #[test]
311 fn test_credit_card_is_pii() {
312 assert_eq!(FieldMasker::detect_sensitivity("credit_card"), FieldSensitivity::PII);
313 }
314
315 #[test]
316 fn test_card_number_is_pii() {
317 assert_eq!(FieldMasker::detect_sensitivity("card_number"), FieldSensitivity::PII);
318 }
319
320 #[test]
321 fn test_cvv_is_pii() {
322 assert_eq!(FieldMasker::detect_sensitivity("cvv"), FieldSensitivity::PII);
323 }
324
325 #[test]
326 fn test_cvc_is_pii() {
327 assert_eq!(FieldMasker::detect_sensitivity("cvc"), FieldSensitivity::PII);
328 }
329
330 #[test]
331 fn test_bank_account_is_pii() {
332 assert_eq!(FieldMasker::detect_sensitivity("bank_account"), FieldSensitivity::PII);
333 }
334
335 #[test]
336 fn test_pin_is_pii() {
337 assert_eq!(FieldMasker::detect_sensitivity("pin"), FieldSensitivity::PII);
338 }
339
340 #[test]
341 fn test_driver_license_is_pii() {
342 assert_eq!(FieldMasker::detect_sensitivity("driver_license"), FieldSensitivity::PII);
343 }
344
345 #[test]
346 fn test_passport_is_pii() {
347 assert_eq!(FieldMasker::detect_sensitivity("passport"), FieldSensitivity::PII);
348 }
349
350 #[test]
355 fn test_password_is_secret() {
356 assert_eq!(FieldMasker::detect_sensitivity("password"), FieldSensitivity::Secret);
357 }
358
359 #[test]
360 fn test_password_hash_is_secret() {
361 assert_eq!(FieldMasker::detect_sensitivity("password_hash"), FieldSensitivity::Secret);
362 }
363
364 #[test]
365 fn test_secret_is_secret() {
366 assert_eq!(FieldMasker::detect_sensitivity("secret"), FieldSensitivity::Secret);
367 }
368
369 #[test]
370 fn test_secret_key_is_secret() {
371 assert_eq!(FieldMasker::detect_sensitivity("secret_key"), FieldSensitivity::Secret);
372 }
373
374 #[test]
375 fn test_token_is_secret() {
376 assert_eq!(FieldMasker::detect_sensitivity("token"), FieldSensitivity::Secret);
377 }
378
379 #[test]
380 fn test_refresh_token_is_secret() {
381 assert_eq!(FieldMasker::detect_sensitivity("refresh_token"), FieldSensitivity::Secret);
382 }
383
384 #[test]
385 fn test_api_key_is_secret() {
386 assert_eq!(FieldMasker::detect_sensitivity("api_key"), FieldSensitivity::Secret);
387 }
388
389 #[test]
390 fn test_auth_token_is_secret() {
391 assert_eq!(FieldMasker::detect_sensitivity("auth_token"), FieldSensitivity::Secret);
392 }
393
394 #[test]
395 fn test_jwt_is_secret() {
396 assert_eq!(FieldMasker::detect_sensitivity("jwt"), FieldSensitivity::Secret);
397 }
398
399 #[test]
400 fn test_jwt_prefixed_is_secret() {
401 assert_eq!(FieldMasker::detect_sensitivity("jwt_token"), FieldSensitivity::Secret);
402 }
403
404 #[test]
405 fn test_id_jwt_is_secret() {
406 assert_eq!(FieldMasker::detect_sensitivity("id_jwt"), FieldSensitivity::Secret);
407 }
408
409 #[test]
410 fn test_nonce_is_secret() {
411 assert_eq!(FieldMasker::detect_sensitivity("nonce"), FieldSensitivity::Secret);
412 }
413
414 #[test]
415 fn test_nonce_value_is_secret() {
416 assert_eq!(FieldMasker::detect_sensitivity("nonce_value"), FieldSensitivity::Secret);
417 }
418
419 #[test]
420 fn test_bearer_is_secret() {
421 assert_eq!(FieldMasker::detect_sensitivity("bearer"), FieldSensitivity::Secret);
422 }
423
424 #[test]
425 fn test_client_secret_is_secret() {
426 assert_eq!(FieldMasker::detect_sensitivity("client_secret"), FieldSensitivity::Secret);
427 }
428
429 #[test]
430 fn test_oauth_client_secret_is_secret() {
431 assert_eq!(
432 FieldMasker::detect_sensitivity("oauth_client_secret"),
433 FieldSensitivity::Secret
434 );
435 }
436
437 #[test]
438 fn test_hash_is_secret() {
439 assert_eq!(FieldMasker::detect_sensitivity("hash"), FieldSensitivity::Secret);
440 }
441
442 #[test]
443 fn test_signature_is_secret() {
444 assert_eq!(FieldMasker::detect_sensitivity("signature"), FieldSensitivity::Secret);
445 }
446
447 #[test]
452 fn test_case_insensitive_email() {
453 assert_eq!(FieldMasker::detect_sensitivity("EMAIL"), FieldSensitivity::Sensitive);
454 }
455
456 #[test]
457 fn test_case_insensitive_password() {
458 assert_eq!(FieldMasker::detect_sensitivity("PASSWORD"), FieldSensitivity::Secret);
459 }
460
461 #[test]
462 fn test_mixed_case_ssn() {
463 assert_eq!(FieldMasker::detect_sensitivity("SSN"), FieldSensitivity::PII);
464 }
465
466 #[test]
471 fn test_public_value_unmasked() {
472 let result = FieldMasker::mask_value("value", FieldSensitivity::Public);
473 assert_eq!(result, "value");
474 }
475
476 #[test]
477 fn test_public_empty_string_unmasked() {
478 let result = FieldMasker::mask_value("", FieldSensitivity::Public);
479 assert_eq!(result, "");
480 }
481
482 #[test]
487 fn test_sensitive_email_masked() {
488 let result = FieldMasker::mask_value("user@example.com", FieldSensitivity::Sensitive);
489 assert_eq!(result, "u***");
490 }
491
492 #[test]
493 fn test_sensitive_phone_masked() {
494 let result = FieldMasker::mask_value("555-1234", FieldSensitivity::Sensitive);
495 assert_eq!(result, "5***");
496 }
497
498 #[test]
499 fn test_sensitive_single_char_masked() {
500 let result = FieldMasker::mask_value("a", FieldSensitivity::Sensitive);
501 assert_eq!(result, "a***");
502 }
503
504 #[test]
505 fn test_sensitive_empty_masked() {
506 let result = FieldMasker::mask_value("", FieldSensitivity::Sensitive);
507 assert_eq!(result, "***");
508 }
509
510 #[test]
515 fn test_pii_ssn_masked() {
516 let result = FieldMasker::mask_value("123-45-6789", FieldSensitivity::PII);
517 assert_eq!(result, "[PII]");
518 }
519
520 #[test]
521 fn test_pii_credit_card_masked() {
522 let result = FieldMasker::mask_value("4111-1111-1111-1111", FieldSensitivity::PII);
523 assert_eq!(result, "[PII]");
524 }
525
526 #[test]
527 fn test_pii_empty_masked() {
528 let result = FieldMasker::mask_value("", FieldSensitivity::PII);
529 assert_eq!(result, "[PII]");
530 }
531
532 #[test]
537 fn test_secret_password_masked() {
538 let result = FieldMasker::mask_value("mypassword123", FieldSensitivity::Secret);
539 assert_eq!(result, "****");
540 }
541
542 #[test]
543 fn test_secret_token_masked() {
544 let result = FieldMasker::mask_value("token_abc123xyz", FieldSensitivity::Secret);
545 assert_eq!(result, "****");
546 }
547
548 #[test]
549 fn test_secret_empty_masked() {
550 let result = FieldMasker::mask_value("", FieldSensitivity::Secret);
551 assert_eq!(result, "****");
552 }
553
554 #[test]
555 fn test_secret_any_value_masked() {
556 let result = FieldMasker::mask_value("anything", FieldSensitivity::Secret);
557 assert_eq!(result, "****");
558 }
559
560 #[test]
565 fn test_standard_profile_no_masking() {
566 let standard = SecurityProfile::standard();
567 assert!(!FieldMasker::should_mask(FieldSensitivity::Public, &standard));
568 assert!(!FieldMasker::should_mask(FieldSensitivity::Sensitive, &standard));
569 assert!(!FieldMasker::should_mask(FieldSensitivity::PII, &standard));
570 assert!(!FieldMasker::should_mask(FieldSensitivity::Secret, &standard));
571 }
572
573 #[test]
574 fn test_regulated_profile_public_no_masking() {
575 let regulated = SecurityProfile::regulated();
576 assert!(!FieldMasker::should_mask(FieldSensitivity::Public, ®ulated));
577 }
578
579 #[test]
580 fn test_regulated_profile_sensitive_masked() {
581 let regulated = SecurityProfile::regulated();
582 assert!(FieldMasker::should_mask(FieldSensitivity::Sensitive, ®ulated));
583 }
584
585 #[test]
586 fn test_regulated_profile_pii_masked() {
587 let regulated = SecurityProfile::regulated();
588 assert!(FieldMasker::should_mask(FieldSensitivity::PII, ®ulated));
589 }
590
591 #[test]
592 fn test_regulated_profile_secret_masked() {
593 let regulated = SecurityProfile::regulated();
594 assert!(FieldMasker::should_mask(FieldSensitivity::Secret, ®ulated));
595 }
596
597 #[test]
602 fn test_very_long_email_masked() {
603 let long_email = "a".repeat(1000) + "@example.com";
604 let result = FieldMasker::mask_value(&long_email, FieldSensitivity::Sensitive);
605 assert_eq!(result, "a***");
606 assert!(result.len() < long_email.len());
607 }
608
609 #[test]
610 fn test_unicode_email_masked() {
611 let result = FieldMasker::mask_value("émail@example.com", FieldSensitivity::Sensitive);
612 assert_eq!(result, "é***");
613 }
614
615 #[test]
616 fn test_sensitivity_display() {
617 assert_eq!(FieldSensitivity::Public.to_string(), "public");
618 assert_eq!(FieldSensitivity::Sensitive.to_string(), "sensitive");
619 assert_eq!(FieldSensitivity::PII.to_string(), "pii");
620 assert_eq!(FieldSensitivity::Secret.to_string(), "secret");
621 }
622}