Worklet: Automox OS Upgrade to Latest Windows10 OS version

The following Worklet will ensure all of your devices are always on the most current release of Windows 10. Since the current version is 2004, that is the version this Worklet will upgrade you.

This Worklet supports multiple languages:

DISCLAIMER: THIS WILL AUTOMATICALLY REBOOT THE DEVICE WHEN THE UPGRADE IS COMPLETE WITHOUT USER NOTIFICATION

Thanks to @aescolastico for creating this!

Evaluation:


if ((Test-Path $iso) -eq $true)
    {Remove-Item $iso
}

$osversion = (Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')

if (($osversion -lt "2004")) 
	{exit 1
		}
else 
	{exit 0
		}

Remediation:

function Get-Win10ISOLink {
    <#
    .SYNOPSIS
        This function generates a fresh download link for a Windows 10 ISO
    .NOTES
        Version:        1.6
        Author:         Andy Escolastico
        Creation Date:  10/11/2019
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [ValidateSet("64-bit", "32-bit")]
        [String] $Architecture = (Get-WmiObject Win32_OperatingSystem).OSArchitecture,
        [Parameter(Mandatory=$false)] 
        [ValidateSet("fr-dz", "es-ar", "en-au", "nl-be", "fr-be", "es-bo", "bs-ba", "pt-br", "en-ca", "fr-ca", "cs-cz", "es-cl", "es-co", "es-cr", "sr-latn-me", "en-cy", "da-dk", "de-de", "es-ec", "et-ee", "en-eg", "es-sv", "es-es", "fr-fr", "es-gt", "en-gulf", "es-hn", "en-hk", "hr-hr", "en-in", "id-id", "en-ie", "is-is", "it-it", "en-jo", "lv-lv", "en-lb", "lt-lt", "hu-hu", "en-my", "en-mt", "es-mx", "fr-ma", "nl-nl", "en-nz", "es-ni", "en-ng", "nb-no", "de-at", "en-pk", "es-pa", "es-py", "es-pe", "en-ph", "pl-pl", "pt-pt", "es-pr", "es-do", "ro-md", "ro-ro", "en-sa", "de-ch", "en-sg", "sl-si", "sk-sk", "en-za", "sr-latn-rs", "en-lk", "fr-ch", "fi-fi", "sv-se", "fr-tn", "tr-tr", "en-gb", "en-us", "es-uy", "es-ve", "vi-vn", "el-gr", "ru-by", "bg-bg", "ru-kz", "ru-ru", "uk-ua", "he-il", "ar-iq", "ar-sa", "ar-ly", "ar-eg", "ar-gulf", "th-th", "ko-kr", "zh-cn", "zh-tw", "ja-jp", "zh-hk")]
        [String] $Locale = (Get-WinSystemLocale).Name,
        [Parameter(Mandatory=$false)]
        [ValidateSet("Arabic", "Brazilian Portuguese", "Bulgarian", "Chinese (Simplified)", "Chinese (Traditional)", "Croatian", "Czech", "Danish", "Dutch", "English", "English International", "Estonian", "Finnish", "French", "French Canadian", "German", "Greek", "Hebrew", "Hungarian", "Italian", "Japanese", "Korean", "Latvian", "Lithuanian", "Norwegian", "Polish", "Portuguese", "Romanian", "Russian", "Serbian Latin", "Slovak", "Slovenian", "Spanish", "Spanish (Mexico)", "Swedish", "Thai", "Turkish", "Ukrainian")]
        [String] $Language = "English",
        [Parameter(Mandatory=$false)] 
        [ValidateSet("1909", "Latest")]
        [String] $Version = "Latest"
    )
    
    # prefered architecture
    if ($Architecture -eq "64-bit"){ $archID = "x64" } else { $archID = "x32" }
    
    # prefered prodID
    if ($Version -eq "Latest") {
        # grabs latest id
        $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri "https://www.microsoft.com/$Locale/software-download/windows10ISO" -UseBasicParsing
        $prodID = ([regex]::Match((($response).RawContent), 'product-info-content.*option value="(.*)">Windows 10')).captures.groups[1].value
    } else{
        # uses hard-coded id
        $prodID = "1429"
    } 

    # variables you might not want to change (unless msft changes their schema)
    $pgeIDs = @("a8f8f489-4c7f-463a-9ca6-5cff94d8d041", "cfa9e580-a81e-4a4b-a846-7b21bf4e2e5b")
    $actIDs = @("getskuinformationbyproductedition", "getproductdownloadlinksbysku")
    $hstParam = "www.microsoft.com"
    $segParam = "software-download"
    $sdvParam = "2"
    $verID = "Windows10ISO"

    # used to spoof a non-windows web request
    $userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362"

    # used to maintain session in subsequent requests
    $sessionID = [GUID]::NewGuid()

    # builds session request url 
    $uri = "https://www.microsoft.com/" + $Locale + "/api/controls/contentinclude/html"
    $uri += "?pageId=" + $pgeIDs[0]
    $uri += "&host=" + $hstParam
    $uri += "&segments=" + $segParam + "," + $verID
    $uri += "&query="
    $uri += "&action=" + $actIDs[0]
    $uri += "&sessionId=" + $sessionID
    $uri += "&productEditionId=" + $prodID
    $uri += "&sdvParam=" + $sdvParam

    # requests user session
    $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri $uri -UseBasicParsing

    # prefered skuid
    if ($Version -eq "Latest") {
        # grabs latest id
        $skuIDs = (($response.RawContent) -replace "&quot;" -replace '</div><script language=.*' -replace  '</select></div>.*' -split '<option value="' -replace '">.*' -replace '{' -replace '}'| Select-String -pattern 'id:') -replace 'id:' -replace 'language:' -replace '\s' | ConvertFrom-String -PropertyNames SkuID, Language -Delimiter ','
        $skuID = $skuIDs | Where-Object {$_.Language -eq "$Language"} | Select-Object -ExpandProperty SkuID
    }
    else{
        # uses hard-coded id
        $skuID = "9029"
    } 

    # builds link request url
    $uri = "https://www.microsoft.com/" + $Locale + "/api/controls/contentinclude/html"
    $uri += "?pageId=" + $pgeIDs[1]
    $uri += "&host=" + $hstParam
    $uri += "&segments=" + $segParam + "," + $verID
    $uri += "&query="
    $uri += "&action=" + $actIDs[1]
    $uri += "&sessionId=" + $sessionID
    $uri += "&skuId=" + $skuID
    $uri += "&lang=" + $Language
    $uri += "&sdvParam=" + $sdvParam

    # requests link data
    $response = Invoke-WebRequest -UserAgent $userAgent -WebSession $session -Uri $uri -UseBasicParsing

    # parses response data 
    $raw = ($response.Links).href
    $clean = $raw.Replace('amp;','')

    # stores download link
    $dlLink = $clean | Where-Object {$_ -like "*$archID*"}

    # outputs download link
    Write-Output $dlLink
}

function Start-Win10UpgradeISO {
    <#
    .SYNOPSIS
        Downloads the latest Windows 10 ISO, mounts it, and runs it silently.
    .NOTES
        Version:        1.1
        Author:         Andy Escolastico
        Creation Date:  02/11/2020
        
        Version 1.0 (2020-02-11)
        Version 1.1 (2020-06-03) - Added handling for case where drive letter was not mounted.                
        Version 1.2 (2020-06-03) - Added ISO download functionality
    #>
    [CmdletBinding()]
    param (
        #THIS FLAG DOES NOT WORK FOR THIS FUNCTION
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        [Parameter(Mandatory=$false)] 
        [ValidateSet("64-bit", "32-bit")]
        [String] $Architecture = (Get-WmiObject Win32_OperatingSystem).OSArchitecture,
        [Parameter(Mandatory=$false)] 
        [String] [String] $DLPath = (Get-Location).Path + "\" +"Win10_" + $Architecture + ".iso",
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path 
    )

    Write-Verbose "Attempting to generate a $Architecture windows 10 iso download link" -Verbose
    try {
        $DLLink = Get-Win10ISOLink -Architecture $Architecture
    }
    catch {
        throw "Failed to generate windows 10 iso download link."
    }
    
    Write-Verbose "Attempting to download windows 10 iso to '$DLPath'" -Verbose
    try {
        (New-Object System.Net.WebClient).DownloadFile($DLLink, "$DLPath")
    }
    catch {
        throw "Failed to download ISO at path specified."
    }
    
    $ISOPath = $DLPath
    
    if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter
    } else {
        throw "ISO could not be found under $($ISOPath)."
    }
    
    Write-Warning "The Upgrade will commence shortly. Your PC will be rebooted soon. Please save any work you do not want to lose."
    
    if ($DriveLetter) {
        if ($Reboot -eq $true){
            Invoke-Expression "$($DriveLetter):\setup.exe /auto Upgrade /quiet /Compat IgnoreWarning /DynamicUpdate disable /copylogs $LogPath"
        } else{
            Invoke-Expression "$($DriveLetter):\setup.exe /auto Upgrade /quiet /NoReboot /NoRestartUI /NoRestart /Compat IgnoreWarning /DynamicUpdate disable /copylogs $LogPath"
        }    
    } else {
        throw "ISO could not be mounted on this system."
    }

}
New-Alias -Name "Start-Win10FeatureUpdate" -Value "Start-Win10UpgradeISO" -ea 0

function Start-Win10UpgradeWUA {
    <#
    .SYNOPSIS
        This function downloads the Windows update assistant tool and runs it silently.
    .NOTES
        Version:        1.0
        Author:         Andy Escolastico
        Creation Date:  05/10/2020
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        #THIS FLAG DOES NOT WORK FOR THIS FUNCTION
        [Parameter(Mandatory=$false)] 
        [String] $DLPath = (Get-Location).Path,
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path
    )
    if(!(Test-Path -Path $DLPath)){$null = New-Item -ItemType directory -Path $DLPath -Force}   
    if(!(Test-Path -Path $LogPath)){$null = New-Item -ItemType directory -Path $LogPath -Force}      
    $DLLink = "https://go.microsoft.com/fwlink/?LinkID=799445"
    $PackagePath = "$DLPath\Win10_WUA.exe"
    $LogPath = "$LogPath\Win10_WUA.log"
    (New-Object System.Net.WebClient).DownloadFile($DLLink, "$PackagePath")
    Write-Host "The Upgrade will commence shortly. Your PC will be rebooted. Please save any work you do not want to lose."
    if ($Reboot -eq $true){
        Invoke-Expression "$PackagePath /copylogs $LogPath /auto upgrade /dynamicupdate /compat ignorewarning enable /skipeula /quietinstall"
    } else{
        Invoke-Expression "$PackagePath /NoReboot /NoRestartUI /NoRestart /copylogs $LogPath /auto upgrade /dynamicupdate /compat ignorewarning enable /skipeula /quietinstall"
    }
}

function Start-Win10UpgradeCAB{
    <#
    .SYNOPSIS
        This function downloads the feature enablement package cab file and runs it silently using dism.exe.
    .NOTES
        Version:        1.0
        Author:         Andy Escolastico
        Creation Date:  06/11/2020
    #>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] 
        [ValidateSet("1909")]
        [String] $Version = "1909",
        [Parameter(Mandatory=$false)] 
        [Boolean] $Reboot = $true,
        [Parameter(Mandatory=$false)]
        [String] $DLPath = (Get-Location).Path,
        [Parameter(Mandatory=$false)] 
        [String] $LogPath = (Get-Location).Path
    )
    if(!(Test-Path -Path $DLPath)){$null = New-Item -ItemType directory -Path $DLPath -Force}   
    if(!(Test-Path -Path $LogPath)){$null = New-Item -ItemType directory -Path $LogPath -Force}    
    if($Version -eq "1909"){
        $DLLink = 'http://b1.download.windowsupdate.com/d/upgr/2019/11/windows10.0-kb4517245-x64_4250e1db7bc9468236c967c2c15f04b755b3d3a9.cab'
    }
    $PackagePath = "$DLPath\Win10_CAB.cab"
    $LogPath = "$LogPath\Win10_CAB.log"
    (New-Object System.Net.WebClient).DownloadFile($DLLink, "$PackagePath")
    if ($Reboot -eq $true){
        Invoke-Expression "DISM.exe /Online /Add-Package /Quiet /PackagePath:$PackagePath /LogPath:$LogPath"
    } else{
        Invoke-Expression "DISM.exe /Online /Add-Package /Quiet /NoRestart /PackagePath:$PackagePath /LogPath:$LogPath"
    }
}

Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'
3 Likes

sweet , the top check still refers to the 1909 iso? should that not be variable too?

@Maikel The 1909 was for removing the iso after the install. This is in the evaluation so it will remove it after remediation is complete and we auto re-evaulate. This is actually not needed anymore so I removed it.

1 Like

What if there is an update in between this one and the one that the device is presently on? Will it step up to the one in between and then this one or just go straight to the latest?
This sounds wonderful, just want to make sure I understand it correctly. My patch window is tomorrow, and I’d like to give this a go on a test group.

The last line

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3, [Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls12; (new-object Net.WebClient).DownloadString('https://raw.githubusercontent.com/RFAInc/windows10-iso/master/win10-iso-functions.ps1') | Invoke-Expression; Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'

actually downloads the entire script from GitHub and runs the function. Its a cool technique for giving clients scripts that you can update after the fact.

Meaning you could replace the entirety of the remediation block with just that last line. With this setup, you get the latest code updates, but you have less control over what gets deployed. I wouldn’t recommend it. I would advise doing what you guys normally do, where you paste the actual code, and run the function. Right now you’re halfway there. You’re still just downloading the code directly from github and invoking that, instead of running what’s above it. ATM they are the same but that could change if we push new code.

It also means that if a device fails to download the script from github, the whole thing will fail. Despite the fact that you have the code downloaded from Automox servers already.

This is what the last line would look like instead:

Start-Win10FeatureUpdate -DLPath 'C:\Windows\Temp\Windows10.iso' -LogPath 'C:\Windows\Temp\WindowsUpgradeLogs'

Apologies if that explanation isnt very clear.

2 Likes

trying to upgrade win10 1909 and i get the following error:

Mount-DiskImage : The I/O operation has been aborted because of either a thread exit or an application request. At line:150 char:25 + $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Vol … + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (MSFT_DiskImage …torageType = 1):ROOT/Microsoft/…/MSFT_DiskImage) [Mou nt-DiskImage], CimException + FullyQualifiedErrorId : HRESULT 0x800703e3,Mount-DiskImage ISO could not be mounted on this system. At line:164 char:9 + throw “ISO could not be mounted on this system.” + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : OperationStopped: (ISO could not be mounted on this system.:String) [], RuntimeException + FullyQualifiedErrorId : ISO could not be mounted on this system. Error: 0x800703E3, -2147023901 Error Source: FACILITY_WIN32 Error Message: FACILITY_WIN32 is not currently supported by this translator.

Thanks @aescolastico, Nic just updated the last line as you defined in this update!

1 Like

We had a few other customers with a similar issue, and we were also able to reproduce in our own testing. Our issue turned out to not be an actual issue with mounting the iso properly, but the drive letter was not returned. Here is what fixed it for me:

In the “Start-Win10UpgradeISO” function

Try replacing this:

if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter

With this:

   if (Test-Path $ISOPath) {
        $DriveLetter = (Mount-DiskImage -ImagePath $ISOPath | Get-Volume).DriveLetter
        $DriveLetter = (Get-DiskImage -ImagePath $ISOPath |Get-Volume).DriveLetter
        if (!$DriveLetter){
            Mount-DiskImage -ImagePath $ISOPath -StorageType ISO
            } 

Hey I noticed a silly bug in my original code the other day.
The two lines that have:
if ($Version = “Latest”)

Are not using correct comparison syntax. Instead it’s assigning a value to the variable $Version.

It should be “-eq” instead of “=“. I’ve since update the code on GitHub. This would cause the script to always install the latest and never actually install version 1909. Apologies for not seeing this earlier. I would recommend updating the code in your worklet.

1 Like

Thanks for catching that @aescolastico - I’ve changed those two lines; let me know if they look ok now.

Looks right.

@Nic You guys ever find out if there’s a way to suppress reboots with the new ISOs?

1 Like

Not that I know of. Let me ping @awhitman and see if he’s found a way for that.