<# .SYNOPSIS Pre-flight check (and optional dependency installer) for the MonitorSpider Windows backup kit (windows-sqlserver.ps1). .DESCRIPTION Default run is READ-ONLY: it verifies everything backup night depends on and prints a pass/fail report - - PowerShell version and execution policy - the tools: 7-Zip (7za), WinSCP .NET assembly, sqlcmd (if SQL is configured) - backup-config.json exists, parses, and has no placeholder password - sqlcmd can actually connect to the configured instance (the ODBC 18 certificate-trust quirk surfaces here, not at 3am) - the upload host answers on the right port - the MonitorSpider ping URL works (sends a harmless status=start ping, so you also see the monitor light up in the dashboard) With -Install it downloads PORTABLE copies of the tools into .\tools - no system-wide installs, no admin rights, no package manager: - 7-Zip standalone console (7za.exe, via the official Extra package) - WinSCP .NET assembly + WinSCP.exe (official Automation package) - Microsoft go-sqlcmd (single-binary sqlcmd), only if SQL is configured Every download is verified against a SHA-256 hash pinned in this script, so you can read below exactly what will be fetched and check the hashes yourself. A hash mismatch or 404 aborts - get the current kit (with fresh pins) from https://monitorspider.com/backup-kits/ instead of bypassing it. .NOTES From https://monitorspider.com/backup-kits/ - MIT licensed. Prefer your own package management? `choco install winscp 7zip` etc. works too; this script's check mode will find tools in Program Files and PATH. #> [CmdletBinding()] param( [switch]$Install, [switch]$Force, # with -Install: re-download tools that are already present [string]$ConfigPath = (Join-Path $PSScriptRoot 'backup-config.json') ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # PS 5.1 defaults to TLS 1.0 for web requests; the vendor sites need 1.2+. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $toolsDir = Join-Path $PSScriptRoot 'tools' # ---- Pinned downloads (versions + SHA-256, verified 2026-07-12) ------------- # 7zr.exe is the tiny self-contained 7z extractor used only to unpack the # Extra package (which is itself a .7z). All binaries are the vendors' own, # UNMODIFIED releases, mirrored on monitorspider.com because some vendor URLs # serve different bytes to different clients (mirror networks / user-agent # interstitials - winscp.net does this), which breaks hash verification. # Verify against the originals yourself: the vendor source of each file is in # the OriginalUrl comment and in tools/TOOLS-NOTICE.txt next to the mirrors. $pins = @( @{ Name = '7zr.exe (bootstrap extractor)' Url = 'https://monitorspider.com/backup-kits/tools/7zr.exe' # Original: https://www.7-zip.org/a/7zr.exe Sha256 = '56B8CC9F4971CEF253644FAFE54063ED7FDCA551D4DEE0F8C6BAA81B855ACD72' File = '7zr.exe' }, @{ Name = '7-Zip 26.02 Extra (7za standalone console)' Url = 'https://monitorspider.com/backup-kits/tools/7z2602-extra.7z' # Original: https://www.7-zip.org/a/7z2602-extra.7z Sha256 = '081DF9E9311DFD9C9E0E98C1C80180B99BB51E4CB24156B5F3057FE3C259D70A' File = '7z-extra.7z' }, @{ Name = 'WinSCP 6.5.4 Automation (.NET assembly)' Url = 'https://monitorspider.com/backup-kits/tools/WinSCP-6.5.4-Automation.zip' # Original: https://winscp.net/download/WinSCP-6.5.4-Automation.zip (UA-dependent response!) Sha256 = '9E6E520028264FC738708B7CE6903C34EA7B782A4B2AF7320C602E8C7FCF9501' File = 'winscp-automation.zip' }, @{ Name = 'Microsoft go-sqlcmd v1.10.0 (x64)' Url = 'https://monitorspider.com/backup-kits/tools/sqlcmd-v1.10.0-windows-amd64.zip' # Original: https://github.com/microsoft/go-sqlcmd/releases/download/v1.10.0/sqlcmd-windows-amd64.zip Sha256 = 'A4C28332FCC6E497D655E53AC8F4939F4AB170F9CFD32D0FA5081B60F4D9D691' File = 'sqlcmd.zip' SqlOnly = $true } ) $script:passCount = 0 $script:failCount = 0 function Report { param([bool]$Ok, [string]$What, [string]$Detail = '') if ($Ok) { $script:passCount++; Write-Host (" [ OK ] {0}" -f $What) -ForegroundColor Green } else { $script:failCount++; Write-Host (" [FAIL] {0}{1}" -f $What, $(if ($Detail) { " - $Detail" } else { '' })) -ForegroundColor Red } } function Get-ConfigValue { param($Object, [string]$Name, $Default = $null) if ($null -ne $Object -and $Object.PSObject.Properties[$Name] -and $null -ne $Object.$Name) { return $Object.$Name } return $Default } # ---- Load config (tolerated missing in -Install; required for full checks) -- $config = $null if (Test-Path $ConfigPath) { try { $config = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json } catch { } # reported below } $sqlConfigured = [bool](Get-ConfigValue $config 'SqlInstance' '') # ---- Install mode ------------------------------------------------------------ if ($Install) { Write-Host "`nInstalling portable tools into $toolsDir ..." -ForegroundColor Cyan New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null $dl = Join-Path $toolsDir 'downloads' New-Item -ItemType Directory -Force -Path $dl | Out-Null foreach ($pin in $pins) { if ($pin.ContainsKey('SqlOnly') -and $pin.SqlOnly -and -not $sqlConfigured) { Write-Host " skip $($pin.Name) (no SqlInstance in config)"; continue } $dest = Join-Path $dl $pin.File if ((Test-Path $dest) -and -not $Force) { Write-Host " have $($pin.File) (use -Force to re-download)" } else { Write-Host " get $($pin.Url)" Invoke-WebRequest -Uri $pin.Url -OutFile $dest -UseBasicParsing } $hash = (Get-FileHash -Algorithm SHA256 -Path $dest).Hash if ($hash -ne $pin.Sha256) { Remove-Item $dest -Force throw ("SHA-256 MISMATCH for {0}`n expected {1}`n got {2}`n" -f $pin.Name, $pin.Sha256, $hash) + "The vendor may have released a new version. Get the current kit (with fresh pins) from https://monitorspider.com/backup-kits/ - do not bypass this check." } Write-Host " sha256 verified" } # 7-Zip: unpack the Extra package with the bootstrap extractor; prefer x64. $sevenDir = Join-Path $toolsDir '7zip' & (Join-Path $dl '7zr.exe') x (Join-Path $dl '7z-extra.7z') "-o$sevenDir" -y | Out-Null if ($LASTEXITCODE -ne 0) { throw '7zr.exe failed to extract the 7-Zip Extra package.' } if (Test-Path (Join-Path $sevenDir 'x64\7za.exe')) { Copy-Item -Path (Join-Path $sevenDir 'x64\*') -Destination $sevenDir -Force } if (-not (Test-Path (Join-Path $sevenDir '7za.exe'))) { throw "7za.exe not found after extraction - the Extra package layout changed; get an updated kit." } Write-Host " ready tools\7zip\7za.exe" # WinSCP: .NET assembly + WinSCP.exe side by side (the assembly drives the exe). $winscpDir = Join-Path $toolsDir 'winscp' Expand-Archive -Path (Join-Path $dl 'winscp-automation.zip') -DestinationPath $winscpDir -Force foreach ($need in @('WinSCPnet.dll', 'WinSCP.exe')) { if (-not (Test-Path (Join-Path $winscpDir $need))) { throw "$need not found after extraction - the Automation package layout changed; get an updated kit." } } Write-Host " ready tools\winscp\WinSCPnet.dll" # go-sqlcmd: single binary; skip the 33 MB debug build. if ($sqlConfigured) { $tmp = Join-Path $dl 'sqlcmd-extract' Expand-Archive -Path (Join-Path $dl 'sqlcmd.zip') -DestinationPath $tmp -Force Copy-Item -Path (Join-Path $tmp 'sqlcmd.exe') -Destination (Join-Path $toolsDir 'sqlcmd.exe') -Force Remove-Item $tmp -Recurse -Force Write-Host " ready tools\sqlcmd.exe" } Write-Host "Install done.`n" -ForegroundColor Cyan } # ---- Checks ------------------------------------------------------------------ Write-Host "`nMonitorSpider backup kit pre-flight:" -ForegroundColor Cyan Report ($PSVersionTable.PSVersion.Major -ge 5) "PowerShell $($PSVersionTable.PSVersion)" 'need 5.1+' $policy = Get-ExecutionPolicy Report ($policy -notin @('Restricted', 'AllSigned')) "Execution policy: $policy" 'run: Set-ExecutionPolicy -Scope LocalMachine RemoteSigned' function Find-Tool { param([string[]]$Candidates) foreach ($c in $Candidates) { if ($c -and (Test-Path $c)) { return $c } } return $null } $sevenZip = Find-Tool @((Join-Path $toolsDir '7zip\7za.exe'), 'C:\Program Files\7-Zip\7z.exe', 'C:\Program Files (x86)\7-Zip\7z.exe') Report ([bool]$sevenZip) "7-Zip: $(if ($sevenZip) { $sevenZip } else { 'not found' })" 'run: .\preflight.ps1 -Install' $winScpDll = Find-Tool @((Join-Path $toolsDir 'winscp\WinSCPnet.dll'), 'C:\Program Files (x86)\WinSCP\WinSCPnet.dll', 'C:\Program Files\WinSCP\WinSCPnet.dll') Report ([bool]$winScpDll) "WinSCP .NET assembly: $(if ($winScpDll) { $winScpDll } else { 'not found' })" 'run: .\preflight.ps1 -Install' if (-not (Test-Path $ConfigPath)) { Report $false "Config: $ConfigPath" 'copy windows-backup-config.example.json there and fill it in' } elseif ($null -eq $config) { Report $false "Config: $ConfigPath" 'file exists but is not valid JSON' } else { Report $true "Config parses: $ConfigPath" $pw = Get-ConfigValue $config 'ArchivePassword' '' Report ($pw -and $pw -notmatch '^<.*>$') 'ArchivePassword is set' 'still the placeholder - the archive would be trivially readable' if ($sqlConfigured) { $sqlcmdExe = Find-Tool @((Join-Path $toolsDir 'sqlcmd.exe'), (Get-Command sqlcmd -ErrorAction SilentlyContinue).Source) Report ([bool]$sqlcmdExe) "sqlcmd: $(if ($sqlcmdExe) { $sqlcmdExe } else { 'not found' })" 'run: .\preflight.ps1 -Install (or install SQL client tools)' if ($sqlcmdExe) { & $sqlcmdExe -S $config.SqlInstance -E -C -b -Q 'SELECT 1' *> $null Report ($LASTEXITCODE -eq 0) "sqlcmd connects to $($config.SqlInstance)" 'check the instance name and that this account has SQL access (-C handles the self-signed cert)' } } else { Write-Host ' [ -- ] SQL backup not configured (SqlInstance empty) - files-only kit' } $upload = Get-ConfigValue $config 'Upload' if ($upload -and (Get-ConfigValue $upload 'HostName' '')) { $proto = Get-ConfigValue $upload 'Protocol' 'Ftp' $port = if ($proto -eq 'Sftp') { 22 } elseif ((Get-ConfigValue $upload 'FtpSecure' 'Explicit') -eq 'Implicit') { 990 } else { 21 } $tcp = Test-NetConnection -ComputerName $upload.HostName -Port $port -WarningAction SilentlyContinue Report $tcp.TcpTestSucceeded "Upload host $($upload.HostName):$port ($proto) reachable" 'firewall / hostname / port' } else { Report $false 'Upload configured' 'fill in the Upload block' } $pingUrl = Get-ConfigValue (Get-ConfigValue $config 'MonitorSpider') 'PingUrl' '' if ($pingUrl -and $pingUrl -notmatch 'YOUR_MONITOR_ID') { try { # status=start is harmless: it never overwrites the last completed # run - and it makes the monitor visibly come alive in the dashboard. Invoke-RestMethod -Uri "$pingUrl&status=start" -TimeoutSec 15 | Out-Null Report $true 'MonitorSpider ping URL works (sent a start ping - check your dashboard)' } catch { Report $false 'MonitorSpider ping URL' "$_" } } else { Report $false 'MonitorSpider ping URL configured' 'create a Backup (push) monitor at monitorspider.com and paste its ping URL - without it, nobody hears this backup die' } $staging = Get-ConfigValue $config 'StagingDir' 'C:\server-backups' try { New-Item -ItemType Directory -Force -Path $staging | Out-Null $freeGb = (Get-PSDrive -Name ($staging[0])).Free / 1GB Report ($freeGb -gt 2) ("Staging {0} writable, {1:N1} GB free" -f $staging, $freeGb) 'less than 2 GB free' } catch { Report $false "Staging dir $staging" "$_" } } Write-Host '' if ($script:failCount -gt 0) { Write-Host "$($script:failCount) check(s) FAILED, $($script:passCount) passed." -ForegroundColor Red exit 1 } Write-Host "All $($script:passCount) checks passed. Schedule windows-sqlserver.ps1 and sleep well." -ForegroundColor Green exit 0