Files
HY2XS_flamy/tools/build/lib/security.sh
T
founder cf094f6e6f fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам
Проход по операциям, которые делают не то, что обещает их имя.

P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать
пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS
продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор
удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же
секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config:
«создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной
транзакцией.

P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и
отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь
недоступный секрет любого пира отклоняет весь запрос с указанием имени.

P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет,
если оно не начиналось с v1: — остаток поколения с открытыми секретами.

P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на
проблему обрывала все живые соединения.

P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в
/etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine
auth переставал совпадать.

P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести
артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и
документации, отсутствие проверяется приёмкой.

P2. UPDATE по отсутствующей строке config считался успехом, и cron
перепланировался при несохранённом значении. Решение по RowsAffected.

P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения
ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage.

P2. Удалены алиасы /:id/client-url и /:id/qr.

Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go —
языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют
среду с versions.env.
2026-08-30 06:48:50 +05:00

146 lines
8.2 KiB
Bash
Raw 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 — граф npm без анализа достижимости, поэтому ограничен
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
# devDependencies в артефакт не попадают.
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
#
# Раньше существовали два: 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
}
# Анализ production-зависимостей frontend.
run_frontend_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: pnpm audit --prod (порог ${PNPM_AUDIT_LEVEL})"
set +e
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --prod --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 printf '%s' "$report" | grep -qiE 'ERR_PNPM_AUDIT_ENDPOINT|ENOTFOUND|ECONNREFUSED|network|getaddrinfo'; 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
}