Files
founder a1c74caa0c fix(build): исключить SIGPIPE из релизных гейтов под pipefail
Поиск с флагом -q прекращает чтение на первом совпадении и закрывает свой конец
канала. Продюсер, которому осталось что писать, получает SIGPIPE и завершается
кодом 141, а `set -o pipefail` делает 141 статусом всей конструкции:

    совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»

Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
конструкции в коде нет» — ложный PASS: запрещённая конструкция найдена, а гейт
зелёный. Отрицательными проверками закреплена половина инвариантов приёмки,
включая запрет обхода тестов и запрет `pnpm audit --prod`.

Порог резкий: пока вывод продюсера помещается в буфер канала (64 KiB на Linux),
он не блокируется и успевает завершиться раньше, чем потребитель начнёт читать.
Замер, 60 прогонов на размер: до 60 KiB — 0 отказов, ровно на 64 KiB — 58/60,
от 96 KiB — 60/60. То есть проверка выглядит исправной ровно до первого
источника крупнее буфера, а такие файлы в репозитории уже есть.

- 56 мест переведены на here-string: `grep -q PATTERN <<<"$content"`;
- продюсеры-команды (ss|awk, dpkg-query, /proc/cpuinfo, systemctl
  list-unit-files) сначала читаются в переменную;
- введён code_has: десять отрицательных сканов держались на `|| true` внутри
  code_without_comments, гасившем 141, — то есть на побочном эффекте
  подавления ошибок, а не на заявленном свойстве;
- несуществующий путь в скане больше не означает успех: `2>/dev/null || true`
  превращал опечатку в пустой вывод, а пустой вывод для проверки «этого в коде
  нет» — это PASS. Проверка явная, а не через set -e: в контексте `! code_has`
  bash отключает errexit на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
2026-09-01 04:28:54 +05:00

161 lines
9.3 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# Проверка графа зависимостей на известные уязвимости.
#
# Зачем отдельный шаг сборки, а не разовая проверка «когда вспомним».
#
# Версии toolchain и библиотек фиксируются в versions.env, go.sum и lock-файлах
# — то есть намеренно НЕ движутся. Это правильно для воспроизводимости и ровно
# поэтому опасно для безопасности: зафиксированный граф не стареет только на
# бумаге, а advisory по нему выходят и после фиксации. Единственный момент,
# когда расхождение между «что мы собираем» и «что про это известно» можно
# поймать дёшево, — сама сборка релиза.
#
# История, ради которой шаг появился: на Go 1.21.13 и графе поколения 0.x
# govulncheck находил 21 ВЫЗЫВАЕМУЮ уязвимость, включая GO-2025-3553 в
# github.com/golang-jwt/jwt v3 — с пометкой `Fixed in: N/A` и путём вызова из
# разбора токена, то есть достижимую с неаутентифицированного запроса. Ни один
# из существовавших шагов сборки этого показать не мог.
#
# Проверяются РАЗНЫЕ вещи в двух экосистемах:
#
# govulncheck — анализ достижимости: уязвимость считается только если до неё
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
# ровно то, что версия Go привносит в production-бинарь;
# 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 по всему графу»)
# ещё до того, как это стало правдой в коде.
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
#
# Раньше существовали два: ALLOW_VULNERABLE_DEPENDENCIES=true записывал в
# metadata `dependency_security_gate=accepted-risk`, SKIP_SECURITY_SCAN=true —
# `skipped`. Оба были описаны в README и docs/02 как способ выпустить релиз,
# зная об уязвимости.
#
# Способом они не были. Финальная приёмка архива требует буквально
#
# grep -q '^dependency_security_gate=true$' metadata/package.env
#
# то есть сборка с любым из этих значений доходила до самого конца — компиляция,
# бандл, тесты, метаданные, tar — и падала на последнем шаге. Продукт
# документировал операцию, которую сам же запрещал, а обнаруживалось это через
# полный цикл сборки.
#
# Из двух непротиворечивых вариантов выбран строгий: гейт обязателен, значение
# в metadata ровно одно. Контракт при этом читается однозначно:
#
# релизный артефакт HY2XS невозможно собрать с непройденной проверкой
# зависимостей.
#
# Для локальной работы обходить нечего: `go test ./...`, `govulncheck ./...` и
# `pnpm audit` запускаются напрямую и к созданию tarball отношения не имеют.
security_gate_failed() {
local scanner="$1"
local details="$2"
fail "$scanner: найдены уязвимости в зависимостях.
Обойти этот шаг нельзя: релизный пакет HY2XS собирается только с пройденной
проверкой. Обновите граф зависимостей (apps/go.sum, apps/frontend/pnpm-lock.yaml)
или версию toolchain в versions.env.
$details"
}
# Анализ Go-графа, включая stdlib выбранной версии Go.
run_go_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: govulncheck ${GOVULNCHECK_VERSION} (Go ${GO_VERSION})"
# GOTOOLCHAIN=local обязателен: без него go может молча скачать другую
# версию toolchain, и проверялась бы не та stdlib, которая попадёт в бинарь.
set +e
report="$(cd "$ui_src" && GOTOOLCHAIN=local "$GO_BIN" run \
"golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... 2>&1)"
status=$?
set -e
# 0 — чисто; 3 — найдены вызываемые уязвимости; остальное — отказ самого
# инструмента, и его нельзя трактовать как «уязвимостей нет».
case "$status" in
0)
log_info "govulncheck: вызываемых уязвимостей не найдено"
;;
3)
security_gate_failed "govulncheck" "$report"
;;
*)
fail "govulncheck завершился с кодом $status (это отказ инструмента, а не результат проверки):
$report"
;;
esac
}
# Анализ всего lock-графа frontend, включая build tooling.
run_frontend_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: pnpm audit по всему графу (порог ${PNPM_AUDIT_LEVEL})"
set +e
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
status=$?
set -e
if [ "$status" -eq 0 ]; then
log_info "pnpm audit: уязвимостей уровня ${PNPM_AUDIT_LEVEL} и выше не найдено"
return 0
fi
# pnpm audit ходит в реестр npm. Недоступность реестра — это отказ проверки,
# а не её отрицательный результат, и молча пропускать его нельзя.
if grep -qiE 'ERR_PNPM_AUDIT_ENDPOINT|ENOTFOUND|ECONNREFUSED|network|getaddrinfo' <<<"$report"; then
fail "pnpm audit не смог обратиться к реестру npm — проверка не выполнена:
$report"
fi
security_gate_failed "pnpm audit" "$report"
}
# Результат шага уезжает в metadata/package.env — так же, как hysteria_compat_gate.
#
# Значение у поля теперь ровно одно: `true`. Пропущенного состояния не бывает,
# потому что не бывает пакета, собранного с пропущенной проверкой; поле остаётся
# в metadata как утверждение о готовом артефакте, а не как переключатель.
run_dependency_security_gate() {
[ -n "${GOVULNCHECK_VERSION:-}" ] \
|| fail "security: GOVULNCHECK_VERSION не задан; load_versions_contract должен выполниться первым"
[ -n "${PNPM_AUDIT_LEVEL:-}" ] \
|| fail "security: PNPM_AUDIT_LEVEL не задан; load_versions_contract должен выполниться первым"
run_go_vulnerability_gate
run_frontend_vulnerability_gate
# Флаг выставляется ПОСЛЕ обеих проверок, а не до них. Разницы в поведении
# сейчас нет — обе ветки отказа завершают сборку, — но «утверждение о
# результате», записанное перед получением результата, рано или поздно
# переживает свою причину.
DEPENDENCY_SECURITY_GATE="true"
export DEPENDENCY_SECURITY_GATE
}