Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Контур: модуль безопасности персональных данных

Прокси между системой банка и LLM. Находит в тексте ПДн 19 типов, заменяет их масками до отправки в модель и возвращает исходные значения в ответ. В LLM уходит только замаскированный текст.

Система-потребитель -> Контур [идентификация -> маскирование] -> LLM -> Контур [демаскирование] -> Система-потребитель
                                         |                                  ^
                                         +-- соответствия в Redis (AES-GCM) +

Результаты на момент сдачи, условия замеров в docs/PERFORMANCE.md:

Что Значение
Слепой набор v1, 487 примеров span similarity 0,994, ложные срабатывания на ловушках 0%
Слепой набор v2, отложенная половина, 249 примеров span similarity 0,972, ложные срабатывания 0,9%
Открытый бенчмарк redmadrobot, отложенная половина, 1184 примера span similarity 0,888, полнота сущностей 0,53
Нагрузка, пары маска/демаска факт RPS 968, p50 18,7 мс, p95 232,5 мс, p99 371,0 мс, ошибок 0%, неточных восстановлений 2
Текст ~100 тыс. токенов (360 тыс. символов) маска и демаска за 3,7 с

Что важно заказчику и где это видно

Вопрос Ответ и проверка
Насколько хорошо находятся ПДн 19 типов, span similarity 0,994 на слепом наборе и 0,972 на отложенной половине второго; uv run python -m tools.eval datasets/golden/blind.jsonl
Сколько ложных срабатываний 0% на ловушках первого слепого набора, 0,9% на втором: поэт Пушкин, адрес отделения банка, горячая линия, номер договора остаются открытыми; раздел 5 в docs/JURY.md
Сохраняется ли смысл для модели значения заменяются типизированными метками [ФИО_1], [НОМЕР_КАРТЫ_1], одинаковое значение получает одну метку, служебные слова «серия», «ул.», «года» остаются; режим synthetic подставляет правдоподобные данные того же формата. Модель видит, что в тексте клиент, карта и телефон, и отвечает по делу, ответ демаскируется по меткам
Правила для разных систем config/systems.yaml: типы ПДн, вид маски, запрет демаскирования, комбо-правила для каждой системы; админ-API меняет политику без перезапуска
Как подключить нового потребителя блок в config/systems.yaml или PUT /admin/systems/{id}, ключ в переменной окружения; для LLM достаточно сменить base_url OpenAI-клиента на адрес модуля
Что при ошибках и отказах Redis недоступен: память воркера и degraded в /health/ready; LLM недоступна: 502/504, данные не уходят; перегрузка: 429 с Retry-After; сбой детектора: весь текст закрывается [СКРЫТО_1]. Таблица в разделе 8 docs/JURY.md
Не попадают ли ПДн в логи и метрики в логе только типы, количества и тайминги, поля с текстом заменяются на <redacted>, метки метрик без значений; это проверяет tests/test_no_leaks.py
Расширяемость новый тип ПДн это новый YAML-файл в config/rules, подхватывается без перезапуска (тест test_new_type_from_yaml); новый сценарий это политика в systems.yaml

Быстрый старт

Нужны Docker с Compose v2 и свободный порт 8080. Скопируйте .env.example в .env. Без правок сервис работает, а для внешней LLM впишите LLM_BASE_URL и LLM_API_KEY. Запустите docker compose up -d --build --wait: поднимутся API на 4 воркерах и Redis. Демо откроется на http://localhost:8080/demo, описание API на http://localhost:8080/docs. Инструкция для жюри с примерами запросов, логами и метриками лежит в docs/JURY.md.

Без Docker: uv sync --extra server, затем uv run granian pii_guard.api.app:app --interface asgi --port 8080 --workers 4. Без REDIS_URL соответствия хранятся в памяти процесса, поэтому демаскирование работает только при одном воркере.

Контракт автопроверки

curl -s localhost:8080/process -H 'Content-Type: application/json' \
  -d '{"payload": "Клиент Иванов Иван, паспорт 4509 123456", "payload_id": "demo-1"}'
# {"result":"Клиент [ФИО_1], паспорт [ПАСПОРТ_1]"}

curl -s localhost:8080/process -H 'Content-Type: application/json' \
  -d '{"payload": "Клиент [ФИО_1], паспорт [ПАСПОРТ_1]", "payload_id": "demo-1"}'
# {"result":"Клиент Иванов Иван, паспорт 4509 123456"}

Первый вызов с новым payload_id маскирует, второй с маской и тем же payload_id демаскирует. Повтор любого из двух вызовов (после ошибки или 429) возвращает тот же ответ. Запросы без заголовков обслуживает система default.

Эндпоинты

Метод и путь Назначение
POST /process контракт автопроверки: маска, затем демаска по payload_id
POST /v1/mask маска и session_id для демаскирования, список найденных сущностей с позициями
POST /v1/unmask восстановление по session_id, в том числе ответа LLM с переставленными токенами
DELETE /v1/sessions/{id} досрочно удалить соответствия
POST /v1/chat/completions OpenAI-совместимый прокси: маска, вызов LLM, демаска ответа
GET /v1/types типы ПДн, которые знает модуль
GET /admin/systems, PUT и DELETE /admin/systems/{id}, POST /admin/reload управление системами, нужен X-Admin-Key
GET /stats RPS, TPS, p50/p95/p99 за 60 секунд по всем воркерам
GET /metrics метрики Prometheus
GET /health/live, GET /health/ready живость процесса и готовность зависимостей
GET /demo демо-страница

Система-потребитель передаёт X-System-Id и X-Api-Key (или Authorization: Bearer <ключ>). Для LLM-прокси достаточно поменять base_url в OpenAI-клиенте на адрес модуля.

Настройки

Настройки делятся на три уровня. Переменные окружения задают процесс, config/systems.yaml задаёт политику каждой системы-потребителя, файлы в config/rules и config/gazetteers задают, что считать ПДн. Все три уровня меняются без правки кода. YAML-файлы перечитываются на лету каждым воркером, перезапуск не нужен.

Переменные окружения

Переменная По умолчанию Смысл
APP_NAME Контур имя сервиса в логах, заголовке API и метриках
REDIS_URL пусто хранилище соответствий; пусто означает память процесса
VAULT_KEY пусто ключ AES-256-GCM (32 байта, urlsafe base64); пусто: ключ создаётся в Redis при первом старте
KEY_CRM, KEY_SUPPORT пусто ключи систем; имя переменной указывается в systems.yaml полем api_key_env
ADMIN_KEY пусто ключ админ-API; пусто означает, что админ-API выключен
LLM_BASE_URL, LLM_API_KEY, LLM_MODEL пусто, пусто, deepseek-ai/DeepSeek-V4-Flash-0731 внешняя LLM с OpenAI-совместимым API; без адреса прокси отвечает демо-ответом
LLM_TIMEOUT_SECONDS, LLM_VERIFY_TLS 90, true таймаут и проверка сертификата LLM
WORKERS, RUNTIME_MODE 4, mt процессы granian и режим рантайма
MAX_CONCURRENT 256 запросов в работе на воркер; сверх лимита ответ 429 с Retry-After
MAX_BODY_BYTES, MAX_TEXT_CHARS 4 МиБ, 1 000 000 лимиты тела запроса и длины текста, сверх лимита 413
LOG_LEVEL INFO уровень логов; строка request.done на каждый запрос содержит типы ПДн и время каждого этапа
METRICS_PREFIX pii префикс имён метрик Prometheus
PORT, API_CPUS 8080, 4 порт на хосте и лимит CPU контейнера в docker-compose.yml

Системы-потребители: config/systems.yaml

Блок defaults применяется ко всем системам, каждая система переопределяет нужные поля. default_system обслуживает запросы без заголовков.

Поле Значения Что делает
enabled true / false выключенная система получает 403, текст не обрабатывается
api_key_env имя переменной ключ системы берётся из окружения, в YAML секретов нет
entity_types all или список какие типы ПДн маскировать
exclude_types список исключения из all
mask_mode placeholder, redact, stars, partial, synthetic вид маски: [ФИО_1], [ФИО], **** ****, 4276********5678, правдоподобные подставные данные
type_modes тип: режим свой режим для отдельных типов, например карта partial
placeholder_lang ru / en язык меток: [ФИО_1] или [PERSON_1]
unmask true / false разрешено ли системе демаскирование
contextual true / false учитывать контекст: публичные персоны, адреса отделений, горячие линии
single_value_mode true / false если весь текст одно значение без метки, угадать тип по форме
bare_date_policy never, birth_like, always маскировать ли даты без слов «дата рождения»
combo_rules список правил маскировать тип только рядом с другим, например PIN только рядом с картой
ttl_seconds 60 .. 604800 сколько хранить соответствия
llm_proxy true / false доступен ли системе /v1/chat/completions

Пример из репозитория: crm получает синтетические данные и частично открытую карту, support_bot не может демаскировать и маскирует PIN и CVV только рядом с номером карты, analytics выключена.

support_bot:
  api_key_env: KEY_SUPPORT
  unmask: false
  exclude_types: [CITIZENSHIP]
  combo_rules:
    - {type: PIN, requires: [BANK_CARD], window: 200}

Те же поля меняются через админ-API: PUT /admin/systems/{id} с JSON-телом из полей выше. Модуль проверяет политику, отклоняет неизвестные типы с кодом 422 и атомарно перезаписывает файл.

Типы ПДн и правила: config/rules/*.yaml

Каждый файл описывает один тип: метки и список правил. Новый тип добавляется новым файлом, ядро не меняется. Пример из теста tests/test_process_contract.py:

type: OMS_POLICY
label_ru: ПОЛИС_ОМС
label_en: OMS_POLICY
rules:
  - id: oms_ctx
    requires_any: ["полис"]
    pattern: "полис\\p{L}*\\s+омс{SEP}(?P<v>\\d{16}){END}"
    confidence: 0.99
    priority: 130

Маскируются именованные группы v, v1, v2 и так далее, текст вокруг остаётся. Поля правила:

Поле Смысл
pattern регулярное выражение модуля regex, регистр не учитывается, макросы {SEP}, {END}, {DATE} и другие из config/rules/_macros.yaml
priority, confidence кто побеждает при пересечении спанов
validators проверка значения: luhn, inn, inn12, snils, phone_digits, not_repeated, date_any, birth_like, plausible_birth, issue_like
negative_context, negative_window, negative_after слова рядом, при которых значение не ПДн: «горячая линия», «код ошибки»
required_context, required_window слова, без которых правило не срабатывает
requires_any, requires_regex, min_digits префильтр: правило не запускается на тексте без нужных подстрок или цифр
subtype, gazetteer_check, policy подтип адреса, проверка по словарю, подчинение политике bare_date_policy

Словари и контекст лежат рядом. config/gazetteers хранит имена, фамилии, страны и публичные персоны, по одному слову в строке. config/context.yaml хранит маркеры публичной персоны, клиента, организации и слова-триггеры перед ФИО. config/address.yaml хранит триггеры адреса и места рождения и служебные слова, которые остаются открытыми.

Проверка качества и нагрузки

uv sync --all-extras
uv run pytest -q                                              # 38 тестов: контракт, качество, утечки
uv run python -m tools.eval datasets/golden/blind.jsonl --show 20
RATE=1000 DURATION=60s sh load/run.sh                         # k6 в Docker, сводка p50/p95/p99
docker compose --profile observability up -d                  # Prometheus :9090, Grafana :3000

Документация

  • docs/JURY.md: как отправить текст, получить маску и демаску, где логи и метрики.
  • docs/ARCHITECTURE.md: схема, конвейер детекции, хранение соответствий, безопасность.
  • docs/PERFORMANCE.md: нагрузочные прогоны, стенд, доп. возможности.
  • docs/LIMITATIONS.md: где подход ошибается и план развития.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages