gendesign/docs/PII_152FZ_Audit.md
Light1YT 11aa011d11
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 2m3s
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (push) Successful in 8m55s
CI / backend-tests (pull_request) Successful in 8m58s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 29s
Deploy / build-worker (push) Successful in 29s
Deploy / deploy (push) Successful in 1m10s
perf(db): covering-index objective_lots district-price (#70) + 152-ФЗ/perf audit docs (#70 #79)
#70: EXPLAIN top-5 hot-path на prod — analyze district-price block по objective_lots
(1.12M) делал Bitmap Heap Scan ~41k блоков (cold 5.3s/warm 474ms), пробивая p95 ≤3с.
Partial covering-index (district, price_per_m2_rub) WHERE price_per_m2_rub IS NOT NULL →
Index Only Scan (verified scratch: 87ms, Heap Fetches 0). Требует VACUUM ANALYZE
objective_lots post-deploy (stale VM иначе глушит выигрыш). database-expert  verified.

#79: 152-ФЗ PII grep-аудит — plain-PII в логах НЕ найдено; Sentry scrub / PRINZIP-хеширование
/ LLM §19-redaction подтверждены. Остаточный риск: pilot_requests хранит сырые ПДн без
retention (рекомендация в docs). httpx DEBUG off (ops-инвариант).

Closes #70
Closes #79
2026-06-14 00:09:09 +05:00

12 KiB
Raw Permalink Blame History

152-ФЗ: аудит кода на PII в logs / Sentry / БД / LLM

Аудит по issue #79 (B7-1, ⚖️ Compliance / Legal). Дата: 2026-06-13. Статус: PASS (plain-PII в логах не обнаружено; ниже — что проверено, покрытие scrub-слоёв, остаточные риски).

Источник правды по решениям — Obsidian vault. Здесь — версионированная в репозитории копия рядом с кодом, на который ссылается.

TL;DR

  • Grep-аудит логов: plain-PII НЕ найдено. Ни один logger.*-вызов в backend/app/ не пишет сырые phone/email/ФИО/паспорт/СНИЛС/ИНН. Исправлять нечего.
  • Sentry/GlitchTip: send_default_pii=False + before_send_transaction-scrub активны и в API (app/main.py), и в Celery (app/workers/celery_app.py).
  • БД: единственное место хранения сырых ПДн — pilot_requests (лид-форма пилота); это by-design (контакт для обратной связи). PRINZIP CRM-зеркало хеширует phone/email при импорте. Остаточный риск: у pilot_requests нет хеширования и нет retention — см. §6.
  • LLM (§19): внешний провайдер (OpenAI) защищён эшелонированным redaction-слоем (allowlist → hard-block confidential → regex-scrub RU PII). По умолчанию LLM выключен (llm_enabled=False).

1. Метод аудита

Прогон по backend/app/ (исключая tests/):

# A) logger.*-вызовы, упоминающие PII-поля
grep -rniE "logger\.(debug|info|warning|error|exception)\(.*(phone|email|passport|\binn\b|снилс|snils|fio|full_?name|фио|телефон|почт)" app/

# B) файлы, читающие PII-поля (трассировка «доходит ли до лога»)
grep -rni -lE "phone|email|passport|снилс|fio|full_name|фио|owner_name|client_name" app/ --include="*.py"

# C) логи, дампящие весь payload/body/request/dict (риск PII через repr объекта)
grep -rnE "logger\.\w+\(.*(payload|body|request\b|\.dict\(|model_dump|\.json\(|%r)" app/

# D) bare print() (запрещён — мог бы вывести PII в stdout)
grep -rnE "(^|[^.])\bprint\(" app/ --include="*.py"

# E) форсированный DEBUG / httpx-logger (может логировать query-params с PII)
grep -rniE "basicConfig|getLogger\(.httpx|setLevel\(.*DEBUG|level=logging\.DEBUG|--log-level" app/ docker-compose*.yml backend/Dockerfile

2. Logs — результаты

2.1. Прямых PII-логов нет

grep A вернул 0 совпадений. Точечно проверены два единственных PII-несущих эндпоинта:

Файл PII на входе Что логируется Вердикт
app/api/v1/pilot.py name/phone/email/company (лид-форма) logger.info("pilot_request saved id=%s source=%s", row["id"], payload.source) — только id строки + источник clean
app/api/v1/admin_leads.py — (читает уже-хешированные) отдаёт только phone_last4 + email_hash; сырые phone/email из SELECT не выбираются clean

2.2. %r-дампы — не PII

Grep C нашёл logger.*(... %r) в скрейперах/парсерах (CBR, Rosstat, OKN, NSPD, DOM.РФ, reservation_ingest, izyatie_ocr). Все источники — публичные датасеты недвижимости/ макроэкономики. Дампятся коды/строки документов/площадки, а не ПДн физлиц.

app/main.py:140 логирует username %r (неизвестный RBAC-юзер) — это операторский логин из Caddy basic_auth (учётка сотрудника), а не субъект ПДн по 152-ФЗ. Оставлено как осознанный security-лог попыток доступа.

2.3. Нет print(), нет форсированного DEBUG

  • Grep D: 0 bare print() в app/.
  • Grep E: единственные logging.basicConfig(level=logging.INFO, …) — внутри if __name__ == "__main__" CLI-блоков standalone-скрейперов (cbr_macro.py, rosstat_emiss.py), НЕ в runtime API/worker. Уровень INFO, не DEBUG.
  • Ни один httpx/httpcore-logger нигде не выставлен в DEBUG. Нет dictConfig/fileConfig.
  • Runtime log-levels (docker-compose.prod.yml): backend uvicorn — дефолт info (нет --log-level debug); Celery worker/beat — --loglevel=info. uvicorn access-log пишет метод+путь+статус, не тело запроса, поэтому форма POST /pilot/request не светит PII в access-логе (а query-string у неё пустой).

Вывод по logs: plain-PII отсутствует. Правок не требуется.

3. Sentry / GlitchTip — coverage

Self-hosted GlitchTip (Sentry-совместимый). Init продублирован для двух процессов:

  • app/main.py (FastAPI) и app/workers/celery_app.py (Celery) — оба с:
    • send_default_pii=False — SDK не прикрепляет cookies/headers/тело/IP-адреса.
    • before_send_transaction=scrub_sensitive_query (app/observability/sentry_scrub.py) — redact-ит apiKey=/api_key=/token=/access_token=/secret= из URL-spans (HttpxIntegration performance-spans), span.description, request.url.
    • traces_sample_rate = glitchtip_traces_sample_rate (default 0.05).
  • Интеграции: Starlette/FastApi/Celery/Sqlalchemy/Httpx/Logging. LoggingIntegration поднимает события только на ERROR (event_level=logging.ERROR); т.к. PII не попадает в сообщения (см. §2), error-события их не несут.
  • Шов: before_send_transaction redact-ит секреты в transaction-spans, но не фильтрует тело error-событий. Это покрыто тем, что (а) send_default_pii=False не прикрепляет request-данные, и (б) сами лог-сообщения PII-free. Если в будущем error начнут включать пользовательский ввод — расширить до before_send (errors), а не только before_send_transaction. Рекомендация R3 §6.

4. БД — где лежат ПДн

Таблица ПДн Защита 152-ФЗ комментарий
pilot_requests (data/sql/118) name, phone, email, companyсырые нет хеша, нет retention Лид-форма пилота. Нужно согласие на обработку + срок хранения — R1 §6
prinzip_crm_leads (data/sql/52) name (сырое); phone/email — хешированы phone_hash=SHA256(phone)[:16], phone_last4, email_hash=MD5(email) Хеширование в ETL 52_import_prinzip_crm.py (hash_phone/hash_email) ПЕРЕД INSERT. Сырые phone/email в PG не пишутся. name оставлен (право на удаление)
audit_log (data/sql/144) username, role, action, method, path, cad_num username = операторский логин (не субъект ПДн). Без phone/email/IP. clean

PRINZIP ETL дополнительно: логирует только lead_id/deal_id + счётчики, не сырые значения.

5. LLM (§19 data-residency)

app/services/llm/redaction.py — критическая граница для внешнего провайдера (OpenAI, данные покидают РФ). Эшелонировано:

  1. Allowlist (первичное): консьюмер обязан собрать SafePayload из проверенных не-чувствительных полей — сырые DB-строки не передаются.
  2. Hard-block (enforced): SafePayload(is_confidential=True)RedactionRefusedError, контент наружу не уходит (детерминированный fallback вместо отправки).
  3. Regex-scrub (вторичное): из свободного текста вырезаются RU PII (телефон/email/ СНИЛС/ИНН/паспорт/ФИО-подобное, incl. «голые» 11-значные форматы) → [REDACTED:<kind>]. scrub_text логирует только kind+count, никогда само значение.

По умолчанию llm_enabled=False и openai_api_key=None → клиент вообще не делает сетевых вызовов. Включение — только после заведения ключа в окружение прод-контейнера И принятия решения по §19.

6. Остаточные риски и рекомендации

# Риск Рекомендация Владелец
R1 pilot_requests хранит сырые name/phone/email без срока хранения и без явной отметки согласия Завести retention (напр. авто-удаление/анонимизация лидов старше N мес) + хранить факт согласия на обработку ПДн (checkbox на форме, cross-ref B3-4). Решение — продукт/legal product + database-expert
R2 httpx DEBUG сейчас выключен в коде и compose (проверено §2.3). Но если кто-то выставит LOG_LEVEL=DEBUG/--log-level debug в окружении — httpx/httpcore начнут логировать URL с query-params Ops-инвариант: не включать DEBUG на проде для backend/worker. (Опционально — явно прибить logging.getLogger("httpx").setLevel(WARNING) в app/main.py как defense-in-depth) devops (ops-decision)
R3 Sentry-scrub покрывает transaction-spans (before_send_transaction); error-события защищены лишь через send_default_pii=False + PII-free логи Если в error-события начнут попадать пользовательские строки — добавить before_send (errors) поверх существующего before_send_transaction backend
R4 pilot.py:email валидируется минимальным regex (без email-validator — он ломал старт контейнера); это не PII-leak, но слабая валидация Принять как есть (валидация формата ≠ защита ПДн) либо вынести валидацию на фронт/CRM — (accepted)

7. Acceptance (issue #79)

  • Grep-аудит логов выполнен (AE) — plain-PII не найдено.
  • PRINZIP CRM ETL: подтверждено хеширование phone/email перед INSERT.
  • Sentry PII-scrubbing активен (send_default_pii=False + before_send_transaction).
  • httpx DEBUG в проде проверен — выключен (R2 — ops-инвариант).
  • Audit-doc с findings, scrub-coverage, остаточными рисками.

Ссылки (код)

  • Sentry init + scrub: backend/app/main.py, backend/app/workers/celery_app.py, backend/app/observability/sentry_scrub.py
  • LLM §19 redaction: backend/app/services/llm/redaction.py, client.py
  • Лид-формы: backend/app/api/v1/pilot.py, backend/app/api/v1/admin_leads.py
  • PRINZIP ETL (хеширование): data/sql/52_import_prinzip_crm.py, data/sql/52_schema_prinzip_crm.sql
  • Аудит-таблица: data/sql/144_audit_log.sql, backend/app/core/audit_middleware.py