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

151 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 152-ФЗ: аудит кода на PII в logs / Sentry / БД / LLM
> Аудит по issue [#79](https://git.gendsgn.ru/lekss361/gendesign/issues/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/`):
```bash
# 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)
- [x] Grep-аудит логов выполнен (AE) — **plain-PII не найдено**.
- [x] PRINZIP CRM ETL: подтверждено хеширование phone/email перед INSERT.
- [x] Sentry PII-scrubbing активен (`send_default_pii=False` + `before_send_transaction`).
- [x] httpx DEBUG в проде проверен — **выключен** (R2 — ops-инвариант).
- [x] 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`