fraiseql_core/security/
field_masking.rs1use 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}