├── .gitignore ├── package.json ├── data ├── windows-11-iot.json ├── windows-11.json ├── windows-2022.json └── windows-2025.json ├── README.md ├── .github └── workflows │ └── build.yml ├── IsoInfo.cs ├── scrape.js └── scrape.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | data/*-scrape.json 4 | *.iso 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-evaluation-isos-scraper", 3 | "version": "1.1.0", 4 | "type": "module", 5 | "description": "get the latest windows evaluation iso url", 6 | "main": "scrape.js", 7 | "author": "Rui Lopes ", 8 | "license": "GPL-3.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/rgl/windows-evaluation-isos-scraper.git" 12 | }, 13 | "dependencies": { 14 | "puppeteer": "^24.3.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /data/windows-11-iot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-11-iot", 3 | "url": "https://software-static.download.prss.microsoft.com/dbazure/998969d5-f34g-4e03-ac9d-1f9786c66749/26100.1742.240906-0331.ge_release_svc_refresh_CLIENT_IOT_LTSC_EVAL_x64FRE_en-us.iso", 4 | "checksum": "2cee70bd183df42b92a2e0da08cc2bb7a2a9ce3a3841955a012c0f77aeb3cb29", 5 | "size": 5060020224, 6 | "createdAt": "2024-09-07T00:00:00+00:00", 7 | "images": [ 8 | { 9 | "index": 1, 10 | "name": "Windows 11 IoT Enterprise LTSC Evaluation", 11 | "version": "10.0.26100.1742" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /data/windows-11.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-11", 3 | "url": "https://software-static.download.prss.microsoft.com/dbazure/888969d5-f34g-4e03-ac9d-1f9786c66749/26100.1742.240906-0331.ge_release_svc_refresh_CLIENT_LTSC_EVAL_x64FRE_en-us.iso", 4 | "checksum": "67cec5865eaa037a72ddc633a717a10a2bed50778862267223ddb9c60ef5da68", 5 | "size": 5112850432, 6 | "createdAt": "2024-09-07T00:00:00+00:00", 7 | "images": [ 8 | { 9 | "index": 1, 10 | "name": "Windows 11 Enterprise LTSC Evaluation", 11 | "version": "10.0.26100.1742" 12 | }, 13 | { 14 | "index": 2, 15 | "name": "Windows 11 Enterprise N LTSC Evaluation", 16 | "version": "10.0.26100.1742" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /data/windows-2022.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-2022", 3 | "url": "https://software-static.download.prss.microsoft.com/sg/download/888969d5-f34g-4e03-ac9d-1f9786c66749/SERVER_EVAL_x64FRE_en-us.iso", 4 | "checksum": "3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325", 5 | "size": 5044094976, 6 | "createdAt": "2022-03-04T00:00:00+00:00", 7 | "images": [ 8 | { 9 | "index": 1, 10 | "name": "Windows Server 2022 Standard Evaluation", 11 | "version": "10.0.20348.587" 12 | }, 13 | { 14 | "index": 2, 15 | "name": "Windows Server 2022 Standard Evaluation (Desktop Experience)", 16 | "version": "10.0.20348.587" 17 | }, 18 | { 19 | "index": 3, 20 | "name": "Windows Server 2022 Datacenter Evaluation", 21 | "version": "10.0.20348.587" 22 | }, 23 | { 24 | "index": 4, 25 | "name": "Windows Server 2022 Datacenter Evaluation (Desktop Experience)", 26 | "version": "10.0.20348.587" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /data/windows-2025.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows-2025", 3 | "url": "https://software-static.download.prss.microsoft.com/dbazure/888969d5-f34g-4e03-ac9d-1f9786c66749/26100.1742.240906-0331.ge_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso", 4 | "checksum": "d0ef4502e350e3c6c53c15b1b3020d38a5ded011bf04998e950720ac8579b23d", 5 | "size": 6014152704, 6 | "createdAt": "2024-09-07T00:00:00+00:00", 7 | "images": [ 8 | { 9 | "index": 1, 10 | "name": "Windows Server 2025 Standard Evaluation", 11 | "version": "10.0.26100.1742" 12 | }, 13 | { 14 | "index": 2, 15 | "name": "Windows Server 2025 Standard Evaluation (Desktop Experience)", 16 | "version": "10.0.26100.1742" 17 | }, 18 | { 19 | "index": 3, 20 | "name": "Windows Server 2025 Datacenter Evaluation", 21 | "version": "10.0.26100.1742" 22 | }, 23 | { 24 | "index": 4, 25 | "name": "Windows Server 2025 Datacenter Evaluation (Desktop Experience)", 26 | "version": "10.0.26100.1742" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This scrapes the Windows Evaluation ISO addresses into a JSON data file. 4 | 5 | ## Scraped Windows Editions 6 | 7 | * [Windows 11](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-11-enterprise) 8 | * [Windows 11 IoT](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-11-iot-enterprise-ltsc) 9 | * [Windows 2022](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022) 10 | * [Windows 2025](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025) 11 | 12 | ## Data Files 13 | 14 | The code in this repository creates a `data/windows-*.json` file for each Windows Edition, for example, the `data/windows-2022.json` file will be alike: 15 | 16 | ```json 17 | { 18 | "name": "windows-2022", 19 | "url": "https://software-static.download.prss.microsoft.com/sg/download/888969d5-f34g-4e03-ac9d-1f9786c66749/SERVER_EVAL_x64FRE_en-us.iso", 20 | "checksum": "3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325", 21 | "size": 5044094976, 22 | "createdAt": "2022-03-04T00:00:00+00:00", 23 | "images": [ 24 | { 25 | "index": 1, 26 | "name": "Windows Server 2022 Standard Evaluation", 27 | "version": "10.0.20348.587" 28 | }, 29 | { 30 | "index": 2, 31 | "name": "Windows Server 2022 Standard Evaluation (Desktop Experience)", 32 | "version": "10.0.20348.587" 33 | }, 34 | { 35 | "index": 3, 36 | "name": "Windows Server 2022 Datacenter Evaluation", 37 | "version": "10.0.20348.587" 38 | }, 39 | { 40 | "index": 4, 41 | "name": "Windows Server 2022 Datacenter Evaluation (Desktop Experience)", 42 | "version": "10.0.20348.587" 43 | } 44 | ] 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | workflow_dispatch: 5 | schedule: 6 | #- cron: '0 * * * *' # hourly. 7 | - cron: '0 0 * * WED' # every wednesday. 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - name: windows-11 15 | - name: windows-11-iot 16 | - name: windows-2022 17 | - name: windows-2025 18 | runs-on: windows-2022 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Build 28 | run: pwsh scrape.ps1 ${{ matrix.name }} 29 | - name: Build summary 30 | run: | 31 | $iso = Get-Content (Resolve-Path data/${{ matrix.name }}.json) | ConvertFrom-Json 32 | Add-Content $env:GITHUB_STEP_SUMMARY @" 33 | | Property | Value | 34 | | :--- | :--- | 35 | | Name | $($iso.name) | 36 | | Version | $($iso.images.version | Select-Object -First 1) | 37 | | Created At | $($iso.createdAt.ToString('O')) | 38 | | Iso | [$(Split-Path -Leaf $iso.url)]($($iso.url)) | 39 | | Checksum | $($iso.checksum) | 40 | | Size | $($iso.size) | 41 | $(($iso.images | ForEach-Object {"| Image #$($_.index) Name | $($_.name) |"}) -join "`n") 42 | "@ 43 | - name: Upload artifact 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: ${{ matrix.name }} 47 | path: | 48 | data/${{ matrix.name }}.json 49 | commit: 50 | if: github.event.schedule 51 | name: Commit 52 | runs-on: ubuntu-22.04 53 | needs: 54 | - build 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | - name: Download artifacts 59 | uses: actions/download-artifact@v4 60 | with: 61 | path: data 62 | - name: Commit data 63 | shell: bash 64 | run: | 65 | set -euxo pipefail 66 | mv data/*/* data 67 | if [ -n "$(git status -s data)" ]; then 68 | git config user.name 'Bot' 69 | git config user.email 'bot@localhost' 70 | echo 'Committing data...' 71 | git add data 72 | git diff --staged 73 | git commit -m 'update iso data' 74 | git push 75 | fi 76 | -------------------------------------------------------------------------------- /IsoInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | // NB this can also be used from PowerShell as: 6 | // Add-Type -Path IsoInfo.cs 7 | // [IsoInfo]::GetVolumeCreationDate('my.iso') 8 | public class IsoInfo 9 | { 10 | // see https://wiki.osdev.org/ISO_9660#The_Primary_Volume_Descriptor 11 | static bool IsPrimaryVolumeDescriptorSector(byte[] sector) 12 | { 13 | const byte PrimaryVolumeDescriptorType = 1; 14 | const byte VolumeDescriptorVersion = 1; 15 | var VolumeDescriptiorIdentifier = new byte[] {(byte)'C', (byte)'D', (byte)'0', (byte)'0', (byte)'1'}; 16 | 17 | if (sector[0] != PrimaryVolumeDescriptorType) 18 | { 19 | return false; 20 | } 21 | 22 | for (var n = 0; n < VolumeDescriptiorIdentifier.Length; ++n) 23 | { 24 | if (sector[1+n] != VolumeDescriptiorIdentifier[n]) 25 | { 26 | return false; 27 | } 28 | } 29 | 30 | if (sector[6] != VolumeDescriptorVersion) 31 | { 32 | return false; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | // see https://wiki.osdev.org/ISO_9660#Date.2Ftime_format 39 | static DateTimeOffset ReadDateTime(byte[] sector, int offset) 40 | { 41 | var year = ReadAsciiInt(sector, offset+0, 4); 42 | var month = ReadAsciiInt(sector, offset+4, 2); 43 | var day = ReadAsciiInt(sector, offset+6, 2); 44 | var hour = ReadAsciiInt(sector, offset+8, 2); 45 | var minute = ReadAsciiInt(sector, offset+10, 2); 46 | var second = ReadAsciiInt(sector, offset+12, 2); 47 | var hundredths = ReadAsciiInt(sector, offset+14, 2); 48 | var utcOffset = TimeSpan.FromMinutes((int)sector[offset+16]*15); 49 | 50 | return new DateTimeOffset(year, month, day, hour, minute, second, hundredths*10, utcOffset); 51 | } 52 | 53 | static int ReadAsciiInt(byte[] sector, int offset, int size) 54 | { 55 | var sb = new StringBuilder(size); 56 | 57 | for (var n = 0; n < size; ++n) 58 | { 59 | sb.Append((char)sector[offset+n]); 60 | } 61 | 62 | return int.Parse(sb.ToString()); 63 | } 64 | 65 | // see https://wiki.osdev.org/ISO_9660 66 | // NB this is equivalent to: 67 | // isoinfo -debug -d -i my.iso 68 | public static DateTimeOffset GetVolumeCreationDate(string path) 69 | { 70 | using (var stream = File.Open(path, FileMode.Open, FileAccess.Read)) 71 | { 72 | const int SectorSize = 2048; 73 | const byte VolumeDescriptorSetTerminatorType = 255; 74 | 75 | // read the Primary Volume Descriptor. 76 | for (var sectorIndex = 16; ; ++sectorIndex) 77 | { 78 | var sector = new byte[SectorSize]; 79 | 80 | stream.Position = sectorIndex*SectorSize; 81 | if (stream.Read(sector, 0, sector.Length) != sector.Length) 82 | { 83 | throw new ApplicationException("failed to read sector"); 84 | } 85 | 86 | if (sector[0] == VolumeDescriptorSetTerminatorType) 87 | { 88 | break; 89 | } 90 | 91 | if (!IsPrimaryVolumeDescriptorSector(sector)) 92 | { 93 | continue; 94 | } 95 | 96 | var volumeCreationDateTime = ReadDateTime(sector, 813); 97 | 98 | return volumeCreationDateTime; 99 | } 100 | 101 | throw new ApplicationException("failed to find the primary volume descriptor sector"); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /scrape.js: -------------------------------------------------------------------------------- 1 | // install dependencies: 2 | // 3 | // npm install 4 | // 5 | // execute: 6 | // 7 | // NB to troubleshoot uncomment $env:DEBUG and set {headless:false,dumpio:true} in main.js. 8 | // 9 | // $env:DEBUG = 'puppeteer:*' 10 | // node main.js 11 | 12 | import puppeteer from "puppeteer"; 13 | import fs from "fs"; 14 | import path from "path"; 15 | 16 | async function getEvaluationIsos(page, name, filterRegExp, url) { 17 | console.log("scraping", name, "from", url); 18 | 19 | await page.goto(url); 20 | 21 | return await page.evaluate(async (name, filterRegExp) => { 22 | var data = []; 23 | const els = document.querySelectorAll("a[aria-label*=' ISO '][aria-label*='(en-US)'][aria-label*=' 64-bit '],a[aria-label*=' ISO '][aria-label*='(en-US)'][aria-label*=' AMD64 ']") 24 | for (const el of els) { 25 | const ariaLabel = el.getAttribute("aria-label"); 26 | if (filterRegExp && !label.match(filterRegExp)) { 27 | continue; 28 | } 29 | const label = ariaLabel 30 | .toLowerCase() 31 | .replace(/\s*(edition|preview|download|server|iso|ltsc|enterprise|64-bit|amd64|\(en-US\))\s*/ig, " ") 32 | .replace(/[^a-z0-9]+/ig, " ") 33 | .trim() 34 | .replace(/ +/ig, "-"); 35 | const url = el.getAttribute("href"); 36 | data.push({ 37 | name: /windows/i.test(label) ? label : name, 38 | url: url, 39 | }); 40 | } 41 | return data; 42 | }, name, filterRegExp); 43 | } 44 | 45 | async function main(name) { 46 | console.log("launching"); 47 | const browser = await puppeteer.launch({ 48 | headless: true, 49 | dumpio: false, 50 | headless: "new", 51 | }); 52 | try { 53 | console.log("getting the browser version"); 54 | console.log("running under", await browser.version()); 55 | 56 | console.log("creating a new browser page"); 57 | const page = await browser.newPage(); 58 | 59 | console.log("lowering the needed bandwidth to scrape the site"); 60 | await page.setRequestInterception(true); 61 | page.on( 62 | "request", 63 | request => { 64 | if (request.resourceType() === "document") { 65 | //console.log("downloading", request.url()); 66 | request.continue(); 67 | } else { 68 | request.abort(); 69 | } 70 | } 71 | ); 72 | 73 | var targets = { 74 | "windows-11": [null, "https://www.microsoft.com/en-us/evalcenter/download-windows-11-enterprise"], 75 | "windows-11-iot": [null, "https://www.microsoft.com/en-us/evalcenter/download-windows-11-iot-enterprise-ltsc-eval"], 76 | "windows-2022": [null, "https://www.microsoft.com/en-us/evalcenter/download-windows-server-2022"], 77 | "windows-2025": [null, "https://www.microsoft.com/en-us/evalcenter/download-windows-server-2025"], 78 | }; 79 | const target = targets[name]; 80 | if (!target) { 81 | throw `unknown target ${name}`; 82 | } 83 | const data = {}; 84 | const isos = await getEvaluationIsos(page, name, ...target); 85 | for (const iso of isos) { 86 | const response = await fetch(iso.url, {method: 'HEAD'}); 87 | data[iso.name] = response.url; 88 | } 89 | 90 | const scrapePath = `data/${name}-scrape.json`; 91 | console.log(`saving to ${scrapePath}`); 92 | fs.mkdirSync(path.dirname(scrapePath), {recursive: true}); 93 | fs.writeFileSync(scrapePath, JSON.stringify(data, null, 4)); 94 | } finally { 95 | await browser.close(); 96 | } 97 | } 98 | 99 | await main(...process.argv.slice(2)); 100 | -------------------------------------------------------------------------------- /scrape.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$name 3 | ) 4 | 5 | Set-StrictMode -Version Latest 6 | $ProgressPreference = 'SilentlyContinue' 7 | $ErrorActionPreference = 'Stop' 8 | trap { 9 | "ERROR: $_" | Write-Host 10 | ($_.ScriptStackTrace -split '\r?\n') -replace '^(.*)$','ERROR: $1' | Write-Host 11 | ($_.Exception.ToString() -split '\r?\n') -replace '^(.*)$','ERROR EXCEPTION: $1' | Write-Host 12 | Exit 1 13 | } 14 | 15 | function Get-Iso($isoUrl, $isoPath) { 16 | Write-Host "Downloading $isoUrl to $isoPath" 17 | Start-BitsTransfer ` 18 | -Source $isoUrl ` 19 | -Destination $isoPath ` 20 | -RetryInterval 60 21 | } 22 | 23 | function Get-IsoWindowsImages($isoPath) { 24 | $isoPath = Resolve-Path $isoPath 25 | Write-Host "Mounting $isoPath" 26 | $isoImage = Mount-DiskImage $isoPath -PassThru 27 | try { 28 | $isoVolume = $isoImage | Get-Volume 29 | $installPath = "$($isoVolume.DriveLetter):\sources\install.wim" 30 | Write-Host "Getting Windows images from $installPath" 31 | Get-WindowsImage -ImagePath $installPath ` 32 | | ForEach-Object { 33 | $image = Get-WindowsImage ` 34 | -ImagePath $installPath ` 35 | -Index $_.ImageIndex 36 | $imageVersion = $image.Version 37 | # workaround the known windows 10 wim version mismatch, by copying it from the filename. 38 | # e.g. 19044.1288.211006-0501.21h2_release_svc_refresh_CLIENTENTERPRISEEVAL_OEMRET_x64FRE_en-us.iso 39 | # ^^^^^^^^^^ 40 | # last two version numbers of the windows version. 41 | if ($imageVersion -like '10.0.*' -and $isoPath -match '[/\\](?\d+\.\d+)[^/]+\.iso$') { 42 | $isoVersion = "10.0.$($Matches.version)" 43 | if ([version]$isoVersion -gt [version]$imageVersion) { 44 | $imageVersion = $isoVersion 45 | } 46 | } 47 | [PSCustomObject]@{ 48 | index = $image.ImageIndex 49 | name = $image.ImageName 50 | version = $imageVersion 51 | } 52 | } 53 | } finally { 54 | Write-Host "Dismounting $isoPath" 55 | Dismount-DiskImage $isoPath | Out-Null 56 | } 57 | } 58 | 59 | function Run([string]$name) { 60 | Add-Type -Path IsoInfo.cs 61 | node scrape.js $name 62 | if ($LASTEXITCODE) { 63 | throw "failed to scrape image with exit code $LASTEXITCODE" 64 | } 65 | $scrapePath = "data/$name-scrape.json" 66 | $scrape = Get-Content -Raw $scrapePath | ConvertFrom-Json 67 | $data = $scrape.PSObject.Properties | ForEach-Object { 68 | $isoUrl = $_.Value 69 | $isoPath = Split-Path -Leaf (([uri]$isoUrl).AbsolutePath) 70 | if (!(Test-Path $isoPath)) { 71 | Get-Iso $isoUrl $isoPath 72 | } 73 | [array]$images = Get-IsoWindowsImages $isoPath 74 | Write-Host "Getting the $isoPath checksum" 75 | $checksum = (Get-FileHash -Algorithm SHA256 -Path $isoPath).Hash.ToLowerInvariant() 76 | $size = (Get-Item $isoPath).Length 77 | $createdAt = [IsoInfo]::GetVolumeCreationDate($isoPath) 78 | # in CI we remove the iso file because there is limited disk space. 79 | # see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources 80 | # see https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables 81 | if ($env:CI) { 82 | Remove-Item $isoPath | Out-Null 83 | } 84 | [PSCustomObject]@{ 85 | name = $_.Name 86 | url = $isoUrl 87 | checksum = $checksum 88 | size = $size 89 | createdAt = $createdAt 90 | images = $images 91 | } 92 | } 93 | if (!$data -or !$data.Count) { 94 | throw 'Could not find any valid data in scrape.json' 95 | } 96 | Set-Content ` 97 | -Path "data/$name.json" ` 98 | -Value ($data | ConvertTo-Json -Depth 100) 99 | } 100 | 101 | Run $name 102 | --------------------------------------------------------------------------------