Тема
ТЗ — Антифрод (cms/antifraud)
Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: catalog, er, freelance Статус: ТЗ к разработке
Назначение и возможности
Детект накрутки отзывов, заявок и других сущностей: IP-кластеры, скорость действий, шаблонность текстов, fingerprint устройства. Скоринг сущности с карантином на модерацию и репутацией пользователя во времени.
- Сигналы: IP-кластеры (много действий с одной подсети/IP), всплеск скорости (N событий за короткое время);
- шаблонность текста (похожие/идентичные отзывы разных аккаунтов), fingerprint браузера/устройства;
- скоринг сущности (отзыв, заявка, публикация) с указанием сработавших причин;
- карантин на модерацию вместо автоудаления — решение остаётся за человеком;
- репутация пользователя: накопительный показатель доверия, влияет на строгость порогов;
- скоринг асинхронный по умолчанию — не блокирует синхронно горячий путь публикации;
- поставляет агрегированные сигналы в
cms/attack-monitor(общая картина аномалий парка).
Зависимости и выключение
requires: ядро · suggests: cms/moderation · provides: fraud-signal
Поведение при выключении: отзывы/заявки/публикации проходят без антифрод-скоринга — остаётся только премодерация модулей-потребителей (если включена), без автоматического выявления накрутки.
Стоимость внешних API: не применимо — скоринг считается по внутренним сигналам (IP-кластер, скорость, шаблонность текста, fingerprint), модуль не вызывает платных внешних сервисов сам.
Модель данных
| Таблица | Ключевые поля | Примечание |
|---|---|---|
cms_antifraud_scores | id, subject_type, subject_id, score, signals (json), status | результат скоринга, полиморфная привязка |
cms_antifraud_user_reputation | id, user_id, trust_score, updated_at | накопительная репутация пользователя |
cms_antifraud_rules | id, signal_type, condition (json), weight, is_active | декларативные правила детекта |
Индекс на (subject_type, subject_id) в cms_antifraud_scores; signals — JSONB → GIN-индекс под фильтр по причинам. user_id в cms_antifraud_user_reputation — FK constrained()->index()->unique() (одна репутация на пользователя); status — PHP Enum. signal_type в cms_antifraud_rules — PHP Enum (ip_cluster, velocity, text_similarity, fingerprint, …); индекс на (signal_type, is_active).
Правила как декларативные условия, не код: сигналы (IP-кластер, скорость, шаблонность текста, fingerprint) конфигурируются как правила с весами в cms_antifraud_rules через Filament (по аналогии с cms_antispam_rules антиспама), а не хардкодятся в PHP; AntifraudService::score() на каждом скоринге читает активные правила из кеша группы и суммирует вес сработавших. Новый сигнал/порог — правка в Filament, деплой не требуется.
ПДн-паспорт (матрица v2.2): signals в cms_antifraud_scores может содержать текст сущности (отзыв/заявка) и fingerprint устройства — срок хранения привязан к жизни сущности-потребителя (subject_type/subject_id — полиморфная кросс-модульная ссылка без FK, каскад через БД невозможен). Ретеншн-джоба antifraud периодически удаляет записи, чей subject в таблице модуля-владельца больше не существует (orphan-очистка, не завязана на конкретное событие удаления каждого потребителя). trust_score в cms_antifraud_user_reputation хранится, пока у пользователя есть активность, либо до истечения простоя trust_score_decay_days; при UserDeleted (ревизия ядра 14.07.2026, п.3) запись репутации удаляется/анонимизируется немедленно — участие в каскаде «забыть по запросу» (152-ФЗ). «Выгрузить всё по субъекту» отдаёт текущий trust_score и историю его изменений, без сырых signals чужих сущностей.
Входные и выходные данные
Входы:
| Источник | Данные/поля | Чем валидируется |
|---|---|---|
Сервис-вызов модуля-потребителя (requires, например cms/reviews) | subject_type, subject_id, actor (user_id/ip/fingerprint), text (опц.), created_at | вызывающий модуль уже провалидировал бизнес-данные; антифрод принимает только объявленный контракт AntifraudService::score() — whitelist параметров |
| Filament: решение модератора по карантину | status (approve/reject) | Policy antifraud.manage |
cms:antifraud:rescore | параметры пересчёта (диапазон дат/тип сущности) | whitelist аргументов команды |
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
Модуль-потребитель (например cms/reviews) | скор + сработавшие сигналы + вердикт | синхронный ответ сервиса (если включён синхронный режим) либо событие/статус после асинхронного скоринга |
cms/attack-monitor | агрегированные сигналы (без сырых ПДн) | provides-контракт fraud-signal |
| Filament (карантин, карточка репутации) | subject_type, subject_id, score, signals[]; user_id, trust_score | таблицы |
события AntifraudQuarantined/AntifraudReputationChanged | см. «События и обмен» | payload события |
При отклонении/карантине вердикт несёт машиночитаемый код fraud_suspected (ревизия ядра 14.07.2026, п.1): модуль-потребитель, публикующий свой ответ через собственный публичный API, обязан прокинуть этот код в конверт {message, code, errors}, а не изобретать собственный.
Настройки (группа antifraud)
| Ключ | Тип | Дефолт | affectsPageCache | Описание |
|---|---|---|---|---|
antifraud.scoring_enabled | bool | true | нет | Kill-switch. false — все сущности проходят как «не проверено» (скоринг не считается, правила не читаются), без выключения модуля целиком |
antifraud.quarantine_threshold | int | 60 | нет | Порог скоринга для карантина |
antifraud.ip_cluster_window_minutes | int | 10 | нет | Окно для детекта IP-кластеров |
antifraud.trust_score_decay_days | int | 90 | нет | Срок затухания репутации без активности |
antifraud.sync_scoring_enabled | bool | false | нет | Скорить синхронно в момент вызова (иначе — асинхронно через очередь, вызывающий получает предварительный «не заблокировано») |
antifraud.sync_scoring_timeout_ms | int | 200 | нет | Таймаут синхронного скоринга — превышение переводит вызов в асинхронный режим на этот запрос |
antifraud.nat_ip_allowance_factor | int | 3 | нет | Множитель к порогу IP-кластера для подсетей, помеченных как офисные/NAT (снижает ложные срабатывания) |
Все пороги и окна выше — лимиты и квоты модуля (матрица v2.2): явные дефолты, достижение — деградация (карантин/асинхронный fallback), не 500 и не тихое обрезание. antifraud.scoring_enabled — аварийная опция на случай ложных массовых блокировок из-за сбоя правил или порогов; переключается в Filament с подтверждением.
API
Отдельного API нет — админ-CRUD через Filament.
Компоненты
Filament: очередь карантина сущностей с указанием сработавших сигналов (объяснение вердикта — см. «Безопасность»), карточка репутации пользователя, редактор правил детекта (cms_antifraud_rules: тип сигнала, условие, вес, активность) — правка применяется к новым скорингам без деплоя. Команды: cms:antifraud:rescore --json (пересчёт по текущим правилам), cms:antifraud:import-legacy --source=<профиль> --json (перенос легаси-репутации, см. «Донорский код»).
События и обмен
| Событие | Когда | Payload |
|---|---|---|
AntifraudQuarantined | сущность ушла в карантин | subject_type, subject_id, score, signals |
AntifraudReputationChanged | изменилась репутация пользователя | user_id, trust_score |
Слушает: UserDeleted (ревизия ядра 14.07.2026, п.3) — удаляет/анонимизирует запись пользователя в cms_antifraud_user_reputation (каскад «забыть по запросу»). Скоринг сущностей при этом не подписан ни на одно чужое событие — вызывается только сервис-вызовом по requires от модулей- потребителей. Provides-контракт fraud-signal — агрегаты потребляет cms/attack-monitor без прямого обращения к чужим таблицам.
Таблица взаимодействий:
| Сущность/модуль | Канал | Направление | Что происходит |
|---|---|---|---|
cms/reviews / другой потребитель (requires со стороны потребителя) | сервис-вызов AntifraudService::score() | in | потребитель просит скор перед публикацией сущности; ответ синхронный или предварительный (см. sync_scoring_enabled) |
Очередь antifraud | джоба асинхронного скоринга | in→out | при асинхронном режиме сущность публикуется предварительно, окончательный вердикт приходит после — потребитель обязан уметь «откатить» публикацию в карантин по событию AntifraudQuarantined |
cms/attack-monitor | provides-контракт fraud-signal | out | агрегаты аномалий (без сырых ПДн) доступны детектору для общей картины атак парка |
cms/moderation (suggests) | сервис-вызов (если установлен) | out | карантинная сущность может попадать в общую очередь модерации вместо отдельного экрана |
| Ядро — жизненный цикл пользователя | событие UserDeleted | in | каскад «забыть по запросу»: репутация пользователя удаляется/анонимизируется |
Фоновая работа
Именованная очередь antifraud для пересчёта скоринга (cms:antifraud:rescore), для затухания репутации по trust_score_decay_days и для ретеншн-очистки cms_antifraud_scores от записей с удалённым в модуле-владельце subject (ПДн-паспорт — см. «Модель данных»); джобы идемпотентны. Основной режим скоринга — асинхронный (см. «Безопасность» и «Крайние случаи»): синхронный вызов допускается только при sync_scoring_enabled=true и обязан укладываться в sync_scoring_timeout_ms.
Ранбук (типовые инциденты):
| Симптом | Что проверить | Команда |
|---|---|---|
| Волна ложных блокировок с одной подсети (офис/NAT) | Не помечена ли подсеть как офисная — nat_ip_allowance_factor действует только на помеченные подсети | Пометить подсеть NAT в Filament, проверить/поднять nat_ip_allowance_factor |
| Резкий всплеск карантина по всему сайту | Корреляция по времени с инцидентом в cms/attack-monitor (атака/бот-волна, а не просто агрессивные правила) | Карточка инцидента cms/attack-monitor; при подтверждении рассинхрона правил — cms:antifraud:rescore --json после правки весов |
Репутация не пересчитывается / trust_score не затухает | Джоба затухания по trust_score_decay_days зарегистрирована в ScheduleRegistrar, очередь antifraud не отстаёт | cms:antifraud:doctor --json, вручную cms:antifraud:rescore --json |
Бэкап/рестор: в бэкап попадают cms_antifraud_scores, cms_antifraud_user_reputation, cms_antifraud_rules. Денормализованных агрегатов модуль не хранит — отдельной команды-восстановителя после рестора не требуется; при подозрении на рассинхрон допустим rescore по диапазону дат, но это не обязательный шаг завершения рестора.
Производительность и кеш
- Ожидаемый объём: сотни–тысячи скорингов в сутки на активный сайт с отзывами/заявками; IP-кластерный анализ работает с окном в минуты, не с полной историей — не растёт линейно с возрастом сайта;
- горячий путь публикации сущности потребителем — бюджет 0 синхронных запросов к antifraud по умолчанию (
sync_scoring_enabled=false): скоринг уходит в очередь, сущность публикуется как «на модерации» или «предварительно опубликована» по правилам потребителя; при включённом синхронном режиме — бюджет ≤2 запроса и обязательный таймаут с fallback на асинхронный путь; - индекс
(subject_type, subject_id)критичен для карантина; для IP-кластерного детектора нужен индекс по(ip, created_at)в исходных данных потребителя (не в таблицах antifraud — сигнал считается из данных, переданных сервис-вызовом); - кешируется:
antifraud:reputation:<user_id>— репутация пользователя, инвалидируется событиемAntifraudReputationChanged; пороговые настройки — из кеша группы (0 запросов на горячем пути скоринга); - батч-пересчёт (
rescore) — чанками (Bus::batch), не единой транзакцией на весь объём — иначе долгая блокировка при большом числе сущностей.
Безопасность
Скоринг вызывается только сервис-вызовом модулей-потребителей по requires, не через прямой HTTP. Сигналы в карантине читаемы и не являются «чёрным ящиком» — причины хранятся структурированно в signals. В логи и агрегаты не попадают ПДн сверх необходимого. Векторы, специфичные для модуля:
- скоринг не должен блокировать синхронно горячий путь — публикация отзыва/заявки не ждёт результата антифрода по умолчанию; синхронный режим — осознанная опция с таймаутом и деградацией к асинхронному при превышении
sync_scoring_timeout_ms; - ложное срабатывание на NAT/офисной сети — множитель
nat_ip_allowance_factorдля помеченных подсетей снижает вес IP-кластерного сигнала; помечать подсети — вручную в Filament (whitelist для конкретного клиента, не автоопределение); - VPN/прокси — детект IP-кластера не единственный сигнал: комбинация с fingerprint устройства и шаблонностью текста снижает вероятность бана честного пользователя за общим VPN-выходом;
- отравление репутации —
trust_scoreменяется только через события модуля, прямая правка в БД/API отсутствует; ручная корректировка — только Filament с правомantifraud.manage-rulesи записью в аудит; - объяснимость вердикта (explainability) — принцип, не опциональная деталь UI: ответ
AntifraudService::score()и карточка карантина в Filament обязаны показывать не только итоговый score, но и перечень сработавших правилcms_antifraud_rulesс их весами и вкладом в сумму — модератор и вызывающий модуль видят «почему», а не только число; сигналы не «чёрный ящик».
Права: antifraud.view, antifraud.manage, antifraud.manage-rules (studio).
Матрица ролей:
| Действие | Менеджер (antifraud.view) | Модератор (antifraud.manage) | Studio (antifraud.manage-rules) |
|---|---|---|---|
| Просмотр карантина и карточки репутации | ✅ | ✅ | ✅ |
| Решение по карантину (одобрить/отклонить, в стоп-лист) | — | ✅ | ✅ |
| Пометка подсети NAT/офис | — | ✅ | ✅ |
Правка правил детекта (cms_antifraud_rules: сигнал, условие, вес) | — | — | ✅ |
Ручная корректировка trust_score пользователя | — | — | ✅ |
Kill-switch antifraud.scoring_enabled | — | — | ✅ |
Решения по конкретным сущностям (карантин, NAT-подсети) — уровень модератора сайта; правила детекта и веса меняют скоринг сразу для всего парка сигналов, ручная правка репутации и аварийное отключение скоринга — операции повышенного риска, доступны только studio-роли.
UX-требования
Админ:
- пустая очередь карантина — «подозрительных сущностей не обнаружено»;
- массовые действия: «одобрить выбранные», «карантин → в стоп-лист автора»;
- карточка репутации показывает историю изменений
trust_score, не только текущее значение — модератору нужен контекст, почему пользователь «токсичен»; - ошибка вызова antifraud потребителем (таймаут, недоступность очереди) — «сущность опубликована без проверки на накрутку, будет пересчитана автоматически», не тихий провал;
- пометка подсети как NAT/офисной — с подтверждением и объяснением эффекта (снижает чувствительность детектора для этой подсети);
- переключение kill-switch
antifraud.scoring_enabled— с подтверждением и явным объяснением эффекта («все сущности будут проходить как непроверенные, карантин новых записей не создаётся; накопленный карантин не трогается»).
Посетитель:
- пользователь, чья заявка/отзыв ушли в карантин, не должен видеть признаков бана — публикация асинхронная, задержка воспринимается как обычная модерация, не как кара;
- нет отдельного UX для антифрода на публичной стороне — вся логика скрыта за потребляющим модулем.
Крайние случаи и типовые баги
- ложное срабатывание на NAT/офисной сети →
nat_ip_allowance_factorповышает порог IP-кластера для помеченных подсетей; без пометки — комбинация сигналов (IP + fingerprint + шаблонность текста), не один IP-кластер в одиночку; - VPN/прокси у честного пользователя → скоринг не блокирует синхронно, уходит в карантин на решение модератора, а не в автоблокировку — человек разбирает пограничные случаи;
- скоринг блокирует горячий путь → ⚠️ Противоречие: донорский код (
catalog,er) исторически считал скор синхронно при сохранении отзыва. Разрешение: в CMS v2 дефолт — асинхронный скоринг (sync_scoring_enabled=false), синхронный — только явная настройка с таймаутом; донорский синхронный путь переносится как основа асинхронной джобы, не как есть; - гонка: два одновременных действия одного пользователя (двойной сабмит отзыва) → скоринг идемпотентен по
(subject_type, subject_id)— повторный вызов на ту же сущность не создаёт вторую запись вcms_antifraud_scores(upsert по уникальному индексу); - параллельный
rescoreи новое событиеAntifraudReputationChanged→ джоба батча берёт снимок на момент старта, конкурентное изменение репутации применяется следующим прогоном — не теряется и не дублируется (идемпотентность §9 стандарта); - выключение модуля посреди накопленного карантина → карантинные записи остаются в БД, но новые вызовы
AntifraudService::score()от потребителей должны получать graceful fallback («не заблокировано, антифрод недоступен»), не исключение; cms/moderationне установлен (suggests отсутствует) → карантин работает через собственный Filament-экран модуля, не через общую очередь модерации — деградация предусмотрена, не поломка;- пустой fingerprint/IP (заявка через API без стандартных заголовков) → сигналы, требующие IP/fingerprint, просто не участвуют в скоринге — сумма по доступным сигналам, не отказ в скоринге целиком;
- огромный объём сущностей при первом включении на существующем сайте →
rescoreобязан идти чанками (Bus::batch) с видимым прогрессом в админке, не одной долгой транзакцией — иначе таймаут команды/память; - репутация не пересчитывается годами (нет активности пользователя) →
trust_score_decay_daysзатухает по расписанию, а не только по событию — джоба планировщика, не завязана на визит пользователя; - kill-switch
antifraud.scoring_enabled=false→ новые вызовыAntifraudService::score()возвращают «не проверено» без обращения к правилам и без постановки в очередьantifraud; уже накопленный карантин не разбирается автоматически — ждёт модератора как обычно, повторное включение не теряет историю; - повторный прогон
cms:antifraud:import-legacy→ идемпотентен поuser_id/external_id: обновляет существующую репутацию, не создаёт дубль;--dry-runтолько отчитывается о расхождениях, в БД не пишет; UserDeletedдля пользователя с активной репутацией →trust_scoreудаляется/ анонимизируется каскадно; уже вынесенные вердикты по его прошлым сущностям (cms_antifraud_scores) задним числом не пересматриваются.
Донорский код
| Что взять | Путь |
|---|---|
| Подходы детекта накрутки отзывов | catalog, er |
| Репутационные сигналы | freelance/project/src/app/Domains/Reputation/ |
Легаси-импорт репутации (§16 стандарта): cms:antifraud:import-legacy --source=<профиль> [--dry-run] --json переносит накопленный trust_score пользователей с донорской площадки (catalog, er, freelance) при миграции клиента — маппинг по user_id/external_id, идемпотентен (повторный прогон обновляет запись, не дублирует), --dry-run отдаёт отчёт расхождений без записи в БД. Сырые signals/исторические причины бана донора не переносятся — только агрегированный trust_score, чтобы не тащить непроверенные основания в новую систему правил.
Тесты и приёмка
- [ ] Контрактный тест: серия однотипных отзывов с одной подсети за окно
ip_cluster_window_minutesуходит в карантин; - [ ] Карантин не удаляет и не публикует сущность до решения модератора;
- [ ] Репутация пользователя снижает/повышает эффективный порог скоринга (доверенные — мягче);
- [ ] Сигналы в карантине читаемы и объясняют причину (не «чёрный ящик»);
- [ ] Деградация при выключении не блокирует публикацию отзывов/заявок;
- [ ] Агрегаты сигналов доступны
cms/attack-monitorбез прямого обращения к чужим таблицам; - [ ] По умолчанию скоринг асинхронный — публикация сущности не ждёт результата antifraud;
- [ ] Синхронный режим соблюдает
sync_scoring_timeout_msи деградирует к асинхронному при превышении; - [ ] Подсеть, помеченная NAT/офисной, не даёт ложного карантина при обычной активности нескольких пользователей;
- [ ]
rescoreидёт чанками с прогрессом, не одной транзакцией на весь объём; - [ ] Правило детекта редактируется в Filament без деплоя и применяется к новым скорингам;
- [ ] Вердикт содержит человеко-читаемое объяснение (список сработавших правил с весами), не только итоговый score;
- [ ]
antifraud.scoring_enabled=falseотключает скоринг целиком (все сущности — «не проверено»), не выключая модуль; - [ ]
cms:antifraud:import-legacyидемпотентен: повторный прогон не дублирует репутацию,--dry-runне пишет в БД; - [ ]
UserDeletedудаляет/анонимизируетcms_antifraud_user_reputation, не затрагивая прошлыеcms_antifraud_scores; - [ ] При отклонении/карантине ответ сервиса несёт код
fraud_suspectedдля проброса в API вызывающего модуля; - [ ] Контрактный набор
cms-testingзелёный, testbench-изоляция пакета, feature-тест на каждый роут; - [ ] Тестовая БД только
antifraud_test;migrate:fresh/refresh/reset,db:wipeзапрещены.