#!/usr/bin/env bash # Post-release verification for desfoto.de. # # .ocauto/verify # # Proves that (a) the live desfoto.de serves the released build and assets, # (b) the security posture matches the promises on /datenschutz/, and # (c) the neighbouring sites (dennyschulz.de, dennyapp.de) are untouched. set -euo pipefail cd "$(dirname "$0")/.." root="$(pwd)" release_sha="${1:-unknown}" remote="${DESFOTO_REMOTE:-prod-main}" base="https://desfoto.de" failures=0 pass() { printf ' ok %s\n' "$1"; } fail() { printf ' FAIL %s\n' "$1" >&2; failures=$((failures + 1)); } check_status() { local url="$1" want="$2" got got="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 "$url" || echo 000)" if [ "$got" = "$want" ]; then pass "$url -> $got"; else fail "$url -> $got (want $want)"; fi } check_contains() { local url="$1" needle="$2" body body="$(curl -sS --max-time 30 "$url" || true)" if printf '%s' "$body" | grep -qF -- "$needle"; then pass "$url contains '$needle'" else fail "$url does not contain '$needle'" fi } check_header() { local url="$1" header="$2" headers headers="$(curl -sSI --max-time 30 "$url" || true)" if printf '%s' "$headers" | grep -qi "^$header:"; then pass "$url sends $header" else fail "$url is missing the $header header" fi } printf '\n== release identity\n' if [ "$release_sha" != "unknown" ] && git -C "$root" rev-parse --verify --quiet "$release_sha" >/dev/null; then local_hash="$(cd "$root/site" && find . -type f ! -name sitemap.xml ! -path './.well-known/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)" remote_hash="$(ssh "$remote" "sudo sh -c 'cd /home/denny/stacks/desfoto/site && find . -type f ! -name sitemap.xml ! -path ./.well-known/\* -print0 | sort -z | xargs -0 sha256sum | sha256sum'" | cut -d' ' -f1)" if [ "$local_hash" = "$remote_hash" ]; then pass "deployed site tree matches the release ($local_hash)" else fail "deployed site tree differs from the release ($local_hash != $remote_hash)" fi remote_release="$(ssh "$remote" 'sudo cat /home/denny/stacks/desfoto/RELEASE' | tr -d '\r\n')" if [ "$remote_release" = "$release_sha" ]; then pass "release marker is $release_sha" else fail "release marker is '$remote_release' (want $release_sha)" fi fi printf '\n== routes\n' for route in / /fotografie/ /businessfotografie/ /portrait-und-model/ /familien-und-paare/ \ /minishootings/ /video/ /social-media/ /projekte/ /ueber/ /kontakt/ /impressum/ /datenschutz/; do check_status "$base$route" 200 done printf '\n== canonical host and redirects\n' www_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 https://www.desfoto.de/ || echo 000)" www_target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 https://www.desfoto.de/ || true)" if [ "$www_status" = "301" ] && [ "$www_target" = "https://desfoto.de/" ]; then pass "www.desfoto.de -> https://desfoto.de/ (301)" else fail "www.desfoto.de -> $www_status $www_target" fi http_target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 http://desfoto.de/ || true)" case "$http_target" in https://desfoto.de/*) pass "http://desfoto.de/ -> $http_target" ;; *) fail "http://desfoto.de/ redirects to '$http_target'" ;; esac printf '\n== content\n' check_contains "$base/" "Bilder und Filme, die nicht beliebig aussehen" check_contains "$base/impressum/" "ยง 5 DDG" check_contains "$base/impressum/" "DE462149560" check_contains "$base/datenschutz/" "keine Zugriffsprotokolle" check_contains "$base/datenschutz/" "youtube-nocookie.com" check_contains "$base/video/" "NWWFTf7l8g0" check_contains "$base/sitemap.xml" "https://desfoto.de/video/" printf '\n== error handling and metadata\n' check_status "$base/diese-seite-gibt-es-nicht/" 404 check_contains "$base/diese-seite-gibt-es-nicht/" "Diese Seite gibt es nicht" check_status "$base/.well-known/security.txt" 200 check_status "$base/robots.txt" 200 printf '\n== assets\n' check_status "$base/assets/img/hero-studio-1200.webp" 200 check_status "$base/assets/img/favicon-32.png" 200 check_status "$base/assets/site.css" 200 check_status "$base/assets/fonts/fraunces-latin.woff2" 200 check_status "$base/assets/img/og-desfoto.jpg" 200 printf '\n== privacy promises and headers\n' check_header "$base/" "Strict-Transport-Security" check_header "$base/" "Content-Security-Policy" check_header "$base/" "X-Content-Type-Options" check_header "$base/" "Referrer-Policy" if curl -sSI --max-time 30 "$base/" | grep -qi '^set-cookie:'; then fail "a cookie is set on the homepage" else pass "no Set-Cookie on the homepage" fi printf '\n== neighbouring sites untouched\n' check_status "https://www.dennyschulz.de/" 200 check_contains "https://www.dennyschulz.de/impressum" "Denny Schulz" check_status "https://dennyapp.de/" 200 printf '\n== TLS\n' if command -v openssl >/dev/null 2>&1; then if echo | openssl s_client -servername desfoto.de -connect desfoto.de:443 2>/dev/null | openssl x509 -noout -checkend 604800 -subject 2>/dev/null; then pass "certificate for desfoto.de is valid for at least 7 more days" else fail "certificate for desfoto.de is missing or expires within 7 days" fi fi if [ "$failures" -ne 0 ]; then printf '\nVERIFY FAIL (%d problem(s)) release=%s\n' "$failures" "$release_sha" >&2 exit 1 fi printf '\nVERIFY PASS release=%s\n' "$release_sha"