Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
25 / 25 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
| AuditDataSanitizer | |
100.00% |
24 / 24 |
|
100.00% |
3 / 3 |
14 | |
100.00% |
1 / 1 |
| sanitize | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
| isSensitiveKey | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| sanitizeStringValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Audit\Application\Service; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | /** |
| 12 | * Audit Data Sanitizer and Sensitive Data Masker. |
| 13 | * |
| 14 | * Implements OWASP ASVS V7.1 / V8.1 controls ensuring passwords, API keys, credentials, |
| 15 | * and PII secrets are never written to audit logs or database records in plaintext. |
| 16 | * |
| 17 | * @package App\Core\Audit\Application\Service |
| 18 | */ |
| 19 | final class AuditDataSanitizer |
| 20 | { |
| 21 | /** |
| 22 | * Case-insensitive list of sensitive key names to redact. |
| 23 | */ |
| 24 | private const array SENSITIVE_KEYS = [ |
| 25 | 'password', |
| 26 | 'password_hash', |
| 27 | 'pass', |
| 28 | 'secret', |
| 29 | 'token', |
| 30 | 'api_key', |
| 31 | 'key_hash', |
| 32 | 'auth', |
| 33 | 'authorization', |
| 34 | 'bearer', |
| 35 | 'credit_card', |
| 36 | 'card_number', |
| 37 | 'cvv', |
| 38 | 'cvc', |
| 39 | 'ssn', |
| 40 | 'pin', |
| 41 | 'private_key', |
| 42 | 'smtp_password', |
| 43 | 'salt', |
| 44 | 'access_token', |
| 45 | 'refresh_token', |
| 46 | 'session_id', |
| 47 | ]; |
| 48 | |
| 49 | /** |
| 50 | * Redaction placeholder constant. |
| 51 | */ |
| 52 | public const string REDACTED_PLACEHOLDER = '[REDACTED]'; |
| 53 | |
| 54 | /** |
| 55 | * Recursively sanitizes an array, redacting sensitive fields. |
| 56 | * |
| 57 | * @param array<string, mixed> $data Data to sanitize. |
| 58 | * @return array<string, mixed> Sanitized data with secrets masked. |
| 59 | */ |
| 60 | public function sanitize(array $data): array |
| 61 | { |
| 62 | $sanitized = []; |
| 63 | |
| 64 | foreach ($data as $key => $value) { |
| 65 | $keyStr = (string) $key; |
| 66 | if ($this->isSensitiveKey($keyStr)) { |
| 67 | $sanitized[$key] = self::REDACTED_PLACEHOLDER; |
| 68 | continue; |
| 69 | } |
| 70 | |
| 71 | if (is_array($value)) { |
| 72 | $sanitized[$key] = $this->sanitize($value); |
| 73 | } elseif (is_string($value)) { |
| 74 | $sanitized[$key] = $this->sanitizeStringValue($value); |
| 75 | } else { |
| 76 | $sanitized[$key] = $value; |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | return $sanitized; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Checks if a field key is in the sensitive list. |
| 85 | * |
| 86 | * @param string $key Field key to check. |
| 87 | * @return bool True if field is sensitive. |
| 88 | */ |
| 89 | public function isSensitiveKey(string $key): bool |
| 90 | { |
| 91 | $lower = strtolower($key); |
| 92 | foreach (self::SENSITIVE_KEYS as $sensitive) { |
| 93 | if ($lower === $sensitive || str_contains($lower, $sensitive)) { |
| 94 | return true; |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return false; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Sanitizes string values (e.g. nested JSON strings or Authorization headers). |
| 103 | * |
| 104 | * @param string $value String value to inspect. |
| 105 | * @return string Sanitized string. |
| 106 | */ |
| 107 | private function sanitizeStringValue(string $value): string |
| 108 | { |
| 109 | if (str_starts_with(strtolower($value), 'bearer ')) { |
| 110 | return 'Bearer ' . self::REDACTED_PLACEHOLDER; |
| 111 | } |
| 112 | |
| 113 | if (str_starts_with($value, '{') && str_ends_with($value, '}')) { |
| 114 | $decoded = json_decode($value, true); |
| 115 | if (is_array($decoded)) { |
| 116 | return (string) json_encode($this->sanitize($decoded), JSON_UNESCAPED_UNICODE); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | return $value; |
| 121 | } |
| 122 | } |