#1 from host-uk/feature/security-hardening
Security hardening for install scripts
This commit is contained in:
commit
89872c14a3
5 changed files with 802 additions and 88 deletions
|
|
@ -1,10 +1,34 @@
|
||||||
# Install the Core CLI (Windows)
|
# Install the Core CLI (Windows)
|
||||||
# Run: .\scripts\install-core.ps1
|
# Run: .\scripts\install-core.ps1
|
||||||
|
#
|
||||||
|
# REQUIREMENTS:
|
||||||
|
# - PowerShell 4.0 or later
|
||||||
|
# - Windows 10/11 or Windows Server 2016+
|
||||||
|
# - 100MB free disk space
|
||||||
|
#
|
||||||
|
# SECURITY CONTROLS:
|
||||||
|
# - Version pinning prevents supply chain attacks via tag manipulation
|
||||||
|
# - SHA256 hash verification ensures binary integrity
|
||||||
|
# - Path validation prevents LOCALAPPDATA redirection attacks
|
||||||
|
# - Symlink detection prevents directory traversal attacks
|
||||||
|
# - Restrictive ACLs on temp directories prevent local privilege escalation
|
||||||
|
# - GPG signature verification (when available) ensures code authenticity
|
||||||
|
#
|
||||||
|
# KNOWN LIMITATIONS:
|
||||||
|
# - Checksums are fetched from same origin as binaries (consider separate trust root)
|
||||||
|
# - No TLS certificate pinning (relies on system CA store)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$Repo = "Snider/Core"
|
# Check PowerShell version (4.0+ required for Get-FileHash and other features)
|
||||||
$InstallDir = "$env:LOCALAPPDATA\Programs\core"
|
if ($PSVersionTable.PSVersion.Major -lt 4) {
|
||||||
|
Write-Host "[ERROR] PowerShell 4.0 or later is required. Current version: $($PSVersionTable.PSVersion)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$Repo = "host-uk/core"
|
||||||
|
$Version = "v0.1.0" # Pinned version - update when releasing new versions
|
||||||
|
$MinDiskSpaceMB = 100 # Minimum required disk space in MB
|
||||||
|
|
||||||
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
|
function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Green }
|
||||||
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
|
function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
|
||||||
|
|
@ -14,54 +38,369 @@ function Test-Command($cmd) {
|
||||||
return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
|
return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Download pre-built binary
|
# Check available disk space
|
||||||
function Download-Binary {
|
function Test-DiskSpace {
|
||||||
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
|
param([string]$Path)
|
||||||
$url = "https://github.com/$Repo/releases/latest/download/core-windows-$arch.exe"
|
|
||||||
|
|
||||||
Write-Info "Attempting to download pre-built binary..."
|
|
||||||
Write-Info "URL: $url"
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
# Get the drive from the path
|
||||||
Invoke-WebRequest -Uri $url -OutFile "$InstallDir\core.exe" -UseBasicParsing
|
$drive = [System.IO.Path]::GetPathRoot($Path)
|
||||||
Write-Info "Downloaded to $InstallDir\core.exe"
|
if ([string]::IsNullOrEmpty($drive)) {
|
||||||
|
$drive = [System.IO.Path]::GetPathRoot($env:LOCALAPPDATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
$driveInfo = Get-PSDrive -Name $drive.Substring(0, 1) -ErrorAction Stop
|
||||||
|
$freeSpaceMB = [math]::Round($driveInfo.Free / 1MB, 2)
|
||||||
|
|
||||||
|
if ($freeSpaceMB -lt $MinDiskSpaceMB) {
|
||||||
|
Write-Err "Insufficient disk space. Need at least ${MinDiskSpaceMB}MB, but only ${freeSpaceMB}MB available on drive $drive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Disk space check passed (${freeSpaceMB}MB available)"
|
||||||
return $true
|
return $true
|
||||||
} catch {
|
} catch {
|
||||||
Write-Warn "No pre-built binary available, will build from source"
|
Write-Warn "Could not verify disk space: $($_.Exception.Message)"
|
||||||
return $false
|
return $true # Continue anyway if check fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build from source
|
# Validate and get secure install directory
|
||||||
|
function Get-SecureInstallDir {
|
||||||
|
# Validate LOCALAPPDATA is within user profile
|
||||||
|
$localAppData = $env:LOCALAPPDATA
|
||||||
|
$userProfile = $env:USERPROFILE
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($localAppData)) {
|
||||||
|
Write-Err "LOCALAPPDATA environment variable is not set"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($userProfile)) {
|
||||||
|
Write-Err "USERPROFILE environment variable is not set"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve to absolute paths
|
||||||
|
$resolvedLocalAppData = [System.IO.Path]::GetFullPath($localAppData)
|
||||||
|
$resolvedUserProfile = [System.IO.Path]::GetFullPath($userProfile)
|
||||||
|
|
||||||
|
# Ensure LOCALAPPDATA is under user profile (prevent redirection attacks)
|
||||||
|
if (-not $resolvedLocalAppData.StartsWith($resolvedUserProfile, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
Write-Err "LOCALAPPDATA ($resolvedLocalAppData) is not within user profile ($resolvedUserProfile). Possible path manipulation detected."
|
||||||
|
}
|
||||||
|
|
||||||
|
$installDir = Join-Path $resolvedLocalAppData "Programs\core"
|
||||||
|
|
||||||
|
# Validate the resolved path doesn't contain suspicious patterns
|
||||||
|
if ($installDir -match '\.\.' -or $installDir -match '[\x00-\x1f]') {
|
||||||
|
Write-Err "Invalid characters detected in install path"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$InstallDir = Get-SecureInstallDir
|
||||||
|
|
||||||
|
# Verify directory is safe for writing (not a symlink, correct permissions)
|
||||||
|
function Test-SecureDirectory {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path $Path)) {
|
||||||
|
return $true # Directory doesn't exist yet, will be created
|
||||||
|
}
|
||||||
|
|
||||||
|
# Primary check: use fsutil for reliable reparse point detection (matches batch script)
|
||||||
|
$fsutilResult = & fsutil reparsepoint query $Path 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Err "Directory '$Path' is a reparse point (symlink or junction). Possible symlink attack detected."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback: check .NET attributes
|
||||||
|
$dirInfo = Get-Item $Path -Force
|
||||||
|
if ($dirInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||||
|
Write-Err "Directory '$Path' is a symbolic link or junction. Possible symlink attack detected."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify SHA256 hash of downloaded file
|
||||||
|
function Test-FileHash {
|
||||||
|
param(
|
||||||
|
[string]$FilePath,
|
||||||
|
[string]$ExpectedHash
|
||||||
|
)
|
||||||
|
|
||||||
|
$actualHash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash
|
||||||
|
if ($actualHash -ne $ExpectedHash) {
|
||||||
|
Remove-Item -Path $FilePath -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Err "Hash verification failed! Expected: $ExpectedHash, Got: $actualHash. The downloaded file may be corrupted or tampered with."
|
||||||
|
}
|
||||||
|
Write-Info "Hash verification passed (SHA256: $($actualHash.Substring(0,16))...)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create directory with atomic check-and-create to minimize TOCTOU window
|
||||||
|
function New-SecureDirectory {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
# Check parent directory for symlinks first
|
||||||
|
$parent = Split-Path $Path -Parent
|
||||||
|
if ($parent -and (Test-Path $parent)) {
|
||||||
|
Test-SecureDirectory -Path $parent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create directory
|
||||||
|
if (-not (Test-Path $Path)) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Immediately verify it's not a symlink (minimize TOCTOU window)
|
||||||
|
Test-SecureDirectory -Path $Path
|
||||||
|
|
||||||
|
return $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set restrictive ACL on directory (owner-only access)
|
||||||
|
# This is REQUIRED for temp directories - failure is fatal
|
||||||
|
function Set-SecureDirectoryAcl {
|
||||||
|
param(
|
||||||
|
[string]$Path,
|
||||||
|
[switch]$Required
|
||||||
|
)
|
||||||
|
|
||||||
|
$maxRetries = 3
|
||||||
|
$retryCount = 0
|
||||||
|
|
||||||
|
while ($retryCount -lt $maxRetries) {
|
||||||
|
try {
|
||||||
|
$acl = Get-Acl $Path
|
||||||
|
|
||||||
|
# Disable inheritance and remove inherited rules
|
||||||
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
|
||||||
|
# Clear existing rules
|
||||||
|
$acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null
|
||||||
|
|
||||||
|
# Add full control for current user only
|
||||||
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
$currentUser,
|
||||||
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||||
|
[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit,
|
||||||
|
[System.Security.AccessControl.PropagationFlags]::None,
|
||||||
|
[System.Security.AccessControl.AccessControlType]::Allow
|
||||||
|
)
|
||||||
|
$acl.AddAccessRule($rule)
|
||||||
|
|
||||||
|
Set-Acl -Path $Path -AclObject $acl
|
||||||
|
Write-Info "Set restrictive permissions on $Path"
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
$retryCount++
|
||||||
|
if ($retryCount -ge $maxRetries) {
|
||||||
|
if ($Required) {
|
||||||
|
Write-Err "SECURITY: Failed to set restrictive ACL on '$Path' after $maxRetries attempts: $($_.Exception.Message)"
|
||||||
|
} else {
|
||||||
|
Write-Warn "Could not set restrictive ACL on '$Path': $($_.Exception.Message)"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download pre-built binary with integrity verification
|
||||||
|
function Download-Binary {
|
||||||
|
$arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "386" }
|
||||||
|
$binaryUrl = "https://github.com/$Repo/releases/download/$Version/core-windows-$arch.exe"
|
||||||
|
$checksumUrl = "https://github.com/$Repo/releases/download/$Version/checksums.txt"
|
||||||
|
|
||||||
|
Write-Info "Attempting to download pre-built binary (version $Version)..."
|
||||||
|
Write-Info "URL: $binaryUrl"
|
||||||
|
|
||||||
|
# Track temp file for cleanup
|
||||||
|
$tempExe = $null
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Create and verify install directory
|
||||||
|
New-SecureDirectory -Path $InstallDir
|
||||||
|
|
||||||
|
# Use a temp file in the same directory (same filesystem for atomic move)
|
||||||
|
$tempExe = Join-Path $InstallDir "core.exe.tmp.$([System.Guid]::NewGuid().ToString('N').Substring(0,8))"
|
||||||
|
|
||||||
|
# Download the binary to temp location
|
||||||
|
Invoke-WebRequest -Uri $binaryUrl -OutFile $tempExe -UseBasicParsing
|
||||||
|
|
||||||
|
# Download and parse checksums
|
||||||
|
Write-Info "Verifying download integrity..."
|
||||||
|
$checksumContent = Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing | Select-Object -ExpandProperty Content
|
||||||
|
|
||||||
|
# Parse checksum file (format: "hash filename")
|
||||||
|
$expectedHash = $null
|
||||||
|
$checksumLines = $checksumContent -split "`n"
|
||||||
|
foreach ($line in $checksumLines) {
|
||||||
|
if ($line -match "^([a-fA-F0-9]{64})\s+.*core-windows-$arch\.exe") {
|
||||||
|
$expectedHash = $matches[1].ToUpper()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($expectedHash)) {
|
||||||
|
Write-Err "Could not find checksum for core-windows-$arch.exe in checksums.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify hash BEFORE any move operation
|
||||||
|
Test-FileHash -FilePath $tempExe -ExpectedHash $expectedHash
|
||||||
|
|
||||||
|
# Re-verify directory hasn't been replaced with symlink (reduce TOCTOU window)
|
||||||
|
Test-SecureDirectory -Path $InstallDir
|
||||||
|
|
||||||
|
# Atomic move to final location (same filesystem)
|
||||||
|
$finalPath = Join-Path $InstallDir "core.exe"
|
||||||
|
Move-Item -Path $tempExe -Destination $finalPath -Force
|
||||||
|
$tempExe = $null # Clear so finally block doesn't try to delete
|
||||||
|
|
||||||
|
Write-Info "Downloaded and verified: $finalPath"
|
||||||
|
return $true
|
||||||
|
} catch [System.Net.WebException] {
|
||||||
|
Write-Warn "Network error during download: $($_.Exception.Message)"
|
||||||
|
Write-Warn "Will attempt to build from source"
|
||||||
|
return $false
|
||||||
|
} catch [System.IO.IOException] {
|
||||||
|
Write-Warn "File system error: $($_.Exception.Message)"
|
||||||
|
Write-Warn "Will attempt to build from source"
|
||||||
|
return $false
|
||||||
|
} catch {
|
||||||
|
Write-Warn "Download failed: $($_.Exception.Message)"
|
||||||
|
Write-Warn "Will attempt to build from source"
|
||||||
|
return $false
|
||||||
|
} finally {
|
||||||
|
# Clean up temp file if it exists (handles Ctrl+C and other interruptions)
|
||||||
|
if ($tempExe -and (Test-Path $tempExe -ErrorAction SilentlyContinue)) {
|
||||||
|
Remove-Item -Path $tempExe -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify GPG signature on git tag (if gpg is available)
|
||||||
|
function Test-GitTagSignature {
|
||||||
|
param(
|
||||||
|
[string]$RepoPath,
|
||||||
|
[string]$Tag
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Command gpg)) {
|
||||||
|
Write-Warn "GPG not available - skipping tag signature verification"
|
||||||
|
Write-Warn "For enhanced security, install GPG and import the project signing key"
|
||||||
|
return $true # Continue without verification
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $RepoPath
|
||||||
|
try {
|
||||||
|
# Attempt to verify the tag signature
|
||||||
|
$result = git tag -v $Tag 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "GPG signature verified for tag $Tag"
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
# Check if tag is unsigned vs signature invalid
|
||||||
|
if ($result -match "error: no signature found") {
|
||||||
|
Write-Warn "Tag $Tag is not signed - continuing without signature verification"
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-Err "GPG signature verification FAILED for tag $Tag. Possible tampering detected."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build from source with security checks
|
||||||
function Build-FromSource {
|
function Build-FromSource {
|
||||||
|
if (-not (Test-Command git)) {
|
||||||
|
Write-Err "Git is required to build from source. Run '.\scripts\install-deps.ps1' first"
|
||||||
|
}
|
||||||
if (-not (Test-Command go)) {
|
if (-not (Test-Command go)) {
|
||||||
Write-Err "Go is required to build from source. Run '.\scripts\install-deps.ps1' first"
|
Write-Err "Go is required to build from source. Run '.\scripts\install-deps.ps1' first"
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpdir = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ }
|
# Create secure temp directory with restrictive ACL
|
||||||
|
$tmpdir = Join-Path ([System.IO.Path]::GetTempPath()) "core-build-$([System.Guid]::NewGuid().ToString('N'))"
|
||||||
|
New-SecureDirectory -Path $tmpdir
|
||||||
|
|
||||||
Write-Info "Cloning $Repo..."
|
# ACL is REQUIRED for temp build directories (security critical)
|
||||||
git clone --depth 1 "https://github.com/$Repo.git" "$tmpdir\Core"
|
Set-SecureDirectoryAcl -Path $tmpdir -Required
|
||||||
|
|
||||||
Write-Info "Building core CLI..."
|
try {
|
||||||
Push-Location "$tmpdir\Core"
|
Write-Info "Cloning $Repo (version $Version)..."
|
||||||
go build -o core.exe ./cmd/core
|
$cloneDir = Join-Path $tmpdir "Core"
|
||||||
Pop-Location
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
# Clone specific tag for reproducibility
|
||||||
Move-Item "$tmpdir\Core\core.exe" "$InstallDir\core.exe" -Force
|
git clone --depth 1 --branch $Version "https://github.com/$Repo.git" $cloneDir
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Err "Failed to clone repository at version $Version"
|
||||||
|
}
|
||||||
|
|
||||||
Remove-Item -Recurse -Force $tmpdir
|
# Verify GPG signature on tag (if available)
|
||||||
Write-Info "Built and installed to $InstallDir\core.exe"
|
Test-GitTagSignature -RepoPath $cloneDir -Tag $Version
|
||||||
|
|
||||||
|
Write-Info "Building core CLI..."
|
||||||
|
Push-Location $cloneDir
|
||||||
|
try {
|
||||||
|
go build -o core.exe .
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Err "Go build failed"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create and verify install directory
|
||||||
|
New-SecureDirectory -Path $InstallDir
|
||||||
|
|
||||||
|
# Move built binary to install location
|
||||||
|
Move-Item (Join-Path $cloneDir "core.exe") (Join-Path $InstallDir "core.exe") -Force
|
||||||
|
|
||||||
|
Write-Info "Built and installed to $InstallDir\core.exe"
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tmpdir) {
|
||||||
|
Remove-Item -Recurse -Force $tmpdir -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add to PATH
|
# Validate and add to PATH with precise matching
|
||||||
function Setup-Path {
|
function Setup-Path {
|
||||||
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
|
$userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
|
||||||
if ($userPath -notlike "*$InstallDir*") {
|
|
||||||
|
# Validate InstallDir is a real directory under user profile
|
||||||
|
if (-not (Test-Path $InstallDir -PathType Container)) {
|
||||||
|
Write-Warn "Install directory does not exist, skipping PATH setup"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedInstallDir = [System.IO.Path]::GetFullPath($InstallDir)
|
||||||
|
$resolvedUserProfile = [System.IO.Path]::GetFullPath($env:USERPROFILE)
|
||||||
|
|
||||||
|
# Ensure we're only adding paths under user profile
|
||||||
|
if (-not $resolvedInstallDir.StartsWith($resolvedUserProfile, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
Write-Warn "Install directory is outside user profile, skipping PATH modification for security"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use precise PATH entry matching (not substring)
|
||||||
|
$pathEntries = $userPath -split ';' | ForEach-Object { $_.TrimEnd('\') }
|
||||||
|
$normalizedInstallDir = $resolvedInstallDir.TrimEnd('\')
|
||||||
|
|
||||||
|
$alreadyInPath = $pathEntries | Where-Object {
|
||||||
|
$_.Equals($normalizedInstallDir, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $alreadyInPath) {
|
||||||
Write-Info "Adding $InstallDir to PATH..."
|
Write-Info "Adding $InstallDir to PATH..."
|
||||||
[Environment]::SetEnvironmentVariable("PATH", "$userPath;$InstallDir", "User")
|
# Trim trailing semicolons to prevent duplicates
|
||||||
|
$cleanPath = $userPath.TrimEnd(';')
|
||||||
|
[Environment]::SetEnvironmentVariable("PATH", "$cleanPath;$InstallDir", "User")
|
||||||
$env:PATH = "$env:PATH;$InstallDir"
|
$env:PATH = "$env:PATH;$InstallDir"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +424,10 @@ function Verify {
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
function Main {
|
function Main {
|
||||||
Write-Info "Installing Core CLI..."
|
Write-Info "Installing Core CLI (version $Version)..."
|
||||||
|
|
||||||
|
# Check disk space before starting
|
||||||
|
Test-DiskSpace -Path $InstallDir
|
||||||
|
|
||||||
# Try download first, fallback to build
|
# Try download first, fallback to build
|
||||||
if (-not (Download-Binary)) {
|
if (-not (Download-Binary)) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,27 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Install the Core CLI
|
# Install the Core CLI (Unix/Linux/macOS)
|
||||||
# Either downloads a pre-built binary or builds from source
|
# Run: ./scripts/install-core.sh
|
||||||
|
#
|
||||||
|
# REQUIREMENTS:
|
||||||
|
# - curl or wget
|
||||||
|
# - sha256sum (Linux) or shasum (macOS)
|
||||||
|
# - git and go (for building from source)
|
||||||
|
#
|
||||||
|
# SECURITY CONTROLS:
|
||||||
|
# - Version pinning prevents supply chain attacks via tag manipulation
|
||||||
|
# - SHA256 hash verification ensures binary integrity
|
||||||
|
# - Secure temp file creation prevents symlink attacks
|
||||||
|
# - Symlink detection prevents directory traversal attacks
|
||||||
|
# - GPG signature verification (when available) ensures code authenticity
|
||||||
|
#
|
||||||
|
# KNOWN LIMITATIONS:
|
||||||
|
# - Checksums are fetched from same origin as binaries (consider separate trust root)
|
||||||
|
# - No TLS certificate pinning (relies on system CA store)
|
||||||
|
|
||||||
REPO="Snider/Core"
|
REPO="host-uk/core"
|
||||||
|
VERSION="v0.1.0" # Pinned version - update when releasing new versions
|
||||||
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
||||||
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-auto}"
|
BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-auto}"
|
||||||
|
|
||||||
|
|
@ -38,49 +55,196 @@ detect_os() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to download pre-built binary
|
# Compute SHA256 hash (cross-platform)
|
||||||
download_binary() {
|
compute_sha256() {
|
||||||
local os=$(detect_os)
|
local file=$1
|
||||||
local arch=$(detect_arch)
|
if has sha256sum; then
|
||||||
local url="https://github.com/$REPO/releases/latest/download/core-${os}-${arch}"
|
sha256sum "$file" | cut -d' ' -f1
|
||||||
|
elif has shasum; then
|
||||||
if [[ "$os" == "windows" ]]; then
|
shasum -a 256 "$file" | cut -d' ' -f1
|
||||||
url="${url}.exe"
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Attempting to download pre-built binary..."
|
|
||||||
info "URL: $url"
|
|
||||||
|
|
||||||
if curl -fsSL -o /tmp/core "$url" 2>/dev/null; then
|
|
||||||
chmod +x /tmp/core
|
|
||||||
mkdir -p "$INSTALL_DIR"
|
|
||||||
mv /tmp/core "$INSTALL_DIR/core"
|
|
||||||
info "Downloaded to $INSTALL_DIR/core"
|
|
||||||
return 0
|
|
||||||
else
|
else
|
||||||
warn "No pre-built binary available, will build from source"
|
error "No SHA256 tool available (need sha256sum or shasum)"
|
||||||
return 1
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build from source
|
# Verify SHA256 hash of downloaded file
|
||||||
|
verify_hash() {
|
||||||
|
local file=$1
|
||||||
|
local expected_hash=$2
|
||||||
|
local actual_hash
|
||||||
|
|
||||||
|
actual_hash=$(compute_sha256 "$file")
|
||||||
|
|
||||||
|
if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then
|
||||||
|
rm -f "$file"
|
||||||
|
error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash. The downloaded file may be corrupted or tampered with."
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Hash verification passed (SHA256: ${actual_hash:0:16}...)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if directory is a symlink (security check)
|
||||||
|
check_not_symlink() {
|
||||||
|
local path=$1
|
||||||
|
|
||||||
|
if [[ -L "$path" ]]; then
|
||||||
|
error "Directory '$path' is a symbolic link. Possible symlink attack detected."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Additional check using file type
|
||||||
|
if [[ -e "$path" ]] && [[ ! -d "$path" ]]; then
|
||||||
|
error "Path '$path' exists but is not a directory."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to download pre-built binary with integrity verification
|
||||||
|
download_binary() {
|
||||||
|
local os arch binary_name binary_url checksum_url
|
||||||
|
os=$(detect_os)
|
||||||
|
arch=$(detect_arch)
|
||||||
|
binary_name="core-${os}-${arch}"
|
||||||
|
binary_url="https://github.com/$REPO/releases/download/$VERSION/${binary_name}"
|
||||||
|
checksum_url="https://github.com/$REPO/releases/download/$VERSION/checksums.txt"
|
||||||
|
|
||||||
|
if [[ "$os" == "windows" ]]; then
|
||||||
|
binary_name="${binary_name}.exe"
|
||||||
|
binary_url="${binary_url}.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Attempting to download pre-built binary (version $VERSION)..."
|
||||||
|
info "URL: $binary_url"
|
||||||
|
|
||||||
|
# Use secure temp file (prevents symlink attacks)
|
||||||
|
local temp_file
|
||||||
|
temp_file=$(mktemp "${TMPDIR:-/tmp}/core.XXXXXXXXXX") || error "Failed to create temp file"
|
||||||
|
|
||||||
|
# Ensure cleanup on exit/interrupt
|
||||||
|
trap "rm -f '$temp_file'" EXIT INT TERM
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
if ! curl -fsSL -o "$temp_file" "$binary_url" 2>/dev/null; then
|
||||||
|
rm -f "$temp_file"
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
warn "No pre-built binary available, will build from source"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download and verify checksum
|
||||||
|
info "Verifying download integrity..."
|
||||||
|
local checksums
|
||||||
|
checksums=$(curl -fsSL "$checksum_url" 2>/dev/null) || {
|
||||||
|
rm -f "$temp_file"
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
warn "Could not download checksums, will build from source"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse checksum file (format: "hash filename")
|
||||||
|
local expected_hash
|
||||||
|
expected_hash=$(echo "$checksums" | grep -E "^[a-fA-F0-9]{64}\s+.*${binary_name}$" | head -1 | cut -d' ' -f1)
|
||||||
|
|
||||||
|
if [[ -z "$expected_hash" ]]; then
|
||||||
|
rm -f "$temp_file"
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
error "Could not find checksum for $binary_name in checksums.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify hash BEFORE any move operation
|
||||||
|
verify_hash "$temp_file" "$expected_hash"
|
||||||
|
|
||||||
|
# Verify install directory is safe
|
||||||
|
check_not_symlink "$INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir
|
||||||
|
|
||||||
|
# Make executable and move to final location
|
||||||
|
chmod +x "$temp_file"
|
||||||
|
mv "$temp_file" "$INSTALL_DIR/core"
|
||||||
|
|
||||||
|
trap - EXIT INT TERM # Clear trap since file was moved
|
||||||
|
|
||||||
|
info "Downloaded and verified: $INSTALL_DIR/core"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify GPG signature on git tag (if gpg is available)
|
||||||
|
verify_git_tag_signature() {
|
||||||
|
local repo_path=$1
|
||||||
|
local tag=$2
|
||||||
|
|
||||||
|
if ! has gpg; then
|
||||||
|
warn "GPG not available - skipping tag signature verification"
|
||||||
|
warn "For enhanced security, install GPG and import the project signing key"
|
||||||
|
return 0 # Continue without verification
|
||||||
|
fi
|
||||||
|
|
||||||
|
pushd "$repo_path" > /dev/null
|
||||||
|
local result
|
||||||
|
result=$(git tag -v "$tag" 2>&1) || {
|
||||||
|
# Check if tag is unsigned vs signature invalid
|
||||||
|
if echo "$result" | grep -q "error: no signature found"; then
|
||||||
|
warn "Tag $tag is not signed - continuing without signature verification"
|
||||||
|
popd > /dev/null
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
popd > /dev/null
|
||||||
|
error "GPG signature verification FAILED for tag $tag. Possible tampering detected."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
info "GPG signature verified for tag $tag"
|
||||||
|
popd > /dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build from source with security checks
|
||||||
build_from_source() {
|
build_from_source() {
|
||||||
|
if ! has git; then
|
||||||
|
error "Git is required to build from source. Run './scripts/install-deps.sh' first"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! has go; then
|
if ! has go; then
|
||||||
error "Go is required to build from source. Run './scripts/install-deps.sh' first"
|
error "Go is required to build from source. Run './scripts/install-deps.sh' first"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local tmpdir=$(mktemp -d)
|
# Create secure temp directory
|
||||||
info "Cloning $REPO..."
|
local tmpdir
|
||||||
git clone --depth 1 "https://github.com/$REPO.git" "$tmpdir/Core"
|
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/core-build.XXXXXXXXXX") || error "Failed to create temp directory"
|
||||||
|
|
||||||
|
# Set restrictive permissions on temp directory
|
||||||
|
chmod 700 "$tmpdir"
|
||||||
|
|
||||||
|
# Ensure cleanup on exit/interrupt
|
||||||
|
trap "rm -rf '$tmpdir'" EXIT INT TERM
|
||||||
|
|
||||||
|
info "Cloning $REPO (version $VERSION)..."
|
||||||
|
local clone_dir="$tmpdir/Core"
|
||||||
|
|
||||||
|
# Clone specific tag for reproducibility
|
||||||
|
if ! git clone --depth 1 --branch "$VERSION" "https://github.com/$REPO.git" "$clone_dir"; then
|
||||||
|
error "Failed to clone repository at version $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify GPG signature on tag (if available)
|
||||||
|
verify_git_tag_signature "$clone_dir" "$VERSION"
|
||||||
|
|
||||||
info "Building core CLI..."
|
info "Building core CLI..."
|
||||||
cd "$tmpdir/Core"
|
pushd "$clone_dir" > /dev/null
|
||||||
go build -o core ./cmd/core
|
if ! go build -o core ./cmd/core; then
|
||||||
|
popd > /dev/null
|
||||||
|
error "Go build failed"
|
||||||
|
fi
|
||||||
|
popd > /dev/null
|
||||||
|
|
||||||
|
# Verify install directory is safe
|
||||||
|
check_not_symlink "$INSTALL_DIR"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
mv core "$INSTALL_DIR/core"
|
check_not_symlink "$INSTALL_DIR" # Re-verify after mkdir
|
||||||
|
|
||||||
|
# Move built binary to install location
|
||||||
|
mv "$clone_dir/core" "$INSTALL_DIR/core"
|
||||||
|
|
||||||
|
trap - EXIT INT TERM # Clear trap
|
||||||
rm -rf "$tmpdir"
|
rm -rf "$tmpdir"
|
||||||
|
|
||||||
info "Built and installed to $INSTALL_DIR/core"
|
info "Built and installed to $INSTALL_DIR/core"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,9 +282,12 @@ verify() {
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
info "Installing Core CLI..."
|
info "Installing Core CLI (version $VERSION)..."
|
||||||
|
|
||||||
|
# Verify install directory is safe before starting
|
||||||
|
check_not_symlink "$INSTALL_DIR"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
check_not_symlink "$INSTALL_DIR"
|
||||||
|
|
||||||
# Try download first, fallback to build
|
# Try download first, fallback to build
|
||||||
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
# Install system dependencies for Host UK development (Windows)
|
# Install system dependencies for Host UK development (Windows)
|
||||||
# Run: .\scripts\install-deps.ps1
|
# Run: .\scripts\install-deps.ps1
|
||||||
|
#
|
||||||
|
# SECURITY NOTES:
|
||||||
|
# - Chocolatey installer is downloaded to temp file before execution
|
||||||
|
# - HTTPS is enforced for all downloads
|
||||||
|
# - For high-security environments, consider auditing install scripts
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
|
@ -12,6 +17,8 @@ function Test-Command($cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install Chocolatey if not present
|
# Install Chocolatey if not present
|
||||||
|
# NOTE: Chocolatey's install script changes frequently, making checksum verification impractical.
|
||||||
|
# The script is fetched over HTTPS. For high-security environments, audit the script first.
|
||||||
function Install-Chocolatey {
|
function Install-Chocolatey {
|
||||||
if (Test-Command choco) {
|
if (Test-Command choco) {
|
||||||
Write-Info "Chocolatey already installed"
|
Write-Info "Chocolatey already installed"
|
||||||
|
|
@ -19,9 +26,29 @@ function Install-Chocolatey {
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Info "Installing Chocolatey..."
|
Write-Info "Installing Chocolatey..."
|
||||||
|
Write-Warn "This downloads and executes a script from chocolatey.org. Review at: https://community.chocolatey.org/install.ps1"
|
||||||
|
|
||||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
|
||||||
|
# Download to temp file first (allows manual inspection if needed, avoids Invoke-Expression with direct download)
|
||||||
|
$tempScript = Join-Path ([System.IO.Path]::GetTempPath()) "choco-install.$([System.Guid]::NewGuid().ToString('N').Substring(0,8)).ps1"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Info "Downloading Chocolatey installer..."
|
||||||
|
Invoke-WebRequest -Uri 'https://community.chocolatey.org/install.ps1' -OutFile $tempScript -UseBasicParsing
|
||||||
|
|
||||||
|
Write-Info "Executing Chocolatey installer..."
|
||||||
|
& $tempScript
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Err "Chocolatey installation failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
# Clean up temp file
|
||||||
|
if (Test-Path $tempScript) {
|
||||||
|
Remove-Item -Path $tempScript -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Refresh PATH
|
# Refresh PATH
|
||||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
|
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "User")
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,55 @@ set -e
|
||||||
|
|
||||||
# Install system dependencies for Host UK development
|
# Install system dependencies for Host UK development
|
||||||
# Supports: macOS (brew), Linux (apt/dnf), Windows (choco via WSL)
|
# Supports: macOS (brew), Linux (apt/dnf), Windows (choco via WSL)
|
||||||
|
#
|
||||||
|
# SECURITY NOTES:
|
||||||
|
# - External install scripts (Homebrew, NodeSource) are downloaded over HTTPS
|
||||||
|
# - Go binary is verified via SHA256 checksum
|
||||||
|
# - Composer installer is verified via SHA256 checksum
|
||||||
|
# - Consider auditing external scripts before running in sensitive environments
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Pinned versions and checksums for security
|
||||||
|
GO_VERSION="1.22.0"
|
||||||
|
GO_AMD64_SHA256="f6c8a87aa03b92c4b0bf3d558e28ea03006eb29db78917daec5cfb6ec1046265"
|
||||||
|
COMPOSER_EXPECTED_SIG="dac665fdc30fdd8ec78b38b9800061b4150413ff2e3b6f88543c636f7cd84f6db9189d43a81e5503cda447da73c7e5b6"
|
||||||
|
|
||||||
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
||||||
|
|
||||||
|
# Compute SHA256 hash (cross-platform)
|
||||||
|
compute_sha256() {
|
||||||
|
local file=$1
|
||||||
|
if command -v sha256sum &> /dev/null; then
|
||||||
|
sha256sum "$file" | cut -d' ' -f1
|
||||||
|
elif command -v shasum &> /dev/null; then
|
||||||
|
shasum -a 256 "$file" | cut -d' ' -f1
|
||||||
|
else
|
||||||
|
error "No SHA256 tool available (need sha256sum or shasum)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify SHA256 hash of downloaded file
|
||||||
|
verify_hash() {
|
||||||
|
local file=$1
|
||||||
|
local expected_hash=$2
|
||||||
|
local actual_hash
|
||||||
|
|
||||||
|
actual_hash=$(compute_sha256 "$file")
|
||||||
|
|
||||||
|
if [[ "${actual_hash,,}" != "${expected_hash,,}" ]]; then
|
||||||
|
rm -f "$file"
|
||||||
|
error "Hash verification failed! Expected: $expected_hash, Got: $actual_hash"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Hash verification passed"
|
||||||
|
}
|
||||||
|
|
||||||
# Detect OS
|
# Detect OS
|
||||||
detect_os() {
|
detect_os() {
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
@ -29,6 +68,9 @@ has() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install Homebrew (macOS/Linux)
|
# Install Homebrew (macOS/Linux)
|
||||||
|
# NOTE: Homebrew's install script changes frequently, making checksum verification impractical.
|
||||||
|
# The script is fetched over HTTPS from GitHub. For high-security environments,
|
||||||
|
# consider auditing the script manually before running.
|
||||||
install_brew() {
|
install_brew() {
|
||||||
if has brew; then
|
if has brew; then
|
||||||
info "Homebrew already installed"
|
info "Homebrew already installed"
|
||||||
|
|
@ -36,7 +78,21 @@ install_brew() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Installing Homebrew..."
|
info "Installing Homebrew..."
|
||||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
warn "This downloads and executes a script from GitHub. Review at: https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
|
||||||
|
|
||||||
|
# Download to temp file first (allows manual inspection if needed)
|
||||||
|
local temp_script
|
||||||
|
temp_script=$(mktemp "${TMPDIR:-/tmp}/brew-install.XXXXXXXXXX.sh")
|
||||||
|
curl -fsSL -o "$temp_script" https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh || {
|
||||||
|
rm -f "$temp_script"
|
||||||
|
error "Failed to download Homebrew installer"
|
||||||
|
}
|
||||||
|
|
||||||
|
/bin/bash "$temp_script" || {
|
||||||
|
rm -f "$temp_script"
|
||||||
|
error "Homebrew installation failed"
|
||||||
|
}
|
||||||
|
rm -f "$temp_script"
|
||||||
|
|
||||||
# Add to PATH for this session
|
# Add to PATH for this session
|
||||||
if [[ -f /opt/homebrew/bin/brew ]]; then
|
if [[ -f /opt/homebrew/bin/brew ]]; then
|
||||||
|
|
@ -115,11 +171,24 @@ setup_linux_apt() {
|
||||||
if ! has go; then
|
if ! has go; then
|
||||||
info "Installing Go..."
|
info "Installing Go..."
|
||||||
sudo apt-get install -y golang-go || {
|
sudo apt-get install -y golang-go || {
|
||||||
# Fallback to manual install for newer version
|
# Fallback to manual install for newer version with integrity verification
|
||||||
curl -LO https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
|
local go_tarball="go${GO_VERSION}.linux-amd64.tar.gz"
|
||||||
|
local go_url="https://go.dev/dl/${go_tarball}"
|
||||||
|
local temp_file
|
||||||
|
temp_file=$(mktemp "${TMPDIR:-/tmp}/go.XXXXXXXXXX.tar.gz")
|
||||||
|
|
||||||
|
info "Downloading Go $GO_VERSION..."
|
||||||
|
curl -fsSL -o "$temp_file" "$go_url" || {
|
||||||
|
rm -f "$temp_file"
|
||||||
|
error "Failed to download Go"
|
||||||
|
}
|
||||||
|
|
||||||
|
info "Verifying Go download integrity..."
|
||||||
|
verify_hash "$temp_file" "$GO_AMD64_SHA256"
|
||||||
|
|
||||||
sudo rm -rf /usr/local/go
|
sudo rm -rf /usr/local/go
|
||||||
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
|
sudo tar -C /usr/local -xzf "$temp_file"
|
||||||
rm go1.22.0.linux-amd64.tar.gz
|
rm -f "$temp_file"
|
||||||
export PATH=$PATH:/usr/local/go/bin
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
@ -127,17 +196,50 @@ setup_linux_apt() {
|
||||||
# PHP
|
# PHP
|
||||||
apt_install php
|
apt_install php
|
||||||
|
|
||||||
# Composer
|
# Composer (with installer signature verification)
|
||||||
if ! has composer; then
|
if ! has composer; then
|
||||||
info "Installing Composer..."
|
info "Installing Composer..."
|
||||||
curl -sS https://getcomposer.org/installer | php
|
local temp_dir
|
||||||
sudo mv composer.phar /usr/local/bin/composer
|
temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/composer.XXXXXXXXXX")
|
||||||
|
chmod 700 "$temp_dir"
|
||||||
|
|
||||||
|
# Download installer
|
||||||
|
curl -fsSL -o "$temp_dir/composer-setup.php" https://getcomposer.org/installer
|
||||||
|
|
||||||
|
# Verify installer signature (SHA384)
|
||||||
|
local actual_sig
|
||||||
|
actual_sig=$(php -r "echo hash_file('sha384', '$temp_dir/composer-setup.php');")
|
||||||
|
if [[ "$actual_sig" != "$COMPOSER_EXPECTED_SIG" ]]; then
|
||||||
|
rm -rf "$temp_dir"
|
||||||
|
error "Composer installer signature verification failed!"
|
||||||
|
fi
|
||||||
|
info "Composer installer signature verified"
|
||||||
|
|
||||||
|
# Run installer
|
||||||
|
php "$temp_dir/composer-setup.php" --install-dir="$temp_dir"
|
||||||
|
sudo mv "$temp_dir/composer.phar" /usr/local/bin/composer
|
||||||
|
rm -rf "$temp_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Node
|
# Node (via NodeSource)
|
||||||
|
# NOTE: NodeSource setup script changes frequently, making checksum verification impractical.
|
||||||
|
# For high-security environments, consider using nvm or building from source.
|
||||||
if ! has node; then
|
if ! has node; then
|
||||||
info "Installing Node.js..."
|
info "Installing Node.js..."
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
warn "This downloads and executes a script from NodeSource. Review at: https://deb.nodesource.com/setup_20.x"
|
||||||
|
|
||||||
|
local temp_script
|
||||||
|
temp_script=$(mktemp "${TMPDIR:-/tmp}/nodesource-setup.XXXXXXXXXX.sh")
|
||||||
|
curl -fsSL -o "$temp_script" https://deb.nodesource.com/setup_20.x || {
|
||||||
|
rm -f "$temp_script"
|
||||||
|
error "Failed to download NodeSource setup script"
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo -E bash "$temp_script" || {
|
||||||
|
rm -f "$temp_script"
|
||||||
|
error "NodeSource setup failed"
|
||||||
|
}
|
||||||
|
rm -f "$temp_script"
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
106
setup.bat
106
setup.bat
|
|
@ -1,51 +1,127 @@
|
||||||
@echo off
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
REM Quick setup script for Windows
|
REM Quick setup script for Windows
|
||||||
REM Run as Administrator: .\setup.bat
|
REM Run as Administrator: setup.bat
|
||||||
|
REM
|
||||||
|
REM SECURITY: This script validates environment before executing
|
||||||
|
REM to prevent path manipulation attacks.
|
||||||
|
|
||||||
echo === Host UK Developer Workspace Setup ===
|
echo === Host UK Developer Workspace Setup ===
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Check for admin rights
|
REM Check for admin rights
|
||||||
net session >nul 2>&1
|
net session >nul 2>&1
|
||||||
if %errorlevel% neq 0 (
|
if !errorlevel! neq 0 (
|
||||||
echo ERROR: Please run this script as Administrator
|
echo ERROR: Please run this script as Administrator
|
||||||
echo Right-click and select "Run as administrator"
|
echo Right-click and select "Run as administrator"
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM Install dependencies
|
REM === SECURITY: Validate LOCALAPPDATA ===
|
||||||
echo Installing dependencies...
|
REM Ensure LOCALAPPDATA is set and appears to be within user profile
|
||||||
powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-deps.ps1"
|
if "%LOCALAPPDATA%"=="" (
|
||||||
if %errorlevel% neq 0 goto :error
|
echo ERROR: LOCALAPPDATA environment variable is not set
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
REM Install core CLI
|
if "%USERPROFILE%"=="" (
|
||||||
|
echo ERROR: USERPROFILE environment variable is not set
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check that LOCALAPPDATA starts with USERPROFILE (basic validation)
|
||||||
|
REM This prevents redirection attacks where LOCALAPPDATA points elsewhere
|
||||||
|
echo !LOCALAPPDATA! | findstr /i /b /c:"!USERPROFILE!" >nul
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo ERROR: LOCALAPPDATA does not appear to be within user profile
|
||||||
|
echo LOCALAPPDATA: !LOCALAPPDATA!
|
||||||
|
echo USERPROFILE: !USERPROFILE!
|
||||||
|
echo This may indicate a path manipulation attack. Aborting.
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Validate paths don't contain suspicious characters that could enable injection
|
||||||
|
REM Blocks: < > | & ^ ` %% (shell metacharacters)
|
||||||
|
echo !LOCALAPPDATA! | findstr /r "[<>|&^`]" >nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
echo ERROR: LOCALAPPDATA contains invalid shell characters
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
REM Check for percent signs (both single and double)
|
||||||
|
set "TEMP_CHECK=!LOCALAPPDATA!"
|
||||||
|
set "TEMP_CHECK=!TEMP_CHECK:%%=!"
|
||||||
|
if not "!TEMP_CHECK!"=="!LOCALAPPDATA!" (
|
||||||
|
echo ERROR: LOCALAPPDATA contains percent signs
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM === Install dependencies ===
|
||||||
|
echo Installing dependencies...
|
||||||
|
call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-deps.ps1"
|
||||||
|
if !errorlevel! neq 0 goto :error
|
||||||
|
|
||||||
|
REM === Install core CLI ===
|
||||||
echo.
|
echo.
|
||||||
echo Installing core CLI...
|
echo Installing core CLI...
|
||||||
powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-core.ps1"
|
call powershell -ExecutionPolicy Bypass -File "%~dp0scripts\install-core.ps1"
|
||||||
if %errorlevel% neq 0 goto :error
|
if !errorlevel! neq 0 goto :error
|
||||||
|
|
||||||
REM Refresh PATH
|
REM === Validate install path before use ===
|
||||||
set "PATH=%PATH%;%LOCALAPPDATA%\Programs\core"
|
set "CORE_PATH=!LOCALAPPDATA!\Programs\core"
|
||||||
|
|
||||||
REM Run doctor
|
REM Verify the path exists and is a directory (not a symlink to elsewhere)
|
||||||
|
if not exist "!CORE_PATH!\core.exe" (
|
||||||
|
echo ERROR: core.exe not found at !CORE_PATH!\core.exe
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if it's a symlink/junction using fsutil (more reliable than attributes)
|
||||||
|
fsutil reparsepoint query "!CORE_PATH!" >nul 2>&1
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
echo ERROR: Install directory is a reparse point (symlink or junction^)
|
||||||
|
echo This may indicate a symlink attack. Aborting.
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Fallback: also check attributes for symlink indicator
|
||||||
|
for %%F in ("!CORE_PATH!") do (
|
||||||
|
set "ATTRS=%%~aF"
|
||||||
|
)
|
||||||
|
echo !ATTRS! | findstr /c:"l" >nul
|
||||||
|
if !errorlevel! equ 0 (
|
||||||
|
echo ERROR: Install directory appears to be a symbolic link
|
||||||
|
echo This may indicate a symlink attack. Aborting.
|
||||||
|
goto :error
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Refresh PATH for this session
|
||||||
|
set "PATH=%PATH%;!CORE_PATH!"
|
||||||
|
|
||||||
|
REM === Run doctor ===
|
||||||
echo.
|
echo.
|
||||||
echo === Verifying environment ===
|
echo === Verifying environment ===
|
||||||
core doctor
|
call "!CORE_PATH!\core.exe" doctor
|
||||||
|
if !errorlevel! neq 0 (
|
||||||
|
echo WARNING: core doctor reported issues
|
||||||
|
)
|
||||||
|
|
||||||
REM Clone repos
|
REM === Clone repos ===
|
||||||
echo.
|
echo.
|
||||||
echo === Cloning repositories ===
|
echo === Cloning repositories ===
|
||||||
core setup
|
call "!CORE_PATH!\core.exe" setup
|
||||||
|
if !errorlevel! neq 0 goto :error
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo === Setup complete! ===
|
echo === Setup complete! ===
|
||||||
echo Run 'core health' to check status
|
echo Run 'core health' to check status
|
||||||
pause
|
pause
|
||||||
|
endlocal
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:error
|
:error
|
||||||
echo.
|
echo.
|
||||||
echo Setup failed! Check the error above.
|
echo Setup failed! Check the error above.
|
||||||
pause
|
pause
|
||||||
|
endlocal
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue