diff --git a/backend/meta/scripts/check-install-scripts.sh b/backend/meta/scripts/check-install-scripts.sh new file mode 100755 index 000000000..1c7f9baa6 --- /dev/null +++ b/backend/meta/scripts/check-install-scripts.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# +# Audit backend install-script sources before running a full npm install. +# Exits non-zero if any package not on the whitelist has hasInstallScript: true. +# +# Usage (from repo root): +# backend/meta/scripts/check-install-scripts.sh +# + +set -euo pipefail + +BACKEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LOCKFILE="${BACKEND_DIR}/package-lock.json" +RUST_GBT_DIR="${BACKEND_DIR}/../rust/gbt" +TMP_AUDIT_DIR="" + +BACKEND_ALLOWED=( + "mempool-backend" + "fsevents" + "unrs-resolver" +) + +RUST_GBT_ALLOWED=() + +is_allowed() { + local pkg="$1" + shift + local allowed + for allowed in "$@"; do + if [[ "$pkg" == "$allowed" ]]; then + return 0 + fi + done + return 1 +} + +cleanup() { + if [[ -n "${TMP_AUDIT_DIR}" && -d "${TMP_AUDIT_DIR}" ]]; then + rm -rf "${TMP_AUDIT_DIR}" + fi +} + +trap cleanup EXIT + +collect_has_install_script_packages() { + local lockfile="$1" + if [[ ! -f "$lockfile" ]]; then + echo "No package-lock.json found at ${lockfile}" + return 1 + fi + + LOCKFILE_PATH="$lockfile" node - <<'NODE' +const fs = require('fs'); + +const lock = JSON.parse(fs.readFileSync(process.env.LOCKFILE_PATH, 'utf8')); +const pkgs = lock.packages || {}; + +for (const [path, meta] of Object.entries(pkgs)) { + if (meta && meta.hasInstallScript) { + const name = path === '' ? (lock.name || '(root)') : path.replace(/^.*node_modules\//, ''); + console.log(name); + } +} +NODE +} + +audit_lockfile() { + local lockfile="$1" + local label="$2" + shift 2 + local allowed=("$@") + local found_violations=0 + local pkg + + while IFS= read -r pkg; do + [[ -z "$pkg" ]] && continue + if ! is_allowed "$pkg" "${allowed[@]}"; then + echo "VIOLATION: unauthorized install script in '${pkg}' (${label}: ${lockfile})" + found_violations=1 + fi + done < <(collect_has_install_script_packages "$lockfile") + + if [[ "$found_violations" -eq 1 ]]; then + echo "" + echo "FAILED: Found packages with install scripts not on the whitelist." + return 1 + fi + + echo "OK: All ${label} install scripts are from whitelisted packages." +} + +get_rust_gbt_napi_cli_version() { + local rust_gbt_package_json="${RUST_GBT_DIR}/package.json" + if [[ ! -f "$rust_gbt_package_json" ]]; then + echo "No rust/gbt package.json found at ${rust_gbt_package_json}" + return 1 + fi + + RUST_GBT_PACKAGE_JSON="$rust_gbt_package_json" node - <<'NODE' +const fs = require('fs'); + +const pkg = JSON.parse(fs.readFileSync(process.env.RUST_GBT_PACKAGE_JSON, 'utf8')); +const buildScript = (pkg.scripts && pkg.scripts.build) || ''; +const match = buildScript.match(/npm install --no-save @napi-rs\/cli@([^\s]+)/); + +if (!match) { + console.error('Could not determine the @napi-rs/cli version from rust/gbt/package.json.'); + process.exit(1); +} + +process.stdout.write(match[1]); +NODE +} + +generate_rust_gbt_audit_lockfile() { + local napi_cli_version="$1" + + TMP_AUDIT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/backend-rust-gbt-audit.XXXXXX")" + cat > "${TMP_AUDIT_DIR}/package.json" </dev/null + ) + + printf '%s\n' "${TMP_AUDIT_DIR}/package-lock.json" +} + +audit_rust_gbt_preinstall_chain() { + local napi_cli_version + local rust_gbt_lockfile + + napi_cli_version="$(get_rust_gbt_napi_cli_version)" + rust_gbt_lockfile="$(generate_rust_gbt_audit_lockfile "${napi_cli_version}")" + if [[ "${#RUST_GBT_ALLOWED[@]}" -gt 0 ]]; then + audit_lockfile "${rust_gbt_lockfile}" "backend preinstall toolchain" "${RUST_GBT_ALLOWED[@]}" + else + audit_lockfile "${rust_gbt_lockfile}" "backend preinstall toolchain" + fi +} + +audit_lockfile "${LOCKFILE}" "backend lockfile" "${BACKEND_ALLOWED[@]}" +audit_rust_gbt_preinstall_chain + +echo "OK: Backend install-script audit passed." diff --git a/backend/meta/scripts/safe-install.sh b/backend/meta/scripts/safe-install.sh new file mode 100755 index 000000000..ac5ed7d39 --- /dev/null +++ b/backend/meta/scripts/safe-install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Safely install backend npm dependencies by refreshing the lockfile without +# running install scripts, auditing it, then doing the real install. +# +# Usage (from repo root): +# backend/meta/scripts/safe-install.sh +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKEND_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +LOCKFILE="${BACKEND_DIR}/package-lock.json" +RESTORE_LOCKFILE_DONE=0 + +# Back up the lockfile so we can restore on failure +if [[ -f "$LOCKFILE" ]]; then + cp "$LOCKFILE" "${LOCKFILE}.bak" +fi + +restore_lockfile() { + trap - ERR INT TERM + if [[ "${RESTORE_LOCKFILE_DONE}" -eq 1 ]]; then + return + fi + RESTORE_LOCKFILE_DONE=1 + + if [[ -f "${LOCKFILE}.bak" ]]; then + mv "${LOCKFILE}.bak" "$LOCKFILE" + echo "Restored original package-lock.json." + elif [[ -f "$LOCKFILE" ]]; then + rm "$LOCKFILE" + echo "Removed generated package-lock.json." + fi +} + +trap restore_lockfile ERR INT TERM + +echo "==> Refreshing backend lockfile (--ignore-scripts --package-lock-only)..." +(cd "$BACKEND_DIR" && npm install --ignore-scripts --package-lock-only --no-audit --no-fund) + +echo "" +echo "==> Auditing lockfile for install scripts..." +bash "${SCRIPT_DIR}/check-install-scripts.sh" + +trap - ERR INT TERM +rm -f "${LOCKFILE}.bak" + +echo "" +echo "==> Installing backend (npm ci)..." +(cd "$BACKEND_DIR" && npm ci) + +echo "" +echo "Done." diff --git a/frontend/meta/scripts/check-install-scripts.sh b/frontend/meta/scripts/check-install-scripts.sh new file mode 100755 index 000000000..89a79489a --- /dev/null +++ b/frontend/meta/scripts/check-install-scripts.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# +# Audit frontend package-lock.json for unexpected hasInstallScript entries. +# Exits non-zero if any package not on the whitelist has hasInstallScript: true. +# +# Usage (from repo root): +# frontend/meta/scripts/check-install-scripts.sh +# + +set -euo pipefail + +FRONTEND_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LOCKFILE="${FRONTEND_DIR}/package-lock.json" + +ALLOWED=( + "mempool-frontend" + "@parcel/watcher" + "cypress" + "esbuild" + "fsevents" + "lmdb" + "msgpackr-extract" +) + +is_allowed() { + local pkg="$1" + for allowed in "${ALLOWED[@]}"; do + if [[ "$pkg" == "$allowed" ]]; then + return 0 + fi + done + return 1 +} + +if [[ ! -f "$LOCKFILE" ]]; then + echo "No package-lock.json found at ${LOCKFILE}" + exit 1 +fi + +found_violations=0 + +violations=$( + LOCKFILE_PATH="$LOCKFILE" node - <<'NODE' +const fs = require('fs'); + +const lock = JSON.parse(fs.readFileSync(process.env.LOCKFILE_PATH, 'utf8')); +const pkgs = lock.packages || {}; + +for (const [path, meta] of Object.entries(pkgs)) { + if (meta && meta.hasInstallScript) { + const name = path === '' ? (lock.name || '(root)') : path.replace(/^.*node_modules\//, ''); + console.log(name); + } +} +NODE +) + +while IFS= read -r pkg; do + [[ -z "$pkg" ]] && continue + if ! is_allowed "$pkg"; then + echo "VIOLATION: unauthorized install script in '${pkg}' (${LOCKFILE})" + found_violations=1 + fi +done <<< "$violations" + +if [[ "$found_violations" -eq 1 ]]; then + echo "" + echo "FAILED: Found packages with install scripts not on the whitelist." + echo "If this is a legitimate new dependency, add it to frontend/meta/scripts/check-install-scripts.sh" + exit 1 +else + echo "OK: All frontend install scripts are from whitelisted packages." +fi diff --git a/frontend/meta/scripts/safe-install.sh b/frontend/meta/scripts/safe-install.sh new file mode 100755 index 000000000..bdd8b72c9 --- /dev/null +++ b/frontend/meta/scripts/safe-install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Safely install frontend npm dependencies by refreshing the lockfile without +# running install scripts, auditing it, then doing the real install. +# +# Usage (from repo root): +# frontend/meta/scripts/safe-install.sh +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FRONTEND_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +LOCKFILE="${FRONTEND_DIR}/package-lock.json" +RESTORE_LOCKFILE_DONE=0 + +# Back up the lockfile so we can restore on failure +if [[ -f "$LOCKFILE" ]]; then + cp "$LOCKFILE" "${LOCKFILE}.bak" +fi + +restore_lockfile() { + trap - ERR INT TERM + if [[ "${RESTORE_LOCKFILE_DONE}" -eq 1 ]]; then + return + fi + RESTORE_LOCKFILE_DONE=1 + + if [[ -f "${LOCKFILE}.bak" ]]; then + mv "${LOCKFILE}.bak" "$LOCKFILE" + echo "Restored original package-lock.json." + elif [[ -f "$LOCKFILE" ]]; then + rm "$LOCKFILE" + echo "Removed generated package-lock.json." + fi +} + +trap restore_lockfile ERR INT TERM + +echo "==> Refreshing frontend lockfile (--ignore-scripts --package-lock-only)..." +(cd "$FRONTEND_DIR" && npm install --ignore-scripts --package-lock-only --no-audit --no-fund) + +echo "" +echo "==> Auditing lockfile for install scripts..." +bash "${SCRIPT_DIR}/check-install-scripts.sh" + +trap - ERR INT TERM +rm -f "${LOCKFILE}.bak" + +echo "" +echo "==> Installing frontend (npm ci)..." +(cd "$FRONTEND_DIR" && npm ci) + +echo "" +echo "Done."