<# .SYNOPSIS Downloads or refreshes all scanner executables defined in scanners.json. .DESCRIPTION Reads scanner definitions from scanners.json and downloads each scanner EXE to the downloads\ subdirectory. Skips scanners with null download URLs. .PARAMETER Force Re-download all scanners even if they already exist locally. .EXAMPLE .\Download-Scanners.ps1 .\Download-Scanners.ps1 -Force #> [CmdletBinding()] param( [switch]$Force ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $ConfigPath = Join-Path $PSScriptRoot 'scanners.json' $DownloadsDir = Join-Path $PSScriptRoot 'downloads' if (-not (Test-Path $ConfigPath)) { Write-Host "[ERROR] scanners.json not found at: $ConfigPath" -ForegroundColor Red exit 1 } $config = Get-Content $ConfigPath -Raw | ConvertFrom-Json if (-not (Test-Path $DownloadsDir)) { New-Item -ItemType Directory -Path $DownloadsDir -Force | Out-Null Write-Host "[INFO] Created downloads directory: $DownloadsDir" -ForegroundColor Cyan } $results = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($scanner in $config.scanners) { $urlToUse = $null $fileToSave = $null # Manual-download entries: print instructions and skip if ($scanner.PSObject.Properties['manual_download'] -and $scanner.manual_download -eq $true) { $note = if ($scanner.PSObject.Properties['manual_download_note']) { $scanner.manual_download_note } else { 'Place EXE manually in downloads\' } Write-Host " [MANUAL] $($scanner.name) — $note" -ForegroundColor Yellow $results.Add([pscustomobject]@{ Scanner = $scanner.name; Status = 'MANUAL'; File = ''; Size = 0 }) continue } if ($scanner.installer_exe -and $scanner.download_url) { $urlToUse = $scanner.download_url $fileToSave = Join-Path $DownloadsDir (Split-Path $scanner.installer_exe -Leaf) } elseif ($scanner.download_url) { $urlToUse = $scanner.download_url $leafName = Split-Path $scanner.exe -Leaf $fileToSave = Join-Path $DownloadsDir $leafName } else { Write-Host " [SKIP] $($scanner.name) — no download URL configured" -ForegroundColor Yellow $results.Add([pscustomobject]@{ Scanner = $scanner.name; Status = 'SKIPPED'; File = ''; Size = 0 }) continue } if ((Test-Path $fileToSave) -and -not $Force) { $existingSize = (Get-Item $fileToSave).Length Write-Host " [OK] $($scanner.name) already exists ($([math]::Round($existingSize / 1MB, 1)) MB)" -ForegroundColor Green $results.Add([pscustomobject]@{ Scanner = $scanner.name; Status = 'EXISTS'; File = $fileToSave; Size = $existingSize }) continue } Write-Host " [....] $($scanner.name) — downloading from $urlToUse" -ForegroundColor Cyan try { $wc = New-Object System.Net.WebClient $wc.Headers.Add('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)') $wc.DownloadFile($urlToUse, $fileToSave) $wc.Dispose() if (-not (Test-Path $fileToSave)) { throw "File not found after download attempt" } $downloadedSize = (Get-Item $fileToSave).Length if ($downloadedSize -eq 0) { throw "Downloaded file is 0 bytes — URL may be invalid or redirected" } Write-Host " [OK] $($scanner.name) downloaded ($([math]::Round($downloadedSize / 1MB, 1)) MB) -> $fileToSave" -ForegroundColor Green $results.Add([pscustomobject]@{ Scanner = $scanner.name; Status = 'DOWNLOADED'; File = $fileToSave; Size = $downloadedSize }) } catch { Write-Host " [ERROR] $($scanner.name) — $($_.Exception.Message)" -ForegroundColor Red if (Test-Path $fileToSave) { Remove-Item $fileToSave -Force -ErrorAction SilentlyContinue } $results.Add([pscustomobject]@{ Scanner = $scanner.name; Status = 'FAILED'; File = $fileToSave; Size = 0 }) } } Write-Host "" Write-Host "=== Download Summary ===" -ForegroundColor Cyan $results | Format-Table -AutoSize $manual = $results | Where-Object { $_.Status -eq 'MANUAL' } if ($manual) { Write-Host "[INFO] $($manual.Count) scanner(s) require manual download — see notes above." -ForegroundColor Yellow } $failed = $results | Where-Object { $_.Status -eq 'FAILED' } if ($failed) { Write-Host "[WARNING] $($failed.Count) scanner(s) failed to download. Scan coverage will be incomplete." -ForegroundColor Yellow exit 1 } exit 0