): string}> */ private const VALUE_PATTERNS = [ // Bearer header value, with or without the keyword. Replaces the // value but keeps the kind prefix as a triage breadcrumb. ['/(Bearer\s+irdb_(?:rep|con|adm|svc)_)[A-Z2-7]{32}/', '$1***'], // SEC_REVIEW F65: Bearer with any non-trivial value. The // floor was {20,} which let a < 20-char Bearer slip through; // dropped to {8,} which still excludes the common literal // strings without false-positive matching prose. ['/(Bearer\s+)[A-Za-z0-9._\-]{8,}/', '$1***'], // SEC_REVIEW F65: raw JWT (`header.payload.signature`) // anywhere in the message or value. Anchored on `eyJ` // because every JWT header is the base64url encoding of a // JSON object that starts with `{"…`, which is `eyJ…`. // Anchoring eliminates false positives like `192.168.1.1` // or `lib.so.6` — those don't start with `eyJ`. Each // segment requires ≥4 chars to skip pathological short // matches. The replacement keeps the `eyJ` prefix as a // triage breadcrumb. ['/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/', 'eyJ***'], // Bare irdb__<32 base32> tokens that aren't preceded by Bearer. ['/\birdb_(rep|con|adm|svc)_[A-Z2-7]{32}\b/', 'irdb_$1_***'], // Argon2 password hashes. ['/\$argon2(?:i|id|d)\$[^\s\'"]+/', '$argon2***'], // bcrypt password hashes. ['/\$2[aby]?\$\d{2}\$[A-Za-z0-9.\/]{53}/', '$2***'], ]; public function __invoke(LogRecord $record): LogRecord { $context = self::scrubArray($record->context); $extra = self::scrubArray($record->extra); $message = self::scrubString($record->message); return $record->with(message: $message, context: $context, extra: $extra); } /** * @param array $data * @return array */ private static function scrubArray(array $data): array { $out = []; foreach ($data as $key => $value) { $keyHit = is_string($key) && self::isSensitiveKey($key); if ($keyHit) { $out[$key] = self::REDACTED; continue; } if (is_array($value)) { $out[$key] = self::scrubArray($value); } elseif (is_string($value)) { $out[$key] = self::scrubString($value); } else { $out[$key] = $value; } } return $out; } private static function isSensitiveKey(string $key): bool { $lower = strtolower($key); foreach (self::SENSITIVE_KEY_NEEDLES as $needle) { if (str_contains($lower, $needle)) { return true; } } return false; } private static function scrubString(string $value): string { foreach (self::VALUE_PATTERNS as [$pattern, $replacement]) { $value = (string) preg_replace($pattern, (string) $replacement, $value); } return $value; } }