diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 44bb8ca..033694c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,17 +24,36 @@ jobs: scripts\build-release.ps1 -BuildDir build-release scripts\run-tests.ps1 -BuildDir build-release scripts\validate-licenses.ps1 - - name: Необязательная подпись и проверка Authenticode + # Стабильный тег vX.Y.Z подписывается обязательно: отсутствие + # сертификата, неудачная подпись, отсутствие метки времени или + # непрошедшая проверка подписи останавливают выпуск. Для тега + # предварительной версии vX.Y.Z-rc.N подпись остаётся необязательной — + # это осознанное исключение, описанное в docs/production/RELEASE_SIGNING.md. + - name: Обязательная подпись Authenticode для стабильного тега + if: ${{ !contains(github.ref_name, '-rc.') }} + shell: powershell + env: + AUTHENTICODE_PFX_B64: ${{ secrets.AUTHENTICODE_PFX_B64 }} + AUTHENTICODE_PFX_PASSWORD: ${{ secrets.AUTHENTICODE_PFX_PASSWORD }} + run: scripts\authenticode-sign.ps1 -File build-release\CHEBURNET.exe -Require + - name: Необязательная подпись Authenticode для предварительного выпуска + if: ${{ contains(github.ref_name, '-rc.') }} shell: powershell env: AUTHENTICODE_PFX_B64: ${{ secrets.AUTHENTICODE_PFX_B64 }} AUTHENTICODE_PFX_PASSWORD: ${{ secrets.AUTHENTICODE_PFX_PASSWORD }} run: scripts\authenticode-sign.ps1 -File build-release\CHEBURNET.exe + # Хеши и подписанный манифест считаются ТОЛЬКО после подписи, поэтому + # опубликованный SHA-256 описывает именно подписанный файл. - name: Сборка релиза и подписанных метаданных обновления shell: powershell env: CHEBURNET_SIGNING_KEY_CNG_BLOB_B64: ${{ secrets.CHEBURNET_SIGNING_KEY_CNG_BLOB_B64 }} run: scripts\prepare-release.ps1 -Tag '${{ github.ref_name }}' -Repository '${{ github.repository }}' -BuildDir build-release -OutDir dist + - name: Независимая проверка подписи публикуемого файла + if: ${{ !contains(github.ref_name, '-rc.') }} + shell: powershell + run: scripts\verify-authenticode.ps1 -File dist\CHEBURNET.exe -RequireTimestamp - name: Публикация релиза GitHub uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..c8cfb50 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,55 @@ +name: Статический анализ + +# Два независимых анализатора: встроенный в MSVC /analyze и CodeQL. +# /analyze запускается с /WX, поэтому любая новая находка в коде CHEBURNET +# останавливает задание. Подавлено ровно одно предупреждение — дефект +# SAL-аннотации в заголовке Windows SDK, обоснование в CMakeLists.txt. + +on: + push: + branches: ['main', 'claude/**'] + pull_request: + schedule: + - cron: '41 5 * * 3' + workflow_dispatch: + +permissions: + contents: read + +jobs: + msvc-analyze: + runs-on: windows-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Сборка с MSVC /analyze + shell: powershell + run: | + . scripts\vsenv.ps1 + Import-VcVars + & $CMakeExe -S . -B build-analyze -G Ninja -DCMAKE_MAKE_PROGRAM="$NinjaExe" ` + -DCMAKE_BUILD_TYPE=Release -DCHEBURNET_ENABLE_ANALYZE=ON + if ($LASTEXITCODE -ne 0) { throw 'configure failed' } + & $CMakeExe --build build-analyze + # ASCII only inside an inline PowerShell block: the runner writes it to a + # .ps1 that Windows PowerShell 5.1 reads as ANSI, so non-ASCII breaks parsing. + if ($LASTEXITCODE -ne 0) { throw 'MSVC static analysis reported findings' } + + codeql: + runs-on: windows-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4 + with: + languages: c-cpp + queries: security-and-quality + - name: Сборка для CodeQL + shell: powershell + run: scripts\build-release.ps1 -BuildDir build-codeql + - uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4 + with: + category: /language:c-cpp diff --git a/.github/workflows/upstream-check.yml b/.github/workflows/upstream-check.yml index a22e7a5..dcd496b 100644 --- a/.github/workflows/upstream-check.yml +++ b/.github/workflows/upstream-check.yml @@ -1,4 +1,17 @@ name: Проверка исходного выпуска + +# Новый стабильный выпуск Flowseal обязан превращаться в устойчивую задачу, +# а не в разовую строку в журнале запуска. +# +# Почему задача, а не черновой pull request: создание ветки требует +# contents: write у запланированного рабочего процесса, то есть постоянного +# права записи в репозиторий у автоматики, которая обрабатывает содержимое +# стороннего выпуска. Импортёр детерминирован и сверяет архив с неизменяемым +# digest релизного ассета, поэтому локальный запуск воспроизводит ровно то, +# что проверил CI, и права записи ничего не добавляют к полезности задачи. +# Разбор различий доступен в артефактах запуска. Ничто и никогда не +# объединяется автоматически. + on: schedule: - cron: '19 7 * * 1' @@ -8,15 +21,118 @@ permissions: contents: read jobs: - check: + detect: runs-on: windows-latest + permissions: + contents: read + outputs: + status: ${{ steps.report.outputs.status }} + version: ${{ steps.report.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Проверка последнего неизменяемого стабильного релиза + - name: Сравнение встроенной нагрузки с последним неизменяемым выпуском + id: report shell: powershell env: GITHUB_TOKEN: ${{ github.token }} - run: scripts\sync-upstream.ps1 -CheckOnly - - name: Политика обновления + run: | + scripts\upstream-report.ps1 -JsonOut upstream\report.json -MarkdownOut upstream\report.md + $report = Get-Content upstream\report.json -Raw | ConvertFrom-Json + "status=$($report.status)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "version=$($report.latest_version)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + # ASCII only inside an inline PowerShell block: the runner writes it to a + # .ps1 that Windows PowerShell 5.1 reads as ANSI, so non-ASCII breaks parsing. + if ($report.status -eq 'error') { throw "upstream release check failed: $($report.error)" } + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: upstream-detect + path: upstream/ + + validate: + needs: detect + if: needs.detect.outputs.status == 'available' + runs-on: windows-latest + permissions: + contents: read + outputs: + validation: ${{ steps.import.outputs.validation }} + detail: ${{ steps.import.outputs.detail }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # Импорт выполняется в одноразовой рабочей копии CI и никуда не + # публикуется. Сам импортёр уже проверяет кандидата в изолированном + # дереве: чистая сборка, полный набор тестов и побайтовая сверка с + # архивом. Здесь дополнительно собирается и тестируется итоговое дерево. + - name: Импорт кандидата и полная проверка + id: import shell: powershell - run: Write-Host 'Новый исходный релиз требует проверенной ветки и запроса на слияние; этот процесс никогда не объединяет бинарники автоматически.' + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + $validation = 'failed' + $detail = '' + try { + scripts\sync-upstream.ps1 -Version '${{ needs.detect.outputs.version }}' + scripts\build-release.ps1 -BuildDir build-upstream + scripts\run-tests.ps1 -BuildDir build-upstream + scripts\verify-upstream.ps1 + $validation = 'passed' + $detail = 'clean build, full test suite and upstream byte fidelity passed' + } catch { + $detail = [string]$_.Exception.Message + } + "validation=$validation" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "detail=$detail" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + git diff --stat -- resources | Out-File -FilePath upstream\payload-diff.txt -Encoding utf8 + git status --porcelain -- resources | Out-File -FilePath upstream\payload-status.txt -Encoding utf8 + scripts\upstream-report.ps1 -ImportedRoot $PWD -Validation $validation ` + -ValidationDetail $detail ` + -RunUrl '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' ` + -JsonOut upstream\report.json -MarkdownOut upstream\report.md + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: upstream-candidate + path: upstream/ + + task: + needs: [detect, validate] + if: always() && needs.detect.outputs.status == 'available' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: upstream-candidate + path: upstream + - name: Создание или обновление одной устойчивой задачи + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + UPSTREAM_VERSION: ${{ needs.detect.outputs.version }} + shell: bash + run: | + set -euo pipefail + marker="" + title="Upstream Flowseal ${UPSTREAM_VERSION} доступен для импорта" + body_file="$(mktemp)" + { + printf '%s\n\n' "$marker" + cat upstream/report.md + if [ -s upstream/payload-diff.txt ]; then + printf '\n### git diff --stat\n\n```\n' + cat upstream/payload-diff.txt + printf '```\n' + fi + } > "$body_file" + # Дедупликация по скрытому маркеру, а не по заголовку: заголовок + # может быть отредактирован человеком, маркер — нет. + existing="$(gh issue list --state all --limit 100 --json number,body \ + --jq "[.[] | select(.body != null and (.body | contains(\"${marker}\"))) | .number] | first // empty")" + if [ -n "$existing" ]; then + gh issue edit "$existing" --body-file "$body_file" + echo "updated existing issue #${existing}" + else + gh issue create --title "$title" --body-file "$body_file" + fi diff --git a/.gitignore b/.gitignore index f929a36..303c8fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Build output /build*/ /dist/ +/dist-*/ /artifacts/ *.obj *.pdb diff --git a/CHANGELOG.md b/CHANGELOG.md index f720ac7..bd379d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # История изменений +## Невыпущенное — подготовка стабильного 1.0.0 + +### Исправлено + +- Семантика версий RC/stable. Сборка по тегу `v1.0.0-rc.N` больше не + сообщает о себе как `1.0.0`: введён единственный источник версии + (`cmake/Version.cmake`), из которого выводятся семантическая версия, числовая + PE-версия и канал. Переход `1.0.0-rc.3 -> 1.0.0` теперь оценивается как + обновление, а не как актуальная версия. +- Порог `minimum_supported_version` включает уже выпущенные RC, иначе + установленный `1.0.0-rc.N` получал бы отказ «слишком старая программа». +- Ревизия upstream вида `1.10.2a` больше не считается понижением + относительно `1.10.2`. +- Полный импорт upstream не мог завершиться: изолированное дерево + проверки собиралось без `docs` и `SECURITY.md`, которые требует шлюз + лицензий. Добавлен регрессионный тест. +- Статический анализ нашёл и исправлено: отсутствие проверки + размера списка атрибутов при создании привилегированного процесса и + буфер передачи 64 КиБ в автоматической памяти. + +### Добавлено + +- Движок Flowseal `zapret-discord-youtube` обновлён до 1.10.2: новая + стратегия `ALT13` (21 -> 22), обновлённые списки и ресурсы. +- Обязательная подпись Authenticode для стабильных тегов: без действительной + подписи с меткой времени выпуск не публикуется; опубликованные байты + связаны с подписанными по SHA-256. +- Проверка обновлений при запуске вынесена из потока интерфейса: + подключение не ждёт сети, недоступный DNS больше не выглядит + зависанием. +- Новый стабильный выпуск Flowseal автоматически превращается в одну + устойчивую задачу с полными метаданными и результатом проверки. +- Релизный шлюз проверки в чистом окружении Windows и воспроизводимая + подготовка виртуальной машины. +- Статический анализ (MSVC `/analyze` с `/WX` и CodeQL), мутационная + проверка свойств разборщиков и полная решётка внесения отказов. +- Локальный экспорт диагностики с редактированием и без отправки. + ## 1.0.0-rc.3 — 2026-08-13 - Стабильный канал обновлений переведён на GitHub `releases/latest/download`: diff --git a/CMakeLists.txt b/CMakeLists.txt index 24918db..bc37ae5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,14 @@ cmake_minimum_required(VERSION 3.25) -project(CHEBURNET VERSION 1.0.0 LANGUAGES CXX) +# Единственный авторитетный источник версии (семантическая + числовая PE + +# prerelease + канал). Подключается до project(), поэтому использует +# CMAKE_CURRENT_LIST_DIR. +include("${CMAKE_CURRENT_LIST_DIR}/cmake/Version.cmake") + +project(CHEBURNET VERSION "${CHEBURNET_VERSION_PE}" LANGUAGES CXX) + +message(STATUS "CHEBURNET: версия ${CHEBURNET_VERSION_SEMANTIC} " + "(PE ${CHEBURNET_VERSION_PE}, канал ${CHEBURNET_RELEASE_CHANNEL})") set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -30,7 +38,7 @@ set(STRATEGY_SRC "${CMAKE_SOURCE_DIR}/resources/strategies_src") file(MAKE_DIRECTORY "${GEN_DIR}") # Launcher version is a single source of truth. C++, PE VERSIONINFO and the -# embedded application manifest are all configured from project(VERSION). +# embedded application manifest are all configured from cmake/Version.cmake. configure_file( "${CMAKE_SOURCE_DIR}/cmake/GeneratedVersion.h.in" "${GEN_DIR}/GeneratedVersion.h" @ONLY) @@ -122,6 +130,7 @@ set(LIB_SOURCES src/update/UpdatePackage.cpp src/update/RuntimeActivation.cpp src/update/UpdateManager.cpp + src/update/UpdateCheckService.cpp src/update/RuntimeStateStore.cpp src/console/ConsoleRenderer.cpp src/console/Mascot.cpp @@ -134,6 +143,8 @@ set(LIB_SOURCES src/ui/Effects.cpp src/ui/Widgets.cpp src/ui/Watchdog.cpp + src/app/Diagnostics.cpp + src/app/RuntimeRecovery.cpp src/app/UiScreens.cpp src/app/App.cpp ) @@ -144,11 +155,32 @@ target_include_directories(cheburnet_lib PUBLIC "${GEN_DIR}") target_compile_definitions(cheburnet_lib PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) -target_link_libraries(cheburnet_lib PUBLIC bcrypt crypt32 winhttp advapi32 shell32 ole32 user32) +target_link_libraries(cheburnet_lib PUBLIC bcrypt crypt32 winhttp advapi32 shell32 ole32 user32 wintrust) # Warnings / hardening for our own code. set(CHEBURNET_WARN /W4 /WX /permissive- /utf-8) set(CHEBURNET_HARDEN /guard:cf) + +# Статический анализ MSVC. Выключен по умолчанию: он заметно замедляет сборку и +# не должен мешать обычной разработке. Включается отдельным заданием CI +# (см. .github/workflows/static-analysis.yml) и локально: +# cmake -S . -B build-analyze -DCHEBURNET_ENABLE_ANALYZE=ON +# Находки разбираются, а не подавляются: список отключённых предупреждений +# ведётся здесь с обоснованием каждого. +option(CHEBURNET_ENABLE_ANALYZE "Enable MSVC /analyze static analysis" OFF) +if(CHEBURNET_ENABLE_ANALYZE) + # /analyze:external- не анализирует заголовки Windows SDK: их находки не + # наши и тонут в шуме. + # + # C28285 подавляется точечно и с обоснованием: это дефект SAL-аннотации + # CryptStringToBinaryA в самом wincrypt.h Windows SDK 10.0.26100.0 + # ("синтаксическая ошибка _Param_(1)"). Он относится к чужому заголовку, + # исправить его на нашей стороне невозможно, и он не указывает ни на какое + # свойство кода CHEBURNET. Никакие другие предупреждения анализа не + # подавляются: все находки в нашем коде разобраны и исправлены. + list(APPEND CHEBURNET_WARN /analyze /analyze:external- /wd28285) + message(STATUS "CHEBURNET: статический анализ MSVC включён") +endif() target_compile_options(cheburnet_lib PRIVATE ${CHEBURNET_WARN}) target_compile_options(cheburnet_lib PUBLIC ${CHEBURNET_HARDEN} $<$:/O2 /Zi>) diff --git a/cmake/GeneratedVersion.h.in b/cmake/GeneratedVersion.h.in index 9cf69f2..40762e6 100644 --- a/cmake/GeneratedVersion.h.in +++ b/cmake/GeneratedVersion.h.in @@ -1,11 +1,26 @@ -// СОЗДАНО АВТОМАТИЧЕСКИ CMake из версии project() — НЕ РЕДАКТИРОВАТЬ. +// СОЗДАНО АВТОМАТИЧЕСКИ CMake из cmake/Version.cmake — НЕ РЕДАКТИРОВАТЬ. #pragma once -#define CHEBURNET_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ -#define CHEBURNET_VERSION_MINOR @PROJECT_VERSION_MINOR@ -#define CHEBURNET_VERSION_PATCH @PROJECT_VERSION_PATCH@ +// Числовые компоненты. TWEAK несёт номер RC (0 для стабильного выпуска) и +// существует только ради числового формата PE-версии. +#define CHEBURNET_VERSION_MAJOR @CHEBURNET_VERSION_MAJOR@ +#define CHEBURNET_VERSION_MINOR @CHEBURNET_VERSION_MINOR@ +#define CHEBURNET_VERSION_PATCH @CHEBURNET_VERSION_PATCH@ +#define CHEBURNET_VERSION_TWEAK @CHEBURNET_VERSION_TWEAK@ -#define CHEBURNET_VERSION_QUAD @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0 -#define CHEBURNET_VERSION_STR "@PROJECT_VERSION@" -#define CHEBURNET_VERSION_RC_STR "@PROJECT_VERSION@.0" -#define CHEBURNET_VERSION_WSTR L"@PROJECT_VERSION@" +// PE VERSIONINFO: только числовая форма. +#define CHEBURNET_VERSION_QUAD @CHEBURNET_VERSION_MAJOR@,@CHEBURNET_VERSION_MINOR@,@CHEBURNET_VERSION_PATCH@,@CHEBURNET_VERSION_TWEAK@ +#define CHEBURNET_VERSION_PE_STR "@CHEBURNET_VERSION_PE@" + +// Семантическая версия — единственное значение, по которому принимаются +// решения об обновлении. Для RC она содержит prerelease-метку. +#define CHEBURNET_VERSION_STR "@CHEBURNET_VERSION_SEMANTIC@" +#define CHEBURNET_VERSION_WSTR L"@CHEBURNET_VERSION_SEMANTIC@" +#define CHEBURNET_VERSION_CORE_STR "@CHEBURNET_VERSION_CORE@" +#define CHEBURNET_VERSION_PRERELEASE_STR "@CHEBURNET_VERSION_PRERELEASE@" + +// Канал выпуска этой сборки. Стабильная сборка никогда не принимает +// предварительные версии; RC-сборка может перейти на новый RC или на стабильный. +#define CHEBURNET_VERSION_IS_PRERELEASE @CHEBURNET_VERSION_IS_PRERELEASE@ +#define CHEBURNET_STABLE_CHANNEL @CHEBURNET_STABLE_CHANNEL@ +#define CHEBURNET_RELEASE_CHANNEL "@CHEBURNET_RELEASE_CHANNEL@" diff --git a/cmake/Version.cmake b/cmake/Version.cmake new file mode 100644 index 0000000..c58ca3e --- /dev/null +++ b/cmake/Version.cmake @@ -0,0 +1,59 @@ +# --------------------------------------------------------------------------- +# CHEBURNET — единственный авторитетный источник версии. +# +# Здесь задаются только четыре поля. Всё остальное (семантическая версия, +# числовая PE-версия, признак предварительного выпуска и канал обновлений) +# выводится ниже, чтобы версия нигде не дублировалась вручную. +# +# Правила: +# * CHEBURNET_VERSION_PRERELEASE == "" -> стабильный выпуск vX.Y.Z +# * CHEBURNET_VERSION_PRERELEASE == "rc.N" -> предварительный выпуск vX.Y.Z-rc.N +# +# Числовая PE-версия намеренно отличается от семантической: PE-формат не умеет +# выражать prerelease, поэтому номер RC переносится в четвёртый компонент +# (1.0.0-rc.3 -> 1.0.0.3, стабильная 1.0.0 -> 1.0.0.0). PE-версия — +# информационная; решения об обновлении принимаются ИСКЛЮЧИТЕЛЬНО по +# семантической версии (CHEBURNET_VERSION_STR), см. docs/UPDATE_SECURITY.md. +# --------------------------------------------------------------------------- + +set(CHEBURNET_VERSION_MAJOR 1) +set(CHEBURNET_VERSION_MINOR 0) +set(CHEBURNET_VERSION_PATCH 0) +set(CHEBURNET_VERSION_PRERELEASE "rc.3") + +# --- производные значения: не редактировать --------------------------------- + +foreach(_component MAJOR MINOR PATCH) + if(NOT "${CHEBURNET_VERSION_${_component}}" MATCHES "^(0|[1-9][0-9]*)$") + message(FATAL_ERROR + "CHEBURNET_VERSION_${_component} должен быть числом без ведущих нулей") + endif() +endforeach() + +set(CHEBURNET_VERSION_CORE + "${CHEBURNET_VERSION_MAJOR}.${CHEBURNET_VERSION_MINOR}.${CHEBURNET_VERSION_PATCH}") + +if(CHEBURNET_VERSION_PRERELEASE STREQUAL "") + set(CHEBURNET_VERSION_SEMANTIC "${CHEBURNET_VERSION_CORE}") + set(CHEBURNET_VERSION_TWEAK 0) + set(CHEBURNET_VERSION_IS_PRERELEASE 0) + set(CHEBURNET_STABLE_CHANNEL 1) + set(CHEBURNET_RELEASE_CHANNEL "stable") +elseif(CHEBURNET_VERSION_PRERELEASE MATCHES "^rc\.([1-9][0-9]*)$") + set(CHEBURNET_VERSION_SEMANTIC + "${CHEBURNET_VERSION_CORE}-${CHEBURNET_VERSION_PRERELEASE}") + set(CHEBURNET_VERSION_TWEAK "${CMAKE_MATCH_1}") + set(CHEBURNET_VERSION_IS_PRERELEASE 1) + set(CHEBURNET_STABLE_CHANNEL 0) + set(CHEBURNET_RELEASE_CHANNEL "prerelease") +else() + message(FATAL_ERROR + "CHEBURNET_VERSION_PRERELEASE должен быть пустым или иметь вид rc.N (N >= 1), " + "получено: '${CHEBURNET_VERSION_PRERELEASE}'") +endif() + +if(CHEBURNET_VERSION_TWEAK GREATER 65534) + message(FATAL_ERROR "номер RC не укладывается в компонент PE-версии") +endif() + +set(CHEBURNET_VERSION_PE "${CHEBURNET_VERSION_CORE}.${CHEBURNET_VERSION_TWEAK}") diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 15df265..851d383 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -32,6 +32,17 @@ BAT отклоняет неподдерживаемый синтаксис и с стабилизации, записывает сведения процесса и откатывает любой частичный сбой. - `UpdateManager` разделяет подписанное обнаружение/скачивание программы и установку пакета движка с версионной активацией. +- `UpdateCheckService` выполняет проверку обновлений вне потока интерфейса, чтобы + недоступные DNS, прокси или сеть не задерживали подключение. Рабочий поток не + отсоединяется, присоединяется до разрушения и не касается интерфейса; модель + доверия совпадает с синхронным путём. +- `RuntimeRecovery` выражает решения восстановления при запуске чистыми функциями + от наблюдаемого состояния: ожидающая среда никогда не повышается до текущей, а + повреждённая активная среда откатывается только при записанной предыдущей + рабочей версии. Решение отделено от исполнения, поэтому вся таблица переходов + проверяется без прав администратора. +- `Diagnostics` собирает локальный архив поддержки с редактированием. Сетевых + вызовов в нём нет; состав описан внутри самого архива. - `UiContext` владеет состоянием консоли и восстанавливает кодовые страницы, режимы, курсор, атрибуты, размеры и стиль окна. @@ -69,3 +80,24 @@ SYSTEM и Administrators имеют полный доступ; обычные Us `pending` после сбоя никогда не становится текущим. Подробности о доверии и ротации ключей: [UPDATE_SECURITY.md](UPDATE_SECURITY.md). + +## Модель версии + +`cmake/Version.cmake` — единственный авторитетный источник версии. Из четырёх +полей выводятся семантическая версия, числовая PE-версия, признак +предварительного выпуска и канал обновлений. Пригодность обновления оценивается +только по семантической версии; PE-версия информационная, потому что формат PE +не умеет выражать prerelease. + +## Проверка выпуска + +- [RELEASE_SIGNING.md](production/RELEASE_SIGNING.md) — политика подписи и + операционные требования к сертификату; +- [E2E_RELEASE_GATE.md](production/E2E_RELEASE_GATE.md) — автоматизированные + сценарии в чистом окружении и ручная часть шлюза; +- [SECURITY_TOOLING.md](production/SECURITY_TOOLING.md) — статический анализ, + фаззинг и внесение отказов; +- [UPSTREAM_AUTOMATION.md](production/UPSTREAM_AUTOMATION.md) — обработка нового + стабильного выпуска upstream; +- [PRIVILEGE_SEPARATION_V2.md](architecture/PRIVILEGE_SEPARATION_V2.md) — + проектная записка о разделении привилегий (только исследование, не входит в 1.0). diff --git a/docs/CURRENT_BEHAVIOR.md b/docs/CURRENT_BEHAVIOR.md index f352297..8f3033d 100644 --- a/docs/CURRENT_BEHAVIOR.md +++ b/docs/CURRENT_BEHAVIOR.md @@ -1,6 +1,6 @@ # Соответствие текущему исходному выпуску -Встроенный исходный проект: `Flowseal/zapret-discord-youtube` 1.10.1, +Встроенный исходный проект: `Flowseal/zapret-discord-youtube` 1.10.2, импортированный из неизменяемого файла релиза. Точные ID релиза, фиксация, URL и SHA-256 архива записаны в `resources/upstream/provenance.json`. diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index f72dfee..54644f6 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -17,3 +17,58 @@ WinHTTP. Заголовок User-Agent содержит версию CHEBURNET. `disabled` («отключены») или выключите проверку при запуске. Локальные журналы, рабочую среду и пользовательские данные удаляет только пользователь; CHEBURNET не удаляет и не отправляет их дистанционно. + +## Экспорт диагностики + +В меню есть пункт `Экспорт диагностики`. Он создаёт локальный архив +`CHEBURNET-diagnostics-ГГГГММДД-ЧЧММСС.zip` в каталоге журналов +(`%ProgramData%\CHEBURNET\logs`). + +Экспорт выполняется только по явному действию пользователя. Перед созданием +архива программа показывает полный состав и точный путь записи и требует +отдельного подтверждения. Отправки нет ни автоматической, ни ручной: в коде +экспорта вообще нет сетевых вызовов, и это закреплено регрессионной проверкой +`PRIVACY diagnostics export never uses a network API`. + +### Что входит в архив + +| Файл | Содержимое | +|---|---| +| `manifest.json` | описание состава самого архива, список редактируемых и никогда не собираемых данных | +| `summary.json` | семантическая версия CHEBURNET, PE-версия и канал; состояние подписи Authenticode лончера (только вердикт); поставщик, версия, происхождение и SHA-256 архива движка; версии рабочей среды — текущая, предыдущая рабочая, ожидающая; результат последней операции и состояние целостности; число доступных стратегий; выбранная стратегия, режим игрового фильтра, режим и канал обновлений; версия сборки Windows и разрядность; ожидаемые SHA-256 файлов, принадлежащих CHEBURNET | +| `application.log` | хвост `cheburnet.log` после редактирования, не более 256 КиБ | + +### Что скрывается перед записью + +- имя пользователя; +- имя компьютера; +- путь профиля пользователя; +- **любой** путь внутри `\Users\`, а не только текущий профиль; +- адреса IPv4 и IPv6; +- строки длиной от 40 символов из алфавита base64/hex, похожие на токены, + подписи и ключи. + +Значения SHA-256 (ровно 64 шестнадцатеричных символа) и SHA-1 (40 символов) +сохраняются намеренно: без хешей файлов, принадлежащих CHEBURNET, диагностика +теряет смысл, а сами хеши не раскрывают ничего о пользователе. + +### Что не собирается никогда + +Переменные окружения, учётные данные прокси, данные браузера и файлы cookie, +дампы трафика, содержимое пользовательских списков из `user\lists`, +идентификаторы машины, сведения о владельце сертификата подписи. + +### Ограничения и безопасность + +- объём ограничен: хвост журнала обрезается, внешние пользовательские файлы не + включаются; +- архив создаётся с флагом `CREATE_NEW`, поэтому существующий файл никогда не + перезаписывается молча; +- при любой ошибке записи частично созданный файл удаляется; +- имена записей внутри архива фиксированы и проверяются: путь с `..` или + разделителем отвергается до записи; +- ZIP пишется без сжатия собственным кодом, сторонняя библиотека ради одной + операции не вводится, формат детерминирован и полностью проверяем. + +Проверки состава и редактирования выполняет тест CTest `diagnostics`, включая +работу с путями, содержащими пробелы и кириллицу. diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index 3e4477c..7b97df8 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -1,5 +1,13 @@ # Ручная проверка публичного кандидата +Часть этого списка автоматизирована: `scripts\e2e\Invoke-CheburnetE2E.ps1` +покрывает первую установку, повторный запуск, прерванное обновление, откат при +повреждённой активной среде, отказ запуска без прав администратора и +fail-closed при подложенном junction. Запускайте сначала его, затем этот список +для того, что требует реальной загрузки драйвера WinDivert и взаимодействия с +интерфейсом. Подготовка виртуальной машины — `scripts\e2e\New-CheburnetTestVm.ps1`; +подробности в [production/E2E_RELEASE_GATE.md](production/E2E_RELEASE_GATE.md). + Выполняйте её на чистых виртуальных машинах Windows 10 x64 и Windows 11 x64 со снимками. Для каждого пункта используйте точный артефакт релиза и его SHA-256, фиксируйте сборку ОС, приложение терминала, результат и путь журнала. Не @@ -26,51 +34,59 @@ 8. Проверьте диагностику, локальный журнал и происхождение/авторство в разделе «О программе». В журнал не должны попадать чувствительное окружение, полная командная строка winws или содержимое пакетов. +9. Выполните «Экспорт диагностики». До записи должен быть показан полный состав + архива и точный путь, а создание — требовать отдельного подтверждения. + Откройте полученный `CHEBURNET-diagnostics-*.zip` и убедитесь, что в + `summary.json` и `application.log` нет имени пользователя, имени компьютера, + путей внутри `\Users\`, адресов IP и похожих на токены строк, а + `manifest.json` описывает состав. Повторный экспорт в тот же файл должен быть + отклонён, а не перезаписать существующий архив. Сетевого обращения при + экспорте быть не должно. ## Конфликты и восстановление -9. Оставьте доверенный winws CHEBURNET работающим, перезапустите программу и +10. Оставьте доверенный winws CHEBURNET работающим, перезапустите программу и проверьте распознавание владельца. Устаревшая запись PID не должна давать исключение. -10. По отдельности запустите посторонний `winws.exe` и службу `zapret`: +11. По отдельности запустите посторонний `winws.exe` и службу `zapret`: CHEBURNET обязан отказаться от дублирования или завершения чужого процесса. -11. Повредите бинарник или список встроенной среды и проверьте точную повторную +12. Повредите бинарник или список встроенной среды и проверьте точную повторную распаковку. Подложите точку соединения или жёсткую ссылку — ожидается безопасный отказ. -12. Перезагрузитесь с действующей записью процесса; проверьте безопасное +13. Перезагрузитесь с действующей записью процесса; проверьте безопасное восстановление. При очистке должны сохраниться текущая, предыдущая рабочая, ожидающая и встроенная версии. ## Матрица обновлений -13. Проверка без сети при запуске и вручную: сообщение о недоступности сети без +14. Проверка без сети при запуске и вручную: сообщение о недоступности сети без блокировки соединения. -14. Проверка текущей версии с сетью: подпись подтверждена, версии корректны. -15. Подмените манифест или подпись; предложите переадресацию HTTP, неверные +15. Проверка текущей версии с сетью: подпись подтверждена, версии корректны. +16. Подмените манифест или подпись; предложите переадресацию HTTP, неверные SHA-256/размер, усечённый пакет, несовместимые сведения, понижение или предварительную версию. Каждый случай должен завершаться «ОБНОВЛЕНИЕ ОТКЛОНЕНО» без запасного пути и изменения состояния. -16. Отмените реальное скачивание пакета: незавершённый файл не активируется. -17. Установите допустимый новый пакет без соединения, подключитесь и проверьте. -18. Установите при соединении: явное подтверждение, защищённый новый каталог, +17. Отмените реальное скачивание пакета: незавершённый файл не активируется. +18. Установите допустимый новый пакет без соединения, подключитесь и проверьте. +19. Установите при соединении: явное подтверждение, защищённый новый каталог, остановка, запуск, стабилизация, проверка, фиксация `active-runtime.json` и сохранность конфигурации и пользовательских списков. -19. Имитируйте ошибку запуска или проверки кандидата: ожидается «ОБНОВЛЕНИЕ +20. Имитируйте ошибку запуска или проверки кандидата: ожидается «ОБНОВЛЕНИЕ ОТКАЧЕНО», предыдущий движок работает, состояние восстановлено. Завершите процесс во время ожидания и перезапустите — кандидат не повышается. -20. Предложите допустимое обновление программы: `CHEBURNET-new.exe` скачивается +21. Предложите допустимое обновление программы: `CHEBURNET-new.exe` скачивается и открывается его каталог, но текущий EXE не перезаписывается и не запускает новый бинарный файл с повышенными правами автоматически. Замените вручную и проверьте версию. ## Платформа и релиз -21. Проверьте Windows Terminal и классический conhost, DPI 100/125/150/200%, +22. Проверьте Windows Terminal и классический conhost, DPI 100/125/150/200%, Defender, SmartScreen и при наличии реальную подпись Authenticode. Не добавляйте исключения антивируса. -22. Проверьте уведомления, лицензии, ссылки на исходники, SHA-256 и наличие +23. Проверьте уведомления, лицензии, ссылки на исходники, SHA-256 и наличие одного пользовательского `CHEBURNET.exe` плюс документации в релизе. -23. Удаление: отключитесь, закройте программу, удалите +24. Удаление: отключитесь, закройте программу, удалите `%ProgramData%\CHEBURNET` и переносимый EXE. Не должно остаться службы, запланированного задания или следов телеметрии. diff --git a/docs/UPDATE_SECURITY.md b/docs/UPDATE_SECURITY.md index 8acdd17..66ebadc 100644 --- a/docs/UPDATE_SECURITY.md +++ b/docs/UPDATE_SECURITY.md @@ -64,6 +64,60 @@ Production endpoint использует `releases/latest/download`. GitHub на проверка обновлений может вернуть состояние «нет сети/источник недоступен» и не мешает работе встроенной known-good среды. +## Версии и канал сборки + +Решения об обновлении принимаются только по семантической версии сборки +(`1.0.0-rc.3` или `1.0.0`), которая выводится из единственного авторитетного +источника `cmake/Version.cmake`. Числовая PE-версия (`1.0.0.3` / `1.0.0.0`) +существует лишь потому, что формат PE не умеет выражать предварительную версию, +и в оценке пригодности обновления не участвует никогда. + +Канал сборки — часть той же модели версии. Стабильная сборка отклоняет любую +предварительную версию-кандидата. Сборка предварительного выпуска может перейти +на более новый RC или на стабильный выпуск, но не на RC более старого номера: +переход `1.0.0-rc.3 → 1.0.0` — обновление, обратный переход — понижение. + +Порог `minimum_supported_version` в манифесте обязан включать уже выпущенные RC +той же базовой версии (`1.0.0-rc.1`), иначе установленный `1.0.0-rc.N` получил бы +отказ «слишком старая программа» при переходе на стабильный `1.0.0`, поскольку +предварительная версия упорядочена ниже стабильной. + +Порядок версий задан одной канонической таблицей `tests/version-order-cases.json`, +против которой независимо проверяются реализация на C++ (`src/update/Version.cpp`) +и реализация релизной оснастки на PowerShell (`scripts/version.ps1`). + +## Фоновая проверка обновлений + +Проверка при запуске выполняется вне потока интерфейса +(`src/update/UpdateCheckService.*`), поэтому недоступный DNS, прокси или сеть не +задерживают подключение и не выглядят как зависание программы. + +Модель доверия при этом не меняется: фоновый поток вызывает тот же +`UpdateManager::CheckNow`, то есть независимое получение манифеста и отдельной +подписи, проверка точных байтов манифеста и оценка версий полностью совпадают с +синхронным путём. Отмена передаётся в WinHTTP и влияет только на транспорт. + +Правила безопасности фонового выполнения: + +- рабочий поток никогда не обращается к интерфейсу и не вызывает владельца + обратно — он только кладёт готовое значение результата в состояние под + мьютексом; +- поток никогда не отсоединяется (`detach`); `Cancel()` идемпотентен, выставляет + отмену и присоединяет поток, а деструктор сервиса вызывает `Cancel()`, поэтому + поток не может пережить объект или увидеть разрушенное состояние; +- мьютекс не удерживается во время сетевого ввода-вывода и во время + присоединения потока; +- результат забирается ровно один раз; если контекст интерфейса сменился, + результат можно просто не забирать — это не приводит ни к утечке, ни к сбою; +- явная проверка обновлений из меню сначала отменяет и присоединяет фоновую, + чтобы два запроса не конкурировали за один кэш проверенного манифеста; +- исключение в фоновой проверке вырождается в нефатальный результат «нет сети», + а не в аварийное завершение программы. + +Поведение ETag сохранено без изменений: кэш проверенного манифеста живёт только в +памяти процесса и обновляется под тем же мьютексом. Непроверенные данные никогда +не сохраняются на диск как доверенное состояние обновления. + ## Ротация ключей и подпись релиза Новый публичный ключ сначала добавляется в проверенный выпуск CHEBURNET, и лишь diff --git a/docs/architecture/PRIVILEGE_SEPARATION_V2.md b/docs/architecture/PRIVILEGE_SEPARATION_V2.md new file mode 100644 index 0000000..69a403e --- /dev/null +++ b/docs/architecture/PRIVILEGE_SEPARATION_V2.md @@ -0,0 +1,131 @@ +# Разделение привилегий: проектная записка для v2 + +Статус: **только исследование.** Ничего из описанного не реализуется в составе +стабильного 1.0 и не должно смешиваться со стабилизацией 1.0. + +## Проблема + +Сегодня `CHEBURNET.exe` — один процесс с `requireAdministrator`. В нём +одновременно находятся: + +- текстовый интерфейс, обработка ввода, отрисовка; +- разбор конфигурации и каталога стратегий; +- сетевой клиент обновлений и разбор подписанного манифеста; +- разбор пакета `.cbpkg` и распаковка в защищённое дерево; +- привилегированные операции файловой системы и ACL; +- создание и остановка процесса `winws.exe`, который загружает драйвер ядра. + +Следствие: любой дефект в разборе ввода или в коде интерфейса исполняется с +правами администратора. Поверхность, работающая с недоверенными данными (сеть, +архив, конфигурация), совпадает с поверхностью, имеющей полные права. + +Текущие компенсации — строгие разборщики fail-closed, подпись манифеста, +проверка личности процесса, жёсткие ACL и транзакционная активация — снижают +вероятность, но не меняют того, что радиус поражения равен «администратор». + +## Целевая структура + +Два процесса с ясной границей доверия. + +| Компонент | Уровень | Ответственность | +|---|---|---| +| `CHEBURNET.exe` | обычная целостность, права пользователя | интерфейс, ввод, отображение состояния, скачивание файла обновления в непривилегированную область, разбор всего недоверенного | +| `cheburnet-broker` | повышенные права (служба либо элевированный помощник) | проверка подписи, привилегированные операции файловой системы, распаковка в защищённое дерево, запуск и остановка `winws.exe`, транзакционная активация | + +Принципиально: **проверка подписи остаётся на привилегированной стороне.** +Непривилегированная часть может скачать файл, но не может убедить брокера, что +файл доверенный. Иначе разделение было бы косметическим. + +## Интерфейс IPC + +Именованный канал с явным дескриптором безопасности: доступ только у +интерактивных пользователей той же сессии; брокер проверяет личность +подключившегося клиента и его образ. + +Набор команд закрытый и узкий. Никакой команды вида «выполнить произвольное +действие» быть не должно: + +```text +GetStatus -> состояние соединения, версии, целостность +Connect -> идентификатор стратегии + режим игрового фильтра +Disconnect -> нет аргументов +ApplyVerifiedPayload -> путь к скачанному файлу + ожидаемые размер и SHA-256 +GetIntegrityState -> отчёт о целостности активной среды +``` + +Правила интерфейса: + +1. аргументы — только перечислимые значения и идентификаторы из каталога, + который брокер знает сам; клиент не передаёт командные строки `winws`; +2. `ApplyVerifiedPayload` не означает «этот файл доверенный»: брокер заново + получает подписанный манифест, проверяет подпись, размер и SHA-256 и только + потом распаковывает; +3. путь к файлу принимается только внутри выделенной области передачи, + принадлежащей брокеру; произвольный путь от клиента отвергается; +4. любая команда идемпотентна либо явно сериализована; параллельные операции + отклоняются, как и сейчас через `OperationState`; +5. ответы — фиксированные структуры, без возврата произвольных путей машины. + +## Что это меняет в модели угроз + +| Угроза | Сейчас | После разделения | +|---|---|---| +| Дефект в разборе манифеста | исполняется с правами администратора | исполняется у пользователя при первичном разборе; привилегированный разбор остаётся, но вход к нему уже прошёл проверку подписи | +| Дефект в разборе `.cbpkg` | права администратора | остаётся привилегированным: распаковка неотделима от записи в защищённое дерево | +| Дефект в коде интерфейса | права администратора | права пользователя | +| Подмена пути обновления | ограничена ACL и проверками | дополнительно ограничена тем, что клиент не выбирает путь | +| Компрометация клиента | полный контроль | доступ только к пяти командам с проверяемыми аргументами | + +Честная оценка: разделение **не** убирает привилегированный разбор пакета. Оно +убирает из-под администратора интерфейс, ввод, сеть и первичный разбор +метаданных — то есть большую часть кода, но не всю недоверенную поверхность. + +## Цена + +- второй исполняемый файл ломает обещание «один `CHEBURNET.exe`»: потребуется + либо установка службы, либо самораспаковка помощника, а это отдельная + поверхность атаки и отдельный вопрос удаления; +- установка службы требует инсталлятора и прав при первом запуске; +- IPC сам становится границей доверия со своим разбором и своими тестами; +- усложняется обновление: обновлять нужно оба компонента согласованно, и + несовпадение версий клиента и брокера должно обрабатываться явно; +- отладка и диагностика усложняются: состояние размазано по двум процессам. + +## Условия, при которых это оправдано + +Разделение стоит делать, если выполнено хотя бы одно: + +1. появляется автоматическое применение обновлений без участия пользователя + (сейчас привилегированное действие всегда подтверждается человеком); +2. появляется любой приём данных из сети, кроме подписанного манифеста и + пакета; +3. интерфейс усложняется настолько, что его код перестаёт быть обозримым; +4. появляется потребность работать без прав администратора в обычном режиме. + +Пока ни одно из условий не выполнено, а текущие защиты (подпись, fail-closed +разбор, ACL, личность процесса, транзакционная активация, обязательная подпись +выпуска) покрывают реалистичные сценарии, приоритет разделения ниже, чем у +проверки в чистом окружении и стабильности обновлений. + +## Рекомендуемый порядок, если работа начнётся + +1. зафиксировать контракт IPC и его тесты **до** переноса кода; +2. первым вынести только `GetStatus` и `GetIntegrityState` — чтение без + побочных эффектов, на нём отлаживается транспорт и проверка личности; +3. затем `Connect`/`Disconnect`; +4. `ApplyVerifiedPayload` — последним, с сохранением текущей транзакционной + активации и её решётки внесения отказов; +5. на каждом шаге сохранять возможность собрать однопроцессную сборку, пока + двухпроцессная не пройдёт тот же релизный шлюз E2E. + +## Что уже сделано в 1.0 и пригодится + +- `OperationState` уже сериализует привилегированные операции; +- `ActivationHooks` уже отделяют решение от исполнения и полностью покрыты + решёткой внесения отказов; +- `RuntimeRecovery` уже отделяет решения восстановления от их исполнения; +- `UpdateCheckService` уже вынес сетевую проверку за пределы потока интерфейса + с явной моделью владения; +- строгие разборщики уже возвращают ошибку, а не бросают исключения. + +Эти границы — естественные места будущего раздела процессов. diff --git a/docs/production/BASELINE_BEFORE_HARDENING.md b/docs/production/BASELINE_BEFORE_HARDENING.md new file mode 100644 index 0000000..3752084 --- /dev/null +++ b/docs/production/BASELINE_BEFORE_HARDENING.md @@ -0,0 +1,159 @@ +# CHEBURNET — базовое состояние перед production-hardening + +Документ фиксирует воспроизводимое состояние репозитория **до** каких-либо +изменений в рамках подготовки стабильного выпуска `v1.0.0`. + +## Git + +| Параметр | Значение | +|---|---| +| Базовая ветка | `main` | +| Базовый commit (HEAD до изменений) | `78cc980a6d0d68f669cf98eff3799c1cf5fa02bd` | +| Тег на базовом commit | `v1.0.0-rc.3` | +| Рабочее дерево | чистое (`git status --porcelain` — пусто) | +| Рабочая ветка изменений | `claude/production-hardening-v1` | +| Существующие теги | `v1.0.0-rc.1`, `v1.0.0-rc.2`, `v1.0.0-rc.3` | + +## Окружение сборки + +| Параметр | Значение | +|---|---| +| ОС | Windows 11 Pro 10.0.26200 | +| Тулчейн | MSVC (Visual Studio 18 BuildTools), x64 | +| Генератор | Ninja | +| Конфигурация | Release | +| Стандарт | C++20, `/W4 /WX /permissive- /utf-8`, `/guard:cf`, статический CRT (`/MT`) | + +## Результат чистой сборки + +``` +scripts\build-release.ps1 -BuildDir build-p0-baseline +``` + +- Результат: **PASS** +- Артефакт: `build-p0-baseline\CHEBURNET.exe`, 3.91 MB +- Предупреждения компилятора: **нет** (сборка идёт с `/WX`, любое предупреждение было бы ошибкой) +- Встроенный тестовый пакет обновления: `UPDATE_PACKAGE: PASS version=1.10.1 files=27 bytes=3399786 sha256=41004f8523af4c176d859590ee8b617834df048e892006dab8cc77cfbd6b7319` + +## Результат полного набора тестов + +``` +scripts\run-tests.ps1 -BuildDir build-p0-baseline +``` + +- CTest: **33/33 PASS**, 0 падений, общее время 7.04 s +- Независимая кросс-проверка стратегий: `STRATEGY_FIDELITY: 84/84 PASS (21 strategies x 4 modes)` +- Итог: `ALL TESTS PASSED` + +Состав CTest (33 теста): + +| Категория | Тесты | +|---|---| +| Аргументы/строки | `args`, `quoting` | +| Конфигурация | `config`, `configmigration` | +| Криптография/целостность | `sha256`, `integrity` | +| Манифест ресурсов | `manifest` | +| Процессы | `process`, `preflight`, `operationstate` | +| Файловая система | `securefs`, `loghandle` | +| UI | `animation`, `progress`, `mascot`, `layout`, `checkpoints`, `menu`, `effects`, `theme`, `framebuffer` | +| JSON | `jsonstrict` | +| Обновления | `updatemanifest`, `updateversion`, `updatesignature`, `updatepackage`, `updaterollback`, `updatestate`, `updatehttp` | +| Скриптовые шлюзы | `package_safety`, `security_regression`, `license_validation`, `release_metadata` | + +## Текущая версия upstream + +`resources/upstream/provenance.json`: + +| Поле | Значение | +|---|---| +| provider | `Flowseal/zapret-discord-youtube` | +| version / tag | `1.10.1` | +| release_id | `367512178` | +| archive_sha256 | `f748d61fec75e4edc992cb5b09d554e914197c68c690384aceb61f143d8f76c9` | +| upstream_commit | `47da17f80ad36a8424cdd25658153fdebd7eb938` | +| immutable | `true` | + +Встроенных стратегий: **21** (`general` + `general (ALT)` … `general (ALT12)` + `general (EXP)` и варианты), проверено в 4 режимах GameFilter. + +Доступная стабильная версия upstream на момент фиксации baseline: **1.10.2** +(`zapret-discord-youtube-1.10.2.zip`, `sha256:5eaac9fb2e4b1abd693487452a3ff3f4dfe9578a45f9ddddfa4bc1f5a6bb62d5`, +`immutable=true`, `prerelease=false`). + +## Текущее поведение семантической версии лончера (дефект) + +Источник версии — единственный: `project(CHEBURNET VERSION 1.0.0)` в `CMakeLists.txt`. + +`cmake/GeneratedVersion.h.in` раскрывается в: + +```c +#define CHEBURNET_VERSION_QUAD 1,0,0,0 +#define CHEBURNET_VERSION_STR "1.0.0" +#define CHEBURNET_VERSION_RC_STR "1.0.0.0" // значение PE FileVersion/ProductVersion +#define CHEBURNET_VERSION_WSTR L"1.0.0" +``` + +Следствия на baseline: + +1. Бинарник, собранный для тега `v1.0.0-rc.3`, во время выполнения + идентифицирует себя как `1.0.0` (`CHEBURNET_VERSION_STR`). +2. `UpdateManager::CheckNow` сравнивает именно `CHEBURNET_VERSION_STR` + с версией из подписанного манифеста + (`src/update/UpdateManager.cpp:138`, `src/update/UpdateManager.cpp:150`). +3. `scripts/prepare-release.ps1` передаёт в генератор манифеста + `-LauncherVersion $sourceVersion`, то есть `1.0.0`, **и для RC-тегов тоже**. +4. Поэтому будущий стабильный `1.0.0` оценивается установленным + `v1.0.0-rc.3` как `Eligibility::Current`, а не `Upgrade`. + +При этом сам компаратор версий корректно упорядочивает prerelease: +`CompareVersions("1.0.0-rc.3", "1.0.0") < 0`. Дефект — исключительно в том, +что prerelease-метка вообще не попадает в версию времени выполнения и в +метаданные выпуска. Это и есть предмет Phase 1; компаратор ослаблять не требуется. + +Дополнительно зафиксированы связанные ограничения baseline: + +- `scripts/generate-update-manifest.ps1` требует строгого равенства + `FileVersion == ".0"`, то есть семантическая версия и + числовая PE-версия жёстко склеены и не могут различаться. +- `scripts/prepare-release.ps1` допускает RC-тег при стабильной исходной + версии, но не умеет выразить RC в метаданных выпуска. + +## Текущий release workflow (ключевые наблюдения) + +`.github/workflows/release.yml`: + +- шаг подписи называется «Необязательная подпись и проверка Authenticode» и + вызывает `scripts/authenticode-sign.ps1`, который при отсутствии + `AUTHENTICODE_PFX_B64` печатает предупреждение и завершается с кодом `0`; +- то есть стабильный тег `vX.Y.Z` на baseline **может быть опубликован без подписи**; +- порядок «подпись → упаковка → хеш» сам по себе корректен: подпись ставится + на `build-release\CHEBURNET.exe`, а `package.ps1` копирует этот файл в `dist` + и только затем считает SHA-256 — то есть хеш уже считается с подписанного файла; +- `permissions: contents: write` — минимальные права для публикации релиза; +- сторонние actions закреплены по неизменяемым commit SHA. + +`.github/workflows/upstream-check.yml` выполняет проверку новых выпусков +upstream, но не создаёт устойчивую задачу (PR/issue) — предмет Phase 4. + +## Текущие метаданные RC-выпуска + +Для тега `v1.0.0-rc.3` публикуются: + +- `CHEBURNET.exe` + `CHEBURNET.exe.sha256` +- `cheburnet-payload-1.10.1.cbpkg` + `.sha256` +- `update-manifest.json` + `update-manifest.json.sig` +- `THIRD_PARTY_NOTICES.md`, `DEPENDENCIES.md`, `LICENSES/*` + +`prerelease` в GitHub Release выставляется по наличию `-rc.` в имени тега. +Внутри `update-manifest.json` канал всегда `stable`, а `launcher.version` на +baseline равен `1.0.0` даже для RC — см. дефект выше. + +## Известные предупреждения / шум + +- Предупреждений компилятора нет. +- В корне рабочей копии присутствует множество локальных каталогов сборки + (`build*`, `dist`, `artifacts`) — все они в `.gitignore` и на состояние + репозитория не влияют. + +## Изменения поведения в этой фазе + +Отсутствуют. Phase 0 — только фиксация состояния. diff --git a/docs/production/E2E_RELEASE_GATE.md b/docs/production/E2E_RELEASE_GATE.md new file mode 100644 index 0000000..3b59c79 --- /dev/null +++ b/docs/production/E2E_RELEASE_GATE.md @@ -0,0 +1,146 @@ +# Релизный шлюз: проверка в чистом окружении Windows + +Документ описывает обязательную проверку стабильного выпуска в чистом клиентском +окружении Windows 10 x64 и Windows 11 x64, её автоматизацию и то, что остаётся +ручным и почему. + +## Почему это отдельный шлюз + +Модульные тесты проверяют логику. Они не проверяют, что на чистой машине +создаётся защищённое дерево с нужными ACL, что распаковывается рабочая среда, +что прерванное обновление не повышается автоматически, что повреждённая среда +откатывается и что драйвер WinDivert вообще загружается. Для стабильного выпуска +этого недостаточно. + +GitHub-hosted runners предоставляют образы Windows Server, а не клиентские +Windows 10/11, поэтому шлюз выполняется на воспроизводимой виртуальной машине. + +## Автоматизированная часть + +`scripts/e2e/Invoke-CheburnetE2E.ps1` + +Ключевая идея: вся работа запуска — bootstrap защищённого дерева, распаковка +рабочей среды, проверка целостности, восстановление после прерванного +обновления и откат при запуске — выполняется в конструкторе `App` **до** +появления интерфейса. Поэтому сценарии управляются запуском настоящего +исполняемого файла и проверкой наблюдаемого состояния +(`%ProgramData%\CHEBURNET`, `active-runtime.json`, `config.json`, коды выхода), +а не имитацией нажатий в текстовом интерфейсе. + +Оснастка **никогда** не запускает пакетный движок и не загружает драйвер +WinDivert. + +| Сценарий | Требует | Критерий | Что проверяется | +|---|---|---|---| +| `refuses-unelevated` | — | AC-08 | Запуск без прав администратора отклоняется Windows (`requireAdministrator` в манифесте), защищённое дерево не изменяется | +| `fresh-install` | elevation | AC-08 | Пустой корень: распаковка, создание конфигурации, перечисление стратегий, запись состояния среды | +| `existing-install` | elevation | AC-08 | Повторный запуск не меняет существующую конфигурацию и текущую версию среды | +| `interrupted-pending` | elevation | AC-04, AC-08 | Состояние `pending`, оставленное прерванным обновлением, **никогда** не повышается до текущего автоматически | +| `startup-rollback` | elevation | AC-04, AC-08 | Повреждённая активная среда либо откатывается к предыдущей рабочей, либо запуск отклоняется | +| `filesystem-guards` | elevation | AC-07 | Junction, подложенный в защищённый каталог журналов, приводит к fail-closed: ни один файл не записывается через него | + +### Защита окружения + +Корень рабочей среды CHEBURNET — фиксированное защищённое расположение, поэтому +привилегированные сценарии изменяют машину, на которой выполняются. Оснастка +отказывается работать, если машина явно не помечена как одноразовая: + +```powershell +New-Item -ItemType File -Path C:\cheburnet-e2e-vm.marker +``` + +Дополнительно она отказывается работать без прав администратора и при наличии +уже запущенного `winws.exe` — чужой процесс не трогается никогда. + +### Самопроверка оснастки + +```powershell +scripts\e2e\Invoke-CheburnetE2E.ps1 -SelfTest +``` + +Не требует прав, ничего не пишет вне временного каталога и входит в набор CTest +как тест `e2e_harness`. Проверяет состав сценариев, привязку каждого сценария к +критерию приёмки, отказ защиты окружения на непомеченной машине и схему записей +доказательств. + +## Воспроизводимая виртуальная машина + +```powershell +scripts\e2e\New-CheburnetTestVm.ps1 -Edition win11 ` + -IsoPath D:\iso\Win11_x64.iso -ReleaseDir C:\Projects\cheburnet\dist +``` + +Автоматизировано: VM Generation 2 с Secure Boot и виртуальным TPM по +зафиксированной спецификации, подготовительный VHDX с артефактами выпуска, +скриптами и маркером одноразовой машины, а также скрипт `Run-InGuest.ps1`, +который внутри гостя сверяет SHA-256 лончера с опубликованным и запускает набор. + +Остаётся ручным: выбор редакции и индекса образа внутри ISO и сама установка +Windows. Универсальный `unattend.xml` с редакцией и ключом продукта нельзя +зафиксировать в репозитории, а угадывание молча дало бы не то окружение. + +Порядок: + +1. `Start-VM` и установка Windows с ISO (редакция выбирается осознанно: нужен + клиентский Windows 10 x64 или Windows 11 x64, не Windows Server); +2. `Checkpoint-VM -SnapshotName 'clean-after-setup'`; +3. в госте из элевированного PowerShell на томе `CHEBURNET-E2E`: + `.\scripts\e2e\Run-InGuest.ps1`; +4. вынести `C:\cheburnet-e2e-results\*\e2e-report.json` и приложить к + `docs/production/FINAL_RELEASE_CERTIFICATION.md`; +5. перед повторным запуском — `Restore-VMSnapshot -Name 'clean-after-setup'`. + +## Ручная часть шлюза + +Эти сценарии требуют реальной загрузки драйвера WinDivert и взаимодействия с +интерфейсом, поэтому выполняются человеком в госте после автоматизированного +набора. Каждый пункт фиксируется в сертификации выпуска. + +| Проверка | Ожидаемый результат | +|---|---| +| Подключение (`Подключиться`) | `winws.exe` запущен, запись процесса создана, состояние «ПОДКЛЮЧЕНО (pid …)» | +| Идентичность процесса | PID, время создания и путь образа совпадают с записью; путь внутри корня рабочей среды | +| Отключение | процесс завершён подтверждённо, запись удалена | +| Повторный запуск после подключения | существующее соединение распознано как своё | +| Выход с сохранением соединения | `winws.exe` продолжает работать по политике объекта заданий | +| Обновление нагрузки | проверенный манифест → скачивание → активация → фиксация состояния | +| Откат обновления | сбой кандидата восстанавливает предыдущую рабочую версию | +| Посторонний `winws.exe` | конфликт сообщается, чужой процесс **не** завершается | + +См. также `docs/SMOKE_TEST.md`. + +## Состав доказательства + +`e2e-report.json`: + +```json +{ + "schema": 1, + "generated_at": "…Z", + "launcher": "…\CHEBURNET.exe", + "launcher_version": "1.0.0.0", + "machine": { "os": "…", "build": "…", "architecture": "AMD64" }, + "environment_refusals": [], + "results": [ { "scenario": "…", "outcome": "passed|failed|skipped", + "detail": "…", "evidence": [ "…" ], "at": "…Z" } ], + "passed": 0, "failed": 0, "skipped": 0 +} +``` + +Стабильный выпуск не может считаться готовым, если по какому-либо обязательному +сценарию нет записи `passed` на обеих целевых ОС. + +## Текущее состояние + +| Окружение | Состояние | +|---|---| +| Самопроверка оснастки | **PASS** (тест CTest `e2e_harness`) | +| `refuses-unelevated` на машине разработки (Windows 11 Pro 26200) | **PASS** — запуск без прав отклонён Windows, защищённое дерево не изменено | +| Привилегированные сценарии на машине разработки | **SKIPPED** — машина не помечена как одноразовая и сессия не элевирована; это намеренный отказ защиты, а не сбой | +| Чистая VM Windows 11 x64 | **НЕ ВЫПОЛНЕНО** | +| Чистая VM Windows 10 x64 | **НЕ ВЫПОЛНЕНО** | +| Ручная часть (подключение, отключение, обновление, откат) | **НЕ ВЫПОЛНЕНО** | + +Это явный блокер стабильного выпуска: запускать привилегированные сценарии на +рабочей машине владельца репозитория недопустимо, а виртуальная машина с +установленной Windows в данном окружении отсутствует. diff --git a/docs/production/FINAL_RELEASE_CERTIFICATION.md b/docs/production/FINAL_RELEASE_CERTIFICATION.md new file mode 100644 index 0000000..35acb56 --- /dev/null +++ b/docs/production/FINAL_RELEASE_CERTIFICATION.md @@ -0,0 +1,170 @@ +# CHEBURNET v1.0.0 Production Certification + +Status: **RELEASE_CANDIDATE_READY_WITH_BLOCKERS** + +Branch: `claude/production-hardening-v1` +SHA: `4b027f7e813abd1476f2784442c04c94da52625b` (база: `78cc980a6d0d68f669cf98eff3799c1cf5fa02bd`, тег `v1.0.0-rc.3`) +Date: 2026-09-11 + +Этот документ фиксирует состояние **кандидата** `1.0.0-rc.3` с выполненной +подготовкой к стабильному выпуску. Стабильный тег `v1.0.0` ещё не создан: +`cmake/Version.cmake` по-прежнему объявляет `rc.3`, и это верно — переключение +на стабильную версию выполняется одной правкой после закрытия блокеров ниже. + +## Launcher + +| Поле | Значение | +|---|---| +| Semantic version | `1.0.0-rc.3` | +| Release channel | `prerelease` | +| PE FileVersion / ProductVersion | `1.0.0.3` | +| Authenticode | **не подписан** (сертификат в этом окружении отсутствует; для RC это разрешено политикой) | +| SHA-256 | `d319010e2f32b91095325bc99cd5e742785ae2a0293a07a5e935739bff664cdf` | +| Размер | 4 159 488 байт | + +## Payload + +| Поле | Значение | +|---|---| +| Provider | `Flowseal/zapret-discord-youtube` | +| Version | `1.10.2` | +| Archive SHA-256 | `5eaac9fb2e4b1abd693487452a3ff3f4dfe9578a45f9ddddfa4bc1f5a6bb62d5` | +| Upstream commit | `dfd8e613b099676cf2aa7b474ee5923801514dec` | +| Release ID | `375784527` | +| Package SHA-256 | `970c8550c3e0498f4a60d5e8900b7e590c7592757975accb5335b126ef463333` | +| Package size | 3 403 061 байт, 27 файлов | +| Стратегий | 22 (было 21; добавлена `ALT13`) | + +## Verification + +Все команды выполнены на `4b027f7`, Windows 11 Pro 26200, MSVC (VS 18 BuildTools), Ninja, Release. + +| Проверка | Команда | Результат | +|---|---|---| +| Clean build | `scripts\build-release.ps1 -BuildDir build-final` | **PASS** — `CHEBURNET.exe` 3.97 MB, предупреждений нет (`/W4 /WX`) | +| CTest | `scripts\run-tests.ps1 -BuildDir build-final` | **PASS — 43/43** | +| Strategy fidelity | (в составе `run-tests.ps1`) | **PASS — 88/88** (22 стратегии × 4 режима GameFilter) | +| Security regression | тест `security_regression` | **PASS** | +| Package safety | тест `package_safety` | **PASS** | +| Release metadata | тест `release_metadata` | **PASS** | +| Release signing | тест `release_signing` | **PASS** | +| Version model | тест `version_model` | **PASS** | +| Upstream automation | тест `upstream_automation` | **PASS** | +| Importer validation tree | тест `importer_validation_tree` | **PASS** | +| E2E harness self-test | тест `e2e_harness` | **PASS** | +| Fuzz / properties | тест `fuzzparsers` | **PASS** (+ кампании 500 000 и 3 × 50 000 итераций на разных зёрнах) | +| Fault injection | тест `faultinjection` | **PASS** | +| Diagnostics privacy | тест `diagnostics` | **PASS** | +| Startup recovery | тест `runtimerecovery` | **PASS** | +| License validation | `scripts\validate-licenses.ps1` | `LICENSE_VALIDATION: PASS` | +| Upstream fidelity | `scripts\verify-upstream.ps1` | `UPSTREAM_FIDELITY: PASS version=1.10.2 files=46 strategies=22` | +| Upstream currency | `scripts\sync-upstream.ps1 -Version 1.10.2 -CheckOnly` | `UPSTREAM_IMPORT: already current (1.10.2)` | +| Static analysis | `cmake -DCHEBURNET_ENABLE_ANALYZE=ON` + сборка | **PASS** — находок в коде CHEBURNET нет, сборка идёт с `/WX` | +| Packaging | `scripts\package.ps1 -BuildDir build-final -OutDir dist-final -SkipBuild` | **PASS** | +| Release metadata generation | `scripts\prepare-release.ps1 -Tag v1.0.0-rc.3 …` | `RELEASE_PREPARATION: PASS tag=v1.0.0-rc.3 launcher=1.0.0-rc.3 pe=1.0.0.3 channel=prerelease payload=1.10.2` | +| Windows 11 clean VM | `scripts\e2e\Invoke-CheburnetE2E.ps1` | **НЕ ВЫПОЛНЕНО** — блокер B1 | +| Windows 10 clean VM | `scripts\e2e\Invoke-CheburnetE2E.ps1` | **НЕ ВЫПОЛНЕНО** — блокер B1 | +| Connect / disconnect / relaunch | ручная часть шлюза | **НЕ ВЫПОЛНЕНО** — блокер B1 | +| Update / rollback на живой системе | ручная часть шлюза | **НЕ ВЫПОЛНЕНО** — блокер B1 | +| Stable Authenticode signature | workflow публикации | **НЕ ВЫПОЛНЕНО** — блокер B2 | + +Проверенный выборочно артефакт подписанного манифеста (RC-канал): + +```json +{"schema":1,"channel":"stable","key_id":"cheburnet-release-2026", + "launcher":{"version":"1.0.0-rc.3","sha256":"d319010e…4cdf","size":4159488, + "minimum_supported_version":"1.0.0-rc.1"}, + "payload":{"provider":"Flowseal/zapret-discord-youtube","version":"1.10.2", + "sha256":"970c8550…3333","size":3403061, + "minimum_launcher_version":"1.0.0-rc.1", + "payload_schema":1,"strategy_schema":1}} +``` + +Это и есть главное доказательство исправления дефекта версий: до работ +`launcher.version` был бы `1.0.0` и для RC-сборки тоже. + +## Remaining known issues + +### B1 — проверка в чистом окружении Windows не выполнена + +- **Серьёзность:** блокирующая для стабильного выпуска (AC-08). +- **Причина:** привилегированные сценарии изменяют фиксированное защищённое + расположение `%ProgramData%\CHEBURNET` и требуют загрузки драйвера ядра. + Выполнять их на рабочей машине владельца репозитория недопустимо; сессия не + элевирована, машина не помечена как одноразовая, виртуальной машины с + установленной Windows в окружении нет. +- **Что уже сделано:** оснастка готова и самопроверена, сценарий отказа запуска + без прав администратора выполнен и пройден, подготовка VM автоматизирована. +- **Точное следующее действие:** + 1. `scripts\e2e\New-CheburnetTestVm.ps1 -Edition win11 -IsoPath -ReleaseDir dist`; + 2. установить Windows, сделать снимок `clean-after-setup`; + 3. в госте `.\scripts\e2e\Run-InGuest.ps1`; + 4. повторить для `-Edition win10`; + 5. выполнить ручную часть шлюза из `docs/production/E2E_RELEASE_GATE.md`; + 6. приложить оба `e2e-report.json` к этому документу. + +### B2 — подпись Authenticode стабильного выпуска не проверена на практике + +- **Серьёзность:** блокирующая для стабильного выпуска (AC-03). +- **Причина:** сертификат подписи кода отсутствует в этом окружении. Политика, + шлюзы и их отказы проверены полностью (включая реальные подписи и файл, + изменённый после подписи), но подпись **именно сертификатом CHEBURNET** ни + разу не выполнялась. +- **Точное следующее действие:** задать секреты `AUTHENTICODE_PFX_B64` и + `AUTHENTICODE_PFX_PASSWORD`, опубликовать RC-тег и убедиться, что шаг печатает + `AUTHENTICODE_SIGN: PASS status=Valid timestamped=True`. Только после этого + создавать стабильный тег. + +### B3 — переключение версии на стабильную не выполнено + +- **Серьёзность:** обязательное действие перед выпуском, не дефект. +- **Причина:** источник версии намеренно оставлен на `rc.3`, чтобы ветка + оставалась кандидатом. +- **Точное следующее действие:** в `cmake/Version.cmake` заменить + `set(CHEBURNET_VERSION_PRERELEASE "rc.3")` на `set(CHEBURNET_VERSION_PRERELEASE "")`, + пересобрать, убедиться что `version_model` и `release_metadata` проходят, и + создать тег `v1.0.0`. Шлюз тега не даст опубликовать стабильный тег при + RC-источнике и наоборот. + +### B4 — тесты CodeQL и процесса static-analysis не выполнялись на GitHub + +- **Серьёзность:** средняя, не блокирующая. +- **Причина:** процессы добавлены в этой ветке и запускаются на GitHub, здесь + выполнить их нельзя. Локальный эквивалент (MSVC `/analyze` с `/WX`) выполнен и + чист. +- **Точное следующее действие:** после push ветки убедиться, что задания + `msvc-analyze` и `codeql` зелёные, и разобрать находки CodeQL, если они будут. + +### B5 — автоматизация upstream не проверена сквозным запуском + +- **Серьёзность:** низкая, не блокирующая. +- **Причина:** процесс срабатывает только при появлении выпуска новее 1.10.2; + сейчас встроенная версия актуальна, поэтому ветка `validate`/`task` не + исполнялась. Разбор дельты, контракт отчёта и политика процесса покрыты + тестом без сети. +- **Точное следующее действие:** запустить процесс вручную + (`workflow_dispatch`) после появления следующего выпуска Flowseal и проверить, + что создаётся ровно одна задача и повторный запуск её обновляет, а не дублирует. + +## Release decision + +Все три P0-задачи закрыты и проверены: семантика версий RC/stable исправлена в +единственном источнике версии и подтверждена реальными метаданными выпуска; +нагрузка обновлена до Flowseal 1.10.2 штатным защищённым импортёром с +побайтовой сверкой; подпись Authenticode для стабильного тега сделана +обязательной, а опубликованные байты привязаны к подписанным по SHA-256 в трёх +местах. + +Сверх этого закрыты пять из шести задач P1 и найдены три дефекта, которых в +задании не было: полный импорт upstream не мог завершиться ни для какой версии, +порог совместимости отсекал бы установленные RC при переходе на стабильный +выпуск, а ревизия upstream вида `1.10.2a` считалась понижением. + +Стабильным выпуск назвать нельзя: два обязательных шлюза — проверка в чистом +окружении Windows 10 и Windows 11 и фактическая подпись сертификатом CHEBURNET — +не выполнены, и выполнить их в текущем окружении невозможно без изменения +рабочей машины владельца и без сертификата. Заявлять готовность при +непроверенных обязательных шлюзах было бы неверно. + +Поэтому статус — `RELEASE_CANDIDATE_READY_WITH_BLOCKERS`: код и оснастка готовы, +осталось выполнить B1, B2 и B3 в указанном порядке. diff --git a/docs/production/RELEASE_SIGNING.md b/docs/production/RELEASE_SIGNING.md new file mode 100644 index 0000000..d9d59e9 --- /dev/null +++ b/docs/production/RELEASE_SIGNING.md @@ -0,0 +1,169 @@ +# Подпись выпусков CHEBURNET (Authenticode) + +Документ описывает политику подписи, операционные требования к сертификату и +проверки, которые останавливают публикацию. Секреты здесь не хранятся и не +воспроизводятся. + +## Политика по каналам + +| Канал | Тег | Подпись | +|---|---|---| +| Стабильный | `vX.Y.Z` | **Обязательна.** Отсутствие сертификата, неудачная подпись, отсутствие метки времени или непрошедшая проверка останавливают выпуск. | +| Предварительный | `vX.Y.Z-rc.N` | Необязательна. Если сертификат настроен — артефакт подписывается и проверяется; если нет — публикуется без подписи с предупреждением. | + +Исключение для предварительных версий сделано осознанно: RC распространяются +ограниченно и служат для проверки сборки, а не для массовой установки. Это +исключение **никогда** не распространяется на стабильный тег: требование +выводится из авторитетной модели версии (`cmake/Version.cmake`), а не из +отдельного флага, который можно забыть выставить. + +## Порядок операций в выпуске + +Порядок критичен: подпись изменяет файл, поэтому любой хеш, посчитанный до +подписи, описывал бы не тот файл, который получит пользователь. + +``` +сборка + -> подпись Authenticode (scripts\authenticode-sign.ps1) + -> упаковка релиза (scripts\package.ps1) + -> SHA-256 опубликованного файла (считается с подписанных байтов) + -> манифест обновления (scripts\generate-update-manifest.ps1) + -> подпись манифеста ECDSA (scripts\sign-update-manifest.ps1) + -> независимая проверка подписи манифеста + -> независимая проверка Authenticode (scripts\verify-authenticode.ps1) + -> публикация GitHub Release +``` + +Этот порядок закреплён регрессией `RELEASE hashes are generated only after +signing`, которая проверяет последовательность шагов прямо в +`.github/workflows/release.yml`. + +## Что именно проверяется + +`scripts/authenticode.ps1` содержит политику как чистую функцию +`Test-CheburnetAuthenticodePolicy`. Артефакт принимается, только если +одновременно выполнено всё: + +| Условие | Что отсекается | +|---|---| +| `Status` строго равен `Valid` | `NotSigned`, `HashMismatch` (файл изменён после подписи), `UnknownError`/`NotTrusted` (цепочка не доверена), любой иной статус | +| Присутствует сертификат подписанта | подпись без подписанта | +| Текущее время внутри окна действия сертификата — **только для подписи без метки времени** | просроченный или ещё не действующий сертификат без контрподписи | +| Присутствует контрподпись метки времени (когда метка настроена) | подпись, которая перестанет проверяться после истечения сертификата | +| SHA-256 совпадает с ожидаемым (когда ожидание задано) | файл, пересобранный или подменённый после подписи | + +Промежуточных состояний нет: «предупредить и продолжить» в политике +отсутствует по построению. + +### Почему окно действия проверяется только без метки времени + +Контрподпись доказывает, что файл был подписан, пока сертификат действовал, и +Windows проверяет цепочку именно на момент метки, сообщая об этом статусом +`Valid`. Именно для этого метка и нужна: правильно подписанный выпуск остаётся +проверяемым и после истечения сертификата. Повторная сверка окна действия с +текущим временем отвергала бы собственные выпуски CHEBURNET в день истечения +сертификата подписи. + +Этот дефект был в первой реализации политики и обнаружен в CI: системный +бинарный файл Windows на раннере подписан сертификатом, истёкшим в 2024 году, +но его подпись по-прежнему `Valid` благодаря метке времени. Случай закрыт +двумя регрессиями в `release_signing`: просроченный сертификат с меткой принимается, +без метки — отвергается. + +Проверка выполняется дважды и разными средствами: `signtool verify /pa /all` +сразу после подписи и независимо через API подписи Windows +(`Get-AuthenticodeSignature`) — в `scripts/verify-authenticode.ps1` перед +публикацией. + +## Привязка опубликованных байтов + +`scripts/prepare-release.ps1`: + +1. вычисляет SHA-256 подписанного файла в каталоге сборки **до** упаковки; +2. после упаковки требует, чтобы файл в `dist` имел ровно тот же SHA-256 и + по-прежнему проходил политику (`-ExpectedSha256`); +3. непосредственно перед публикацией повторно сверяет три величины: + SHA-256 публикуемого файла, `launcher.sha256` в подписанном манифесте + обновления и содержимое `CHEBURNET.exe.sha256`. + +Расхождение любой пары останавливает выпуск. + +## Операционные требования к сертификату + +Секреты передаются только через GitHub Actions secrets и никогда не попадают +в репозиторий. + +| Секрет | Назначение | +|---|---| +| `AUTHENTICODE_PFX_B64` | PFX с закрытым ключом подписи кода, закодированный Base64 | +| `AUTHENTICODE_PFX_PASSWORD` | Пароль к PFX | + +Требования: + +- сертификат должен быть **Code Signing** (EKU `1.3.6.1.5.5.7.3.3`) от + публичного центра сертификации; самоподписанный сертификат политику не + проходит, потому что цепочка не доверена; +- срок действия должен покрывать дату выпуска; рекомендуется обновлять + сертификат не позднее чем за 30 дней до истечения; +- служба меток времени по умолчанию — `http://timestamp.digicert.com`; + метка обязательна, она сохраняет проверяемость подписи после истечения + сертификата; +- алгоритм файла и метки — SHA-256; +- секреты не должны быть доступны сборкам из forked pull request: подпись + выполняется только в workflow публикации по тегу, у которого + `permissions: contents: write` и никаких прав на содержимое PR. + +### Ротация + +1. Получить новый сертификат и экспортировать PFX. +2. Обновить оба секрета репозитория. +3. Опубликовать RC-тег и убедиться, что шаг подписи печатает + `AUTHENTICODE_SIGN: PASS status=Valid timestamped=True`. +4. Только затем публиковать стабильный тег. + +Старый сертификат отзывать не требуется, если он не скомпрометирован: ранее +выпущенные файлы остаются проверяемыми благодаря метке времени. При +компрометации ключа сертификат необходимо отозвать и перевыпустить все +затронутые стабильные артефакты. + +## Что делать, если стабильный выпуск остановлен + +| Сообщение | Причина | Действие | +|---|---|---| +| `AUTHENTICODE_SIGNING_REQUIRED: no signing certificate is configured` | секреты не заданы | задать `AUTHENTICODE_PFX_B64` и `AUTHENTICODE_PFX_PASSWORD` | +| `signtool failed to sign the file` | неверный пароль, повреждённый PFX, истёкший сертификат | проверить PFX локально, при необходимости перевыпустить | +| `Authenticode policy rejected ...: Authenticode status is 'UnknownError'` | цепочка не доверена (самоподписанный или неполная цепочка) | использовать сертификат публичного ЦС, включить промежуточные сертификаты в PFX | +| `... signature is not timestamped` | служба меток недоступна | повторить запуск; при устойчивом сбое временно указать другую службу через `-TimestampUrl` | +| `signed artifact mismatch ...` | файл пересобран или подменён после подписи | не обходить проверку: перезапустить выпуск целиком с чистой сборки | + +Обход шлюза не предусмотрен. Опция «опубликовать без подписи» для стабильного +канала отсутствует намеренно. + +## Автоматические проверки + +| Проверка | Где | +|---|---| +| Политика принимает корректную подпись с меткой времени | тест CTest `release_signing` | +| Отклоняются `NotSigned`, `HashMismatch`, `UnknownError`, `NotTrusted`, `NotSupportedFileFormat` | `release_signing` | +| Принимается просроченный сертификат с контрподписью метки времени | `release_signing` | +| Отклоняется просроченный сертификат без метки времени | `release_signing` | +| Отклоняется ещё не действующий сертификат без метки времени | `release_signing` | +| Отклоняется подпись без метки времени, когда метка требуется | `release_signing` | +| Отклоняется недоверенный самоподписанный сертификат (реальная подпись) | `release_signing` | +| Обнаруживается файл, изменённый после подписи (реальная подпись) | `release_signing` | +| Стабильный тег без сертификата не публикуется | `release_signing` | +| Стабильный тег с неподписанным файлом отвергается до сборки артефактов | `release_signing` | +| Предварительный тег без сертификата по-прежнему разрешён | `release_signing` | +| Подпись выполняется раньше генерации метаданных | `release_signing`, `security_regression` | +| Опубликованный файл привязан к подписанным байтам | `security_regression` | + +### Ограничение локальной проверки + +Положительный сценарий «действительная доверенная подпись» проверяется на +реальном системном бинарном файле Windows, подписанном Microsoft: он даёт +`status=Valid timestamped=True` и проходит политику. Создать локально +собственную доверенную подпись невозможно без изменения хранилищ доверия +машины, а релизная оснастка не имеет права менять системные настройки +безопасности. Поэтому подпись **именно сертификатом CHEBURNET** впервые +проверяется в workflow публикации; первый стабильный тег следует +предварять RC-тегом с тем же сертификатом (шаг 3 процедуры ротации). diff --git a/docs/production/SECURITY_TOOLING.md b/docs/production/SECURITY_TOOLING.md new file mode 100644 index 0000000..a674819 --- /dev/null +++ b/docs/production/SECURITY_TOOLING.md @@ -0,0 +1,128 @@ +# Инструменты безопасности: статический анализ, фаззинг, внесение отказов + +## Статический анализ + +### MSVC `/analyze` + +Включается отдельной опцией, по умолчанию выключен (анализ заметно замедляет +сборку и не должен мешать обычной разработке): + +```powershell +. scripts\vsenv.ps1; Import-VcVars +cmake -S . -B build-analyze -G Ninja -DCMAKE_MAKE_PROGRAM="$NinjaExe" ` + -DCMAKE_BUILD_TYPE=Release -DCHEBURNET_ENABLE_ANALYZE=ON +cmake --build build-analyze +``` + +Анализ работает **с `/WX`**, поэтому любая новая находка в коде CHEBURNET +останавливает сборку. В CI это задание `msvc-analyze` +(`.github/workflows/static-analysis.yml`). + +#### Разобранные находки + +Все находки первого прогона разобраны по существу, а не подавлены. + +| Находка | Место | Разбор | Действие | +|---|---|---|---| +| `C6387` «`attrList` может быть равен 0» | `src/core/ProcessManager.cpp` | Реальный пробел. Размер списка атрибутов брался из вызова `InitializeProcThreadAttributeList(nullptr, …)` без проверки: при неожиданном отказе размер остался бы нулевым, выделение — нулевой длины, а последующие вызовы писали бы через указатель без хранилища. Это привилегированный путь создания дочернего процесса. | Добавлена проверка диапазона размера и указателя; при отказе путь fail-closed, элевированный дочерний процесс не создаётся. | +| `C6262` «функция использует 66040 байт стека» | `src/update/WinHttpClient.cpp` | Реальный риск. Буфер передачи 64 КиБ лежал в автоматической памяти одного кадра; эта же функция теперь исполняется в фоновом потоке проверки обновлений, где запас стека не должен иметь значения. | Буфер перенесён в динамическую память. | +| `C6326` «сравнение константы с константой» ×3 | `src/core/Launcher.cpp`, `src/app/UiScreens.cpp` | Проверка «встроенный resource manifest не пуст» выполнялась во время исполнения, хотя это свойство генерации. | Проверка вынесена в `static_assert`: сборка падает сразу, а не у пользователя при запуске. Экран состояния показывает фактическое число записей. | +| `C28285` «синтаксическая ошибка `_Param_(1)`» | `wincrypt.h` Windows SDK 10.0.26100.0 | Дефект SAL-аннотации `CryptStringToBinaryA` **в чужом заголовке**. На нашей стороне не исправим, о свойствах кода CHEBURNET не говорит. | Подавлено точечно (`/wd28285`) с обоснованием в `CMakeLists.txt`. Единственное подавленное предупреждение анализа. | + +### CodeQL + +Задание `codeql` в том же процессе: `languages: c-cpp`, набор запросов +`security-and-quality`, права `security-events: write` только у этого задания. +Сборка для анализа — обычный `scripts\build-release.ps1`, поэтому CodeQL видит +ровно тот код, который попадает в выпуск. + +## Фаззинг и свойства + +libFuzzer с MSVC в существующем тулчейне не используется: он потребовал бы +отдельной сборки под clang-cl и параллельного тулчейна, что дестабилизирует +основной конвейер. Вместо этого в наборе тестов есть детерминированная +мутационная проверка свойств — тест CTest `fuzzparsers`. + +Устройство: xorshift-генератор с фиксированным зерном мутирует корректные +входные данные (замена, удаление, вставка байта, усечение, внедрение `NUL`, +вставка 4-байтового UTF-8 и недопустимого ведущего байта). Проверяемое свойство +одинаково для всех разборщиков: мутированный вход либо разобран в значение, +удовлетворяющее инвариантам самого разборщика, либо отклонён. Он никогда не +должен аварийно завершаться, зависать или быть принятым с нарушением инварианта. + +| Цель | Проверяемые инварианты принятого значения | +|---|---| +| Разбор версии (`ParseVersion`) | непустое ядро не длиннее 8 компонентов; при `prerelease` непустой набор идентификаторов; ревизия не длиннее одного символа; сравнение рефлексивно и антисимметрично | +| Манифест обновления (`ParseManifest`) | `schema == 1`, канал `stable`, SHA-256 ровно 64 символа, ненулевые размеры, все URL — HTTPS, поставщик точный, версии разбираемы, `key_id` непустой | +| Строгий JSON (`json::Parse`) | документ сверх лимита байтов никогда не принимается | +| Путь внутри пакета (`NormalizePackagePath`) | результат внутри точного allowlist, относительный, без обхода по компонентам, без `\`, `:` и `NUL` | +| Имя файла в защищённой области (`IsSafeStagingFileName`) | длина в пределах, без ведущей точки, без завершающей точки или пробела, без разделителей пути и `:` | + +Дополнительно проверяются явные враждебные формы путей: `../bin/winws.exe`, +`bin/../../evil`, `bin/./winws.exe`, `/bin/winws.exe`, `bin\winws.exe`, +`C:/bin/winws.exe`, `bin/`, пустая строка, `bin/con.exe`, `bin/nul`, +`bin/winws.exe ` (завершающий пробел), `bin/winws.exe.` (завершающая точка), +`bin/..`, `lists/../../../etc/passwd`. + +### Длинные кампании + +CI использует фиксированное зерно и 4000 итераций на цель, поэтому любой сбой +воспроизводится. Для длинной кампании без отдельного тулчейна: + +```powershell +$env:CHEBURNET_FUZZ_ITERATIONS = '2000000' +$env:CHEBURNET_FUZZ_SEED = '12345' +build\tests\cheburnet_tests.exe fuzzparsers +``` + +Выполненные кампании: 500 000 итераций на зерне по умолчанию и по 50 000 на +зёрнах `1`, `999983`, `123456789` — находок нет. + +Найденное фаззером на этапе разработки: предикат теста считал обходом путь +`..` в любом месте строки, тогда как обходом он является только как **целый** +компонент пути (`WinDivert64.s..ys` — обычное имя файла). Ошибка была в тесте, +не в продукте; предикат исправлен на покомпонентную проверку, и добавлены +явные враждебные формы выше. + +## Внесение отказов + +Транзакционная активация рабочей среды (`src/update/RuntimeActivation.cpp`) уже +построена на явных хуках для каждой границы, поэтому отказы вносятся без +изменения продукта. Тест CTest `faultinjection` проходит полную решётку: + +точки отказа × наличие предыдущей рабочей версии + +| Точка отказа | Соответствует реальному событию | +|---|---| +| `Preflight` | кандидат не прошёл предварительную проверку | +| `Stop` | доверенный текущий процесс не удалось остановить | +| `StartCandidate` | кандидат не запустился | +| `HealthCandidate` | кандидат не прошёл проверку состояния | +| `Commit` | состояние не удалось зафиксировать | +| `StartPrevious` | откат не смог запустить предыдущую версию | +| `HealthPrevious` | откат запустил предыдущую версию, но она не прошла проверку | + +Проверяемые инварианты для каждой комбинации: + +1. кандидат становится текущим **только** при полной фиксации; +2. при любом отказе остаётся или восстанавливается предыдущая рабочая версия; +3. фиксация недостижима после отказа на более ранней границе; +4. работающий процесс не останавливается, пока предварительная проверка не + одобрила кандидата; +5. откат никогда не запускает кандидата повторно; +6. зафиксированное состояние всегда помнит заменяемую версию как предыдущую + рабочую; +7. отсутствующий хук трактуется как отказ, а не как успех. + +Состояние `pending` на диске и его невозможность автоматического повышения +проверяются отдельно: тестом `updatestate` и сценарием `interrupted-pending` +релизного шлюза E2E. + +## Что осталось за рамками + +- libFuzzer/ASAN под clang-cl как параллельный тулчейн: не добавлялось, чтобы не + дестабилизировать основной конвейер MSVC; мутационная проверка свойств + покрывает те же разборщики детерминированно; +- внесение отказов в привилегированные операции файловой системы: выполняется + сценарием `filesystem-guards` релизного шлюза E2E, который требует + одноразовой машины. diff --git a/docs/production/UPSTREAM_1_10_2_IMPORT.md b/docs/production/UPSTREAM_1_10_2_IMPORT.md new file mode 100644 index 0000000..e690dcf --- /dev/null +++ b/docs/production/UPSTREAM_1_10_2_IMPORT.md @@ -0,0 +1,153 @@ +# Импорт Flowseal 1.10.1 → 1.10.2 + +Документ фиксирует результат Phase 2: перевод встроенной полезной нагрузки +CHEBURNET с Flowseal `1.10.1` на стабильный `1.10.2`. + +## Способ импорта + +Использован штатный защищённый импортёр, ручное копирование файлов не +применялось: + +```powershell +scripts\sync-upstream.ps1 -Version 1.10.2 +``` + +Результат: + +``` +UPSTREAM_IMPORT: PASS version=1.10.2 sha256=5eaac9fb2e4b1abd693487452a3ff3f4dfe9578a45f9ddddfa4bc1f5a6bb62d5 +``` + +Импортёр отработал полный транзакционный сценарий: проверка неизменяемости +выпуска → сверка SHA-256 архива с digest релизного ассета → валидация каждой +записи ZIP до материализации → изолированная генерация стратегий и ресурсов → +чистая сборка и полный набор тестов в отдельном дереве → замена production-файлов +→ побайтовая сверка установленной нагрузки с архивом. + +## Найденный и устранённый дефект импортёра + +Первый запуск полного импорта завершился ошибкой **не из-за 1.10.2**, а из-за +самого импортёра: + +``` +32/34 Test #32: license_validation ...............***Failed +missing license: SECURITY.md +``` + +`sync-upstream.ps1` собирал изолированное дерево проверки из явного списка +файлов, в котором не было `SECURITY.md` и каталога `docs`. При этом +`scripts/validate-licenses.ps1` требует `SECURITY.md`, `docs/PRIVACY.md` и +`docs/UPDATE_SECURITY.md`. То есть полный импорт **не мог завершиться успешно +ни для какой версии upstream** — шлюз падал на отсутствующем файле репозитория, +а не на импортируемой нагрузке. Дефект существовал до текущих работ. + +Исправление: + +1. список копирования дополнен `docs`, `SECURITY.md`, `CHANGELOG.md`, + `DEPENDENCIES.md`, `CONTRIBUTING.md`; +2. добавлен регрессионный тест `tests/importer_validation_tree.ps1` + (тест CTest `importer_validation_tree`), который извлекает список + копирования **из исходника самого импортёра**, собирает по нему дерево и + прогоняет против него релизные шлюзы. Тест не требует сети и не скачивает + нагрузку, поэтому расхождение списка и шлюзов больше не может пройти молча. + +Проверено, что тест ловит исходный дефект: со старым списком +`validate-licenses.ps1` завершается с `missing license: SECURITY.md`, с новым — +`LICENSE_VALIDATION: PASS`. + +## Provenance + +`resources/upstream/provenance.json`: + +| Поле | Было (1.10.1) | Стало (1.10.2) | +|---|---|---| +| version / tag | `1.10.1` | `1.10.2` | +| release_id | `367512178` | `375784527` | +| archive_sha256 | `f748d61f…76c9` | `5eaac9fb2e4b1abd693487452a3ff3f4dfe9578a45f9ddddfa4bc1f5a6bb62d5` | +| upstream_commit | `47da17f8…b938` | `dfd8e613b099676cf2aa7b474ee5923801514dec` | +| immutable | `true` | `true` | +| source_url | …/1.10.1/…zip | `https://github.com/Flowseal/zapret-discord-youtube/releases/download/1.10.2/zapret-discord-youtube-1.10.2.zip` | + +`imported_at_utc`: `2026-09-11T07:13:00Z`. + +## Состав изменений нагрузки + +### Стратегии + +| Изменение | Файл | +|---|---| +| добавлена | `general (ALT13).bat` | +| изменена | `general (EXP).bat` | + +Количество стратегий: **21 → 22**. Идентификаторы типизированного каталога: + +``` +general, alt, alt2 … alt12, alt13, exp, +faketls_auto, faketls_auto_alt, faketls_auto_alt2, faketls_auto_alt3, +simple_fake, simple_fake_alt, simple_fake_alt2 +``` + +Счётчик стратегий в рантайме остаётся динамическим — это отдельно проверяется +регрессией `STRATEGY runtime has no fixed strategy count`. + +### Бинарные ресурсы + +| Изменение | Файл | +|---|---| +| добавлен | `bin/tls_clienthello_sochi_park.bin` | +| переименован | `bin/quic_initial_4pda.to.bin` → `bin/quic_initial_4pda_to.bin` | +| удалён | `bin/quic_initial_dbankcloud_ru.bin` | + +Проверено, что удалённые и переименованные ресурсы **не остались** во встроенной +нагрузке: после чистой переконфигурации ни `quic_initial_4pda.to.bin`, ни +`quic_initial_dbankcloud_ru.bin` не присутствуют в `GeneratedManifest.h` и +`cheburnet_payload.rc` (проверка по точному совпадению строки). Новые ресурсы +присутствуют. Это ожидаемо: `resources/payload` заменяется целиком, а +`.rc`-манифест генерируется из каталога нагрузки на этапе configure. + +### Списки + +| Файл | Изменение | +|---|---| +| `lists/list-general.txt` | +9 строк | +| `lists/list-exclude.txt` | +27 / −6 строк | + +## Совместимость импортёра + +Расширение грамматики строгого парсера **не потребовалось**. `ALT13` использует +уже поддерживаемый синтаксис, включая `--dpi-desync=fake,hostfakesplit`, +`--dpi-desync-hostfakesplit-mod=host=…,altorder=1` и `--dpi-desync-fake-unknown`. +Изменение в `general (EXP).bat` — добавление `--dpi-desync-cutoff=n4` и +переименование ссылки на `quic_initial_4pda_to.bin`. + +Неизвестный синтаксис по-прежнему отклоняется fail-closed; BAT-файлы upstream +в обычном рантайме не исполняются. + +## Проверка + +Все команды выполнены на ветке `claude/production-hardening-v1`. + +| Проверка | Команда | Результат | +|---|---|---| +| Чистая сборка | `scripts\build-release.ps1 -BuildDir build-p2` | **PASS**, `CHEBURNET.exe` 3.92 MB, предупреждений нет (`/W4 /WX`) | +| Пакет обновления | (в составе сборки) | `UPDATE_PACKAGE: PASS version=1.10.2 files=27 bytes=3403061 sha256=970c8550…3333` | +| Полный набор тестов | `scripts\run-tests.ps1 -BuildDir build-p2` | **35/35 PASS** | +| Достоверность стратегий | (в составе `run-tests.ps1`) | `STRATEGY_FIDELITY: 88/88 PASS (22 strategies x 4 modes)` | +| Достоверность upstream | `scripts\verify-upstream.ps1` | `UPSTREAM_FIDELITY: PASS version=1.10.2 files=46 strategies=22 archive_sha256=5eaac9fb…62d5` | +| Идемпотентность импорта | `scripts\sync-upstream.ps1 -Version 1.10.2 -CheckOnly` | `UPSTREAM_IMPORT: already current (1.10.2)` | +| Дерево проверки импортёра | тест CTest `importer_validation_tree` | **PASS** (15 записей, релизные шлюзы удовлетворены) | + +Режимы GameFilter покрыты полностью: 22 стратегии × 4 режима +(`Off`, `All`, `Tcp`, `Udp`) = 88 независимых сверок аргументов `winws` +с независимым разбором исходных BAT-файлов. + +## Риски + +- Поведение новой стратегии `ALT13` в реальной сети не проверялось: CHEBURNET + гарантирует лишь побайтовое соответствие импортированных аргументов + upstream-оригиналу. Функциональная эффективность стратегии — зона + ответственности upstream. +- Пользовательская конфигурация с сохранённым `strategyId`, отсутствующим в + 1.10.2, будет сброшена на `general` с записью в журнал. В 1.10.2 ни одна + стратегия не удалена, поэтому практического влияния нет. +- Размер пакета обновления вырос незначительно (3 399 786 → 3 403 061 байт). diff --git a/docs/production/UPSTREAM_AUTOMATION.md b/docs/production/UPSTREAM_AUTOMATION.md new file mode 100644 index 0000000..df7fc86 --- /dev/null +++ b/docs/production/UPSTREAM_AUTOMATION.md @@ -0,0 +1,124 @@ +# Автоматизация импорта исходных выпусков + +Документ описывает, что происходит при появлении нового стабильного выпуска +Flowseal и почему автоматика устроена именно так. + +## Что делает запланированный процесс + +`.github/workflows/upstream-check.yml` запускается по расписанию (еженедельно) +и вручную. Он состоит из трёх заданий с раздельными правами. + +### 1. `detect` — `contents: read` + +`scripts/upstream-report.ps1` запрашивает последний выпуск Flowseal и сравнивает +его с `resources/upstream/provenance.json`. Выпуск принимается к рассмотрению +только если он не черновик, не предварительный и помечен как неизменяемый, а +релизный ZIP-ассет имеет SHA-256 digest. + +Результат — машиночитаемый `report.json` и готовый `report.md`. Статус: + +| Статус | Значение | +|---|---| +| `current` | встроенная нагрузка уже соответствует последнему стабильному выпуску | +| `available` | существует более новый неизменяемый стабильный выпуск | +| `error` | состояние upstream определить не удалось (задание падает) | + +### 2. `validate` — `contents: read` + +Выполняется только при статусе `available`. + +Запускается штатный защищённый импортёр `scripts/sync-upstream.ps1`, который сам +проверяет кандидата в изолированном дереве: сверка SHA-256 архива с digest +релизного ассета, валидация каждой записи ZIP до материализации, генерация +стратегий и ресурсов, чистая сборка, полный набор тестов и побайтовая сверка +установленной нагрузки с архивом. Затем итоговое дерево собирается и тестируется +ещё раз, и проверяется достоверность upstream. + +Импорт происходит в одноразовой рабочей копии раннера и **никуда не публикуется**. +В артефакты запуска выгружаются: `report.json`, `report.md`, +`payload-diff.txt` (`git diff --stat`) и `payload-status.txt`. + +Дельта нагрузки вычисляется из фактического вывода `git status --porcelain` +(`scripts/upstream-delta.ps1`), а не угадывается: учитываются добавления, +удаления, изменения, переименования и закавыченные пути с пробелами. + +### 3. `task` — `issues: write` + +Создаёт **одну** задачу на версию upstream либо обновляет существующую. +Дедупликация выполняется по скрытому маркеру в теле задачи: + +``` + +``` + +Маркер, а не заголовок: заголовок человек может отредактировать, маркер — нет. +Поиск идёт по задачам в любом состоянии, поэтому повторный запуск не создаёт +дубликат и не открывает заново уже закрытую задачу. + +## Состав задачи + +| Поле | Источник | +|---|---| +| Старая версия | `provenance.version` | +| Новая версия | тег релиза upstream | +| Ссылка на выпуск | `release.html_url` | +| SHA-256 архива | digest релизного ассета | +| Размер архива | `asset.size` | +| Commit upstream | разрешение тега до неизменяемого commit | +| Число стратегий до и после | дельта из `git status` | +| Добавленные / удалённые / изменённые стратегии и файлы нагрузки | дельта из `git status` | +| Результат проверки | `passed` / `failed` с пояснением | +| Ссылка на запуск | `actions/runs/` | +| Команды для локального импорта | шаблон в `report.md` | + +## Почему задача, а не черновой pull request + +Предпочтительным вариантом в задании был черновой pull request. Он требует +`contents: write` у запланированного процесса, то есть постоянного права записи +в репозиторий у автоматики, которая обрабатывает содержимое стороннего выпуска. +Это расширение поверхности атаки цепочки поставок ради удобства просмотра +различий. + +Выбран вариант с задачей, явно разрешённый заданием как приемлемая альтернатива, +потому что он даёт ту же полезность без этого права: + +- импортёр детерминирован и сверяет архив с неизменяемым digest релизного + ассета, поэтому локальный запуск `scripts\sync-upstream.ps1 -Version <версия>` + воспроизводит ровно то, что проверил CI; +- разбор различий доступен в артефактах запуска (`payload-diff.txt`) и в теле + задачи; +- решение о внесении изменений остаётся за человеком, как и при черновом PR. + +Переход на черновой PR потребует: `contents: write` и `pull-requests: write` у +этого процесса, отдельной ветки вида `upstream/<версия>`, принудительного +`draft: true` и запрета автослияния. Ничто в текущей оснастке этому не мешает; +это сознательно отложено как расширение прав, не дающее новой гарантии. + +## Чего автоматика не делает + +- не объединяет ничего автоматически (проверяется регрессией: + в процессе нет `gh pr merge`, `--auto`, `--admin`, `--merge`); +- не получает права записи в содержимое репозитория + (`contents: write` отсутствует, проверяется регрессией); +- не запрашивает `pull-requests: write`, которым не пользуется; +- не публикует импортированную нагрузку в ветку; +- не принимает черновики, предварительные и изменяемые выпуски upstream. + +Все сторонние actions закреплены по неизменяемым commit SHA; это проверяется +тестом `upstream_automation` для каждого `uses:`. + +## Автоматические проверки + +| Проверка | Где | +|---|---| +| Разбор дельты: добавление, удаление, изменение, переименование, закавыченные пути | тест CTest `upstream_automation` | +| Пути вне нагрузки игнорируются | `upstream_automation` | +| Счётчики стратегий «до» и «после» выводятся из дельты | `upstream_automation` | +| Отчёт содержит все обязательные поля | `upstream_automation` | +| Отчёт отклоняет черновик, предварительный и изменяемый выпуск | `upstream_automation` | +| Процесс не получает `contents: write` | `upstream_automation` | +| Процесс запрашивает `issues: write` и не запрашивает `pull-requests: write` | `upstream_automation` | +| Процесс запускает защищённый импортёр | `upstream_automation` | +| Дедупликация по маркеру присутствует | `upstream_automation` | +| Автослияние отсутствует | `upstream_automation` | +| Все сторонние actions закреплены по commit SHA | `upstream_automation` | diff --git a/resources/app.manifest.in b/resources/app.manifest.in index 495b962..e1b9ca1 100644 --- a/resources/app.manifest.in +++ b/resources/app.manifest.in @@ -3,7 +3,7 @@