#!/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_DIRTY_BUILD: новое # advisory может выйти в неудачный момент, и решение «выпускаем, зная о нём» # должно приниматься человеком ЯВНО, а не пропуском шага по умолчанию. ALLOW_VULNERABLE_DEPENDENCIES="${ALLOW_VULNERABLE_DEPENDENCIES:-false}" security_gate_failed() { local scanner="$1" local details="$2" if [ "$ALLOW_VULNERABLE_DEPENDENCIES" = "true" ]; then log_info "WARNING: $scanner нашёл уязвимости, но сборка продолжена по ALLOW_VULNERABLE_DEPENDENCIES=true" log_info "$details" DEPENDENCY_SECURITY_GATE="accepted-risk" export DEPENDENCY_SECURITY_GATE return 0 fi fail "$scanner: найдены уязвимости в зависимостях. Обновите граф или, осознанно принимая риск, выставьте ALLOW_VULNERABLE_DEPENDENCIES=true. $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: по готовому tarball должно # быть видно, проверялся он или собран с пропущенной проверкой. Ровно так же # устроен hysteria_compat_gate. run_dependency_security_gate() { if [ "${SKIP_SECURITY_SCAN:-false}" = "true" ]; then log_info "WARNING: проверка зависимостей на уязвимости пропущена (SKIP_SECURITY_SCAN=true)" DEPENDENCY_SECURITY_GATE="skipped" export DEPENDENCY_SECURITY_GATE return 0 fi [ -n "${GOVULNCHECK_VERSION:-}" ] \ || fail "security: GOVULNCHECK_VERSION не задан; load_versions_contract должен выполниться первым" [ -n "${PNPM_AUDIT_LEVEL:-}" ] \ || fail "security: PNPM_AUDIT_LEVEL не задан; load_versions_contract должен выполниться первым" DEPENDENCY_SECURITY_GATE="true" export DEPENDENCY_SECURITY_GATE run_go_vulnerability_gate run_frontend_vulnerability_gate }