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
#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
151 lines
12 KiB
Markdown
151 lines
12 KiB
Markdown
# 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-аудит логов выполнен (A–E) — **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`
|