# Инструкция для жюри по проверке Модуль принимает текст, находит в нём персональные данные, подменяет их и по тому же идентификатору возвращает исходный текст. Ниже — как это воспроизвести и где смотреть журнал и метрики. Сервис слушает `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` скроет оба.