mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
PROJECT_DIR="${TEST_ROOT}/project"
|
||||
FAKE_BIN="${TEST_ROOT}/fake-bin"
|
||||
mkdir -p "${PROJECT_DIR}" "${FAKE_BIN}"
|
||||
cp "${REPO_ROOT}/deploy.sh" "${PROJECT_DIR}/deploy.sh"
|
||||
chmod 0755 "${PROJECT_DIR}/deploy.sh"
|
||||
printf 'FROM scratch\n' >"${PROJECT_DIR}/Dockerfile.app.local"
|
||||
printf 'services: {}\n' >"${PROJECT_DIR}/docker-compose.yml"
|
||||
printf 'services: {}\n' >"${PROJECT_DIR}/docker-compose.local.yml"
|
||||
|
||||
cat >"${FAKE_BIN}/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "${1:-}" == "image" && "${2:-}" == "inspect" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
if [[ " $* " == *" ps -q "* ]]; then
|
||||
printf 'fixture-container\n'
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "${FAKE_BIN}/docker"
|
||||
cp "${FAKE_BIN}/docker" "${FAKE_BIN}/docker-compose"
|
||||
|
||||
VICTIM="${TEST_ROOT}/victim"
|
||||
printf 'known-good\n' >"${VICTIM}"
|
||||
ln -s -- "${VICTIM}" "${PROJECT_DIR}/.code-hash"
|
||||
|
||||
if (cd -- "${PROJECT_DIR}" && PATH="${FAKE_BIN}:${PATH}" bash ./deploy.sh) >/dev/null 2>&1; then
|
||||
fail_test "deploy accepted a symbolic-link build-state file"
|
||||
fi
|
||||
[[ "$(cat -- "${VICTIM}")" == "known-good" ]] \
|
||||
|| fail_test "deploy overwrote the symbolic-link target"
|
||||
[[ -L "${PROJECT_DIR}/.code-hash" ]] \
|
||||
|| fail_test "deploy unexpectedly replaced the rejected symbolic link"
|
||||
|
||||
rm -f -- "${PROJECT_DIR}/.code-hash"
|
||||
(cd -- "${PROJECT_DIR}" && PATH="${FAKE_BIN}:${PATH}" bash ./deploy.sh) >/dev/null
|
||||
|
||||
[[ -f "${PROJECT_DIR}/.code-hash" && ! -L "${PROJECT_DIR}/.code-hash" ]] \
|
||||
|| fail_test "deploy did not atomically create a regular build-state file"
|
||||
mode="$(stat -c '%a' "${PROJECT_DIR}/.code-hash" 2>/dev/null || stat -f '%Lp' "${PROJECT_DIR}/.code-hash")"
|
||||
[[ "${mode}" == "600" ]] || fail_test "deploy build-state mode is ${mode}, expected 600"
|
||||
if find "${PROJECT_DIR}" -maxdepth 1 -name '.code-hash.tmp.*' -print -quit | grep -q .; then
|
||||
fail_test "deploy left a temporary build-state file"
|
||||
fi
|
||||
|
||||
echo "PASS: deploy build-state symlink and atomic-write fixtures"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_archive_rejected() {
|
||||
local archive="$1"
|
||||
if (validate_release_archive "${archive}" >/dev/null 2>&1); then
|
||||
fail_test "unsafe archive was accepted: ${archive}"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "${TEST_ROOT}/bundle/bin" "${TEST_ROOT}/bundle/frontend"
|
||||
printf '#!/bin/sh\nexit 0\n' >"${TEST_ROOT}/bundle/bin/aether-gateway"
|
||||
printf '<!doctype html>\n' >"${TEST_ROOT}/bundle/frontend/index.html"
|
||||
chmod 0755 "${TEST_ROOT}/bundle/bin/aether-gateway"
|
||||
tar -czf "${TEST_ROOT}/valid.tar.gz" -C "${TEST_ROOT}" bundle
|
||||
|
||||
TMP_ROOT="${TEST_ROOT}/validation"
|
||||
mkdir -p "${TMP_ROOT}"
|
||||
[[ "$(validate_release_archive "${TEST_ROOT}/valid.tar.gz" bundle)" == "bundle" ]] \
|
||||
|| fail_test "valid release archive was rejected"
|
||||
|
||||
original_entry_limit="${MAX_RELEASE_ARCHIVE_ENTRIES}"
|
||||
MAX_RELEASE_ARCHIVE_ENTRIES=2
|
||||
assert_archive_rejected "${TEST_ROOT}/valid.tar.gz"
|
||||
MAX_RELEASE_ARCHIVE_ENTRIES="${original_entry_limit}"
|
||||
|
||||
original_size_limit="${MAX_RELEASE_UNPACKED_BYTES}"
|
||||
MAX_RELEASE_UNPACKED_BYTES=1
|
||||
assert_archive_rejected "${TEST_ROOT}/valid.tar.gz"
|
||||
MAX_RELEASE_UNPACKED_BYTES="${original_size_limit}"
|
||||
|
||||
mkdir -p "${TEST_ROOT}/linked-bundle"
|
||||
ln -s /etc/passwd "${TEST_ROOT}/linked-bundle/aether-gateway"
|
||||
tar -czf "${TEST_ROOT}/link.tar.gz" -C "${TEST_ROOT}" linked-bundle
|
||||
assert_archive_rejected "${TEST_ROOT}/link.tar.gz"
|
||||
|
||||
echo "PASS: installer archive validation fixtures"
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_generated_key_output() {
|
||||
local label="$1"
|
||||
local output="$2"
|
||||
local key value
|
||||
local -a values=()
|
||||
|
||||
for key in \
|
||||
JWT_SECRET_KEY ENCRYPTION_KEY DB_PASSWORD REDIS_PASSWORD \
|
||||
MYSQL_PASSWORD MYSQL_ROOT_PASSWORD; do
|
||||
value="$(printf '%s\n' "${output}" | awk -F= -v key="${key}" '
|
||||
$1 == key { print substr($0, length(key) + 2); exit }
|
||||
')"
|
||||
[[ "${value}" =~ ^[A-Za-z0-9_-]{40,}$ ]] \
|
||||
|| fail_test "${label} did not generate a strong URL-safe ${key}"
|
||||
values+=("${value}")
|
||||
done
|
||||
[[ "$(printf '%s\n' "${values[@]}" | sort -u | wc -l | tr -d '[:space:]')" == "6" ]] \
|
||||
|| fail_test "${label} reused a generated secret"
|
||||
}
|
||||
|
||||
assert_line() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
grep -Fqx -- "${expected}" "${file}" \
|
||||
|| fail_test "missing expected line in ${file}: ${expected}"
|
||||
}
|
||||
|
||||
APP_DOCKERFILE="${REPO_ROOT}/Dockerfile.app"
|
||||
COMPOSE_FILES=(
|
||||
"${REPO_ROOT}/docker-compose.yml"
|
||||
"${REPO_ROOT}/docker-compose.single-node.yml"
|
||||
)
|
||||
|
||||
assert_line "${APP_DOCKERFILE}" "USER 65532:65532"
|
||||
assert_line "${APP_DOCKERFILE}" " HOME=/tmp/aether-home \\"
|
||||
if grep -Eq '^USER[[:space:]]+(root|0)(:0)?[[:space:]]*$' "${APP_DOCKERFILE}"; then
|
||||
fail_test "production image still selects a root runtime identity"
|
||||
fi
|
||||
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
assert_line "${compose_file}" \
|
||||
' user: "${AETHER_CONTAINER_UID:-65532}:${AETHER_CONTAINER_GID:-65532}"'
|
||||
assert_line "${compose_file}" " read_only: true"
|
||||
assert_line "${compose_file}" " cap_drop:"
|
||||
assert_line "${compose_file}" " - ALL"
|
||||
assert_line "${compose_file}" " security_opt:"
|
||||
assert_line "${compose_file}" " - no-new-privileges:true"
|
||||
assert_line "${compose_file}" " tmpfs:"
|
||||
assert_line "${compose_file}" " - /tmp:rw,nosuid,nodev,noexec,mode=1777"
|
||||
if grep -Eq '^[[:space:]]+user:[[:space:]]+"?(root|0)(:0)?"?[[:space:]]*$' "${compose_file}"; then
|
||||
fail_test "production Compose file still selects a root runtime identity: ${compose_file}"
|
||||
fi
|
||||
done
|
||||
|
||||
standard_install_body="$(declare -f install_compose_mode)"
|
||||
single_node_install_body="$(declare -f install_compose_single_node_mode)"
|
||||
if grep -Fq "prepare_compose_single_node_data_directory" <<<"${standard_install_body}"; then
|
||||
fail_test "standard Postgres Compose install unexpectedly prepares a SQLite data directory"
|
||||
fi
|
||||
grep -Fq "prepare_compose_single_node_data_directory" <<<"${single_node_install_body}" \
|
||||
|| fail_test "single-node Compose install does not prepare its SQLite data directory"
|
||||
|
||||
assert_line "${REPO_ROOT}/.env.example" "DB_PASSWORD="
|
||||
assert_line "${REPO_ROOT}/.env.example" "REDIS_PASSWORD="
|
||||
assert_line "${REPO_ROOT}/.env.example" "MYSQL_PASSWORD="
|
||||
assert_line "${REPO_ROOT}/.env.example" "MYSQL_ROOT_PASSWORD="
|
||||
assert_line "${REPO_ROOT}/README.md" "chmod 600 .env"
|
||||
assert_line "${REPO_ROOT}/README.md" \
|
||||
"docker compose -f docker-compose.single-node.yml stop app"
|
||||
assert_line "${REPO_ROOT}/README.md" \
|
||||
"sudo chown -R -P 65532:65532 ./data"
|
||||
grep -Fq '${DB_PASSWORD:?set DB_PASSWORD in .env}' "${REPO_ROOT}/docker-compose.yml" \
|
||||
|| fail_test "Postgres password is not required by Compose"
|
||||
grep -Fq '${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}' "${REPO_ROOT}/docker-compose.yml" \
|
||||
|| fail_test "Redis password is not required by Compose"
|
||||
grep -Fq '${MYSQL_PASSWORD:?set MYSQL_PASSWORD in .env}' "${REPO_ROOT}/docker-compose.yml" \
|
||||
|| fail_test "MySQL application password is not required by Compose"
|
||||
grep -Fq '${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}' "${REPO_ROOT}/docker-compose.yml" \
|
||||
|| fail_test "MySQL root password is not required by Compose"
|
||||
if grep -Eq '(DB_PASSWORD|REDIS_PASSWORD|MYSQL_PASSWORD|MYSQL_ROOT_PASSWORD):?-?=?aether(_root)?([}"[:space:]]|$)' \
|
||||
"${REPO_ROOT}/docker-compose.yml" "${REPO_ROOT}/.env.example"; then
|
||||
fail_test "production Compose configuration contains a weak infrastructure password default"
|
||||
fi
|
||||
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'chmod -R u+rwX "${TEST_ROOT}" 2>/dev/null || true; rm -rf "${TEST_ROOT}"' EXIT
|
||||
COMPOSE_DIR="${TEST_ROOT}/compose"
|
||||
mkdir -p "${COMPOSE_DIR}/data"
|
||||
fake_bin="${TEST_ROOT}/fake-bin"
|
||||
mkdir -p "${fake_bin}"
|
||||
printf '#!/usr/bin/env bash\nprintf "false\\n"\n' >"${fake_bin}/docker"
|
||||
chmod 0755 "${fake_bin}/docker"
|
||||
PATH="${fake_bin}:${PATH}"
|
||||
fixture_uid="$(id -u)"
|
||||
fixture_gid="$(id -g)"
|
||||
[[ "${fixture_uid}" != "0" ]] || fixture_uid="65532"
|
||||
[[ "${fixture_gid}" != "0" ]] || fixture_gid="65532"
|
||||
printf 'AETHER_CONTAINER_UID=%s\nAETHER_CONTAINER_GID=%s\n' \
|
||||
"${fixture_uid}" "${fixture_gid}" >"${COMPOSE_DIR}/.env"
|
||||
printf 'sqlite fixture\n' >"${COMPOSE_DIR}/data/aether.db"
|
||||
chmod 0755 "${COMPOSE_DIR}/data/aether.db"
|
||||
chmod 4755 "${COMPOSE_DIR}/data/aether.db" 2>/dev/null || true
|
||||
|
||||
prepare_compose_single_node_data_directory
|
||||
[[ "$(stat_file_mode "${COMPOSE_DIR}/data")" == "700" ]] \
|
||||
|| fail_test "single-node data directory was not restricted to mode 0700"
|
||||
[[ "$(stat_file_mode "${COMPOSE_DIR}/data/aether.db")" == "600" ]] \
|
||||
|| fail_test "single-node SQLite file was not restricted to mode 0600"
|
||||
|
||||
printf 'AETHER_CONTAINER_UID=0\nAETHER_CONTAINER_GID=%s\n' \
|
||||
"${fixture_gid}" >"${COMPOSE_DIR}/.env"
|
||||
if (prepare_compose_single_node_data_directory) >/dev/null 2>&1; then
|
||||
fail_test "root container uid was accepted"
|
||||
fi
|
||||
|
||||
printf 'AETHER_CONTAINER_UID=%s\nAETHER_CONTAINER_GID=%s\n' \
|
||||
"${fixture_uid}" "${fixture_gid}" >"${COMPOSE_DIR}/.env"
|
||||
ln -s "${TEST_ROOT}" "${COMPOSE_DIR}/data/unsafe-link"
|
||||
if (prepare_compose_single_node_data_directory) >/dev/null 2>&1; then
|
||||
fail_test "symbolic link inside the managed SQLite directory was accepted"
|
||||
fi
|
||||
|
||||
rm -f "${COMPOSE_DIR}/data/unsafe-link"
|
||||
mkfifo "${COMPOSE_DIR}/data/unsafe-fifo"
|
||||
if (prepare_compose_single_node_data_directory) >/dev/null 2>&1; then
|
||||
fail_test "FIFO inside the managed SQLite directory was accepted"
|
||||
fi
|
||||
rm -f "${COMPOSE_DIR}/data/unsafe-fifo"
|
||||
|
||||
printf '#!/usr/bin/env bash\nprintf "true\\n"\n' >"${fake_bin}/docker"
|
||||
chmod 0755 "${fake_bin}/docker"
|
||||
if (prepare_compose_single_node_data_directory) >/dev/null 2>&1; then
|
||||
fail_test "running app container did not block SQLite permission migration"
|
||||
fi
|
||||
printf '#!/usr/bin/env bash\nprintf "false\\n"\n' >"${fake_bin}/docker"
|
||||
chmod 0755 "${fake_bin}/docker"
|
||||
|
||||
ln "${COMPOSE_DIR}/data/aether.db" "${COMPOSE_DIR}/data/unsafe-hardlink"
|
||||
if (prepare_compose_single_node_data_directory) >/dev/null 2>&1; then
|
||||
fail_test "hard link inside the managed SQLite directory was accepted"
|
||||
fi
|
||||
rm -f "${COMPOSE_DIR}/data/unsafe-hardlink"
|
||||
|
||||
cp "${REPO_ROOT}/.env.example" "${COMPOSE_DIR}/.env.example"
|
||||
ADMIN_PASSWORD="test-admin-password"
|
||||
APP_IMAGE="example.invalid/aether:test"
|
||||
AETHER_CONTAINER_UID="${fixture_uid}"
|
||||
AETHER_CONTAINER_GID="${fixture_gid}"
|
||||
JWT_SECRET_KEY=""
|
||||
ENCRYPTION_KEY=""
|
||||
generated_env="${TEST_ROOT}/generated.env"
|
||||
generate_compose_env "${generated_env}"
|
||||
|
||||
generated_secrets=()
|
||||
for key in \
|
||||
JWT_SECRET_KEY ENCRYPTION_KEY DB_PASSWORD REDIS_PASSWORD \
|
||||
MYSQL_PASSWORD MYSQL_ROOT_PASSWORD; do
|
||||
value="$(env_file_value "${generated_env}" "${key}")"
|
||||
[[ "${value}" =~ ^[A-Za-z0-9_-]{40,}$ ]] \
|
||||
|| fail_test "installer did not generate a strong URL-safe ${key}"
|
||||
generated_secrets+=("${value}")
|
||||
done
|
||||
[[ "$(printf '%s\n' "${generated_secrets[@]}" | sort -u | wc -l | tr -d '[:space:]')" == "6" ]] \
|
||||
|| fail_test "installer reused a generated secret"
|
||||
|
||||
assert_generated_key_output \
|
||||
"repository generate_keys.sh" "$("${REPO_ROOT}/generate_keys.sh")"
|
||||
generated_key_script="${TEST_ROOT}/generated-keys.sh"
|
||||
write_generate_keys_script "${generated_key_script}"
|
||||
assert_generated_key_output \
|
||||
"installer-generated key script" "$("${generated_key_script}")"
|
||||
|
||||
injection_env="${TEST_ROOT}/injection.env"
|
||||
printf 'SAFE=value\n' >"${injection_env}"
|
||||
if (replace_or_append_env "${injection_env}" "SAFE" $'value\nINJECTED=true') >/dev/null 2>&1; then
|
||||
fail_test "dotenv CR/LF injection was accepted"
|
||||
fi
|
||||
grep -Fqx "SAFE=value" "${injection_env}" \
|
||||
|| fail_test "rejected dotenv injection still modified the target"
|
||||
if grep -Fq "INJECTED" "${injection_env}"; then
|
||||
fail_test "rejected dotenv injection added another variable"
|
||||
fi
|
||||
|
||||
literal_backslash_value='literal\nINJECTED=true'
|
||||
replace_or_append_env "${injection_env}" "SAFE" "${literal_backslash_value}"
|
||||
grep -Fqx "SAFE=${literal_backslash_value}" "${injection_env}" \
|
||||
|| fail_test "dotenv replacement interpreted a literal backslash escape"
|
||||
[[ "$(wc -l <"${injection_env}" | tr -d '[:space:]')" == "1" ]] \
|
||||
|| fail_test "literal backslash escape injected another dotenv line"
|
||||
|
||||
echo "PASS: production container runtime hardening fixtures"
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
|
||||
TEST_ROOT="$(mktemp -d)/aether installer fixtures"
|
||||
mkdir -p "${TEST_ROOT}"
|
||||
|
||||
cleanup_test_root() {
|
||||
rm -rf -- "$(dirname -- "${TEST_ROOT}")"
|
||||
}
|
||||
trap cleanup_test_root EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_link_target() {
|
||||
local link="$1"
|
||||
local expected="$2"
|
||||
[[ -L "${link}" ]] || fail_test "${link} is not a symbolic link"
|
||||
[[ "$(readlink "${link}")" == "${expected}" ]] \
|
||||
|| fail_test "${link} does not point to ${expected}"
|
||||
}
|
||||
|
||||
assert_switch_rejected() {
|
||||
local release_dir="$1"
|
||||
local current_link="$2"
|
||||
if (switch_current_release_link "${release_dir}" "${current_link}"); then
|
||||
fail_test "unsafe current-link fixture was accepted: ${current_link}"
|
||||
fi
|
||||
}
|
||||
|
||||
test_initial_switch() {
|
||||
local fixture="${TEST_ROOT}/initial"
|
||||
local release_dir="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${release_dir}"
|
||||
|
||||
switch_current_release_link "${release_dir}" "${current_link}"
|
||||
|
||||
assert_link_target "${current_link}" "${release_dir}"
|
||||
[[ ! -e "${current_link}.new" && ! -L "${current_link}.new" ]] \
|
||||
|| fail_test "temporary link remained after initial switch"
|
||||
}
|
||||
|
||||
test_existing_link_switch() {
|
||||
local fixture="${TEST_ROOT}/existing link"
|
||||
local old_release="${fixture}/release old"
|
||||
local new_release="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${old_release}" "${new_release}"
|
||||
ln -s -- "${old_release}" "${current_link}"
|
||||
|
||||
switch_current_release_link "${new_release}" "${current_link}"
|
||||
|
||||
assert_link_target "${current_link}" "${new_release}"
|
||||
[[ ! -e "${old_release}/current.new" && ! -L "${old_release}/current.new" ]] \
|
||||
|| fail_test "temporary link was moved into the previous release directory"
|
||||
}
|
||||
|
||||
test_dangling_links_are_replaced() {
|
||||
local fixture="${TEST_ROOT}/dangling links"
|
||||
local release_dir="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${release_dir}"
|
||||
ln -s -- "${fixture}/missing current target" "${current_link}"
|
||||
ln -s -- "${fixture}/missing temporary target" "${current_link}.new"
|
||||
|
||||
switch_current_release_link "${release_dir}" "${current_link}"
|
||||
|
||||
assert_link_target "${current_link}" "${release_dir}"
|
||||
[[ ! -e "${current_link}.new" && ! -L "${current_link}.new" ]] \
|
||||
|| fail_test "temporary dangling link remained after switch"
|
||||
}
|
||||
|
||||
test_current_file_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/current file"
|
||||
local release_dir="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${release_dir}"
|
||||
printf '%s\n' "keep-current-file" >"${current_link}"
|
||||
|
||||
assert_switch_rejected "${release_dir}" "${current_link}"
|
||||
|
||||
[[ "$(cat "${current_link}")" == "keep-current-file" ]] \
|
||||
|| fail_test "current file was modified"
|
||||
[[ ! -e "${current_link}.new" && ! -L "${current_link}.new" ]] \
|
||||
|| fail_test "temporary link was created for an unsafe current file"
|
||||
}
|
||||
|
||||
test_current_directory_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/current directory"
|
||||
local release_dir="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${release_dir}" "${current_link}"
|
||||
|
||||
assert_switch_rejected "${release_dir}" "${current_link}"
|
||||
|
||||
[[ -d "${current_link}" && ! -L "${current_link}" ]] \
|
||||
|| fail_test "current directory was modified"
|
||||
[[ ! -e "${current_link}/current.new" && ! -L "${current_link}/current.new" ]] \
|
||||
|| fail_test "temporary link was moved into an unsafe current directory"
|
||||
}
|
||||
|
||||
test_temporary_file_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/temporary file"
|
||||
local old_release="${fixture}/release old"
|
||||
local new_release="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${old_release}" "${new_release}"
|
||||
ln -s -- "${old_release}" "${current_link}"
|
||||
printf '%s\n' "keep-temporary-file" >"${current_link}.new"
|
||||
|
||||
assert_switch_rejected "${new_release}" "${current_link}"
|
||||
|
||||
assert_link_target "${current_link}" "${old_release}"
|
||||
[[ "$(cat "${current_link}.new")" == "keep-temporary-file" ]] \
|
||||
|| fail_test "temporary file was modified"
|
||||
}
|
||||
|
||||
test_temporary_directory_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/temporary directory"
|
||||
local old_release="${fixture}/release old"
|
||||
local new_release="${fixture}/release new"
|
||||
local current_link="${fixture}/current"
|
||||
mkdir -p "${old_release}" "${new_release}" "${current_link}.new"
|
||||
ln -s -- "${old_release}" "${current_link}"
|
||||
|
||||
assert_switch_rejected "${new_release}" "${current_link}"
|
||||
|
||||
assert_link_target "${current_link}" "${old_release}"
|
||||
[[ -d "${current_link}.new" && ! -L "${current_link}.new" ]] \
|
||||
|| fail_test "temporary directory was modified"
|
||||
}
|
||||
|
||||
test_managed_directory_link_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/managed directory link"
|
||||
local target="${fixture}/target"
|
||||
local link="${fixture}/managed"
|
||||
mkdir -p "${target}"
|
||||
ln -s -- "${target}" "${link}"
|
||||
|
||||
if (ensure_directory "${link}"); then
|
||||
fail_test "managed directory symbolic link was accepted"
|
||||
fi
|
||||
}
|
||||
|
||||
test_existing_release_is_never_rewritten_in_place() {
|
||||
local fixture="${TEST_ROOT}/immutable release"
|
||||
local requested="${fixture}/releases/v1.2.3"
|
||||
local selected
|
||||
mkdir -p "${requested}"
|
||||
printf '%s\n' "old-release" >"${requested}/marker"
|
||||
|
||||
selected="$(select_release_install_directory "${requested}")"
|
||||
|
||||
[[ "${selected}" != "${requested}" ]] \
|
||||
|| fail_test "an existing release directory was selected for in-place rewriting"
|
||||
[[ -d "${selected}" && ! -L "${selected}" ]] \
|
||||
|| fail_test "replacement release directory was not allocated safely"
|
||||
[[ "$(cat "${requested}/marker")" == "old-release" ]] \
|
||||
|| fail_test "the existing release was modified while allocating its replacement"
|
||||
}
|
||||
|
||||
test_new_release_keeps_the_requested_version_path() {
|
||||
local fixture="${TEST_ROOT}/new release"
|
||||
local requested="${fixture}/releases/v1.2.3"
|
||||
local selected
|
||||
mkdir -p "$(dirname -- "${requested}")"
|
||||
|
||||
selected="$(select_release_install_directory "${requested}")"
|
||||
[[ "${selected}" == "${requested}" ]] \
|
||||
|| fail_test "a new release unexpectedly received a suffixed directory"
|
||||
[[ ! -e "${requested}" && ! -L "${requested}" ]] \
|
||||
|| fail_test "release selection created the requested path before installation"
|
||||
}
|
||||
|
||||
test_initial_switch
|
||||
test_existing_link_switch
|
||||
test_dangling_links_are_replaced
|
||||
test_current_file_is_rejected
|
||||
test_current_directory_is_rejected
|
||||
test_temporary_file_is_rejected
|
||||
test_temporary_directory_is_rejected
|
||||
test_managed_directory_link_is_rejected
|
||||
test_existing_release_is_never_rewritten_in_place
|
||||
test_new_release_keeps_the_requested_version_path
|
||||
|
||||
echo "PASS: current release link safety fixtures"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
make_bundle() {
|
||||
local bundle="$1"
|
||||
mkdir -p "${bundle}/bin" "${bundle}/frontend/assets"
|
||||
printf '#!/bin/sh\nexit 0\n' >"${bundle}/bin/aether-gateway"
|
||||
printf '<!doctype html>\n' >"${bundle}/frontend/index.html"
|
||||
printf 'asset\n' >"${bundle}/frontend/assets/app.js"
|
||||
chmod 0755 "${bundle}/bin/aether-gateway"
|
||||
}
|
||||
|
||||
assert_rejected() {
|
||||
local bundle="$1"
|
||||
local description="$2"
|
||||
if (validate_local_bundle_tree "${bundle}"); then
|
||||
fail_test "unsafe local bundle was accepted: ${description}"
|
||||
fi
|
||||
}
|
||||
|
||||
valid="${TEST_ROOT}/valid"
|
||||
make_bundle "${valid}"
|
||||
validate_local_bundle_tree "${valid}"
|
||||
|
||||
victim="${TEST_ROOT}/victim"
|
||||
printf 'do-not-touch\n' >"${victim}"
|
||||
|
||||
frontend_link="${TEST_ROOT}/frontend-link"
|
||||
make_bundle "${frontend_link}"
|
||||
rm -rf -- "${frontend_link}/frontend"
|
||||
ln -s -- "${TEST_ROOT}" "${frontend_link}/frontend"
|
||||
assert_rejected "${frontend_link}" "frontend symbolic link"
|
||||
[[ "$(cat "${victim}")" == "do-not-touch" ]] \
|
||||
|| fail_test "frontend symbolic-link validation modified its target"
|
||||
|
||||
binary_link="${TEST_ROOT}/binary-link"
|
||||
make_bundle "${binary_link}"
|
||||
rm -f -- "${binary_link}/bin/aether-gateway"
|
||||
ln -s -- "${valid}/bin/aether-gateway" "${binary_link}/bin/aether-gateway"
|
||||
assert_rejected "${binary_link}" "binary symbolic link"
|
||||
|
||||
hardlink="${TEST_ROOT}/hardlink"
|
||||
make_bundle "${hardlink}"
|
||||
ln "${hardlink}/frontend/index.html" "${hardlink}/frontend/index-copy.html"
|
||||
assert_rejected "${hardlink}" "multiply-linked frontend file"
|
||||
|
||||
fifo="${TEST_ROOT}/fifo"
|
||||
make_bundle "${fifo}"
|
||||
mkfifo "${fifo}/frontend/events.pipe"
|
||||
assert_rejected "${fifo}" "FIFO frontend entry"
|
||||
|
||||
bundle_link="${TEST_ROOT}/bundle-link"
|
||||
ln -s -- "${valid}" "${bundle_link}"
|
||||
assert_rejected "${bundle_link}" "bundle symbolic link"
|
||||
|
||||
echo "PASS: local release bundle type and link safety fixtures"
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_rejected() {
|
||||
if ("$@") >/dev/null 2>&1; then
|
||||
fail_test "unsafe managed-file fixture was accepted: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_victim_unchanged() {
|
||||
local victim="$1"
|
||||
local expected="$2"
|
||||
[[ "$(cat "${victim}")" == "${expected}" ]] \
|
||||
|| fail_test "managed-file guard modified ${victim}"
|
||||
}
|
||||
|
||||
mkdir -p "${TEST_ROOT}/managed" "${TEST_ROOT}/config"
|
||||
source_file="${TEST_ROOT}/source"
|
||||
victim="${TEST_ROOT}/victim"
|
||||
target="${TEST_ROOT}/managed/target"
|
||||
printf '%s\n' "replacement" >"${source_file}"
|
||||
printf '%s\n' "keep-victim" >"${victim}"
|
||||
|
||||
ln -s "${victim}" "${target}"
|
||||
assert_rejected atomic_install_managed_file "${source_file}" "${target}" 0600
|
||||
assert_victim_unchanged "${victim}" "keep-victim"
|
||||
rm -f "${target}"
|
||||
|
||||
ln "${victim}" "${target}"
|
||||
atomic_install_managed_file "${source_file}" "${target}" 0600
|
||||
assert_victim_unchanged "${victim}" "keep-victim"
|
||||
[[ "$(cat "${target}")" == "replacement" ]] \
|
||||
|| fail_test "atomic managed-file replacement did not update the target"
|
||||
[[ "$(stat_file_link_count "${target}")" == "1" ]] \
|
||||
|| fail_test "atomic managed-file replacement retained an attacker-controlled hard link"
|
||||
|
||||
keys_victim="${TEST_ROOT}/keys-victim"
|
||||
keys_target="${TEST_ROOT}/managed/generate_keys.sh"
|
||||
printf '%s\n' "keep-keys-victim" >"${keys_victim}"
|
||||
ln -s "${keys_victim}" "${keys_target}"
|
||||
CONFIG_DIR="${TEST_ROOT}/config"
|
||||
assert_rejected write_generate_keys_script "${keys_target}"
|
||||
assert_victim_unchanged "${keys_victim}" "keep-keys-victim"
|
||||
|
||||
env_victim="${TEST_ROOT}/env-victim"
|
||||
env_target="${TEST_ROOT}/config/aether-gateway.env"
|
||||
printf '%s\n' "keep-env-victim" >"${env_victim}"
|
||||
ln -s "${env_victim}" "${env_target}"
|
||||
assert_rejected replace_or_append_env "${env_target}" "AETHER_UPDATE_STRATEGY" "manual"
|
||||
assert_victim_unchanged "${env_victim}" "keep-env-victim"
|
||||
|
||||
ENV_TARGET="${env_target}"
|
||||
SERVICE_GROUP="$(id -gn)"
|
||||
assert_rejected install_env_target_from "${source_file}"
|
||||
assert_victim_unchanged "${env_victim}" "keep-env-victim"
|
||||
rm -f "${env_target}"
|
||||
printf '%s\n' "AETHER_UPDATE_STRATEGY=self" >"${env_target}"
|
||||
chmod 0640 "${env_target}"
|
||||
replace_or_append_env "${env_target}" "AETHER_UPDATE_STRATEGY" "manual"
|
||||
grep -Fxq "AETHER_UPDATE_STRATEGY=manual" "${env_target}" \
|
||||
|| fail_test "env update did not replace the managed value"
|
||||
[[ "$(stat_file_mode "${env_target}")" == "640" ]] \
|
||||
|| fail_test "env update did not preserve the managed file mode"
|
||||
|
||||
# Exercise the real writers without requiring root ownership changes in the
|
||||
# temporary fixture tree. The original atomic replacement implementation is
|
||||
# retained; only ownership application and privileged-directory creation are
|
||||
# adapted for an unprivileged test process.
|
||||
eval "$(declare -f atomic_install_managed_file | \
|
||||
sed '1s/atomic_install_managed_file/original_atomic_install_managed_file/')"
|
||||
atomic_install_managed_file() {
|
||||
original_atomic_install_managed_file "$1" "$2" "$3"
|
||||
}
|
||||
ensure_privileged_directory() {
|
||||
ensure_directory "$1" "$2"
|
||||
}
|
||||
systemctl() {
|
||||
return 0
|
||||
}
|
||||
|
||||
systemd_dir="${TEST_ROOT}/systemd"
|
||||
SYSTEMD_UNIT_PATH="${systemd_dir}/aether-gateway.service"
|
||||
mkdir -p "${systemd_dir}"
|
||||
unit_victim="${TEST_ROOT}/unit-victim"
|
||||
printf '%s\n' "keep-unit-victim" >"${unit_victim}"
|
||||
ln -s "${unit_victim}" "${SYSTEMD_UNIT_PATH}"
|
||||
assert_rejected install_systemd_unit
|
||||
assert_victim_unchanged "${unit_victim}" "keep-unit-victim"
|
||||
rm -f "${SYSTEMD_UNIT_PATH}"
|
||||
install_systemd_unit
|
||||
[[ -f "${SYSTEMD_UNIT_PATH}" && ! -L "${SYSTEMD_UNIT_PATH}" ]] \
|
||||
|| fail_test "systemd unit was not installed as a regular file"
|
||||
|
||||
INSTALL_ROOT="${TEST_ROOT}/install path/\$(touch wrapper-injected)"
|
||||
CONFIG_DIR="${TEST_ROOT}/config"
|
||||
ENV_TARGET="${TEST_ROOT}/config path/\$(touch env-injected)"
|
||||
mkdir -p "$(dirname "${ENV_TARGET}")"
|
||||
wrapper="$(launchd_wrapper_path)"
|
||||
mkdir -p "$(dirname "${wrapper}")"
|
||||
wrapper_victim="${TEST_ROOT}/wrapper-victim"
|
||||
printf '%s\n' "keep-wrapper-victim" >"${wrapper_victim}"
|
||||
ln -s "${wrapper_victim}" "${wrapper}"
|
||||
assert_rejected write_launchd_wrapper
|
||||
assert_victim_unchanged "${wrapper_victim}" "keep-wrapper-victim"
|
||||
rm -f "${wrapper}"
|
||||
write_launchd_wrapper
|
||||
bash -n "${wrapper}"
|
||||
if (cd "${TEST_ROOT}" && "${wrapper}") >/dev/null 2>&1; then
|
||||
fail_test "launchd wrapper unexpectedly ran without its env file"
|
||||
fi
|
||||
[[ ! -e "${TEST_ROOT}/wrapper-injected" && ! -e "${TEST_ROOT}/env-injected" ]] \
|
||||
|| fail_test "launchd wrapper executed a generated-path shell injection"
|
||||
|
||||
LAUNCHD_LOG_DIR="${TEST_ROOT}/launchd-logs"
|
||||
mkdir -p "${LAUNCHD_LOG_DIR}"
|
||||
log_victim="${TEST_ROOT}/log-victim"
|
||||
printf '%s\n' "keep-log-victim" >"${log_victim}"
|
||||
ln -s "${log_victim}" "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
assert_rejected install_launchd_log_files
|
||||
assert_victim_unchanged "${log_victim}" "keep-log-victim"
|
||||
rm -f "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
ln "${log_victim}" "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
assert_rejected install_launchd_log_files
|
||||
assert_victim_unchanged "${log_victim}" "keep-log-victim"
|
||||
rm -f "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
printf '%s\n' "keep-existing-log" >"${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
chmod 0600 "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log"
|
||||
ln -s "${log_victim}" "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.err.log"
|
||||
assert_rejected install_launchd_log_files
|
||||
[[ "$(cat "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log")" == "keep-existing-log" ]] \
|
||||
|| fail_test "launchd log preflight changed an earlier valid log"
|
||||
[[ "$(stat_file_mode "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.out.log")" == "600" ]] \
|
||||
|| fail_test "launchd log preflight changed permissions before rejecting a later target"
|
||||
rm -f "${LAUNCHD_LOG_DIR}/${SERVICE_NAME}.err.log"
|
||||
|
||||
plist_dir="${TEST_ROOT}/launchd"
|
||||
LAUNCHD_PLIST_PATH="${plist_dir}/aether-gateway.plist"
|
||||
mkdir -p "${plist_dir}"
|
||||
plist_victim="${TEST_ROOT}/plist-victim"
|
||||
printf '%s\n' "keep-plist-victim" >"${plist_victim}"
|
||||
ln -s "${plist_victim}" "${LAUNCHD_PLIST_PATH}"
|
||||
assert_rejected install_launchd_unit
|
||||
assert_victim_unchanged "${plist_victim}" "keep-plist-victim"
|
||||
|
||||
LAUNCHD_LABEL="aether&-test"
|
||||
SERVICE_USER="user&-test"
|
||||
SERVICE_GROUP="group<-test"
|
||||
rendered_plist="${TEST_ROOT}/rendered.plist"
|
||||
render_launchd_plist >"${rendered_plist}"
|
||||
grep -Fq '<string>aether&amp-test</string>' "${rendered_plist}" \
|
||||
|| fail_test "launchd label was not XML escaped"
|
||||
grep -Fq '<string>user&amp-test</string>' "${rendered_plist}" \
|
||||
|| fail_test "launchd account was not XML escaped"
|
||||
|
||||
SERVICE_USER="aether"
|
||||
SERVICE_GROUP="aether"
|
||||
LAUNCHD_LABEL="../outside"
|
||||
assert_rejected validate_launchd_label "${LAUNCHD_LABEL}"
|
||||
assert_rejected validate_managed_absolute_path "install root" "/opt/aether/../outside"
|
||||
assert_rejected validate_managed_absolute_path "install root" "/opt/aether path"
|
||||
|
||||
ancestor_target="${TEST_ROOT}/ancestor-target"
|
||||
ancestor_link="${TEST_ROOT}/ancestor-link"
|
||||
mkdir -p "${ancestor_target}"
|
||||
ln -s "${ancestor_target}" "${ancestor_link}"
|
||||
assert_rejected validate_privileged_path_ancestor "${ancestor_link}/managed"
|
||||
|
||||
echo "PASS: privileged installer managed-file safety fixtures"
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p "${TEST_ROOT}/bin" "${TEST_ROOT}/frontend"
|
||||
printf '#!/bin/sh\nexit 0\n' >"${TEST_ROOT}/bin/aether-gateway"
|
||||
chmod +x "${TEST_ROOT}/bin/aether-gateway"
|
||||
|
||||
definitions="$({
|
||||
awk '/^current_script_dir\(\)/ { emit=1 } emit { print } emit && /^}/ { emit=0 }' "${REPO_ROOT}/install.sh"
|
||||
awk '/^local_bundle_dir\(\)/ { emit=1 } emit { print } emit && /^}/ { emit=0 }' "${REPO_ROOT}/install.sh"
|
||||
})"
|
||||
|
||||
if printf '%s\ncd -- %q\nif local_bundle_dir; then exit 9; fi\n' \
|
||||
"${definitions}" "${TEST_ROOT}" | bash; then
|
||||
:
|
||||
else
|
||||
status=$?
|
||||
[[ "${status}" -ne 9 ]] || fail_test "stdin execution trusted a bundle from the current directory"
|
||||
fail_test "stdin trust regression fixture failed unexpectedly with status ${status}"
|
||||
fi
|
||||
|
||||
# shellcheck source=../install.sh
|
||||
source "${REPO_ROOT}/install.sh"
|
||||
[[ "$(current_script_dir)" == "${REPO_ROOT}" ]] \
|
||||
|| fail_test "a real installer file no longer resolves its adjacent bundle directory"
|
||||
|
||||
echo "PASS: installer source-directory trust boundary"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
COMPOSE_FILE="${REPO_ROOT}/docker-compose.yml"
|
||||
RELEASE_WORKFLOW="${REPO_ROOT}/.github/workflows/release.yml"
|
||||
TUNNEL_RELEASE_WORKFLOW="${REPO_ROOT}/.github/workflows/build-tunnel.yml"
|
||||
APP_DOCKERFILE="${REPO_ROOT}/Dockerfile.app"
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_line() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
grep -Fqx -- "${expected}" "${file}" \
|
||||
|| fail_test "missing expected line in ${file}: ${expected}"
|
||||
}
|
||||
|
||||
assert_line "${COMPOSE_FILE}" \
|
||||
" image: postgres:15.19@sha256:5f72c7b5bd616308ccfd2e74d6be16fb06364e5eecbb815fe9dc6ab9761d2111"
|
||||
assert_line "${COMPOSE_FILE}" \
|
||||
" image: redis:7.4.11-alpine@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf"
|
||||
assert_line "${COMPOSE_FILE}" \
|
||||
" image: mysql:8.0.46@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b"
|
||||
|
||||
if grep -Eq '^[[:space:]]+image:[[:space:]]+(postgres|redis|mysql):[^@[:space:]]+[[:space:]]*$' "${COMPOSE_FILE}"; then
|
||||
fail_test "compose contains a mutable third-party image tag"
|
||||
fi
|
||||
|
||||
assert_line "${APP_DOCKERFILE}" \
|
||||
"FROM busybox:1.37.0-musl@sha256:fc6dddc4c44b1bfe37f41cae8e67d1693828e8f42a91862816d7953e2c9d3f23 AS layout"
|
||||
assert_line "${APP_DOCKERFILE}" \
|
||||
"FROM gcr.io/distroless/static-debian12@sha256:6447365a6337c3732f412d1b74357b30a633831955b2bc45552b0086be907687"
|
||||
|
||||
if grep -Eq '^FROM[[:space:]]+[^[:space:]@]+(:[^[:space:]@]+)?([[:space:]]+AS[[:space:]]+[^[:space:]]+)?$' "${APP_DOCKERFILE}"; then
|
||||
fail_test "production Dockerfile contains an unpinned base image"
|
||||
fi
|
||||
|
||||
assert_line "${RELEASE_WORKFLOW}" " attestations: write"
|
||||
assert_line "${RELEASE_WORKFLOW}" " id-token: write"
|
||||
assert_line "${RELEASE_WORKFLOW}" \
|
||||
" uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2"
|
||||
assert_line "${RELEASE_WORKFLOW}" ' subject-digest: ${{ steps.push.outputs.digest }}'
|
||||
assert_line "${RELEASE_WORKFLOW}" " push-to-registry: true"
|
||||
assert_line "${RELEASE_WORKFLOW}" " subject-path: |"
|
||||
assert_line "${RELEASE_WORKFLOW}" " release-assets/install.sh"
|
||||
assert_line "${RELEASE_WORKFLOW}" " release-assets/SHA256SUMS"
|
||||
assert_line "${RELEASE_WORKFLOW}" " release-assets/AETHER_RELEASE_PROVENANCE.sigstore.json"
|
||||
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" " attestations: write"
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" " id-token: write"
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" \
|
||||
" uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2"
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" " artifacts/SHA256SUMS.txt"
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" \
|
||||
" artifacts/AETHER_TUNNEL_RELEASE_PROVENANCE.sigstore.json"
|
||||
assert_line "${TUNNEL_RELEASE_WORKFLOW}" \
|
||||
" tar czf ../../../aether-tunnel-\${{ matrix.name }}.tar.gz aether-tunnel.exe"
|
||||
|
||||
if grep -ERq '^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]+[^[:space:]#]+@[^0-9a-f[:space:]][^[:space:]]*([[:space:]#]|$)' \
|
||||
"${REPO_ROOT}/.github/workflows"; then
|
||||
fail_test "workflow contains a mutable third-party action reference"
|
||||
fi
|
||||
|
||||
echo "PASS: release supply-chain pins and provenance workflow"
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
INSTALLER="${REPO_ROOT}/apps/aether-tunnel/install.sh"
|
||||
POWERSHELL_INSTALLER="${REPO_ROOT}/apps/aether-tunnel/install.ps1"
|
||||
TEST_ROOT="${REPO_ROOT}/.tmp-tunnel-installer-test.$$"
|
||||
|
||||
cleanup_test_root() {
|
||||
chmod -R u+rwX "${TEST_ROOT}" 2>/dev/null || true
|
||||
rm -rf -- "${TEST_ROOT}"
|
||||
}
|
||||
trap cleanup_test_root EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_rejected() {
|
||||
if ("$@") >/dev/null 2>&1; then
|
||||
fail_test "unsafe installer fixture was accepted: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
file_mode() {
|
||||
stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
|
||||
}
|
||||
|
||||
mkdir -m 700 "${TEST_ROOT}"
|
||||
LIB="${TEST_ROOT}/installer-lib.sh"
|
||||
sed '/^main()/,$d' "${INSTALLER}" >"${LIB}"
|
||||
# shellcheck source=/dev/null
|
||||
source "${LIB}"
|
||||
trap cleanup_test_root EXIT
|
||||
|
||||
validate_release_repo "fawney19/Aether"
|
||||
validate_tunnel_release_tag "tunnel-v0.3.16-rc.1"
|
||||
validate_tunnel_release_tag "tunnel-v1.2.3+build.7"
|
||||
validate_release_asset_name "aether-tunnel-linux-amd64.tar.gz"
|
||||
validate_node_name "Tokyo edge 01"
|
||||
validate_node_name "东京节点"
|
||||
assert_rejected validate_release_repo "owner/repo/extra"
|
||||
assert_rejected validate_release_repo 'owner/repo;touch injected'
|
||||
assert_rejected validate_tunnel_release_tag "../tunnel-v0.3.16"
|
||||
assert_rejected validate_tunnel_release_tag "gateway-v0.3.16"
|
||||
assert_rejected validate_tunnel_release_tag "tunnel-v1.2"
|
||||
assert_rejected validate_tunnel_release_tag "tunnel-v01.2.3"
|
||||
assert_rejected validate_tunnel_release_tag "tunnel-v1.2.3-01"
|
||||
assert_rejected validate_release_asset_name "../aether-tunnel-linux-amd64.tar.gz"
|
||||
assert_rejected validate_node_name " trailing "
|
||||
assert_rejected validate_node_name $'node\tname'
|
||||
assert_rejected validate_node_name $'node\nname'
|
||||
assert_rejected validate_node_name "$(printf 'n%.0s' {1..256})"
|
||||
|
||||
assert_rejected validate_https_download_url "http://example.test/release.tar.gz"
|
||||
assert_rejected validate_https_download_url "https://user:pass@example.test/release.tar.gz"
|
||||
assert_rejected validate_https_download_url "https://example.test/release.tar.gz#fragment"
|
||||
validate_trusted_github_download_url "https://github.com/fawney19/Aether/releases/download/tunnel-v1.2.3/aether-tunnel-linux-amd64.tar.gz"
|
||||
validate_trusted_github_download_url "https://release-assets.githubusercontent.com/example/object"
|
||||
assert_rejected validate_trusted_github_download_url "https://github.com.evil.example/release.tar.gz"
|
||||
assert_rejected validate_trusted_github_download_url "https://github.com:444/release.tar.gz"
|
||||
|
||||
CURL_ARGS="${TEST_ROOT}/curl-args"
|
||||
curl() {
|
||||
printf '%s\n' "$@" >"${CURL_ARGS}"
|
||||
local headers='' output='' url=''
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--dump-header) headers="$2"; shift 2 ;;
|
||||
--output) output="$2"; shift 2 ;;
|
||||
--write-out) shift 2 ;;
|
||||
-*) shift ;;
|
||||
*) url="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
: >"${headers}"
|
||||
printf '%s\n' "verified download from ${url}" >"${output}"
|
||||
printf '200'
|
||||
}
|
||||
download "https://github.com/fawney19/Aether/release.tar.gz" "${TEST_ROOT}/downloaded"
|
||||
grep -Fxq -- '--proto' "${CURL_ARGS}" || fail_test "curl HTTPS protocol restriction is missing"
|
||||
grep -Fxq -- '=https' "${CURL_ARGS}" || fail_test "curl HTTPS protocol value is missing"
|
||||
grep -Fxq -- '--max-redirs' "${CURL_ARGS}" || fail_test "curl automatic redirects were not disabled"
|
||||
unset -f curl
|
||||
|
||||
checksum_asset="aether-tunnel-linux-amd64.tar.gz"
|
||||
checksum_archive="${TEST_ROOT}/${checksum_asset}"
|
||||
checksum_manifest="${TEST_ROOT}/SHA256SUMS.txt"
|
||||
printf '%s\n' "verified release payload" >"${checksum_archive}"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
checksum="$(sha256sum "${checksum_archive}" | awk '{print $1}')"
|
||||
else
|
||||
checksum="$(shasum -a 256 "${checksum_archive}" | awk '{print $1}')"
|
||||
fi
|
||||
printf '%s %s\n' "${checksum}" "${checksum_asset}" >"${checksum_manifest}"
|
||||
verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%064d %s\n' 0 "${checksum_asset}" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%063d %s\n' 0 "${checksum_asset}" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%s %s\n' "${checksum}" "other-asset.tar.gz" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%s %s trailing-field\n' "${checksum}" "${checksum_asset}" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%s extra-field %s\n' "${checksum}" "${checksum_asset}" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
printf '%s %s\n%s *%s\n' \
|
||||
"${checksum}" "${checksum_asset}" "${checksum}" "${checksum_asset}" >"${checksum_manifest}"
|
||||
assert_rejected verify_checksum "${checksum_archive}" "${checksum_manifest}" "${checksum_asset}"
|
||||
|
||||
CONFIG_PATH="${TEST_ROOT}/normal/aether-tunnel.toml"
|
||||
append_server_config "https://aether.example" "secret-token" "node-one" "off" ""
|
||||
[[ "$(file_mode "${CONFIG_PATH}")" == "600" ]] || fail_test "config mode is not 0600"
|
||||
grep -Fq 'management_token = "secret-token"' "${CONFIG_PATH}" || fail_test "config block was not written"
|
||||
|
||||
source_binary="${TEST_ROOT}/source-aether-tunnel"
|
||||
printf '#!/bin/sh\nexit 0\n' >"${source_binary}"
|
||||
chmod 755 "${source_binary}"
|
||||
INSTALL_DIR="${TEST_ROOT}/install-bin"
|
||||
install_tunnel_binary_file "${source_binary}"
|
||||
[[ -x "${INSTALL_DIR}/aether-tunnel" ]] || fail_test "tunnel binary was not installed"
|
||||
|
||||
binary_victim="${TEST_ROOT}/binary-victim"
|
||||
printf '%s\n' "keep-binary-victim" >"${binary_victim}"
|
||||
rm -f "${INSTALL_DIR}/aether-tunnel"
|
||||
ln -s "${binary_victim}" "${INSTALL_DIR}/aether-tunnel"
|
||||
if (install_tunnel_binary_file "${source_binary}") 2>/dev/null; then
|
||||
fail_test "symbolic-link binary target was accepted"
|
||||
fi
|
||||
[[ "$(cat "${binary_victim}")" == "keep-binary-victim" ]] \
|
||||
|| fail_test "symbolic-link binary target was modified"
|
||||
|
||||
rm -f "${INSTALL_DIR}/aether-tunnel"
|
||||
binary_hardlink_victim="${TEST_ROOT}/binary-hardlink-victim"
|
||||
printf '%s\n' "keep-hardlink-victim" >"${binary_hardlink_victim}"
|
||||
ln "${binary_hardlink_victim}" "${INSTALL_DIR}/aether-tunnel"
|
||||
if (install_tunnel_binary_file "${source_binary}") 2>/dev/null; then
|
||||
fail_test "hard-linked binary target was accepted"
|
||||
fi
|
||||
[[ "$(cat "${binary_hardlink_victim}")" == "keep-hardlink-victim" ]] \
|
||||
|| fail_test "hard-linked binary victim was modified"
|
||||
|
||||
linked_install_target="${TEST_ROOT}/linked-install-target"
|
||||
mkdir -m 700 "${linked_install_target}"
|
||||
INSTALL_DIR="${TEST_ROOT}/linked-install-dir"
|
||||
ln -s "${linked_install_target}" "${INSTALL_DIR}"
|
||||
if (install_tunnel_binary_file "${source_binary}") 2>/dev/null; then
|
||||
fail_test "symbolic-link install directory was accepted"
|
||||
fi
|
||||
[[ ! -e "${linked_install_target}/aether-tunnel" ]] \
|
||||
|| fail_test "symbolic-link install directory target was modified"
|
||||
|
||||
ancestor_target="${TEST_ROOT}/ancestor-target"
|
||||
mkdir -m 700 "${ancestor_target}"
|
||||
ancestor_link="${TEST_ROOT}/ancestor-link"
|
||||
ln -s "${ancestor_target}" "${ancestor_link}"
|
||||
INSTALL_DIR="${ancestor_link}/nested/bin"
|
||||
if (install_tunnel_binary_file "${source_binary}") 2>/dev/null; then
|
||||
fail_test "symbolic-link install ancestor was accepted"
|
||||
fi
|
||||
[[ ! -e "${ancestor_target}/nested/bin/aether-tunnel" ]] \
|
||||
|| fail_test "symbolic-link install ancestor target was modified"
|
||||
|
||||
chmod 644 "${CONFIG_PATH}"
|
||||
append_server_config "https://aether.example" "second-token" "node-two" "off" ""
|
||||
[[ "$(file_mode "${CONFIG_PATH}")" == "600" ]] || fail_test "existing config was not protected before update"
|
||||
backup="$(find "$(dirname "${CONFIG_PATH}")" -maxdepth 1 -type f -name 'aether-tunnel.toml.bak.*' | head -n1)"
|
||||
[[ -n "${backup}" && "$(file_mode "${backup}")" == "600" ]] || fail_test "backup mode is not 0600"
|
||||
|
||||
victim="${TEST_ROOT}/victim.txt"
|
||||
printf '%s\n' "keep-me" >"${victim}"
|
||||
CONFIG_PATH="${TEST_ROOT}/linked.toml"
|
||||
ln -s "${victim}" "${CONFIG_PATH}"
|
||||
if (append_server_config "https://aether.example" "stolen-token" "node-link" "off" "") 2>/dev/null; then
|
||||
fail_test "symbolic-link config was accepted"
|
||||
fi
|
||||
[[ "$(cat "${victim}")" == "keep-me" ]] || fail_test "symbolic-link target was modified"
|
||||
|
||||
hardlink_victim="${TEST_ROOT}/hardlink-victim.toml"
|
||||
printf '%s\n' 'victim = true' >"${hardlink_victim}"
|
||||
CONFIG_PATH="${TEST_ROOT}/hardlinked.toml"
|
||||
ln "${hardlink_victim}" "${CONFIG_PATH}"
|
||||
if (append_server_config "https://aether.example" "stolen-token" "node-hardlink" "off" "") 2>/dev/null; then
|
||||
fail_test "hard-linked config was accepted"
|
||||
fi
|
||||
[[ "$(cat "${hardlink_victim}")" == 'victim = true' ]] || fail_test "hard-linked config victim was modified"
|
||||
|
||||
grep -Fq 'Get-Content -LiteralPath' "${POWERSHELL_INSTALLER}" || fail_test "PowerShell reads are not literal-path safe"
|
||||
grep -Fq '[IO.File]::Replace' "${POWERSHELL_INSTALLER}" || fail_test "PowerShell config replacement is not atomic"
|
||||
grep -Fq 'FileAttributes]::ReparsePoint' "${POWERSHELL_INSTALLER}" || fail_test "PowerShell reparse-point guard is missing"
|
||||
grep -Fq 'Assert-NoReparsePointAncestors' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell ancestor reparse-point guard is missing"
|
||||
grep -Fq "LinkType -eq 'HardLink'" "${POWERSHELL_INSTALLER}" || fail_test "PowerShell hard-link guard is missing"
|
||||
grep -Fq 'AllowAutoRedirect = $false' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell automatic redirects are still enabled"
|
||||
grep -Fq 'Assert-TrustedGithubUri $CurrentUri' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell trusted redirect validation is missing"
|
||||
if grep -Eq 'Invoke-(WebRequest|RestMethod)' "${POWERSHELL_INSTALLER}"; then
|
||||
fail_test "PowerShell installer still uses an automatically redirecting download command"
|
||||
fi
|
||||
grep -Fq '[IO.File]::Replace($TempBinary, $TargetBinary' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell binary replacement is not atomic"
|
||||
grep -Fq '$ExpectedLines.Count -ne 1' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell checksum matching does not reject duplicates"
|
||||
grep -Fq 'if (-not $ExpectedMatch.Success)' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell checksum matching does not reject malformed target entries"
|
||||
if grep -Fq 'Select-Object -First 1' "${POWERSHELL_INSTALLER}"; then
|
||||
fail_test "PowerShell checksum matching still silently accepts duplicate entries"
|
||||
fi
|
||||
grep -Fq 'Assert-SafeReleaseRepo $Repo' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell release repository validation is missing"
|
||||
grep -Fq 'Assert-SafeTunnelReleaseTag $Tag' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell release tag validation is missing"
|
||||
grep -Fq 'Assert-SafeNodeName $NodeName' "${POWERSHELL_INSTALLER}" \
|
||||
|| fail_test "PowerShell node-name validation is missing"
|
||||
if grep -Eq 'Add-Content[[:space:]]+-Path' "${POWERSHELL_INSTALLER}"; then
|
||||
fail_test "PowerShell config still uses wildcard-aware Add-Content -Path"
|
||||
fi
|
||||
|
||||
echo "PASS: tunnel installer config safety fixtures"
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${TEST_ROOT}"' EXIT
|
||||
|
||||
fail_test() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
make_fixture() {
|
||||
local fixture="$1"
|
||||
mkdir -p "${fixture}/bin" "${fixture}/compose"
|
||||
: >"${fixture}/compose/docker-compose.yml"
|
||||
: >"${fixture}/calls"
|
||||
cp "${REPO_ROOT}/update.sh" "${fixture}/compose/update.sh"
|
||||
chmod 0755 "${fixture}/compose/update.sh"
|
||||
}
|
||||
|
||||
test_wait_failure_is_not_ignored() {
|
||||
local fixture="${TEST_ROOT}/wait-failure"
|
||||
make_fixture "${fixture}"
|
||||
cat >"${fixture}/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >>"${AETHER_TEST_CALLS}"
|
||||
case "$*" in
|
||||
"compose version"|"info") exit 0 ;;
|
||||
*"config --services") printf 'app\n' ;;
|
||||
*"up --help") printf '%s\n' ' --wait Wait for services' ;;
|
||||
*"up -d --wait --wait-timeout 120 app") exit 42 ;;
|
||||
*"pull app") exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "${fixture}/bin/docker"
|
||||
|
||||
if PATH="${fixture}/bin:${PATH}" AETHER_TEST_CALLS="${fixture}/calls" \
|
||||
"${fixture}/compose/update.sh" --compose-dir "${fixture}/compose" \
|
||||
>"${fixture}/stdout" 2>"${fixture}/stderr"; then
|
||||
fail_test "update succeeded after compose --wait reported an unhealthy app"
|
||||
fi
|
||||
|
||||
[[ "$(grep -Fc 'up -d' "${fixture}/calls")" -eq 1 ]] \
|
||||
|| fail_test "failed update retried an unverified simple recreate"
|
||||
! grep -Fq '>>> Done.' "${fixture}/stdout" \
|
||||
|| fail_test "failed update was reported as successful"
|
||||
}
|
||||
|
||||
test_legacy_compose_uses_health_polling() {
|
||||
local fixture="${TEST_ROOT}/legacy-wait"
|
||||
make_fixture "${fixture}"
|
||||
cat >"${fixture}/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >>"${AETHER_TEST_CALLS}"
|
||||
case "$*" in
|
||||
"compose version"|"info") exit 0 ;;
|
||||
*"config --services") printf 'app\n' ;;
|
||||
*"up --help") printf '%s\n' 'Usage: docker compose up' ;;
|
||||
*"pull app"|*"up -d app"|*" ps") exit 0 ;;
|
||||
*"ps -q app") printf 'container-id\n' ;;
|
||||
"inspect --format={{.State.Health.Status}} container-id") printf 'healthy\n' ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "${fixture}/bin/docker"
|
||||
|
||||
PATH="${fixture}/bin:${PATH}" AETHER_TEST_CALLS="${fixture}/calls" \
|
||||
"${fixture}/compose/update.sh" --compose-dir "${fixture}/compose" \
|
||||
>"${fixture}/stdout" 2>"${fixture}/stderr"
|
||||
|
||||
grep -Fq 'up -d app' "${fixture}/calls" \
|
||||
|| fail_test "legacy Compose path did not recreate the app"
|
||||
grep -Fq '>>> Container is healthy.' "${fixture}/stdout" \
|
||||
|| fail_test "legacy Compose path did not require a healthy container"
|
||||
}
|
||||
|
||||
test_legacy_compose_health_failure_is_not_ignored() {
|
||||
local fixture="${TEST_ROOT}/legacy-unhealthy"
|
||||
make_fixture "${fixture}"
|
||||
cat >"${fixture}/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >>"${AETHER_TEST_CALLS}"
|
||||
case "$*" in
|
||||
"compose version"|"info") exit 0 ;;
|
||||
*"config --services") printf 'app\n' ;;
|
||||
*"up --help") printf '%s\n' 'Usage: docker compose up' ;;
|
||||
*"pull app"|*"up -d app") exit 0 ;;
|
||||
*"ps -q app") printf 'container-id\n' ;;
|
||||
"inspect --format={{.State.Health.Status}} container-id") printf 'unhealthy\n' ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
cat >"${fixture}/bin/sleep" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
EOF
|
||||
chmod 0755 "${fixture}/bin/docker" "${fixture}/bin/sleep"
|
||||
|
||||
if PATH="${fixture}/bin:${PATH}" AETHER_TEST_CALLS="${fixture}/calls" \
|
||||
"${fixture}/compose/update.sh" --compose-dir "${fixture}/compose" \
|
||||
>"${fixture}/stdout" 2>"${fixture}/stderr"; then
|
||||
fail_test "legacy Compose update succeeded with an unhealthy app"
|
||||
fi
|
||||
|
||||
grep -Fq 'health check timed out' "${fixture}/stdout" \
|
||||
|| fail_test "legacy unhealthy app did not reach the health failure path"
|
||||
! grep -Fq '>>> Done.' "${fixture}/stdout" \
|
||||
|| fail_test "legacy unhealthy update was reported as successful"
|
||||
}
|
||||
|
||||
test_option_like_service_name_is_rejected() {
|
||||
local fixture="${TEST_ROOT}/unsafe-service"
|
||||
make_fixture "${fixture}"
|
||||
|
||||
if PATH="${fixture}/bin:${PATH}" "${fixture}/compose/update.sh" \
|
||||
--compose-dir "${fixture}/compose" --service --ansi \
|
||||
>"${fixture}/stdout" 2>"${fixture}/stderr"; then
|
||||
fail_test "option-like service name was accepted"
|
||||
fi
|
||||
grep -Fq 'service name contains unsafe characters' "${fixture}/stderr" \
|
||||
|| fail_test "unsafe service name was not rejected by the identifier guard"
|
||||
}
|
||||
|
||||
test_wait_failure_is_not_ignored
|
||||
test_legacy_compose_uses_health_polling
|
||||
test_legacy_compose_health_failure_is_not_ignored
|
||||
test_option_like_service_name_is_rejected
|
||||
|
||||
echo "PASS: compose updater failure and argument safety fixtures"
|
||||
Reference in New Issue
Block a user