1958 lines
83 KiB
Bash
Executable File
1958 lines
83 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
|
cd "$ROOT"
|
|
|
|
SHELLCHECK_IMAGE="koalaman/shellcheck-alpine:v0.11.0@sha256:9955be09ea7f0dbf7ae942ac1f2094355bb30d96fffba0ec09f5432207544002"
|
|
HADOLINT_IMAGE="hadolint/hadolint:v2.14.0-debian@sha256:158cd0184dcaa18bd8ec20b61f4c1cabdf8b32a592d062f57bdcb8e4c1d312e2"
|
|
ACTIONLINT_IMAGE="rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667"
|
|
TRIVY_IMAGE="aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969"
|
|
PROMETHEUS_IMAGE="quay.io/prometheus/prometheus:v3.13.1@sha256:3c42b892cf723fa54d2f262c37a0e1f80aa8c8ddb1da7b9b0df9455a35a7f893"
|
|
ALERTMANAGER_IMAGE="quay.io/prometheus/alertmanager:v0.33.1@sha256:9e082985f56f4c8c9f724e18f2288c6708f472e56a5286b8863d080434ea065d"
|
|
PYTHON_IMAGE="python:3.14.6-alpine3.23@sha256:b165067c5afc37fa5608a3c05609cc3d51aafd808a30fbfd822ee594fef55ad4"
|
|
|
|
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
|
|
project="wnh_quality_$(printf '%s' "$run_id" | tr -d '-')"
|
|
quality_image="who-need-help:quality-$run_id"
|
|
assets_image="who-need-help:assets-audit-$run_id"
|
|
e2e_image="who-need-help:e2e-audit-$run_id"
|
|
release_image="who-need-help:security-$run_id"
|
|
backup_image="who-need-help:backup-audit-$run_id"
|
|
minio_image="who-need-help:minio-audit-$run_id"
|
|
mc_image="who-need-help:mc-audit-$run_id"
|
|
boundary_mock_image="who-need-help:boundary-mock-audit-$run_id"
|
|
socket_proxy_image="who-need-help:socket-proxy-audit-$run_id"
|
|
postgis_image="who-need-help:postgis-audit-$run_id"
|
|
caddy_image="who-need-help:caddy-audit-$run_id"
|
|
traefik_image="who-need-help:traefik-audit-$run_id"
|
|
mailpit_image="who-need-help:mailpit-audit-$run_id"
|
|
socket_proxy_container="wnh-socket-proxy-audit-$run_id"
|
|
scan_dir=$(mktemp -d "${TMPDIR:-/tmp}/wnh-quality-scan.XXXXXX")
|
|
scan_list="${scan_dir}.files"
|
|
scan_tar="${scan_dir}.tar"
|
|
android_fingerprint_probe_dir="$ROOT/android/app/src/main/assets"
|
|
android_fingerprint_probe="$android_fingerprint_probe_dir/.quality-source-fingerprint-$run_id"
|
|
android_metadata_probe="$ROOT/android/play-store/.quality-metadata-$run_id.md"
|
|
|
|
umask 077
|
|
QUALITY_POSTGRES_USER="wnh_quality_$(openssl rand -hex 6)"
|
|
QUALITY_POSTGRES_PASSWORD=$(openssl rand -base64 48 | tr -d '\n')
|
|
export QUALITY_POSTGRES_USER QUALITY_POSTGRES_PASSWORD
|
|
QUALITY_POSTGIS_IMAGE=$postgis_image
|
|
export QUALITY_POSTGIS_IMAGE
|
|
|
|
compose="docker compose -p $project -f $ROOT/compose.quality.yaml"
|
|
|
|
cleanup() {
|
|
trap - EXIT HUP INT TERM
|
|
$compose down --volumes --remove-orphans >/dev/null 2>&1 || true
|
|
docker rm --force "$socket_proxy_container" >/dev/null 2>&1 || true
|
|
docker image rm "$quality_image" "$assets_image" "$e2e_image" "$release_image" \
|
|
"$backup_image" "$minio_image" "$mc_image" \
|
|
"$boundary_mock_image" "$socket_proxy_image" "$postgis_image" \
|
|
"$caddy_image" "$traefik_image" "$mailpit_image" \
|
|
>/dev/null 2>&1 || true
|
|
rm -f "$android_fingerprint_probe"
|
|
rmdir "$android_fingerprint_probe_dir" >/dev/null 2>&1 || true
|
|
rm -f "$android_metadata_probe"
|
|
rm -rf "$scan_dir" "$scan_list" "$scan_tar"
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
scan_image() {
|
|
image=$1
|
|
scan_image_sequence=$((scan_image_sequence + 1))
|
|
image_archive="$scan_dir/trivy-image-$scan_image_sequence.tar"
|
|
|
|
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
|
docker pull "$image" >/dev/null
|
|
fi
|
|
|
|
docker image save --output "$image_archive" "$image"
|
|
docker run --rm \
|
|
--volume "$scan_dir:/scan:ro" \
|
|
--volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \
|
|
"$TRIVY_IMAGE" image \
|
|
--input "/scan/$(basename "$image_archive")" \
|
|
--scanners vuln \
|
|
--severity HIGH,CRITICAL \
|
|
--ignore-unfixed \
|
|
--exit-code 1
|
|
rm -f "$image_archive"
|
|
}
|
|
|
|
scan_image_sequence=0
|
|
|
|
echo "Checking the development kind runtime Secret allowlist"
|
|
kind_runtime_env="$scan_dir/kind-runtime.env"
|
|
kind_runtime_rendered="$scan_dir/kind-runtime.rendered"
|
|
cat >"$kind_runtime_env" <<'EOF'
|
|
DATABASE_URL=must-not-be-copied
|
|
GOOGLE_OAUTH_CLIENT_ID=quality-google-id
|
|
GOOGLE_OAUTH_CLIENT_SECRET="quality-google-secret"
|
|
GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-android-id
|
|
WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public
|
|
FCM_PROJECT_ID=
|
|
EOF
|
|
chmod 600 "$kind_runtime_env"
|
|
./scripts/sync-kind-runtime-secret.sh \
|
|
"$kind_runtime_env" --render-only "$kind_runtime_rendered" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-google-id' \
|
|
"$kind_runtime_rendered" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-google-secret' \
|
|
"$kind_runtime_rendered" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-android-id' \
|
|
"$kind_runtime_rendered" >/dev/null
|
|
grep -Fx 'WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public' \
|
|
"$kind_runtime_rendered" >/dev/null
|
|
if grep -Eq '^(DATABASE_URL|FCM_PROJECT_ID)=' "$kind_runtime_rendered"; then
|
|
echo "Kind runtime Secret renderer copied an unmanaged or empty value." >&2
|
|
exit 1
|
|
fi
|
|
printf '%s\n' 'GOOGLE_OAUTH_CLIENT_ID=duplicate' >>"$kind_runtime_env"
|
|
if ./scripts/sync-kind-runtime-secret.sh \
|
|
"$kind_runtime_env" --render-only "$kind_runtime_rendered" >/dev/null 2>&1; then
|
|
echo "Kind runtime Secret renderer accepted a duplicate managed key." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Checking shell scripts with ShellCheck 0.11.0"
|
|
# Word splitting is intentional: find emits repository-controlled paths and
|
|
# ShellCheck expects each file as a separate argument.
|
|
# shellcheck disable=SC2046
|
|
docker run --rm \
|
|
--volume "$ROOT:/mnt:ro" \
|
|
--workdir /mnt \
|
|
--entrypoint shellcheck \
|
|
"$SHELLCHECK_IMAGE" \
|
|
$(find scripts -type f -name '*.sh' -print | sort)
|
|
|
|
echo "Checking production restore PostgreSQL readiness"
|
|
bash test/scripts/production_offsite_backup_readiness_test.sh
|
|
|
|
echo "Checking the production read-only load safety boundary"
|
|
./scripts/production-readonly-load-drill.sh
|
|
|
|
echo "Checking isolated production application rollback plan/apply"
|
|
./scripts/production-rollback-drill.sh
|
|
|
|
echo "Checking release migration compatibility policy"
|
|
./scripts/check-migration-registry.sh
|
|
./scripts/release-migration-policy-drill.sh
|
|
|
|
echo "Checking isolated production release orchestration"
|
|
./scripts/production-release-drill.sh
|
|
|
|
echo "Checking Dockerfiles with Hadolint 2.14.0"
|
|
for dockerfile in Dockerfile Dockerfile.backup Dockerfile.minio \
|
|
Dockerfile.postgis Dockerfile.socket-proxy Dockerfile.traefik \
|
|
Dockerfile.caddy Dockerfile.mailpit \
|
|
android/Dockerfile e2e/Dockerfile ops/external-boundaries/Dockerfile; do
|
|
docker run --rm --interactive "$HADOLINT_IMAGE" \
|
|
hadolint --failure-threshold warning - <"$dockerfile"
|
|
done
|
|
|
|
echo "Checking the GitHub and Gitea Actions workflows with actionlint 1.7.12"
|
|
docker run --rm \
|
|
--volume "$ROOT:/repo:ro" \
|
|
--workdir /repo \
|
|
"$ACTIONLINT_IMAGE" \
|
|
-config-file .github/actionlint.yaml \
|
|
.github/workflows/quality.yml \
|
|
.gitea/workflows/quality.yml
|
|
|
|
echo "Checking crash dumps are excluded from the Docker build context"
|
|
grep -Fx 'core' .dockerignore >/dev/null
|
|
grep -Fx 'core.*' .dockerignore >/dev/null
|
|
|
|
echo "Checking local Gitea runner state is excluded from Git and Docker contexts"
|
|
grep -Fx '/.runner' .gitignore >/dev/null
|
|
grep -Fx '/act_runner' .gitignore >/dev/null
|
|
grep -Fx '/act_runner-data/' .gitignore >/dev/null
|
|
grep -Fx '/.runner' .dockerignore >/dev/null
|
|
grep -Fx '/act_runner' .dockerignore >/dev/null
|
|
grep -Fx '/act_runner-data/' .dockerignore >/dev/null
|
|
|
|
echo "Checking Android artifacts are bound to their exact source tree"
|
|
android_fingerprint_before=$(./scripts/android-source-fingerprint.sh)
|
|
android_artifact_probe="$scan_dir/android-artifact"
|
|
mkdir "$android_artifact_probe"
|
|
printf '%s\n' "$android_fingerprint_before" \
|
|
>"$android_artifact_probe/source-fingerprint.sha256"
|
|
./scripts/verify-android-artifact-source.sh \
|
|
"$android_artifact_probe" \
|
|
scripts/android-development-build.sh >/dev/null
|
|
|
|
printf '%s\n' 'quality store metadata mutation' >"$android_metadata_probe"
|
|
if [ "$(./scripts/android-source-fingerprint.sh)" != "$android_fingerprint_before" ]; then
|
|
echo "Android source fingerprint changed for Play Store metadata." >&2
|
|
exit 1
|
|
fi
|
|
./scripts/verify-android-artifact-source.sh \
|
|
"$android_artifact_probe" \
|
|
scripts/android-development-build.sh >/dev/null
|
|
rm -f "$android_metadata_probe"
|
|
|
|
mkdir -p "$android_fingerprint_probe_dir"
|
|
printf '%s\n' 'quality source mutation' >"$android_fingerprint_probe"
|
|
android_fingerprint_after=$(./scripts/android-source-fingerprint.sh)
|
|
if [ "$android_fingerprint_before" = "$android_fingerprint_after" ]; then
|
|
echo "Android source fingerprint did not change for a source mutation." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/verify-android-artifact-source.sh \
|
|
"$android_artifact_probe" \
|
|
scripts/android-development-build.sh >/dev/null 2>&1; then
|
|
echo "Android artifact verifier accepted stale source inputs." >&2
|
|
exit 1
|
|
fi
|
|
|
|
rm -f "$android_fingerprint_probe"
|
|
rmdir "$android_fingerprint_probe_dir" >/dev/null 2>&1 || true
|
|
test "$(./scripts/android-source-fingerprint.sh)" = "$android_fingerprint_before"
|
|
printf '%s\n' malformed >"$android_artifact_probe/source-fingerprint.sha256"
|
|
if ./scripts/verify-android-artifact-source.sh \
|
|
"$android_artifact_probe" \
|
|
scripts/android-development-build.sh >/dev/null 2>&1; then
|
|
echo "Android artifact verifier accepted a malformed fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
rm -f "$android_artifact_probe/source-fingerprint.sha256"
|
|
if ./scripts/verify-android-artifact-source.sh \
|
|
"$android_artifact_probe" \
|
|
scripts/android-development-build.sh >/dev/null 2>&1; then
|
|
echo "Android artifact verifier accepted a missing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
unset android_fingerprint_before android_fingerprint_after
|
|
|
|
echo "Checking atomic environment credential imports"
|
|
credential_env="$scan_dir/credentials.env"
|
|
cp .env.example "$credential_env"
|
|
chmod 600 "$credential_env"
|
|
|
|
credential_values="$scan_dir/credential-values"
|
|
printf '%s\n' \
|
|
'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' \
|
|
'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' \
|
|
"SMTP_PASSWORD=quality\$literal" >"$credential_values"
|
|
chmod 600 "$credential_values"
|
|
credential_output=$(
|
|
./scripts/set-env-values.sh "$credential_env" "$credential_values"
|
|
)
|
|
if printf '%s' "$credential_output" | grep -F 'quality-imported-secret' >/dev/null; then
|
|
echo "Environment updater printed a secret value." >&2
|
|
exit 1
|
|
fi
|
|
test "$(stat -c '%a' "$credential_env")" = 600
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-imported-client' "$credential_env" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-imported-secret' "$credential_env" >/dev/null
|
|
grep -Fx "SMTP_PASSWORD=quality\$\$literal" "$credential_env" >/dev/null
|
|
|
|
compose_env_probe="$scan_dir/compose-env-probe.yaml"
|
|
printf '%s\n' \
|
|
'services:' \
|
|
' probe:' \
|
|
" image: $SHELLCHECK_IMAGE" \
|
|
' network_mode: none' \
|
|
' entrypoint: ["/usr/bin/env"]' \
|
|
' environment:' \
|
|
" SMTP_PASSWORD: \${SMTP_PASSWORD}" \
|
|
>"$compose_env_probe"
|
|
resolved_smtp_password=$(
|
|
docker compose \
|
|
--project-name "$project" \
|
|
--env-file "$credential_env" \
|
|
--file "$compose_env_probe" \
|
|
run --rm --no-deps probe |
|
|
awk -F= '
|
|
$1 == "SMTP_PASSWORD" {
|
|
print substr($0, index($0, "=") + 1)
|
|
exit
|
|
}
|
|
'
|
|
)
|
|
test "$resolved_smtp_password" = "quality\$literal"
|
|
|
|
duplicate_env="$scan_dir/credentials-duplicate.env"
|
|
cp "$credential_env" "$duplicate_env"
|
|
printf '%s\n' 'GOOGLE_OAUTH_CLIENT_ID=duplicate' >>"$duplicate_env"
|
|
if ./scripts/set-env-values.sh \
|
|
"$duplicate_env" "$credential_values" >/dev/null 2>&1; then
|
|
echo "Environment updater accepted a duplicate target key." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Checking single-file environment template synchronization"
|
|
sync_template="$scan_dir/sync-template.env.example"
|
|
sync_env="$scan_dir/sync.env"
|
|
printf '%s\n' \
|
|
'# Template comment' \
|
|
'FIRST_VALUE=template-default' \
|
|
'SECOND_VALUE=' \
|
|
>"$sync_template"
|
|
printf '%s\n' \
|
|
'SECOND_VALUE=preserve-this-value' \
|
|
'LOCAL_ONLY_VALUE=preserve-local-key' \
|
|
'FIRST_VALUE=preserve-first-value' \
|
|
>"$sync_env"
|
|
chmod 600 "$sync_env"
|
|
sync_output=$(
|
|
./scripts/sync-env-template.sh "$sync_env" "$sync_template"
|
|
)
|
|
if printf '%s' "$sync_output" | grep -F 'preserve-this-value' >/dev/null; then
|
|
echo "Environment synchronizer printed an environment value." >&2
|
|
exit 1
|
|
fi
|
|
test "$(stat -c '%a' "$sync_env")" = 600
|
|
grep -Fx '# Template comment' "$sync_env" >/dev/null
|
|
grep -Fx 'FIRST_VALUE=preserve-first-value' "$sync_env" >/dev/null
|
|
grep -Fx 'SECOND_VALUE=preserve-this-value' "$sync_env" >/dev/null
|
|
grep -Fx 'LOCAL_ONLY_VALUE=preserve-local-key' "$sync_env" >/dev/null
|
|
test "$(grep -Fc 'FIRST_VALUE=' "$sync_env")" = 1
|
|
test "$(grep -Fc 'SECOND_VALUE=' "$sync_env")" = 1
|
|
test "$(grep -Fc 'LOCAL_ONLY_VALUE=' "$sync_env")" = 1
|
|
sync_hash=$(sha256sum "$sync_env" | awk '{print $1}')
|
|
./scripts/sync-env-template.sh "$sync_env" "$sync_template" >/dev/null
|
|
test "$(sha256sum "$sync_env" | awk '{print $1}')" = "$sync_hash"
|
|
|
|
sync_duplicate="$scan_dir/sync-duplicate.env"
|
|
printf '%s\n' \
|
|
'FIRST_VALUE=one' \
|
|
'FIRST_VALUE=two' \
|
|
>"$sync_duplicate"
|
|
chmod 600 "$sync_duplicate"
|
|
if ./scripts/sync-env-template.sh \
|
|
"$sync_duplicate" "$sync_template" >/dev/null 2>&1; then
|
|
echo "Environment synchronizer accepted duplicate source keys." >&2
|
|
exit 1
|
|
fi
|
|
|
|
google_client="$scan_dir/google-oauth-client.json"
|
|
printf '%s\n' \
|
|
'{"web":{"client_id":"quality-google-client","project_id":"quality-development","client_secret":"quality-google-secret","redirect_uris":["https://dev.help.test/auth/google/callback"]}}' \
|
|
>"$google_client"
|
|
chmod 600 "$google_client"
|
|
sed -i 's|^WNH_BASE_URL=.*|WNH_BASE_URL=https://dev.help.test|' "$credential_env"
|
|
oauth_output=$(
|
|
./scripts/import-google-oauth-client.sh "$credential_env" "$google_client"
|
|
)
|
|
if printf '%s' "$oauth_output" | grep -F 'quality-google-secret' >/dev/null; then
|
|
echo "Google OAuth importer printed a client secret." >&2
|
|
exit 1
|
|
fi
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-google-client' "$credential_env" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_SECRET=quality-google-secret' "$credential_env" >/dev/null
|
|
|
|
firebase_client="$scan_dir/google-services.json"
|
|
printf '%s\n' \
|
|
'{"project_info":{"project_number":"123456789","project_id":"quality-development"},"client":[{"client_info":{"mobilesdk_app_id":"1:123456789:android:quality","android_client_info":{"package_name":"org.whoneedhelp.mobile.staging"}},"api_key":[{"current_key":"quality-firebase-api-key"}]}]}' \
|
|
>"$firebase_client"
|
|
sed -i \
|
|
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
|
"$credential_env"
|
|
./scripts/import-firebase-android-config.sh \
|
|
"$credential_env" "$firebase_client" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality' "$credential_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_API_KEY=quality-firebase-api-key' "$credential_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-development' "$credential_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$credential_env" >/dev/null
|
|
|
|
firebase_mismatched_client="$scan_dir/google-services-mismatched.json"
|
|
printf '%s\n' \
|
|
'{"project_info":{"project_number":"123456789","project_id":"quality-development"},"client":[{"client_info":{"mobilesdk_app_id":"1:987654321:android:quality","android_client_info":{"package_name":"org.whoneedhelp.mobile.staging"}},"api_key":[{"current_key":"quality-firebase-api-key"}]}]}' \
|
|
>"$firebase_mismatched_client"
|
|
if ./scripts/import-firebase-android-config.sh \
|
|
"$credential_env" "$firebase_mismatched_client" >/dev/null 2>&1; then
|
|
echo "Firebase importer accepted an application ID from another project number." >&2
|
|
exit 1
|
|
fi
|
|
|
|
firebase_injected_client="$scan_dir/google-services-injected.json"
|
|
injected_firebase_project=$(
|
|
printf 'quality-development\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
|
)
|
|
jq --null-input \
|
|
--arg project_id "$injected_firebase_project" \
|
|
'{
|
|
project_info: {
|
|
project_number: "123456789",
|
|
project_id: $project_id
|
|
},
|
|
client: [
|
|
{
|
|
client_info: {
|
|
mobilesdk_app_id: "1:123456789:android:quality",
|
|
android_client_info: {
|
|
package_name: "org.whoneedhelp.mobile.staging"
|
|
}
|
|
},
|
|
api_key: [
|
|
{
|
|
current_key: "quality-firebase-api-key"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}' >"$firebase_injected_client"
|
|
credential_hash=$(sha256sum "$credential_env" | awk '{print $1}')
|
|
if ./scripts/import-firebase-android-config.sh \
|
|
"$credential_env" "$firebase_injected_client" >/dev/null 2>&1; then
|
|
echo "Firebase importer accepted a line-breaking project ID." >&2
|
|
exit 1
|
|
fi
|
|
test "$(sha256sum "$credential_env" | awk '{print $1}')" = "$credential_hash"
|
|
|
|
echo "Checking production Play Android identity import"
|
|
play_env="$scan_dir/play-production.env"
|
|
cp .env.example "$play_env"
|
|
chmod 600 "$play_env"
|
|
play_upload_sha256=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
|
play_sha1_one=1111111111111111111111111111111111111111
|
|
play_sha1_two=2222222222222222222222222222222222222222
|
|
play_sha256_one=D7C4F1124DF468E5B354DED896E801512941F18A710C18B0E798AA2B81DA11DF
|
|
play_sha256_two=A5742BAE70C6D034E37544B62E37A375C0E005647450F40F29B2A984F9FDB8FB
|
|
play_upload_colon=$(printf '%s' "$play_upload_sha256" | sed 's/../&:/g; s/:$//')
|
|
play_sha256_one_colon=$(printf '%s' "$play_sha256_one" | sed 's/../&:/g; s/:$//')
|
|
play_sha256_two_colon=$(printf '%s' "$play_sha256_two" | sed 's/../&:/g; s/:$//')
|
|
sed -i \
|
|
-e 's|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=production|' \
|
|
-e 's|^PHX_HOST=.*|PHX_HOST=help.test|' \
|
|
-e 's|^PHX_SCHEME=.*|PHX_SCHEME=https|' \
|
|
-e 's|^PHX_URL_PORT=.*|PHX_URL_PORT=443|' \
|
|
-e 's|^WNH_BASE_URL=.*|WNH_BASE_URL=https://help.test|' \
|
|
-e 's|^GOOGLE_OAUTH_CLIENT_ID=.*|GOOGLE_OAUTH_CLIENT_ID=quality-production-web-client|' \
|
|
-e 's|^GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=.*|GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-upload-android-client|' \
|
|
-e 's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile|' \
|
|
-e "s|^ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=.*|ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$play_upload_colon|" \
|
|
-e 's|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=|' \
|
|
-e 's|^WNH_FIREBASE_PROJECT_ID=.*|WNH_FIREBASE_PROJECT_ID=quality-production|' \
|
|
-e 's|^FCM_PROJECT_ID=.*|FCM_PROJECT_ID=quality-production|' \
|
|
"$play_env"
|
|
|
|
play_google_services="$scan_dir/play-google-services.json"
|
|
jq --null-input \
|
|
--arg sha1_one "$play_sha1_one" \
|
|
--arg sha1_two "$play_sha1_two" \
|
|
'{
|
|
project_info: {
|
|
project_number: "987654321",
|
|
project_id: "quality-production"
|
|
},
|
|
client: [
|
|
{
|
|
client_info: {
|
|
mobilesdk_app_id: "1:987654321:android:quality-production",
|
|
android_client_info: {package_name: "org.whoneedhelp.mobile"}
|
|
},
|
|
oauth_client: [
|
|
{
|
|
client_id: "quality-play-android-client-one",
|
|
client_type: 1,
|
|
android_info: {
|
|
package_name: "org.whoneedhelp.mobile",
|
|
certificate_hash: $sha1_one
|
|
}
|
|
},
|
|
{
|
|
client_id: "quality-play-android-client-two",
|
|
client_type: 1,
|
|
android_info: {
|
|
package_name: "org.whoneedhelp.mobile",
|
|
certificate_hash: $sha1_two
|
|
}
|
|
}
|
|
],
|
|
api_key: [{current_key: "quality-production-firebase-api-key"}]
|
|
}
|
|
]
|
|
}' >"$play_google_services"
|
|
chmod 600 "$play_google_services"
|
|
|
|
play_identities="$scan_dir/play-identities.json"
|
|
jq --null-input \
|
|
--arg sha1_one "$play_sha1_one" \
|
|
--arg sha1_two "$play_sha1_two" \
|
|
--arg sha256_one "$play_sha256_one" \
|
|
--arg sha256_two "$play_sha256_two" \
|
|
'{
|
|
package_name: "org.whoneedhelp.mobile",
|
|
identities: [
|
|
{sha1: $sha1_one, sha256: $sha256_one},
|
|
{sha1: $sha1_two, sha256: $sha256_two}
|
|
]
|
|
}' >"$play_identities"
|
|
chmod 600 "$play_identities"
|
|
|
|
play_hash_before=$(sha256sum "$play_env" | awk '{print $1}')
|
|
play_plan_output=$(
|
|
./scripts/import-play-android-config.sh \
|
|
"$play_env" "$play_google_services" "$play_identities"
|
|
)
|
|
test "$(sha256sum "$play_env" | awk '{print $1}')" = "$play_hash_before"
|
|
printf '%s' "$play_plan_output" | grep -F 'Plan only: no files were changed.' >/dev/null
|
|
if printf '%s' "$play_plan_output" |
|
|
grep -E 'quality-production-firebase-api-key|quality-play-android-client' >/dev/null; then
|
|
echo "Play Android identity plan printed a provider value." >&2
|
|
exit 1
|
|
fi
|
|
|
|
./scripts/import-play-android-config.sh \
|
|
"$play_env" "$play_google_services" "$play_identities" --apply >/dev/null
|
|
grep -Fx \
|
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$play_upload_colon,$play_sha256_one_colon,$play_sha256_two_colon" \
|
|
"$play_env" >/dev/null
|
|
grep -Fx \
|
|
"ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=$play_sha256_one_colon,$play_sha256_two_colon" \
|
|
"$play_env" >/dev/null
|
|
grep -Fx \
|
|
'GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-upload-android-client,quality-play-android-client-one,quality-play-android-client-two' \
|
|
"$play_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality-production' \
|
|
"$play_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_API_KEY=quality-production-firebase-api-key' \
|
|
"$play_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-production' "$play_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=987654321' "$play_env" >/dev/null
|
|
|
|
play_hash_after=$(sha256sum "$play_env" | awk '{print $1}')
|
|
./scripts/import-play-android-config.sh \
|
|
"$play_env" "$play_google_services" "$play_identities" --apply >/dev/null
|
|
test "$(sha256sum "$play_env" | awk '{print $1}')" = "$play_hash_after"
|
|
|
|
play_bad_identities="$scan_dir/play-identities-unmatched.json"
|
|
jq '.identities[0].sha1 = "3333333333333333333333333333333333333333"' \
|
|
"$play_identities" >"$play_bad_identities"
|
|
chmod 600 "$play_bad_identities"
|
|
if ./scripts/import-play-android-config.sh \
|
|
"$play_env" "$play_google_services" "$play_bad_identities" --apply \
|
|
>/dev/null 2>&1; then
|
|
echo "Play Android identity import accepted an unmatched Play SHA-1." >&2
|
|
exit 1
|
|
fi
|
|
test "$(sha256sum "$play_env" | awk '{print $1}')" = "$play_hash_after"
|
|
|
|
echo "Checking Google Play installed Android verification"
|
|
fake_play_adb="$scan_dir/fake-play-adb"
|
|
cat >"$fake_play_adb" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
if [[ ${1:-} != -s || ${2:-} != quality-play-device ]]; then
|
|
echo "unexpected device selection" >&2
|
|
exit 1
|
|
fi
|
|
shift 2
|
|
|
|
case "${1:-} ${2:-} ${3:-}" in
|
|
"get-state ")
|
|
printf 'device\n'
|
|
;;
|
|
"shell pm path")
|
|
printf 'package:/data/app/quality/base.apk\n'
|
|
;;
|
|
"shell dumpsys package")
|
|
cat <<REPORT
|
|
Package [org.whoneedhelp.mobile] (quality):
|
|
versionCode=${FAKE_PLAY_VERSION_CODE:-1} minSdk=24 targetSdk=37
|
|
versionName=${FAKE_PLAY_VERSION_NAME:-0.1.0}
|
|
installerPackageName=${FAKE_PLAY_INSTALLER:-com.android.vending}
|
|
REPORT
|
|
;;
|
|
"shell pm get-app-links")
|
|
cat <<REPORT
|
|
org.whoneedhelp.mobile:
|
|
Signatures: [${FAKE_PLAY_SIGNATURE:?Set FAKE_PLAY_SIGNATURE}]
|
|
Domain verification state:
|
|
whoneedhelp.com: ${FAKE_PLAY_DOMAIN_STATE:-verified}
|
|
REPORT
|
|
;;
|
|
"shell cmd package")
|
|
if [[ " $* " != *" -c android.intent.category.DEFAULT "* ]] ||
|
|
[[ " $* " != *" -c android.intent.category.BROWSABLE "* ]]; then
|
|
echo "App Link resolution omitted a required intent category." >&2
|
|
exit 1
|
|
fi
|
|
if [[ " $* " == *"/auth/google/callback"* ]]; then
|
|
printf '%s\n' "${FAKE_PLAY_CALLBACK_ACTIVITY:-com.android.browser/.BrowserActivity}"
|
|
else
|
|
printf '%s\n' "${FAKE_PLAY_ACTIVITY:-org.whoneedhelp.mobile/.MainActivity}"
|
|
fi
|
|
;;
|
|
*)
|
|
printf 'unexpected adb command: %s\n' "$*" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
EOF
|
|
chmod 700 "$fake_play_adb"
|
|
|
|
play_device_output=$(
|
|
WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_sha256_two_colon" \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0
|
|
)
|
|
printf '%s' "$play_device_output" |
|
|
grep -F 'Google Play installed Android verification passed.' >/dev/null
|
|
if printf '%s' "$play_device_output" |
|
|
grep -F "$play_sha256_two_colon" >/dev/null; then
|
|
echo "Play-installed verifier printed a signing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_sha256_two_colon" \
|
|
FAKE_PLAY_INSTALLER=null \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
|
|
echo "Play-installed verifier accepted a sideloaded package." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_upload_colon" \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
|
|
echo "Play-installed verifier accepted the upload certificate as a Play identity." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_sha256_one_colon" \
|
|
FAKE_PLAY_DOMAIN_STATE=none \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
|
|
echo "Play-installed verifier accepted an unverified production App Link." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_sha256_one_colon" \
|
|
FAKE_PLAY_ACTIVITY=com.android.browser/.BrowserActivity \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
|
|
echo "Play-installed verifier accepted browser App Link resolution." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if WNH_ADB_BIN="$fake_play_adb" \
|
|
FAKE_PLAY_SIGNATURE="$play_sha256_one_colon" \
|
|
FAKE_PLAY_CALLBACK_ACTIVITY=org.whoneedhelp.mobile/.MainActivity \
|
|
./scripts/verify-play-installed-android.sh \
|
|
"$play_identities" quality-play-device 1 0.1.0 >/dev/null 2>&1; then
|
|
echo "Play-installed verifier accepted an Android-claimed browser OAuth callback." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Checking Android environment isolation"
|
|
./scripts/android-play-policy-check.sh >/dev/null
|
|
android_fingerprint=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF
|
|
android_env="$scan_dir/android-development.env"
|
|
printf '%s\n' \
|
|
'DEPLOYMENT_ENV=development' \
|
|
'PHX_HOST=dev.help.test' \
|
|
'PHX_SCHEME=https' \
|
|
'PHX_URL_PORT=443' \
|
|
'WNH_BASE_URL=https://dev.help.test' \
|
|
'GOOGLE_OAUTH_CLIENT_ID=quality-web-client' \
|
|
'GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-android-client' \
|
|
'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.development' \
|
|
"ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=$android_fingerprint" \
|
|
'ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=' \
|
|
>"$android_env"
|
|
chmod 600 "$android_env"
|
|
./scripts/validate-android-environment.sh \
|
|
"$android_env" development >/dev/null
|
|
|
|
android_missing_authorized_party_env="$scan_dir/android-missing-authorized-party.env"
|
|
grep -v '^GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=' \
|
|
"$android_env" >"$android_missing_authorized_party_env"
|
|
chmod 600 "$android_missing_authorized_party_env"
|
|
if ./scripts/validate-android-environment.sh \
|
|
"$android_missing_authorized_party_env" development >/dev/null 2>&1; then
|
|
echo "Android validation accepted an empty Google authorized-party allowlist." >&2
|
|
exit 1
|
|
fi
|
|
|
|
sed -i \
|
|
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
|
"$android_env"
|
|
if ./scripts/validate-android-environment.sh \
|
|
"$android_env" development >/dev/null 2>&1; then
|
|
echo "Development Android validation accepted the test package." >&2
|
|
exit 1
|
|
fi
|
|
|
|
sed -i \
|
|
's|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=test|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
|
"$android_env"
|
|
./scripts/validate-android-environment.sh "$android_env" test >/dev/null
|
|
|
|
sed -i \
|
|
"s|^DEPLOYMENT_ENV=.*|DEPLOYMENT_ENV=production|; s|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile|; s|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=$android_fingerprint|" \
|
|
"$android_env"
|
|
./scripts/validate-android-environment.sh \
|
|
"$android_env" production >/dev/null
|
|
|
|
fcm_service_account="$scan_dir/fcm-service-account.json"
|
|
printf '%s\n' \
|
|
'{"type":"service_account","project_id":"quality-development","client_email":"quality-fcm@quality-development.iam.gserviceaccount.com","private_key":"quality-private-key"}' \
|
|
>"$fcm_service_account"
|
|
chmod 600 "$fcm_service_account"
|
|
fcm_output=$(
|
|
./scripts/import-fcm-service-account.sh \
|
|
"$credential_env" "$fcm_service_account"
|
|
)
|
|
if printf '%s' "$fcm_output" | grep -F 'quality-private-key' >/dev/null; then
|
|
echo "FCM importer printed a private key." >&2
|
|
exit 1
|
|
fi
|
|
grep -Fx 'FCM_PROJECT_ID=quality-development' "$credential_env" >/dev/null
|
|
grep -Fx 'FCM_SERVICE_ACCOUNT_FILE=' "$credential_env" >/dev/null
|
|
credential_fcm_base64=$(
|
|
awk -F= '
|
|
$1 == "FCM_SERVICE_ACCOUNT_JSON_BASE64" {
|
|
print substr($0, index($0, "=") + 1)
|
|
exit
|
|
}
|
|
' "$credential_env"
|
|
)
|
|
printf '%s' "$credential_fcm_base64" |
|
|
base64 -d |
|
|
jq --exit-status \
|
|
'.project_id == "quality-development" and .private_key == "quality-private-key"' \
|
|
>/dev/null
|
|
|
|
fcm_injected_service_account="$scan_dir/fcm-service-account-injected.json"
|
|
injected_fcm_project=$(
|
|
printf 'quality-development\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
|
)
|
|
jq --null-input \
|
|
--arg project_id "$injected_fcm_project" \
|
|
'{
|
|
type: "service_account",
|
|
project_id: $project_id,
|
|
client_email: "quality-fcm@example.invalid",
|
|
private_key: "quality-private-key"
|
|
}' >"$fcm_injected_service_account"
|
|
chmod 600 "$fcm_injected_service_account"
|
|
credential_hash=$(sha256sum "$credential_env" | awk '{print $1}')
|
|
if ./scripts/import-fcm-service-account.sh \
|
|
"$credential_env" "$fcm_injected_service_account" >/dev/null 2>&1; then
|
|
echo "FCM importer accepted a line-breaking project ID." >&2
|
|
exit 1
|
|
fi
|
|
test "$(sha256sum "$credential_env" | awk '{print $1}')" = "$credential_hash"
|
|
rm -f "$fcm_injected_service_account"
|
|
|
|
echo "Checking the existing load environment upgrade path"
|
|
legacy_load_env="$scan_dir/legacy-load.env"
|
|
printf '%s\n' \
|
|
'LOAD_PROJECT=who_need_help_load' \
|
|
'LOAD_WEB_REPLICAS=3' \
|
|
'SECRET_KEY_BASE=preserve-existing-secret' >"$legacy_load_env"
|
|
chmod 600 "$legacy_load_env"
|
|
WNH_LOAD_ENV_FILE="$legacy_load_env" \
|
|
./scripts/ensure-local-load-env.sh >/dev/null
|
|
test "$(stat -c '%a' "$legacy_load_env")" = 600
|
|
grep -Fx 'APP_IMAGE=who-need-help:load' "$legacy_load_env" >/dev/null
|
|
grep -Fx 'POSTGIS_IMAGE=who-need-help:postgis-load' "$legacy_load_env" >/dev/null
|
|
grep -Fx 'SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-load' "$legacy_load_env" >/dev/null
|
|
grep -Fx 'APP_TOPOLOGY=split' "$legacy_load_env" >/dev/null
|
|
grep -Fx 'LOAD_WEB_REPLICAS=3' "$legacy_load_env" >/dev/null
|
|
test "$(grep -Fc 'SECRET_KEY_BASE=preserve-existing-secret' "$legacy_load_env")" = 1
|
|
legacy_load_hash=$(sha256sum "$legacy_load_env" | awk '{print $1}')
|
|
WNH_LOAD_ENV_FILE="$legacy_load_env" \
|
|
./scripts/ensure-local-load-env.sh >/dev/null
|
|
test "$(sha256sum "$legacy_load_env" | awk '{print $1}')" = "$legacy_load_hash"
|
|
|
|
echo "Checking independent test and production environment initialization"
|
|
quality_fcm_base64=$(
|
|
printf '%s' \
|
|
'{"type":"service_account","project_id":"quality-production","client_email":"quality-fcm@quality-production.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
|
base64 -w 0
|
|
)
|
|
quality_test_fcm_base64=$(
|
|
printf '%s' \
|
|
'{"type":"service_account","project_id":"quality-test","client_email":"quality-fcm@quality-test.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
|
base64 -w 0
|
|
)
|
|
quality_other_fcm_base64=$(
|
|
printf '%s' \
|
|
'{"type":"service_account","project_id":"quality-other","client_email":"quality-fcm@quality-other.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
|
base64 -w 0
|
|
)
|
|
test_env="$scan_dir/test.env"
|
|
if ./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.missing-codex.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted a missing Codex session ID." >&2
|
|
exit 1
|
|
fi
|
|
TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
|
TEST_GOOGLE_OAUTH_CLIENT_SECRET="quality-test\$secret" \
|
|
TEST_GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-test-android-client \
|
|
TEST_WEB_PUSH_VAPID_PUBLIC_KEY=quality-test-vapid-public \
|
|
TEST_WEB_PUSH_VAPID_PRIVATE_KEY=quality-test-vapid-private \
|
|
TEST_WEB_PUSH_VAPID_SUBJECT=mailto:test@help.test \
|
|
TEST_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test \
|
|
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
|
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
|
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
TEST_FCM_PROJECT_ID=quality-test \
|
|
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_test_fcm_base64" \
|
|
TEST_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging \
|
|
TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF \
|
|
./scripts/init-test-env.sh test.help.test "$test_env" >/dev/null
|
|
test "$(stat -c '%a' "$test_env")" = 600
|
|
grep -Fx 'DEPLOYMENT_ENV=test' "$test_env" >/dev/null
|
|
grep -Fx 'COMPOSE_PROJECT_NAME=who_need_help_test' "$test_env" >/dev/null
|
|
grep -E '^APP_IMAGE=who-need-help:test-[0-9a-f]{12}$' "$test_env" >/dev/null
|
|
grep -Fx 'APP_TOPOLOGY=compact' "$test_env" >/dev/null
|
|
grep -Fx 'DATABASE_MODE=container' "$test_env" >/dev/null
|
|
grep -Fx 'POSTGRES_DB=who_need_help_test' "$test_env" >/dev/null
|
|
grep -Fx 'PUBLIC_EDGE_ENABLED=true' "$test_env" >/dev/null
|
|
grep -Fx 'PUBLIC_UPSTREAM_NAME=who-need-help-test' "$test_env" >/dev/null
|
|
grep -Fx 'PHX_HOST=test.help.test' "$test_env" >/dev/null
|
|
grep -Fx 'PHX_SCHEME=https' "$test_env" >/dev/null
|
|
grep -Fx 'PHX_URL_PORT=443' "$test_env" >/dev/null
|
|
grep -Fx 'EMAIL_DELIVERY_PROVIDER=smtp' "$test_env" >/dev/null
|
|
grep -Fx 'SMTP_RELAY=mailpit' "$test_env" >/dev/null
|
|
grep -Fx 'EMAIL_FROM_NAME="Who Need Help Test"' "$test_env" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_CLIENT_ID=quality-test-client' "$test_env" >/dev/null
|
|
grep -Fx "GOOGLE_OAUTH_CLIENT_SECRET=quality-test\$\$secret" "$test_env" >/dev/null
|
|
grep -Fx 'GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-test-android-client' \
|
|
"$test_env" >/dev/null
|
|
grep -Fx 'WEB_PUSH_VAPID_SUBJECT=mailto:test@help.test' "$test_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test' "$test_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_PROJECT_ID=quality-test' "$test_env" >/dev/null
|
|
grep -Fx 'WNH_FIREBASE_GCM_SENDER_ID=123456789' "$test_env" >/dev/null
|
|
grep -Fx 'FCM_PROJECT_ID=quality-test' "$test_env" >/dev/null
|
|
grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging' "$test_env" >/dev/null
|
|
grep -Fx 'ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF' "$test_env" >/dev/null
|
|
./scripts/validate-test-env.sh "$test_env" test.help.test >/dev/null
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.partial-google.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted partial Google OAuth credentials." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-test-env.sh test.help.test "$test_env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer overwrote an existing file." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality-test \
|
|
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
|
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
|
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.mismatched-firebase.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted a Firebase application from another sender." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_FCM_PROJECT_ID=quality-test \
|
|
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.mismatched-fcm-credential.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted an FCM service account from another project." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality-test \
|
|
TEST_WNH_FIREBASE_API_KEY=quality-test-public-client-key \
|
|
TEST_WNH_FIREBASE_PROJECT_ID=quality-test \
|
|
TEST_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
TEST_FCM_PROJECT_ID=quality-other \
|
|
TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.mismatched-firebase-fcm.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted different Firebase and FCM projects." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \
|
|
TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.production-package.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted the production Android package." >&2
|
|
exit 1
|
|
fi
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_WEB_PUSH_VAPID_PUBLIC_KEY=quality-test-vapid-public \
|
|
TEST_WEB_PUSH_VAPID_PRIVATE_KEY=quality-test-vapid-private \
|
|
TEST_WEB_PUSH_VAPID_SUBJECT=ftp://help.test \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.invalid-vapid-subject.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted an invalid VAPID subject." >&2
|
|
exit 1
|
|
fi
|
|
injected_test_secret=$(
|
|
printf 'quality-test-secret\nSMTP_PASSWORD=injected'
|
|
)
|
|
if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
TEST_GOOGLE_OAUTH_CLIENT_ID=quality-test-client \
|
|
TEST_GOOGLE_OAUTH_CLIENT_SECRET="$injected_test_secret" \
|
|
./scripts/init-test-env.sh test.help.test \
|
|
"$scan_dir/test.injected-line.env" >/dev/null 2>&1; then
|
|
echo "Test environment initializer accepted a line-breaking credential." >&2
|
|
exit 1
|
|
fi
|
|
test ! -e "$scan_dir/test.injected-line.env"
|
|
production_env="$scan_dir/production.env"
|
|
missing_codex_env="$scan_dir/production.missing-codex.env"
|
|
if PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
|
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
|
PRODUCTION_SMTP_PORT=587 \
|
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
|
PRODUCTION_SMTP_PASSWORD=quality-password \
|
|
PRODUCTION_SMTP_AUTH=always \
|
|
PRODUCTION_SMTP_TLS=always \
|
|
PRODUCTION_SMTP_SSL=false \
|
|
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|
./scripts/init-production-env.sh help.test "$missing_codex_env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted a missing Codex session ID." >&2
|
|
exit 1
|
|
fi
|
|
PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
|
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
|
PRODUCTION_SMTP_PORT=587 \
|
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
|
PRODUCTION_SMTP_PASSWORD="quality\$password" \
|
|
PRODUCTION_SMTP_AUTH=always \
|
|
PRODUCTION_SMTP_TLS=always \
|
|
PRODUCTION_SMTP_SSL=false \
|
|
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_GOOGLE_OAUTH_CLIENT_ID=quality-production-client \
|
|
PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET=quality-production-secret \
|
|
PRODUCTION_GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-production-android-client \
|
|
PRODUCTION_WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public \
|
|
PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY=quality-vapid-private \
|
|
PRODUCTION_WEB_PUSH_VAPID_SUBJECT=mailto:contact@help.test \
|
|
PRODUCTION_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality \
|
|
PRODUCTION_WNH_FIREBASE_API_KEY=quality-public-client-key \
|
|
PRODUCTION_WNH_FIREBASE_PROJECT_ID=quality-production \
|
|
PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
PRODUCTION_FCM_PROJECT_ID=quality-production \
|
|
PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_fcm_base64" \
|
|
PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \
|
|
PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
|
PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
|
PRODUCTION_SUPPORT_INBOX_ADDRESS=support@help.test \
|
|
PRODUCTION_SUPPORT_INBOUND_RECIPIENT=support@reply.help.test \
|
|
./scripts/init-production-env.sh help.test "$production_env" >/dev/null
|
|
test "$(stat -c '%a' "$production_env")" = 600
|
|
grep -Fx "SMTP_PASSWORD=quality\$\$password" "$production_env" >/dev/null
|
|
grep -Fx 'SUPPORT_INBOUND_RECIPIENT=support@reply.help.test' "$production_env" >/dev/null
|
|
support_inbound_token=$(
|
|
awk -F= '$1 == "SUPPORT_INBOUND_WEBHOOK_TOKEN" { print substr($0, index($0, "=") + 1); exit }' \
|
|
"$production_env"
|
|
)
|
|
case "$support_inbound_token" in
|
|
"" | *[!0-9a-f]*)
|
|
echo "Production initializer generated an invalid inbound-support token." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ "${#support_inbound_token}" -ne 64 ]; then
|
|
echo "Production initializer generated an invalid inbound-support token length." >&2
|
|
exit 1
|
|
fi
|
|
grep -Fx 'GOOGLE_OAUTH_AUTHORIZED_PARTY_IDS=quality-production-android-client' \
|
|
"$production_env" >/dev/null
|
|
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
|
|
./scripts/check-environment-readiness.sh "$production_env" --require-release >/dev/null
|
|
partial_inbound_env="$scan_dir/production.partial-inbound.env"
|
|
cp "$production_env" "$partial_inbound_env"
|
|
sed -i 's|^SUPPORT_INBOUND_WEBHOOK_TOKEN=.*|SUPPORT_INBOUND_WEBHOOK_TOKEN=|' \
|
|
"$partial_inbound_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$partial_inbound_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted a partial inbound-support configuration." >&2
|
|
exit 1
|
|
fi
|
|
disabled_rate_limits_env="$scan_dir/production.disabled-rate-limits.env"
|
|
cp "$production_env" "$disabled_rate_limits_env"
|
|
sed -i 's|^RATE_LIMIT_POLICIES_JSON=.*|RATE_LIMIT_POLICIES_JSON={}|' \
|
|
"$disabled_rate_limits_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$disabled_rate_limits_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted disabled shared rate limits." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$disabled_rate_limits_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted disabled shared rate limits." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality \
|
|
PRODUCTION_WNH_FIREBASE_API_KEY=quality-public-client-key \
|
|
PRODUCTION_WNH_FIREBASE_PROJECT_ID=quality-production \
|
|
PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.mismatched-firebase-init.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted a Firebase application from another sender." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_FCM_PROJECT_ID=quality-production \
|
|
PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.mismatched-fcm-credential.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted an FCM service account from another project." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_WNH_FIREBASE_APPLICATION_ID=1:123456789:android:quality \
|
|
PRODUCTION_WNH_FIREBASE_API_KEY=quality-public-client-key \
|
|
PRODUCTION_WNH_FIREBASE_PROJECT_ID=quality-production \
|
|
PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID=123456789 \
|
|
PRODUCTION_FCM_PROJECT_ID=quality-other \
|
|
PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64="$quality_other_fcm_base64" \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.mismatched-firebase-fcm.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted different Firebase and FCM projects." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging \
|
|
PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
|
PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.staging-package.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted the staging Android package." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile \
|
|
PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB \
|
|
PRODUCTION_ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.unpublished-play-signing.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted a Play fingerprint absent from assetlinks." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_WEB_PUSH_VAPID_PUBLIC_KEY=quality-vapid-public \
|
|
PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY=quality-vapid-private \
|
|
PRODUCTION_WEB_PUSH_VAPID_SUBJECT=ftp://help.test \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.invalid-vapid-subject.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted an invalid VAPID subject." >&2
|
|
exit 1
|
|
fi
|
|
injected_smtp_password=$(
|
|
printf 'quality-password\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
|
)
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_SMTP_PASSWORD="$injected_smtp_password" \
|
|
./scripts/init-production-env.sh help.test \
|
|
"$scan_dir/production.injected-line.env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer accepted a line-breaking credential." >&2
|
|
exit 1
|
|
fi
|
|
test ! -e "$scan_dir/production.injected-line.env"
|
|
invalid_fcm_env="$scan_dir/production.invalid-fcm.env"
|
|
invalid_fcm_base64=$(
|
|
printf '%s' '{"type":"service_account","project_id":"quality-production"}' |
|
|
base64 -w 0
|
|
)
|
|
cp "$production_env" "$invalid_fcm_env"
|
|
sed -i \
|
|
"s|^FCM_SERVICE_ACCOUNT_JSON_BASE64=.*|FCM_SERVICE_ACCOUNT_JSON_BASE64=$invalid_fcm_base64|" \
|
|
"$invalid_fcm_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$invalid_fcm_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted an incomplete FCM service account." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$invalid_fcm_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted an incomplete FCM service account." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mismatched_firebase_env="$scan_dir/production.mismatched-firebase.env"
|
|
cp "$production_env" "$mismatched_firebase_env"
|
|
sed -i \
|
|
's|^WNH_FIREBASE_APPLICATION_ID=.*|WNH_FIREBASE_APPLICATION_ID=1:987654321:android:quality|' \
|
|
"$mismatched_firebase_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$mismatched_firebase_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted a Firebase application from another sender." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$mismatched_firebase_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted a Firebase application from another sender." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mismatched_fcm_env="$scan_dir/production.mismatched-fcm.env"
|
|
mismatched_fcm_base64=$(
|
|
printf '%s' \
|
|
'{"type":"service_account","project_id":"another-project","client_email":"quality-fcm@another-project.iam.gserviceaccount.com","private_key":"quality-private-key"}' |
|
|
base64 -w 0
|
|
)
|
|
cp "$production_env" "$mismatched_fcm_env"
|
|
sed -i \
|
|
"s|^FCM_SERVICE_ACCOUNT_JSON_BASE64=.*|FCM_SERVICE_ACCOUNT_JSON_BASE64=$mismatched_fcm_base64|" \
|
|
"$mismatched_fcm_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$mismatched_fcm_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted an FCM service account from another project." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$mismatched_fcm_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted an FCM service account from another project." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mismatched_app_links_env="$scan_dir/production.mismatched-app-links.env"
|
|
cp "$production_env" "$mismatched_app_links_env"
|
|
sed -i \
|
|
's|^ANDROID_APP_LINKS_PACKAGE_NAME=.*|ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile.staging|' \
|
|
"$mismatched_app_links_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$mismatched_app_links_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted the staging Android package." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$mismatched_app_links_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted the staging Android package for production." >&2
|
|
exit 1
|
|
fi
|
|
|
|
upload_only_app_links_env="$scan_dir/production.upload-only-app-links.env"
|
|
cp "$production_env" "$upload_only_app_links_env"
|
|
sed -i \
|
|
's|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=|' \
|
|
"$upload_only_app_links_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$upload_only_app_links_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted upload-only Android App Links." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$upload_only_app_links_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted upload-only Android App Links." >&2
|
|
exit 1
|
|
fi
|
|
./scripts/validate-production-env.sh \
|
|
"$upload_only_app_links_env" help.test --allow-pre-play >/dev/null
|
|
./scripts/check-environment-readiness.sh \
|
|
"$upload_only_app_links_env" --require-server-release >/dev/null
|
|
|
|
unpublished_play_app_links_env="$scan_dir/production.unpublished-play-app-links.env"
|
|
cp "$production_env" "$unpublished_play_app_links_env"
|
|
sed -i \
|
|
's|^ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=.*|ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=D7:C4:F1:12:4D:F4:68:E5:B3:54:DE:D8:96:E8:01:51:29:41:F1:8A:71:0C:18:B0:E7:98:AA:2B:81:DA:11:DF|' \
|
|
"$unpublished_play_app_links_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$unpublished_play_app_links_env" help.test >/dev/null 2>&1; then
|
|
echo "Production validation accepted an unpublished Play App Signing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$unpublished_play_app_links_env" --require-release >/dev/null 2>&1; then
|
|
echo "Environment readiness accepted an unpublished Play App Signing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/validate-production-env.sh \
|
|
"$unpublished_play_app_links_env" help.test \
|
|
--allow-pre-play >/dev/null 2>&1; then
|
|
echo "Pre-Play validation accepted an unpublished Play App Signing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/check-environment-readiness.sh \
|
|
"$unpublished_play_app_links_env" \
|
|
--require-server-release >/dev/null 2>&1; then
|
|
echo "Server-release readiness accepted an unpublished Play App Signing fingerprint." >&2
|
|
exit 1
|
|
fi
|
|
|
|
grep -Fx 'COMPOSE_PROJECT_NAME=who_need_help_production' "$production_env" >/dev/null
|
|
grep -E '^APP_IMAGE=who-need-help:production-[0-9a-f]{12}$' "$production_env" >/dev/null
|
|
grep -Fx 'EDGE_COMPOSE_PROJECT_NAME=who_need_help_edge' "$production_env" >/dev/null
|
|
grep -Fx 'PRIMARY_DOMAIN=help.test' "$production_env" >/dev/null
|
|
grep -Fx 'TEST_DOMAIN=test.help.test' "$production_env" >/dev/null
|
|
grep -Fx 'ANDROID_APP_LINKS_PACKAGE_NAME=org.whoneedhelp.mobile' "$production_env" >/dev/null
|
|
grep -Fx 'ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB' "$production_env" >/dev/null
|
|
grep -Fx 'ANDROID_PLAY_APP_SIGNING_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3:75:44:B6:2E:37:A3:75:C0:E0:05:64:74:50:F4:0F:29:B2:A9:84:F9:FD:B8:FB' "$production_env" >/dev/null
|
|
./scripts/validate-edge-env.sh "$production_env" >/dev/null
|
|
|
|
test_checkout="$scan_dir/test-checkout"
|
|
production_checkout="$scan_dir/production-checkout"
|
|
mkdir "$test_checkout" "$production_checkout"
|
|
git -C "$test_checkout" init --quiet
|
|
git -C "$production_checkout" init --quiet
|
|
cp "$test_env" "$test_checkout/.env"
|
|
cp "$production_env" "$production_checkout/.env"
|
|
chmod 600 "$test_checkout/.env" "$production_checkout/.env"
|
|
./scripts/validate-deployment-isolation.sh \
|
|
"$test_checkout" "$production_checkout" >/dev/null
|
|
|
|
# Account-level SMTP logins may be shared by a relay, but the independently
|
|
# revocable SMTP passwords must remain isolated between deployments.
|
|
production_smtp_relay=$(awk -F= '$1 == "SMTP_RELAY" { print substr($0, index($0, "=") + 1); exit }' "$production_checkout/.env")
|
|
production_smtp_username=$(awk -F= '$1 == "SMTP_USERNAME" { print substr($0, index($0, "=") + 1); exit }' "$production_checkout/.env")
|
|
production_smtp_password=$(awk -F= '$1 == "SMTP_PASSWORD" { print substr($0, index($0, "=") + 1); exit }' "$production_checkout/.env")
|
|
sed -i \
|
|
-e 's/^EMAIL_DELIVERY_PROVIDER=.*/EMAIL_DELIVERY_PROVIDER=smtp/' \
|
|
-e "s|^SMTP_RELAY=.*|SMTP_RELAY=$production_smtp_relay|" \
|
|
-e "s|^SMTP_USERNAME=.*|SMTP_USERNAME=$production_smtp_username|" \
|
|
-e 's/^SMTP_PASSWORD=.*/SMTP_PASSWORD=quality-test-isolated-smtp-password/' \
|
|
"$test_checkout/.env"
|
|
./scripts/validate-deployment-isolation.sh \
|
|
"$test_checkout" "$production_checkout" >/dev/null
|
|
sed -i \
|
|
"s|^SMTP_PASSWORD=.*|SMTP_PASSWORD=$production_smtp_password|" \
|
|
"$test_checkout/.env"
|
|
if ./scripts/validate-deployment-isolation.sh \
|
|
"$test_checkout" "$production_checkout" >/dev/null 2>&1; then
|
|
echo "Deployment isolation accepted a shared SMTP password." >&2
|
|
exit 1
|
|
fi
|
|
cp "$test_env" "$test_checkout/.env"
|
|
chmod 600 "$test_checkout/.env"
|
|
placeholder_codex_env="$scan_dir/.env.production.placeholder-codex"
|
|
cp "$production_env" "$placeholder_codex_env"
|
|
chmod 600 "$placeholder_codex_env"
|
|
sed -i \
|
|
's/^CODEX_SESSION_ID=.*/CODEX_SESSION_ID=copy-the-main-local-codex-session-id/' \
|
|
"$placeholder_codex_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$placeholder_codex_env" help.test >/dev/null 2>&1; then
|
|
echo "Production environment validator accepted the template Codex session ID." >&2
|
|
exit 1
|
|
fi
|
|
external_production_env="$scan_dir/.env.production.external-db"
|
|
PRODUCTION_DATABASE_MODE=external \
|
|
PRODUCTION_DATABASE_URL=ecto://quality:external-password@database.internal/who_need_help \
|
|
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
|
PRODUCTION_SMTP_PORT=587 \
|
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
|
PRODUCTION_SMTP_PASSWORD=quality-password \
|
|
PRODUCTION_SMTP_AUTH=always \
|
|
PRODUCTION_SMTP_TLS=always \
|
|
PRODUCTION_SMTP_SSL=false \
|
|
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh help.test "$external_production_env" >/dev/null
|
|
./scripts/validate-production-env.sh "$external_production_env" help.test >/dev/null
|
|
external_socket_production_env="$scan_dir/.env.production.external-db-socket"
|
|
PRODUCTION_DATABASE_MODE=external \
|
|
PRODUCTION_DATABASE_URL=ecto://quality:external-password@localhost/who_need_help \
|
|
PRODUCTION_DATABASE_SOCKET_DIR=/var/run/postgresql \
|
|
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
|
PRODUCTION_SMTP_PORT=587 \
|
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
|
PRODUCTION_SMTP_PASSWORD=quality-password \
|
|
PRODUCTION_SMTP_AUTH=always \
|
|
PRODUCTION_SMTP_TLS=always \
|
|
PRODUCTION_SMTP_SSL=false \
|
|
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh help.test "$external_socket_production_env" >/dev/null
|
|
./scripts/validate-production-env.sh \
|
|
"$external_socket_production_env" help.test >/dev/null
|
|
if PRODUCTION_DATABASE_MODE=external \
|
|
PRODUCTION_DATABASE_URL=ecto://quality:external-password@localhost/who_need_help \
|
|
PRODUCTION_DATABASE_SOCKET_DIR=relative/socket \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh \
|
|
help.test "$scan_dir/.env.production.invalid-socket" >/dev/null 2>&1; then
|
|
echo "Production initializer accepted a relative database socket path." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_DATABASE_SOCKET_DIR=/var/run/postgresql \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh \
|
|
help.test "$scan_dir/.env.production.container-socket" >/dev/null 2>&1; then
|
|
echo "Production initializer accepted a host socket in container database mode." >&2
|
|
exit 1
|
|
fi
|
|
external_split_production_env="$scan_dir/.env.production.external-db-split"
|
|
PRODUCTION_APP_TOPOLOGY=split \
|
|
PRODUCTION_DATABASE_MODE=external \
|
|
PRODUCTION_DATABASE_URL=ecto://quality:external-password@database.internal/who_need_help \
|
|
PRODUCTION_TRAEFIK_TRUSTED_IPS=172.20.0.1/32 \
|
|
PRODUCTION_SMTP_RELAY=smtp.help.test \
|
|
PRODUCTION_SMTP_PORT=587 \
|
|
PRODUCTION_SMTP_USERNAME=quality-user \
|
|
PRODUCTION_SMTP_PASSWORD=quality-password \
|
|
PRODUCTION_SMTP_AUTH=always \
|
|
PRODUCTION_SMTP_TLS=always \
|
|
PRODUCTION_SMTP_SSL=false \
|
|
PRODUCTION_EMAIL_FROM_ADDRESS=contact@help.test \
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh help.test "$external_split_production_env" >/dev/null
|
|
./scripts/validate-production-env.sh "$external_split_production_env" help.test >/dev/null
|
|
invalid_external_env="$scan_dir/.env.production.invalid-external-db"
|
|
cp "$external_production_env" "$invalid_external_env"
|
|
chmod 600 "$invalid_external_env"
|
|
sed -i 's#^DATABASE_URL=.*#DATABASE_URL=ecto://quality:external-password@db/who_need_help#' \
|
|
"$invalid_external_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$invalid_external_env" help.test >/dev/null 2>&1; then
|
|
echo "Production environment validator accepted the Compose db host in external mode." >&2
|
|
exit 1
|
|
fi
|
|
partial_google_env="$scan_dir/.env.production.partial-google"
|
|
cp "$production_env" "$partial_google_env"
|
|
chmod 600 "$partial_google_env"
|
|
sed -i 's/^GOOGLE_OAUTH_CLIENT_ID=.*/GOOGLE_OAUTH_CLIENT_ID=quality-client/' \
|
|
"$partial_google_env"
|
|
sed -i 's/^GOOGLE_OAUTH_CLIENT_SECRET=.*/GOOGLE_OAUTH_CLIENT_SECRET=/' \
|
|
"$partial_google_env"
|
|
if ./scripts/validate-production-env.sh \
|
|
"$partial_google_env" help.test >/dev/null 2>&1; then
|
|
echo "Production environment validator accepted partial Google OAuth credentials." >&2
|
|
exit 1
|
|
fi
|
|
if ./scripts/init-production-env.sh help.test "$production_env" >/dev/null 2>&1; then
|
|
echo "Production environment initializer overwrote an existing file." >&2
|
|
exit 1
|
|
fi
|
|
if PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
PRODUCTION_WNH_FIREBASE_PROJECT_ID=partial-firebase \
|
|
./scripts/init-production-env.sh \
|
|
help.test "$scan_dir/.env.production.partial-firebase" >/dev/null 2>&1; then
|
|
echo "Production initializer accepted partial Firebase Android configuration." >&2
|
|
exit 1
|
|
fi
|
|
incomplete_production_env="$scan_dir/.env.production.incomplete"
|
|
PRODUCTION_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
|
|
./scripts/init-production-env.sh help.test "$incomplete_production_env" >/dev/null
|
|
if ./scripts/validate-production-env.sh \
|
|
"$incomplete_production_env" help.test >/dev/null 2>&1; then
|
|
echo "Production environment validator accepted unresolved deployment inputs." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Rendering every Docker Compose profile"
|
|
docker compose --project-name who_need_help_edge \
|
|
--project-directory "$ROOT" --env-file "$production_env" \
|
|
--file compose.edge.yaml config --format json |
|
|
jq --exit-status '
|
|
.name == "who_need_help_edge" and
|
|
(.services.edge.image | startswith("who-need-help:caddy-production-")) and
|
|
.services.edge.user == "1000:1000" and
|
|
.services.edge.read_only == true and
|
|
.services.edge.cap_drop == ["ALL"] and
|
|
.services.edge.cap_add == ["NET_BIND_SERVICE"] and
|
|
.services.edge.security_opt == ["no-new-privileges:true"] and
|
|
(.services.edge.tmpfs | index("/tmp") != null) and
|
|
(.services.edge.ports | map(select(.target == 80 and .published == "80" and .protocol == "tcp")) | length) == 1 and
|
|
(.services.edge.ports | map(select(.target == 443 and .published == "443" and .protocol == "tcp")) | length) == 1 and
|
|
(.services.edge.ports | map(select(.target == 443 and .published == "443" and .protocol == "udp")) | length) == 1 and
|
|
.networks.public_edge.name == "who_need_help_public_edge"
|
|
' >/dev/null
|
|
./scripts/compose.sh .env.example config --quiet
|
|
./scripts/compose.sh "$production_env" config --quiet
|
|
./scripts/compose.sh "$external_production_env" config --quiet
|
|
./scripts/compose.sh "$external_socket_production_env" config --quiet
|
|
./scripts/compose.sh "$external_split_production_env" config --quiet
|
|
./scripts/compose.sh "$test_env" config --format json |
|
|
jq --exit-status '
|
|
.services.app.networks.internal.interface_name == "eth0" and
|
|
.services.app.networks.egress.interface_name == "eth1" and
|
|
.services.app.networks.public_edge.interface_name == "eth2" and
|
|
.services.app.networks.public_edge.aliases == ["who-need-help-test"]
|
|
' >/dev/null
|
|
./scripts/compose.sh .env.example config --format json |
|
|
jq --exit-status '
|
|
. as $root
|
|
| [$root.services.migrate, $root.services.web, $root.services.worker]
|
|
| all(
|
|
.environment.ERL_ZFLAGS == "+Q 65536" and
|
|
.environment.CLUSTER_INTERFACE == "eth0" and
|
|
.read_only == true and
|
|
.cap_drop == ["ALL"] and
|
|
.security_opt == ["no-new-privileges:true"] and
|
|
(.tmpfs | index("/tmp") != null)
|
|
)
|
|
and $root.services.web.environment.POOL_SIZE == "4"
|
|
and $root.services.worker.environment.POOL_SIZE == "2"
|
|
and $root.services.migrate.environment.POOL_SIZE == "2"
|
|
and $root.services.worker.environment.OBAN_MAINTENANCE_CONCURRENCY == "2"
|
|
and $root.services.worker.environment.OBAN_PUSH_CONCURRENCY == "1"
|
|
and $root.services.worker.environment.OBAN_MAIL_CONCURRENCY == "1"
|
|
and $root.services.web.deploy.replicas == 2
|
|
and $root.services.worker.deploy.replicas == 2
|
|
and ($root.services.proxy.networks | keys | sort) == ["docker-api", "edge", "ingress"]
|
|
and ($root.services.web.networks | keys | sort) == ["egress", "ingress", "internal"]
|
|
and ($root.services.worker.networks | keys | sort) == ["egress", "internal"]
|
|
and ($root.services.migrate.networks | keys | sort) == ["egress", "internal"]
|
|
and $root.services.web.networks.internal.interface_name == "eth0"
|
|
and $root.services.web.networks.internal.aliases == ["cluster-web"]
|
|
and $root.services.web.networks.ingress.interface_name == "eth1"
|
|
and $root.services.web.networks.egress.interface_name == "eth2"
|
|
and $root.services.web.networks.egress.gw_priority == 1
|
|
and $root.services.worker.networks.internal.interface_name == "eth0"
|
|
and $root.services.worker.networks.egress.interface_name == "eth1"
|
|
and $root.services.worker.networks.egress.gw_priority == 1
|
|
and ($root.services.db.networks | keys) == ["internal"]
|
|
and $root.networks.ingress.internal == true
|
|
and $root.networks.internal.internal == true
|
|
and ($root.networks.egress.internal // false) == false
|
|
and $root.services.db.security_opt == ["no-new-privileges:true"]
|
|
and $root.services.proxy.ports[0].host_ip == "0.0.0.0"
|
|
and $root.services.mailpit.ports[0].host_ip == "127.0.0.1"
|
|
' >/dev/null
|
|
./scripts/compose.sh "$production_env" config --format json |
|
|
jq --exit-status '
|
|
(.services | has("app")) and
|
|
(.services | has("db")) and
|
|
(.services | has("web") | not) and
|
|
(.services | has("worker") | not) and
|
|
(.services | has("proxy") | not) and
|
|
(.services | has("docker-api-proxy") | not) and
|
|
(.services | has("mailpit") | not) and
|
|
.services.app.environment.APP_ROLE == "combined" and
|
|
.services.app.environment.DNS_CLUSTER_QUERY == "ignore" and
|
|
.services.app.environment.POOL_SIZE == "4" and
|
|
.services.app.networks.internal.interface_name == "eth0" and
|
|
.services.app.networks.egress.interface_name == "eth1" and
|
|
.services.app.networks.public_edge.interface_name == "eth2" and
|
|
.services.app.networks.public_edge.aliases == ["who-need-help-production"] and
|
|
.networks.public_edge.external == true and
|
|
.services.app.ports[0].host_ip == "127.0.0.1"
|
|
' >/dev/null
|
|
./scripts/compose.sh "$external_production_env" config --format json |
|
|
jq --exit-status '
|
|
(.services | has("app")) and
|
|
(.services | has("db") | not) and
|
|
(.services | has("web") | not) and
|
|
(.services | has("worker") | not) and
|
|
(.services | has("proxy") | not) and
|
|
.services.app.networks.public_edge.interface_name == "eth2" and
|
|
.services.app.networks.public_edge.aliases == ["who-need-help-production"]
|
|
' >/dev/null
|
|
./scripts/compose.sh "$external_socket_production_env" config --format json |
|
|
jq --exit-status '
|
|
(.services | has("app")) and
|
|
(.services | has("db") | not) and
|
|
.services.app.environment.DATABASE_SOCKET_DIR == "/var/run/postgresql" and
|
|
.services.migrate.environment.DATABASE_SOCKET_DIR == "/var/run/postgresql" and
|
|
(.services.app.volumes |
|
|
any(
|
|
.type == "bind" and
|
|
.source == "/var/run/postgresql" and
|
|
.target == "/var/run/postgresql" and
|
|
.read_only == true
|
|
)) and
|
|
(.services.migrate.volumes |
|
|
any(
|
|
.type == "bind" and
|
|
.source == "/var/run/postgresql" and
|
|
.target == "/var/run/postgresql" and
|
|
.read_only == true
|
|
))
|
|
' >/dev/null
|
|
./scripts/compose.sh "$external_socket_production_env" config --profiles |
|
|
grep -Fx container-database >/dev/null
|
|
./scripts/compose.sh "$external_split_production_env" config --format json |
|
|
jq --exit-status '
|
|
(.services | has("db") | not) and
|
|
(.services | has("app") | not) and
|
|
(.services | has("web")) and
|
|
(.services | has("worker")) and
|
|
(.services | has("proxy")) and
|
|
.services.web.networks.internal.interface_name == "eth0" and
|
|
.services.web.networks.ingress.interface_name == "eth1" and
|
|
.services.web.networks.egress.interface_name == "eth2" and
|
|
.services.web.networks.public_edge.interface_name == "eth3" and
|
|
.services.web.networks.public_edge.aliases == ["who-need-help-production"] and
|
|
.services.web.deploy.replicas == 2 and
|
|
.services.worker.deploy.replicas == 2
|
|
' >/dev/null
|
|
HTTP_BIND_ADDRESS=127.0.0.1 \
|
|
./scripts/compose.sh .env.example config --format json |
|
|
jq --exit-status '
|
|
.services.proxy.ports[0].host_ip == "127.0.0.1"
|
|
' >/dev/null
|
|
WEB_REPLICAS=1 WORKER_REPLICAS=1 \
|
|
./scripts/compose.sh .env.example config --format json |
|
|
jq --exit-status '
|
|
.services.web.deploy.replicas == 1 and
|
|
.services.worker.deploy.replicas == 1
|
|
' >/dev/null
|
|
CPU_REPLAY_CPUSET=0 \
|
|
CPU_REPLAY_WEB_CPUS=1 \
|
|
CPU_REPLAY_WORKER_CPUS=1 \
|
|
CPU_REPLAY_WEB_SCHEDULERS=1 \
|
|
CPU_REPLAY_WORKER_SCHEDULERS=1 \
|
|
docker compose --env-file .env.example \
|
|
-f compose.yaml -f compose.cpu-replay.yaml config --quiet
|
|
APP_IMAGE=who-need-help:portability-render \
|
|
SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-portability-render \
|
|
POSTGIS_IMAGE=who-need-help:postgis-portability-render \
|
|
PORTABILITY_IMAGE=who-need-help:portability-render \
|
|
PORTABILITY_SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-portability-render \
|
|
PORTABILITY_POSTGIS_IMAGE=who-need-help:postgis-portability-render \
|
|
PORTABILITY_TRAEFIK_IMAGE=who-need-help:traefik-portability-render \
|
|
docker compose --env-file .env.example \
|
|
-f compose.yaml -f compose.portability.yaml config --quiet
|
|
mkdir -p "$scan_dir/e2e-output"
|
|
E2E_OUTPUT_DIR="$scan_dir/e2e-output" docker compose --env-file .env.e2e.example \
|
|
-f compose.yaml -f compose.e2e.yaml config --quiet
|
|
REHEARSAL_IMAGE=who-need-help:rehearsal-render \
|
|
docker compose --env-file .env.e2e.example \
|
|
-f compose.yaml -f compose.upgrade-rehearsal.yaml config --quiet
|
|
docker compose --env-file .env.load.example \
|
|
-f compose.yaml -f compose.load.yaml config --quiet
|
|
test "$(
|
|
docker compose --env-file .env.load.example \
|
|
-f compose.yaml -f compose.load.yaml config --format json |
|
|
jq -r '.services.db.command | join(" ")'
|
|
)" = "postgres -c shared_preload_libraries=pg_stat_statements -c compute_query_id=on"
|
|
mkdir -p "$scan_dir/observability-runtime/prometheus" \
|
|
"$scan_dir/observability-runtime/grafana"
|
|
OBSERVABILITY_RUNTIME_DIR="$scan_dir/observability-runtime" \
|
|
docker compose --env-file .env.load.example \
|
|
-f compose.yaml -f compose.load.yaml -f compose.observability.yaml \
|
|
--profile observability config --quiet
|
|
mkdir -p "$scan_dir/backup-runtime"
|
|
BACKUP_RUNTIME_DIR="$scan_dir/backup-runtime" \
|
|
BACKUP_HOST_UID="$(id -u)" \
|
|
BACKUP_HOST_GID="$(id -g)" \
|
|
docker compose --env-file .env.load.example \
|
|
-f compose.yaml -f compose.load.yaml -f compose.backup.yaml \
|
|
--profile backup config --quiet
|
|
docker compose -p "$project" -f compose.quality.yaml config --quiet
|
|
mkdir -p "$scan_dir/external-boundary-output"
|
|
EXTERNAL_BOUNDARY_APP_IMAGE=who-need-help:boundary-render \
|
|
EXTERNAL_BOUNDARY_MOCK_IMAGE=who-need-help:boundary-mock-render \
|
|
EXTERNAL_BOUNDARY_OUTPUT_DIR="$scan_dir/external-boundary-output" \
|
|
EXTERNAL_BOUNDARY_HOST_UID="$(id -u)" \
|
|
EXTERNAL_BOUNDARY_HOST_GID="$(id -g)" \
|
|
EXTERNAL_OAUTH_CLIENT_ID=render-client \
|
|
EXTERNAL_OAUTH_CLIENT_SECRET=render-secret \
|
|
EXTERNAL_PUSH_BEARER_TOKEN=render-push-token \
|
|
EXTERNAL_METRICS_TOKEN=render-metrics-token \
|
|
EXTERNAL_POSTGRES_PASSWORD=render-database-secret \
|
|
EXTERNAL_DATABASE_URL=ecto://boundary:render-database-secret@boundary-db/boundary \
|
|
EXTERNAL_SECRET_KEY_BASE=render-secret-key-base \
|
|
EXTERNAL_HANDOVER_SECRET=render-handover-secret \
|
|
docker compose -f compose.external-boundaries.yaml config --quiet
|
|
|
|
echo "Validating local observability configuration"
|
|
sed \
|
|
-e 's/__SCRAPE_INTERVAL__/1s/g' \
|
|
-e 's/__EVALUATION_INTERVAL__/1s/g' \
|
|
ops/observability/prometheus.template.yml \
|
|
>"$scan_dir/observability-runtime/prometheus/prometheus.yml"
|
|
printf '%s' 'isolated-quality-metrics-token' \
|
|
>"$scan_dir/observability-runtime/prometheus/metrics-token"
|
|
printf '%s\n' '[]' \
|
|
>"$scan_dir/observability-runtime/prometheus/web-targets.json"
|
|
printf '%s\n' '[]' \
|
|
>"$scan_dir/observability-runtime/prometheus/worker-targets.json"
|
|
docker run --rm \
|
|
--user 0:0 \
|
|
--volume "$scan_dir/observability-runtime/prometheus:/runtime:ro" \
|
|
--volume "$ROOT/ops/observability/rules.yml:/etc/prometheus/rules.yml:ro" \
|
|
--entrypoint /bin/promtool \
|
|
"$PROMETHEUS_IMAGE" check config /runtime/prometheus.yml
|
|
docker run --rm \
|
|
--user 0:0 \
|
|
--volume "$ROOT/ops/observability/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro" \
|
|
--entrypoint /bin/amtool \
|
|
"$ALERTMANAGER_IMAGE" check-config /etc/alertmanager/alertmanager.yml
|
|
docker run --rm \
|
|
--volume "$ROOT/scripts/alert-receiver.py:/src/alert-receiver.py:ro" \
|
|
"$PYTHON_IMAGE" python -c \
|
|
'import py_compile; py_compile.compile("/src/alert-receiver.py", cfile="/tmp/alert-receiver.pyc", doraise=True)'
|
|
docker run --rm \
|
|
--volume "$ROOT:/src:ro" \
|
|
--workdir /src \
|
|
"$PYTHON_IMAGE" python test/scripts/production_external_monitor_test.py
|
|
docker run --rm \
|
|
--volume "$ROOT:/src:ro" \
|
|
--workdir /src \
|
|
"$PYTHON_IMAGE" python test/scripts/override_production_monitor_smtp_test.py
|
|
docker run --rm \
|
|
--volume "$ROOT:/src:ro" \
|
|
--workdir /src \
|
|
"$PYTHON_IMAGE" python test/scripts/install_production_external_monitor_test.py
|
|
python3 test/scripts/production_release_artifact_root_test.py
|
|
python3 test/scripts/production_release_clean_test.py
|
|
docker run --rm \
|
|
--volume "$ROOT/ops/external-boundaries/mock_server.py:/src/mock_server.py:ro" \
|
|
"$PYTHON_IMAGE" python -c \
|
|
'import py_compile; py_compile.compile("/src/mock_server.py", cfile="/tmp/mock_server.pyc", doraise=True)'
|
|
jq --exit-status \
|
|
'type == "object" and .uid == "wnh-overview" and (.panels | length) == 12' \
|
|
ops/observability/grafana/dashboards/who-need-help-overview.json \
|
|
>/dev/null
|
|
|
|
echo "Linting the Helm chart"
|
|
"$ROOT/scripts/bootstrap-kubernetes-tools.sh" >/dev/null
|
|
"$ROOT/.tools/bin/helm" lint \
|
|
--values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \
|
|
"$ROOT/deploy/helm/who-need-help"
|
|
|
|
echo "Scanning only tracked and non-ignored source files"
|
|
# The single-quoted program must expand $path inside the child shell.
|
|
# shellcheck disable=SC2016
|
|
git ls-files --cached --others --exclude-standard -z |
|
|
xargs -0 -r sh -c '
|
|
for path do
|
|
if [ -f "$path" ]; then
|
|
printf "%s\0" "$path"
|
|
fi
|
|
done
|
|
' sh >"$scan_list"
|
|
tar --null --no-recursion --files-from="$scan_list" --create --file="$scan_tar"
|
|
tar --extract --file="$scan_tar" --directory "$scan_dir"
|
|
"$ROOT/.tools/bin/helm" template who-need-help \
|
|
--values "$ROOT/deploy/helm/who-need-help/values-kind.yaml" \
|
|
"$ROOT/deploy/helm/who-need-help" \
|
|
>"$scan_dir/rendered-helm.yaml"
|
|
test "$(
|
|
grep -c 'name: ERL_ZFLAGS' "$scan_dir/rendered-helm.yaml"
|
|
)" -eq 5
|
|
test "$(
|
|
grep -c 'value: "+Q 65536"' "$scan_dir/rendered-helm.yaml"
|
|
)" -eq 5
|
|
test "$(
|
|
grep -c 'name: PHX_CHECK_ORIGINS' "$scan_dir/rendered-helm.yaml"
|
|
)" -eq 2
|
|
test "$(
|
|
grep -c 'value: "https://whoneedhelp.imalto.site,http://localhost:4011"' \
|
|
"$scan_dir/rendered-helm.yaml"
|
|
)" -eq 2
|
|
test "$(
|
|
grep -c 'value: "whoneedhelp.imalto.site"' "$scan_dir/rendered-helm.yaml"
|
|
)" -eq 2
|
|
test "$(
|
|
grep -c '^kind: NetworkPolicy$' "$scan_dir/rendered-helm.yaml"
|
|
)" -eq 1
|
|
mkdir -p "$ROOT/.tools/trivy-cache"
|
|
docker run --rm \
|
|
--volume "$scan_dir:/scan:ro" \
|
|
--volume "$ROOT/.tools/trivy-cache:/root/.cache/trivy" \
|
|
"$TRIVY_IMAGE" fs \
|
|
--scanners misconfig,secret \
|
|
--severity HIGH,CRITICAL \
|
|
--exit-code 1 \
|
|
/scan
|
|
|
|
echo "Building, smoke-testing, and scanning pinned runtime infrastructure images"
|
|
docker build --tag "$socket_proxy_image" --file Dockerfile.socket-proxy .
|
|
docker build --tag "$postgis_image" --file Dockerfile.postgis .
|
|
docker build --tag "$caddy_image" --file Dockerfile.caddy .
|
|
docker build --tag "$traefik_image" --file Dockerfile.traefik .
|
|
docker build --tag "$mailpit_image" --file Dockerfile.mailpit .
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$socket_proxy_image")" = "haproxy"
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$postgis_image")" = "postgres"
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$caddy_image")" = "1000:1000"
|
|
docker run --rm "$caddy_image" version |
|
|
grep -F 'v2.11.4-wnh-go1.26.7-grpc1.82.1-xtext0.40.0' >/dev/null
|
|
docker run --rm "$traefik_image" version |
|
|
grep -F 'v3.7.10-wnh-grpc1.82.1-xmod0.40.0' >/dev/null
|
|
docker run --rm "$mailpit_image" version |
|
|
grep -F 'v1.30.7-wnh-go1.26.7-xmod0.40.0' >/dev/null
|
|
docker run --rm --entrypoint sh "$postgis_image" -euc '
|
|
test ! -e /usr/local/bin/gosu
|
|
test "$(id -u)" = 70
|
|
'
|
|
|
|
docker run --detach \
|
|
--name "$socket_proxy_container" \
|
|
--read-only \
|
|
--tmpfs /run:uid=99,gid=99,mode=0755 \
|
|
--tmpfs /tmp \
|
|
--cap-drop ALL \
|
|
--group-add "$(stat -c '%g' /var/run/docker.sock)" \
|
|
--security-opt no-new-privileges \
|
|
--env CONTAINERS=1 \
|
|
--env EVENTS=1 \
|
|
--env INFO=1 \
|
|
--env NETWORKS=1 \
|
|
--env PING=1 \
|
|
--env POST=0 \
|
|
--env VERSION=1 \
|
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
|
--publish 127.0.0.1::2375 \
|
|
"$socket_proxy_image" >/dev/null
|
|
socket_proxy_port=$(
|
|
docker port "$socket_proxy_container" 2375/tcp |
|
|
sed -n 's/.*://p' |
|
|
head -n 1
|
|
)
|
|
test -n "$socket_proxy_port"
|
|
socket_proxy_ready=false
|
|
for _attempt in $(seq 1 30); do
|
|
if curl --fail --silent --show-error \
|
|
"http://127.0.0.1:$socket_proxy_port/_ping" >/dev/null; then
|
|
socket_proxy_ready=true
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
test "$socket_proxy_ready" = true
|
|
test "$(
|
|
curl --silent --output /dev/null --write-out '%{http_code}' \
|
|
"http://127.0.0.1:$socket_proxy_port/containers/json"
|
|
)" = "200"
|
|
test "$(
|
|
curl --silent --output /dev/null --write-out '%{http_code}' \
|
|
--request POST \
|
|
"http://127.0.0.1:$socket_proxy_port/containers/create"
|
|
)" = "403"
|
|
docker rm --force "$socket_proxy_container" >/dev/null
|
|
|
|
for image in \
|
|
"$socket_proxy_image" \
|
|
"$postgis_image" \
|
|
"$traefik_image" \
|
|
"$caddy_image" \
|
|
"$mailpit_image"; do
|
|
scan_image "$image"
|
|
done
|
|
|
|
echo "Building the pinned quality image and cached Dialyzer PLTs"
|
|
docker build --target quality --tag "$quality_image" .
|
|
|
|
echo "Checking isolated VAPID generation and atomic single-file import"
|
|
generated_vapid_env="$scan_dir/generated-vapid.env"
|
|
cp .env.example "$generated_vapid_env"
|
|
chmod 600 "$generated_vapid_env"
|
|
vapid_output=$(
|
|
WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
|
./scripts/generate-vapid-env.sh \
|
|
"$generated_vapid_env" mailto:contact@help.test
|
|
)
|
|
generated_vapid_public=$(
|
|
awk -F= '
|
|
$1 == "WEB_PUSH_VAPID_PUBLIC_KEY" {
|
|
print substr($0, index($0, "=") + 1)
|
|
exit
|
|
}
|
|
' "$generated_vapid_env"
|
|
)
|
|
generated_vapid_private=$(
|
|
awk -F= '
|
|
$1 == "WEB_PUSH_VAPID_PRIVATE_KEY" {
|
|
print substr($0, index($0, "=") + 1)
|
|
exit
|
|
}
|
|
' "$generated_vapid_env"
|
|
)
|
|
test -n "$generated_vapid_public"
|
|
test -n "$generated_vapid_private"
|
|
test "$generated_vapid_public" != "$generated_vapid_private"
|
|
grep -Fx 'WEB_PUSH_VAPID_SUBJECT=mailto:contact@help.test' \
|
|
"$generated_vapid_env" >/dev/null
|
|
if printf '%s' "$vapid_output" |
|
|
grep -F -- "$generated_vapid_public" >/dev/null ||
|
|
printf '%s' "$vapid_output" |
|
|
grep -F -- "$generated_vapid_private" >/dev/null; then
|
|
echo "VAPID generator printed generated key material." >&2
|
|
exit 1
|
|
fi
|
|
docker run --rm \
|
|
--network none \
|
|
--read-only \
|
|
--volume "$generated_vapid_env:/tmp/generated-vapid.env:ro" \
|
|
--entrypoint elixir \
|
|
"$quality_image" \
|
|
-e '
|
|
values =
|
|
"/tmp/generated-vapid.env"
|
|
|> File.read!()
|
|
|> String.split("\n", trim: true)
|
|
|> Enum.reject(&(String.starts_with?(&1, "#") or not String.contains?(&1, "=")))
|
|
|> Map.new(fn line ->
|
|
[key, value] = String.split(line, "=", parts: 2)
|
|
{key, value}
|
|
end)
|
|
|
|
{:ok, public_key} =
|
|
Base.url_decode64(values["WEB_PUSH_VAPID_PUBLIC_KEY"], padding: false)
|
|
|
|
{:ok, private_key} =
|
|
Base.url_decode64(values["WEB_PUSH_VAPID_PRIVATE_KEY"], padding: false)
|
|
|
|
unless byte_size(public_key) == 65 and
|
|
:binary.first(public_key) == 4 and
|
|
byte_size(private_key) == 32 do
|
|
raise "unexpected VAPID key shape"
|
|
end
|
|
'
|
|
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
|
./scripts/generate-vapid-env.sh \
|
|
"$generated_vapid_env" mailto:contact@help.test >/dev/null 2>&1; then
|
|
echo "VAPID generator rotated an existing environment identity." >&2
|
|
exit 1
|
|
fi
|
|
fresh_vapid_env="$scan_dir/fresh-vapid.env"
|
|
cp .env.example "$fresh_vapid_env"
|
|
chmod 600 "$fresh_vapid_env"
|
|
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
|
./scripts/generate-vapid-env.sh \
|
|
"$fresh_vapid_env" ftp://help.test >/dev/null 2>&1; then
|
|
echo "VAPID generator accepted an invalid subject." >&2
|
|
exit 1
|
|
fi
|
|
fresh_vapid_hash=$(sha256sum "$fresh_vapid_env" | awk '{print $1}')
|
|
injected_vapid_subject=$(
|
|
printf 'mailto:contact@help.test\nGOOGLE_OAUTH_CLIENT_SECRET=injected'
|
|
)
|
|
if WNH_VAPID_GENERATOR_IMAGE="$quality_image" \
|
|
./scripts/generate-vapid-env.sh \
|
|
"$fresh_vapid_env" "$injected_vapid_subject" >/dev/null 2>&1; then
|
|
echo "VAPID generator accepted a line-breaking subject." >&2
|
|
exit 1
|
|
fi
|
|
test "$(sha256sum "$fresh_vapid_env" | awk '{print $1}')" = "$fresh_vapid_hash"
|
|
if find "$scan_dir" -maxdepth 1 -name '.vapid-generation.*' -print |
|
|
grep -q .; then
|
|
echo "VAPID generator retained a temporary credential directory." >&2
|
|
exit 1
|
|
fi
|
|
unset generated_vapid_public generated_vapid_private
|
|
|
|
echo "Running Elixir format, compiler, xref, Credo, Sobelow, Dialyzer, and Hex audit"
|
|
docker run --rm "$quality_image" sh -euc '
|
|
mix format --check-formatted
|
|
mix compile --force --warnings-as-errors
|
|
mix gettext.extract --check-up-to-date
|
|
mix xref graph --label compile-connected --fail-above 0
|
|
mix credo --strict --min-priority high
|
|
mix sobelow --exit --strict --private --skip
|
|
mix dialyzer
|
|
mix hex.audit
|
|
'
|
|
|
|
echo "Starting an isolated PostgreSQL/PostGIS volume for the Phoenix suite"
|
|
$compose up --detach --wait db
|
|
docker run --rm \
|
|
--network "${project}_internal" \
|
|
--env MIX_ENV=test \
|
|
--env DB_HOST=db \
|
|
--env "DB_USER=$QUALITY_POSTGRES_USER" \
|
|
--env "DB_PASSWORD=$QUALITY_POSTGRES_PASSWORD" \
|
|
--env TEST_POOL_SIZE=10 \
|
|
"$quality_image" \
|
|
mix test
|
|
|
|
echo "Auditing locked browser dependencies"
|
|
docker build --target node_deps --tag "$assets_image" .
|
|
docker run --rm \
|
|
--volume "$ROOT/assets/js:/assets/js:ro" \
|
|
--volume "$ROOT/priv/static/sw.js:/priv/static/sw.js:ro" \
|
|
"$assets_image" \
|
|
npm test
|
|
docker run --rm "$assets_image" npm audit --audit-level=high
|
|
docker build --tag "$e2e_image" e2e
|
|
docker run --rm "$e2e_image" npm audit --audit-level=high
|
|
|
|
echo "Building and scanning the pinned non-root backup tool image"
|
|
docker build --tag "$backup_image" --file Dockerfile.backup .
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$backup_image")" = \
|
|
"10001:10001"
|
|
backup_versions=$(docker run --rm \
|
|
--user 10001:10001 \
|
|
--read-only \
|
|
--tmpfs /tmp \
|
|
"$backup_image" \
|
|
sh -euc 'restic version; pg_dump --version; test "$(id -u)" = 10001')
|
|
printf '%s\n' "$backup_versions"
|
|
printf '%s\n' "$backup_versions" |
|
|
grep -F 'restic 0.19.1 compiled with go1.26.7' >/dev/null
|
|
printf '%s\n' "$backup_versions" |
|
|
grep -F 'pg_dump (PostgreSQL) 18.6' >/dev/null
|
|
scan_image "$backup_image"
|
|
|
|
echo "Building and scanning the pinned non-root MinIO server and client images"
|
|
docker build --target server --tag "$minio_image" --file Dockerfile.minio .
|
|
docker build --target client --tag "$mc_image" --file Dockerfile.minio .
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$minio_image")" = \
|
|
"10001:10001"
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$mc_image")" = \
|
|
"10001:10001"
|
|
minio_version=$(docker run --rm \
|
|
--user 10001:10001 \
|
|
--read-only \
|
|
--tmpfs /tmp \
|
|
"$minio_image" \
|
|
--version)
|
|
mc_version=$(docker run --rm \
|
|
--user 10001:10001 \
|
|
--read-only \
|
|
--tmpfs /tmp \
|
|
"$mc_image" \
|
|
--version)
|
|
printf '%s\n' "$minio_version"
|
|
printf '%s\n' "$mc_version"
|
|
printf '%s\n' "$minio_version" |
|
|
grep -F 'RELEASE.2025-10-15T17-29-55Z' >/dev/null
|
|
printf '%s\n' "$minio_version" |
|
|
grep -F 'commit-id=9e49d5e7a648f00e26f2246f4dc28e6b07f8c84a' >/dev/null
|
|
printf '%s\n' "$minio_version" |
|
|
grep -F 'Runtime: go1.26.7 linux/' >/dev/null
|
|
printf '%s\n' "$mc_version" |
|
|
grep -F 'RELEASE.2025-08-13T08-35-41Z' >/dev/null
|
|
printf '%s\n' "$mc_version" |
|
|
grep -F 'commit-id=7394ce0dd2a80935aded936b09fa12cbb3cb8096' >/dev/null
|
|
printf '%s\n' "$mc_version" |
|
|
grep -F 'Runtime: go1.26.7 linux/' >/dev/null
|
|
for image in "$minio_image" "$mc_image"; do
|
|
scan_image "$image"
|
|
done
|
|
|
|
echo "Building and scanning the pinned non-root external-boundary mock image"
|
|
docker build \
|
|
--tag "$boundary_mock_image" \
|
|
--file ops/external-boundaries/Dockerfile \
|
|
ops/external-boundaries
|
|
test "$(docker image inspect --format '{{.Config.User}}' "$boundary_mock_image")" = \
|
|
"10001:10001"
|
|
scan_image "$boundary_mock_image"
|
|
|
|
echo "Building and scanning the production release image"
|
|
docker build --target release --tag "$release_image" .
|
|
release_security_versions=$(docker run --rm \
|
|
--entrypoint dpkg-query \
|
|
"$release_image" \
|
|
-W \
|
|
-f='${Package}=${Version}\n' \
|
|
bsdutils \
|
|
libblkid1 \
|
|
liblastlog2-2 \
|
|
libmount1 \
|
|
libsmartcols1 \
|
|
libuuid1 \
|
|
login \
|
|
mount \
|
|
util-linux)
|
|
printf '%s\n' "$release_security_versions"
|
|
for expected_release_package in \
|
|
'bsdutils=1:2.41.5-0+deb13u1' \
|
|
'libblkid1=2.41.5-0+deb13u1' \
|
|
'liblastlog2-2=2.41.5-0+deb13u1' \
|
|
'libmount1=2.41.5-0+deb13u1' \
|
|
'libsmartcols1=2.41.5-0+deb13u1' \
|
|
'libuuid1=2.41.5-0+deb13u1' \
|
|
'login=1:4.16.0-2+really2.41.5-0+deb13u1' \
|
|
'mount=2.41.5-0+deb13u1' \
|
|
'util-linux=2.41.5-0+deb13u1'; do
|
|
printf '%s\n' "$release_security_versions" |
|
|
grep -F -x "$expected_release_package" >/dev/null
|
|
done
|
|
scan_image "$release_image"
|
|
|
|
echo "All isolated quality and security gates passed."
|