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 crate::utils::text::truncate_for_display(&sanitized, 100)
327 }
328
329 fn hide_pattern(text: &str, pattern: &str, replacement: &str) -> String {
331 if text.contains(pattern) {
332 text.replace(pattern, replacement)
333 } else {
334 text.to_string()
335 }
336 }
337
338 fn redact_paths(text: &str) -> String {
340 let mut result = text.to_string();
342
343 if result.contains('/') && result.contains(".rs") {
345 result = result.replace('/', "*");
346 }
347
348 if result.contains('\\') {
350 result = result.replace('\\', "*");
351 }
352
353 result
354 }
355
356 fn redact_ips(text: &str) -> String {
358 let mut result = String::new();
360 let mut current_word = String::new();
361
362 for c in text.chars() {
363 if c.is_numeric() || c == '.' {
364 current_word.push(c);
365 } else {
366 if Self::looks_like_ip(¤t_word) {
368 result.push_str("[IP]");
369 } else {
370 result.push_str(¤t_word);
371 }
372 current_word.clear();
373 result.push(c);
374 }
375 }
376
377 if Self::looks_like_ip(¤t_word) {
379 result.push_str("[IP]");
380 } else {
381 result.push_str(¤t_word);
382 }
383
384 result
385 }
386
387 fn redact_emails(text: &str) -> String {
389 let mut result = String::new();
391 let mut in_email = false;
392 let mut email = String::new();
393
394 for c in text.chars() {
395 if c == '@' {
396 in_email = true;
397 email.clear();
398 email.push(c);
399 } else if in_email {
400 email.push(c);
401 if c == ' ' || c == '\n' {
402 result.push_str("[email]");
403 result.push(c);
404 in_email = false;
405 email.clear();
406 }
407 } else {
408 result.push(c);
409 }
410 }
411
412 if in_email && email.contains('@') {
414 result.push_str("[email]");
415 } else {
416 result.push_str(&email);
417 }
418
419 result
420 }
421
422 fn looks_like_ip(s: &str) -> bool {
424 if !s.contains('.') {
425 return false;
426 }
427
428 let parts: Vec<&str> = s.split('.').collect();
429 if parts.len() != 4 {
430 return false;
431 }
432
433 parts.iter().all(|p| {
434 !p.is_empty()
435 && p.chars().all(|c| c.is_ascii_digit())
436 && p.parse::<u32>().unwrap_or(256) <= 255
437 })
438 }
439
440 #[must_use]
442 pub const fn detail_level(&self) -> DetailLevel {
443 self.detail_level
444 }
445
446 #[must_use]
448 pub const fn config(&self) -> &SanitizationConfig {
449 &self.config
450 }
451}