fix(release): harden environment credential bootstrap

This commit is contained in:
SimpleTest 2026-07-24 03:10:22 +03:00
parent 5129e1253a
commit cbb5efb62a
9 changed files with 733 additions and 29 deletions

View File

@ -152,9 +152,10 @@ PUSH_HTTP_RECEIVE_TIMEOUT_MS=
PUSH_HTTP_CONNECT_TIMEOUT_MS=
PUSH_HTTP_RETRY_DELAY_MS=
# Direct browser Web Push. Generate one VAPID key pair per environment and
# keep the private key only in that environment's .env. The subject must be a
# mailto: or HTTPS contact owned by the operator.
# Direct browser Web Push. Generate one VAPID key pair per environment with
# scripts/generate-vapid-env.sh and keep the private key only in that
# environment's .env. The subject must be a mailto: or HTTPS contact owned by
# the operator.
WEB_PUSH_VAPID_PUBLIC_KEY=
WEB_PUSH_VAPID_PRIVATE_KEY=
WEB_PUSH_VAPID_SUBJECT=

View File

@ -213,6 +213,9 @@ Provider downloads can be imported into that same file without placing
secrets on a command line or printing them:
```bash
./scripts/generate-vapid-env.sh \
.env mailto:contact@YOUR_DOMAIN
chmod 600 /secure/downloads/google-oauth-client.json
./scripts/import-google-oauth-client.sh \
.env /secure/downloads/google-oauth-client.json
@ -225,10 +228,19 @@ chmod 600 /secure/downloads/fcm-service-account.json
.env /secure/downloads/fcm-service-account.json
```
The VAPID helper runs the exact locked `web_push_elixir` generator in an
isolated, network-disabled container, imports the result atomically, removes
its one-run image tag and temporary files, and never prints either key. It
refuses to replace an existing VAPID identity because an unplanned rotation
invalidates existing browser subscriptions.
The importers validate the exact OAuth callback, Android package, Firebase
project relationship, and required service-account fields before atomically
replacing existing keys. They preserve mode `0600` and never create another
permanent environment file. OAuth and service-account downloads still contain
permanent environment file. Provider and initializer values that cannot be
represented as one unquoted Compose `.env` line are rejected before mutation.
Literal dollar signs are stored as `$$`, which Compose resolves back to one
`$` inside the container. OAuth and service-account downloads still contain
private credentials after import; deliberately move them to protected backup
storage or remove them after verification.

161
scripts/generate-vapid-env.sh Executable file
View File

@ -0,0 +1,161 @@
#!/bin/sh
set -eu
umask 077
if [ "$#" -ne 2 ]; then
echo "Usage: $0 ENV_FILE VAPID_SUBJECT" >&2
exit 1
fi
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
env_file=$1
subject=$2
if [ ! -f "$env_file" ]; then
echo "Environment file does not exist: $env_file" >&2
exit 1
fi
if [ "$(stat -c '%a' "$env_file")" != 600 ]; then
echo "Environment file must have mode 0600: $env_file" >&2
exit 1
fi
case "$subject" in
mailto:?* | https://?*) ;;
*)
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
exit 1
;;
esac
case "$subject" in
*'
'*)
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
exit 1
;;
esac
if printf '%s' "$subject" | LC_ALL=C grep -q '[[:space:]]'; then
echo "VAPID_SUBJECT must be a non-empty mailto: or https:// URI without whitespace." >&2
exit 1
fi
for command in awk chmod date docker grep mktemp rm stat; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command is unavailable: $command" >&2
exit 1
fi
done
for key in \
WEB_PUSH_VAPID_PUBLIC_KEY \
WEB_PUSH_VAPID_PRIVATE_KEY \
WEB_PUSH_VAPID_SUBJECT; do
key_count=$(
awk -F= -v key="$key" '$1 == key { count++ } END { print count + 0 }' \
"$env_file"
)
if [ "$key_count" -ne 1 ]; then
echo "Environment must contain exactly one $key entry before VAPID generation." >&2
exit 1
fi
existing_value=$(
awk -F= -v key="$key" '
$1 == key {
print substr($0, index($0, "=") + 1)
exit
}
' "$env_file"
)
if [ -n "$existing_value" ]; then
echo "Refusing to rotate an existing VAPID identity: $key is already configured." >&2
exit 1
fi
done
unset existing_value
env_dir=$(CDPATH='' cd -- "$(dirname -- "$env_file")" && pwd)
temporary_dir=$(mktemp -d "$env_dir/.vapid-generation.XXXXXX")
chmod 700 "$temporary_dir"
raw_keys="$temporary_dir/generated.txt"
values_file="$temporary_dir/values.env"
generator_image=${WNH_VAPID_GENERATOR_IMAGE:-}
owned_image=false
run_id="$(date -u +%Y%m%d%H%M%S)-$$"
generator_container="wnh-vapid-tool-$run_id"
cleanup() {
trap - EXIT HUP INT TERM
rm -rf "$temporary_dir"
docker rm --force "$generator_container" >/dev/null 2>&1 || true
if [ "$owned_image" = true ]; then
docker image rm "$generator_image" >/dev/null 2>&1 || true
fi
}
cleanup_on_exit() {
exit_status=$?
cleanup
exit "$exit_status"
}
trap cleanup_on_exit EXIT
trap 'cleanup; exit 129' HUP
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
if [ -z "$generator_image" ]; then
generator_image="who-need-help:vapid-tool-$run_id"
owned_image=true
docker build --quiet --target test --tag "$generator_image" "$ROOT" >/dev/null
fi
docker image inspect "$generator_image" >/dev/null
docker run --rm \
--name "$generator_container" \
--network none \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=16m \
--entrypoint mix \
"$generator_image" \
generate.vapid.keys >"$raw_keys"
chmod 600 "$raw_keys"
if ! awk -F'"' -v subject="$subject" '
/^[[:space:]]*vapid_private_key:/ {
private_count++
private_key = $2
}
/^[[:space:]]*vapid_public_key:/ {
public_count++
public_key = $2
}
END {
valid_private = private_key ~ /^[A-Za-z0-9_-]+$/
valid_public = public_key ~ /^[A-Za-z0-9_-]+$/
if (private_count != 1 ||
public_count != 1 ||
!valid_private ||
!valid_public ||
private_key == public_key) {
exit 40
}
print "WEB_PUSH_VAPID_PUBLIC_KEY=" public_key
print "WEB_PUSH_VAPID_PRIVATE_KEY=" private_key
print "WEB_PUSH_VAPID_SUBJECT=" subject
}
' "$raw_keys" >"$values_file"; then
echo "The pinned web_push_elixir generator returned an unexpected key format." >&2
exit 1
fi
chmod 600 "$values_file"
"$ROOT/scripts/set-env-values.sh" "$env_file" "$values_file" >/dev/null
echo "Generated a new environment-specific VAPID identity without printing its keys."
echo "Updated the single mode-0600 environment file: $env_file"

View File

@ -26,8 +26,10 @@ esac
if ! jq --exit-status '
.type == "service_account"
and (.project_id | type == "string" and length > 0)
and (.client_email | type == "string" and length > 0)
and (.project_id
| type == "string" and length > 0 and test("^[^\r\n]+$"))
and (.client_email
| type == "string" and length > 0 and test("^[^\r\n]+$"))
and (.private_key | type == "string" and length > 0)
' "$service_account_file" >/dev/null; then
echo "FCM service-account JSON is incomplete." >&2

View File

@ -38,12 +38,16 @@ if ! jq --exit-status --arg package "$package_name" '
| select(.client_info.android_client_info.package_name == $package)
] as $clients
| ($clients | length == 1)
and (.project_info.project_id | type == "string" and length > 0)
and (.project_info.project_number | type == "string" and length > 0)
and ($clients[0].client_info.mobilesdk_app_id | type == "string" and length > 0)
and (.project_info.project_id
| type == "string" and length > 0 and test("^[^\r\n]+$"))
and (.project_info.project_number
| type == "string" and length > 0 and test("^[^\r\n]+$"))
and ($clients[0].client_info.mobilesdk_app_id
| type == "string" and length > 0 and test("^[^\r\n]+$"))
and ($clients[0].client_info.mobilesdk_app_id
| startswith("1:" + $project_number + ":android:"))
and ($clients[0].api_key[0].current_key | type == "string" and length > 0)
and ($clients[0].api_key[0].current_key
| type == "string" and length > 0 and test("^[^\r\n]+$"))
' "$client_file" >/dev/null; then
echo "Firebase Android configuration does not contain exactly one complete client for package $package_name." >&2
exit 1

View File

@ -10,6 +10,31 @@ usage() {
echo "Usage: $0 DOMAIN [OUTPUT_FILE]" >&2
}
require_single_line_env_value() {
value_name=$1
value=$2
case "$value" in
*'
'*)
echo "$value_name must not contain control characters because .env stores one value per line." >&2
exit 1
;;
esac
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
echo "$value_name must not contain control characters because .env stores one value per line." >&2
exit 1
fi
case "$value" in
' '* | *' ' | \"* | \'* | *' #'*)
echo "$value_name cannot be represented safely as an unquoted Compose .env value." >&2
exit 1
;;
esac
}
if [ -z "$domain" ]; then
usage
exit 1
@ -69,6 +94,30 @@ test_upstream=${PRODUCTION_TEST_UPSTREAM:-who-need-help-test:4000}
edge_compose_project_name=${PRODUCTION_EDGE_COMPOSE_PROJECT_NAME:-who_need_help_edge}
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
require_single_line_env_value PRODUCTION_DATABASE_MODE "$database_mode"
require_single_line_env_value PRODUCTION_APP_TOPOLOGY "$app_topology"
require_single_line_env_value PRODUCTION_COMPOSE_PROJECT_NAME "$compose_project_name"
require_single_line_env_value PRODUCTION_PUBLIC_EDGE_ENABLED "$public_edge_enabled"
require_single_line_env_value PRODUCTION_PUBLIC_EDGE_NETWORK "$public_edge_network"
require_single_line_env_value PRODUCTION_PUBLIC_UPSTREAM_NAME "$public_upstream_name"
require_single_line_env_value PRODUCTION_CODEX_SESSION_ID "$codex_session_id"
require_single_line_env_value PRODUCTION_GOOGLE_OAUTH_CLIENT_ID "$google_oauth_client_id"
require_single_line_env_value PRODUCTION_GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_PUBLIC_KEY "$web_push_vapid_public_key"
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_PRIVATE_KEY "$web_push_vapid_private_key"
require_single_line_env_value PRODUCTION_WEB_PUSH_VAPID_SUBJECT "$web_push_vapid_subject"
require_single_line_env_value PRODUCTION_WNH_FIREBASE_APPLICATION_ID "$firebase_application_id"
require_single_line_env_value PRODUCTION_WNH_FIREBASE_API_KEY "$firebase_api_key"
require_single_line_env_value PRODUCTION_WNH_FIREBASE_PROJECT_ID "$firebase_project_id"
require_single_line_env_value PRODUCTION_WNH_FIREBASE_GCM_SENDER_ID "$firebase_sender_id"
require_single_line_env_value PRODUCTION_FCM_PROJECT_ID "$fcm_project_id"
require_single_line_env_value PRODUCTION_FCM_SERVICE_ACCOUNT_JSON_BASE64 "$fcm_service_account_json_base64"
require_single_line_env_value PRODUCTION_ANDROID_APP_LINKS_PACKAGE_NAME "$android_app_links_package_name"
require_single_line_env_value PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS "$android_app_links_fingerprints"
require_single_line_env_value PRODUCTION_TEST_DOMAIN "$test_domain"
require_single_line_env_value PRODUCTION_TEST_UPSTREAM "$test_upstream"
require_single_line_env_value PRODUCTION_EDGE_COMPOSE_PROJECT_NAME "$edge_compose_project_name"
if [ -z "$codex_session_id" ]; then
echo "PRODUCTION_CODEX_SESSION_ID is required for the Build Week feedback page." >&2
exit 1
@ -87,6 +136,7 @@ if { [ -n "$android_app_links_package_name" ] || [ -n "$android_app_links_finger
fi
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
firebase_configured=false
if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
for value in "$firebase_application_id" "$firebase_api_key" \
"$firebase_project_id" "$firebase_sender_id"; do
@ -95,6 +145,15 @@ if printf '%s\n' "$firebase_values" | grep -q '[^[:space:]]'; then
exit 1
}
done
firebase_prefix="1:$firebase_sender_id:android:"
if ! printf '%s\n' "$firebase_sender_id" | grep -Eq '^[0-9]+$' ||
[ "${firebase_application_id#"$firebase_prefix"}" = "$firebase_application_id" ] ||
[ -z "${firebase_application_id#"$firebase_prefix"}" ]; then
echo "Production Firebase application ID must belong to the configured numeric sender/project number." >&2
exit 1
fi
firebase_configured=true
fi
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
@ -106,6 +165,14 @@ if printf '%s\n' "$vapid_values" | grep -q '[^[:space:]]'; then
exit 1
}
done
case "$web_push_vapid_subject" in
mailto:?* | https://?*) ;;
*)
echo "Production WEB_PUSH_VAPID_SUBJECT must be a non-empty mailto: or https:// URI." >&2
exit 1
;;
esac
fi
if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } &&
@ -114,6 +181,7 @@ if { [ -n "$fcm_project_id" ] || [ -n "$fcm_service_account_json_base64" ]; } &&
exit 1
fi
fcm_credential_project_id=
if [ -n "$fcm_service_account_json_base64" ]; then
for command in base64 jq; do
command -v "$command" >/dev/null 2>&1 || {
@ -121,17 +189,38 @@ if [ -n "$fcm_service_account_json_base64" ]; then
exit 1
}
done
if ! printf '%s' "$fcm_service_account_json_base64" |
if ! fcm_credential_project_id=$(printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null |
jq -e '
.type == "service_account" and
(.project_id | type == "string" and length > 0) and
(.client_email | type == "string" and length > 0) and
(.private_key | type == "string" and length > 0)
' >/dev/null 2>&1; then
jq -er '
. as $credential
| (
($credential.type == "service_account") and
($credential.project_id | type == "string" and length > 0) and
($credential.client_email | type == "string" and length > 0) and
($credential.private_key | type == "string" and length > 0)
)
| if . then $credential.project_id else error("incomplete service account") end
' 2>/dev/null); then
echo "Production FCM credential is not a complete service-account JSON document." >&2
exit 1
fi
if [ "$fcm_credential_project_id" != "$fcm_project_id" ]; then
echo "Production FCM service-account project must match PRODUCTION_FCM_PROJECT_ID." >&2
exit 1
fi
if [ "$firebase_configured" = true ] &&
[ "$fcm_project_id" != "$firebase_project_id" ]; then
echo "Production Firebase Android client and FCM service account must use the same project." >&2
exit 1
fi
fi
if [ -n "$android_app_links_package_name" ] &&
[ "$android_app_links_package_name" != org.whoneedhelp.mobile ]; then
echo "Production Android App Links package must be org.whoneedhelp.mobile." >&2
exit 1
fi
case "$compose_project_name" in
@ -203,6 +292,22 @@ smtp_ssl=${PRODUCTION_SMTP_SSL:-false}
email_from_address=${PRODUCTION_EMAIL_FROM_ADDRESS:-"contact@$domain"}
support_inbox_address=${PRODUCTION_SUPPORT_INBOX_ADDRESS:-}
require_single_line_env_value PRODUCTION_DATABASE_URL "$database_url"
require_single_line_env_value PRODUCTION_DATABASE_SOCKET_DIR "$database_socket_dir"
require_single_line_env_value PRODUCTION_HTTP_BIND_ADDRESS "$http_bind_address"
require_single_line_env_value PRODUCTION_HTTP_PORT "$http_port"
require_single_line_env_value PRODUCTION_TRAEFIK_TRUSTED_IPS "$trusted_proxy_ips"
require_single_line_env_value PRODUCTION_EMAIL_DELIVERY_PROVIDER "$email_delivery_provider"
require_single_line_env_value PRODUCTION_SMTP_RELAY "$smtp_relay"
require_single_line_env_value PRODUCTION_SMTP_PORT "$smtp_port"
require_single_line_env_value PRODUCTION_SMTP_USERNAME "$smtp_username"
require_single_line_env_value PRODUCTION_SMTP_PASSWORD "$smtp_password"
require_single_line_env_value PRODUCTION_SMTP_AUTH "$smtp_auth"
require_single_line_env_value PRODUCTION_SMTP_TLS "$smtp_tls"
require_single_line_env_value PRODUCTION_SMTP_SSL "$smtp_ssl"
require_single_line_env_value PRODUCTION_EMAIL_FROM_ADDRESS "$email_from_address"
require_single_line_env_value PRODUCTION_SUPPORT_INBOX_ADDRESS "$support_inbox_address"
[ "$email_delivery_provider" = smtp ] || {
echo "PRODUCTION_EMAIL_DELIVERY_PROVIDER must be smtp." >&2
exit 1
@ -326,6 +431,10 @@ TEST_UPSTREAM_VALUE=$test_upstream \
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = ""
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
for (key in replacement) {
gsub(/\$/, "$$", replacement[key])
}
}
{
separator = index($0, "=")
@ -347,6 +456,7 @@ unset postgres_password secret_key_base handover_secret release_cookie metrics_t
unset smtp_password
unset google_oauth_client_secret
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
unset fcm_credential_project_id
unset android_app_links_fingerprints
echo "Generated independent deployment secrets without printing them."

View File

@ -11,6 +11,27 @@ if [[ -z "$domain" ]]; then
exit 1
fi
require_single_line_env_value() {
local value_name=$1
local value=$2
if [[ "$value" == *$'\n'* ]]; then
echo "$value_name must not contain control characters because .env stores one value per line." >&2
exit 1
fi
if printf '%s' "$value" | LC_ALL=C grep -q '[[:cntrl:]]'; then
echo "$value_name must not contain control characters because .env stores one value per line." >&2
exit 1
fi
if [[ "$value" == ' '* || "$value" == *' ' ||
"$value" == \"* || "$value" == \'* || "$value" == *' #'* ]]; then
echo "$value_name cannot be represented safely as an unquoted Compose .env value." >&2
exit 1
fi
}
if [[ ! "$domain" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$ ]]; then
echo "DOMAIN must be a lowercase ASCII DNS hostname without a scheme, port, or path." >&2
exit 1
@ -20,7 +41,7 @@ if [[ "$target" != /* ]]; then
target="$ROOT/$target"
fi
for command in awk docker git mktemp openssl stat; do
for command in awk docker git grep mktemp openssl stat; do
command -v "$command" >/dev/null 2>&1 || {
echo "Required command is unavailable: $command" >&2
exit 1
@ -67,6 +88,29 @@ android_app_links_fingerprints=${TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS
support_inbox_address=${TEST_SUPPORT_INBOX_ADDRESS:-}
git_sha=$(git -C "$ROOT" rev-parse --short=12 HEAD)
require_single_line_env_value TEST_COMPOSE_PROJECT_NAME "$compose_project_name"
require_single_line_env_value TEST_PUBLIC_EDGE_NETWORK "$public_edge_network"
require_single_line_env_value TEST_PUBLIC_UPSTREAM_NAME "$public_upstream_name"
require_single_line_env_value TEST_HTTP_BIND_ADDRESS "$http_bind_address"
require_single_line_env_value TEST_HTTP_PORT "$http_port"
require_single_line_env_value TEST_MAILPIT_BIND_ADDRESS "$mailpit_bind_address"
require_single_line_env_value TEST_MAILPIT_PORT "$mailpit_port"
require_single_line_env_value TEST_CODEX_SESSION_ID "$codex_session_id"
require_single_line_env_value TEST_GOOGLE_OAUTH_CLIENT_ID "$google_oauth_client_id"
require_single_line_env_value TEST_GOOGLE_OAUTH_CLIENT_SECRET "$google_oauth_client_secret"
require_single_line_env_value TEST_WEB_PUSH_VAPID_PUBLIC_KEY "$web_push_vapid_public_key"
require_single_line_env_value TEST_WEB_PUSH_VAPID_PRIVATE_KEY "$web_push_vapid_private_key"
require_single_line_env_value TEST_WEB_PUSH_VAPID_SUBJECT "$web_push_vapid_subject"
require_single_line_env_value TEST_WNH_FIREBASE_APPLICATION_ID "$firebase_application_id"
require_single_line_env_value TEST_WNH_FIREBASE_API_KEY "$firebase_api_key"
require_single_line_env_value TEST_WNH_FIREBASE_PROJECT_ID "$firebase_project_id"
require_single_line_env_value TEST_WNH_FIREBASE_GCM_SENDER_ID "$firebase_sender_id"
require_single_line_env_value TEST_FCM_PROJECT_ID "$fcm_project_id"
require_single_line_env_value TEST_FCM_SERVICE_ACCOUNT_JSON_BASE64 "$fcm_service_account_json_base64"
require_single_line_env_value TEST_ANDROID_APP_LINKS_PACKAGE_NAME "$android_app_links_package_name"
require_single_line_env_value TEST_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS "$android_app_links_fingerprints"
require_single_line_env_value TEST_SUPPORT_INBOX_ADDRESS "$support_inbox_address"
[[ "$compose_project_name" =~ ^[a-zA-Z0-9_-]+$ ]] || {
echo "TEST_COMPOSE_PROJECT_NAME contains unsupported characters." >&2
exit 1
@ -98,6 +142,7 @@ if [[ -n "$android_app_links_package_name" || -n "$android_app_links_fingerprint
fi
firebase_values="$firebase_application_id $firebase_api_key $firebase_project_id $firebase_sender_id"
firebase_configured=false
if grep -q '[^[:space:]]' <<<"$firebase_values"; then
for value in "$firebase_application_id" "$firebase_api_key" \
"$firebase_project_id" "$firebase_sender_id"; do
@ -106,6 +151,14 @@ if grep -q '[^[:space:]]' <<<"$firebase_values"; then
exit 1
}
done
firebase_prefix="1:$firebase_sender_id:android:"
if [[ ! "$firebase_sender_id" =~ ^[0-9]+$ ||
"$firebase_application_id" != "$firebase_prefix"?* ]]; then
echo "Test Firebase application ID must belong to the configured numeric sender/project number." >&2
exit 1
fi
firebase_configured=true
fi
vapid_values="$web_push_vapid_public_key $web_push_vapid_private_key $web_push_vapid_subject"
@ -117,6 +170,12 @@ if grep -q '[^[:space:]]' <<<"$vapid_values"; then
exit 1
}
done
if [[ "$web_push_vapid_subject" != mailto:?* &&
"$web_push_vapid_subject" != https://?* ]]; then
echo "Test WEB_PUSH_VAPID_SUBJECT must be a non-empty mailto: or https:// URI." >&2
exit 1
fi
fi
if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") &&
@ -125,6 +184,7 @@ if [[ (-n "$fcm_project_id" || -n "$fcm_service_account_json_base64") &&
exit 1
fi
fcm_credential_project_id=
if [[ -n "$fcm_service_account_json_base64" ]]; then
for command in base64 jq; do
command -v "$command" >/dev/null 2>&1 || {
@ -132,17 +192,38 @@ if [[ -n "$fcm_service_account_json_base64" ]]; then
exit 1
}
done
if ! printf '%s' "$fcm_service_account_json_base64" |
if ! fcm_credential_project_id=$(printf '%s' "$fcm_service_account_json_base64" |
base64 --decode 2>/dev/null |
jq -e '
.type == "service_account" and
(.project_id | type == "string" and length > 0) and
(.client_email | type == "string" and length > 0) and
(.private_key | type == "string" and length > 0)
' >/dev/null 2>&1; then
jq -er '
. as $credential
| (
($credential.type == "service_account") and
($credential.project_id | type == "string" and length > 0) and
($credential.client_email | type == "string" and length > 0) and
($credential.private_key | type == "string" and length > 0)
)
| if . then $credential.project_id else error("incomplete service account") end
' 2>/dev/null); then
echo "Test FCM credential is not a complete service-account JSON document." >&2
exit 1
fi
if [[ "$fcm_credential_project_id" != "$fcm_project_id" ]]; then
echo "Test FCM service-account project must match TEST_FCM_PROJECT_ID." >&2
exit 1
fi
if [[ "$firebase_configured" == true &&
"$fcm_project_id" != "$firebase_project_id" ]]; then
echo "Test Firebase Android client and FCM service account must use the same project." >&2
exit 1
fi
fi
if [[ -n "$android_app_links_package_name" &&
"$android_app_links_package_name" != org.whoneedhelp.mobile.staging ]]; then
echo "Test Android App Links package must be org.whoneedhelp.mobile.staging." >&2
exit 1
fi
for pair in "HTTP:$http_port" "MAILPIT:$mailpit_port"; do
@ -267,6 +348,10 @@ CODEX_SESSION_ID_VALUE=$codex_session_id \
replacement["WNH_ANDROID_DEVELOPMENT_SIGNING_KEY_ALIAS"] = ""
replacement["WNH_ANDROID_STAGING_SIGNING_KEY_ALIAS"] = "who-need-help-staging"
replacement["CODEX_SESSION_ID"] = ENVIRON["CODEX_SESSION_ID_VALUE"]
for (key in replacement) {
gsub(/\$/, "$$", replacement[key])
}
}
{
separator = index($0, "=")
@ -282,6 +367,7 @@ trap - EXIT HUP INT TERM
unset postgres_password secret_key_base handover_secret release_cookie metrics_token
unset google_oauth_client_secret
unset web_push_vapid_private_key firebase_api_key fcm_service_account_json_base64
unset fcm_credential_project_id
unset android_app_links_fingerprints
"$ROOT/scripts/compose.sh" "$target" config --quiet

View File

@ -105,7 +105,8 @@ 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' >"$credential_values"
'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"
@ -117,6 +118,32 @@ 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"
@ -212,6 +239,41 @@ if ./scripts/import-firebase-android-config.sh \
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 Android environment isolation"
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"
@ -277,6 +339,28 @@ printf '%s' "$credential_fcm_base64" |
'.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' \
@ -304,6 +388,16 @@ quality_fcm_base64=$(
'{"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
@ -312,7 +406,16 @@ if ./scripts/init-test-env.sh test.help.test \
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_CLIENT_SECRET="quality-test\$secret" \
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
@ -332,6 +435,12 @@ 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 '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
@ -347,6 +456,65 @@ if TEST_CODEX_SESSION_ID=00000000-0000-0000-0000-000000000001 \
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 \
@ -366,7 +534,7 @@ 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_PASSWORD="quality\$password" \
PRODUCTION_SMTP_AUTH=always \
PRODUCTION_SMTP_TLS=always \
PRODUCTION_SMTP_SSL=false \
@ -388,8 +556,67 @@ PRODUCTION_ANDROID_APP_LINKS_SHA256_CERT_FINGERPRINTS=A5:74:2B:AE:70:C6:D0:34:E3
PRODUCTION_SUPPORT_INBOX_ADDRESS=support@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
./scripts/validate-production-env.sh "$production_env" help.test >/dev/null
./scripts/check-environment-readiness.sh "$production_env" --require-release >/dev/null
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 \
./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_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"}' |
@ -984,6 +1211,105 @@ 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

View File

@ -55,7 +55,9 @@ if ! awk '
exit 40
}
replacement[key] = substr($0, index($0, "=") + 1)
value = substr($0, index($0, "=") + 1)
gsub(/\$/, "$$", value)
replacement[key] = value
replacement_count++
next
}