redact($data); */ final readonly class SensitiveDataRedactor { /** * Liste der Keys, die immer redacted werden */ private const SENSITIVE_KEYS = [ 'password', 'passwd', 'pwd', 'secret', 'api_key', 'apikey', 'api_secret', 'apisecret', 'token', 'access_token', 'refresh_token', 'bearer', 'auth', 'authorization', 'private_key', 'privatekey', 'encryption_key', 'encryptionkey', 'session_id', 'sessionid', 'cookie', 'csrf', 'csrf_token', 'credit_card', 'creditcard', 'card_number', 'cardnumber', 'cvv', 'cvc', 'ssn', 'social_security', 'tax_id', 'passport', ]; /** * Regex-Patterns für Content-basierte Erkennung */ private const PATTERNS = [ // Kreditkarten (Visa, MasterCard, Amex, Discover) 'credit_card' => '/\b(?:\d{4}[-\s]?){3}\d{4}\b/', // Email-Adressen 'email' => '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', // Bearer Tokens (JWT-ähnlich) 'bearer_token' => '/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/', // API Keys (alphanumerische Strings mit mindestens 20 Zeichen) 'api_key' => '/\b[A-Za-z0-9]{20,}\b/', // IP-Adressen (IPv4) 'ipv4' => '/\b(?:\d{1,3}\.){3}\d{1,3}\b/', // Sozialversicherungsnummern (US Format: XXX-XX-XXXX) 'ssn' => '/\b\d{3}-\d{2}-\d{4}\b/', ]; public function __construct( private RedactionMode $mode = RedactionMode::PARTIAL, private bool $redactEmails = false, private bool $redactIps = false, private string $mask = '[REDACTED]' ) {} /** * Redacted sensitive Daten in einem Array rekursiv * * @param array $data * @return array */ public function redact(array $data): array { $result = []; foreach ($data as $key => $value) { $normalizedKey = $this->normalizeKey($key); // Key-basierte Redaction if ($this->isSensitiveKey($normalizedKey)) { $result[$key] = $this->maskValue($value); continue; } // Rekursive Redaction für Arrays if (is_array($value)) { $result[$key] = $this->redact($value); continue; } // Content-basierte Redaction für Strings if (is_string($value)) { $result[$key] = $this->redactContent($value); continue; } // Andere Werte unverändert $result[$key] = $value; } return $result; } /** * Redacted sensitive Daten in einem String */ public function redactString(string $content): string { return $this->redactContent($content); } /** * Prüft ob ein Key sensitiv ist */ private function isSensitiveKey(string $key): bool { return in_array($key, self::SENSITIVE_KEYS, true); } /** * Normalisiert einen Key für Vergleich (lowercase, underscores) */ private function normalizeKey(string $key): string { return strtolower(str_replace(['-', ' '], '_', $key)); } /** * Maskiert einen Wert basierend auf RedactionMode */ private function maskValue(mixed $value): string|array { // Arrays rekursiv redacten if (is_array($value)) { return array_map(fn($v) => $this->mask, $value); } // Strings basierend auf Mode maskieren if (is_string($value)) { return match ($this->mode) { RedactionMode::FULL => $this->mask, RedactionMode::PARTIAL => $this->partialMask($value), RedactionMode::HASH => $this->hashValue($value), }; } // Andere Typen vollständig redacten return $this->mask; } /** * Partial Masking: Zeige erste und letzte Zeichen */ private function partialMask(string $value): string { $length = mb_strlen($value); if ($length <= 4) { return str_repeat('*', $length); } $visible = 2; $start = mb_substr($value, 0, $visible); $end = mb_substr($value, -$visible); $masked = str_repeat('*', max(0, $length - ($visible * 2))); return $start . $masked . $end; } /** * Hash-basierte Redaction für deterministische Maskierung */ private function hashValue(string $value): string { $hash = hash('sha256', $value); return '[HASH:' . substr($hash, 0, 12) . ']'; } /** * Content-basierte Redaction mit Regex-Patterns */ private function redactContent(string $content): string { // Kreditkarten $content = preg_replace( self::PATTERNS['credit_card'], '[CREDIT_CARD]', $content ); // Bearer Tokens $content = preg_replace( self::PATTERNS['bearer_token'], 'Bearer [REDACTED]', $content ); // API Keys (nur wenn nicht bereits durch Key-Matching redacted) $content = preg_replace( self::PATTERNS['api_key'], '[API_KEY]', $content ); // SSN $content = preg_replace( self::PATTERNS['ssn'], '[SSN]', $content ); // Optional: Email-Adressen if ($this->redactEmails) { $content = preg_replace_callback( self::PATTERNS['email'], fn($matches) => $this->maskEmail($matches[0]), $content ); } // Optional: IP-Adressen if ($this->redactIps) { $content = preg_replace( self::PATTERNS['ipv4'], '[IP_ADDRESS]', $content ); } return $content; } /** * Maskiert Email-Adresse: john.doe@example.com → j***e@example.com */ private function maskEmail(string $email): string { [$local, $domain] = explode('@', $email, 2); $length = mb_strlen($local); if ($length <= 2) { $maskedLocal = str_repeat('*', $length); } else { $maskedLocal = mb_substr($local, 0, 1) . str_repeat('*', max(0, $length - 2)) . mb_substr($local, -1); } return $maskedLocal . '@' . $domain; } /** * Factory: Erstelle Redactor für Production (Full Redaction) */ public static function production(): self { return new self( mode: RedactionMode::FULL, redactEmails: true, redactIps: true ); } /** * Factory: Erstelle Redactor für Development (Partial Redaction) */ public static function development(): self { return new self( mode: RedactionMode::PARTIAL, redactEmails: false, redactIps: false ); } /** * Factory: Erstelle Redactor für Testing (Hash-based Redaction) */ public static function testing(): self { return new self( mode: RedactionMode::HASH, redactEmails: false, redactIps: false ); } }