chore: initial commit — recon artifacts + design spec + Phase 1 plan
@@ -0,0 +1,40 @@
|
|||||||
|
# Recon artifacts (large binaries, not for code review / not shareable)
|
||||||
|
recon/artifacts/
|
||||||
|
recon/patched/
|
||||||
|
*.apk
|
||||||
|
*.xapk
|
||||||
|
*.mitm
|
||||||
|
*.har
|
||||||
|
*.dmp
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
minio.env
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.vite/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# PremiumDriverApp
|
||||||
|
|
||||||
|
Замена вендорного Android-приложения для водителей PremiumPark, с привязкой к Premium CRM (TaxiDashboard).
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Текущее приложение для водителей поставляется подрядчиком 1С на условиях абонентки. Подрядчик заморозил разработку. Цель проекта — построить собственное приложение, сохранив совместимость со старым функционалом (балансы, штрафы, платежи) и добавив новые функции (эксплуатация авто).
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
PremiumDriverApp/
|
||||||
|
├── docs/superpowers/specs/ Дизайн-документы (spec'и) по этапам
|
||||||
|
├── recon/ Подпроект «Разведка» — изучение старого приложения
|
||||||
|
│ ├── artifacts/ APK, декомпил, дампы трафика (не в git)
|
||||||
|
│ ├── scripts/ mitmproxy фильтры, frida-скрипты
|
||||||
|
│ └── findings/ Карта API, диаграммы, отчёты
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Текущая фаза
|
||||||
|
|
||||||
|
**Подпроект «Разведка»** — см. [docs/superpowers/specs/2026-05-11-driver-app-recon-design.md](docs/superpowers/specs/2026-05-11-driver-app-recon-design.md).
|
||||||
|
|
||||||
|
Следующие подпроекты («Новое приложение», «BFF», «Платёжный канал») запускаются после утверждения отчёта разведки.
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
# Driver App Recon — Design Doc
|
||||||
|
|
||||||
|
- **Дата:** 2026-05-11
|
||||||
|
- **Статус:** Draft, ожидает review владельца
|
||||||
|
- **Владелец:** vladtechno@gmail.com (PremiumPark)
|
||||||
|
- **Родительский проект:** PremiumDriverApp
|
||||||
|
- **Экосистема:** Premium CRM (TaxiDashboard)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Контекст и проблема
|
||||||
|
|
||||||
|
У PremiumPark есть Android-приложение для водителей проката, поставляемое подрядчиком 1С на условиях абонентской платы. Текущее приложение умеет:
|
||||||
|
|
||||||
|
- авторизовать водителя (логин/пароль),
|
||||||
|
- показывать остаток баланса и список штрафов,
|
||||||
|
- принимать платежи через приложение (детали неизвестны, выяснит разведка).
|
||||||
|
|
||||||
|
Все оплаты от водителей принимает юр.лицо PremiumPark. Подрядчик прекратил доработки приложения, оставаясь при этом получателем абонентской платы. Никаких функций эксплуатации авто (ТО, ремонты, фото повреждений, заявки в техслужбу) в приложении нет.
|
||||||
|
|
||||||
|
Стратегическая цель — построить собственное приложение, интегрированное с Premium CRM, и постепенно вытащить интеллектуальную собственность из 1С, сняв зависимость от подрядчика.
|
||||||
|
|
||||||
|
**Настоящий документ описывает только подпроект «Разведка»** — фазу изучения существующего приложения, предшествующую любой разработке. Подпроект «Новое приложение» имеет отдельный дизайн-док, который пишется после завершения разведки.
|
||||||
|
|
||||||
|
## 2. Цели и не-цели
|
||||||
|
|
||||||
|
### Цели
|
||||||
|
|
||||||
|
1. Получить полную карту HTTP/HTTPS-эндпоинтов, к которым ходит существующее приложение.
|
||||||
|
2. Документировать поток аутентификации и lifecycle сессии/токенов.
|
||||||
|
3. Документировать платёжный поток end-to-end: провайдер, инициация, токенизация, фискализация (54-ФЗ), связь с балансом в 1С.
|
||||||
|
4. Документировать механизм обновлений (poll / push / FCM) — как водитель узнаёт о новом штрафе.
|
||||||
|
5. Идентифицировать защитные механизмы (TLS pinning, обфускация, root detection, request signing, Play Integrity).
|
||||||
|
6. Принять решение go/no-go по подпроекту «Новое приложение» на основе фактов.
|
||||||
|
|
||||||
|
### Не-цели
|
||||||
|
|
||||||
|
- Не разрабатываем код нового приложения, BFF или платёжного канала.
|
||||||
|
- Не модифицируем APK и не публикуем «свой клиент».
|
||||||
|
- Не подключаем mitmproxy к боевому трафику реальных водителей — работаем только на тестовом аккаунте, заведённом в 1С специально для разведки.
|
||||||
|
- Не передаём APK, дампы, decompiled-исходники или отчёты третьим лицам.
|
||||||
|
- Не реализуем платёжный канал в этом подпроекте.
|
||||||
|
|
||||||
|
## 3. Юридическая рамка
|
||||||
|
|
||||||
|
- Декомпиляция выполняется на собственном устройстве/эмуляторе пользователя, с собственным тестовым аккаунтом, для целей совместимости и переноса данных — попадает под ст. 1280 ГК РФ (декомпилирование программы для ЭВМ для interoperability).
|
||||||
|
- PremiumPark является плательщиком абонентской платы за продукт и владельцем данных (балансы, штрафы, платежи водителей).
|
||||||
|
- Артефакты разведки хранятся локально, в `recon/artifacts/`, не публикуются в git (см. `.gitignore`).
|
||||||
|
- В случае официального запроса от подрядчика — разведка приостанавливается, артефакты архивируются.
|
||||||
|
|
||||||
|
## 4. Технический стенд
|
||||||
|
|
||||||
|
### 4.1 Хост
|
||||||
|
|
||||||
|
- Windows 11 (рабочая машина владельца)
|
||||||
|
- Python 3.10+ (для mitmproxy и frida-tools)
|
||||||
|
- Android Studio с Emulator и Platform-tools (`adb`)
|
||||||
|
- Свободное место: ≥30 GB (эмулятор + APK + дампы + декомпил)
|
||||||
|
|
||||||
|
### 4.2 Эмулятор Android
|
||||||
|
|
||||||
|
- **AVD:** Pixel 6 или Pixel 4
|
||||||
|
- **System Image:** Google APIs (НЕ Google Play) — это критично, иначе `adb root` недоступен и не получится положить mitmproxy CA в системные сертификаты
|
||||||
|
- **API Level:** 33 или 34
|
||||||
|
- **Архитектура:** x86_64 (для совместимости с frida-server)
|
||||||
|
|
||||||
|
При обнаружении сильной анти-эмуляторной защиты допустимо переключение на физическое Android-устройство с Magisk; это указано как fallback в плане исполнения.
|
||||||
|
|
||||||
|
### 4.3 Инструменты
|
||||||
|
|
||||||
|
| Инструмент | Назначение | Установка |
|
||||||
|
|---|---|---|
|
||||||
|
| `jadx-gui` | Декомпиляция APK в Java | github.com/skylot/jadx/releases |
|
||||||
|
| `apktool` | Распаковка манифеста и ресурсов | scoop install apktool |
|
||||||
|
| `mitmproxy` | Перехват HTTPS-трафика | `pip install mitmproxy` |
|
||||||
|
| `frida-tools` | Динамические хуки | `pip install frida-tools` |
|
||||||
|
| `objection` | Frida-обёртка с готовыми bypass'ами | `pip install objection` |
|
||||||
|
| `adb` | Управление эмулятором | Android SDK Platform-tools |
|
||||||
|
|
||||||
|
Конкретные команды установки и шаги настройки идут в план исполнения (writing-plans), не в спеку.
|
||||||
|
|
||||||
|
### 4.4 Файловая структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
PremiumDriverApp/
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore # игнорирует recon/artifacts/
|
||||||
|
├── docs/superpowers/specs/
|
||||||
|
│ └── 2026-05-11-driver-app-recon-design.md # этот документ
|
||||||
|
└── recon/
|
||||||
|
├── artifacts/ # НЕ в git
|
||||||
|
│ ├── original.apk # вытащенный APK
|
||||||
|
│ ├── decompiled/ # вывод jadx
|
||||||
|
│ ├── unpacked/ # вывод apktool
|
||||||
|
│ └── flows/ # mitmproxy дампы (*.mitm)
|
||||||
|
├── scripts/
|
||||||
|
│ ├── frida-bypass-pinning.js # шаблон обхода TLS pinning
|
||||||
|
│ ├── mitm-filter.py # фильтр mitmproxy для шумовых хостов
|
||||||
|
│ └── setup-emulator.ps1 # подготовка AVD
|
||||||
|
└── findings/
|
||||||
|
├── 00-final-report.md # финальный отчёт + go/no-go
|
||||||
|
├── 01-static-analysis.md
|
||||||
|
├── 02-network-capture.md
|
||||||
|
├── 03-auth-flow.md
|
||||||
|
├── 04-payment-flow.md
|
||||||
|
├── 05-realtime.md
|
||||||
|
└── 06-defenses.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Этапы разведки
|
||||||
|
|
||||||
|
### Этап 1. Статический анализ APK
|
||||||
|
|
||||||
|
**Цель:** понять, что в принципе делает приложение, найти константы (URL, ключи провайдеров, package id), оценить уровень защиты.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
|
||||||
|
1. Получить APK: `adb shell pm path <package>` → `adb pull <path> recon/artifacts/original.apk`
|
||||||
|
2. Декомпиляция: `jadx-gui recon/artifacts/original.apk` → `recon/artifacts/decompiled/`
|
||||||
|
3. Распаковка манифеста: `apktool d original.apk -o recon/artifacts/unpacked/`
|
||||||
|
4. Извлечь из manifest и кода:
|
||||||
|
- `package` name, version, target SDK, min SDK
|
||||||
|
- permissions (особенно `INTERNET`, `RECEIVE_SMS`, `ACCESS_FINE_LOCATION`, payment-related)
|
||||||
|
- `network_security_config.xml` (cleartext policy, pinning declarations)
|
||||||
|
- встроенные base-URL — grep по `https://`
|
||||||
|
- сторонние SDK — поиск пакетов `ru.yoomoney`, `ru.tinkoff`, `com.google.firebase`, `okhttp3`, `retrofit2`
|
||||||
|
5. Оценить обфускацию: имена классов читаемые → R8/ProGuard не агрессивный; одно-/двухбуквенные имена → агрессивный.
|
||||||
|
6. Найти root/emulator detection: grep по `RootBeer`, `isDebuggerConnected`, `build.fingerprint`, `Build.PRODUCT`.
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/01-static-analysis.md` со следующими разделами:
|
||||||
|
- Package metadata
|
||||||
|
- Permissions
|
||||||
|
- Network config
|
||||||
|
- Встроенные URL-ы и хосты
|
||||||
|
- Сторонние SDK
|
||||||
|
- Уровень обфускации
|
||||||
|
- Защитные механизмы (предварительный список)
|
||||||
|
|
||||||
|
### Этап 2. Перехват сетевого трафика
|
||||||
|
|
||||||
|
**Цель:** получить полный лог HTTP/HTTPS-запросов в обычных пользовательских сценариях.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
|
||||||
|
1. Подготовить эмулятор:
|
||||||
|
- `emulator -avd Pixel_6_API_34 -writable-system -no-snapshot`
|
||||||
|
- `adb root` → доступ к /system
|
||||||
|
- Установить mitmproxy CA в `/system/etc/security/cacerts/` (имя файла — hash сертификата + `.0`)
|
||||||
|
2. Прописать прокси: `Settings → Network → WiFi → Modify → Proxy: manual → 10.0.2.2:8080`
|
||||||
|
3. Запустить mitmproxy на хосте: `mitmweb --listen-port 8080`
|
||||||
|
4. Установить приложение: `adb install recon/artifacts/original.apk`
|
||||||
|
5. Прогнать сценарии (каждый — отдельным mitmproxy save):
|
||||||
|
- **S1. Вход:** запуск приложения → ввод логина/пароля → попадание на главный экран
|
||||||
|
- **S2. Просмотр баланса:** открытие экрана баланса, ожидание 60 сек (захватить полл, если есть)
|
||||||
|
- **S3. Просмотр штрафов:** открытие списка штрафов, открытие одного штрафа
|
||||||
|
- **S4. Экран платежа (без оплаты):** перейти к оплате, выбрать позицию, дойти до выбора способа оплаты, остановиться
|
||||||
|
- **S5. Инициация платежа:** довести до момента подтверждения, **но не платить реальной картой**
|
||||||
|
- **S6. Выход:** logout
|
||||||
|
6. Сохранить flow'ы: `recon/artifacts/flows/S1-login.mitm`, `S2-balance.mitm`, …
|
||||||
|
|
||||||
|
**При TLS pinning:**
|
||||||
|
- Запустить frida-server в эмуляторе.
|
||||||
|
- `objection --gadget <package> explore` → `android sslpinning disable`
|
||||||
|
- Если не помогает — кастомный Frida-скрипт (шаблон в `recon/scripts/frida-bypass-pinning.js`)
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/02-network-capture.md` с таблицей всех увиденных эндпоинтов:
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth | Сценарий | Запрос (схема) | Ответ (схема) | Частота |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
|
||||||
|
И ссылки на исходные mitm-файлы для воспроизведения.
|
||||||
|
|
||||||
|
### Этап 3. Глубокое погружение в auth
|
||||||
|
|
||||||
|
**Цель:** понять формат и lifecycle токена, наличие подписей.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
|
||||||
|
1. Распарсить токен (jwt.io если JWT) — header, payload, claims, alg, iss, exp.
|
||||||
|
2. Проверить, есть ли request signing (HMAC, подпись тела) — искать в декомпиле использование `Mac.getInstance` / `Signature`.
|
||||||
|
3. Сэмплировать сессии:
|
||||||
|
- повторный логин → тот же токен или новый?
|
||||||
|
- есть ли refresh-токен и отдельный refresh-эндпоинт?
|
||||||
|
- что происходит при 401?
|
||||||
|
4. Построить sequence diagram (Mermaid).
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/03-auth-flow.md`
|
||||||
|
|
||||||
|
### Этап 4. Платёжная подсистема (центральный этап)
|
||||||
|
|
||||||
|
**Цель:** разобрать поток оплаты end-to-end, выявить провайдера и механизм фискализации.
|
||||||
|
|
||||||
|
**Гипотезы, которые проверяем:**
|
||||||
|
|
||||||
|
- H1. Приложение использует SDK провайдера (YooKassa / Tinkoff Acquiring / СберPay) и общается напрямую с провайдером после получения init-токена от 1С.
|
||||||
|
- H2. Приложение прокидывает данные в 1С, а 1С общается с провайдером (приложение видит только наши эндпоинты).
|
||||||
|
- H3. Гибрид: init через 1С → провайдер через SDK → колбэк в 1С.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
|
||||||
|
1. В декомпиле найти SDK провайдера (по package name).
|
||||||
|
2. В трафике (этап 2, сценарий S5) изолировать запросы к провайдерским доменам vs к 1С.
|
||||||
|
3. Проследить токенизацию: куда уходят данные карты — в провайдер напрямую или в 1С (если последнее — большой ред-флаг по PCI DSS).
|
||||||
|
4. Понять чек 54-ФЗ:
|
||||||
|
- кто формирует — 1С / провайдер / ОФД напрямую?
|
||||||
|
- где водитель видит чек (push, email, экран приложения)?
|
||||||
|
5. Корреляция платёж ↔ баланс:
|
||||||
|
- до платежа `GET /balance` → X
|
||||||
|
- инициация → колбэк → когда `GET /balance` возвращает X + delta? (сразу/асинхронно/после явного pull)
|
||||||
|
6. Sequence diagram (Mermaid) с участниками: App, 1С, Provider, ОФД.
|
||||||
|
|
||||||
|
**Безопасность разведки:** не доводить до фактического списания. Останавливаемся на моменте, когда приложение запрашивает у провайдера init-токен и формирует webview/redirect — этого достаточно для понимания флоу.
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/04-payment-flow.md`
|
||||||
|
|
||||||
|
### Этап 5. Push / realtime
|
||||||
|
|
||||||
|
**Цель:** понять, как водитель узнаёт о новом штрафе/начислении.
|
||||||
|
|
||||||
|
**Действия:**
|
||||||
|
|
||||||
|
- Проверить наличие FCM в манифесте (`com.google.firebase.MESSAGING_EVENT`).
|
||||||
|
- Если FCM — посмотреть payload (требует регистрации в FCM или симуляции).
|
||||||
|
- Если нет FCM — проверить наличие long-polling эндпоинта в трафике.
|
||||||
|
- Если возможно — попросить ответственного администратора 1С создать тестовый штраф для тестового водителя и засечь, как именно эта инфа приходит.
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/05-realtime.md`
|
||||||
|
|
||||||
|
### Этап 6. Защитные механизмы (фоном)
|
||||||
|
|
||||||
|
В течение всех этапов фиксировать в `recon/findings/06-defenses.md`:
|
||||||
|
|
||||||
|
- TLS pinning — есть / нет / способ обхода
|
||||||
|
- Root/emulator detection — есть / нет / срабатывает ли на нашем стенде
|
||||||
|
- Request signing / HMAC — есть / нет / откуда берётся ключ
|
||||||
|
- Play Integrity API — есть / нет
|
||||||
|
- Антидебаг и анти-Frida хуки
|
||||||
|
|
||||||
|
### Этап 7. Финальный отчёт
|
||||||
|
|
||||||
|
**Deliverable:** `recon/findings/00-final-report.md`. Структура:
|
||||||
|
|
||||||
|
1. **Executive Summary** (1 страница, для бизнес-обсуждения)
|
||||||
|
2. **Полная карта API** (агрегированная таблица из этапа 2)
|
||||||
|
3. **Auth-flow** (Mermaid + текст)
|
||||||
|
4. **Payment-flow** (Mermaid + текст, ключевая часть)
|
||||||
|
5. **Realtime** (Mermaid + текст)
|
||||||
|
6. **Защитные механизмы и их преодоление**
|
||||||
|
7. **Оценка трудозатрат на «Новое приложение»** (грубая, в неделях разработчика)
|
||||||
|
8. **Рекомендация go/no-go**
|
||||||
|
9. **Открытые риски и неизвестные**
|
||||||
|
|
||||||
|
## 6. Критерии go / no-go
|
||||||
|
|
||||||
|
### Go — двигаемся в подпроект «Новое приложение»
|
||||||
|
|
||||||
|
- API эндпоинты и аутентификация поняты, стабильны (нет признаков частой смены контракта).
|
||||||
|
- Платёжный поток разобран, и видна возможность построить параллельный канал (свой эквайринг → свой backend → свой чек).
|
||||||
|
- Защитные механизмы преодолимы статически (т.е. мы понимаем, как мимикрировать или обойти без runtime-вмешательства).
|
||||||
|
- В трафике нет признаков hardware-bound криптографии, доступной только подрядчику.
|
||||||
|
|
||||||
|
### No-go — пересмотр стратегии
|
||||||
|
|
||||||
|
- API использует interactive challenge, который нельзя автоматизировать (CAPTCHA, манипуляция UI и пр.).
|
||||||
|
- Платёжный поток жёстко зависит от 1С на стороне сервера, и параллельный канал требует доступа к серверным обработкам.
|
||||||
|
- Используется криптография, привязанная к серверному секрету подрядчика (например, server-issued tokens с проверкой подписи приложения).
|
||||||
|
- Уровень защиты несоразмерен ценности — реверс займёт больше, чем оригинальная разработка с нуля.
|
||||||
|
|
||||||
|
### В случае no-go — альтернативы
|
||||||
|
|
||||||
|
1. **Прямые переговоры с подрядчиком** о публикации API (на руках уже будет отчёт разведки — рычаг в диалоге).
|
||||||
|
2. **Companion App** — приложение **только с новыми функциями** (блок B: эксплуатация авто), старое приложение остаётся для балансов/платежей. Реверс не нужен.
|
||||||
|
3. **Полная замена 1С** — крупный проект, не на этом цикле.
|
||||||
|
|
||||||
|
## 7. Open questions (на разведку)
|
||||||
|
|
||||||
|
Заполняются по ходу. Начальный список:
|
||||||
|
|
||||||
|
- Package name приложения — будет известно из APK на этапе 1.
|
||||||
|
- Платёжный провайдер — выясняется на этапе 4.
|
||||||
|
- Частота поллинга баланса — выясняется на этапе 2.
|
||||||
|
- Используется ли FCM — выясняется на этапе 5.
|
||||||
|
- Уровень обфускации — выясняется на этапе 1.
|
||||||
|
|
||||||
|
## 8. Следующий шаг
|
||||||
|
|
||||||
|
После утверждения этой спеки владельцем:
|
||||||
|
|
||||||
|
1. Создаём подробный план исполнения через `writing-plans` skill — пошагово, с чёткими DoD по каждому этапу.
|
||||||
|
2. Скрипты-заготовки в `recon/scripts/`: `setup-emulator.ps1`, `frida-bypass-pinning.js`, `mitm-filter.py`.
|
||||||
|
3. README с инструкцией «как развернуть стенд на пустой машине».
|
||||||
|
4. Запускаем этап 1 (статический анализ).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приложение A. Контактные точки с экосистемой Premium CRM
|
||||||
|
|
||||||
|
Этот подпроект ничего не пишет в боевую инфраструктуру (PremiumCRM, PostgreSQL, n8n, ASR-стек) и ничего не читает оттуда. Разведка изолирована — все артефакты локальны.
|
||||||
|
|
||||||
|
Контактные точки появятся **только** в подпроекте «Новое приложение»:
|
||||||
|
|
||||||
|
- FastAPI backend PremiumCRM получит новые роуты `/api/v1/driver/*`.
|
||||||
|
- PostgreSQL получит новые таблицы (maintenance, repairs, tickets, photos, driver_external_ids).
|
||||||
|
- Дейв (user_id=26) может стать author'ом авто-комментариев в новых сущностях.
|
||||||
|
- Telegram-бот @p_park_bot — потенциальный канал доставки OTP при логине нового приложения (TBD на этапе дизайна нового приложения).
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
# Premium Механик — PWA Design Doc
|
||||||
|
|
||||||
|
- **Дата:** 2026-05-16
|
||||||
|
- **Статус:** Draft, ожидает review владельца
|
||||||
|
- **Владелец:** vladtechno@gmail.com (PremiumPark)
|
||||||
|
- **Родительский проект:** PremiumDriverApp
|
||||||
|
- **Экосистема:** Premium CRM (TaxiDashboard) на crm.pptaxi.ru
|
||||||
|
- **Предшественник:** реверс-разведка приложения «Механик» NaughtySoft (см. `01-static-analysis.md` + перехват трафика)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Контекст и цель
|
||||||
|
|
||||||
|
Сотрудники-механики PremiumPark сейчас работают через приложение «Механик» вендора NaughtySoft (`com.naughtysoft.ttc`, Xamarin/.NET). Все данные осмотров уходят в 1С вендора, доступ к ним из PremiumCRM невозможен без прохода через вендорские API.
|
||||||
|
|
||||||
|
Через разведку (см. `02-network-capture.md`) подтверждён формат backend-API вендора (REST на `api.ttcontrol.naughtysoft.ru`). Эндпоинты осмотров (`/api/vehicletechinspection/byvehicle`, `/api/vehiclestate/history`, `/api/image/<id>`, `/api/inspectionvideo`) подтверждают, что **модель данных осмотров с координатами повреждений и фото уже существует и нам полностью видна**.
|
||||||
|
|
||||||
|
**Цель проекта:** построить **собственное** приложение «Premium Механик» (PWA), которое:
|
||||||
|
|
||||||
|
1. Полностью заменяет вендорское приложение для сотрудников-механиков.
|
||||||
|
2. Хранит данные осмотров **в нашей PostgreSQL** в PremiumCRM, не отправляя их к вендору.
|
||||||
|
3. Поддерживает основные кейсы: съёмка фото авто, обводка повреждений на фото, метки повреждений с координатами, сравнение с прошлым осмотром при приёмке.
|
||||||
|
4. Развёртывается без RuStore / App Store — деплой = `git push`.
|
||||||
|
|
||||||
|
**Долгосрочная цель:** отвязать парк от 1С вендора, начав с самой ценной модели данных (осмотры с фото).
|
||||||
|
|
||||||
|
## 2. Цели и не-цели
|
||||||
|
|
||||||
|
### Цели
|
||||||
|
|
||||||
|
1. Делать осмотр авто: 4-8 фото по сторонам, метки и обводки повреждений на каждой фото.
|
||||||
|
2. При приёмке машины обратно от водителя — показать прошлые повреждения для сравнения «было / стало».
|
||||||
|
3. Все данные осмотра хранятся в PostgreSQL PremiumCRM (новая БД-схема), без участия вендора.
|
||||||
|
4. Доступ для авторизованных сотрудников-механиков (через существующий auth PremiumCRM).
|
||||||
|
5. Работает на Android-планшетах механиков, на iPad/iPhone, на десктопе — без раздельных сборок.
|
||||||
|
|
||||||
|
### Не-цели
|
||||||
|
|
||||||
|
- Не делаем приложение для водителя в рамках этого подпроекта (отдельная фаза).
|
||||||
|
- Не интегрируем фактическую интеграцию обратно в 1С вендора (наоборот — уходим от неё).
|
||||||
|
- Не делаем нативные APK / IPA в первой версии. Только PWA. Возможен Capacitor-fallback в фазе 3 при необходимости.
|
||||||
|
- Не делаем offline-first в MVP (механик работает с WiFi в парке). Service Worker — задел на будущее.
|
||||||
|
- Не делаем real-time чат / уведомления через push на iOS (сложно, неприоритетно).
|
||||||
|
- Не импортируем существующие осмотры из вендора в первой версии (отдельная задача миграции данных).
|
||||||
|
|
||||||
|
## 3. Технологический стек
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
| Слой | Выбор | Обоснование |
|
||||||
|
|---|---|---|
|
||||||
|
| Build/Dev | **Vite + TypeScript** | Быстрая итерация, типизация. Стек, уже знакомый команде. |
|
||||||
|
| UI Framework | **React 18** | Команда уже работает с React (SuperLanding на Astro + React islands). |
|
||||||
|
| UI Library | **shadcn/ui + Tailwind CSS** | Совпадает с design preferences (cream palette, Inter only, без жёлтого, без emoji). |
|
||||||
|
| State Client | **TanStack Query (React Query)** | Кэширование fetches, optimistic updates, retries. |
|
||||||
|
| Local State | **Zustand** | Минималистичный store без boilerplate. |
|
||||||
|
| Routing | **React Router 7** | Standard в React экосистеме. |
|
||||||
|
| Forms | **React Hook Form + Zod** | Типизированная валидация, лёгкий контроль. |
|
||||||
|
| Canvas/Annotation | **Konva.js + react-konva** | Лучший выбор для photo annotations: layers, touch, gestures, undo/redo. |
|
||||||
|
| HTTP | **Native fetch + ky или axios** | TBD на старте. |
|
||||||
|
| PWA | **vite-plugin-pwa** | Service Worker, web manifest, install prompt — из коробки. |
|
||||||
|
| QR-сканер | **html5-qrcode** | Для сканирования госномера-стикера машины. |
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
| Слой | Выбор |
|
||||||
|
|---|---|
|
||||||
|
| Расширение | Новый модуль `app/mechanic/` в FastAPI PremiumCRM |
|
||||||
|
| Базовый URL | `/api/v1/mechanic/*` |
|
||||||
|
| ORM | SQLAlchemy (используется в TaxiDashboard) |
|
||||||
|
| Миграции | Alembic |
|
||||||
|
| Auth | Существующий PremiumCRM (JWT cookie + bcrypt PIN) — переиспользуем |
|
||||||
|
| Permissions | Существующая dept-based система permissions (см. memory: `project_taxidashboard_permissions`) |
|
||||||
|
| Файловое хранилище | **MinIO** на 100.64.0.12 (контейнер `taxi-minio`). Bucket **`pp-inspections`** (уже создан). Endpoint `s3.pptaxi.ru` (HTTPS), admin UI `s3-admin.pptaxi.ru`. Креды в `/opt/sites/taxi-dashboard/minio.env`. |
|
||||||
|
| S3 client | **`aiobotocore`** — переиспользуем существующую интеграцию из TaxiDashboard (она же используется для фото осмотров уже). |
|
||||||
|
| Upload-схема | **Pre-signed POST** — фронтенд льёт фото напрямую в MinIO, минуя FastAPI. Backend выдаёт presigned URL + поля. |
|
||||||
|
| Read-схема | Существующий nginx-proxy `/media/<key>` на VDS (уже работает для других фото TaxiDashboard) ИЛИ pre-signed GET с TTL 1 час. Решим на этапе implementation. |
|
||||||
|
| Image-processing | Pillow для thumbnails и EXIF strip — запускается celery-task'ом после `confirm` upload'а. |
|
||||||
|
| Дополнительно | **Только MinIO**, без локального дубля на FS (решение владельца). Расхождение с существующим dual-write-паттерном в TaxiDashboard оправдано тяжестью медиа осмотров (10-20 фото на осмотр) — забивать диск VDS дублями нецелесообразно. |
|
||||||
|
|
||||||
|
### Развёртывание
|
||||||
|
|
||||||
|
| Хост | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| 100.64.0.12 (PremiumCRM VDS) | Backend + DB + статика фото |
|
||||||
|
| Caddy | Reverse proxy + TLS + raw уролы для фото |
|
||||||
|
| Поддомен | `mechanic.pptaxi.ru` либо `crm.pptaxi.ru/mechanic/` (TBD, см. open question) |
|
||||||
|
|
||||||
|
## 4. Модель данных
|
||||||
|
|
||||||
|
Новые таблицы в существующей `taxi_dashboard` PostgreSQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Осмотр
|
||||||
|
CREATE TABLE inspections (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
vehicle_id BIGINT NOT NULL REFERENCES vehicles(id),
|
||||||
|
type VARCHAR(32) NOT NULL, -- 'handover'|'return'|'periodic'|'ad-hoc'
|
||||||
|
performed_by INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
driver_id INTEGER REFERENCES users(id),
|
||||||
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
finished_at TIMESTAMPTZ,
|
||||||
|
status VARCHAR(16) NOT NULL DEFAULT 'in_progress', -- 'in_progress'|'completed'|'cancelled'
|
||||||
|
notes TEXT,
|
||||||
|
prev_inspection_id BIGINT REFERENCES inspections(id), -- ссылка на предыдущий осмотр того же авто
|
||||||
|
meta JSONB DEFAULT '{}'::jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_inspections_vehicle_started ON inspections(vehicle_id, started_at DESC);
|
||||||
|
|
||||||
|
-- Фото осмотра
|
||||||
|
CREATE TABLE inspection_photos (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
|
||||||
|
side VARCHAR(16) NOT NULL, -- 'front'|'rear'|'left'|'right'|'interior'|'vin'|'odometer'|'wheel-fl'|'wheel-fr'|'wheel-rl'|'wheel-rr'|'free'
|
||||||
|
slot_index INT DEFAULT 0,
|
||||||
|
storage_key TEXT NOT NULL, -- путь относительно /opt/premiumcrm/uploads/
|
||||||
|
annotated_key TEXT, -- путь к фото с canvas-обводкой (опц.)
|
||||||
|
thumb_key TEXT,
|
||||||
|
width INT,
|
||||||
|
height INT,
|
||||||
|
bytes BIGINT,
|
||||||
|
taken_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
display_order INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Метка повреждения (на фото и/или на схеме авто)
|
||||||
|
CREATE TABLE damage_markers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
|
||||||
|
photo_id BIGINT REFERENCES inspection_photos(id), -- NULL если только на схеме
|
||||||
|
side VARCHAR(16), -- сторона авто
|
||||||
|
x DECIMAL(6,4), -- normalized 0..1 в координатах фото или схемы
|
||||||
|
y DECIMAL(6,4),
|
||||||
|
polygon JSONB, -- опционально: координаты обведённого контура [{x,y}, ...]
|
||||||
|
damage_type VARCHAR(32), -- 'scratch'|'dent'|'paint'|'crack'|'missing'|'rust'|'glass'
|
||||||
|
severity VARCHAR(16), -- 'cosmetic'|'minor'|'moderate'|'severe'
|
||||||
|
description TEXT,
|
||||||
|
carried_over_from_id BIGINT REFERENCES damage_markers(id), -- если унаследовано от прошлого осмотра
|
||||||
|
resolved BOOLEAN DEFAULT FALSE, -- было ли устранено
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_damage_markers_inspection ON damage_markers(inspection_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ключевая идея для дифа «было/стало»:**
|
||||||
|
|
||||||
|
- При создании return-inspection backend копирует все `damage_markers` из `prev_inspection_id` со ссылкой `carried_over_from_id`. Механик может либо подтвердить «есть» (оставить), либо отметить «устранено» (`resolved=true`), либо добавить новые маркеры. На фронте отображается две группы: «было раньше» (полупрозрачные) + «новые» (яркие).
|
||||||
|
|
||||||
|
## 5. API контракт
|
||||||
|
|
||||||
|
Все endpoints под `/api/v1/mechanic/`. Auth — через существующий JWT cookie PremiumCRM. Permission gate: `mechanic:inspect` (новое право в матрице, добавляется в модуль permissions).
|
||||||
|
|
||||||
|
### Машины
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /vehicles?q=<гос-номер|VIN>&limit=20
|
||||||
|
→ [{id, license_plate, vin, make, model, year, current_driver, last_inspection_at}]
|
||||||
|
|
||||||
|
GET /vehicles/{id}
|
||||||
|
→ детали авто + список последних осмотров
|
||||||
|
|
||||||
|
GET /vehicles/{id}/last-inspection
|
||||||
|
→ последний завершённый осмотр (для дифа при приёмке)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Осмотры
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /inspections
|
||||||
|
body: {vehicle_id, type, driver_id?}
|
||||||
|
→ {id, ...} создан в статусе in_progress; если type='return', копирует markers из prev
|
||||||
|
|
||||||
|
GET /inspections/{id}
|
||||||
|
→ полная карточка с photos + markers + meta
|
||||||
|
|
||||||
|
PATCH /inspections/{id}
|
||||||
|
body: {status?, notes?, finished_at?}
|
||||||
|
|
||||||
|
POST /inspections/{id}/photos/upload-url
|
||||||
|
body: {side, slot_index, content_type, size}
|
||||||
|
→ {photo_id, presigned_url, fields, storage_key, expires_at}
|
||||||
|
# фронт льёт сам файл напрямую в MinIO через presigned_url
|
||||||
|
|
||||||
|
POST /photos/{photo_id}/confirm
|
||||||
|
body: {actual_size?, width?, height?}
|
||||||
|
→ {id, storage_key, thumb_key, status: 'processing'|'ready'}
|
||||||
|
# после confirm backend ставит celery-task на thumbnail и EXIF strip
|
||||||
|
|
||||||
|
POST /inspections/{id}/markers
|
||||||
|
body: {photo_id?, side, x, y, polygon?, damage_type, severity, description}
|
||||||
|
→ {id, ...}
|
||||||
|
|
||||||
|
PATCH /markers/{id}
|
||||||
|
body: {damage_type?, severity?, description?, polygon?, resolved?}
|
||||||
|
|
||||||
|
DELETE /markers/{id}
|
||||||
|
|
||||||
|
POST /photos/{id}/annotated/upload-url
|
||||||
|
body: {content_type, size}
|
||||||
|
→ {presigned_url, fields, storage_key, expires_at}
|
||||||
|
# аналогично: фронт льёт annotated-версию напрямую в MinIO
|
||||||
|
|
||||||
|
POST /photos/{id}/annotated/confirm
|
||||||
|
→ {id, annotated_key, status: 'ready'}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Чтение фото
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /photos/{id} → 302 Redirect на pre-signed S3 URL (TTL 1 час)
|
||||||
|
GET /photos/{id}/thumb → 302 Redirect на pre-signed S3 URL thumbnail
|
||||||
|
GET /photos/{id}/annotated → 302 Redirect на pre-signed S3 URL annotated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Меня (текущий механик)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /me
|
||||||
|
→ {id, name, dept, permissions: [...], avatar_url}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. UI экраны и поток
|
||||||
|
|
||||||
|
### Экран 1. Login
|
||||||
|
PIN-вход в PremiumCRM (как уже работает в TaxiDashboard).
|
||||||
|
|
||||||
|
### Экран 2. Главная — выбор машины
|
||||||
|
- Поле поиска по госномеру/VIN
|
||||||
|
- Кнопка «Сканировать QR» (стикер на лобовом стекле машины с госномером)
|
||||||
|
- Список последних 10 машин, с которыми работал этот механик
|
||||||
|
- На каждой карточке: гос-номер, модель, водитель, дата последнего осмотра
|
||||||
|
|
||||||
|
### Экран 3. Карточка авто
|
||||||
|
- Резюме (гос-номер, VIN, модель, цвет, год)
|
||||||
|
- Текущий водитель (если в аренде)
|
||||||
|
- **Кнопки:** «Передача водителю» (handover) | «Приёмка от водителя» (return) | «Плановый осмотр» (periodic) | «Свободный осмотр» (ad-hoc)
|
||||||
|
- **Прошлые осмотры** — лента (превью фото + метки)
|
||||||
|
|
||||||
|
### Экран 4. Новый осмотр — список «слотов» фото
|
||||||
|
Карточка-grid: 4 обязательных + 4 опциональных:
|
||||||
|
- Перед, Зад, Левый, Правый (обязательные)
|
||||||
|
- VIN, Одометр, Салон, Свободное фото
|
||||||
|
- Каждый слот: пустой → камера UI; заполнен → миниатюра + индикатор количества меток
|
||||||
|
- В режиме `return` каждая карточка слота показывает «было N меток, добавь новые / подтверди / отметь устранённые»
|
||||||
|
|
||||||
|
### Экран 5. Камера
|
||||||
|
- Полноэкранный preview (через `getUserMedia()` если поддерживается, иначе `<input type="file" capture="environment">`)
|
||||||
|
- Overlay-направляющая под текущий слот (силуэт стороны авто для guidance)
|
||||||
|
- Снять → preview → принять/перенять
|
||||||
|
|
||||||
|
### Экран 6a. Vehicle Scheme (zone-навигация)
|
||||||
|
|
||||||
|
- Интерактивная SVG-схема авто с тремя проекциями: top-down (вид сверху), боковая (left/right), интерьер
|
||||||
|
- Каждая зона (бампер, капот, дверь, крыша, диск, лобовое и т.д.) — кликабельный hot-spot SVG `<path>`
|
||||||
|
- При тапе на зону → переход в Экран 5 (Камера) с предзаполненным `side`/`slot_index`
|
||||||
|
- Поверх схемы — полупрозрачные метки прошлых повреждений (для return-режима): красные точки на тех зонах, где были повреждения
|
||||||
|
- Прогресс осмотра: подсветка зон, у которых уже есть фото (зелёный outline)
|
||||||
|
|
||||||
|
### Экран 6b. Annotation Editor (главная фишка)
|
||||||
|
- Фото на весь экран, поверх — Konva canvas
|
||||||
|
- Tool panel:
|
||||||
|
- 🔘 **Маркер** — тап создаёт точку-метку
|
||||||
|
- ✏️ **Обводка** — рисуем замкнутый контур (полигон) вокруг повреждения
|
||||||
|
- ↩️ Undo / 🗑️ Erase
|
||||||
|
- 🎨 Тип/severity выбор (4 типа × 4 severity)
|
||||||
|
- При тапе на маркер — modal с типом, severity, описанием
|
||||||
|
- В `return`-режиме старые маркеры показываются полупрозрачными; тап → «есть/устранено»
|
||||||
|
|
||||||
|
### Экран 7. Обзор осмотра
|
||||||
|
- Карточка-grid всех фото + список всех маркеров + общие notes
|
||||||
|
- Кнопка «Завершить осмотр» → finished_at, status=completed
|
||||||
|
|
||||||
|
### Экран 8. Подтверждение (опц.)
|
||||||
|
- Подпись водителя (canvas) — опционально
|
||||||
|
- Сохранение, переход на главную
|
||||||
|
|
||||||
|
## 7. Архитектура frontend
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/
|
||||||
|
│ ├── client.ts # настройка fetch/axios + auth interceptor
|
||||||
|
│ ├── inspections.ts # endpoints через TanStack Query
|
||||||
|
│ ├── vehicles.ts
|
||||||
|
│ └── photos.ts
|
||||||
|
├── components/
|
||||||
|
│ ├── ui/ # shadcn-сгенерированные
|
||||||
|
│ ├── camera/
|
||||||
|
│ │ ├── CameraCapture.tsx
|
||||||
|
│ │ └── PhotoSlot.tsx
|
||||||
|
│ ├── annotation/
|
||||||
|
│ │ ├── AnnotationCanvas.tsx # Konva-based
|
||||||
|
│ │ ├── ToolPalette.tsx
|
||||||
|
│ │ └── MarkerModal.tsx
|
||||||
|
│ ├── vehicle-scheme/
|
||||||
|
│ │ ├── VehicleScheme.tsx # SVG interactive scheme
|
||||||
|
│ │ ├── schemes/ # SVG-файлы по типу кузова (sedan, hatch, suv)
|
||||||
|
│ │ │ ├── sedan-top.svg
|
||||||
|
│ │ │ ├── sedan-side.svg
|
||||||
|
│ │ │ └── interior.svg
|
||||||
|
│ │ └── HotSpot.tsx # переиспользуемая зона-кнопка
|
||||||
|
│ └── shared/
|
||||||
|
├── pages/
|
||||||
|
│ ├── Login.tsx
|
||||||
|
│ ├── Home.tsx
|
||||||
|
│ ├── VehicleCard.tsx
|
||||||
|
│ ├── InspectionStart.tsx
|
||||||
|
│ ├── InspectionEditor.tsx
|
||||||
|
│ └── InspectionReview.tsx
|
||||||
|
├── store/ # Zustand store(s)
|
||||||
|
│ ├── inspectionDraft.ts
|
||||||
|
│ └── auth.ts
|
||||||
|
├── lib/
|
||||||
|
│ ├── pwa.ts # SW registration
|
||||||
|
│ └── coords.ts # norm/denorm coords
|
||||||
|
├── App.tsx
|
||||||
|
└── main.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Архитектура backend (модуль в PremiumCRM)
|
||||||
|
|
||||||
|
```
|
||||||
|
app/mechanic/
|
||||||
|
├── __init__.py
|
||||||
|
├── router.py # /api/v1/mechanic/ роуты
|
||||||
|
├── models.py # SQLAlchemy: Inspection, InspectionPhoto, DamageMarker
|
||||||
|
├── schemas.py # Pydantic
|
||||||
|
├── service.py # бизнес-логика (start_inspection, finalize, copy_markers, ...)
|
||||||
|
├── storage.py # сохранение файлов на VDS + thumbnails
|
||||||
|
├── deps.py # auth + permission deps
|
||||||
|
└── migrations/
|
||||||
|
└── 20260516_mechanic_init.py # Alembic
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Фазы реализации
|
||||||
|
|
||||||
|
### Фаза 1. MVP — Skeleton (3-4 нед)
|
||||||
|
Только основной flow без annotation, для первичного теста UX:
|
||||||
|
|
||||||
|
- Backend: модуль `mechanic`, миграции, endpoints вэхикулов и осмотров, фото upload (без annotation)
|
||||||
|
- Frontend: Login → Home → Vehicle → Inspection (создать, выбрать тип, добавить фото в 4 слота, добавить просто маркер-точку без сложной обводки) → Review → Complete
|
||||||
|
- Дифф с прошлым: список маркеров прошлого осмотра, чекбоксы «есть/устранено»
|
||||||
|
|
||||||
|
### Фаза 2. Annotation Editor (+2-3 нед)
|
||||||
|
- Konva-based canvas-overlay с инструментами обводки (polygon)
|
||||||
|
- Сохранение annotated-фото отдельным URL
|
||||||
|
- Старые маркеры/обводки полупрозрачно поверх новой фотографии для сравнения
|
||||||
|
|
||||||
|
### Фаза 3. Polish (+1-2 нед)
|
||||||
|
- Service Worker + offline queue (фото снимаются на устройство, загружаются по WiFi)
|
||||||
|
- QR-сканер стикеров
|
||||||
|
- Push-уведомления для админа (через web push с FCM-фолбэком или Telegram-бота)
|
||||||
|
- Дашборд статистики в админ-части PremiumCRM
|
||||||
|
|
||||||
|
### Фаза 4 (опционально). Capacitor wrapper
|
||||||
|
- Если в production столкнёмся с ограничениями iOS Safari camera или offline
|
||||||
|
- Тот же codebase → native APK + IPA
|
||||||
|
- ~1-2 недели
|
||||||
|
|
||||||
|
## 10. Связь с разведкой
|
||||||
|
|
||||||
|
- Endpoints, обнаруженные у вендора (`api.ttcontrol.naughtysoft.ru/api/vehicletechinspection/byvehicle` и пр.), могут быть использованы **отдельно** для **миграции данных** — одноразовый импорт прошлых осмотров вендора в нашу базу. Это отдельный sub-project (не входит в MVP).
|
||||||
|
- В первой версии работаем только с НОВЫМИ осмотрами, создаваемыми в PremiumMechanic — старые остаются в системе вендора и используются для справки до момента миграции.
|
||||||
|
|
||||||
|
## 11. Решения и открытые вопросы
|
||||||
|
|
||||||
|
### Решения (по итогам ревью владельца)
|
||||||
|
|
||||||
|
| # | Решение |
|
||||||
|
|---|---|
|
||||||
|
| Поддомен | **`mechanic.pptaxi.ru`** (отдельный поддомен). DNS уже настроен владельцем. На VDS 100.64.0.12 в Caddyfile добавляется блок с TLS через ACME. |
|
||||||
|
| Подпись водителя | **Не нужна** в MVP. Из спеки исключена. |
|
||||||
|
| 2D-схема авто | **Нужна обязательно с MVP.** Делаем SVG-схему сами: top-down + боковая проекция + интерьер. Не привязываемся к ассетам вендора (в APK schemes как раздельных файлов нет, у них всё через WebView + либо embedded в Xamarin DLL, либо отдельная фотография крупным планом). У нас SVG-первый подход с zone-based hot-spots. |
|
||||||
|
|
||||||
|
### Замечания по разведке вендорского редактора
|
||||||
|
|
||||||
|
- Layout `vehicleinspectiondetaildrawinglayout.xml` подтверждает: вендор использует `<android.webkit.WebView>` для редактора рисования повреждений. То есть **наша PWA-стратегия = технически тот же подход, что у вендора**, только без обёртки Xamarin.
|
||||||
|
- Заголовок WebView в layout = «Левая дверь» — у вендора **zone-based** детализация (кнопка → крупный план зоны → редактирование).
|
||||||
|
- ObjectID формата `660e7abfd13231f1dd6e8a4d` в `vehicleId` доказывает, что у вендора на бэке **MongoDB**, а не 1С. Имя `taksi.0nalog.com:1703` (виденное в Driver-приложении) — отдельный 1С-узел, не основной storage. Это означает: имитировать «1С-структуру» нам не надо — у вендора и так нет 1С на ключевом пути данных осмотров.
|
||||||
|
|
||||||
|
### Остающиеся open questions
|
||||||
|
|
||||||
|
1. **Permissions:** добавить новое право `mechanic:inspect` или ограничиться существующим `dept=Тех. служба`? — выбор на этапе implementation.
|
||||||
|
2. **Storage:** решено использовать **MinIO**, **только** MinIO без локального FS-дубля (разрешено отклониться от dual-write паттерна для тяжёлых медиа). Параметры: bucket `pp-inspections`, endpoint `s3.pptaxi.ru` HTTPS, креды в `/opt/sites/taxi-dashboard/minio.env`. Существующая `aiobotocore`-интеграция переиспользуется. Остаётся **операционно**: настроить CORS на bucket для allowed origins (`https://mechanic.pptaxi.ru`, `https://crm.pptaxi.ru`, dev `http://localhost:5173`) — задача в плане реализации.
|
||||||
|
3. **Связь с CheckAuto:** опционально показывать в карточке машины автоматические проверки водителя — после MVP.
|
||||||
|
4. **Импорт истории осмотров от вендора:** через VendorBridge sub-project — после MVP.
|
||||||
|
|
||||||
|
## 12. Следующий шаг
|
||||||
|
|
||||||
|
После утверждения этой спеки:
|
||||||
|
|
||||||
|
1. Создать подробный план реализации Фазы 1 через `writing-plans` skill.
|
||||||
|
2. Сгенерировать миграции БД + scaffold бэкенд-модуля.
|
||||||
|
3. Скаффолд React-проекта в `mechanic-pwa/frontend/` с базовой страницей логина.
|
||||||
|
4. На итерациях по 2-3 экрана.
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,283 @@
|
|||||||
|
# Этап 1. Статический анализ — отчёт
|
||||||
|
|
||||||
|
- **Дата:** 2026-05-16
|
||||||
|
- **Аналитик:** vladtechno@gmail.com + Claude
|
||||||
|
- **Источники:** локальные APK в `recon/artifacts/`, jadx 1.5.5 декомпиляция
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что разбирали
|
||||||
|
|
||||||
|
Два приложения одного вендора **NaughtySoft**:
|
||||||
|
|
||||||
|
| Приложение | APK | Версия | Назначение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| «Водитель» | `com.naughtysoft.TtcDriver` (XAPK split) | 1.2.313 (build 323) | Основное приложение для водителей: балансы, штрафы, платежи, обращения, осмотры |
|
||||||
|
| «Механик» | `com.naughtysoft.ttc` | 1.8.13 (build 165) | Приложение для механиков парка: фото машин в 1С |
|
||||||
|
|
||||||
|
Оба от одного вендора (`com.naughtysoft.*`), технологический стек **разный** (см. §3) — признак того, что вендор находится в процессе технологической миграции и не унифицирует разработку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Метаданные APK
|
||||||
|
|
||||||
|
### Водитель (`com.naughtysoft.TtcDriver`)
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Version | 1.2.313 (build 323) |
|
||||||
|
| Target SDK | 36 (Android 16) |
|
||||||
|
| Min SDK | 24 (Android 7) |
|
||||||
|
| Main Activity | `com.naughtysoft.TtcDriver.MainActivity` |
|
||||||
|
| Permissions | INTERNET, CAMERA, RECORD_AUDIO, STORAGE, WAKE_LOCK, POST_NOTIFICATIONS, FCM (`c2dm.permission.RECEIVE`), AD_ID, ACCESS_NETWORK_STATE |
|
||||||
|
| Permissions, которых **НЕТ** | GPS (ACCESS_FINE/COARSE_LOCATION), SMS, READ_CONTACTS, READ_PHONE_STATE, BIND_NOTIFICATION_LISTENER |
|
||||||
|
| `android:usesCleartextTraffic` | **true** (приложение умеет HTTP без TLS) |
|
||||||
|
| `network_security_config.xml` | **отсутствует** (TLS pinning через XML не настроен) |
|
||||||
|
| Распространение | XAPK с App Bundle (base + split: arm64_v8a, en, mdpi, zh) |
|
||||||
|
| Native ABI | **только arm64_v8a** (нет x86_64, нет armeabi-v7a) |
|
||||||
|
|
||||||
|
Отсутствие GPS-разрешений = приложение пассивное, не трекает позицию водителя.
|
||||||
|
Отсутствие SMS-разрешений = OTP-перехвата автоматического нет, логин классический.
|
||||||
|
|
||||||
|
### Механик (`com.naughtysoft.ttc`)
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Version | 1.8.13 (build 165) |
|
||||||
|
| Target SDK | 33 (Android 13) |
|
||||||
|
| Min SDK | 19 (Android 4.4) |
|
||||||
|
| Application Label | «Механик» |
|
||||||
|
| Permissions | INTERNET, CAMERA, RECORD_AUDIO, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, STORAGE, VIBRATE, NETWORK_STATE, WIFI_STATE |
|
||||||
|
| Native ABI | только arm64_v8a |
|
||||||
|
|
||||||
|
В Механике в отличие от Водителя есть **GPS** и используется **Xamarin** (см. §3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Технологические стеки
|
||||||
|
|
||||||
|
### Водитель — Flutter (Dart AOT)
|
||||||
|
|
||||||
|
Подтверждено наличием:
|
||||||
|
|
||||||
|
- `lib/arm64-v8a/libflutter.so` — Flutter engine (11 МБ)
|
||||||
|
- `lib/arm64-v8a/libapp.so` — **Dart AOT snapshot** с бизнес-логикой (9.6 МБ)
|
||||||
|
- Java-пакеты: `io/flutter/`, `dev/flutter/pigeon`, `_COROUTINE/`, Kotlin runtime
|
||||||
|
- Flutter-плагины: `dev/fluttercommunity/plus`, `com/baseflow/permissionhandler`, `com/jrai/flutter_keyboard_visibility`, `io/scer/pdfx`, `studio/midoridesign/gal`, `io/scer/pdf_renderer`, `dev/fluttered/map_launcher`
|
||||||
|
|
||||||
|
**Что это значит для реверса:**
|
||||||
|
|
||||||
|
- Бизнес-логика приложения — **в Dart, скомпилированном AOT в `libapp.so`**. jadx-декомпиляция Java/Kotlin даёт только тонкие plugin wrappers, не бизнес-код.
|
||||||
|
- Все строковые константы (URL, paths, ключи) хранятся в `libapp.so` как plain ASCII → извлекаемы через `grep -a`/`strings` (см. §4).
|
||||||
|
- HTTP-запросы Flutter делает через стандартный Dart `HttpClient` → видимы в mitmproxy как обычный HTTPS (без custom низкоуровневых трюков).
|
||||||
|
- Дальнейший реверс Dart-кода (поведение, проверки) — через **reFlutter** (требует пересборки snapshot) или динамический анализ через mitmproxy. На статическом этапе ограничиваемся извлечёнными строками.
|
||||||
|
|
||||||
|
### Механик — Xamarin (.NET/Mono)
|
||||||
|
|
||||||
|
Подтверждено наличием:
|
||||||
|
|
||||||
|
- Java-пакеты: `xamarin/`, `mono/`, `crc64xxxxxxxxxxxxxxxx/` (CRC64-обфускация имён классов — характерный паттерн Xamarin)
|
||||||
|
- `assets/assemblies/assemblies.blob` (4 МБ) — упакованный контейнер всех .NET DLL
|
||||||
|
- `assets/assemblies/assemblies.manifest` (1.6 КБ) — манифест содержимого blob
|
||||||
|
|
||||||
|
**Что это значит для реверса:**
|
||||||
|
|
||||||
|
- jadx даёт ноль информации о логике Механика (всё в C# DLL).
|
||||||
|
- Для распаковки нужен **pyxamstore** (распаковать blob → отдельные DLL) + **dnSpy/ILSpy** для C# декомпиляции.
|
||||||
|
- Это **отдельный поток работы**, отложен. Приоритет — водитель.
|
||||||
|
|
||||||
|
**Вывод о стеке вендора:** Xamarin (Mechanic) сейчас deprecated Microsoft'ом в пользу .NET MAUI. Driver переписан на Flutter — современный стек. Вендор находится в процессе технологической миграции, что согласуется с фактом «прекратил доработки» — ресурсы ушли на перенос/переход, а не на обновление наших фич.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Сетевая инфраструктура
|
||||||
|
|
||||||
|
Все извлечены из `libapp.so` водительского приложения через `grep -aoE 'https?://...'`.
|
||||||
|
|
||||||
|
### Бэкенды
|
||||||
|
|
||||||
|
| Endpoint | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| **`https://api.ttcontrol.naughtysoft.ru/api`** | Главный API вендора NaughtySoft. Основной канал общения приложения с сервером. |
|
||||||
|
| **`https://taksi.0nalog.com:1703/`** | Нестандартный порт **1703** — классический паттерн **HTTP-сервиса 1С Предприятие**. С большой вероятностью это сам сервер 1С PremiumPark, на котором работает конфигурация вендора. Имя `0nalog` (вместо `nalog`) — возможный obscurity-приём. |
|
||||||
|
| **`https://ttcdriver.firebaseio.com`** | Firebase Realtime Database. Помимо FCM-пушей, часть данных может синхронизироваться через Realtime DB. Project ID: `ttcdriver`. |
|
||||||
|
|
||||||
|
### Архитектурная гипотеза
|
||||||
|
|
||||||
|
```
|
||||||
|
Driver App (Flutter)
|
||||||
|
│
|
||||||
|
├──► api.ttcontrol.naughtysoft.ru (HTTPS, основное взаимодействие)
|
||||||
|
│ │
|
||||||
|
│ └──► (на бэке вендора, вероятно) ──► taksi.0nalog.com:1703 (1С)
|
||||||
|
│
|
||||||
|
├──► taksi.0nalog.com:1703 (возможно, прямые запросы — нужно подтвердить на этапе 2)
|
||||||
|
│
|
||||||
|
├──► ttcdriver.firebaseio.com (Realtime DB + FCM регистрация)
|
||||||
|
│
|
||||||
|
└──► FCM (Firebase Cloud Messaging — push-уведомления о штрафах и пр.)
|
||||||
|
```
|
||||||
|
|
||||||
|
Подтвердить факт прямых обращений на `0nalog:1703` (или это идёт только через прокси `api.ttcontrol`) — задача этапа 2 (mitmproxy).
|
||||||
|
|
||||||
|
### Карта API endpoints
|
||||||
|
|
||||||
|
Извлечены пути `/v1/...` и `/v3/...` из `libapp.so`. Префикс домена — `api.ttcontrol.naughtysoft.ru/api`.
|
||||||
|
|
||||||
|
| Endpoint | Назначение (гипотеза) |
|
||||||
|
|---|---|
|
||||||
|
| `/v1/GetBalance` | Остаток баланса водителя |
|
||||||
|
| `/v1/GetFines` | Список штрафов |
|
||||||
|
| `/v1/GetChecks` | Чеки 54-ФЗ |
|
||||||
|
| `/v1/GetPayroll` | Зарплата/начисления v1 |
|
||||||
|
| `/v3/GetPayroll` | Зарплата/начисления v3 (новее) |
|
||||||
|
| **`/v1/CreatePayment`** | **Инициация платежа** |
|
||||||
|
| `/v1/GetPaymentDetails/` | Детали платежа |
|
||||||
|
| **`/v1/BanksSBP`** | **Список банков СБП** — платежи через Систему Быстрых Платежей |
|
||||||
|
| `/v1/ChangeQIWIWalletCardNumber` | QIWI Wallet (legacy — QIWI закрылся в 2024, мёртвый код) |
|
||||||
|
| `/v1/CreateAppeal` | Создать обращение/заявку |
|
||||||
|
| `/v1/GetAppealHistory` | История обращений водителя |
|
||||||
|
| `/v1/AddMessageAppeal` | Добавить сообщение к обращению |
|
||||||
|
| **`/v1/AddInspection`** | **Добавить осмотр (фото повреждений)** |
|
||||||
|
| **`/v1/FOTO/`** | **Endpoint для фотографий** |
|
||||||
|
| `/v1/AttractedAdd` | Добавить привлечённого (реферал?) |
|
||||||
|
| `/v1/AttractedList` | Список привлечённых |
|
||||||
|
| `/v1/OnLine` | Heartbeat / online status |
|
||||||
|
| `/v1/GetStatus/` | Статус (требует расшифровки на этапе 2) |
|
||||||
|
| `/v1/Sell` | Продать что-то (требует расшифровки) |
|
||||||
|
| `/v1/GetListOfQuestions` | Список вопросов (FAQ / скрининг) |
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
|
||||||
|
Найдены характерные строки в `libapp.so`:
|
||||||
|
|
||||||
|
- `Authorization` — заголовок HTTP
|
||||||
|
- `Bearer` — Bearer-токен
|
||||||
|
- `apiKey` — упоминание (вероятно Firebase API key)
|
||||||
|
- `authorization` (lowercase) — тоже встречается
|
||||||
|
|
||||||
|
**Гипотеза:** стандартная Bearer-token аутентификация. Логин → получение access_token → дальше во всех запросах `Authorization: Bearer <token>`. Подтвердить и понять lifecycle (refresh? expiry?) — задача этапа 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Защитные механизмы
|
||||||
|
|
||||||
|
### TLS pinning
|
||||||
|
|
||||||
|
- **На уровне Android system (network_security_config.xml):** отсутствует — конфиг не найден в декомпиле.
|
||||||
|
- **Cleartext traffic разрешён** — приложение умеет HTTP без TLS (вероятно для `taksi.0nalog.com:1703`).
|
||||||
|
- **Программный TLS pinning в Dart-коде:** возможен, но в `libapp.so` явных маркеров pinning'а (типа `setTrustedRoots`, custom SecurityContext с pinned certs) поверхностный grep не выявил. Проверится на этапе 2: если mitmproxy сразу читает трафик — pinning'а нет; если нет — нужен reFlutter/Frida.
|
||||||
|
|
||||||
|
**Прогноз:** на этапе 2 mitmproxy будет работать **без обхода TLS pinning**. Это сильно упрощает разведку.
|
||||||
|
|
||||||
|
### Root/Emulator detection
|
||||||
|
|
||||||
|
Не сканировалось специально (требует анализа Dart-кода в `libapp.so`). На этапе 2 проверим эмпирически — запустится ли приложение на эмуляторе. Учитывая что:
|
||||||
|
|
||||||
|
- Приложение Flutter (без agressive anti-debug по умолчанию)
|
||||||
|
- Нет premium-фич, ориентированных на security (банковский класс, антифрод)
|
||||||
|
|
||||||
|
— вероятность блокирующего detection'а низкая.
|
||||||
|
|
||||||
|
### Request signing / HMAC
|
||||||
|
|
||||||
|
Маркеров явного HMAC (типа `Mac.getInstance("HmacSHA256")`) в Kotlin-стороне не найдено (большая часть Java-кода — Flutter engine + plugins, без бизнес-логики). В Dart возможна реализация, но это нестандартный паттерн для Flutter приложений — обычно полагаются на Bearer + TLS.
|
||||||
|
|
||||||
|
### Прочее
|
||||||
|
|
||||||
|
- **Play Integrity API** — не используется (нет соответствующих библиотек в декомпиле).
|
||||||
|
- **App attestation / SafetyNet** — отсутствует.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Сторонние SDK и зависимости
|
||||||
|
|
||||||
|
### Из Kotlin/Java декомпиляции водителя
|
||||||
|
|
||||||
|
- `com.google.firebase.*` — Firebase (FCM, аналитика)
|
||||||
|
- `com.google.android.gms.*` — Google Play Services
|
||||||
|
- `dev.flutter.pigeon` — Flutter Pigeon (Dart ↔ Native bridges)
|
||||||
|
- `io.flutter.*` — Flutter engine
|
||||||
|
- `com.baseflow.permissionhandler` — permission_handler plugin
|
||||||
|
- `com.jrai.flutter_keyboard_visibility` — keyboard visibility plugin
|
||||||
|
- `io.scer.pdfx`, `io.scer.pdf_renderer` — PDF rendering plugins
|
||||||
|
- `studio.midoridesign.gal` — gal plugin (сохранение в галерею)
|
||||||
|
- `dev.fluttercommunity.plus` — Flutter community plugins
|
||||||
|
- `dev.fluttered.map_launcher` — map_launcher plugin (открытие карт)
|
||||||
|
- `com.github.rmtmckenzie.native_device_orientation` — ориентация устройства
|
||||||
|
- `com.softmaestri.notification` — кастомные нотификации (другой вендор?)
|
||||||
|
- `com.getkeepsafe.relinker` — динамическая загрузка native libraries
|
||||||
|
- `org.apache.commons` — Apache Commons
|
||||||
|
- `org.chromium.support_lib_boundary` — WebView boundary library
|
||||||
|
|
||||||
|
### **НЕ найдено** (важно):
|
||||||
|
|
||||||
|
- **`ru.yoomoney.*`** — нет ЮКассы SDK
|
||||||
|
- **`ru.tinkoff.*`** — нет Tinkoff Acquiring SDK
|
||||||
|
- **`com.yandex.payments.*`** — нет Yandex Pay
|
||||||
|
- **`com.sberbank.*`** — нет Сбер SDK
|
||||||
|
- `com.squareup.okhttp3` / `retrofit2` — нет (HTTP идёт через Dart)
|
||||||
|
- `dagger`, `hilt` — нет DI-фреймворков (Flutter сам по себе их не требует)
|
||||||
|
|
||||||
|
**Вывод по платежам:** на Kotlin-стороне платёжных SDK провайдеров нет. Платёж через `/v1/CreatePayment` скорее всего возвращает **deep link** (типа `https://qr.nspk.ru/<id>` для СБП или URL веб-страницы провайдера), и Flutter открывает его через стандартный intent. Это **отличный сценарий для миграции** — точно повторяемо в нашем приложении без необходимости лицензировать чьи-то SDK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Промежуточные гипотезы
|
||||||
|
|
||||||
|
1. **Архитектура двухслойная**: NaughtySoft API (`api.ttcontrol`) — фасад, под капотом стоит 1С (`taksi.0nalog:1703`). Подтвердить на этапе 2: смотреть, идут ли с приложения **прямые** запросы на `0nalog:1703` (тогда оба слоя независимы), или только на `api.ttcontrol` (тогда `0nalog` — публично доступный backend 1С только в декомпиле, а реально приложение его не дёргает).
|
||||||
|
2. **Платежи через СБП + deep links**, без нативных SDK провайдеров → **легко повторимо** в нашем будущем приложении.
|
||||||
|
3. **Фото-функция (`/v1/AddInspection`, `/v1/FOTO/`) уже есть на бэке** — она просто не выдана водителям в UI текущего приложения. Если на этапе 2 удастся снять трафик этих endpoints (через Механика или через намеренный вызов в Driver если кнопка где-то есть), мы получим готовый протокол для нашего блока B без дизайна с нуля.
|
||||||
|
4. **QIWI код мёртв** — индикатор того, что подрядчик не зачищает мёртвый функционал. Подтверждает «прекратили доработки».
|
||||||
|
5. **TLS pinning слабый или отсутствует** → mitmproxy будет работать в этап 2 «из коробки».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Open questions для следующих этапов
|
||||||
|
|
||||||
|
- Какой формат запроса/ответа на `/v1/CreatePayment`? (этап 2)
|
||||||
|
- Использует ли приложение `/api/...` префикс или `/v1/...` идут напрямую от корня? (этап 2)
|
||||||
|
- Использует ли водитель `taksi.0nalog.com:1703` напрямую или только через прокси `api.ttcontrol`? (этап 2)
|
||||||
|
- Программный TLS pinning в Dart — есть? (этап 2, эмпирически)
|
||||||
|
- Refresh-токен и его lifecycle — какой? (этап 3)
|
||||||
|
- Что приходит в FCM-сообщении при штрафе? Полный объект или только триггер «обнови данные»? (этап 5)
|
||||||
|
- Что лежит в `assemblies.blob` Механика? Те же endpoints или другие? (отдельный поток)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Блокеры и риски
|
||||||
|
|
||||||
|
| # | Риск | Митигация |
|
||||||
|
|---|---|---|
|
||||||
|
| R1 | **ARM-only ABI** в обоих APK — на x86_64 эмуляторе `adb install` падает с `INSTALL_FAILED_NO_MATCHING_ABIS` | Для этапа 2 либо: (a) создать AVD x86_64 API 33+ с включённой ARM-translation, (b) использовать физическое Android-устройство, (c) использовать ARM-эмулятор (медленный) |
|
||||||
|
| R2 | Программный TLS pinning в Dart-коде возможен, но не подтверждён на статике | Проверить эмпирически на этапе 2; если есть — обойти через **reFlutter** |
|
||||||
|
| R3 | Механик на Xamarin требует отдельного тулинга (pyxamstore + dnSpy) для разбора C# логики | Отдельный поток работы; приоритет — водитель |
|
||||||
|
| R4 | Часть Dart-логики (валидация, формат запросов) невидима без динамики | Перенесено на этап 2 (mitmproxy) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Артефакты
|
||||||
|
|
||||||
|
| Файл | Что |
|
||||||
|
|---|---|
|
||||||
|
| `recon/artifacts/original-driver.xapk` | Исходный XAPK водителя из APKPure |
|
||||||
|
| `recon/artifacts/driver-xapk/` | Распакованный XAPK (base APK + splits + manifest.json) |
|
||||||
|
| `recon/artifacts/original-mechanic.apk` | APK Механика |
|
||||||
|
| `recon/artifacts/decompiled/driver/` | jadx-декомпиляция водителя (4331 классов, 25 errors ≈ 0.6%) |
|
||||||
|
| `recon/artifacts/decompiled/mechanic/` | jadx-декомпиляция Механика (даёт только Xamarin glue, не C# логику) |
|
||||||
|
| `recon/artifacts/native/driver/lib/arm64-v8a/libapp.so` | Извлечённый Dart AOT snapshot водителя — главный источник URL и endpoints |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Следующий шаг (этап 2)
|
||||||
|
|
||||||
|
Все необходимые данные для разворачивания этапа 2 готовы:
|
||||||
|
|
||||||
|
- Список endpoints для отслеживания — есть
|
||||||
|
- Базовые URL'ы — есть
|
||||||
|
- Подтверждено отсутствие XML-pinning'а — mitmproxy будет работать
|
||||||
|
- Топология FCM известна — этап 5 пойдёт от Firebase project `ttcdriver`
|
||||||
|
|
||||||
|
**Перед этапом 2 нужно решить ARM-блокер** (R1): выбрать ARM-эмулятор / x86_64 с ARM translation / физическое устройство. Это **открытый пункт для пользователя**.
|
||||||
|
|
||||||
|
После этого по плану: установка приложения на стенд → mitmproxy + системный CA → проигрыш сценариев S1-S6 (см. §5 design-doc'а) → каталог реальных запросов/ответов.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Read mitmproxy flow file, print unique endpoints by host/method/path."""
|
||||||
|
import sys, collections
|
||||||
|
from mitmproxy import io as miio
|
||||||
|
from mitmproxy.exceptions import FlowReadException
|
||||||
|
|
||||||
|
flow_file = sys.argv[1]
|
||||||
|
filter_host = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
|
stats = collections.Counter()
|
||||||
|
endpoints = collections.defaultdict(set)
|
||||||
|
hosts = collections.Counter()
|
||||||
|
status_codes = collections.Counter()
|
||||||
|
|
||||||
|
with open(flow_file, "rb") as f:
|
||||||
|
reader = miio.FlowReader(f)
|
||||||
|
try:
|
||||||
|
for flow in reader.stream():
|
||||||
|
if not hasattr(flow, "request"):
|
||||||
|
continue
|
||||||
|
req = flow.request
|
||||||
|
host = req.pretty_host
|
||||||
|
if filter_host and filter_host not in host:
|
||||||
|
continue
|
||||||
|
hosts[host] += 1
|
||||||
|
key = (req.method, req.path.split("?")[0])
|
||||||
|
endpoints[host].add(key)
|
||||||
|
stats[(req.method, host, req.path.split("?")[0])] += 1
|
||||||
|
if flow.response:
|
||||||
|
status_codes[flow.response.status_code] += 1
|
||||||
|
except FlowReadException as e:
|
||||||
|
print(f"[warn] flow truncated: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
print("=== Hosts ===")
|
||||||
|
for h, c in hosts.most_common():
|
||||||
|
print(f" {c:5d} {h}")
|
||||||
|
print()
|
||||||
|
print("=== Endpoints grouped by host ===")
|
||||||
|
for h in sorted(endpoints.keys()):
|
||||||
|
print(f"\n--- {h} ---")
|
||||||
|
for method, path in sorted(endpoints[h]):
|
||||||
|
count = stats[(method, h, path)]
|
||||||
|
print(f" [{count:3d}x] {method:6s} {path}")
|
||||||
|
print()
|
||||||
|
print("=== Status codes ===")
|
||||||
|
for code, c in status_codes.most_common():
|
||||||
|
print(f" {code}: {c}")
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Dump request URL + response body for flows matching a filter substring."""
|
||||||
|
import sys, json
|
||||||
|
from mitmproxy import io as miio
|
||||||
|
from mitmproxy.exceptions import FlowReadException
|
||||||
|
|
||||||
|
flow_file = sys.argv[1]
|
||||||
|
filter_substr = sys.argv[2]
|
||||||
|
max_dumps = int(sys.argv[3]) if len(sys.argv) > 3 else 1
|
||||||
|
|
||||||
|
dumps = 0
|
||||||
|
with open(flow_file, "rb") as f:
|
||||||
|
reader = miio.FlowReader(f)
|
||||||
|
try:
|
||||||
|
for flow in reader.stream():
|
||||||
|
if not hasattr(flow, "request"):
|
||||||
|
continue
|
||||||
|
url = flow.request.pretty_url
|
||||||
|
if filter_substr not in url:
|
||||||
|
continue
|
||||||
|
if not flow.response:
|
||||||
|
continue
|
||||||
|
dumps += 1
|
||||||
|
print(f"\n=== {flow.request.method} {url} ===")
|
||||||
|
print(f"Status: {flow.response.status_code}")
|
||||||
|
print(f"Content-Type: {flow.response.headers.get('Content-Type', '?')}")
|
||||||
|
print("--- response (first 4000 chars) ---")
|
||||||
|
try:
|
||||||
|
body = flow.response.get_text(strict=False)
|
||||||
|
if body:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(body)
|
||||||
|
body = json.dumps(parsed, indent=2, ensure_ascii=False)
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
pass
|
||||||
|
print(body[:4000])
|
||||||
|
else:
|
||||||
|
print("(binary or empty)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[error decoding body: {e}]")
|
||||||
|
if dumps >= max_dumps:
|
||||||
|
break
|
||||||
|
except FlowReadException as e:
|
||||||
|
print(f"[warn] flow truncated: {e}", file=sys.stderr)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""Scan a binary blob for MZ markers, validate as PE, dump each as separate file."""
|
||||||
|
import struct, sys, pathlib
|
||||||
|
|
||||||
|
blob_path = pathlib.Path(sys.argv[1])
|
||||||
|
out_dir = pathlib.Path(sys.argv[2])
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
data = blob_path.read_bytes()
|
||||||
|
|
||||||
|
def pe_total_size(buf, off):
|
||||||
|
"""Compute size of PE image at offset using section table."""
|
||||||
|
# MZ at off; PE header offset at off+0x3C
|
||||||
|
if off + 0x40 > len(buf):
|
||||||
|
return None
|
||||||
|
pe_off = struct.unpack_from("<I", buf, off + 0x3C)[0]
|
||||||
|
pe_start = off + pe_off
|
||||||
|
if pe_start + 24 > len(buf) or buf[pe_start:pe_start+4] != b"PE\x00\x00":
|
||||||
|
return None
|
||||||
|
num_sections = struct.unpack_from("<H", buf, pe_start + 6)[0]
|
||||||
|
optional_header_size = struct.unpack_from("<H", buf, pe_start + 20)[0]
|
||||||
|
sections_start = pe_start + 24 + optional_header_size
|
||||||
|
last_end = sections_start + num_sections * 40
|
||||||
|
for i in range(num_sections):
|
||||||
|
s = sections_start + i * 40
|
||||||
|
raw_size = struct.unpack_from("<I", buf, s + 16)[0]
|
||||||
|
raw_off = struct.unpack_from("<I", buf, s + 20)[0]
|
||||||
|
end = off + raw_off + raw_size
|
||||||
|
last_end = max(last_end, end)
|
||||||
|
return last_end - off
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
n = 0
|
||||||
|
while True:
|
||||||
|
j = data.find(b"MZ", i)
|
||||||
|
if j == -1:
|
||||||
|
break
|
||||||
|
sz = pe_total_size(data, j)
|
||||||
|
if sz and 4096 < sz < 5_000_000:
|
||||||
|
out = out_dir / f"plain_{j:08x}.dll"
|
||||||
|
out.write_bytes(data[j:j+sz])
|
||||||
|
print(f"[ok] offset=0x{j:08x} size={sz} -> {out.name}")
|
||||||
|
n += 1
|
||||||
|
i = j + sz
|
||||||
|
else:
|
||||||
|
i = j + 2
|
||||||
|
|
||||||
|
print(f"\nExtracted {n} plain PE files into {out_dir}")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Search a binary as UTF-16 LE, return regex matches."""
|
||||||
|
import re, sys, pathlib, glob
|
||||||
|
|
||||||
|
pattern = sys.argv[1]
|
||||||
|
paths = sys.argv[2:]
|
||||||
|
rx = re.compile(pattern)
|
||||||
|
|
||||||
|
found = {}
|
||||||
|
for pat in paths:
|
||||||
|
for p in glob.glob(pat):
|
||||||
|
try:
|
||||||
|
data = pathlib.Path(p).read_bytes().decode("utf-16-le", errors="ignore")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[skip] {p}: {e}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
for m in rx.findall(data):
|
||||||
|
key = m if isinstance(m, str) else m[0]
|
||||||
|
found.setdefault(key, []).append(p)
|
||||||
|
|
||||||
|
for key in sorted(found.keys()):
|
||||||
|
files = sorted(set(pathlib.Path(f).name for f in found[key]))
|
||||||
|
print(f"{','.join(files):20s} {key}")
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Distill XABA/XALZ assemblies.blob into individual .NET assemblies.
|
||||||
|
Format ref (reverse-engineered): https://github.com/dotnet/android/blob/main/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyStore.cs
|
||||||
|
"""
|
||||||
|
import struct, sys, pathlib, lz4.block
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: unpack_xaba.py <assemblies.blob> <out_dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
blob_path = pathlib.Path(sys.argv[1])
|
||||||
|
out_dir = pathlib.Path(sys.argv[2])
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
data = blob_path.read_bytes()
|
||||||
|
assert data[:4] == b"XABA", f"not a XABA blob, got {data[:4]!r}"
|
||||||
|
|
||||||
|
# Scan for XALZ markers (each = one compressed assembly)
|
||||||
|
i = 0
|
||||||
|
extracted = 0
|
||||||
|
while True:
|
||||||
|
idx = data.find(b"XALZ", i)
|
||||||
|
if idx == -1:
|
||||||
|
break
|
||||||
|
# XALZ: 4 magic + 4 descriptor_index + 4 uncompressed_size + N lz4_data
|
||||||
|
desc_idx = struct.unpack_from("<I", data, idx + 4)[0]
|
||||||
|
uncompressed = struct.unpack_from("<I", data, idx + 8)[0]
|
||||||
|
payload_start = idx + 12
|
||||||
|
# Find next XALZ to bound payload (or EOF)
|
||||||
|
next_idx = data.find(b"XALZ", payload_start)
|
||||||
|
payload_end = next_idx if next_idx != -1 else len(data)
|
||||||
|
compressed = data[payload_start:payload_end]
|
||||||
|
try:
|
||||||
|
decompressed = lz4.block.decompress(compressed, uncompressed_size=uncompressed)
|
||||||
|
except Exception as e:
|
||||||
|
# Try without explicit size as fallback
|
||||||
|
try:
|
||||||
|
decompressed = lz4.block.decompress(compressed)
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"[skip] desc={desc_idx} decompress failed: {e2}")
|
||||||
|
i = payload_start
|
||||||
|
continue
|
||||||
|
out_file = out_dir / f"{desc_idx:03d}.dll"
|
||||||
|
out_file.write_bytes(decompressed)
|
||||||
|
extracted += 1
|
||||||
|
print(f"[ok] desc={desc_idx:03d} size={len(decompressed)} -> {out_file.name}")
|
||||||
|
i = payload_start
|
||||||
|
|
||||||
|
print(f"\nExtracted {extracted} assemblies into {out_dir}")
|
||||||