use std::fmt;
use crate::security::SecurityProfile;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FieldSensitivity {
Public,
Sensitive,
PII,
Secret,
}
impl fmt::Display for FieldSensitivity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Public => write!(f, "public"),
Self::Sensitive => write!(f, "sensitive"),
Self::PII => write!(f, "pii"),
Self::Secret => write!(f, "secret"),
}
}
}
#[derive(Debug)]
pub struct FieldMasker;
impl FieldMasker {
#[must_use]
pub fn detect_sensitivity(field_name: &str) -> FieldSensitivity {
let lower = field_name.to_lowercase();
if lower.starts_with("password")
|| lower.starts_with("secret")
|| lower.starts_with("token")
|| lower.contains("token") || lower.starts_with("key")
|| lower.starts_with("api_key")
|| lower.starts_with("api_secret")
|| lower.starts_with("auth")
|| lower.starts_with("oauth")
|| lower == "hash"
|| lower == "signature"
|| lower.contains("webhook_secret")
|| lower.contains("private_key")
|| lower.contains("certificate")
|| lower.contains("tls_secret")
|| lower.contains("encryption_key")
|| lower.contains("database_url")
|| lower.contains("connection_string")
|| lower.contains("access_token")
|| lower == "jwt"
|| lower.starts_with("jwt_")
|| lower.ends_with("_jwt")
|| lower == "nonce"
|| lower.starts_with("nonce_")
|| lower.ends_with("_nonce")
|| lower == "bearer"
|| lower.starts_with("bearer_")
|| lower == "client_secret"
|| lower.contains("client_secret")
{
return FieldSensitivity::Secret;
}
if lower == "ssn"
|| lower == "social_security_number"
|| lower.contains("credit_card")
|| lower.contains("card_number")
|| lower == "cvv"
|| lower == "cvc"
|| lower.contains("bank_account")
|| lower == "pin"
|| lower.contains("driver_license")
|| lower.contains("driver's_license")
|| lower.contains("passport")
|| lower == "date_of_birth"
|| lower == "dob"
|| lower.contains("maiden_name")
|| lower.contains("mother's_name")
|| lower.contains("routing_number")
|| lower.contains("swift_code")
|| lower == "iban"
|| lower.contains("health_record")
|| lower.contains("medical_record")
|| lower.contains("state_id")
|| lower.contains("drivers_license_number")
{
return FieldSensitivity::PII;
}
if lower == "email"
|| lower.starts_with("email_")
|| lower.ends_with("_email")
|| lower == "phone"
|| lower == "phone_number"
|| lower.starts_with("phone_")
|| lower == "mobile"
|| lower.starts_with("mobile_")
|| lower == "telephone"
|| lower == "fax"
|| lower.contains("ip_address")
|| lower.contains("ipaddress")
|| lower == "mac_address"
|| lower == "macaddress"
|| lower == "username"
|| lower.starts_with("username_")
|| lower.contains("login_name")
|| lower.contains("im_handle")
|| lower.contains("slack_id")
|| lower.contains("twitter_handle")
|| lower.contains("billing_address")
|| lower.contains("shipping_address")
|| lower.contains("home_address")
|| lower.contains("work_address")
|| lower.contains("zip_code")
|| lower.contains("postal_code")
|| lower.contains("ssn_last_four")
{
return FieldSensitivity::Sensitive;
}
FieldSensitivity::Public
}
#[must_use]
pub fn mask_value(value: &str, sensitivity: FieldSensitivity) -> String {
match sensitivity {
FieldSensitivity::Public => value.to_string(),
FieldSensitivity::Sensitive => Self::mask_sensitive(value),
FieldSensitivity::PII => Self::mask_pii(value),
FieldSensitivity::Secret => Self::mask_secret(value),
}
}
fn mask_sensitive(value: &str) -> String {
if value.is_empty() {
"***".to_string()
} else {
let first_char = value.chars().next().unwrap_or('*');
format!("{first_char}***")
}
}
fn mask_pii(_value: &str) -> String {
"[PII]".to_string()
}
fn mask_secret(_value: &str) -> String {
"****".to_string()
}
#[must_use]
pub fn should_mask(sensitivity: FieldSensitivity, profile: &SecurityProfile) -> bool {
match profile {
SecurityProfile::Standard => false,
SecurityProfile::Regulated => sensitivity != FieldSensitivity::Public,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_is_public() {
assert_eq!(FieldMasker::detect_sensitivity("id"), FieldSensitivity::Public);
}
#[test]
fn test_name_is_public() {
assert_eq!(FieldMasker::detect_sensitivity("name"), FieldSensitivity::Public);
}
#[test]
fn test_title_is_public() {
assert_eq!(FieldMasker::detect_sensitivity("title"), FieldSensitivity::Public);
}
#[test]
fn test_description_is_public() {
assert_eq!(FieldMasker::detect_sensitivity("description"), FieldSensitivity::Public);
}
#[test]
fn test_created_at_is_public() {
assert_eq!(FieldMasker::detect_sensitivity("created_at"), FieldSensitivity::Public);
}
#[test]
fn test_email_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("email"), FieldSensitivity::Sensitive);
}
#[test]
fn test_email_address_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("email_address"), FieldSensitivity::Sensitive);
}
#[test]
fn test_user_email_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("user_email"), FieldSensitivity::Sensitive);
}
#[test]
fn test_phone_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("phone"), FieldSensitivity::Sensitive);
}
#[test]
fn test_phone_number_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("phone_number"), FieldSensitivity::Sensitive);
}
#[test]
fn test_mobile_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("mobile"), FieldSensitivity::Sensitive);
}
#[test]
fn test_mobile_phone_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("mobile_phone"), FieldSensitivity::Sensitive);
}
#[test]
fn test_ip_address_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("ip_address"), FieldSensitivity::Sensitive);
}
#[test]
fn test_mac_address_is_sensitive() {
assert_eq!(FieldMasker::detect_sensitivity("mac_address"), FieldSensitivity::Sensitive);
}
#[test]
fn test_ssn_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("ssn"), FieldSensitivity::PII);
}
#[test]
fn test_social_security_is_pii() {
assert_eq!(
FieldMasker::detect_sensitivity("social_security_number"),
FieldSensitivity::PII
);
}
#[test]
fn test_credit_card_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("credit_card"), FieldSensitivity::PII);
}
#[test]
fn test_card_number_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("card_number"), FieldSensitivity::PII);
}
#[test]
fn test_cvv_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("cvv"), FieldSensitivity::PII);
}
#[test]
fn test_cvc_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("cvc"), FieldSensitivity::PII);
}
#[test]
fn test_bank_account_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("bank_account"), FieldSensitivity::PII);
}
#[test]
fn test_pin_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("pin"), FieldSensitivity::PII);
}
#[test]
fn test_driver_license_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("driver_license"), FieldSensitivity::PII);
}
#[test]
fn test_passport_is_pii() {
assert_eq!(FieldMasker::detect_sensitivity("passport"), FieldSensitivity::PII);
}
#[test]
fn test_password_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("password"), FieldSensitivity::Secret);
}
#[test]
fn test_password_hash_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("password_hash"), FieldSensitivity::Secret);
}
#[test]
fn test_secret_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("secret"), FieldSensitivity::Secret);
}
#[test]
fn test_secret_key_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("secret_key"), FieldSensitivity::Secret);
}
#[test]
fn test_token_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("token"), FieldSensitivity::Secret);
}
#[test]
fn test_refresh_token_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("refresh_token"), FieldSensitivity::Secret);
}
#[test]
fn test_api_key_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("api_key"), FieldSensitivity::Secret);
}
#[test]
fn test_auth_token_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("auth_token"), FieldSensitivity::Secret);
}
#[test]
fn test_jwt_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("jwt"), FieldSensitivity::Secret);
}
#[test]
fn test_jwt_prefixed_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("jwt_token"), FieldSensitivity::Secret);
}
#[test]
fn test_id_jwt_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("id_jwt"), FieldSensitivity::Secret);
}
#[test]
fn test_nonce_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("nonce"), FieldSensitivity::Secret);
}
#[test]
fn test_nonce_value_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("nonce_value"), FieldSensitivity::Secret);
}
#[test]
fn test_bearer_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("bearer"), FieldSensitivity::Secret);
}
#[test]
fn test_client_secret_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("client_secret"), FieldSensitivity::Secret);
}
#[test]
fn test_oauth_client_secret_is_secret() {
assert_eq!(
FieldMasker::detect_sensitivity("oauth_client_secret"),
FieldSensitivity::Secret
);
}
#[test]
fn test_hash_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("hash"), FieldSensitivity::Secret);
}
#[test]
fn test_signature_is_secret() {
assert_eq!(FieldMasker::detect_sensitivity("signature"), FieldSensitivity::Secret);
}
#[test]
fn test_case_insensitive_email() {
assert_eq!(FieldMasker::detect_sensitivity("EMAIL"), FieldSensitivity::Sensitive);
}
#[test]
fn test_case_insensitive_password() {
assert_eq!(FieldMasker::detect_sensitivity("PASSWORD"), FieldSensitivity::Secret);
}
#[test]
fn test_mixed_case_ssn() {
assert_eq!(FieldMasker::detect_sensitivity("SSN"), FieldSensitivity::PII);
}
#[test]
fn test_public_value_unmasked() {
let result = FieldMasker::mask_value("value", FieldSensitivity::Public);
assert_eq!(result, "value");
}
#[test]
fn test_public_empty_string_unmasked() {
let result = FieldMasker::mask_value("", FieldSensitivity::Public);
assert_eq!(result, "");
}
#[test]
fn test_sensitive_email_masked() {
let result = FieldMasker::mask_value("user@example.com", FieldSensitivity::Sensitive);
assert_eq!(result, "u***");
}
#[test]
fn test_sensitive_phone_masked() {
let result = FieldMasker::mask_value("555-1234", FieldSensitivity::Sensitive);
assert_eq!(result, "5***");
}
#[test]
fn test_sensitive_single_char_masked() {
let result = FieldMasker::mask_value("a", FieldSensitivity::Sensitive);
assert_eq!(result, "a***");
}
#[test]
fn test_sensitive_empty_masked() {
let result = FieldMasker::mask_value("", FieldSensitivity::Sensitive);
assert_eq!(result, "***");
}
#[test]
fn test_pii_ssn_masked() {
let result = FieldMasker::mask_value("123-45-6789", FieldSensitivity::PII);
assert_eq!(result, "[PII]");
}
#[test]
fn test_pii_credit_card_masked() {
let result = FieldMasker::mask_value("4111-1111-1111-1111", FieldSensitivity::PII);
assert_eq!(result, "[PII]");
}
#[test]
fn test_pii_empty_masked() {
let result = FieldMasker::mask_value("", FieldSensitivity::PII);
assert_eq!(result, "[PII]");
}
#[test]
fn test_secret_password_masked() {
let result = FieldMasker::mask_value("mypassword123", FieldSensitivity::Secret);
assert_eq!(result, "****");
}
#[test]
fn test_secret_token_masked() {
let result = FieldMasker::mask_value("token_abc123xyz", FieldSensitivity::Secret);
assert_eq!(result, "****");
}
#[test]
fn test_secret_empty_masked() {
let result = FieldMasker::mask_value("", FieldSensitivity::Secret);
assert_eq!(result, "****");
}
#[test]
fn test_secret_any_value_masked() {
let result = FieldMasker::mask_value("anything", FieldSensitivity::Secret);
assert_eq!(result, "****");
}
#[test]
fn test_standard_profile_no_masking() {
let standard = SecurityProfile::standard();
assert!(!FieldMasker::should_mask(FieldSensitivity::Public, &standard));
assert!(!FieldMasker::should_mask(FieldSensitivity::Sensitive, &standard));
assert!(!FieldMasker::should_mask(FieldSensitivity::PII, &standard));
assert!(!FieldMasker::should_mask(FieldSensitivity::Secret, &standard));
}
#[test]
fn test_regulated_profile_public_no_masking() {
let regulated = SecurityProfile::regulated();
assert!(!FieldMasker::should_mask(FieldSensitivity::Public, ®ulated));
}
#[test]
fn test_regulated_profile_sensitive_masked() {
let regulated = SecurityProfile::regulated();
assert!(FieldMasker::should_mask(FieldSensitivity::Sensitive, ®ulated));
}
#[test]
fn test_regulated_profile_pii_masked() {
let regulated = SecurityProfile::regulated();
assert!(FieldMasker::should_mask(FieldSensitivity::PII, ®ulated));
}
#[test]
fn test_regulated_profile_secret_masked() {
let regulated = SecurityProfile::regulated();
assert!(FieldMasker::should_mask(FieldSensitivity::Secret, ®ulated));
}
#[test]
fn test_very_long_email_masked() {
let long_email = "a".repeat(1000) + "@example.com";
let result = FieldMasker::mask_value(&long_email, FieldSensitivity::Sensitive);
assert_eq!(result, "a***");
assert!(result.len() < long_email.len());
}
#[test]
fn test_unicode_email_masked() {
let result = FieldMasker::mask_value("émail@example.com", FieldSensitivity::Sensitive);
assert_eq!(result, "é***");
}
#[test]
fn test_sensitivity_display() {
assert_eq!(FieldSensitivity::Public.to_string(), "public");
assert_eq!(FieldSensitivity::Sensitive.to_string(), "sensitive");
assert_eq!(FieldSensitivity::PII.to_string(), "pii");
assert_eq!(FieldSensitivity::Secret.to_string(), "secret");
}
}