208 lines
8.3 KiB
Bash
Executable File
208 lines
8.3 KiB
Bash
Executable File
#!/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"
|
||
CI_CONTAINER_SCRIPT="$PROJECT_ROOT/scripts/run-ci-container.sh"
|
||
CI_CONTAINER_TEST="$PROJECT_ROOT/scripts/test-run-ci-container.sh"
|
||
|
||
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() {
|
||
local security_timeout
|
||
|
||
require_file "$WORKFLOW" || return
|
||
require_file "$CI_CONTAINER_SCRIPT" || return
|
||
require_file "$CI_CONTAINER_TEST" || return
|
||
|
||
require_count '^[[:space:]]+sbom:[[:space:]]+true$' 2 \
|
||
"для обоих образов должна публиковаться SBOM attestation"
|
||
require_count '^[[:space:]]+provenance:[[:space:]]+mode=max$' 2 \
|
||
"для обоих образов должна публиковаться provenance attestation"
|
||
require_count 'image --timeout 15m --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"
|
||
require_count 'bash scripts/run-ci-container\.sh' 4 \
|
||
"тесты и Trivy должны передавать файлы через Docker API"
|
||
require_count 'bash scripts/test-run-ci-container\.sh' 1 \
|
||
"workflow должен проверять helper запуска CI-контейнеров"
|
||
require_count '--env DOCKER_CONFIG=/workspace' 2 \
|
||
"оба Trivy scan должны получать registry credentials через Docker API"
|
||
|
||
security_timeout=$(awk '
|
||
$0 == " security-scan:" {
|
||
in_job = 1
|
||
next
|
||
}
|
||
in_job && $0 ~ /^ [[:alnum:]_-]+:$/ {
|
||
exit
|
||
}
|
||
in_job && $1 == "timeout-minutes:" {
|
||
print $2
|
||
exit
|
||
}
|
||
' "$WORKFLOW")
|
||
if [[ "$security_timeout" != "35" ]]; then
|
||
fail "security-scan job должен иметь таймаут 35 минут"
|
||
fi
|
||
|
||
if grep -Fq '${{ github.workspace }}' "$WORKFLOW" \
|
||
|| grep -Fq -- '-v "$HOME' "$WORKFLOW"; then
|
||
fail "нельзя использовать пути job-контейнера в bind mount внешнего Docker daemon"
|
||
fi
|
||
|
||
if ! grep -Eq '^ needs: \[build-and-push-backend, build-and-push-frontend, security-scan\]$' "$WORKFLOW"; then
|
||
fail "deploy обязан зависеть от успешного security-scan"
|
||
fi
|
||
if ! grep -Eq '^[[:space:]]{2}push:[[:space:]]*$' "$WORKFLOW" \
|
||
|| ! grep -Eq '^[[:space:]]{6}- main[[:space:]]*$' "$WORKFLOW"; then
|
||
fail "production deploy должен автоматически запускаться после push в main"
|
||
fi
|
||
if grep -Fq "if: \${{ gitea.event_name == 'workflow_dispatch' }}" "$WORKFLOW"; then
|
||
fail "production deploy не должен требовать ручной workflow_dispatch"
|
||
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_runtime_updates() {
|
||
local component
|
||
local dockerfile
|
||
|
||
for component in backend frontend; do
|
||
dockerfile="$PROJECT_ROOT/$component/Dockerfile"
|
||
require_file "$dockerfile" || continue
|
||
if ! grep -Fq 'RUN apk upgrade --no-cache' "$dockerfile"; then
|
||
fail "runtime-образ $component должен устанавливать доступные исправления Alpine"
|
||
fi
|
||
done
|
||
}
|
||
|
||
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_runtime_updates
|
||
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'
|