HISTORY: історія входів, аудит алертів і команди, і одна помилкова тривога
Some checks failed
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 1m4s
CI / server (push) Successful in 1m10s
CI / dbtest (push) Failing after 14s
CI / agent (push) Successful in 2m34s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
byrsapty 2026-08-29 01:10:58 +03:00
parent 95a19b074d
commit 64a55d1f7e

View file

@ -8056,3 +8056,63 @@ ROADMAP називав три причини, з яких ламається с
пісочниці народжується чистою. І `журнал аудиту 0 → 0` — проба з
правилом `min`, тобто нуль у ній нічого не доводить; чому після повної
установки аудит порожній, варто подивитись окремо.
---
## 2026-08-29 — Хто заходив і хто що вимкнув
Дві сліпі зони, знайдені питанням «чому в пісочниці аудит порожній».
Відповідь на саме питання виявилась нудною (установник робить лише те,
що в переліку сліпих зон), а от дорога до неї — ні.
### Історія входів: 93 записи були в базі й ніде не показувались
`core.login_attempts` наповнювалась на кожній спробі входу від першого
дня. Читалась рівно в одному місці — для стримування підбору. Ні
ендпойнта, ні сторінки, ні згадки у вебі. Тобто на питання «хто заходив
у мій моніторинг» продукт мав відповідь у базі й не мав способу її
показати. Для B2B це перше, що питає служба безпеки клієнта.
**Головним була не верстка.** У таблиці немає `tenant_id` — саме тому ці
події й не в аудиті (журнал вимагає кабінет, а на момент перевірки
пароля кабінет ще невідомий). Показати «як є» означало б віддати одному
кабінету спроби входу чужих людей. Прив'язка непряма: спроба →
користувач → членство, трьома шляхами одночасно (`user_id`, `username`,
`email`) в одному `JOIN LATERAL` з `tenant_id` усередині. Саме `JOIN`, а
не `LEFT JOIN`: без збігу рядок ПРИБИРАЄТЬСЯ, а не лишається без імені.
На бойових даних це виявилось не теорією: у власника **немає пошти**,
тож за `email` не прив'язується ЖОДНА з 94 спроб. Шлях через `username`
дає 93. Тобто зіставлення «як усі роблять» показало б порожню сторінку —
і виглядало б справним.
Невдала спроба з неіснуючим логіном не належить нікому. Обрано:
**рядком не показувати, але порахувати** — «ще N спроб з M адрес не
показані». Сигнал «логіни перебирають» лишається, чужа людина в чужий
кабінет не потрапляє. Однозначно правильного варіанта тут немає, і в
коді так і написано.
### Аудит: алерти й склад команди
За цілий день роботи (драбина, тригери, канал, правила відповідності,
політика відкату) у журналі не з'явилось нічого. Тепер пишуться правила
алертів, канали, драбини, правила відповідності, а також додавання
людини в кабінет, зміна ролі, вилучення й скидання пароля.
Два рішення про зміст:
* **вимкнення видно з НАЗВИ дії** (`alr.rule.disable`), а не з різниці
подробиць. Питання «хто вимкнув правило, за яким приходив алерт»
має відповідатись переліком, а не порівнянням двох записів;
* **`config` каналу не їде в запис ВЗАГАЛІ.** Там не лише токен бота: у
webhook-каналі лежить адреса (доступ на запис у чужий чат) і заголовок
`Authorization`. Замість вмісту — прапорець `secret_changed`.
Перевірено наживо: створення й видалення каналу лягли в журнал, токен у
ньому не з'явився.
### Помилкова тривога, названа для чесності
Я вирішив, що сім подій у `core.event_outbox` «висять». Ні: усі сім
позначені доставленими, а `pruneLoop` щогодини прибирає доставлені,
старші за граничний вік. Найстаршій було 11 годин. Механізм справний.