build: закрыть обходы релизного гейта тестов и проверять весь граф npm

Два гейта сборки проверяли не то, что обещали.

1. pnpm audit проверял production-подграф вместо всего lock-графа.

   Гейт запускался с --prod под обоснованием «devDependencies в артефакт не
   попадают». Для frontend build tooling это неверно по существу: vite и
   rollup действительно не копируются на production-сервер как node_modules,
   но они ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и порождают тот
   самый production-бандл, который уезжает в артефакт.

   Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый
   бандл, и `pnpm audit --prod` его не показывал — по всему графу тот же
   прогон дал 33 предупреждения против нуля. Критерий приёмки №47 в docs/11
   формулировал «по всему графу» правильно ещё до того, как это стало правдой
   в коде.

   На текущем lock-файле полный граф на пороге high чист.

2. SKIP_TESTS позволял собрать production-артефакт без тестов.

   Переменная была описана как «аварийное отключение тестов; для
   release-сборок недопустимо». Недопустимость держалась исключительно на этой
   фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
   запускались. То есть

       SKIP_TESTS=true ./tools/build/build.sh

   доходила до конца и выдавала обычный tarball с build_profile=production и
   dependency_security_gate=true — артефакт, по которому невозможно отличить
   проверенную сборку от непроверенной.

   Глушила она при этом не только тесты: под тем же флагом пропускались
   `tsc --noEmit` для оркестратора и `go vet` для админки, то есть проверка
   типов и статический анализ того самого кода, который уезжает в production.

   Выбран тот же строгий вариант, что уже принят для проверки зависимостей:
   обхода нет. Готовый пакет объявляет tests_gate=true в metadata, и это
   утверждение опирается на результат — обе функции прогона выставляют свой
   флаг только после успешного завершения, а write_metadata отказывается
   писать метаданные, если хотя бы один не подтверждён.

Приёмка закрепляет оба инварианта: --prod не может вернуться в гейт, SKIP_TESTS
не может вернуться ни в один модуль сборки и ни в README/docs, tests_gate=true
обязателен в metadata, а утверждение о прогоне обязано следовать за прогоном.
This commit is contained in:
2026-08-30 18:18:13 +05:00
parent e84fdedc4b
commit 594525dd73
8 changed files with 290 additions and 20 deletions
+21 -6
View File
@@ -23,9 +23,24 @@ set -euo pipefail
# govulncheck — анализ достижимости: уязвимость считается только если до неё
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
# ровно то, что версия Go привносит в production-бинарь;
# pnpm audit — граф npm без анализа достижимости, поэтому ограничен
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
# devDependencies в артефакт не попадают.
# pnpm audit — ВЕСЬ lock-граф frontend без анализа достижимости, с порогом
# PNPM_AUDIT_LEVEL.
#
# Про «весь граф» отдельно, потому что здесь стояло `--prod` с обоснованием
# «devDependencies в артефакт не попадают».
#
# Для frontend build tooling это обоснование неверно по существу. Да, vite и
# rollup не копируются на production-сервер как node_modules. Но они
# ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и ПОРОЖДАЮТ тот самый
# production-бандл, который уезжает в артефакт. Уязвимость в них — это
# уязвимость в том, что мы выпускаем.
#
# Это не гипотеза. GHSA по DOM clobbering в Rollup затрагивал именно
# генерируемый бандл, то есть уезжал в production, — и `pnpm audit --prod` его
# не показывал. По всему графу тот же прогон дал 33 предупреждения против нуля.
#
# docs/11 формулировал критерий приёмки правильно («pnpm audit по всему графу»)
# ещё до того, как это стало правдой в коде.
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
#
@@ -95,15 +110,15 @@ $report"
esac
}
# Анализ production-зависимостей frontend.
# Анализ всего lock-графа frontend, включая build tooling.
run_frontend_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: pnpm audit --prod (порог ${PNPM_AUDIT_LEVEL})"
log_step "Security: pnpm audit по всему графу (порог ${PNPM_AUDIT_LEVEL})"
set +e
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --prod --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
status=$?
set -e