#!/usr/bin/env bash # This repository is public, and server/ describes real infrastructure. The # rule that makes that safe has three parts, and each is pinned here because # each fails silently: # # 1. No tracked file under server/ carries a secret. Compose files reference # secrets as ${VAR} interpolations resolved from the .env beside them on # the machine; .env.example names the variables with CHANGE_ME in place # of every value that matters. # 2. .gitignore keeps .env and data/ out of server/containers/ even when a # cutover or a mistake puts one there. The live files belong in # ~/Server//, outside the checkout entirely -- the ignore is a # seatbelt, and a seatbelt that got deleted should fail loudly. # 3. Nothing named .env is tracked, full stop. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" server_dir="$repo_dir/server" findings=() note() { findings+=("$1"); } [[ -d "$server_dir" ]] || { printf 'compose secrets contract: no server/ directory\n' >&2; exit 1; } # ── 1. Scanner fixtures and tracked content ────────────────────────────────── scanner="$repo_dir/tests/server/scan-tracked-secrets.py" fixtures_dir="$repo_dir/tests/server/fixtures/secrets" if ! python3 "$scanner" "$fixtures_dir/clean" compose.yml .env.example README.md; then note 'the clean secret-scanning fixture was rejected' fi expect_leak() { local fixture="$1" local expected="$2" local output local status output="$(python3 "$scanner" "$fixtures_dir/leaked" "$fixture" 2>&1)" status=$? if (( status != 1 )); then note "the leaked $fixture fixture exited $status instead of 1" elif [[ "$output" != "$expected" ]]; then note "the leaked $fixture fixture reported '$output' instead of '$expected'" fi } expect_leak compose.yml 'compose.yml:4: POSTGRES_PASSWORD' expect_leak .env.example '.env.example:1: API_TOKEN' expect_leak plain-list.yml 'plain-list.yml:4: API_TOKEN' expect_leak quoted-mapping.yml 'quoted-mapping.yml:4: API_TOKEN' expect_leak quoted-list.yml 'quoted-list.yml:4: API_TOKEN' mapfile -t tracked_server_files < <(git -C "$repo_dir" ls-files 'server/**' 'server/*') if ! output="$(python3 "$scanner" "$repo_dir" "${tracked_server_files[@]}" 2>&1)"; then while IFS= read -r hit; do [[ -n "$hit" ]] && note "$hit" done <<< "$output" fi # ── 2. The ignore still stands ─────────────────────────────────────────────── # # check-ignore consults the real gitignore for a path that need not exist, so # this asserts the rule rather than the current absence of violations. git -C "$repo_dir" check-ignore -q 'server/containers/AnyService/.env' \ || note '.gitignore no longer covers .env under server/containers/' git -C "$repo_dir" check-ignore -q 'server/containers/AnyService/data/dump.sql' \ || note '.gitignore no longer covers data/ under server/containers/' # ── 3. No .env is tracked ──────────────────────────────────────────────────── while IFS= read -r tracked; do note "a live .env is tracked: $tracked" done < <(git -C "$repo_dir" ls-files 'server/**/.env' 'server/.env') # ── Report ─────────────────────────────────────────────────────────────────── if (( ${#findings[@]} > 0 )); then mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u) printf 'compose secrets contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'compose secrets contract: PASS\n'