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

76 KiB
Raw Permalink Blame History

Аудит 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 / уточнения семантики

  1. RISK-001 / M — главный reliability priority: bounded wait и recovery abandoned/settlement work в живом процессе; isolated SQLite failure injection и поздний ответ старого attempt.
  2. 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