<?php
 namespace App\Logging; final class SensitiveDataRedactor { public const PLACEHOLDER = '***REDACTED***'; private const SENSITIVE_KEY_SUBSTRINGS = [ 'card_number', 'cardnumber', 'card_no', 'cardno', 'cvv', 'cvc', 'cv2', 'card_cvv', 'cardcvv', 'card_holder', 'cardholder', 'cardholder_name', 'card_expiry', 'cardexpiry', 'card_exp', 'billing_cvc', 'billing_cvv', 'password', 'passwd', 'pwd', 'secret', 'api_secret', 'apisecret', 'client_secret', 'token', 'access_token', 'refresh_token', 'bearer', 'api_key', 'apikey', 'license_key', 'licensekey', 'private_key', 'privatekey', 'auth_token', 'cpanel_password', 'cpanelpassword', 'whm_password', 'whmpassword', 'plesk_password', 'pleskpassword', 'server_password', 'serverpassword', 'root_password', 'rootpassword', 'admin_password', 'adminpassword', 'mysql_password', 'mysqlpassword', 'db_password', 'dbpassword', 'ftp_password', 'ftppassword', 'mail_password', 'mailpassword', 'resetcode', 'reset_code', 'reset_token', 'verificationcode', 'verification_code', 'verify_code', 'otp', 'one_time_password', 'two_factor_secret', 'totp_secret', '2fa_secret', 'tckn', 'tc_kimlik', 'identification_number', 'iban', 'account_number', 'accountnumber', 'ssn', 'social_security', 'mc_data', 'minfraud_data', ]; private const SENSITIVE_KEY_EXACT = [ 'pan', 'key', 'auth', 'pin', 'pwd', ]; public static function redact(array $context): array { return self::walkArray($context); } public static function maskString(?string $value, int $visible = 2): string { $value = (string) ($value ?? ''); if ($value === '') { return ''; } if (mb_strlen($value) <= $visible) { return str_repeat('*', max(3, mb_strlen($value))); } return mb_substr($value, 0, $visible) . '***'; } public static function maskEmail(?string $email): string { $email = (string) ($email ?? ''); if ($email === '' || !str_contains($email, '@')) { return $email === '' ? '' : '***'; } [$local, $domain] = explode('@', $email, 2); $localMasked = mb_substr($local, 0, 1) . '***'; return $localMasked . '@' . $domain; } public static function maskIp(?string $ip): string { $ip = (string) ($ip ?? ''); if ($ip === '') { return ''; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $parts = explode('.', $ip); return $parts[0] . '.' . $parts[1] . '.x.x'; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $parts = explode(':', $ip); return ($parts[0] ?? '') . ':' . ($parts[1] ?? '') . '::***'; } return '***'; } public static function maskPhone(?string $phone): string { $phone = (string) ($phone ?? ''); $digits = preg_replace('/\D+/', '', $phone); if ($digits === '' || strlen($digits) < 4) { return $digits === '' ? '' : '***'; } $last4 = substr($digits, -4); return '***' . $last4; } public static function maskPan(?string $pan): string { $pan = (string) ($pan ?? ''); $digits = preg_replace('/\D+/', '', $pan); $len = strlen($digits); if ($len < 10) { return self::PLACEHOLDER; } return substr($digits, 0, 6) . str_repeat('*', $len - 10) . substr($digits, -4); } public static function maskName(?string $name): string { $name = trim((string) ($name ?? '')); if ($name === '') { return ''; } $parts = preg_split('/\s+/', $name) ?: []; if (count($parts) === 1) { return mb_substr($parts[0], 0, 1) . '***'; } $first = array_shift($parts); $rest = array_map(static fn (string $p) => mb_substr($p, 0, 1) . '***', $parts); return trim($first . ' ' . implode(' ', $rest)); } public static function isSensitiveKey(int|string $key): bool { if (is_int($key)) { return false; } $needle = strtolower($key); if (in_array($needle, self::SENSITIVE_KEY_EXACT, true)) { return true; } foreach (self::SENSITIVE_KEY_SUBSTRINGS as $pattern) { if (str_contains($needle, $pattern)) { return true; } } return false; } private static function walkArray(array $arr): array { $out = []; foreach ($arr as $key => $value) { if (self::isSensitiveKey($key)) { $out[$key] = self::PLACEHOLDER; continue; } if (is_array($value)) { $out[$key] = self::walkArray($value); continue; } $out[$key] = $value; } return $out; } } 