use std::fmt;
use serde::{Deserialize, Serialize};
use crate::security::errors::SecurityError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DetailLevel {
Development,
Staging,
Production,
}
impl fmt::Display for DetailLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Development => write!(f, "Development"),
Self::Staging => write!(f, "Staging"),
Self::Production => write!(f, "Production"),
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct SanitizationConfig {
pub hide_database_urls: bool,
pub hide_sql: bool,
pub hide_paths: bool,
pub hide_ips: bool,
pub hide_emails: bool,
pub hide_credentials: bool,
}
impl SanitizationConfig {
#[must_use]
pub const fn permissive() -> Self {
Self {
hide_database_urls: false,
hide_sql: false,
hide_paths: false,
hide_ips: false,
hide_emails: false,
hide_credentials: false,
}
}
#[must_use]
pub const fn standard() -> Self {
Self {
hide_database_urls: true,
hide_sql: true,
hide_paths: false,
hide_ips: true,
hide_emails: true,
hide_credentials: true,
}
}
#[must_use]
pub const fn strict() -> Self {
Self {
hide_database_urls: true,
hide_sql: true,
hide_paths: true,
hide_ips: true,
hide_emails: true,
hide_credentials: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorFormatter {
detail_level: DetailLevel,
config: SanitizationConfig,
}
impl ErrorFormatter {
#[must_use]
pub const fn new(detail_level: DetailLevel) -> Self {
let config = Self::config_for_level(detail_level);
Self {
detail_level,
config,
}
}
#[must_use]
pub const fn with_config(detail_level: DetailLevel, config: SanitizationConfig) -> Self {
Self {
detail_level,
config,
}
}
#[must_use]
pub const fn development() -> Self {
Self::new(DetailLevel::Development)
}
#[must_use]
pub const fn staging() -> Self {
Self::new(DetailLevel::Staging)
}
#[must_use]
pub const fn production() -> Self {
Self::new(DetailLevel::Production)
}
const fn config_for_level(level: DetailLevel) -> SanitizationConfig {
match level {
DetailLevel::Development => SanitizationConfig::permissive(),
DetailLevel::Staging => SanitizationConfig::standard(),
DetailLevel::Production => SanitizationConfig::strict(),
}
}
#[must_use]
pub fn format_error(&self, error_msg: &str) -> String {
match self.detail_level {
DetailLevel::Development => {
error_msg.to_string()
},
DetailLevel::Staging => {
self.sanitize_error(error_msg)
},
DetailLevel::Production => {
if Self::is_security_related(error_msg) {
"Security validation failed".to_string()
} else {
"An error occurred while processing your request".to_string()
}
},
}
}
#[must_use]
pub fn format_security_error(&self, error: &SecurityError) -> String {
let error_msg = error.to_string();
match self.detail_level {
DetailLevel::Development => {
error_msg
},
DetailLevel::Staging => {
self.extract_error_type_and_sanitize(&error_msg)
},
DetailLevel::Production => {
match error {
SecurityError::AuthRequired => "Authentication required".to_string(),
SecurityError::InvalidToken
| SecurityError::TokenExpired { .. }
| SecurityError::TokenMissingClaim { .. }
| SecurityError::InvalidTokenAlgorithm { .. } => {
"Invalid authentication".to_string()
},
SecurityError::TlsRequired { .. }
| SecurityError::TlsVersionTooOld { .. }
| SecurityError::MtlsRequired { .. }
| SecurityError::InvalidClientCert { .. } => {
"Connection security validation failed".to_string()
},
SecurityError::QueryTooDeep { .. }
| SecurityError::QueryTooComplex { .. }
| SecurityError::QueryTooLarge { .. } => "Query validation failed".to_string(),
SecurityError::IntrospectionDisabled { .. } => {
"Schema introspection is not available".to_string()
},
_ => "An error occurred while processing your request".to_string(),
}
},
}
}
fn sanitize_error(&self, error_msg: &str) -> String {
let mut result = error_msg.to_string();
if self.config.hide_database_urls {
result = Self::hide_pattern(&result, "postgresql://", "**hidden**");
result = Self::hide_pattern(&result, "mysql://", "**hidden**");
result = Self::hide_pattern(&result, "mongodb://", "**hidden**");
}
if self.config.hide_sql {
result = Self::hide_pattern(&result, "SELECT ", "[SQL hidden]");
result = Self::hide_pattern(&result, "INSERT ", "[SQL hidden]");
result = Self::hide_pattern(&result, "UPDATE ", "[SQL hidden]");
result = Self::hide_pattern(&result, "DELETE ", "[SQL hidden]");
}
if self.config.hide_paths {
result = Self::redact_paths(&result);
}
if self.config.hide_ips {
result = Self::redact_ips(&result);
}
if self.config.hide_emails {
result = Self::redact_emails(&result);
}
if self.config.hide_credentials {
result = Self::hide_pattern(&result, "@", "[credentials redacted]");
}
result
}
fn is_security_related(error_msg: &str) -> bool {
let lower = error_msg.to_lowercase();
lower.contains("auth")
|| lower.contains("permission")
|| lower.contains("forbidden")
|| lower.contains("security")
|| lower.contains("tls")
|| lower.contains("https")
}
fn extract_error_type_and_sanitize(&self, error_msg: &str) -> String {
let sanitized = self.sanitize_error(error_msg);
crate::utils::text::truncate_for_display(&sanitized, 100)
}
fn hide_pattern(text: &str, pattern: &str, replacement: &str) -> String {
if text.contains(pattern) {
text.replace(pattern, replacement)
} else {
text.to_string()
}
}
fn redact_paths(text: &str) -> String {
let mut result = text.to_string();
if result.contains('/') && result.contains(".rs") {
result = result.replace('/', "*");
}
if result.contains('\\') {
result = result.replace('\\', "*");
}
result
}
fn redact_ips(text: &str) -> String {
let mut result = String::new();
let mut current_word = String::new();
for c in text.chars() {
if c.is_numeric() || c == '.' {
current_word.push(c);
} else {
if Self::looks_like_ip(¤t_word) {
result.push_str("[IP]");
} else {
result.push_str(¤t_word);
}
current_word.clear();
result.push(c);
}
}
if Self::looks_like_ip(¤t_word) {
result.push_str("[IP]");
} else {
result.push_str(¤t_word);
}
result
}
fn redact_emails(text: &str) -> String {
let mut result = String::new();
let mut in_email = false;
let mut email = String::new();
for c in text.chars() {
if c == '@' {
in_email = true;
email.clear();
email.push(c);
} else if in_email {
email.push(c);
if c == ' ' || c == '\n' {
result.push_str("[email]");
result.push(c);
in_email = false;
email.clear();
}
} else {
result.push(c);
}
}
if in_email && email.contains('@') {
result.push_str("[email]");
} else {
result.push_str(&email);
}
result
}
fn looks_like_ip(s: &str) -> bool {
if !s.contains('.') {
return false;
}
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 4 {
return false;
}
parts.iter().all(|p| {
!p.is_empty()
&& p.chars().all(|c| c.is_ascii_digit())
&& p.parse::<u32>().unwrap_or(256) <= 255
})
}
#[must_use]
pub const fn detail_level(&self) -> DetailLevel {
self.detail_level
}
#[must_use]
pub const fn config(&self) -> &SanitizationConfig {
&self.config
}
}