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,
}
}
}