Some checks failed
CICD Start / Sanity and Base Decision (pull_request) Failing after 10m34s
Add Dockerfile boundary checks and deployable image purity validation for backend/frontend runtime artifacts. Wire enforcement into CI workflows and document runtime-vs-validation ownership.
269 lines
5.3 KiB
Bash
269 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
scripts/verify-deployable-image-purity.sh --image <image-ref> --profile <backend|frontend>
|
|
USAGE
|
|
}
|
|
|
|
image_ref=""
|
|
profile=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--image)
|
|
image_ref="${2:-}"
|
|
shift 2
|
|
;;
|
|
--profile)
|
|
profile="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${image_ref}" || -z "${profile}" ]]; then
|
|
usage
|
|
exit 2
|
|
fi
|
|
|
|
case "${profile}" in
|
|
backend)
|
|
checks=(
|
|
ruff
|
|
pyright
|
|
pytest
|
|
pydoclint
|
|
xdoctest
|
|
pre-commit
|
|
yamllint
|
|
toml-sort
|
|
eslint
|
|
prettier
|
|
tsc
|
|
vitest
|
|
playwright
|
|
yarn
|
|
npm
|
|
node
|
|
pnpm
|
|
)
|
|
;;
|
|
frontend)
|
|
checks=(
|
|
ruff
|
|
pyright
|
|
pytest
|
|
pydoclint
|
|
xdoctest
|
|
pre-commit
|
|
yamllint
|
|
toml-sort
|
|
eslint
|
|
prettier
|
|
tsc
|
|
vitest
|
|
playwright
|
|
uv
|
|
python
|
|
python3
|
|
pip
|
|
pip3
|
|
node
|
|
npm
|
|
yarn
|
|
pnpm
|
|
)
|
|
;;
|
|
*)
|
|
echo "Unsupported profile: ${profile}" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
tmp_file="$(mktemp)"
|
|
trap 'rm -f "${tmp_file}"' EXIT
|
|
|
|
printf '%s\n' "${checks[@]}" > "${tmp_file}"
|
|
|
|
binary_violations="$(docker run --rm --entrypoint /bin/sh "${image_ref}" -c '
|
|
set -e
|
|
while IFS= read -r cmd; do
|
|
if command -v "$cmd" >/dev/null 2>&1; then
|
|
printf "%s\n" "$cmd"
|
|
fi
|
|
done
|
|
' < "${tmp_file}")"
|
|
|
|
if [[ -n "${binary_violations}" ]]; then
|
|
echo "❌ Found CI/development tooling binaries in ${profile} image: ${image_ref}" >&2
|
|
echo "${binary_violations}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "${profile}" in
|
|
backend)
|
|
backend_python_modules=(
|
|
pytest
|
|
ruff
|
|
pyright
|
|
pydoclint
|
|
xdoctest
|
|
pre_commit
|
|
yamllint
|
|
toml_sort
|
|
playwright
|
|
)
|
|
|
|
backend_python_packages=(
|
|
pytest
|
|
ruff
|
|
pyright
|
|
pydoclint
|
|
xdoctest
|
|
pre-commit
|
|
yamllint
|
|
toml-sort
|
|
playwright
|
|
)
|
|
|
|
module_violations="$(docker run --rm --entrypoint /bin/sh \
|
|
-e MODULES="${backend_python_modules[*]}" \
|
|
"${image_ref}" -c '
|
|
set -e
|
|
|
|
PYTHON_BIN=""
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
PYTHON_BIN="python3"
|
|
elif command -v python >/dev/null 2>&1; then
|
|
PYTHON_BIN="python"
|
|
fi
|
|
|
|
if [ -z "$PYTHON_BIN" ]; then
|
|
echo "__missing_python_runtime__"
|
|
exit 0
|
|
fi
|
|
|
|
for module in ${MODULES}; do
|
|
if "$PYTHON_BIN" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('${module}') else 1)" >/dev/null 2>&1; then
|
|
printf "%s\n" "$module"
|
|
fi
|
|
done
|
|
')"
|
|
|
|
if echo "${module_violations}" | grep -q '__missing_python_runtime__'; then
|
|
echo "❌ Backend deployable image is missing python runtime: ${image_ref}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "${module_violations}" ]]; then
|
|
echo "❌ Found CI/development Python modules importable in backend image: ${image_ref}" >&2
|
|
echo "${module_violations}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
pip_violations="$(docker run --rm --entrypoint /bin/sh \
|
|
-e PACKAGES="${backend_python_packages[*]}" \
|
|
"${image_ref}" -c '
|
|
set -e
|
|
|
|
PIP_BIN=""
|
|
if command -v pip3 >/dev/null 2>&1; then
|
|
PIP_BIN="pip3"
|
|
elif command -v pip >/dev/null 2>&1; then
|
|
PIP_BIN="pip"
|
|
fi
|
|
|
|
if [ -z "$PIP_BIN" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
for package in ${PACKAGES}; do
|
|
if "$PIP_BIN" show "$package" >/dev/null 2>&1; then
|
|
printf "%s\n" "$package"
|
|
fi
|
|
done
|
|
')"
|
|
|
|
if [[ -n "${pip_violations}" ]]; then
|
|
echo "❌ Found CI/development Python packages in backend image metadata: ${image_ref}" >&2
|
|
echo "${pip_violations}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
frontend)
|
|
frontend_os_packages=(
|
|
nodejs
|
|
npm
|
|
yarn
|
|
python3
|
|
py3-pip
|
|
py3-setuptools
|
|
)
|
|
|
|
os_pkg_violations="$(docker run --rm --entrypoint /bin/sh \
|
|
-e PACKAGES="${frontend_os_packages[*]}" \
|
|
"${image_ref}" -c '
|
|
set -e
|
|
|
|
if command -v apk >/dev/null 2>&1; then
|
|
for package in ${PACKAGES}; do
|
|
if apk info -e "$package" >/dev/null 2>&1; then
|
|
printf "apk:%s\n" "$package"
|
|
fi
|
|
done
|
|
elif command -v dpkg-query >/dev/null 2>&1; then
|
|
for package in ${PACKAGES}; do
|
|
if dpkg-query -W -f='"'"'${db:Status-Status}'"'"' "$package" 2>/dev/null | grep -q '^installed$'; then
|
|
printf "dpkg:%s\n" "$package"
|
|
fi
|
|
done
|
|
fi
|
|
')"
|
|
|
|
if [[ -n "${os_pkg_violations}" ]]; then
|
|
echo "❌ Found CI/development OS packages in frontend runtime image: ${image_ref}" >&2
|
|
echo "${os_pkg_violations}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
dir_violations="$(docker run --rm --entrypoint /bin/sh "${image_ref}" -c '
|
|
set -e
|
|
|
|
check_dir_tree() {
|
|
local base_dir="$1"
|
|
[ -d "$base_dir" ] || return 0
|
|
|
|
find "$base_dir" -maxdepth 5 -type d \
|
|
\( -name node_modules -o -name .venv -o -name site-packages -o -name dist-packages \) \
|
|
2>/dev/null || true
|
|
}
|
|
|
|
check_dir_tree /app
|
|
check_dir_tree /usr/local/lib
|
|
check_dir_tree /usr/lib
|
|
check_dir_tree /opt
|
|
')"
|
|
|
|
if [[ -n "${dir_violations}" ]]; then
|
|
echo "❌ Found development package directories in frontend runtime image: ${image_ref}" >&2
|
|
echo "${dir_violations}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "✅ ${profile} image is clean of disallowed CI/development tooling binaries and metadata"
|