#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
12 KiB
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): backenduvicorn— дефолт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_transactionredact-ит секреты в 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,
данные покидают РФ). Эшелонировано:
- Allowlist (первичное): консьюмер обязан собрать
SafePayloadиз проверенных не-чувствительных полей — сырые DB-строки не передаются. - Hard-block (enforced):
SafePayload(is_confidential=True)→RedactionRefusedError, контент наружу не уходит (детерминированный fallback вместо отправки). - 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-аудит логов выполнен (A–E) — 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