Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
AuditDataSanitizer
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
3 / 3
14
100.00% covered (success)
100.00%
1 / 1
 sanitize
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 isSensitiveKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 sanitizeStringValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Audit\Application\Service;
8
9defined('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 */
19final 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}