feat: ui + docs: для жюри

This commit is contained in:
Максименко Никита Владимирович
2026-09-23 23:12:10 +03:00
parent 121a281fc6
commit 88a6117eda
7 changed files with 796 additions and 5 deletions
+127
View File
@@ -0,0 +1,127 @@
# Схема архитектуры и настройки
Модуль стоит между системой-потребителем и внешней языковой моделью. Потребитель отдаёт текст один раз на вход и один раз на выход. В модель уходит уже подменённый текст, потребителю возвращается текст с восстановленными значениями.
```mermaid
flowchart LR
consumer["Система-потребитель"]
api["POST /process и POST /proxy"]
policy["Политика системы\nconfig/systems.json"]
rules["Правила и словари"]
ner["Вторая ступень\nNER, если включена"]
filters["Отсев ложных\nсрабатываний"]
masker["Маскирование\nMASK / STRICT / TOKEN / SYNTHETIC"]
store["Хранилище соответствий\nпамять, AES-GCM"]
llm["Внешняя LLM\nили заглушка"]
consumer --> api --> policy --> rules --> ner --> filters --> masker --> store
masker -->|"только /proxy"| llm
llm -->|"демаскирование по токенам"| consumer
store -->|"демаскирование по payload_id"| consumer
```
`POST /process` — контракт для системы-потребителя: маскирование и демаскирование. `POST /proxy` — демонстрация всей цепочки до модели и обратно; проверяющий контур может его не использовать.
Настройки систем, перечень типов и вид маски задаются файлом `config/systems.json`. Код для смены политики пересобирать не нужно. Файл перечитывается при изменении и по `POST /admin/reload`.
## Что происходит с одним текстом
1. По заголовку `X-System-Id` выбирается политика. Неизвестное имя получает политику `default`. Выключенная система и неверный `X-System-Key` получают `403` до обработки текста.
2. Если `payload_id` уже есть в хранилище этой системы, направление определяется сравнением текста с сохранённой маской и с исходником.
3. Иначе текст проходит детекцию. Сначала правила: контрольные суммы (карта, ИНН, СНИЛС, ОГРН), однозначные форматы (email, телефон), затем шаблоны с якорным словом (документ, адрес, дата, ФИО). Цифровые значения с нестандартными разделителями дополнительно собираются в кластер и проверяются той же контрольной суммой.
4. Если включена вторая ступень, модель смотрит только непокрытые кандидаты: цепочки слов с заглавной буквы и, для юридической модели, цифровые кластеры. В поставке по умолчанию ступень выключена (`pdguard.ner.*-engine: off`). В `compose.yaml` она включается, если в каталог `models/` положены веса.
5. Ложные срабатывания снимаются до маски: имя в составе организации и на вывеске, общеизвестное имя без других персональных данных рядом, адрес отделения, типы-спутники без самостоятельного персонального данного.
6. Оставшиеся фрагменты заменяются по `maskMode` системы. Пара «исходный текст ↔ маска» пишется в хранилище этой системы. Исходный текст в хранилище шифруется AES-GCM, ключ — `pdguard.store.encryption-key`.
Демаскирование не запускает детектор заново: по `payload_id` (и запасным отпечатком маски) достаётся сохранённый исходник. Поэтому звёздочки тоже обратимы, пока жива запись.
## Системы-потребители
Файл `config/systems.json`, путь переопределяется свойством `pdguard.systems-file`.
| Система | Включена | Демаскирование | Режим | Типы |
|---------|----------|----------------|-------|------|
| `default` | да | да | `MASK` | все (`*`) |
| `crm` | да | нет | `TOKEN` | ФИО, телефон, email, город, улица, дом, квартира |
| `analytics` | да | нет | `SYNTHETIC` | все (`*`) |
| `strict` | да | да | `STRICT` | все (`*`) |
| `legacy-billing` | нет | нет | `MASK` | все (`*`), запросы отклоняются |
Запрос без заголовка идёт в `default`. Хранилище у каждой системы своё: одна система не читает соответствия другой.
Поля политики:
| Поле | Назначение |
|------|------------|
| `enabled` | `false` — модуль отвечает `403` и текст не обрабатывает |
| `demask` | разрешено ли обратное преобразование |
| `maskMode` | чем заменяется найденное значение |
| `types` | какие типы маскировать; `"*"` — все, которые знает сборка |
| `requireCompanion` | типы, которые маскируются только рядом с самостоятельным персональным данным |
| `key` | общий секрет; если задан, заголовок `X-System-Key` обязан совпасть. Только ASCII |
Пример добавления системы — дописать объект и перечитать файл:
```json
"dms": {
"enabled": true,
"demask": true,
"maskMode": "MASK",
"types": ["FIO", "PHONE", "MEDICAL_POLICY", "BIRTH_DATE"],
"key": "dms-secret"
}
```
После `POST /admin/reload` запросы с `X-System-Id: dms` и `X-System-Key: dms-secret` маскируют только перечисленные типы, звёздочками, и умеют демаскировать.
Новый тип персональных данных добавляется правилом в реестре (`DocumentRules`, `FinanceRules`, `DateRules`, `FioRules`, `ContactRules`, `AddressRules`) и именем в `PdTypes`. Политики, где указано `"*"`, подхватывают его без правки конфига. Политика с явным списком — только если имя типа туда добавить.
## Типы персональных данных
Список отдаёт `GET /admin/types`. Группы:
| Группа | Типы |
|--------|------|
| Человек | `FIO`, `CARDHOLDER` |
| Документы | `PASSPORT`, `PASSPORT_ISSUER`, `PASSPORT_DATE`, `DEPT_CODE`, `FOREIGN_PASSPORT`, `DRIVER_LICENSE`, `MILITARY_ID`, `BIRTH_CERTIFICATE`, `MEDICAL_POLICY` |
| Контакты | `PHONE`, `EMAIL` |
| Адрес | `ADDRESS_COUNTRY`, `ADDRESS_POSTCODE`, `ADDRESS_CITY`, `ADDRESS_STREET`, `ADDRESS_HOUSE`, `ADDRESS_FLAT`. `ADDRESS_REGION` и `ADDRESS_DISTRICT` размечает только модель второй ступени |
| Даты и гражданство | `BIRTH_DATE`, `BIRTH_PLACE`, `DATE`, `CITIZENSHIP` |
| Платёжные данные | `CARD`, `CARD_EXPIRY`, `CVV`, `PIN` |
| Реквизиты | `INN`, `SNILS`, `ACCOUNT_NUMBER`, `BIK`, `OGRN`, `OGRNIP`, `KPP` |
| Прочее | `INCOME`, `BIOMETRIC` |
Правила устойчивы к регистру, к дате числом и словами, к вставке слов между серией и номером документа, к уменьшительным формам имён. Карта, ИНН, СНИЛС, ОГРН и ОГРНИП без верной контрольной суммы не маскируются.
## Режимы маскирования
Режим задаётся полем `maskMode` и действует на все типы, которые политика разрешила.
| Режим | Пример для `Иванов Иван Иванович` и паспорта `4509 123456` | Когда уместен |
|-------|--------------------------------------------------------------|---------------|
| `MASK` | `И. И. И.`, паспорт `45** ****56` | человеку остаётся узнаваемый контур, середина закрыта. Края коротких серий (загранпаспорт, военный билет, свидетельство о рождении) не открываются. CVV и PIN закрываются целиком |
| `STRICT` | сплошные звёздочки на всю длину, включая ФИО | ничего из исходных знаков не остаётся |
| `TOKEN` | `[FIO_1]`, `[PASSPORT_1]` | однозначная обратимая подстановка, удобная и для демаскирования ответа модели |
| `SYNTHETIC` | вымышленные ФИО и номер той же формы | модель видит правдоподобный текст. Для типов без своей подстановки остаётся токен |
Одинаковое исходное значение внутри одного текста получает одну и ту же замену.
В `POST /proxy` режим системы для отправки в модель заменяется на `TOKEN`: одинаковые звёздочки нельзя однозначно вернуть на место в ответе модели.
## Контекстное правило
Типы из `requireCompanion` сами по себе персональными данными не считаются. Они маскируются, только если в том же тексте есть находка самостоятельного типа.
В политиках `default` и `strict` спутники такие: `CVV`, `PIN`, `DATE`, `BIRTH_PLACE`, `ADDRESS_COUNTRY`, `ACCOUNT_NUMBER`, `BIK`, `OGRN`, `OGRNIP`, `KPP`, `INCOME`, `BIOMETRIC`.
Два спутника друг друга не подтверждают. Дата рядом с ОГРН без имени и документа человека не маскируется. PIN рядом с номером карты — маскируется. Список спутников у каждой системы свой; пустой список означает, что маскируется всё найденное из `types`.
Отдельно от этого списка снимаются ложные ФИО и адреса: «Александр Пушкин» без других персональных данных, «Институт Склифосовского», адрес отделения банка. Если рядом с общеизвестным именем есть другой тип персональных данных, имя остаётся замаскированным: однофамилец защиту не теряет.
## Состояние и наблюдаемость
Хранилище соответствий по умолчанию — память процесса (`pdguard.store.backend=memory`): потолок `pdguard.store.max-chars` (512 МБ символов), срок жизни записи `pdguard.store.ttl-minutes` (30 минут). При `backend=redis` та же запись дублируется в Redis, чтобы демаскирование попало на другой узел. В текущем `compose.yaml` поднят один узел, Redis не используется.
Одновременные запросы ограничивает адаптивный предел: он растёт, пока задержка укладывается в `pdguard.target-latency-ms` (200 мс), и сжимается, когда перестаёт. Лишние запросы получают `429`, очередь не копится.
Метрики отдаёт Micrometer на `/actuator/prometheus`. Готовые Prometheus и Grafana лежат в `monitoring/` и поднимаются тем же `docker compose`.