259 lines
12 KiB
Markdown
259 lines
12 KiB
Markdown
# Аудит для Sonnet — пофайловый разбор
|
||
|
||
Ты должен прочитать КАЖДЫЙ файл из списка ниже и выдать по нему детальный отчёт.
|
||
|
||
## Формат отчёта ПОФАЙЛОВО
|
||
|
||
Для каждого файла:
|
||
```
|
||
### convert_server.py
|
||
| Строка | Тип | Серьёзность | Что не так | Как исправить |
|
||
|--------|-----|-------------|------------|---------------|
|
||
| 242 | dead code | low | Дубликат _handle_cleanup | Удалить второй |
|
||
```
|
||
|
||
После пофайлового разбора — СВОДНАЯ ТАБЛИЦА всех проблем по серьёзности: critical → high → medium → low.
|
||
|
||
---
|
||
|
||
## Файл 1: `contractor/deploy/convert_server.py`
|
||
|
||
**Что это:** HTTP роутер на http.server + ThreadingMixIn. Все endpoint'ы приложения.
|
||
|
||
**Что смотреть:**
|
||
- ВСЕ `do_GET`, `do_POST`, `do_DELETE` — правильная диспетчеризация? Нет мёртвых путей?
|
||
- `_handle_process_v2` — валидация UUID? SSE корректно закрывается при ошибке? Утечка соединений?
|
||
- `_handle_upload` — размер тела? Content-Type проверка? Таумаут?
|
||
- `_handle_cleanup` — ДВА определения (строки ~242 и ~255). Второй перекрывает первый. Dead code. Порядок DELETE правильный?
|
||
- `_handle_api_sync` — читает JSON body. Что если тело пустое/битое? Что если keep_ids содержит 10000 id? Нет лимита.
|
||
- `_handle_api_document` — doc_id из URL без валидации UUID → psycopg2.InvalidTextRepresentation на "fake-id".
|
||
- `_handle_api_document_delete` — cascade порядок: spec_current → spec_events → supplements → document. Правильный?
|
||
- `_handle_classify_batch` — batch_id из JSON без валидации UUID.
|
||
- `_handle_apply_groups` — валидация структуры groups?
|
||
- `_handle_api_prompts_*` — role из query params без валидации.
|
||
- `_json()` — экранирует ли спецсимволы в данных?
|
||
- `_sse()` — f-string с json.dumps. Данные от LLM могут содержать спецсимволы.
|
||
- Импорт `execute` на строке 12 — не конфликтует с другими импортами?
|
||
- `ALTER TABLE ADD COLUMN IF NOT EXISTS` — права на DDL? Идемпотентно?
|
||
- CORS — `_send_cors()` вызывается везде? OPTIONS?
|
||
|
||
---
|
||
|
||
## Файл 2: `contractor/deploy/app.js`
|
||
|
||
**Что это:** Весь фронтенд (42KB). Загрузка, классификация, группы, сравнение, промпты.
|
||
|
||
**Что смотреть:**
|
||
- `fileQueue`, `contractId`, `batchId` — глобальные. Где расходятся с сервером?
|
||
- `renderTable()` — `innerHTML` из `fileQueue[].name`, `fileQueue[].status`. XSS?
|
||
- `fileInput change` — гонка удаления старых + upload новых?
|
||
- `syncDB()` — fire-and-forget, без await, без проверки ответа.
|
||
- `runClassify()` — утечка таймеров при ошибке?
|
||
- `loadGroups()` — ЕДИНСТВЕННОЕ место где diffBody. Больше никто не должен.
|
||
- `runCompareForGroup()` — compareCard. Все ссылки compareBody/compareStatus?
|
||
- `llmBtn` — compareCard. То же.
|
||
- SSE handler'ы — ДВА почти идентичных. Дублирование.
|
||
- `showText()` — `escHtml(classify_raw)` ок, но `counterparty`, `own_number` — НЕ экранированы! XSS.
|
||
- `escHtml()` — экранирует `<>&"'`?
|
||
- `stepDone/Active/resetStepper` — innerHTML через replace. Спецсимволы в id?
|
||
- `window._groupsData` — устаревает после remove→re-classify. runCompareForGroup использует индекс.
|
||
- `showClassifyBtn()` — идемпотентно?
|
||
- `lucide.createIcons()` — не ломает onclick?
|
||
|
||
---
|
||
|
||
## Файл 3: `contractor/deploy/app_utils.js`
|
||
|
||
**Что это:** Утилиты: форматирование, removeFile, escHtml, модалки.
|
||
|
||
**Что смотреть:**
|
||
- `removeFile()` — syncDB+resetStepper+showClassifyBtn через typeof. Если нет — молча.
|
||
- `formatDate()`, `formatSize()` — null/undefined safe?
|
||
- `escHtml()` — все опасные символы: `< > & " '`?
|
||
- `moveUp/Down` — не вызывают syncDB (правильно, состав не меняется).
|
||
|
||
---
|
||
|
||
## Файл 4: `contractor/deploy/services/classify.py`
|
||
|
||
**Что это:** LLM-классификация. ThreadPoolExecutor, выжимка, вызов LLM.
|
||
|
||
**Что смотреть:**
|
||
- `classify_batch()` — reset + list_pending. Гонка между ними?
|
||
- `_classify_one()` — `_smart_extract` может упасть до LLM. Обрабатывается?
|
||
- `_smart_extract()` — неожиданная структура elements_json?
|
||
- `_call_llm_classify()` — httpx `verify=False`. MITM уязвимость.
|
||
- `_safe_json_parse()` — edge cases: Null/None, числа без кавычек, пустой ответ.
|
||
- `MAX_WORKERS=4` — 4 одновременных запроса создают каждый свой пул?
|
||
- `LLM_KEY` из env — если не задан?
|
||
- API key в коде? (берётся из env, ок)
|
||
|
||
---
|
||
|
||
## Файл 5: `contractor/deploy/services/grouping.py`
|
||
|
||
**Что это:** Группировка документов по контрактам + виртуальные группы.
|
||
|
||
**Что смотреть:**
|
||
- `group_documents()` — виртуальные группы: что если parent_number и own_number оба null?
|
||
- Коллизия normalize_number: разные номера → одинаковый нормализованный.
|
||
- Сортировка по doc_date: None у всех → нестабильный порядок.
|
||
- `apply_groups()` — нет проверки на существующий contract (дубликат при повторе).
|
||
- `supplements_list.remove(s)` внутри цикла for — может пропускать элементы.
|
||
|
||
---
|
||
|
||
## Файл 6: `contractor/deploy/services/process.py`
|
||
|
||
**Что это:** SSE пайплайн сравнения.
|
||
|
||
**Что смотреть:**
|
||
- `run_pipeline()` — битая ссылка supplement→document? timeout? retry?
|
||
- `call_llm()` — таймаут?
|
||
- SSE события — все ли обрабатываются на фронте?
|
||
|
||
---
|
||
|
||
## Файл 7: `contractor/deploy/services/parse.py`
|
||
|
||
**Что это:** Парсинг PDF/DOCX.
|
||
|
||
**Что смотреть:**
|
||
- Расширение: .PDF uppercase? Без расширения?
|
||
- PDF: битый/зашифрованный → исключение?
|
||
- DOCX: .doc (OLE) → исключение?
|
||
- Таблицы: пустые, объединённые ячейки?
|
||
- file_data = None?
|
||
|
||
---
|
||
|
||
## Файл 8: `contractor/deploy/services/upload.py`
|
||
|
||
**Что это:** Multipart загрузка через cgi.FieldStorage.
|
||
|
||
**Что смотреть:**
|
||
- cgi.FieldStorage deprecated. Большие файлы?
|
||
- content_length — отрицательное/огромное → rfile.read?
|
||
- filename — path traversal (`../../etc/passwd`)?
|
||
- 100MB файл → весь в памяти.
|
||
- contract_id без валидации → SQL.
|
||
- `delete_by_document` до создания нового → исключение = потеря старого без создания нового.
|
||
- base64 → +33% размер.
|
||
|
||
---
|
||
|
||
## Файл 9: `contractor/deploy/db/connection.py`
|
||
|
||
**Что это:** ThreadedConnectionPool.
|
||
|
||
**Что смотреть:**
|
||
- `DB_PASS` из env, пустой → ошибка подключения.
|
||
- `minconn=1, maxconn=10` — достаточно?
|
||
- getconn/putconn всегда в finally?
|
||
- `_connection` глобальная — сервер не стартует без БД.
|
||
|
||
---
|
||
|
||
## Файл 10: `contractor/deploy/db/documents.py`
|
||
|
||
**Что это:** CRUD documents + classify_raw.
|
||
|
||
**Что смотреть:**
|
||
- `insert()` — все параметры через %s?
|
||
- `set_classification()` — classify_raw=None → NULL.
|
||
- `set_classify_failed()` — не чистит старые поля классификации.
|
||
- `delete()` — без cascade!
|
||
- `get()` — SELECT *, может вернуть base64 original_bytes.
|
||
|
||
---
|
||
|
||
## Файл 11: `contractor/deploy/db/supplements.py`
|
||
|
||
**Что это:** CRUD supplements + cascade.
|
||
|
||
**Что смотреть:**
|
||
- `delete_by_document()` — spec_current WHERE contract_id удаляет ВСЁ для контракта. Если несколько supplements → остальные теряют spec_current.
|
||
- `list_by_contract()` — orphan supplements игнорируются.
|
||
- `insert()` — нет FK проверки.
|
||
|
||
---
|
||
|
||
## Файл 12: `contractor/deploy/db/contracts.py`
|
||
|
||
**Что это:** CRUD contracts.
|
||
|
||
**Что смотреть:**
|
||
- `insert()` — нет уникальности, можно дубликат.
|
||
- `delete_orphaned()` — вызывается? Где?
|
||
|
||
---
|
||
|
||
## Файл 13: `contractor/deploy/db/spec_events.py`
|
||
|
||
**Что это:** Event Sourcing.
|
||
|
||
**Что смотреть:**
|
||
- `get_next_seq()` — `MAX(seq)+1`. Гонка! Два потока → одинаковый seq.
|
||
- `reset()` — необратимо.
|
||
- `apply_ops()` — валидация структуры?
|
||
|
||
---
|
||
|
||
## Файл 14: `contractor/deploy/db/spec_current.py`
|
||
|
||
**Что это:** Текущая спецификация.
|
||
|
||
**Что смотреть:**
|
||
- Кто обновляет spec_current после удаления spec_events?
|
||
- `get_elements_json()` — где используется?
|
||
|
||
---
|
||
|
||
## Файл 15: `contractor/deploy/db/prompts.py`
|
||
|
||
**Что это:** CRUD промптов с версионированием.
|
||
|
||
**Что смотреть:**
|
||
- `seed_defaults()` — `if count>0: return` — если есть extract но нет classify → classify не создастся.
|
||
- `_ensure_classify_prompt()` — отдельный механизм, почему?
|
||
- `save_new_version()` — UPDATE+INSERT не атомарно. Гонка.
|
||
- `activate()` — гонка.
|
||
- `delete_prompt()` — проверка is_active потом DELETE. Гонка.
|
||
- `_serialize()` — мутирует словарь.
|
||
|
||
---
|
||
|
||
## Файл 16: `contractor/deploy/llm_prompt.py`
|
||
|
||
**Что это:** Билдер промптов.
|
||
|
||
**Что смотреть:**
|
||
- `build_prompt()` — f-string с данными парсинга. Безопасно?
|
||
- `_fetch_prompt()` — HTTP к Lucee. Таймаут? Fallback при недоступности?
|
||
- `build_classify_prompt()` — прямой доступ к БД, не через HTTP. Почему?
|
||
- FALLBACK_* хардкод — дублирование с БД.
|
||
|
||
---
|
||
|
||
## Файл 17: `contractor/index.cfm`
|
||
|
||
**Что это:** HTML-оболочка.
|
||
|
||
**Что смотреть:**
|
||
- `?v=1.0.175` хардкод — менять при каждом обновлении JS.
|
||
- pipelineStepper — ○⏳✓ через replace. Надёжно?
|
||
- XSS через prompt body в textarea/div?
|
||
- Z-index конфликты модалок?
|
||
- inline onclick — ломаются при перезагрузке JS?
|
||
|
||
---
|
||
|
||
## СВОДНАЯ ТАБЛИЦА (выдать после пофайлового разбора)
|
||
|
||
| # | Файл:строка | Серьёзность | Тип | Описание | Как исправить |
|
||
|---|-------------|-------------|-----|----------|---------------|
|
||
|
||
## ТОП-5 (срочно)
|
||
|
||
5 проблем, которые надо чинить прямо сейчас.
|