Masking brick

Корисний лог майже завжди тягне за собою персональні дані: email у context, пароль у exception message, токен у SQL. Ховати їх «магією» в ядрі небезпечно (або сховає зайве, або недостатньо). Тому маскування — окрема capability: ви явно задаєте правила, а sinks самі вирішують, чи застосовувати їх.

addMasking() на ConceptStack реєструє extension-data-masker як DataMaskerInterface і повертає MaskingBuilder. Це ще не маскує app.log: Logging / Database / Validation підключають masker через свій withMasking(). Рецепт — Логування і маскування.

Методи MaskingBuilder:

Метод Параметри Опис Залежність
setPatterns() array<string, string> $patterns Regex → заміна у рядках (message, trace, …) —
setKeyPatterns() list<string> $keyPatterns Regex на ім’я ключа; значення → *** —
setRules() list<class-string<DataMaskerRuleInterface>> $rules Власні rule-класи з контейнера —

setPatterns()

$stack->addMasking()
    ->setPatterns([
        '/[a-z0-9_\-\+\.]+@[a-z0-9\-]+\.[a-z]{2,}/i' => '***@***.***',
        '/\d{4}-\d{4}-\d{4}-\d{4}/' => '****-****-****-****',
    ]);

Коли секрет уже всередині тексту — exception message, stack trace, рядок SQL, чужий debug dump — key patterns безсилі: ключа email немає. String patterns шукають фрагмент у рядку і підставляють заміну. Потрібні саме тоді, коли дані «розчинились» у message, а не лежать у структурованому context.

setKeyPatterns()

$stack->addMasking()
    ->setKeyPatterns([
        '/^email$/i',
        '/.*password.*/i',
        '/.*token.*/i',
        '/.*secret.*/i',
    ]);

Для вашого коду це основний інструмент: ви самі кладете ['email' => $email] у $logger->info(...). Якщо ім’я ключа (або властивості об’єкта) матчить regex, ціле значення стає *** — без здогадок про формат пароля. Надійніше й простіше, ніж ловити email regex-ом у кожному рядку, коли структура вже є.

setRules()

$stack->addMasking()
    ->setKeyPatterns(['/^email$/i'])
    ->setRules([CardLastFourRule::class]);

Regex не покриває все (часткове маскування картки, умовна логіка по namespace). Тоді клас із DataMaskerRuleInterface (isSensitiveKey / apply) резолвиться з контейнера. Деталі контракту й приклад — Data Masker.

Підключення до sinks

Builder лише збирає правила. Щоб вони спрацювали на конкретному каналі, sink має викликати withMasking() (і тим самим require capability masking):

$stack->addMasking()
    ->setPatterns([ /* … */ ])
    ->setKeyPatterns([ /* … */ ]);

$stack->addLogging()
    ->toRotatingFile($rootPath . 'storage/logs/app.log')
    ->withMasking();

// пізніше, на діагностичних sinks:
// $stack->addValidation()->withLogging(...)->withMasking();
// $stack->addDatabase()->withQueryLogging(...)->withMasking();

Без withMasking() на Logging правила лежать у контейнері, але app-логер їх не застосовує — можна навмисно маскувати лише SQL, лишаючи app.log «сирим» у локальному дебагу (рідко, але можливо). Типовий рецепт маскує app-канал одразу з появою логера.

Що реєструє brick

  • DataMaskerInterface — shared masker з RegexDataMaskerRule (якщо є patterns / keyPatterns) і вашими setRules

Пакет не залежить від Monolog: Logging тягне masker опційно через factory. Прямий інжект у сервіси (audit, Slack) — теж валідний сценарій, див. extension page.

Рецепт — 8. Логування і маскування. Пакет — Data Masker. Sink — Logging brick (withMasking()).