feat: ui + docs: для жюри
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
# Инструкция для жюри по проверке
|
||||
|
||||
Модуль принимает текст, находит в нём персональные данные, подменяет их и по тому же идентификатору возвращает исходный текст. Ниже — как это воспроизвести и где смотреть журнал и метрики.
|
||||
|
||||
Сервис слушает `http://localhost:8080`.
|
||||
|
||||
## Запуск
|
||||
|
||||
Нужны Java 21 и Maven 3.8+ (или обёртка `./mvnc`). Для дашборда — Docker Compose.
|
||||
|
||||
Локально:
|
||||
|
||||
```bash
|
||||
./mvnc package -DskipTests
|
||||
java -jar target/pd-guard-spring-1.0.0.jar
|
||||
```
|
||||
|
||||
Контейнер, Prometheus и Grafana (`compose.yaml` ссылается на уже собранный образ `pd-guard-spring:jvm`, сам его не строит):
|
||||
|
||||
```bash
|
||||
docker build -f src/main/docker/Dockerfile -t pd-guard-spring:jvm .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Если локальной Java нет — тот же образ собирается полностью внутри Docker, `mvn package` идёт в отдельной стадии сборки (дольше первого запуска, зато не требует ничего на хосте кроме Docker):
|
||||
|
||||
```bash
|
||||
docker build -f src/main/docker/Dockerfile.build -t pd-guard-spring:jvm .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Готовность: `GET http://localhost:8080/health` отвечает `OK`.
|
||||
|
||||
Действующие системы читаются из `config/systems.json`. Это не встроенный файл в jar: при запуске из каталога проекта используется именно он.
|
||||
|
||||
## Как устроен запрос
|
||||
|
||||
`POST /process`
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{"payload": "текст", "payload_id": "устойчивый-идентификатор"}
|
||||
```
|
||||
|
||||
Заголовки:
|
||||
|
||||
| Заголовок | Смысл |
|
||||
|-----------|--------|
|
||||
| `X-System-Id` | имя системы из `config/systems.json`. Нет заголовка или имя неизвестно — применяется политика `default` |
|
||||
| `X-System-Key` | нужен только если у системы в конфиге задано поле `key` |
|
||||
|
||||
Направление выбирается по `payload_id`, а не по отдельному флагу:
|
||||
|
||||
1. Идентификатор ещё не встречался — текст маскируется, пара «исходник ↔ маска» запоминается.
|
||||
2. Пришёл ранее выданный текст маски и у системы включено `demask` — возвращается исходный текст.
|
||||
3. Пришёл тот же исходный текст — возвращается та же маска, что и в первый раз.
|
||||
|
||||
Ответ: `{"result": "..."}`.
|
||||
|
||||
Коды: `200` успех, `400` нет `payload` или `payload_id`, `403` система выключена или неверный ключ, `429` перегрузка (заголовок `Retry-After: 1`). При внутреннем сбое сервис отвечает `200` и текстом `[обработка недоступна]`, чтобы исходные персональные данные не ушли наружу.
|
||||
|
||||
## Маскирование в браузере
|
||||
|
||||
Откройте `http://localhost:8080/`.
|
||||
|
||||
Четыре режима на странице — это четыре системы из конфига:
|
||||
|
||||
| Кнопка | Заголовок | Что увидите |
|
||||
|--------|-----------|-------------|
|
||||
| MASK | `default` | звёздочки с открытыми краями номера, ФИО инициалами |
|
||||
| STRICT | `strict` | сплошные звёздочки, длина сохраняется |
|
||||
| TOKEN | `crm` | токены вида `[FIO_1]`, `[PASSPORT_1]` |
|
||||
| SYNTHETIC | `analytics` | правдоподобная подмена (вымышленное ФИО, номер, адрес) |
|
||||
|
||||
Вставьте текст, например:
|
||||
|
||||
```text
|
||||
Клиент Иванов Иван Иванович, паспорт 4509 123456, тел +7 916 123-45-67
|
||||
```
|
||||
|
||||
Нажмите «Обработать». Результат появится в блоке под кнопкой.
|
||||
|
||||
Страница каждый раз создаёт новый `payload_id`. Она показывает только маскирование. Демаскирование проверяется запросом к API с тем же идентификатором.
|
||||
|
||||
## Маскирование через API
|
||||
|
||||
Система `default` маскирует все известные типы и умеет демаскировать. Режим — звёздочки с открытыми краями номера (`MASK`).
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/process \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-System-Id: default" \
|
||||
-d "{\"payload\":\"Клиент Иванов Иван Иванович, паспорт 4509 123456, тел +7 916 123-45-67\",\"payload_id\":\"doc-1\"}"
|
||||
```
|
||||
|
||||
Ожидаемый вид ответа: в `result` ФИО заменено на инициалы (`И. И. И.`), середина паспорта и телефона закрыта звёздочками, края видны.
|
||||
|
||||
Сплошные звёздочки — тот же запрос с заголовком `X-System-Id: strict`.
|
||||
|
||||
Токены вида `[FIO_1]`, `[PASSPORT_1]`, `[PHONE_1]` — тот же запрос с заголовком `X-System-Id: crm` (но у `crm` в политике нет `PASSPORT`, см. ниже).
|
||||
|
||||
Синтетика — `X-System-Id: analytics`. У этой системы обратное преобразование выключено.
|
||||
|
||||
Система `crm` маскирует только ФИО, телефон, email и части адреса (город, улица, дом, квартира). Паспорт в её политике не входит и в ответе останется открытым. Демаскирование у `crm` выключено.
|
||||
|
||||
Система `legacy-billing` выключена: тот же запрос с `X-System-Id: legacy-billing` даёт `403`.
|
||||
|
||||
## Демаскирование
|
||||
|
||||
Нужны три условия одновременно:
|
||||
|
||||
- тот же `payload_id`, что при маскировании (`doc-1` в примере выше);
|
||||
- в `payload` — точная строка из поля `result` предыдущего ответа;
|
||||
- система с `"demask": true`. Сейчас это `default` и `strict`. У `crm` и `analytics` демаскирование выключено намеренно.
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/process \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-System-Id: default" \
|
||||
-d "{\"payload\":\"<сюда строка result из маскирования>\",\"payload_id\":\"doc-1\"}"
|
||||
```
|
||||
|
||||
В `result` вернётся исходная фраза с ФИО, паспортом и телефоном.
|
||||
|
||||
Соответствие живёт в памяти процесса 30 минут и пропадает после перезапуска. Повторный запрос после перезапуска будет обработан как новое маскирование.
|
||||
|
||||
Хранилище разделено по системам: маска, полученная от `default`, не раскрывается запросом от `strict` с тем же `payload_id`.
|
||||
|
||||
## Цепочка до модели
|
||||
|
||||
`POST /proxy` показывает, что ушло бы во внешнюю модель и что вернулось бы потребителю. Адрес модели по умолчанию пуст, поэтому отвечает заглушка: она возвращает присланный (уже замаскированный) текст. В модель подставляются токены, даже если у системы выбран другой режим: по звёздочкам однозначное восстановление невозможно.
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/proxy \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-System-Id: default" \
|
||||
-d "{\"prompt\":\"Клиент Иванов Иван Иванович, карта 4111 1111 1111 1111. Кратко опишите профиль.\"}"
|
||||
```
|
||||
|
||||
В ответе:
|
||||
|
||||
| Поле | Смысл |
|
||||
|------|--------|
|
||||
| `prompt_masked` | текст, который ушёл бы в модель |
|
||||
| `llm_response_masked` | ответ модели (или заглушки) ещё с токенами |
|
||||
| `response` | текст потребителю; при `demask: true` токены заменены обратно |
|
||||
| `replaced` | таблица «токен → исходное значение» |
|
||||
| `llm` | `заглушка` либо имя модели |
|
||||
|
||||
В `prompt_masked` не должно остаться открытых ФИО и номера карты.
|
||||
|
||||
## Где смотреть логи
|
||||
|
||||
Отдельного файла журнала нет. Строки пишет процесс в стандартный вывод.
|
||||
|
||||
Локальный запуск — окно, где выполнена команда `java -jar`.
|
||||
|
||||
Docker:
|
||||
|
||||
```bash
|
||||
docker compose logs -f pd-guard
|
||||
```
|
||||
|
||||
На каждое маскирование есть строка уровня INFO. В ней идентификатор, длина текста и счётчики по типам. Сами значения персональных данных в журнал не пишутся:
|
||||
|
||||
```text
|
||||
payload_id=doc-1 символов=72 найдено={FIO=1, PASSPORT=1, PHONE=1}
|
||||
```
|
||||
|
||||
Рядом по смыслу, если они случаются:
|
||||
|
||||
- `Системе … обращение в модуль запрещено настройками` — выключенная система;
|
||||
- `Системе … отказано: неверный ключ`;
|
||||
- `демаскирование не нашло соответствие` — идентификатор и отпечаток маски неизвестны, текст обработан как новый;
|
||||
- `Настройки систем перечитаны из …` — после правки `config/systems.json`.
|
||||
|
||||
Для прокси: `proxy: система=… заменено=… модель=…`. Число замен есть, значения — нет.
|
||||
|
||||
## Где смотреть метрики
|
||||
|
||||
Сырые метрики сервиса, без Docker:
|
||||
|
||||
| Адрес | Что это |
|
||||
|-------|---------|
|
||||
| `GET /actuator/prometheus` | все метрики в формате Prometheus |
|
||||
| `GET /actuator/metrics` | список имён Micrometer |
|
||||
| `GET /actuator/metrics/pdguard.process` | длительность обработки |
|
||||
| `GET /actuator/metrics/pdguard.pd.detected` | сколько фрагментов найдено, в разрезе типа и системы |
|
||||
| `GET /actuator/health` | проба Spring Boot |
|
||||
|
||||
Имеет смысл смотреть после одного-двух вызовов `/process`:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8080/actuator/prometheus | findstr pdguard
|
||||
```
|
||||
|
||||
На Linux и macOS вместо `findstr` — `grep pdguard`.
|
||||
|
||||
Основные ряды:
|
||||
|
||||
| Метрика | О чём |
|
||||
|---------|--------|
|
||||
| `pdguard_process_seconds` | длительность, метки `direction` (`mask` / `unmask`) и `system` |
|
||||
| `pdguard_pd_detected_total` | найденные фрагменты, метки `type` и `system` |
|
||||
| `pdguard_requests_rejected_total` | отказы: `overload`, `malformed`, `system_disabled`, `internal_error` |
|
||||
| `pdguard_concurrency_limit` | текущий потолок одновременных запросов |
|
||||
| `pdguard_concurrency_in_flight` | сколько запросов в работе |
|
||||
| `pdguard_store_chars` | объём хранилища соответствий в символах |
|
||||
| `pdguard_tokens_processed_total` | оценка числа обработанных токенов |
|
||||
| `pdguard_demask_unresolved_total` | демаскирование без найденного соответствия |
|
||||
| `pdguard_ner_*` | вторая ступень (модель). В поставке без скачанных моделей ступень выключена, ряды почти не растут |
|
||||
|
||||
Дашборд появляется вместе с `docker compose`:
|
||||
|
||||
- Grafana: `http://localhost:3000` (анонимный просмотр включён). Дашборд «Модуль безопасности персональных данных»: `http://localhost:3000/d/pd-guard`. Обновление раз в 5 секунд. Блоки: обращения и типы ПДн, задержка и доля ответов быстрее 0,5 с, отказы, вторая ступень, ресурсы узла.
|
||||
- Prometheus: `http://localhost:9090`. Цель сбора — `pd-guard:8080`, интервал 5 секунд.
|
||||
|
||||
## Что поменять без перезапуска
|
||||
|
||||
Отредактируйте `config/systems.json` и вызовите:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8080/admin/reload
|
||||
```
|
||||
|
||||
Файл также перечитывается сам, если изменилось время модификации (проверка не чаще раза в секунду). Текущие политики: `GET /admin/config`. Список типов, которые умеет распознавать сборка: `GET /admin/types`.
|
||||
|
||||
Проверка набора типов: в политике `crm` нет `PASSPORT`. Текст с паспортом и телефоном под `X-System-Id: crm` скроет телефон и оставит номер паспорта. Под `default` скроет оба.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Производительность и дополнительные возможности
|
||||
|
||||
Целевой уровень из задания — задержка не выше 0,5 с при 1000 запросах в секунду. Замер ниже снят на правилах, без нейросетевой ступени: именно так работает поставка, пока каталог `models/` пуст.
|
||||
|
||||
## Нагрузочный тест
|
||||
|
||||
Инструмент — [k6](https://k6.io/), сценарий `k6-load-test.js`.
|
||||
|
||||
Условия:
|
||||
|
||||
- один узел в Docker, порт 8080;
|
||||
- система `crm` (маскирование, без демаскирования; типы — ФИО, телефон, email и части адреса);
|
||||
- пять коротких текстов, часть из них с персональными данными, один — без них;
|
||||
- профиль виртуальных пользователей: 30 с до 500, затем по 30 с на 1000, 1500 и 2000, затем спад до нуля;
|
||||
- между запросами одного пользователя пауза 0,1 с;
|
||||
- пороги сценария: доля ошибок ниже 1 %, p95 длительности HTTP ниже 200 мс.
|
||||
|
||||
Запуск при уже поднятом сервисе:
|
||||
|
||||
```bash
|
||||
k6 run k6-load-test.js
|
||||
```
|
||||
|
||||
Другой адрес: `k6 run -e BASE_URL=http://localhost:8080 k6-load-test.js`.
|
||||
|
||||
Зафиксированный прогон этого сценария на одном узле:
|
||||
|
||||
| Метрика | Значение |
|
||||
|---------|----------|
|
||||
| Пропускная способность | ~9 500 запросов/с |
|
||||
| Задержка p50 | 1,03 мс |
|
||||
| Задержка p95 | 13,87 мс |
|
||||
| Ошибки | 0,00 % |
|
||||
|
||||
0,5 с при 1000 запросах/с перекрыто с запасом: p95 на этом профиле около 14 мс, поток около 9 500 запросов/с.
|
||||
|
||||
Отдельный замер внутри процесса (`PerformanceBenchmarkTest`) гоняет тот же набор текстов по правилам после прогрева JIT. В тесте закреплены пороги: p99 маскирования одного обращения ниже 5 мс и пропускная способность выше 1000 обращений/с на доступных ядрах. Это порог регрессии, а не замена цифр k6.
|
||||
|
||||
Потолок одновременных запросов не фиксирован. `AdaptiveConcurrencyLimiter` держит его между `pdguard.min-concurrent` (8) и `pdguard.max-concurrent` (2000) и подстраивает по фактической задержке с целью 200 мс. На старте `PipelineWarmup` прогоняет горячий путь несколько тысяч раз на отдельном коротком хранилище, чтобы первые боевые запросы не попали на непрогретый код: без этого p95 в первые десятки секунд примерно на порядок хуже.
|
||||
|
||||
Вторая ступень (NER) в замер не входила. Модель зовётся только на непокрытые кандидаты и стоит десятки миллисекунд на вызов; на текстах, которые уже закрыты правилами, она не вызывается. Доля таких обращений видна в метрике `pdguard_ner_requests_total`.
|
||||
|
||||
На дашборде Grafana (`http://localhost:3000/d/pd-guard`) во время прогона смотрят обращения в секунду, p95 маскирования, долю ответов быстрее 0,5 с, отказы по перегрузке и текущий предел конкурентности.
|
||||
|
||||
## Дополнительные возможности
|
||||
|
||||
Сверх маскирования заданного перечня типов реализовано следующее.
|
||||
|
||||
**Четыре режима подмены.** Звёздочки с сохранением краёв и разделителей (`MASK`), сплошное закрытие (`STRICT`), обратимые токены (`TOKEN`), правдоподобные вымышленные значения (`SYNTHETIC`). Синтетический номер карты проходит проверку Луна. Один и тот же фрагмент в тексте всегда получает одну и ту же замену.
|
||||
|
||||
**Политика на систему без пересборки.** Включение, демаскирование, режим, белый список типов, типы-спутники и общий секрет задаются в `config/systems.json` и применяются на лету. Хранилище соответствий разделено по системам.
|
||||
|
||||
**Контекстное маскирование.** PIN, CVV, дата без якоря, место рождения, страна, реквизиты организации, доход и упоминание биометрии маскируются только рядом с самостоятельным персональным данным. Список спутников настраивается у каждой системы. Два спутника друг друга не подтверждают.
|
||||
|
||||
**Защита от ложных срабатываний.** Общеизвестные имена и правители снимаются, если рядом нет других персональных данных; внешний список `config/well-known.txt` дополняет встроенный и перечитывается сам. Имя в названии организации и на вывеске не маскируется. Адрес отделения банка и улица в рассказе о городе не считаются адресом клиента. Словари имён, стран и населённых пунктов учитывают склонения и уменьшительные формы.
|
||||
|
||||
**Контрольные суммы и свободная запись чисел.** Карта, ИНН, СНИЛС, ОГРН и ОГРНИП подтверждаются контрольной суммой. Те же номера находятся, если между цифрами стоят пробелы, точки, дефисы или скобки.
|
||||
|
||||
**Вторая ступень распознавания.** Три необязательные ONNX-модели: имена (WikiNEuRal), составляющие адреса (ruBERT), юридические реквизиты. Модели в репозиторий не входят, скачиваются `tools/fetch-ner-model.sh`. Сбой ступени её отключает и оставляет правила. Регион и район адреса размечаются только этой ступенью.
|
||||
|
||||
**Безопасный отказ.** При внутренней ошибке наружу уходит фиксированная строка `[обработка недоступна]`, а не исходный текст. В журнал пишутся идентификатор, длина и счётчики типов; значения персональных данных не пишутся.
|
||||
|
||||
**Хранилище.** Исходный текст шифруется AES-GCM. Запись живёт ограниченное время и вытесняется по объёму. Демаскирование возможно по `payload_id` и по отпечатку маски. Повтор того же исходного текста с тем же идентификатором возвращает прежнюю маску. Общий слой Redis включается настройкой `pdguard.store.backend=redis` и при серии сбоев на время перестаёт опрашиваться, не роняя запрос.
|
||||
|
||||
**Демонстрация модели.** `POST /proxy` показывает замаскированный запрос, ответ модели и восстановленный текст. Без адреса модели работает заглушка; ошибка модели тоже сводится к заглушке.
|
||||
|
||||
**Наблюдаемость.** Пробы `/health` и `/actuator/health`, метрики Micrometer и Prometheus, готовые Prometheus и Grafana. Гистограмма `pdguard.process` размечена корзинами до 10 с, на дашборде видна доля ответов быстрее 0,5 с. Веб-страница `http://localhost:8080/` гоняет три режима маскирования без отдельного клиента.
|
||||
|
||||
**Прогрев и предел нагрузки.** Прогрев JIT на старте и адаптивный лимитер конкурентности с ответом `429`, чтобы задержка не упиралась в таймаут вызывающей стороны.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Ограничения решения и план развития
|
||||
|
||||
Ограничения ниже относятся к поставке, с которой работает жюри: один процесс, правила без моделей, файл `config/systems.json`, ключ шифрования из `application.yml`.
|
||||
|
||||
## Ограничения
|
||||
|
||||
**Состояние демаскирования не переживает процесс.** Соответствия лежат в памяти узла, не дольше 30 минут и не больше заданного объёма символов. Перезапуск, вытеснение и истечение срока делают обратное преобразование невозможным: запрос обрабатывается как новое маскирование, счётчик `pdguard.demask.unresolved` увеличивается. Общий слой Redis в коде есть и включается `pdguard.store.backend=redis`, но в текущем `compose.yaml` его нет: поднят один узел.
|
||||
|
||||
**Демаскирование разрешено не всем системам и не из браузера.** В конфиге оно включено у `default` и `strict`. У `crm` и `analytics` выключено. Страница `http://localhost:8080/` каждый раз создаёт новый `payload_id`, поэтому с неё можно проверить только маскирование. Чужая система не читает чужое хранилище.
|
||||
|
||||
**Неизвестное имя системы получает политику `default`.** Отсекаются только явно выключенная система и неверный ключ. Ключ проверяется лишь там, где поле `key` заполнено; у систем в поставленном файле ключей нет.
|
||||
|
||||
**Административные методы открыты.** `GET /admin/config`, `GET /admin/types` и `POST /admin/reload` не требуют аутентификации. Для стенда хакатона это удобно, для контура с несколькими потребителями — нет.
|
||||
|
||||
**Ключ шифрования хранилища лежит в конфигурации приложения.** AES-GCM включён, но ключ записан в `application.yml`. Это демонстрационный ключ. В рабочем контуре его нужно задавать снаружи и не хранить в репозитории.
|
||||
|
||||
**Качество детекции держится на правилах и словарях.** Иностранные имена без русских словообразовательных признаков, нестандартные топонимы, составляющие адреса «регион» и «район», а также юридические и банковские реквизиты в нетиповых формулировках без второй и третьей ступеней не размечаются. Третья ступень (LLAIM Legal NER) отдельно закрывает именно эти реквизиты — на датасете реальных логов (`NodeLogsDatasetTest`) и датасете размещений (`PlacementVariantsTest`) число подтверждённых утечек заметно ниже именно за счёт неё, регресс без неё был бы больше. Модели в репозиторий не входят: без `tools/fetch-ner-model.sh` и включённых `pdguard.ner.*-engine` сервис остаётся на правилах. Сбой любой ступени отключает её до перезапуска, остальные продолжают работать.
|
||||
|
||||
**Часть типов маскируется только в контексте.** PIN, CVV, «голая» дата, место рождения, страна, банковские реквизиты организации, доход и слово о биометрии сами по себе не закрываются. Биометрия в тексте — это упоминание, а не шаблон из базы. Режим `MASK` по задумке оставляет края длинных номеров и инициалы ФИО; полностью закрывает режим `STRICT`.
|
||||
|
||||
**Синтетика покрывает не все типы.** Где своей подстановки нет, `SYNTHETIC` ставит токен вида `[TYPE_1]`.
|
||||
|
||||
**Журнал не является аудитом значений.** Пишутся идентификатор, длина и счётчики типов. Восстановить по журналу, что именно скрыто, нельзя — и отдельного аудита решений оператора тоже нет.
|
||||
|
||||
**Ответ модели — внешняя зависимость.** Пока `pdguard.llm.url` пуст, `POST /proxy` отвечает заглушкой. Ошибка живой модели тоже подменяется заглушкой, чтобы сбой модели не раскрывал исходный текст. Контракт `/process` от модели не зависит.
|
||||
|
||||
**Нагрузка измерена на коротких текстах и на правилах.** Прогон k6 использует систему `crm` и тексты в одну-две строки. Длинный документ и включённая модель этот профиль не описывают: модель вызывается точечно, но один вызов BERT — это уже десятки миллисекунд.
|
||||
|
||||
**Внутренняя ошибка выглядит как успешный ответ.** HTTP-код остаётся 200, тело — `[обработка недоступна]`. Так исходный текст не утекает и автоматический прогон не останавливается на серии ошибок. Отличить сбой от маски можно по этой фиксированной строке и по метрике `pdguard_requests_rejected_total{reason="internal_error"}`.
|
||||
|
||||
## План развития после хакатона
|
||||
|
||||
**Контур и секреты.** Вынести ключ шифрования и секреты систем в хранилище секретов. Закрыть `/admin` аутентификацией. Неизвестную систему отклонять, а не сажать на `default`. Включить Redis в поставку compose как общий слой соответствий и прогнать демаскирование через балансировщик.
|
||||
|
||||
**Детекция.** Поставлять модели второй ступени отдельным артефактом со проверкой целостности и измеренным p95 на включённой ступени. Добавить типы документов, которых не хватает по отраслевому перечню, отдельными правилами в реестре — ядро и политики с `"*"` подхватят их без миграции. Расширить внешние словари (известные люди, денилист улиц) как файлы, которые перечитываются так же, как `config/systems.json`.
|
||||
|
||||
**Качество и нагрузка.** Закрепить в CI прогон размеченных наборов (precision/recall по типам) и сценарий k6 с порогом p95 ≤ 0,5 с при 1000 запросах/с на полном перечне типов, а не только на политике `crm`. Замерить длинные документы отдельно от коротких обращений.
|
||||
|
||||
**Наблюдаемость для эксплуатации.** Журнал решений без значений персональных данных в централизованный сбор. Алерты на рост `demask_unresolved`, на отказы по перегрузке и на выключение второй ступени. Срок хранения соответствий и потолок объёма сделать разными для систем.
|
||||
|
||||
**Интерфейс политики.** Страница проверки сейчас только маскирует. Следующий шаг — показать демаскирование тем же `payload_id` и дать править перечень типов и режим без ручного JSON, с тем же файлом `systems.json` под капотом.
|
||||
Reference in New Issue
Block a user