mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
add scripts for checking for common supply chain attack vectors
This commit is contained in:
parent
bcba5fb0c9
commit
a1821f9e57
4 changed files with 336 additions and 0 deletions
153
backend/meta/scripts/check-install-scripts.sh
Executable file
153
backend/meta/scripts/check-install-scripts.sh
Executable file
|
|
@ -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" <<EOF
|
||||
{
|
||||
"name": "rust-gbt-install-audit",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "${napi_cli_version}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
(
|
||||
cd "${TMP_AUDIT_DIR}"
|
||||
npm install --package-lock-only --ignore-scripts --no-audit --no-fund >/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."
|
||||
55
backend/meta/scripts/safe-install.sh
Executable file
55
backend/meta/scripts/safe-install.sh
Executable file
|
|
@ -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."
|
||||
73
frontend/meta/scripts/check-install-scripts.sh
Executable file
73
frontend/meta/scripts/check-install-scripts.sh
Executable file
|
|
@ -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
|
||||
55
frontend/meta/scripts/safe-install.sh
Executable file
55
frontend/meta/scripts/safe-install.sh
Executable file
|
|
@ -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."
|
||||
Loading…
Add table
Add a link
Reference in a new issue