Data Masker
Пакет php-concept/extension-data-masker з’являється не
«для красивого API логера», а з конкретного болю: щойно ви пишете
корисні логи, у файл потрапляють email, паролі, токени, шматки
тіла запиту. Том storage/logs/, бекап у S3, доступ
усієї команди до Docker volume — і «дебаг» стає витоком.
Core і Logging не знають вашого списку секретів і не маскують «все підряд»: чужа магія або ховає занадто багато (немає чим дебажити), або замало (витік лишається). Data Masker — тонкий шар правил «що замінити», який sinks (Logging, пізніше validation/SQL log) викликають перед записом. Підключаєте, коли з’явився цей біль — у рецепті одразу з Monolog: Логування і маскування. API builder-а — Masking brick.
Коли потрібен і коли ні
- Потрібен, якщо в лог/debug потрапляє context з персональними полями, exception message з credentials, або SQL з літералами паролів.
- Не обов’язковий, поки логер пише лише безособові рядки без context (рідкісний випадок) або ви взагалі без Logging.
- Не замінює авторизацію доступу до логів і шифрування дисків — лише зменшує шкоду, коли файл уже хтось відкрив.
Збірка через Stack
У додатку capability реєструє addMasking(): у
контейнері з’являється DataMaskerInterface з
вашими правилами. Це ще не маскує логи — лише кладе
сервіс. Кожен sink сам каже «хочу masker»:
$stack->addMasking()
->setPatterns([
'/[a-z0-9_\-\+\.]+@[a-z0-9\-]+\.[a-z]{2,}/i' => '***@***.***',
])
->setKeyPatterns([
'/^email$/i',
'/.*password.*/i',
'/.*token.*/i',
]);
$stack->addLogging()
->toRotatingFile($rootPath . 'storage/logs/app.log')
->withMasking(); // require(LOGGING, MASKING)
Без withMasking() на Logging правила лежать у
контейнері марно — і це навмисно: той самий masker пізніше
можна повісити на Database
(withQueryLogging() + withMasking())
або Validation log, не чіпаючи інші sinks. Деталі app-каналу —
Logging brick
(withMasking()).
Методи MaskingBuilder:
| Метод | Параметри | Навіщо |
|---|---|---|
setPatterns() |
array<string, string> — regex → заміна |
Фрагменти вже «розчинені» в message / trace / SQL-рядку |
setKeyPatterns() |
list<string> — regex на ім’я ключа |
Ціле значення в array/object context за ім’ям поля |
setRules() |
list<class-string<DataMaskerRuleInterface>> |
Власна логіка, коли regex недостатньо |
Як працює mask()
Єдиний вхід — DataMaskerInterface::mask(mixed $data): mixed.
Якщо правил немає — дані повертаються як є (no-op). Інакше:
-
array — deep clone, для кожного ключа: якщо
isSensitiveKey($key)→ значення***; інакше рекурсія в значення - object — clone + reflection по властивостях (та сама логіка імен)
-
string — послідовно
apply()усіх rules (regex-заміни тощо) - інше (int, bool, null) — без змін
Logger Monolog вішає processor: маскує і
$record->message, і $record->context
перед handler. Тому key pattern на email ховає
context, а string pattern — email у тексті повідомлення.
// типовий результат після key pattern /^email$/i
$masker->mask(['email' => 'ada@example.com', 'ok' => 1]);
// ['email' => '***', 'ok' => 1]
// string pattern на адресу в тексті
$masker->mask('User ada@example.com failed');
// 'User ***@***.*** failed'
Key patterns vs string patterns
Секрети потрапляють у логи двома шляхами — тому й два механізми.
Key patterns — коли ваш код уже поклав дані в
структурований context:
$logger->info('…', ['email' => $email, 'password' => $pwd]).
Достатньо домовитись про імена полів (/^email$/i,
/.*password.*/i). Значення замінюється цілком на
*** — не треба вгадувати формат пароля. Це
найнадійніший шлях для вашого коду.
String patterns
(setPatterns(['/regex/' => 'replacement']) — коли
секрет уже всередині рядка: exception message, trace, чужий
SQL dump, "User ada@… failed". Тут key patterns
безсилі: ключа email немає. Regex шукає фрагмент
і підставляє заміну (наприклад email → ***@***.***).
На практиці майже завжди потрібні обидва: key — для вашого context, string — для того, що «просочилось» у текст. Рецепт кроку 8 показує обидва на contact-формі.
Правило за замовчуванням: RegexDataMaskerRule
Якщо передали patterns і/або keyPatterns,
ServiceProvider додає один
RegexDataMaskerRule:
-
isSensitiveKey()—preg_filterпо списку keyPatterns -
apply()—preg_replaceусіх pattern → replacement
Окремо реєструвати цей клас не потрібно — Stack робить це з аргументів builder-а.
Власні rules
Regex не покриває все (наприклад, маскувати лише останні 4
цифри картки за алгоритмом, або ховати поле лише в певному
namespace). Тоді реалізуєте
DataMaskerRuleInterface:
use Concept\Extensions\DataMasker\Contracts\DataMaskerRuleInterface;
final class CardLastFourRule implements DataMaskerRuleInterface
{
public function isSensitiveKey(string $key): bool
{
return (bool) preg_match('/^card_number$/i', $key);
}
public function apply(string $value): string
{
if (!preg_match('/^\d{12,19}$/', $value)) {
return $value;
}
return str_repeat('*', max(0, strlen($value) - 4)) . substr($value, -4);
}
}
$stack->addMasking()
->setKeyPatterns(['/^email$/i'])
->setRules([CardLastFourRule::class]);
Клас має бути резолвибельним з контейнера (автоwiring або
ваш provider). Rules застосовуються після (разом із)
Regex-правилом: sensitive key перевіряється по всіх rules;
для рядків — послідовний apply().
Пряме використання
Masker не прив’язаний лише до Monolog. Будь-який сервіс може
інжектити DataMaskerInterface і чистити дані перед
відправкою в зовнішній audit, Slack, telemetry:
public function __construct(
private readonly DataMaskerInterface $masker,
) {}
public function audit(array $payload): void
{
$safe = $this->masker->mask($payload);
// …
}
clearRules() на інтерфейсі — для тестів / рідкісних
сценаріїв скидання; у звичайному HTTP-запиті правила задає
Bootstrap один раз через Stack.
Склад пакета
Concept\Extensions\DataMasker\
├── Contracts/
│ ├── DataMaskerInterface.php # mask(), clearRules()
│ └── DataMaskerRuleInterface.php # isSensitiveKey(), apply()
├── DataMasker.php # обхід array/object/string
├── RegexDataMaskerRule.php # patterns + keyPatterns
└── DataMaskerServiceProvider.php # реєстрація в League Container
Залежності свідомо вузькі: PHP 8.4, League Container, подія
ExtensionAwakened. Немає прив’язки до Monolog —
Logging тягне masker опційно через factory.
Мінімальний чеклист правил
- Імена полів форм і API, які ви самі кладете в context
password,*token*,secret,authorization- Email / картка / телефон у string patterns, якщо вони бувають у message
- Перевірка: один POST з реальним email → у файлі
***, не адреса - Контрольна перевірка: прибрати
withMasking()→ email знову відкритий (доводить, що маскує саме цей ланцюг)
Практика в додатку —
8. Логування і маскування.
Stack —
Masking brick.
Sink —
Logging brick
(withMasking()). Наступні sinks (validation / SQL) —
Діагностичні логи.