> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Контроль рисков

> Внешние списки блокировок и детекция аномалий — свидетельство для оператора, а не автоматическая санкция

Omniflow умеет подписываться на внешние списки блокировок и следить за
несколькими метриками на предмет значений, которые выглядят неправильно. Обе
поверхности порождают записи, которые читает оператор.

Всю эту область определяет одно правило, и оно закреплено в схеме, в слое
сервисов и в текстах панели: **здесь ничто не наказывает клиента**. Совпадение и
сигнал — это свидетельство. Неблагоприятное действие, если оно вообще
происходит, — это обычная мутация клиента или финансов на собственной странице
клиента, со своим правом доступа, своей обязательной причиной и своим событием
аудита.

Именно это делает автоматическую детекцию безопасной для запуска вообще:
худшее, что может сделать ложное срабатывание, — отнять у оператора время,
которое нужно, чтобы его отклонить.

## Списки блокировок

Оператор настраивает источник: отображаемое имя, то, что идентифицируют его
записи (`telegram_id`, `email` или `username`), HTTPS-адрес, необязательный
заголовок авторизации и интервал обновления.

### Что хранится

Только дайджест. Каждая запись хранится как SHA-256 от нормализованного
значения, посоленный видом идентификатора. Отсюда следуют три вещи.

Omniflow не держит читаемой копии чужого списка, а значит, не может её утечь или
переопубликовать. Отпечаток из одной установки ничего не значит в другой. И
поиск по-прежнему остаётся точным обращением к индексу — единственным видом
поиска, который нужен списку блокировок.

Нормализация происходит до хеширования и зависит от вида. Идентификаторы
Telegram приводятся к канонической десятичной форме, имена пользователей
переводятся в нижний регистр с отбрасыванием ведущего `@` или префикса `t.me/`,
а адрес переводится в нижний регистр только в доменной части — приведение
локальной части к нижнему регистру или удаление точек является соглашением
конкретного провайдера, из-за которого два по-настоящему разных адреса
столкнулись бы.

### Обновление

Обновление заменяет записи источника целиком в рамках одной транзакции, поэтому
запись, удалённая издателем, перестаёт совпадать в тот же миг, когда начинает
действовать новый набор, а обновление, упавшее на середине, оставляет прежний
список нетронутым, а не произвольный префикс нового.

Принимаются и JSON-массив, и обычный текстовый файл с одним значением на строку,
причём форма определяется по первому непробельному байту, а не по объявленному
типу содержимого — список, отданный с небрежным `text/plain`, всё равно
импортируется. Строка, которая не нормализуется, пропускается и учитывается в
счётчике, а не роняет обновление: одна испорченная строка в чужом списке не
должна оставить оператора с устаревшим списком блокировок и без объяснений.

Источник, публикующий более 500 000 записей, отклоняется. Список, который вдруг
вырос до такого размера, гораздо вероятнее неверно настроен или
перенаправлен, чем действительно настолько велик, и его импорт заменил бы
работающий список мусором.

### Решение по совпадению

Совпадение фиксирует клиента, источник и вид идентификатора, который сработал.
Оператор выносит решение `allowed` или `blocked` с обязательной причиной, и его
личность записывается, поэтому неблагоприятное решение никогда не может быть
анонимным.

Повторное обнаружение совпадения, по которому уже принято решение, не открывает
его заново. Список разрешений идёт дальше: клиент в списке разрешений
пропускается проверкой целиком, и именно это позволяет решению оператора «с
этим всё в порядке» пережить следующее обновление источника.

Заблокированное совпадение можно обжаловать, и тогда оно возвращается в очередь
на рассмотрение.

## Детекция аномалий

Наблюдать можно за четырьмя метриками, у каждой своё правило:

| Метрика | Что измеряет |
| - | - |
| `purchase` | Стоимость оплаченных заказов на клиента в окне |
| `refund` | Сумма возвратов на клиента в окне |
| `referral` | Квалифицированные рефералы на пригласившего в окне |
| `traffic` | Наблюдаемый трафик по подписке |

Правило несёт окно, порог предупреждения, порог тревоги и минимальную выборку.
Пороги указываются в собственных единицах метрики — байты для трафика,
минорные единицы для покупок и возвратов, простой счёт для рефералов, — потому
что их пересчёт означал бы показ оператору числа, которого он не вводил.

`minimum_sample` — это то, что не даёт совсем новой установке поднять тревогу на
первых трёх заказах. Окно, в котором наблюдений меньше этого значения, не
поднимает ничего.

> [!NOTE]
>
> Правило для трафика — проверка **уровня**, а не скорости. Трафиком владеет
> Remnawave, а Omniflow держит лишь счётчик, который наблюдал последним, поэтому
> правило отмечает подписку, чьё наблюдаемое потребление перешло порог, а не ту,
> чьё потребление быстро выросло. Окно правила управляет тем, как часто такая
> подписка поднимается повторно, а не тем, что измеряется.

### Дедупликация

Состояние, сохраняющееся на протяжении нескольких прогонов оценки, — это один
сигнал, а не по одному на прогон. Ключ дедупликации — это метрика, субъект и
корзина окна, в которую попадает наблюдение, поэтому клиент, который держится
выше порога шесть часов, порождает одну тревогу на окно, а не одну на проход.
Сигнал, который оператор уже рассмотрел, остаётся рассмотренным, когда то же
состояние наблюдается снова.

### Свидетельства

Каждый сигнал несёт числа, которые его породили, — счётчики, суммы, валюту,
окно — и никогда не несёт тела сообщения, ссылки, токена, адреса или любого
другого содержимого клиента. Оператору, разбирающему сигнал, нужно видеть,
почему тот сработал, а это арифметика, а не переписка.

### Уведомления

Поднятый сигнал ставится в очередь в операторский топик `anomaly` в той же
транзакции, что и сохраняет его, поэтому тревога не может быть объявлена по
сигналу, который был откачен. Уведомление называет метрику, серьёзность и
числа. Оно не называет клиента: операторская группа — это чат, а панель — то
место, где обладатель права доступа найдёт субъект.

## Когда всё это выполняется

Обе поверхности приводятся в движение `internal/sweeper` в процессе-воркере, а
не запросом. Его проходы выполняются как периодические задачи River —
`lifecycle_sweep` и `anomaly_evaluation`, — поэтому каждый выполняется один раз
за интервал, сколько бы реплик воркера ни работало, и каждый идемпотентен,
поэтому проход, выполнившийся дважды, приводит к тому же состоянию, что и
выполнившийся один раз. См.
[Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#периодические-проходы-и-внутрипроцессные-циклы).

| Проход | Периодичность |
| - | - |
| Истечение подарков и персональных предложений | Каждый `lifecycle_sweep`, пять минут |
| Обновление списков блокировок | Каждый `lifecycle_sweep`, но только для источников, у которых истёк собственный интервал |
| Оценка аномалий | Собственный проход `anomaly_evaluation`, пятнадцать минут |

Оценка аномалий выполняется по собственному расписанию, потому что она агрегирует по таблице
заказов, а сохраняющийся сигнал дедуплицируется по окну — более частый запуск
не даёт дополнительных тревог, только дополнительную работу.

Упавший проход никогда не останавливает остальные. Недоступность издателя не
должна мешать истечению подарков, а правило, которое не удаётся оценить, не
должно останавливать обновление списка. Каждый источник списка блокировок
независим по той же причине: падение одного оставляет остальные обновлёнными и
сохраняет прежние записи упавшего источника, вместо того чтобы опустошить
список, который некому заменить.

## Изменение порогов и источников

И то и другое редактируется в панели в разделе **Риски**, с правом `risk.write`.

Форма порогов применяет то же правило согласованности, что и оценщик: порог
тревоги не меньше порога предупреждения, а окно не короче пяти минут — поэтому
правило, которое было бы молча пропущено во время оценки, отклоняется уже при
сохранении. Каждое поле называет единицу, в которой оно задано.

Форма источника принимает только HTTPS, а пустой заголовок авторизации означает
«оставить сохранённый», поэтому повторное сохранение источника никогда не
возвращает его учётные данные в браузер. Это верно, только пока URL не меняется:
сохранение нового URL без повторного ввода заголовка отклоняется с
`422 secret_required`, потому что заголовок, введённый для одного издателя
списка, никогда не должен уйти другому.

Воркер скачивает источник только с **публичного** адреса. Loopback, частные,
link-local (включая облачные метаданные) и прочие непубличные диапазоны
отклоняются в момент подключения, после разрешения имени хоста, и повторно для
каждого перенаправления, которое к тому же обязано оставаться на HTTPS.
Отклонённый так источник помечается как сбойный с кодом ошибки
`destination_refused`, отличным от `unreachable`, поэтому панель показывает, что
проблема именно в адресе.

## Права доступа

`risk.read` читает источники, совпадения, правила и сигналы. `risk.write`
выносит решение по совпадению, обжалует его, управляет источниками и правилами и
редактирует список разрешений.

Каждая роль, обладающая `risk.write`, обладает и `customers.read`, что
утверждается модульным тестом: роль, способная решить судьбу совпадения, но не
способная прочитать клиента, решала бы вслепую.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.