Files
main/audits/2026-10-11/reports/13-reliability-and-security-report.md
codex-bot d07fbfabb1 docs: archive technical audit 2026-10-11 in develop (#36)
Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
2026-10-17 19:38:03 +03:00

356 lines
76 KiB
Markdown
Raw Permalink 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.
# Аудит 13 — Reliability & Security
## 1. Audit metadata
- **Audit ID:** 13, Reliability & Security.
- **Дата:** 2026-10-11.
- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`.
- **Режим:** пассивный read-only аудит исходников, конфигураций, существующих тестов и behavioral specs. Единственный созданный файл — этот отчёт вне четырёх исследованных репозиториев.
| Repo | Найденный путь относительно workspace | Точный HEAD | Начальный `git status --short` |
| --- | --- | --- | --- |
| backend | `backend/` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | Пустой, дерево чистое |
| frontend | `frontend/` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | Пустой, дерево чистое |
| specs | `specs/` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | Пустой, дерево чистое |
| docs | `docs/` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | Пустой, дерево чистое |
| workspace root | `./` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | Пустой, дерево чистое |
Прочитаны собственные `AGENTS.md` всех четырёх репозиториев и применимые локальные правила. Правила использовались для навигации, а не как доказательство корректности или необходимости механизма.
**Skills:** установленный `ponytail-audit` найден и прочитан. По его фактическому контракту security/correctness находятся вне его scope: он применён только к вопросу безопасного удаления/упрощения защит. Такие удаления не обоснованы; число удаляемых строк/dependencies не оценивалось. Security-проход выполнен по `security-review` с трассировкой входов, проверкой ownership и контраргументов. `graphify` прочитан, существующий `graphify-out/graph.json` не найден; построение графа и установка пакетов не выполнялись из-за read-only ограничения.
**Предыдущий аудит:** `audit-results/technical-audit-2026-10-11.md` использован как реестр гипотез. Выводы ниже заново проверены по указанным исходникам и вызывающим путям. Совпадающие гипотезы отмечены в Related IDs, но прежний отчёт не служит доказательством.
**Что запускалось:** `git status --short`, `git rev-parse HEAD` в root и четырёх repos; точечные файловые/content searches; два статических подсчёта контроллеров и HTTP-декораторов через `rg`. При завершении по `verification-before-completion` — повторные Git status и `git diff --check`, просмотр нового отчёта, `git check-ignore` для его пути и `git diff --no-index --check /dev/null audit-results/13-reliability-and-security-report.md` для whitespace нового ignored файла.
**Что не запускалось:** приложение, HTTP-запросы, payload attacks, реальные T-Bank/MOEX/NCR API, тесты, build, full-suite/verify, install, codegen, миграции, dependency/secret scanners, чтение реальной БД и env dump. Тесты ниже **прочитаны, но не исполнены**. Runtime-воспроизведение и фактическая конфигурация production — `UNVERIFIED`.
Root `.gitignore:1–37` игнорирует `audit-results/`; allowlist не менялся. `deploy/` обнаружен как дополнительный самостоятельный checkout и не включён в аудит четырёх repos. Для deployment рассмотрены только предоставленный root `compose.yaml`, frontend Nginx config и тематическая docs.
## 2. Executive verdict
1. **Факт / P1:** ошибка persistence в дочерней работе или завершении hourly может оставить durable `running`. Координатор ждёт без конечной границы, а следующий обход того же процесса отклоняется. Это наиболее важный отказ восстановления, потому что затрагивает дальнейшую свежесть нескольких финансовых ресурсов — RISK-001.
2. **Факт / P2:** исключение общего уровня обхода не добавляется в список failures и может завершиться как `succeeded`. Сохранённые данные при этом не обязательно обновлены — RISK-002.
3. **Факт / P2:** выбор fallback прогнозов способен блокировать новый полный результат для изменившегося окна; календарь облигаций после первой загрузки вообще не освежается обычным hourly-путём. Это два независимых механизма сохранения устаревших выплат — RISK-003/004.
4. **Факт / P2:** основной экран счёта теряет уже подготовленную metadata портфеля и скрывает failure фонового чтения. Пользователь не получает необходимого контекста сохранённых сумм — RISK-005.
5. **Факты / P2:** online instrument paths превращают постоянную ошибку брокерского token в `500`; нижняя граница пароля имеет разные единицы frontend/backend. Оба дефекта имеют локальные исправления у существующих владельцев — RISK-006/007.
6. **Сохранённые гарантии:** в выбранных потоках не найден подтверждённый IDOR, SQL injection, XSS или передача брокерского token в публичный ответ. Ownership, generation/attempt fences, banking CAS/fingerprint/tombstones и точная денежная арифметика нужны. Это вывод о проверенной выборке, а не сертификат отсутствия уязвимостей во всей системе.
7. **Риск / `UNVERIFIED`:** в приложении и поставляемом Nginx нет ограничения публичных login/register попыток; внешний HTTPS proxy может закрывать этот риск, но его конфигурация не предоставлена — RISK-008. Подтверждённых P0, эксплуатаций и измерений ущерба нет.
Итого: **7 дефектов по коду** — один P1 и шесть P2; **1 условный security risk** с Decision `INVESTIGATE`. Основная работа — корректные failure states и recovery, а не сокращение защит или внедрение нового framework.
## 3. Inventory & coverage
### 3.1. Фактический стек и метод подсчёта
- Backend: NestJS/TypeScript, Prisma с `adapter-better-sqlite3`, локальная SQLite, bcrypt, JWT, Pino, T-Bank gRPC; MOEX/NCR HTTP — `backend/package.json:40–67`, `backend/src/prisma/prisma.service.ts:12–30`, `backend/prisma/schema.prisma:1–9`.
- Frontend: React/Vite SPA, TanStack Query, generated OpenAPI transport, auth в памяти; PWA cache только статической оболочки — `frontend/package.json:39–62`, `frontend/vite.config.ts:45–51`.
- Найдено **11 controller source files и 52 HTTP method declarations**. Метод: из `backend/` выполнены `rg --files src/modules -g '*.controller.ts' | wc -l` и `rg --count '^\s*@(Get|Post|Put|Patch|Delete)\(' src/modules -g '*.controller.ts'`; второй результат суммирован: `1+2+1+10+2+3+8+2+14+5+4=52`. Это source inventory, не runtime route count; Swagger/framework routes и внешние proxies не включены.
- При просмотре guards все 48 прикладных declarations защищены class-/method-level `JwtAuthGuard`; четыре намеренно публичных — register, login, refresh, logout. Наличие guard отдельно не принималось за доказательство object authorization.
### 3.2. Asset / threat map
| Asset | Actor / capability | Entry point | Значимая угроза | Проверенный владелец / controls |
| --- | --- | --- | --- | --- |
| User credentials, Session | Неаутентифицированный клиент; владелец действующей cookie | `/auth/register`, `/login`, `/refresh`, `/logout` | Подбор, replay, конкурирующая rotation, утечка | Runtime DTO, bcrypt, generic login error, HS256/type/TTL, refresh hash CAS, cookie boundary |
| Brokerage credentials | Авторизованный User; поздний RPC прежнего подключения | PUT/DELETE `/tbank-connection` | Plaintext exposure, повреждение рабочего подключения, late writes | Проверка token до transaction, AES-GCM, атомарная замена generation, каскады |
| Accounts, portfolio, value history | User с чужим accountId или старой generation | Brokerage account reads / background publication | IDOR, раскрытие прежних данных, ложная свежесть | User/revision/account-scoped queries, подтверждённая membership, conditional publish |
| Bank cards, operations, refunds | User с чужими nested IDs; retry/lost response; competing writers | `/bank-cards/**` | Cross-user links, double effect, lost update, refund overrun | Owner checks внутри transaction, client UUID + fingerprint, version CAS, tombstone, BigInt |
| Ledger / income | User с чужим/устаревшим cursor; malformed upstream page | Operations/income GET, staged publication | Перескок owner/filter, частичный reconciliation | HMAC cursor + binding, scoped reads, stage markers, atomic publication |
| Public catalog, screener artifacts | User с чужим searchId; отказ token producer | Stocks/bonds search/detail/company | Межпользовательский результат, error poisoning, лишние retries | Projection, searchId owner/revision/TTL, credentials separated from shared public rows |
| Stored forecasts / durable work | Ошибка сети/SQLite, смена membership, process crash | Hourly walk, forecast resolution | Неверные суммы, устаревший forecast, вечный running | Dependency versions, transactional fences, processRunId; найденные пробелы RISK-001–004 |
| Logs / backup | Оператор; сторонний читатель логов/копий | HTTP completion, application events, recovery runbook | Secrets/PII exposure, утрата ключа/данных | Safe completion fields, redaction, allowlist, отдельный encryption key; live storage не проверено |
### 3.3. Threat Boundary Matrix — семь прослеженных семейств потоков
| Flow | Вход → обязательная boundary → consumer | Проверка nested ownership / replay | Результат |
| --- | --- | --- | --- |
| 1. Login / refresh / logout | AuthController → CredentialsDto/AuthService → Session и AuthProvider/shared fetch | JWT algorithm/type, hash rotation; неуспешный refresh не пишет cookie | CAS сохранить; password mismatch RISK-007; perimeter RISK-008 |
| 2. Replace / delete connection | Controller `request.user.sub` → TbankConnectionService → token validator/crypto → SQLite transaction → frontend reset | Client userId не authority; old generation каскадно удаляется; неудачная validation сохраняет старую connection | Проверенные fences сохранить |
| 3. Portfolio / events / all | Guard + user.sub → `access.local(userId, accountId)` → scoped repositories/ForecastService → AccountPage | `all` ограничен membership пользователя; forecasts scoped user+revision; нет fallback другого owner | RISK-003/004/005; confirmed IDOR path не найден |
| 4. Ledger / income / sync status | Controllers → access.local → scoped readPage и cursor HMAC/binding → durable work status | Binding включает userId, revision, account revisions и filters; sync status читает user/revision/id | Authorizing checks сохранить; read-owner drift — cross-audit signal |
| 5. Banking create / transfer / refund / update / delete | Guard → DTO/input → BankCardsRepository.write → owner lookups + CAS + DB constraint → explicit DTO | Проверены source/target card, category, original expense, duplicate UUID другого owner, deleted replay | Равносильной замены fingerprint/CAS/tombstone не найдено; KEEP |
| 6. Instrument search / detail / screener | Guard → current/onlineToken → fixed upstream → public projection; searchId → owner+revision+TTL | Public instrument sources отделены от personal holdings; screener pages проверяют User и generation | RISK-006; shared public cache сам по себе не утечка |
| 7. Hourly work / publication / restart | Native timer → reserveHourly → membership/portfolio/operations/events owners → conditional settle | AttemptId/processRunId и transaction fences; stale publication не становится актуальной | RISK-001/002; multi-process lease пока не нужен при одном процессе |
**Основные evidence для матрицы:** `backend/src/modules/auth/application/auth.service.ts:64–140,166–201`; `backend/src/modules/auth/presentation/auth.controller.ts:81–137`; `backend/src/modules/investments/tbank-connection/tbank-connection.controller.ts:32–56`; `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31–88`; `backend/src/integrations/tbank/tbank-access.service.ts:148–237,262–288`; `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:82–148`; `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:347–388`; `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:313–389`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–240,251–330`; `backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:65–109`.
### 3.4. Репрезентативность и пределы
Выборка risk-based: все controller guards просмотрены, а детальная трассировка ограничена семью семействами выше и их общими security/persistence boundaries. Прочитаны репрезентативные configured HTTP/SQLite tests: auth rotation/storage failures, banking foreign links/refund race/replay, forecast filtering/late calendar; наличие test source не доказывает его прохождение.
Pattern searches в frontend `src/**/*.{ts,tsx}` по `dangerouslySetInnerHTML|innerHTML|eval\(|new Function|localStorage|sessionStorage` и backend `src/**/*.ts` по unsafe/raw SQL, `child_process`, `execSync`, `eval` совпадений не дали. Это ограниченный inventory потенциальных sinks, не автоматический SAST и не доказательство безопасности всех данных.
Вне покрытия: dependency CVE audit, все финансовые формулы/инструменты, вся active OpenSpec history, CI/tooling, реальные secrets и DB, серверные права/HTTPS/WAF, live restore, raw/generated artifact scans. Отсутствие `graphify` graph и информации о внешнем proxy ограничивает только соответствующие способы подтверждения, не отменяет source findings.
## 4. Security & Reliability Findings
### RISK-001 — Durable `running` не имеет гарантированного выхода после persistence failure
- **Decision = FIX; Severity = P1; Confidence = High; Effort = M.** Тип: дефект recovery по коду; частота SQLite failures `UNVERIFIED`.
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:166–189,197–218`; `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:190–201,223–242`; `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:563–593`; `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:195–230,296–301`.
- **Claim:** кратковременная ошибка publish/finish/fail либо settle может закончить process-level работу, оставив durable row `running`. Hourly waiter не имеет deadline; `reserveHourly` не заменяет такой row в том же processRunId.
- **Current mechanism:** events refresh возвращает work IDs; `settledWorks` опрашивает их каждые 100 ms до выхода из `running`. Общий catch enrichment очищает completions/active/queue, но не переводит abandoned rows в terminal state. `settleHourly(...).catch(() => undefined)` также подавляет ошибку записи.
- **Threat model:** свежие personal datasets → storage failure/прерванный worker → publish/finish или settle → предположение «каждый running обязательно завершится» → бесконечное ожидание и запрет следующего обхода → existing attempt/generation fences → bounded observation и согласованный recovery.
- **Impact / actual cost:** автоматическое обновление connection может остановиться до restart или иного пути, заново резервирующего дочернюю работу. Также остаётся периодический DB polling. **Impact unmeasured**: число затронутых connections, длительность и DB/runtime cost не измерены.
- **Why now:** hourly — текущий владелец обновления personal datasets; пассивный portfolio GET не исправляет lifecycle. Если stuck row принадлежит hourly, даже восстановление SQLite само по себе его не освобождает.
- **Minimum change:** ограничить ожидание дочерних attempts; различать завершённую и abandoned работу; при восстановившемся storage условно завершать/перерезервировать запись по исходному attemptId. Ошибку самого settlement сделать наблюдаемой и повторно сверять её на штатном tick. Одного дополнительного UPDATE в catch недостаточно, если SQLite ещё недоступна.
- **Guarantees at risk:** timeout не равен cancellation RPC. При освобождении work поздний ответ должен проиграть attempt/generation fence; старый child не должен завершить новый attempt. Сохранённые успешные snapshots нельзя удалять ради выхода из running.
- **Counterarguments / trade-offs:** перезапуск помогает благодаря processRunId, а новый data request иногда восстановит child; это не гарантированное восстановление в живом процессе. Межпроцессный lease проблему одного процесса не решает. Ограничение observation должно учитывать допустимую длительность большого портфеля.
- **Verification or rollback:** isolated SQLite regression с однократным отказом `publishSource`/`finish` и отдельно `settleHourly`: storage восстанавливается, row выходит из running, следующая eligibility разрешает новую работу, late publication старого attempt отклоняется. Существующие suites hourly не проверяют: `backend/test/upcoming-events.e2e-spec.ts:140–147` подменяет scheduler. Runtime proof сейчас `UNVERIFIED`; rollback локальных lifecycle changes не должен менять schema/data.
- **Related finding IDs:** RISK-002, RISK-004; прежняя гипотеза technical F2 заново подтверждена.
### RISK-002 — Общая ошибка обхода может публиковаться как полный успех
- **Decision = FIX; Severity = P2; Confidence = High; Effort = S.** Тип: code-level correctness bug статуса.
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93–110,113–125,145–151,178–190`; `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:32–56,245–271`; consumer `backend/src/modules/investments/portfolio/application/portfolio.service.ts:55–75,207–229`.
- **Claim:** exception из `membership.request`, `workError`, `access.local` или orchestration попадает в общий catch без добавления failure. Пустой `failures` становится `succeeded`, даже когда обязательные шаги не выполнены.
- **Current mechanism:** итог выводится из длины failures и счётчика attempted, а не из достигнутого завершения обязательного обхода. Например, `membership.request` может бросить до первого increment; catch вызывает `settle([], 0)` → `succeeded`.
- **Threat model:** trustworthy freshness/outcome → DB exception → walk catch → отсутствие локального failure ошибочно принимается за успех → клиент получает ложный `lastAttempt` → existing безопасные public codes → явно unsuccessful aggregate outcome.
- **Impact / actual cost:** пользователь и диагностика видят успешный обход со старыми либо отсутствующими данными. **Impact unmeasured**; payload не обязательно повреждён — дефект в достоверности статуса.
- **Why now:** этот status переносится в metadata нескольких финансовых resources; он нужен для понимания ошибок обновления.
- **Minimum change:** общий catch обязан явно фиксировать безопасную причину и failed/partial outcome; считать skipped mandatory sections незавершёнными. Использовать текущие progress/sections и logger, не вводить новый status framework.
- **Guarantees at risk:** успешные account snapshots должны сохраняться при частичной ошибке. Смена/deletion connection — interruption, а не ложная проблема новой generation. Не выводить raw exception/message в public sections.
- **Counterarguments / trade-offs:** локальные operations/events catches уже добавляют failures и работают лучше; они не защищают исключения вне своих блоков. `attempted` не доказывает completion и может давать partial при фактическом отсутствии выполненных обязательных секций.
- **Verification or rollback:** controlled orchestration case с reject membership reservation до increment и с reject `workError` после завершённого child: `lastAttempt` не succeeded, сохранённый payload неизменен. Проверить отдельный partial-success case. Runtime proof `UNVERIFIED`.
- **Related finding IDs:** RISK-001, RISK-005; прежняя technical F2.
### RISK-003 — Overlapping fallback блокирует публикацию нового полного forecast
- **Decision = FIX; Severity = P2; Confidence = High; Effort = M.** Тип: code-level correctness bug и расхождение behavioral requirement.
- **Evidence:** `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79,80–174`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152–173,271–279`; `backend/src/modules/investments/portfolio/application/forecast.service.ts:35–94,98–123`; `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:10–24,63–76`.
- **Claim:** при новом scopeKey и существующем пересекающемся forecast `if (!previous && fallback) return fallback` возвращает старый payload даже при полном новом candidate. Ежедневный сдвиг default window — обычный достижимый путь, не искусственная атака.
- **Current mechanism:** accountId/from/to входят в persisted identity. Без exact row ищется первый overlap без `orderBy`; новый candidate не достигает dependency validation/upsert. При отсутствии candidate и непересекающемся окне последний forecast также не выбирается, хотя spec требует успешный пустой фильтр сохранённого результата.
- **Threat model:** прогнозные суммы и исходное окно → нормальная смена UTC дня/новых holdings → events.get/resolve → fallback принят за достаточную замену нового полного расчёта → сохранение прежних выплат → transactional version checks → отделить публикацию полного результата от фильтрации чтения.
- **Impact / actual cost:** пользователь может продолжать получать старое количество бумаг/суммы после обновления sources; out-of-window read без candidate может вернуть `data: null` вместо сохранённого пустого результата. **Impact unmeasured**.
- **Why now:** окно меняется штатно, а fallback является основной защитой последнего полного прогноза при failures. Ошибка находится внутри этой важной защиты.
- **Minimum change:** у существующих ForecastService/Repository развести выбор последнего полного результата конкретного owner/account и фильтрацию request window. Полный новый hourly-расчёт должен публиковаться, неуспех — оставлять прежний. Задать детерминированный порядок выбора сохранённого forecast.
- **Guarantees at risk:** не заменять полный прогноз частичным; сохранить его исходные `calculatedAt`, dependency revisions и окно при простом фильтрующем GET. Membership/portfolio/source CAS checks необходимы. Простого удаления строки 79 без проверки read semantics недостаточно.
- **Counterarguments / trade-offs:** fallback нужен; существующий `backend/test/upcoming-events.e2e-spec.ts:619–640` правильно требует сохранение timestamp при узком read window. Он не проверяет новый полный candidate после смены holdings и окна. Само пересечение не доказывает актуальность расчёта.
- **Verification or rollback:** isolated repository/service cases: старый window + новый полный candidate с изменившимся количеством; narrow read без новой публикации; failed refresh; несколько сохранённых windows; пустое пересечение. Проверить суммы, `calculatedAt`, выбранную revision и сохранность rollback, а не число DB calls. Runtime proof `UNVERIFIED`.
- **Related finding IDs:** RISK-004, RISK-005; прежняя technical F1.
### RISK-004 — Hourly не обновляет сохранённые event calendars
- **Decision = FIX; Severity = P2; Confidence = High; Effort = S.** Тип: дефект refresh policy.
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:166–175`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:182–239`; `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,152–158,164–193,607–623,679–692`; `specs/openspec/specs/hourly-investment-sync/spec.md:8–9`.
- **Claim:** hourly events.refresh идёт через readMany/requestMany с `force=false`; dividends/bond-events имеют `stable` policy. Если row уже существует, её наличие считается достаточным, независимо от времени наблюдения.
- **Current mechanism:** обновление по TTL есть только у quote kinds. `bond-events` не имеет rangeKey, поэтому может остаться на первом успешном ответе бессрочно. Dividends получают новый key при изменении окна/дня и могут загружаться заново тогда, но изменение выплаты внутри прежнего окна не подхватывается последующими часовыми обходами.
- **Threat model:** полнота ожидаемых выплат → изменившиеся данные provider → hourly calendar refresh → календарь ошибочно считается неизменяемым справочником → новые/отменённые выплаты не отражены → versioned source publication → целевой hourly refresh calendars.
- **Impact / actual cost:** обещанный обход выполняется без RPC за актуальным календарём; RISK-003 может затем отдельно заблокировать и новый forecast. **Impact unmeasured**; дополнительные необходимые RPC тоже пока не измерены.
- **Why now:** portfolio holdings и payout calendars — изменяемые источники, на которых построены пользовательские прогнозы.
- **Minimum change:** передавать существующий force именно для event-calendar sources в hourly-пути либо задать им обоснованный ограниченный TTL. Стабильные metadata переиспользовать; обычный GET оставить пассивным. Не force-обновлять все kinds ради двух календарей.
- **Guarantees at risk:** rate budget, one producer/observer coordination, сохранение старого полного calendar при malformed/error и late generation fence.
- **Counterarguments / trade-offs:** public shared calendar cache полезен и не требует удаления. Daily key частично маскирует проблему dividends, но не решает intraday изменения и постоянный bond-events key. Force всех instrument sources создаст ненужный traffic.
- **Verification or rollback:** два controlled hourly refresh одной connection и окна с разными provider responses; второй calendar действительно обновляет revision и результат. Fail второго запроса сохраняет первый; обычный GET не делает RPC. Runtime proof `UNVERIFIED`.
- **Related finding IDs:** RISK-001, RISK-003; прежняя technical F3.
### RISK-005 — AccountPage скрывает свежесть portfolio и failed background read
- **Decision = FIX; Severity = P2; Confidence = High; Effort = S.** Тип: failure-state UX / достоверность финансового представления.
- **Evidence:** `frontend/src/entities/portfolio/api/usePortfolio.ts:19–25`; `frontend/src/shared/api/resource-query.ts:129–147`; `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:86–101,125–135`; `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:24–34`; `frontend/src/pages/account/ui/AccountPage/components/AccountPageLoaded.view.tsx:55–59`; `frontend/src/shared/ui/SourceSyncStatus/SourceSyncStatus.tsx:23–47`; test consumer `frontend/src/pages/account/ui/AccountPage/AccountPage.test.tsx:751–855`.
- **Claim:** VM возвращает `source: portfolio.meta`, но container не передаёт source в loaded view. При cached data transport error не включается в loaded presentation и не даёт retry.
- **Current mechanism:** query wrapper предоставляет data/meta/error раздельно. SourceSyncStatus уже умеет показывать timestamp, stale и lastAttempt, но получает undefined. VM сохраняет loaded при наличии data; это полезно, однако ошибка остаётся невидимой.
- **Threat model:** смысл сохранённых финансовых сумм → upstream/read failure → cached Query → metadata потеряна на VM→view boundary → старые суммы показаны без freshness/error context → existing status component → передать metadata и локальный read error.
- **Impact / actual cost:** пользователь не видит возраст данных и неудачное перечитывание; риск решений по устаревшим суммам. **Impact unmeasured**. Чужие данные этим дефектом не раскрываются.
- **Why now:** silent stale UI усиливает последствия RISK-001–004. Тест явно закрепляет отсутствие уведомлений, поэтому обычный зелёный suite не устранит проблему.
- **Minimum change:** передать существующий source prop; добавить безопасный local background-read error/retry у нынешнего page owner. Отдельно сохранить neutral first-load и access-failure path.
- **Guarantees at risk:** не стирать cached snapshot из-за transient failure, не показывать прежнего owner после auth/connection switch, не запускать personal sync кнопкой повторного чтения. Status должен быть доступным текстом/accessible name.
- **Counterarguments / trade-offs:** тихое сохранение payload предотвращает flash/error page, но сохранение и отсутствие failure context — разные решения. Частичный snapshot уже отображается отдельно и не заменяет freshness.
- **Verification or rollback:** existing route test с stale/failed `meta` и MSW background `503`: старые суммы остаются, timestamp/предупреждение видны, retry выполняет GET; успешный refetch снимает error. Runtime/browser proof `UNVERIFIED`.
- **Related finding IDs:** RISK-002, RISK-003, RISK-004; прежняя technical F5.
### RISK-006 — Invalid broker token в online paths превращается в retryable `500`
- **Decision = FIX; Severity = P2; Confidence = High; Effort = S.** Тип: error mapping / recovery defect.
- **Evidence:** `backend/src/integrations/tbank/tbank.client.ts:527–565`; `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:143–165,227–249,295–317`; `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:241–267`; `backend/src/integrations/tbank/tbank-token.service.ts:26–35`; `backend/src/common/filters/http-exception.filter.ts:19–31`; consumer `frontend/src/shared/api/client.ts:127–151,162–193`.
- **Claim:** если сохранённый token внешне отозван и ещё не помечен `invalidToken`, online RPC бросает обычный Error с внутренним code; online application path не переводит его в публичный `INVALID_TBANK_TOKEN`.
- **Current mechanism:** background owners умеют terminal invalid-token handling; connection.put использует `translateInvalidToken`. Online search/detail/company вызывают client напрямую. Filter безопасно скрывает Error, но выдаёт `500 INTERNAL_SERVER_ERROR`, который frontend считает transient.
- **Threat model:** корректное recovery connection → внешнее revoke credentials → online GET → постоянная ошибка принята за внутренний transient failure → повтор заведомо неуспешного RPC/неверное сообщение → existing translator → применить перевод на online boundary.
- **Impact / actual cost:** пользователь теряет recovery code. Для одного GET frontend допускает до **трёх HTTP попыток**: две итерации `RETRY_DELAYS_MS` и finalAttempt; это верхняя граница по коду, не измеренный traffic. Реальное amplification зависит от caller/provider. **Impact unmeasured**.
- **Why now:** online reference paths реализованы и доступны через защищённые controllers; background handling не защищает эти calls.
- **Minimum change:** переиспользовать текущий invalid-token translator у online application owner. Сохранить отдельные outcomes transient unavailable / not found. Решение о durable invalidToken flag должно быть generation-aware и не требовать записи без captured revision.
- **Guarantees at risk:** не отдавать raw provider error; не пометить новое подключение ошибочным из-за позднего failure старого token; не убрать bounded network retry для действительно transient errors.
- **Counterarguments / trade-offs:** `onlineToken` уже возвращает правильный code, если invalidToken flag выставлен; defect существует до обнаружения revoke background owner. `500` не раскрывает secret — это не data leak, а нарушение классификации.
- **Verification or rollback:** controlled online endpoint с provider `UNAUTHENTICATED`/`PERMISSION_DENIED`: публичный `400 INVALID_TBANK_TOKEN`, frontend GET не повторяется; transient upstream по-прежнему даёт `503`. Runtime proof `UNVERIFIED`.
- **Related finding IDs:** RISK-005; прежняя technical F13.
### RISK-007 — Нижняя граница пароля проверяется в разных единицах
- **Decision = FIX; Severity = P2; Confidence = High; Effort = S.** Тип: runtime validation / UX correctness, не password bypass.
- **Evidence:** `backend/src/modules/auth/presentation/auth.dto.ts:43–54,66–74`; `frontend/src/features/auth/model/auth.schema.ts:36–43`; `specs/openspec/specs/user-authentication/spec.md:10,109–110`; `docs/domains/authentication/overview.md:12–21`.
- **Claim:** frontend и behavioral spec требуют 12–72 UTF-8 bytes; backend minimum — MinLength(12), maximum — UTF-8 bytes. Для шести кириллических букв frontend minimum выполняется, backend minimum — нет.
- **Current mechanism:** единственный backend byte validator проверяет только максимум. `CredentialsDto` общий для login и registration; расхождение docs уже отмечает, но правило runtime остаётся несогласованным.
- **Threat model:** доступный auth flow → пользователь с не-ASCII password → frontend schema / server DTO → единицы длины ошибочно считаются одинаковыми → API отвергает принятый формой ввод → existing backend validator → единая policy на runtime boundary.
- **Impact / actual cost:** лишний запрос и непредсказуемый отказ формы; потенциальная несовместимость policy для существующих credentials. **Impact unmeasured**. Более строгий minimum не является подтверждённой эксплуатацией.
- **Why now:** это явно сохранённое требование, а не рекомендация внешнего checklist о более длинном пароле.
- **Minimum change:** проверять обе byte bounds в текущем PasswordBytesConstraint и согласовать minimum metadata. Если нужно изменить саму password policy — сначала отдельное решение владельца, затем согласованный frontend/backend contract.
- **Guarantees at risk:** верхние 72 bytes защищают от bcrypt truncation; пароль нельзя trim/normalize. Нельзя silently сделать существующий password непригодным для login при ужесточении registration policy.
- **Counterarguments / trade-offs:** byte minimum допускает меньше видимых не-ASCII символов; это уже выбранный spec contract, а не автоматически лучшая security policy. Изменение policy не следует маскировать исправлением единиц.
- **Verification or rollback:** existing configured HTTP validation boundary и frontend schema: ASCII и не-ASCII на 11/12/72/73 bytes, сохранение пробелов в password. Runtime proof `UNVERIFIED`.
- **Related finding IDs:** RISK-008; прежняя technical F12.
### RISK-008 — Защита public auth от перебора и resource exhaustion не подтверждена
- **Decision = INVESTIGATE; Severity = P2; Confidence = Medium; Effort = S.** Тип: architecture/deployment risk; **UNVERIFIED для фактической публичной установки**.
- **Evidence:** `backend/src/modules/auth/presentation/auth.controller.ts:55–79`; `backend/src/modules/auth/application/auth.service.ts:19–26,35–78`; `backend/src/main.ts:19–41`; `backend/src/app.module.ts:18–35`; `frontend/docker/nginx.conf:20–33`; `docs/domains/authentication/overview.md:89–95`; root `compose.yaml:40–43`.
- **Claim:** app-level ограничение auth attempts не реализовано; поставляемый Nginx proxy также не содержит его. Можно подтвердить отсутствие защиты в этой цепочке, но нельзя по ней подтвердить доступность обходного public пути или отсутствие защиты внешнего HTTPS proxy.
- **Current mechanism:** public login всегда выполняет bcrypt compare, включая dummy hash неизвестного email; register нового email выполняет bcrypt hash. Source inventory не обнаружил auth throttler/global guard. Frontend block submit не ограничивает прямые HTTP-клиенты.
- **Threat model:** account confidentiality / CPU availability → неаутентифицированный клиент с доступом к public auth → login/register → отсутствует доказанный admission budget → подбор credentials/накопление bcrypt work → existing generic errors, expensive hash, внешний proxy возможен → проверить perimeter и ограничить при необходимости.
- **Impact / actual cost:** условный brute-force/resource-exhaustion risk. Частота попыток, доступность сервера, CPU budget и реальные controls **not measured / UNVERIFIED**; account compromise не заявляется.
- **Why now:** credentials открывают доступ к сохранённым финансовым данным, а register/login намеренно публичны.
- **Minimum change:** сначала прочитать реальную внешнюю proxy policy и путь обхода. Если защиты нет, задать ограниченный budget auth requests на существующей boundary, отдельно рассмотрев распределённый подбор одного account. Не добавлять lockout, CAPTCHA, Redis или новый auth subsystem без этой проверки.
- **Guarantees at risk:** account lockout может дать attacker-controlled DoS; IP policy зависит от доверенной proxy chain. Generic login errors и dummy hashing сохраняются; logging попыток не должен содержать credentials/email values.
- **Counterarguments / trade-offs:** root Compose публикует frontend на loopback, внешний reverse proxy обязателен для HTTPS и может уже фильтровать. Docs честно описывает app limitation. При закрытом доступе remediation priority может быть ниже; нет оснований для P0/P1 exploitation claim.
- **Verification or rollback:** самый дешёвый шаг — static inspect live proxy configuration/правильность client IP без attack/load test; затем isolated auth boundary test rejection budget, отсутствие bcrypt/upstream work после rejection и восстановление доступа. Не проводить production brute force.
- **Related finding IDs:** RISK-007.
## 5. Protected Invariants Registry
`Safe-to-remove? = No` означает отсутствие доказанной равносильной замены, а не необходимость сохранить каждую нынешнюю строку реализации.
| Invariant | Mechanism | Evidence | Safe-to-remove? |
| --- | --- | --- | --- |
| Client не выбирает identity; nested resource чужого User недоступен | Verified user.sub + scoped card/operation/category/account queries | `backend/src/modules/banking/bank-cards/presentation/bank-cards.controller.ts:94–166`; `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:62–72`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:100–197`; `backend/src/integrations/tbank/tbank-access.service.ts:262–288` | No |
| Password hash и token не являются публичной User моделью | bcrypt и явный toUser projection | `backend/src/modules/auth/application/auth.service.ts:48–52,64–78,142–144`; `backend/src/modules/auth/presentation/auth.controller.ts:113–119` | No |
| Один current refresh имеет одного победителя; loser не отзывает победителя | SHA-256 hash высокоэнтропийного token + conditional updateMany + unique jti | `backend/src/modules/auth/application/auth.service.ts:99–113,166–205`; `backend/test/app.e2e-spec.ts:161–214` | No |
| Refresh cookie закрыта JS и ограничена auth boundary | HttpOnly, SameSite=Lax, Secure вне development, host-only/path | `backend/src/modules/auth/presentation/auth.controller.ts:125–137` | No |
| Auth/session switch не публикует старый response/cache в новой identity | In-memory token, generation checks, clear/reset Query cache, serialised cookie writes | `frontend/src/features/auth/model/auth.vm.ts:41–115,121–151`; `frontend/src/shared/api/client.ts:53–119` | No |
| Broker token зашифрован с integrity protection | AES-256-GCM с random nonce и authenticated tag; validated key | `backend/src/integrations/tbank/tbank.crypto.ts:14–49`; `backend/src/config/env.validation.ts:75–83` | No |
| Failed token replacement не портит прежнюю connection; старые writes не проходят | Validation до DB transaction; fresh revision/cascade + attempt fences | `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31–88`; `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:121–147`; `backend/prisma/schema.prisma:25–71,232–262` | No |
| Lost create response не даёт второй banking effect, changed intent не переиспользует ID | UUID primary key + normalized createFingerprint после ownership check | `backend/src/modules/banking/bank-cards/application/bank-cards.service.ts:74–95`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:214–240`; `backend/src/modules/banking/bank-cards/application/bank-card-input.ts:74–81` | No |
| Deleted operation не воскресает от delayed create replay | Tombstone + same-ID replay → 410 | `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:221–230,286–330` | No |
| Competing edit/refund сохраняет version и refund ceiling | CAS source/version, refund sum и write transaction/retry | `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:152–209,251–330`; `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–59`; `backend/test/bank-cards.e2e-spec.ts:462–514` | No |
| Деньги не теряют точность и не суммируются между валютами | BigInt decimal и RUB kopecks bounds; grouped currency totals | `backend/src/common/decimal.ts:19–83`; `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38`; `backend/src/modules/investments/portfolio/infrastructure/tbank-portfolio.mapper.ts:109–138` | No |
| Cursor сохраняет owner/filter/data identity | HMAC verification + owner/revision/filter binding | `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:347–388`; `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:128–148` | No |
| Частичная external page не заменяет полный operation dataset | Stage marker, continuation checks, duplicate conflict detection, atomic rows+coverage+revision | `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:602–699,712–881` | No |
| Forecast publication соответствует версиям обязательных dependencies | Connection write fence + portfolio/source revision/timestamp checks | `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:35–49,80–174` | No; исправление RISK-003 не удаляет fence |
| Shared public sources не содержат personal holdings/credentials | Public projection, initiating/observing attempt authorization | `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–677`; `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–180,252–277` | No |
| Upstream overload и secret errors имеют bounded handling | TLS, queue bound, deadline, два RPC attempts, безопасные context/error codes | `backend/src/integrations/tbank/tbank.client.ts:49–54,214–218,504–586`; `backend/src/integrations/moex/moex.client.ts:164–198`; `backend/src/integrations/ncr/ncr-ratings.client.ts:36–57` | No |
| Raw secrets/body/SQL/error stack не выдаются в HTTP completion/envelope | Problem filter + allowlisted completion/context | `backend/src/common/filters/http-exception.filter.ts:19–47`; `backend/src/common/logging/logging.module.ts:15–54`; `backend/src/common/logging/structured-logger.service.ts:7–25,43–60` | No; текущие messages дополнительно проверяются семантически |
| Backup/restore не создаёт смесь БД и ключа/новых данных | Stop writers, full data-directory backup, отдельный key, explicit isolated restore | `docs/architecture/deployment.md:137–176` | No; runtime execution UNVERIFIED |
**Важные контрсценарии:** убрать fingerprint → retry с тем же UUID, но другой суммой больше не отличим от исходного intent; убрать tombstone → delayed POST сможет создать удалённую операцию; убрать CAS → stale PATCH или competing refund теряет установленную границу; убрать revision/attempt fence → ответ до replacement может опубликоваться после него. Эти механизмы не являются ритуальными guards.
### Never remove without replacement
- Server-side DTO validation, explicit identity/ownership и проверки всех nested references.
- Bcrypt byte ceiling, safe public User projection, refresh hash CAS, token type/algorithm/expiry и cookie flags.
- AES-GCM integrity/key validation, generation rotation, conditional late-publication fences.
- Banking createFingerprint, client UUID, version CAS, tombstones, refund serialization и transaction rollback.
- Exact Money representation и currency separation.
- Staging completeness/continuation/duplicate guards и атомарное обновление rows/coverage/revisions.
- Forecast dependency checks и сохранение последнего полного результата при partial failure.
- TLS, queue bounds, timeout/retry limits, safe error mapping и log projections.
- Durable attempt identity и восстановление после restart; RISK-001 требует усилить lifecycle, а не удалить его.
## 6. Failure Recovery Map
| Failure | Текущий механизм / observable state | Recovery и предел | Evidence / decision |
| --- | --- | --- | --- |
| Unknown email / wrong password | Generic INVALID_CREDENTIALS, dummy bcrypt для unknown user | Новый login; admission budget вне app не подтверждён | `auth.service.ts:64–78`; RISK-008 |
| Concurrent / superseded refresh | Один hash CAS winner, loser 401 без Set-Cookie | Winner usable; loser допускает re-login; нет cross-tab lock/history | `auth.service.ts:99–113,183–190`; `auth.controller.ts:81–90`; KEEP |
| Refresh/logout storage failure | 500 через safe filter, cookie не перезаписывается неуспехом | Возможен retry; frontend очищает локальную identity при failed refresh/logout | `backend/test/app.e2e-spec.ts:102–159`; `frontend/src/features/auth/model/auth.vm.ts:81–115,135–141`; tests не запускались |
| Invalid replacement broker token | Validation вне transaction, InvalidToken translator | Старые connection/private rows сохранены | `tbank-connection.service.ts:31–72`; KEEP |
| Upstream transient / queue full | Два bounded RPC attempts; queue overflow сразу 503 | Следующая штатная попытка, без вложенного service retry | `tbank.client.ts:504–568`; KEEP |
| Lost banking POST response | Same UUID + same fingerprint → current DTO | Safe replay; changed intent 409; deleted replay 410 | `bank-operations.service.ts:214–240`; KEEP |
| Competing banking edit / refund | CAS/transaction конфликт откатывает записи | Перечитать version и повторить осознанный edit | `bank-operations.service.ts:152–209,251–330`; KEEP |
| Partial operations pagination | Staging не публикуется без complete marker | Old shown scope сохранён; re-reservation меняет attempt и очищает stage | `brokerage-operations.repository.ts:409–436,625–639,724–734`; KEEP |
| Calendar/provider parse failure | Safe unavailable, старый full source/forecast сохраняется | Следующая authorized refresh; stable policy может не освежить сохранённый calendar | `catalog-enrichment.service.ts:524–578`; RISK-004 |
| Child persistence failure / hourly settle failure | In-memory worker завершён, durable running может остаться | Automatic live-process recovery не гарантирован; restart помогает, но не заменяет нормальный recovery | RISK-001 |
| Orchestration exception до completion | Общий catch с прежним списком failures | Может falsely succeeded; требуется явный unsuccessful aggregate | RISK-002 |
| Process restart | Другой processRunId отменяет hourly running, срок определяется началом прежней попытки | Не более одного актуального следующего запуска; multi-process lease отсутствует намеренно | `tbank-sync.repository.ts:174–220`; single process — root `compose.yaml:32–38` |
| Old forecast / changed request window | Exact scope либо первый overlap | Новый full candidate может быть отвергнут; non-overlap без candidate даёт null | RISK-003 |
| Cached portfolio + read failure | Cached payload сохраняется | Status/retry на loaded page теряются | RISK-005 |
| Внешне отозванный token до background detection | Online path даёт safe 500 | Frontend повторяет transient GET вместо нужного recovery | RISK-006 |
| SQLite/disk/key loss | Docs требует согласованный backup и isolated restore | Локальный backup не защищает от потери диска; live/off-host restore и сохранность ключа UNVERIFIED | `docs/architecture/deployment.md:137–176` |
Сокращённые имена source files в этой таблице относятся к полным evidence в findings и registry; это карта механизмов, не новые findings.
## 7. DELETE / SIMPLIFY / KEEP / INVESTIGATE
| Decision | Объект | Основание |
| --- | --- | --- |
| DELETE | Подтверждённых безопасных удалений protective mechanisms нет | No replacement proof; общий dead-code cleanup — другой audit |
| SIMPLIFY | Подтверждённых обязательных complexity changes нет | Новый workflow/retry/lease framework не решает найденные локальные дефекты |
| KEEP | Ownership, refresh CAS/cookie boundary, AES-GCM | Защищают identity/credentials, не заменяются UI validation |
| KEEP | Banking UUID + fingerprint + version CAS + tombstones + transactions | Разные сценарии: replay, changed intent, stale edit, delayed resurrection, refund concurrency |
| KEEP | Connection/attempt revisions, stage completeness, forecast dependency checks | Защита late publication, partial reconciliation и исходных финансовых assumptions |
| KEEP | Exact decimal/BigInt/currency grouping | Защита monetary correctness |
| KEEP | Bounded RPC retry/queue/TLS, safe Problem/log projections | Необходимые integration/error boundaries |
| KEEP | Один native scheduler timer без inter-process lease | Минимально достаточно при подтверждённом single-process deployment contract; перед scaling нужна замена |
| FIX | RISK-001–007 | Восстановление, достоверность результатов и согласованность runtime boundaries |
| INVESTIGATE | RISK-008: external auth admission policy | Приложение не ограничивает auth; production perimeter не проверен |
| INVESTIGATE | Off-host backup + original encryption key + live restore | Инструкции есть, выполнение/сохранность копий не доказаны; не объявляется code vulnerability |
## 8. Cost & benefit
| Action | Класс ожидаемой пользы / стоимости | Фактическое измерение | Обязательная граница регрессии |
| --- | --- | --- | --- |
| RISK-001/002 | Reliability, диагностируемость, меньше бесконечного polling/manual restart; локальные дополнительные branches | not measured | Late attempt fencing, partial success, DB rollback, восстановление без data deletion |
| RISK-003/004 | Financial correctness/freshness; calendars потребуют дополнительных RPC | not measured | Старый full forecast при failure, passive GET, shared-source isolation, rate budget |
| RISK-005 | User trust/recovery; reuse existing status component | not measured | Cached payload не стирается; auth/access change его скрывает |
| RISK-006 | Runtime traffic и корректное recovery постоянной ошибки | not measured; максимум три frontend GET attempts — только source bound | Transient retry остаётся bounded; новая connection не помечается старым failure |
| RISK-007 | Меньше rejected auth input и cognitive load несогласованной policy | not measured | Bcrypt 72-byte ceiling и неизменность password bytes |
| RISK-008 verification | Подтвердить существующую защиту прежде, чем добавить новую | not measured | Trusted client IP, отсутствие attacker-induced account lockout |
| KEEP registry | Избежать data-loss/security regression от cleanup | not measured | Сохранение всех перечисленных invariants |
Экономия CI minutes, agent tokens, процентов runtime, строк кода/dependencies не измерялась и не заявляется. Подключение нового Redis, distributed scheduler или security framework не обосновано текущей проверенной областью.
## 9. Top actions
### Quick wins — подтверждённые локальные дефекты
1. **RISK-005 / S:** передать существующий portfolio metadata prop и показать read failure/retry, сохраняя snapshot.
2. **RISK-006 / S:** подключить existing invalid-token translation к online paths, проверить status/code и отсутствие frontend повторов постоянной ошибки.
3. **RISK-002 / S:** общий exception обхода завершать явным unsuccessful outcome; не выдавать `succeeded` до обязательного completion.
4. **RISK-004 / S:** targeted hourly refresh только payout calendars через существующий механизм force/TTL.
5. **RISK-007 / S:** выровнять runtime UTF-8 bounds по выбранной password policy.
### Требуют test proof / уточнения семантики
6. **RISK-001 / M — главный reliability priority:** bounded wait и recovery abandoned/settlement work в живом процессе; isolated SQLite failure injection и поздний ответ старого attempt.
7. **RISK-003 / M:** закрепить разделение hourly full publication и фильтрующего forecast GET, затем исправить selection/persistence. ADR нужен только при изменении самой behavioral policy, а не ради локальной правки guard.
RISK-008 не превращён в подтверждённое внедрение rate limiter: сначала дешёвая verification внешнего proxy в следующем разделе. Масштабный рефакторинг, новое хранилище и миграции не входят в эти действия.
## 10. Cross-audit signals
- **08 — Test quality:** отсутствие прямых hourly `tick/reserveHourly/settleHourly` assertions и старые local-read race сценарии; `backend/test/upcoming-events.e2e-spec.ts:140–147`, `backend/test/tbank-local-reads.e2e-spec.ts:489–515`. Не дублировать общий audit test quality; relevant regression cases уже перечислены выше.
- **07 — API/data-flow efficiency:** income read ещё инициирует membership/request planning; `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:408–435`. Это другой owner refresh, а не отдельная security находка данного аудита.
- **15 — SDD/docs:** существующий events DTO не содержит исходное forecast window, хотя spec его требует; `backend/src/modules/investments/upcoming-events/upcoming-events.dto.ts:68–79`, `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:12,86–87`. Требуется согласовать contract, не добавить произвольное поле в рамках этого read-only аудита.
- **06/15 — Backend policy/spec coherence:** eligibility длительного обхода разрешается по elapsed hour вместо пропущенного term; `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:190–201`, `specs/openspec/specs/hourly-investment-sync/spec.md:19–25`. Не трактуется как overlap эксплуатация: running guard существует.
- **12 — Configuration:** off-host backup, permissions ключей и внешние HTTPS/rate policies принадлежат реальному deployment; `docs/architecture/deployment.md:123–143`. Не создавать дублирующий конфигурационный аудит без этих inputs.
## 11. Open questions / verification plan
| Вопрос, меняющий решение | Текущее основание | Самый дешёвый следующий proof |
| --- | --- | --- |
| Ограничивает ли live external HTTPS proxy auth и можно ли обойти его? | RISK-008; frontend Nginx без limit, root port на loopback | Пассивно посмотреть реальную proxy policy и network exposure. Если контролируется — KEEP существующую защиту, не внедрять второй limiter |
| Какой deadline допустим для большого hourly portfolio и как fence отменяет старую работу? | RISK-001; нет верхней границы aggregate waiter | Controlled isolated failure case сначала; затем representative длительность без live accounts. Deadline не должен разрешать stale publication |
| Чем отделяются новая полная forecast publication и простое фильтрующее чтение? | RISK-003 и существующий narrow-window test | Утвердить ожидаемые outcomes трёх случаев: new full candidate, failed refresh, filter-only read. Один isolated SQLite case на каждую границу |
| Можно ли доказать auth recovery при зависшем, а не rejected transport? | `frontend/src/shared/api/client.ts:127–131` обходит GET deadline для auth; `frontend/src/features/auth/model/auth.vm.ts:85–118,135–140` ожидает serialised запрос | MSW deferred refresh без ответа + controlled timer. Source показывает отсутствие app deadline; необходимость и допустимый срок требуют принятого bootstrap/recovery contract. До этого не добавлять speculative timeout во все mutations |
| Какая auth atomicity нужна при failure создания первой Session после User commit? | `backend/src/modules/auth/application/auth.service.ts:48–61,152–164`: отдельные writes | Isolated failure в session.create после user.create; определить требуемый rollback или явный login recovery. Account сохраняется и login возможен, поэтому не заявлена потеря credentials/critical exploit |
| Должны ли concurrent logout/refresh возвращать строго один terminal session outcome? | `backend/src/modules/auth/application/auth.service.ts:95–113,127–138,183–190`: refresh CAS только по hash, logout проверяет hash до отдельного update | Controlled two-request barrier на isolated DB; согласовать семантику до расширения CAS. Bounded revocation принята в `docs/adr/006-auth-session-policy.md:11–15`; access до 15 минут после logout явно допускается в `docs/domains/authentication/overview.md:89–95`, сам по себе это не auth bypass finding |
| Сохранены ли independent backup и original encryption key, выполнялся ли restore? | `docs/architecture/deployment.md:137–176`; AES-GCM не читает данные с новым ключом | Проверить только наличие процедуры/метаданные подтверждённого restore, не читать secrets/PII. При отсутствии proof — изолированный restore существующими средствами в отдельной разрешённой задаче |
| Missing MOEX block действительно означает отсутствие событий или повреждённый контракт? | `backend/src/integrations/moex/moex.client.ts:182–198` vs spec `brokerage-account-upcoming-events:82–84` | Согласовать provider contract на обезличенном response fixture. До этого KEEP malformed-row/cursor guards, не превращать unknown в подтверждённый пустой календарь |
### Завершение и ограничения доказательств
Проведена смысловая сверка findings с source evidence, consumer paths, контрсценариями и защищаемыми invariants. У каждого существенного defect указан runnable verification scenario для последующей исправляющей задачи. В этом аудите тесты и exploitation не проводились, поэтому ни один defect не назван runtime-воспроизведённым.
Финальная проверка: root/backend/frontend/specs/docs остаются с пустым `git status --short`; `git diff --check` не сообщает ошибок. Новый ignored отчёт просмотрен отдельно и проверен `git diff --no-index --check /dev/null audit-results/13-reliability-and-security-report.md`: обычный Git diff его не проверяет. `git check-ignore` подтверждает путь вне root allowlist. Boy Scout review отчёта: evidence не опирается на предыдущий аудит, severity не выведена из наличия guards, лишние удаления/метрики/абстракции не предложены. Исходники, specs, docs, тесты, конфигурации и history не изменены.
## 12. Consolidation table
| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| RISK-001 | FIX | P1 | High | backend | src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts; src/modules/investments/instruments/application/catalog-enrichment.service.ts; src/modules/investments/operations/application/brokerage-operations-sync.service.ts | Persistence failure может оставить running и заблокировать дальнейшие обходы живого процесса | M | RISK-002,RISK-004 |
| RISK-002 | FIX | P2 | High | backend | src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts | Неисполненный из-за общего exception обход может считаться succeeded | S | RISK-001,RISK-005 |
| RISK-003 | FIX | P2 | High | backend | src/modules/investments/portfolio/infrastructure/forecast.repository.ts; src/modules/investments/portfolio/application/forecast.service.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts | Старый overlapping forecast блокирует новый полный результат изменившегося окна | M | RISK-004,RISK-005 |
| RISK-004 | FIX | P2 | High | backend | src/modules/investments/instruments/application/catalog-enrichment.service.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts | Stable calendar policy пропускает новые/изменённые выплаты в hourly refresh | S | RISK-001,RISK-003 |
| RISK-005 | FIX | P2 | High | frontend | src/pages/account/ui/AccountPage/AccountPage.tsx; src/pages/account/ui/AccountPage/AccountPage.vm.ts; src/pages/account/ui/AccountPage/AccountPage.test.tsx | Cached financial sums показаны без уже подготовленной freshness/error metadata | S | RISK-002,RISK-003,RISK-004 |
| RISK-006 | FIX | P2 | High | backend,frontend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts; frontend/src/shared/api/client.ts | Постоянный invalid token превращается в 500 и повторные HTTP reads | S | RISK-005 |
| RISK-007 | FIX | P2 | High | backend,frontend | backend/src/modules/auth/presentation/auth.dto.ts; frontend/src/features/auth/model/auth.schema.ts | Non-ASCII password проходит frontend byte minimum и отвергается backend character minimum | S | RISK-008 |
| RISK-008 | INVESTIGATE | P2 | Medium | backend,frontend | backend/src/modules/auth/presentation/auth.controller.ts; backend/src/modules/auth/application/auth.service.ts; frontend/docker/nginx.conf | Public auth не ограничен в app/provided proxy; live perimeter protection UNVERIFIED | S | RISK-007 |