Hardens security against injection attacks
Prevents shell injection through input validation and safe parameter passing Replaces direct sourcing with manual config parsing to avoid code execution Downloads and validates install scripts before execution instead of piping Uses base64 encoding for secure VM parameter transmission Adds checksum verification for binary downloads Creates secure temporary directories and files with proper permissions Addresses multiple security vulnerabilities in environment setup process
This commit is contained in:
parent
8c6fb6c3bc
commit
20fa7fa3c5
1 changed files with 405 additions and 59 deletions
464
setup_env.sh
464
setup_env.sh
|
|
@ -29,6 +29,13 @@ set -euo pipefail
|
|||
# ============================================================================
|
||||
ERLANG_VERSION="28.3.1"
|
||||
ELIXIR_VERSION="1.19.5-otp-28"
|
||||
PLAYWRIGHT_VERSION="1.50.1"
|
||||
# Claude Code version - check https://www.npmjs.com/package/@anthropic-ai/claude-code for latest
|
||||
CLAUDE_CODE_VERSION="1.0.16"
|
||||
# yq version - check https://github.com/mikefarah/yq/releases for latest
|
||||
YQ_VERSION="4.45.1"
|
||||
# watchexec version - check https://github.com/watchexec/watchexec/releases for latest
|
||||
WATCHEXEC_VERSION="2.3.2"
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
|
|
@ -63,6 +70,113 @@ command_exists() {
|
|||
command -v "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Security: Validate input contains only safe characters for config storage
|
||||
# Allows alphanumeric, spaces, common punctuation, but blocks shell metacharacters
|
||||
validate_safe_input() {
|
||||
local input="$1"
|
||||
local field_name="$2"
|
||||
# Block dangerous shell metacharacters: backticks, $, (), {}, ;, |, &, <, >, newlines
|
||||
if [[ "$input" =~ [\`\$\(\)\{\}\;\|\&\<\>\"] ]] || [[ "$input" == *$'\n'* ]]; then
|
||||
log_error "$field_name contains invalid characters (shell metacharacters not allowed)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Security: Validate VM name contains only safe characters
|
||||
validate_vm_name() {
|
||||
local name="$1"
|
||||
if [[ ! "$name" =~ ^[a-zA-Z][a-zA-Z0-9_-]*$ ]]; then
|
||||
log_error "VM name must start with a letter and contain only alphanumeric characters, hyphens, and underscores"
|
||||
return 1
|
||||
fi
|
||||
if [ ${#name} -gt 64 ]; then
|
||||
log_error "VM name must be 64 characters or less"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Security: Validate VNC password
|
||||
validate_vnc_password() {
|
||||
local password="$1"
|
||||
if [ ${#password} -lt 6 ]; then
|
||||
echo "Password must be at least 6 characters."
|
||||
return 1
|
||||
fi
|
||||
if [ ${#password} -gt 64 ]; then
|
||||
echo "Password must be 64 characters or less."
|
||||
return 1
|
||||
fi
|
||||
# Block characters that could cause shell injection
|
||||
if [[ "$password" =~ [\`\$\(\)\{\}\;\|\&\<\>\'\"] ]]; then
|
||||
echo "Password contains invalid characters."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Security: Detect system architecture and return normalized name
|
||||
# Returns: amd64, arm64, or exits with error for unsupported architectures
|
||||
detect_architecture() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64|amd64)
|
||||
echo "amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
echo "arm64"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported architecture: $arch"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Security: Download and verify a binary with optional checksum
|
||||
# Args: $1=url, $2=output_path, $3=expected_checksum (optional, sha256)
|
||||
download_verified_binary() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
local expected_checksum="${3:-}"
|
||||
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Download to temp file first
|
||||
if ! curl -fsSL "$url" -o "$temp_file"; then
|
||||
log_error "Failed to download: $url"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify checksum if provided
|
||||
if [ -n "$expected_checksum" ]; then
|
||||
local actual_checksum
|
||||
actual_checksum=$(sha256sum "$temp_file" | awk '{print $1}')
|
||||
if [ "$actual_checksum" != "$expected_checksum" ]; then
|
||||
log_error "Checksum mismatch for $url"
|
||||
log_error "Expected: $expected_checksum"
|
||||
log_error "Got: $actual_checksum"
|
||||
rm -f "$temp_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Move to final location
|
||||
if [[ "$output" == /usr/* ]]; then
|
||||
sudo mv "$temp_file" "$output"
|
||||
sudo chmod +x "$output"
|
||||
else
|
||||
mv "$temp_file" "$output"
|
||||
chmod +x "$output"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# macOS Host Mode
|
||||
# ============================================================================
|
||||
|
|
@ -87,42 +201,87 @@ if [[ "$(uname -s)" == "Darwin" ]]; then
|
|||
# VM name from argument or default
|
||||
VM_NAME="${1:-dev-sandbox}"
|
||||
|
||||
# Security: Validate VM name (Issue #6)
|
||||
if ! validate_vm_name "$VM_NAME"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Locate config.env relative to this script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/config.env"
|
||||
|
||||
# Security: Load config safely without executing code (Issue #2)
|
||||
# Instead of sourcing, parse key=value pairs manually
|
||||
load_config_safely() {
|
||||
local config_file="$1"
|
||||
while IFS='=' read -r key value || [ -n "$key" ]; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
# Remove leading/trailing whitespace from key
|
||||
key=$(echo "$key" | xargs)
|
||||
# Remove surrounding quotes from value
|
||||
value=$(echo "$value" | sed 's/^"//;s/"$//;s/^'"'"'//;s/'"'"'$//')
|
||||
case "$key" in
|
||||
GIT_NAME) GIT_NAME="$value" ;;
|
||||
GIT_EMAIL) GIT_EMAIL="$value" ;;
|
||||
# VNC_PASSWORD intentionally not stored - prompted each time for security
|
||||
esac
|
||||
done < "$config_file"
|
||||
}
|
||||
|
||||
# Create or load config
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
source "$CONFIG_FILE"
|
||||
load_config_safely "$CONFIG_FILE"
|
||||
echo -e "${YELLOW}Using saved config from: $CONFIG_FILE${NC}"
|
||||
echo " Name: $GIT_NAME"
|
||||
echo " Email: $GIT_EMAIL"
|
||||
echo " VNC Pass: ******"
|
||||
echo ""
|
||||
else
|
||||
echo "First run — creating config file for shared credentials."
|
||||
echo ""
|
||||
read -rp "Enter your name for git config: " GIT_NAME
|
||||
read -rp "Enter your email for git config: " GIT_EMAIL
|
||||
|
||||
# Security: Validate git name (Issue #5, #10, #11)
|
||||
while true; do
|
||||
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
|
||||
echo
|
||||
if [ ${#VNC_PASSWORD} -ge 6 ]; then
|
||||
read -rp "Git commit author name: " GIT_NAME
|
||||
if [ -z "$GIT_NAME" ]; then
|
||||
echo "Name is required."
|
||||
continue
|
||||
fi
|
||||
if [ ${#GIT_NAME} -gt 256 ]; then
|
||||
echo "Name must be 256 characters or less."
|
||||
continue
|
||||
fi
|
||||
if validate_safe_input "$GIT_NAME" "Name"; then
|
||||
break
|
||||
fi
|
||||
echo "Password must be at least 6 characters."
|
||||
done
|
||||
|
||||
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
|
||||
log_error "Name and email are required."
|
||||
exit 1
|
||||
fi
|
||||
# Security: Validate git email (Issue #5, #10, #11)
|
||||
while true; do
|
||||
read -rp "Git commit author email: " GIT_EMAIL
|
||||
if [ -z "$GIT_EMAIL" ]; then
|
||||
echo "Email is required."
|
||||
continue
|
||||
fi
|
||||
if [ ${#GIT_EMAIL} -gt 256 ]; then
|
||||
echo "Email must be 256 characters or less."
|
||||
continue
|
||||
fi
|
||||
if ! [[ "$GIT_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
|
||||
echo "Please enter a valid email address."
|
||||
continue
|
||||
fi
|
||||
if validate_safe_input "$GIT_EMAIL" "Email"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
GIT_NAME="$GIT_NAME"
|
||||
GIT_EMAIL="$GIT_EMAIL"
|
||||
VNC_PASSWORD="$VNC_PASSWORD"
|
||||
EOF
|
||||
# Security: Write config with proper escaping (Issue #10)
|
||||
# VNC password is NOT stored - will be prompted when VNC is selected
|
||||
{
|
||||
printf 'GIT_NAME="%s"\n' "$GIT_NAME"
|
||||
printf 'GIT_EMAIL="%s"\n' "$GIT_EMAIL"
|
||||
} > "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
echo ""
|
||||
echo "Config saved to: $CONFIG_FILE"
|
||||
|
|
@ -260,6 +419,27 @@ EOF
|
|||
fi
|
||||
echo ""
|
||||
|
||||
# Security: Prompt for VNC password only when VNC is selected (Issue #4)
|
||||
# Password is never stored to disk - prompted fresh each time
|
||||
VNC_PASSWORD=""
|
||||
VNC_INDEX=-1
|
||||
for ((i=0; i<COMP_COUNT; i++)); do
|
||||
if [ "${COMP_IDS[$i]}" = "vnc" ]; then
|
||||
VNC_INDEX=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$VNC_INDEX" -ge 0 ] && [ "${COMP_SELECTED[$VNC_INDEX]}" = "1" ]; then
|
||||
while true; do
|
||||
read -rsp "Enter VNC password (6-64 chars): " VNC_PASSWORD
|
||||
echo
|
||||
if validate_vnc_password "$VNC_PASSWORD"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if VM already exists
|
||||
if orb list 2>/dev/null | grep -qw "$VM_NAME"; then
|
||||
log_error "VM '$VM_NAME' already exists."
|
||||
|
|
@ -294,7 +474,14 @@ EOF
|
|||
fi
|
||||
echo -e "${YELLOW}Running provisioning inside VM (this will take a while)...${NC}"
|
||||
echo ""
|
||||
orb run -m "$VM_NAME" bash -c "${SKIP_EXPORTS}export GIT_NAME='$GIT_NAME'; export GIT_EMAIL='$GIT_EMAIL'; export VNC_PASSWORD='$VNC_PASSWORD'; bash /tmp/setup_env.sh --non-interactive"
|
||||
|
||||
# Security: Use base64 encoding to safely pass values to VM (Issue #1)
|
||||
# This prevents any shell injection through special characters
|
||||
GIT_NAME_B64=$(printf '%s' "$GIT_NAME" | base64)
|
||||
GIT_EMAIL_B64=$(printf '%s' "$GIT_EMAIL" | base64)
|
||||
VNC_PASSWORD_B64=$(printf '%s' "$VNC_PASSWORD" | base64)
|
||||
|
||||
orb run -m "$VM_NAME" bash -c "${SKIP_EXPORTS}export GIT_NAME=\$(echo '$GIT_NAME_B64' | base64 -d); export GIT_EMAIL=\$(echo '$GIT_EMAIL_B64' | base64 -d); export VNC_PASSWORD=\$(echo '$VNC_PASSWORD_B64' | base64 -d); bash /tmp/setup_env.sh --non-interactive"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}============================================================================${NC}"
|
||||
|
|
@ -326,9 +513,18 @@ fi
|
|||
# Linux VM Mode (everything below runs inside the VM)
|
||||
# ============================================================================
|
||||
|
||||
# Security: Create log file with restrictive permissions (Issue #13)
|
||||
LOG_FILE="/tmp/setup_env_$(date +%Y%m%d_%H%M%S).log"
|
||||
LOG_DIR="/tmp/setup_env_steps_$$"
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE"
|
||||
chmod 600 "$LOG_FILE"
|
||||
|
||||
# Security: Use mktemp for unpredictable temp directory (Issue #8, #9)
|
||||
LOG_DIR=$(mktemp -d "/tmp/setup_env_steps.XXXXXXXXXX")
|
||||
if [ ! -d "$LOG_DIR" ]; then
|
||||
log_error "Failed to create secure temp directory"
|
||||
exit 1
|
||||
fi
|
||||
chmod 700 "$LOG_DIR"
|
||||
|
||||
# Prevent mise from reading host config via OrbStack mount (/Users/john/.mise.toml)
|
||||
export MISE_GLOBAL_CONFIG_FILE="$HOME/.config/mise/config.toml"
|
||||
|
|
@ -629,7 +825,7 @@ cleanup() {
|
|||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
# Note: trap is set later to include both cleanup and stop_sudo_keepalive
|
||||
|
||||
# ============================================================================
|
||||
# Step functions (each is a self-contained install)
|
||||
|
|
@ -658,7 +854,12 @@ install_postgresql() {
|
|||
|
||||
[ -z "$PG_HBA" ] || [ -z "$PG_CONF" ] && return 1
|
||||
|
||||
sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+scram-sha-256$/host all all 127.0.0.1\/32 trust/' "$PG_HBA"
|
||||
# Security: Use 'peer' for local connections (authenticates via OS user)
|
||||
# This is more secure than 'trust' while still convenient for development
|
||||
# Users connect via: psql -U $USER dbname (no password needed if OS user matches DB user)
|
||||
sudo sed -i 's/^local\s\+all\s\+all\s\+peer$/local all all peer/' "$PG_HBA"
|
||||
# For TCP localhost, keep scram-sha-256 (requires password) instead of trust
|
||||
# This prevents any local process from accessing the DB without credentials
|
||||
|
||||
if ! sudo grep -q "# OrbStack host access" "$PG_HBA"; then
|
||||
echo "# OrbStack host access" | sudo tee -a "$PG_HBA" > /dev/null
|
||||
|
|
@ -678,7 +879,30 @@ install_mise() {
|
|||
echo "mise already installed"
|
||||
return 0
|
||||
fi
|
||||
curl -fsSL https://mise.run | sh
|
||||
|
||||
# Security: Download script first, validate, then execute (Issue #3)
|
||||
# This allows for inspection and avoids direct pipe to shell
|
||||
local mise_script
|
||||
mise_script=$(mktemp)
|
||||
curl -fsSL https://mise.run -o "$mise_script"
|
||||
|
||||
# Verify it's a shell script with multiple checks
|
||||
if ! head -1 "$mise_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
|
||||
log_error "Downloaded mise script doesn't have a valid shell shebang"
|
||||
rm -f "$mise_script"
|
||||
return 1
|
||||
fi
|
||||
# Check file size is reasonable (not empty, not huge)
|
||||
local script_size
|
||||
script_size=$(wc -c < "$mise_script")
|
||||
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
|
||||
log_error "Downloaded mise script has suspicious size: $script_size bytes"
|
||||
rm -f "$mise_script"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sh "$mise_script"
|
||||
rm -f "$mise_script"
|
||||
|
||||
# Add to bashrc
|
||||
if ! grep -q 'mise activate bash' ~/.bashrc; then
|
||||
|
|
@ -726,11 +950,11 @@ install_chromium() {
|
|||
sudo apt-get install -y -q chromium --no-install-recommends 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Symlink if installed
|
||||
# Symlink if installed (check for existing file, not just symlink)
|
||||
local bin=""
|
||||
command_exists chromium-browser && bin=$(which chromium-browser)
|
||||
command_exists chromium && bin=$(which chromium)
|
||||
if [ -n "$bin" ] && [ ! -L /usr/bin/google-chrome ]; then
|
||||
if [ -n "$bin" ] && [ ! -e /usr/bin/google-chrome ]; then
|
||||
sudo ln -sf "$bin" /usr/bin/google-chrome
|
||||
fi
|
||||
}
|
||||
|
|
@ -738,8 +962,17 @@ install_chromium() {
|
|||
install_vnc() {
|
||||
sudo apt-get install -y -q tigervnc-standalone-server xfce4 xfce4-terminal dbus-x11
|
||||
|
||||
# Create .vnc directory with secure permissions
|
||||
mkdir -p ~/.vnc
|
||||
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
|
||||
chmod 700 ~/.vnc
|
||||
|
||||
# Security: Create password file atomically with correct permissions
|
||||
# Use umask to ensure file is created with 600 permissions from the start
|
||||
(
|
||||
umask 077
|
||||
echo "$VNC_PASSWORD" | vncpasswd -f > ~/.vnc/passwd
|
||||
)
|
||||
# Ensure permissions are correct (belt and suspenders)
|
||||
chmod 600 ~/.vnc/passwd
|
||||
|
||||
cat > ~/.vnc/xstartup <<'XEOF'
|
||||
|
|
@ -754,6 +987,10 @@ XEOF
|
|||
|
||||
cat > ~/bin/vnc-start <<'VEOF'
|
||||
#!/bin/bash
|
||||
# Security note: VNC listens on all interfaces (-localhost no) to allow
|
||||
# connections from the macOS host via OrbStack's network. The VM is only
|
||||
# accessible from the host machine, not external networks.
|
||||
# VNC is password-protected (set during setup).
|
||||
VNC_HOST="$(hostname -s).orb.local"
|
||||
if pgrep -f "Xtigervnc :1" > /dev/null 2>&1; then
|
||||
echo "VNC is already running on display :1"
|
||||
|
|
@ -782,24 +1019,49 @@ install_ollama() {
|
|||
echo "Ollama already installed"
|
||||
return 0
|
||||
fi
|
||||
curl -fsSL https://ollama.com/install.sh | sudo sh
|
||||
|
||||
# Security: Download script first, validate, then execute with sudo (Issue #3)
|
||||
local ollama_script
|
||||
ollama_script=$(mktemp)
|
||||
curl -fsSL https://ollama.com/install.sh -o "$ollama_script"
|
||||
|
||||
# Verify it's a shell script with multiple checks
|
||||
if ! head -1 "$ollama_script" | grep -qE '^#!\s*/(bin|usr/bin)/(env\s+)?(ba)?sh'; then
|
||||
log_error "Downloaded Ollama script doesn't have a valid shell shebang"
|
||||
rm -f "$ollama_script"
|
||||
return 1
|
||||
fi
|
||||
# Check file size is reasonable (not empty, not huge)
|
||||
local script_size
|
||||
script_size=$(wc -c < "$ollama_script")
|
||||
if [ "$script_size" -lt 100 ] || [ "$script_size" -gt 1000000 ]; then
|
||||
log_error "Downloaded Ollama script has suspicious size: $script_size bytes"
|
||||
rm -f "$ollama_script"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sudo sh "$ollama_script"
|
||||
rm -f "$ollama_script"
|
||||
}
|
||||
|
||||
install_claude() {
|
||||
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
# Security: Pin to specific version to prevent supply chain attacks
|
||||
npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
|
||||
}
|
||||
|
||||
install_playwright() {
|
||||
export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"
|
||||
npx --yes playwright install --with-deps chromium
|
||||
# Security: Install specific version globally, then use it (avoids npx auto-download)
|
||||
npm install -g "playwright@${PLAYWRIGHT_VERSION}"
|
||||
playwright install --with-deps chromium
|
||||
|
||||
# Symlink Playwright's Chromium if no system one
|
||||
# Symlink Playwright's Chromium if no system one (check for existing files)
|
||||
if ! command_exists chromium-browser && ! command_exists chromium; then
|
||||
local pw_chrome=$(find "$HOME/.cache/ms-playwright" -name "chrome" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$pw_chrome" ]; then
|
||||
sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
|
||||
sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
|
||||
[ ! -e /usr/local/bin/chromium ] && sudo ln -sf "$pw_chrome" /usr/local/bin/chromium
|
||||
[ ! -e /usr/local/bin/google-chrome ] && sudo ln -sf "$pw_chrome" /usr/local/bin/google-chrome
|
||||
echo "Using Playwright's bundled Chromium"
|
||||
fi
|
||||
fi
|
||||
|
|
@ -838,20 +1100,79 @@ install_base_deps() {
|
|||
python3 python3-pip python3-venv
|
||||
|
||||
# fd symlink (fd-find installs as 'fdfind', symlink to 'fd')
|
||||
if [ ! -L /usr/local/bin/fd ] && command_exists fdfind; then
|
||||
# Check for existing file, not just symlink
|
||||
if [ ! -e /usr/local/bin/fd ] && command_exists fdfind; then
|
||||
sudo ln -sf "$(which fdfind)" /usr/local/bin/fd
|
||||
fi
|
||||
|
||||
# yq
|
||||
# yq - with architecture detection, version pinning, and checksum verification
|
||||
if ! command_exists yq; then
|
||||
sudo curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" -o /usr/local/bin/yq
|
||||
sudo chmod +x /usr/local/bin/yq
|
||||
local yq_arch
|
||||
local arch
|
||||
arch=$(detect_architecture)
|
||||
case "$arch" in
|
||||
amd64) yq_arch="amd64" ;;
|
||||
arm64) yq_arch="arm64" ;;
|
||||
esac
|
||||
local yq_binary="yq_linux_${yq_arch}"
|
||||
local yq_url="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/${yq_binary}"
|
||||
local yq_checksum_url="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/checksums"
|
||||
|
||||
# Fetch and verify checksum
|
||||
local temp_dir
|
||||
temp_dir=$(mktemp -d)
|
||||
curl -fsSL "$yq_checksum_url" -o "$temp_dir/checksums"
|
||||
local expected_checksum
|
||||
expected_checksum=$(grep "${yq_binary}$" "$temp_dir/checksums" | awk '{print $1}')
|
||||
|
||||
if [ -n "$expected_checksum" ]; then
|
||||
download_verified_binary "$yq_url" "/usr/local/bin/yq" "$expected_checksum"
|
||||
else
|
||||
log_error "Could not find checksum for yq, downloading without verification"
|
||||
download_verified_binary "$yq_url" "/usr/local/bin/yq"
|
||||
fi
|
||||
rm -rf "$temp_dir"
|
||||
fi
|
||||
|
||||
# watchexec
|
||||
# watchexec - with architecture detection, version pinning, and checksum verification
|
||||
if ! command_exists watchexec; then
|
||||
curl -fsSL "https://github.com/watchexec/watchexec/releases/download/v2.3.2/watchexec-2.3.2-aarch64-unknown-linux-gnu.tar.xz" | \
|
||||
sudo tar -xJ --strip-components=1 -C /usr/local/bin/ --wildcards '*/watchexec'
|
||||
local watchexec_arch
|
||||
local arch
|
||||
arch=$(detect_architecture)
|
||||
case "$arch" in
|
||||
amd64) watchexec_arch="x86_64-unknown-linux-gnu" ;;
|
||||
arm64) watchexec_arch="aarch64-unknown-linux-gnu" ;;
|
||||
esac
|
||||
local watchexec_tarball="watchexec-${WATCHEXEC_VERSION}-${watchexec_arch}.tar.xz"
|
||||
local watchexec_url="https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/${watchexec_tarball}"
|
||||
local watchexec_checksum_url="https://github.com/watchexec/watchexec/releases/download/v${WATCHEXEC_VERSION}/SHA512SUMS"
|
||||
|
||||
local temp_dir
|
||||
temp_dir=$(mktemp -d)
|
||||
|
||||
# Fetch checksum file
|
||||
curl -fsSL "$watchexec_checksum_url" -o "$temp_dir/SHA512SUMS" 2>/dev/null || true
|
||||
local expected_checksum
|
||||
expected_checksum=$(grep "${watchexec_tarball}$" "$temp_dir/SHA512SUMS" 2>/dev/null | awk '{print $1}')
|
||||
|
||||
if curl -fsSL "$watchexec_url" -o "$temp_dir/watchexec.tar.xz"; then
|
||||
# Verify checksum if available (SHA512 for watchexec)
|
||||
if [ -n "$expected_checksum" ]; then
|
||||
local actual_checksum
|
||||
actual_checksum=$(sha512sum "$temp_dir/watchexec.tar.xz" | awk '{print $1}')
|
||||
if [ "$actual_checksum" != "$expected_checksum" ]; then
|
||||
log_error "Checksum mismatch for watchexec"
|
||||
rm -rf "$temp_dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
tar -xJf "$temp_dir/watchexec.tar.xz" -C "$temp_dir" --strip-components=1
|
||||
sudo mv "$temp_dir/watchexec" /usr/local/bin/watchexec
|
||||
sudo chmod +x /usr/local/bin/watchexec
|
||||
else
|
||||
log_error "Failed to download watchexec"
|
||||
fi
|
||||
rm -rf "$temp_dir"
|
||||
fi
|
||||
|
||||
# direnv hook
|
||||
|
|
@ -875,35 +1196,55 @@ if ! command_exists sudo; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Security: Sudo keepalive - prevent sudo timeout during long operations
|
||||
# Runs in background and refreshes sudo timestamp every 50 seconds
|
||||
SUDO_KEEPALIVE_PID=""
|
||||
start_sudo_keepalive() {
|
||||
# In non-interactive mode (VM), sudo should be passwordless - skip keepalive
|
||||
if [ "$NON_INTERACTIVE" = true ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Validate sudo access first (allow password prompt to show)
|
||||
if ! sudo -v; then
|
||||
log_error "Failed to obtain sudo privileges"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Start background process to refresh sudo timestamp
|
||||
(
|
||||
while true; do
|
||||
sudo -n true 2>/dev/null
|
||||
sleep 50
|
||||
done
|
||||
) &
|
||||
SUDO_KEEPALIVE_PID=$!
|
||||
}
|
||||
|
||||
stop_sudo_keepalive() {
|
||||
if [ -n "$SUDO_KEEPALIVE_PID" ] && kill -0 "$SUDO_KEEPALIVE_PID" 2>/dev/null; then
|
||||
kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||
wait "$SUDO_KEEPALIVE_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure keepalive is stopped on exit
|
||||
# Update trap to include both cleanup functions
|
||||
trap 'stop_sudo_keepalive; cleanup' EXIT
|
||||
|
||||
# ============================================================================
|
||||
# Git configuration prompts
|
||||
# Git configuration prompts (VM-side, non-interactive mode only)
|
||||
# ============================================================================
|
||||
if [ "$NON_INTERACTIVE" = true ]; then
|
||||
if [ -z "${GIT_NAME:-}" ] || [ -z "${GIT_EMAIL:-}" ]; then
|
||||
log_error "In non-interactive mode, GIT_NAME and GIT_EMAIL env vars are required."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${VNC_PASSWORD:-}" ]; then
|
||||
log_error "In non-interactive mode, VNC_PASSWORD env var is required (min 6 chars)."
|
||||
# Only require VNC_PASSWORD if VNC is being installed
|
||||
if [ -z "${SKIP_VNC:-}" ] && [ -z "${VNC_PASSWORD:-}" ]; then
|
||||
log_error "In non-interactive mode, VNC_PASSWORD env var is required when VNC is selected (min 6 chars)."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
read -rp "Enter your name for git config: " GIT_NAME
|
||||
read -rp "Enter your email for git config: " GIT_EMAIL
|
||||
|
||||
if [ -z "$GIT_NAME" ] || [ -z "$GIT_EMAIL" ]; then
|
||||
log_error "Name and email are required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -rsp "Enter VNC password (min 6 chars): " VNC_PASSWORD
|
||||
echo
|
||||
if [ ${#VNC_PASSWORD} -ge 6 ]; then
|
||||
break
|
||||
fi
|
||||
echo "Password must be at least 6 characters."
|
||||
done
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
|
|
@ -956,6 +1297,11 @@ echo ""
|
|||
draw_dashboard
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Start sudo keepalive to prevent timeout during long operations
|
||||
# ============================================================================
|
||||
start_sudo_keepalive
|
||||
|
||||
# ============================================================================
|
||||
# Phase 1: Base dependencies (sequential, everything depends on this)
|
||||
# ============================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue