1use std::fmt;
36
37use serde::{Deserialize, Serialize};
38
39use crate::security::errors::SecurityError;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[non_exhaustive]
46pub enum DetailLevel {
47 Development,
49
50 Staging,
52
53 Production,
55}
56
57impl fmt::Display for DetailLevel {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::Development => write!(f, "Development"),
61 Self::Staging => write!(f, "Staging"),
62 Self::Production => write!(f, "Production"),
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
71#[allow(clippy::struct_excessive_bools)] pub struct SanitizationConfig {
73 pub hide_database_urls: bool,
75
76 pub hide_sql: bool,
78
79 pub hide_paths: bool,
81
82 pub hide_ips: bool,
84
85 pub hide_emails: bool,
87
88 pub hide_credentials: bool,
90}
91
92impl SanitizationConfig {
93 #[must_use]
97 pub const fn permissive() -> Self {
98 Self {
99 hide_database_urls: false,
100 hide_sql: false,
101 hide_paths: false,
102 hide_ips: false,
103 hide_emails: false,
104 hide_credentials: false,
105 }
106 }
107
108 #[must_use]
112 pub const fn standard() -> Self {
113 Self {
114 hide_database_urls: true,
115 hide_sql: true,
116 hide_paths: false,
117 hide_ips: true,
118 hide_emails: true,
119 hide_credentials: true,
120 }
121 }
122
123 #[must_use]
127 pub const fn strict() -> Self {
128 Self {
129 hide_database_urls: true,
130 hide_sql: true,
131 hide_paths: true,
132 hide_ips: true,
133 hide_emails: true,
134 hide_credentials: true,
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
144pub struct ErrorFormatter {
145 detail_level: DetailLevel,
146 config: SanitizationConfig,
147}
148
149impl ErrorFormatter {
150 #[must_use]
152 pub const fn new(detail_level: DetailLevel) -> Self {
153 let config = Self::config_for_level(detail_level);
154 Self {
155 detail_level,
156 config,
157 }
158 }
159
160 #[must_use]
162 pub const fn with_config(detail_level: DetailLevel, config: SanitizationConfig) -> Self {
163 Self {
164 detail_level,
165 config,
166 }
167 }
168
169 #[must_use]
171 pub const fn development() -> Self {
172 Self::new(DetailLevel::Development)
173 }
174
175 #[must_use]
177 pub const fn staging() -> Self {
178 Self::new(DetailLevel::Staging)
179 }
180
181 #[must_use]
183 pub const fn production() -> Self {
184 Self::new(DetailLevel::Production)
185 }
186
187 const fn config_for_level(level: DetailLevel) -> SanitizationConfig {
189 match level {
190 DetailLevel::Development => SanitizationConfig::permissive(),
191 DetailLevel::Staging => SanitizationConfig::standard(),
192 DetailLevel::Production => SanitizationConfig::strict(),
193 }
194 }
195
196 #[must_use]
204 pub fn format_error(&self, error_msg: &str) -> String {
205 match self.detail_level {
206 DetailLevel::Development => {
207 error_msg.to_string()
209 },
210 DetailLevel::Staging => {
211 self.sanitize_error(error_msg)
213 },
214 DetailLevel::Production => {
215 if Self::is_security_related(error_msg) {
217 "Security validation failed".to_string()
218 } else {
219 "An error occurred while processing your request".to_string()
220 }
221 },
222 }
223 }
224
225 #[must_use]
227 pub fn format_security_error(&self, error: &SecurityError) -> String {
228 let error_msg = error.to_string();
229
230 match self.detail_level {
231 DetailLevel::Development => {
232 error_msg
234 },
235 DetailLevel::Staging => {
236 self.extract_error_type_and_sanitize(&error_msg)
238 },
239 DetailLevel::Production => {
240 match error {
242 SecurityError::AuthRequired => "Authentication required".to_string(),
243 SecurityError::InvalidToken
244 | SecurityError::TokenExpired { .. }
245 | SecurityError::TokenMissingClaim { .. }
246 | SecurityError::InvalidTokenAlgorithm { .. } => {
247 "Invalid authentication".to_string()
248 },
249 SecurityError::TlsRequired { .. }
250 | SecurityError::TlsVersionTooOld { .. }
251 | SecurityError::MtlsRequired { .. }
252 | SecurityError::InvalidClientCert { .. } => {
253 "Connection security validation failed".to_string()
254 },
255 SecurityError::QueryTooDeep { .. }
256 | SecurityError::QueryTooComplex { .. }
257 | SecurityError::QueryTooLarge { .. } => "Query validation failed".to_string(),
258 SecurityError::IntrospectionDisabled { .. } => {
259 "Schema introspection is not available".to_string()
260 },
261 _ => "An error occurred while processing your request".to_string(),
262 }
263 },
264 }
265 }
266
267 fn sanitize_error(&self, error_msg: &str) -> String {
269 let mut result = error_msg.to_string();
270
271 if self.config.hide_database_urls {
273 result = Self::hide_pattern(&result, "postgresql://", "**hidden**");
274 result = Self::hide_pattern(&result, "mysql://", "**hidden**");
275 result = Self::hide_pattern(&result, "mongodb://", "**hidden**");
276 }
277
278 if self.config.hide_sql {
280 result = Self::hide_pattern(&result, "SELECT ", "[SQL hidden]");
281 result = Self::hide_pattern(&result, "INSERT ", "[SQL hidden]");
282 result = Self::hide_pattern(&result, "UPDATE ", "[SQL hidden]");
283 result = Self::hide_pattern(&result, "DELETE ", "[SQL hidden]");
284 }
285
286 if self.config.hide_paths {
288 result = Self::redact_paths(&result);
289 }
290
291 if self.config.hide_ips {
293 result = Self::redact_ips(&result);
294 }
295
296 if self.config.hide_emails {
298 result = Self::redact_emails(&result);
299 }
300
301 if self.config.hide_credentials {
303 result = Self::hide_pattern(&result, "@", "[credentials redacted]");
304 }
305
306 result
307 }
308
309 fn is_security_related(error_msg: &str) -> bool {
311 let lower = error_msg.to_lowercase();
312 lower.contains("auth")
313 || lower.contains("permission")
314 || lower.contains("forbidden")
315 || lower.contains("security")
316 || lower.contains("tls")
317 || lower.contains("https")
318 }
319
320 fn extract_error_type_and_sanitize(&self, error_msg: &str) -> String {
322 let sanitized = self.sanitize_error(error_msg);
323
324 if sanitized.len() > 100 {
326 format!("{}...", &sanitized[..100])
327 } else {
328 sanitized
329 }
330 }
331
332 fn hide_pattern(text: &str, pattern: &str, replacement: &str) -> String {
334 if text.contains(pattern) {
335 text.replace(pattern, replacement)
336 } else {
337 text.to_string()
338 }
339 }
340
341 fn redact_paths(text: &str) -> String {
343 let mut result = text.to_string();
345
346 if result.contains('/') && result.contains(".rs") {
348 result = result.replace('/', "*");
349 }
350
351 if result.contains('\\') {
353 result = result.replace('\\', "*");
354 }
355
356 result
357 }
358
359 fn redact_ips(text: &str) -> String {
361 let mut result = String::new();
363 let mut current_word = String::new();
364
365 for c in text.chars() {
366 if c.is_numeric() || c == '.' {
367 current_word.push(c);
368 } else {
369 if Self::looks_like_ip(¤t_word) {
371 result.push_str("[IP]");
372 } else {
373 result.push_str(¤t_word);
374 }
375 current_word.clear();
376 result.push(c);
377 }
378 }
379
380 if Self::looks_like_ip(¤t_word) {
382 result.push_str("[IP]");
383 } else {
384 result.push_str(¤t_word);
385 }
386
387 result
388 }
389
390 fn redact_emails(text: &str) -> String {
392 let mut result = String::new();
394 let mut in_email = false;
395 let mut email = String::new();
396
397 for c in text.chars() {
398 if c == '@' {
399 in_email = true;
400 email.clear();
401 email.push(c);
402 } else if in_email {
403 email.push(c);
404 if c == ' ' || c == '\n' {
405 result.push_str("[email]");
406 result.push(c);
407 in_email = false;
408 email.clear();
409 }
410 } else {
411 result.push(c);
412 }
413 }
414
415 if in_email && email.contains('@') {
417 result.push_str("[email]");
418 } else {
419 result.push_str(&email);
420 }
421
422 result
423 }
424
425 fn looks_like_ip(s: &str) -> bool {
427 if !s.contains('.') {
428 return false;
429 }
430
431 let parts: Vec<&str> = s.split('.').collect();
432 if parts.len() != 4 {
433 return false;
434 }
435
436 parts.iter().all(|p| {
437 !p.is_empty()
438 && p.chars().all(|c| c.is_ascii_digit())
439 && p.parse::<u32>().unwrap_or(256) <= 255
440 })
441 }
442
443 #[must_use]
445 pub const fn detail_level(&self) -> DetailLevel {
446 self.detail_level
447 }
448
449 #[must_use]
451 pub const fn config(&self) -> &SanitizationConfig {
452 &self.config
453 }
454}