#!/usr/bin/env bash set -euo pipefail PROJECT_ROOT=${PROJECT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)} WORKFLOW="$PROJECT_ROOT/.gitea/workflows/docker-build.yaml" failures=0 fail() { printf '[АРТЕФАКТЫ] Ошибка: %s\n' "$1" >&2 failures=$((failures + 1)) } require_file() { local file=$1 if [[ ! -f "$file" ]]; then fail "не найден обязательный файл ${file#"$PROJECT_ROOT"/}" return 1 fi } check_dockerfile() { local file=$1 local line require_file "$file" || return while IFS= read -r line; do if [[ ! "$line" =~ ^FROM[[:space:]]+[^[:space:]]+@sha256:[0-9a-f]{64}([[:space:]]+AS[[:space:]]+[A-Za-z0-9._-]+)?$ ]]; then fail "базовый образ в ${file#"$PROJECT_ROOT"/} должен содержать полный sha256 digest: $line" fi done < <(grep -E '^FROM[[:space:]]+' "$file") } check_compose_images() { local line require_file "$PROJECT_ROOT/compose.yaml" || return while IFS= read -r line; do if [[ ! "$line" =~ image:[[:space:]]+[^[:space:]]+@sha256:[0-9a-f]{64}[[:space:]]*$ ]]; then fail "образ Compose должен содержать полный sha256 digest: ${line#*image: }" fi done < <(grep -E '^[[:space:]]+image:[[:space:]]+' "$PROJECT_ROOT/compose.yaml") } check_actions() { local action_ref require_file "$WORKFLOW" || return while IFS= read -r action_ref; do if [[ ! "$action_ref" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}$ ]]; then fail "Action должен быть закреплён полным commit SHA: $action_ref" fi done < <(sed -nE 's/^[[:space:]]+uses:[[:space:]]+([^[:space:]#]+).*/\1/p' "$WORKFLOW") } require_count() { local pattern=$1 local expected=$2 local description=$3 local actual actual=$(grep -Ec "$pattern" "$WORKFLOW" || true) if (( actual < expected )); then fail "$description: ожидается не менее $expected, найдено $actual" fi } check_workflow_gates() { require_file "$WORKFLOW" || return require_count '^[[:space:]]+sbom:[[:space:]]+true$' 2 \ "для обоих образов должна публиковаться SBOM attestation" require_count '^[[:space:]]+provenance:[[:space:]]+mode=max$' 2 \ "для обоих образов должна публиковаться provenance attestation" require_count 'image --scanners vuln --exit-code 1 --severity HIGH,CRITICAL' 2 \ "оба образа должны проходить блокирующее HIGH/CRITICAL-сканирование" require_count 'TRIVY_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \ "образ Trivy должен быть закреплён digest" require_count 'MAVEN_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \ "образ Maven для CI должен быть закреплён digest" require_count 'NODE_IMAGE: [^[:space:]]+@sha256:[0-9a-f]{64}$' 1 \ "образ Node.js для CI должен быть закреплён digest" if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then fail "deploy обязан зависеть от успешного security-scan" fi if ! grep -Eq '^ KUBECTL_VERSION: v[0-9]+\.[0-9]+\.[0-9]+$' "$WORKFLOW" \ || ! grep -Eq '^ KUBECTL_SHA256: [0-9a-f]{64}$' "$WORKFLOW" \ || ! grep -Fq 'sha256sum --check --strict' "$WORKFLOW"; then fail "kubectl должен иметь точную версию и обязательную SHA-256 проверку" fi } check_downloaded_backend_artifact() { local dockerfile="$PROJECT_ROOT/backend/Dockerfile" require_file "$dockerfile" || return if ! grep -Eq '^ARG OTEL_JAVAAGENT_VERSION=[0-9]+\.[0-9]+\.[0-9]+$' "$dockerfile" \ || ! grep -Eq '^ARG OTEL_JAVAAGENT_SHA256=[0-9a-f]{64}$' "$dockerfile" \ || ! grep -Fq 'sha256sum -c -' "$dockerfile"; then fail "OpenTelemetry Java Agent должен иметь точную версию и SHA-256 проверку" fi } check_testcontainer_images() { local image_catalog="$PROJECT_ROOT/backend/src/test/java/com/magistr/app/testing/TestContainerImages.java" [[ -d "$PROJECT_ROOT/backend/src/test/java" ]] || return 0 require_file "$image_catalog" || return if ! grep -Eq 'postgres:[^"[:space:]]+@sha256:[0-9a-f]{64}' "$image_catalog" \ || ! grep -Fq 'asCompatibleSubstituteFor("postgres")' "$image_catalog"; then fail "образ PostgreSQL Testcontainers должен содержать digest и явную совместимость" fi } check_optional_kubernetes_images() { local k8s_dir=${K8S_DIR:-} local line [[ -n "$k8s_dir" ]] || return 0 if [[ ! -d "$k8s_dir" ]]; then fail "каталог Kubernetes не найден: $k8s_dir" return fi while IFS= read -r line; do if [[ "$line" =~ image:[[:space:]]+([^[:space:]]+) ]]; then local image_ref=${BASH_REMATCH[1]} if [[ ! "$image_ref" =~ @sha256:[0-9a-f]{64}$ ]]; then fail "production-образ Kubernetes должен содержать digest: $image_ref" fi fi done < <(grep -R -h -E '^[[:space:]]+image:[[:space:]]+' "$k8s_dir" --include='*.yaml' --include='*.yml' || true) } check_dockerfile "$PROJECT_ROOT/backend/Dockerfile" check_dockerfile "$PROJECT_ROOT/frontend/Dockerfile" check_compose_images check_actions check_workflow_gates check_downloaded_backend_artifact check_testcontainer_images check_optional_kubernetes_images if (( failures > 0 )); then printf '[АРТЕФАКТЫ] Проверка завершена с ошибками: %d\n' "$failures" >&2 exit 1 fi printf '[АРТЕФАКТЫ] Все версии, digest, checksum, SBOM и scan gates проверены\n'