This commit is contained in:
Reinier Criel 2026-04-13 11:01:45 -07:00
parent 1cf8fd1241
commit d064d46668
32 changed files with 429 additions and 400 deletions

View file

@ -4,25 +4,49 @@
param(
[switch]$ci,
[switch]$includepython
[switch]$includepython,
[string]$InstallDir
)
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
function Test-InstallDir {
param([string]$Dir)
# Validate SAFE_CHAIN_DIR before use
if ($env:SAFE_CHAIN_DIR) {
if (-not [System.IO.Path]::IsPathRooted($env:SAFE_CHAIN_DIR)) {
Write-Host "[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: $($env:SAFE_CHAIN_DIR)" -ForegroundColor Red; exit 1
if ([string]::IsNullOrWhiteSpace($Dir)) {
return @{ Ok = $true; Normalized = $null }
}
if ($env:SAFE_CHAIN_DIR -match '\.\.') {
Write-Host "[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)" -ForegroundColor Red; exit 1
if (-not [System.IO.Path]::IsPathRooted($Dir)) {
return @{ Ok = $false; Reason = "-InstallDir must be an absolute path, got: $Dir" }
}
if ($env:SAFE_CHAIN_DIR -match '^[A-Za-z]:[/\\]?$' -or $env:SAFE_CHAIN_DIR -eq '/') {
Write-Host "[ERROR] SAFE_CHAIN_DIR cannot be a root or drive-root directory" -ForegroundColor Red; exit 1
if ($Dir.Contains([System.IO.Path]::PathSeparator)) {
return @{ Ok = $false; Reason = "-InstallDir must not contain the PATH separator ($([System.IO.Path]::PathSeparator))" }
}
$normalized = [System.IO.Path]::GetFullPath($Dir)
$root = [System.IO.Path]::GetPathRoot($normalized)
if ($normalized.TrimEnd('\', '/') -eq $root.TrimEnd('\', '/')) {
return @{ Ok = $false; Reason = "-InstallDir cannot be a root or drive-root directory" }
}
$segments = $normalized.Substring($root.Length).Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries)
if ($segments -contains "..") {
return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" }
}
return @{ Ok = $true; Normalized = $normalized }
}
$SafeChainBase = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $env:USERPROFILE ".safe-chain" }
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
$SafeChainBase = if ($InstallDir) { $InstallDir } else { Join-Path $env:USERPROFILE ".safe-chain" }
$installDirValidation = Test-InstallDir -Dir $SafeChainBase
if (-not $installDirValidation.Ok) {
Write-Host "[ERROR] $($installDirValidation.Reason)" -ForegroundColor Red
exit 1
}
$SafeChainBase = $installDirValidation.Normalized
$InstallDir = Join-Path $SafeChainBase "bin"
$RepoUrl = "https://github.com/AikidoSec/safe-chain"

View file

@ -6,24 +6,50 @@
set -e # Exit on error
validate_install_dir() {
dir="$1"
if [ -z "$dir" ]; then
return 0
fi
case "$dir" in
/*) ;;
*)
printf '[ERROR] --install-dir must be an absolute path, got: %s\n' "$dir" >&2
exit 1
;;
esac
case "$dir" in
*:*)
printf '[ERROR] --install-dir must not contain the PATH separator (:)\n' >&2
exit 1
;;
esac
if [ "$dir" = "/" ]; then
printf '[ERROR] --install-dir cannot be a root or drive-root directory\n' >&2
exit 1
fi
old_ifs=$IFS
IFS='/'
set -- $dir
IFS=$old_ifs
for segment in "$@"; do
if [ "$segment" = ".." ]; then
printf '[ERROR] --install-dir must not contain path traversal segments\n' >&2
exit 1
fi
done
}
# Configuration
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
SAFE_CHAIN_BASE="${HOME}/.safe-chain"
# Validate SAFE_CHAIN_DIR before use
if [ -n "${SAFE_CHAIN_DIR}" ]; then
case "${SAFE_CHAIN_DIR}" in
/*) ;;
*) printf '[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: %s\n' "${SAFE_CHAIN_DIR}" >&2; exit 1 ;;
esac
case "${SAFE_CHAIN_DIR}" in
*../*|*/..*|..) printf '[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)\n' >&2; exit 1 ;;
esac
if [ "${SAFE_CHAIN_DIR}" = "/" ]; then
printf '[ERROR] SAFE_CHAIN_DIR cannot be the root directory\n' >&2; exit 1
fi
fi
SAFE_CHAIN_BASE="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}"
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
REPO_URL="https://github.com/AikidoSec/safe-chain"
@ -245,19 +271,33 @@ remove_nvm_installation() {
# Parse command-line arguments
parse_arguments() {
for arg in "$@"; do
case "$arg" in
while [ $# -gt 0 ]; do
case "$1" in
--ci)
USE_CI_SETUP=true
;;
--install-dir)
shift
if [ $# -eq 0 ]; then
error "Missing value for --install-dir"
fi
SAFE_CHAIN_BASE="$1"
;;
--install-dir=*)
SAFE_CHAIN_BASE="${1#--install-dir=}"
;;
--include-python)
warn "--include-python is deprecated and ignored. Python ecosystem is now included by default."
;;
*)
error "Unknown argument: $arg"
error "Unknown argument: $1"
;;
esac
shift
done
validate_install_dir "${SAFE_CHAIN_BASE}"
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
}
# Main installation

View file

@ -5,22 +5,6 @@
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
# Validate SAFE_CHAIN_DIR before use
if ($env:SAFE_CHAIN_DIR) {
if (-not [System.IO.Path]::IsPathRooted($env:SAFE_CHAIN_DIR)) {
Write-Host "[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: $($env:SAFE_CHAIN_DIR)" -ForegroundColor Red; exit 1
}
if ($env:SAFE_CHAIN_DIR -match '\.\.') {
Write-Host "[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)" -ForegroundColor Red; exit 1
}
if ($env:SAFE_CHAIN_DIR -match '^[A-Za-z]:[/\\]?$' -or $env:SAFE_CHAIN_DIR -eq '/') {
Write-Host "[ERROR] SAFE_CHAIN_DIR cannot be a root or drive-root directory" -ForegroundColor Red; exit 1
}
}
$DotSafeChain = if ($env:SAFE_CHAIN_DIR) { $env:SAFE_CHAIN_DIR } else { Join-Path $HomeDir ".safe-chain" }
$InstallDir = Join-Path $DotSafeChain "bin"
# Helper functions
function Write-Info {
param([string]$Message)
@ -38,6 +22,64 @@ function Write-Error-Custom {
exit 1
}
function Get-InstallDirFromBinaryPath {
param([string]$BinaryPath)
if ([string]::IsNullOrWhiteSpace($BinaryPath)) {
return $null
}
try {
$resolvedPath = (Resolve-Path -LiteralPath $BinaryPath -ErrorAction Stop).Path
}
catch {
$resolvedPath = [System.IO.Path]::GetFullPath($BinaryPath)
}
$fileName = [System.IO.Path]::GetFileName($resolvedPath)
if (($fileName -ne "safe-chain") -and ($fileName -ne "safe-chain.exe")) {
return $null
}
if ($resolvedPath -match '\.(js|cjs|mjs|cmd|ps1)$') {
return $null
}
$binDir = Split-Path -Parent $resolvedPath
if ((Split-Path -Leaf $binDir) -ne "bin") {
return $null
}
return (Split-Path -Parent $binDir)
}
function Get-SafeChainInstallDir {
$command = Get-Command safe-chain -ErrorAction SilentlyContinue | Select-Object -First 1
if ($command) {
try {
$reportedInstallDir = & safe-chain get-install-dir 2>$null | Select-Object -First 1
if ($reportedInstallDir) {
$reportedInstallDir = $reportedInstallDir.Trim()
}
if ($reportedInstallDir) {
return $reportedInstallDir
}
}
catch {
# Fall back to deriving the install dir from the discovered command path
}
}
if ($command -and $command.Path) {
$discoveredInstallDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path
if ($discoveredInstallDir) {
return $discoveredInstallDir
}
}
return (Join-Path $HomeDir ".safe-chain")
}
# Check and uninstall npm global package if present
function Remove-NpmInstallation {
# Check if npm is available
@ -90,6 +132,8 @@ function Remove-VoltaInstallation {
# Main uninstallation
function Uninstall-SafeChain {
Write-Info "Uninstalling safe-chain..."
$DotSafeChain = Get-SafeChainInstallDir
$InstallDir = Join-Path $DotSafeChain "bin"
# Run teardown if safe-chain is available
# Check for both safe-chain.exe (Windows) and safe-chain (Unix) since PowerShell Core runs on all platforms

View file

@ -8,22 +8,6 @@ set -e # Exit on error
# Configuration
# Validate SAFE_CHAIN_DIR before use
if [ -n "${SAFE_CHAIN_DIR}" ]; then
case "${SAFE_CHAIN_DIR}" in
/*) ;;
*) printf '[ERROR] SAFE_CHAIN_DIR must be an absolute path, got: %s\n' "${SAFE_CHAIN_DIR}" >&2; exit 1 ;;
esac
case "${SAFE_CHAIN_DIR}" in
*../*|*/..*|..) printf '[ERROR] SAFE_CHAIN_DIR must not contain path traversal (..)\n' >&2; exit 1 ;;
esac
if [ "${SAFE_CHAIN_DIR}" = "/" ]; then
printf '[ERROR] SAFE_CHAIN_DIR cannot be the root directory\n' >&2; exit 1
fi
fi
DOT_SAFE_CHAIN="${SAFE_CHAIN_DIR:-${HOME}/.safe-chain}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
@ -49,6 +33,78 @@ command_exists() {
command -v "$1" >/dev/null 2>&1
}
resolve_path() {
target="$1"
while [ -L "$target" ]; do
link_target=$(readlink "$target" 2>/dev/null || echo "")
if [ -z "$link_target" ]; then
break
fi
case "$link_target" in
/*) target="$link_target" ;;
*)
target="$(dirname "$target")/$link_target"
;;
esac
done
target_dir=$(dirname "$target")
target_name=$(basename "$target")
if cd "$target_dir" 2>/dev/null; then
printf '%s/%s\n' "$(pwd -P)" "$target_name"
else
printf '%s\n' "$target"
fi
}
derive_install_dir_from_binary() {
binary_path="$1"
if [ -z "$binary_path" ]; then
return 1
fi
resolved_path=$(resolve_path "$binary_path")
binary_name=$(basename "$resolved_path")
case "$binary_name" in
safe-chain|safe-chain.exe) ;;
*) return 1 ;;
esac
case "$resolved_path" in
*.js|*.cjs|*.mjs|*.cmd|*.ps1) return 1 ;;
esac
binary_dir=$(dirname "$resolved_path")
if [ "$(basename "$binary_dir")" != "bin" ]; then
return 1
fi
dirname "$binary_dir"
}
get_install_dir() {
if command_exists safe-chain; then
install_dir=$(safe-chain get-install-dir 2>/dev/null || true)
if [ -n "$install_dir" ]; then
printf '%s\n' "$install_dir"
return 0
fi
command_path=$(command -v safe-chain)
install_dir=$(derive_install_dir_from_binary "$command_path" || true)
if [ -n "$install_dir" ]; then
printf '%s\n' "$install_dir"
return 0
fi
fi
printf '%s\n' "${HOME}/.safe-chain"
}
# Check and uninstall npm global package if present
remove_npm_installation() {
if ! command_exists npm; then
@ -154,6 +210,7 @@ remove_nvm_installation() {
# Main uninstallation
main() {
DOT_SAFE_CHAIN=$(get_install_dir)
SAFE_CHAIN_LOCATION="$DOT_SAFE_CHAIN/bin/safe-chain"
if [ -x "$SAFE_CHAIN_LOCATION" ]; then