diff --git a/FFUDevelopment/BuildFFUVM.ps1 b/FFUDevelopment/BuildFFUVM.ps1 index d788a05..886246e 100644 --- a/FFUDevelopment/BuildFFUVM.ps1 +++ b/FFUDevelopment/BuildFFUVM.ps1 @@ -15,6 +15,9 @@ When set to $true, will allow the use of media identified as External Hard Disk .PARAMETER AllowVHDXCaching When set to $true, will cache the VHDX file to the $FFUDevelopmentPath\VHDXCache folder and create a config json file that will keep track of the Windows build information, the updates installed, and the logical sector byte size information. Default is $false. +.PARAMETER AppListPath +Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json. + .PARAMETER AppsScriptVariables When passed a hashtable, the script will alter the $FFUDevelopmentPath\Apps\InstallAppsandSysprep.cmd file to set variables with the hashtable keys as variable names and the hashtable values their content. @@ -117,6 +120,9 @@ When set to $true, will optimize the FFU file. Default is $true. .PARAMETER OptionalFeatures Provide a semicolon-separated list of Windows optional features you want to include in the FFU (e.g., netfx3;TFTP). +.PARAMETER PEDriversFolder +Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers. + .PARAMETER Processors Number of virtual processors for the virtual machine. Recommended to use at least 4. @@ -132,6 +138,9 @@ When set to $true, will remove the FFU file from the $FFUDevelopmentPath\FFU fol .PARAMETER ShareName Name of the shared folder for FFU capture. The default is FFUCaptureShare. This share will be created with rights for the user account. When finished, the share will be removed. +.PARAMETER UpdateADK +When set to $true, the script will check for and install the latest Windows ADK and WinPE add-on if they are not already installed or up-to-date. Default is $true. + .PARAMETER UpdateEdge When set to $true, will download and install the latest Microsoft Edge for Windows 10/11. Default is $false. @@ -141,12 +150,12 @@ When set to $true, will download and install the latest cumulative update for Wi .PARAMETER UpdatePreviewCU When set to $true, will download and install the latest Preview cumulative update for Windows 10/11. Default is $false. -.PARAMETER UpdateLatestNet -When set to $true, will download and install the latest .NET Framework for Windows 10/11. Default is $false. - .PARAMETER UpdateLatestDefender When set to $true, will download and install the latest Windows Defender definitions and Defender platform update. Default is $false. +.PARAMETER UpdateLatestMicrocode +When set to $true, will download and install the latest microcode updates for applicable Windows releases (e.g., Windows Server 2016/2019, Windows 10 LTSC 2016/2019) into the FFU. Default is $false. + .PARAMETER UpdateLatestMSRT When set to $true, will download and install the latest Windows Malicious Software Removal Tool. Default is $false. @@ -224,13 +233,36 @@ param( [Parameter(Mandatory = $false, Position = 0)] [ValidateScript({ Test-Path $_ })] [string]$ISOPath, - [ValidateSet('Home', 'Home N', 'Home Single Language', 'Education', 'Education N', 'Pro', 'Pro N', 'Pro Education', 'Pro Education N', 'Pro for Workstations', 'Pro N for Workstations', 'Enterprise', 'Enterprise N', 'Standard', 'Standard (Desktop Experience)', 'Datacenter', 'Datacenter (Desktop Experience)')] + [ValidateSet( + 'Home', + 'Home N', + 'Home Single Language', + 'Education', + 'Education N', + 'Pro', + 'Pro N', + 'Pro Education', + 'Pro Education N', + 'Pro for Workstations', + 'Pro N for Workstations', + 'Enterprise', + 'Enterprise N', + 'Enterprise 2016 LTSB', + 'Enterprise N 2016 LTSB', + 'Enterprise LTSC', + 'Enterprise N LTSC', + 'IoT Enterprise LTSC', + 'IoT Enterprise N LTSC', + 'Standard', + 'Standard (Desktop Experience)', + 'Datacenter', + 'Datacenter (Desktop Experience)' + )] [string]$WindowsSKU = 'Pro', [ValidateScript({ Test-Path $_ })] [string]$FFUDevelopmentPath = $PSScriptRoot, - [bool]$InstallApps, - + [string]$AppListPath, [hashtable]$AppsScriptVariables, [bool]$InstallOffice, [ValidateSet('Microsoft', 'Dell', 'HP', 'Lenovo')] @@ -284,7 +316,7 @@ param( [string]$ProductKey, [bool]$BuildUSBDrive, [Parameter(Mandatory = $false)] - [ValidateSet(10, 11, 2016, 2019, 2022, 2025)] + [ValidateSet(10, 11, 2016, 2019, 2021, 2022, 2024, 2025)] [int]$WindowsRelease = 11, [Parameter(Mandatory = $false)] [string]$WindowsVersion = '24h2', @@ -321,6 +353,7 @@ param( [bool]$RemoveFFU, [bool]$UpdateLatestCU, [bool]$UpdatePreviewCU, + [bool]$UpdateLatestMicrocode, [bool]$UpdateLatestNet, [bool]$UpdateLatestDefender, [bool]$UpdateLatestMSRT, @@ -359,9 +392,10 @@ param( [ValidateScript({ $_ -eq $null -or (Test-Path $_) })] [string]$ConfigFile, [Parameter(Mandatory = $false)] - [string]$ExportConfigFile + [string]$ExportConfigFile, + [bool]$UpdateADK = $true ) -$version = '2412.3' +$version = '2505.1' # If a config file is specified and it exists, load it if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { @@ -383,12 +417,12 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { } # If this is the Headers parameter, convert PSCustomObject to hashtable - if ($key -eq 'Headers' -and $value -is [System.Management.Automation.PSCustomObject]) { - $headers = [hashtable]::new() + if ((($key -eq 'Headers') -or ($key -eq 'AppsScriptVariables')) -and ($value -is [System.Management.Automation.PSCustomObject])) { + $hashtableValue = [hashtable]::new() foreach ($prop in $value.psobject.Properties) { - $headers[$prop.Name] = $prop.Value + $hashtableValue[$prop.Name] = $prop.Value } - $value = $headers + $value = $hashtableValue } # Check if this key matches a parameter in the script @@ -400,6 +434,53 @@ if ($ConfigFile -and (Test-Path -Path $ConfigFile)) { } } +# Validate that the selected Windows SKU is compatible with the chosen Windows release and ensure an ISO is provided for unsupported releases +$clientSKUs = @( + 'Home', + 'Home N', + 'Home Single Language', + 'Education', + 'Education N', + 'Pro', + 'Pro N', + 'Pro Education', + 'Pro Education N', + 'Pro for Workstations', + 'Pro N for Workstations', + 'Enterprise', + 'Enterprise N' +) +$LTSCSKUs = @( + 'Enterprise 2016 LTSB', + 'Enterprise N 2016 LTSB', + 'Enterprise LTSC', + 'Enterprise N LTSC', + 'IoT Enterprise LTSC', + 'IoT Enterprise N LTSC' +) +$ServerSKUs = @( + 'Standard', + 'Standard (Desktop Experience)', + 'Datacenter', + 'Datacenter (Desktop Experience)' +) +$releaseToSKUMapping = @{ + 10 = $clientSKUs + 11 = $clientSKUs + 2016 = $LTSCSKUs + $ServerSKUs + 2019 = $LTSCSKUs + $ServerSKUs + 2021 = $LTSCSKUs + 2022 = $ServerSKUs + 2024 = $LTSCSKUs + 2025 = $ServerSKUs +} +if ($releaseToSKUMapping.ContainsKey($WindowsRelease) -and $WindowsSKU -notin $releaseToSKUMapping[$WindowsRelease]) { + throw "Selected SKU is $WindowsSKU. Windows $WindowsRelease requires one of these SKUs: $($releaseToSKUMapping[$WindowsRelease] -join ', ')" +} +if ($WindowsRelease -notin 10, 11 -and -not $ISOPath) { + throw "Windows $WindowsRelease cannot automatically be downloaded. Please specify your own ISO using the -ISOPath parameter." +} + #Class definition for vhdx cache class VhdxCacheUpdateItem { [string]$Name @@ -451,6 +532,7 @@ if (-not $VHDXPath) { $VHDXPath = "$VMPath\$VMName.vhdx" } if (-not $FFUCaptureLocation) { $FFUCaptureLocation = "$FFUDevelopmentPath\FFU" } if (-not $LogFile) { $LogFile = "$FFUDevelopmentPath\FFUDevelopment.log" } if (-not $KBPath) { $KBPath = "$FFUDevelopmentPath\KB" } +if (-not $MicrocodePath) { $MicrocodePath = "$KBPath\Microcode" } if (-not $DefenderPath) { $DefenderPath = "$AppsPath\Defender" } if (-not $MSRTPath) { $MSRTPath = "$AppsPath\MSRT" } if (-not $OneDrivePath) { $OneDrivePath = "$AppsPath\OneDrive" } @@ -461,7 +543,7 @@ if (-not $UnattendFolder) { $UnattendFolder = "$FFUDevelopmentPath\Unattend" } if (-not $AutopilotFolder) { $AutopilotFolder = "$FFUDevelopmentPath\Autopilot" } if (-not $PEDriversFolder) { $PEDriversFolder = "$FFUDevelopmentPath\PEDrivers" } if (-not $VHDXCacheFolder) { $VHDXCacheFolder = "$FFUDevelopmentPath\VHDXCache" } -if (-not $installationType) { $installationType = if ($WindowsRelease.ToString().Length -eq 2) { 'Client' } else { 'Server' } } +if (-not $installationType) { $installationType = if ($WindowsSKU -like "Standard*" -or $WindowsSKU -like "Datacenter*") { 'Server' } else { 'Client' } } if ($installationType -eq 'Server'){ #Map $WindowsRelease to $WindowsVersion for Windows Server switch ($WindowsRelease) { @@ -471,6 +553,17 @@ if ($installationType -eq 'Server'){ 2025 { $WindowsVersion = '24H2' } } } +if (-not $AppListPath) { $AppListPath = "$AppsPath\AppList.json" } + +if ($WindowsSKU -like "*LTS*") { + switch ($WindowsRelease) { + 2016 { $WindowsVersion = '1607' } + 2019 { $WindowsVersion = '1809' } + 2021 { $WindowsVersion = '21H2' } + 2024 { $WindowsVersion = '24H2' } + } + $isLTSC = $true +} #FUNCTIONS function WriteLog($LogText) { @@ -1732,18 +1825,21 @@ function Confirm-ADKVersionIsLatest { function Get-ADK { # Check if latest ADK and WinPE add-on are installed - $latestADKInstalled = Confirm-ADKVersionIsLatest -ADKOption "Windows ADK" - $latestWinPEInstalled = Confirm-ADKVersionIsLatest -ADKOption "WinPE add-on" + if ($UpdateADK) { + WriteLog "Checking if latest ADK and WinPE add-on are installed" + $latestADKInstalled = Confirm-ADKVersionIsLatest -ADKOption "Windows ADK" + $latestWinPEInstalled = Confirm-ADKVersionIsLatest -ADKOption "WinPE add-on" - # Uninstall older versions and install latest versions if necessary - if (-not $latestADKInstalled) { - Uninstall-ADK -ADKOption "Windows ADK" - Install-ADK -ADKOption "Windows ADK" - } + # Uninstall older versions and install latest versions if necessary + if (-not $latestADKInstalled) { + Uninstall-ADK -ADKOption "Windows ADK" + Install-ADK -ADKOption "Windows ADK" + } - if (-not $latestWinPEInstalled) { - Uninstall-ADK -ADKOption "WinPE add-on" - Install-ADK -ADKOption "WinPE add-on" + if (-not $latestWinPEInstalled) { + Uninstall-ADK -ADKOption "WinPE add-on" + Install-ADK -ADKOption "WinPE add-on" + } } # Define registry path @@ -1811,7 +1907,7 @@ function Get-WindowsESD { elseif ($WindowsRelease -eq 11) { 'https://go.microsoft.com/fwlink/?LinkId=2156292' } else { - throw "Downloading Windows Server is not supported. Please use the -ISOPath parameter to specify the path to the Windows Server ISO file." + throw "Downloading Windows $WindowsRelease is not supported. Please use the -ISOPath parameter to specify the path to the Windows $WindowsRelease ISO file." } # Download cab file @@ -2429,23 +2525,30 @@ function Get-KBLink { $VerbosePreference = 'SilentlyContinue' $results = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=$Name" -Headers $Headers -UserAgent $UserAgent $VerbosePreference = $OriginalVerbosePreference + + # Extract the first KB article ID from the HTML content and store it globally + # Edge and Defender do not have KB article IDs + if ($Name -notmatch 'Defender|Edge') { + if ($results.Content -match '>\s*([^\(<]+)\(KB(\d+)\)\s*<') { + $kbArticleID = "KB$($matches[2])" + $global:LastKBArticleID = $kbArticleID + WriteLog "Found KB article ID: $kbArticleID" + } + else { + WriteLog "No KB article ID found in search results." + $global:LastKBArticleID = $null + } + } + $kbids = $results.InputFields | Where-Object { $_.type -eq 'Button' -and $_.Value -eq 'Download' } | Select-Object -ExpandProperty ID - # Write-Verbose -Message "$kbids" - if (-not $kbids) { Write-Warning -Message "No results found for $Name" return } - # $guids = $results.Links | - # Where-Object ID -match '_link' | - # Where-Object { $_.OuterHTML -match ( "(?=.*" + ( $Filter -join ")(?=.*" ) + ")" ) } | - # ForEach-Object { $_.id.replace('_link', '') } | - # Where-Object { $_ -in $kbids } - $guids = $results.Links | Where-Object ID -match '_link' | Where-Object { $_.OuterHTML -match ( "(?=.*" + ( $Filter -join ")(?=.*" ) + ")" ) } | @@ -2522,41 +2625,63 @@ function Save-KB { [string[]]$Name, [string]$Path ) - - if ($WindowsArch -eq 'x64') { - [array]$WindowsArch = @("x64", "amd64") - } foreach ($kb in $name) { $links = Get-KBLink -Name $kb foreach ($link in $links) { - if (!($link -match 'x64' -or $link -match 'amd64' -or $link -match 'x86' -or $link -match 'arm64')) { - WriteLog "No architecture found in $link, skipping" - continue - } + # if (!($link -match 'x64' -or $link -match 'amd64' -or $link -match 'x86' -or $link -match 'arm64')) { + # WriteLog "No architecture found in $link, skipping" + # continue + # } if ($link -match 'x64' -or $link -match 'amd64') { - if($WindowsArch -is [array]) { - if ($link -match $WindowsArch[0] -or $link -match $WindowsArch[1]) { - Writelog "Downloading $Link for $WindowsArch to $Path" - Start-BitsTransferWithRetry -Source $link -Destination $Path - $fileName = ($link -split '/')[-1] - Writelog "Returning $fileName" - #With Windows 11 24H2 and Checkpoint CUs, there are multiple files that are downloaded - # break - } + if ($WindowsArch -eq 'x64') { + Writelog "Downloading $link for $WindowsArch to $Path" + Start-BitsTransferWithRetry -Source $link -Destination $Path + $fileName = ($link -split '/')[-1] + Writelog "Returning $fileName" } } - if ($link -match 'arm64') { + elseif ($link -match 'arm64') { if ($WindowsArch -eq 'arm64') { Writelog "Downloading $Link for $WindowsArch to $Path" Start-BitsTransferWithRetry -Source $link -Destination $Path $fileName = ($link -split '/')[-1] Writelog "Returning $fileName" - #With Windows 11 24H2 and Checkpoint CUs, there are multiple files that are downloaded - # break } - } + } + elseif ($link -match 'x86') { + if ($WindowsArch -eq 'x86') { + Writelog "Downloading $link for $WindowsArch to $Path" + Start-BitsTransferWithRetry -Source $link -Destination $Path + $fileName = ($link -split '/')[-1] + Writelog "Returning $fileName" + } + + } + else { + WriteLog "No architecture found in $link" + + #If no architecture is found, download the file and run it through Get-PEArchitecture to determine the architecture + Writelog "Downloading $link to $Path and analyzing file for architecture" + Start-BitsTransferWithRetry -Source $link -Destination $Path + + #Take the file and run it through Get-PEArchitecture to determine the architecture + $fileName = ($link -split '/')[-1] + $filePath = Join-Path -Path $Path -ChildPath $fileName + $arch = Get-PEArchitecture -FilePath $filePath + Writelog "$fileName is $arch" + #If the architecture matches $WindowsArch, keep the file, otherwise delete it + if ($arch -eq $WindowsArch) { + Writelog "Architecture for $fileName matches $WindowsArch, keeping file" + return $fileName + } + else { + Writelog "Deleting $fileName, architecture does not match" + Remove-Item -Path $filePath -Force + } + } + } } return $fileName @@ -2621,6 +2746,10 @@ function Get-WimIndex { 'Pro_Edu_N' { 9 } 'Pro_WKS' { 10 } 'Pro_WKS_N' { 11 } + 'Enterprise' { 3 } + 'Enterprise N' { 4 } + 'Enterprise LTSC' { 1 } + 'Enterprise N LTSC' { 2 } Default { 6 } } } @@ -3133,43 +3262,57 @@ function Get-ShortenedWindowsSKU { ) $shortenedWindowsSKU = switch ($WindowsSKU) { 'Core' { 'Home' } - 'CoreN' { 'Home_N' } - 'CoreSingleLanguage' { 'Home_SL' } - 'Education' { 'Edu' } - 'EducationN' { 'Edu_N' } - 'Professional' { 'Pro' } - 'ProfessionalN' { 'Pro_N' } - 'ProfessionalEducation' { 'Pro_Edu' } - 'ProfessionalEducationN' { 'Pro_Edu_N' } - 'ProfessionalWorkstation' { 'Pro_WKS' } - 'ProfessionalWorkstationN' { 'Pro_WKS_N' } - 'Enterprise' { 'Ent' } - 'EnterpriseN' { 'Ent_N' } - 'ServerStandard' { 'Srv_Std' } - 'ServerDatacenter' { 'Srv_Dtc' } 'Home' { 'Home' } + 'CoreN' { 'Home_N' } 'Home N' { 'Home_N' } + 'CoreSingleLanguage' { 'Home_SL' } 'Home Single Language' { 'Home_SL' } 'Education' { 'Edu' } + 'EducationN' { 'Edu_N' } 'Education N' { 'Edu_N' } 'Professional' { 'Pro' } + 'Pro' { 'Pro' } + 'ProfessionalN' { 'Pro_N' } 'Pro N' { 'Pro_N' } + 'ProfessionalEducation' { 'Pro_Edu' } 'Pro Education' { 'Pro_Edu' } + 'ProfessionalEducationN' { 'Pro_Edu_N' } 'Pro Education N' { 'Pro_Edu_N' } + 'ProfessionalWorkstation' { 'Pro_WKS' } 'Pro for Workstations' { 'Pro_WKS' } + 'ProfessionalWorkstationN' { 'Pro_WKS_N' } 'Pro N for Workstations' { 'Pro_WKS_N' } 'Enterprise' { 'Ent' } + 'EnterpriseN' { 'Ent_N' } 'Enterprise N' { 'Ent_N' } + 'Enterprise N LTSC' { 'Ent_N_LTSC' } + 'EnterpriseS' { 'Ent_LTSC' } + 'EnterpriseSN' { 'Ent_N_LTSC' } + 'Enterprise LTSC' { 'Ent_LTSC' } + 'Enterprise 2016 LTSB' { 'Ent_LTSC' } + 'Enterprise N 2016 LTSB' { 'Ent_N_LTSC' } + 'IoT Enterprise LTSC' { 'IoT_Ent_LTSC' } + 'IoTEnterpriseS' { 'IoT_Ent_LTSC' } + 'IoT Enterprise N LTSC' { 'IoT_Ent_N_LTSC' } + 'ServerStandard' { 'Srv_Std' } 'Standard' { 'Srv_Std' } - 'Standard (Desktop Experience)' { 'Srv_Std_DE' } + 'ServerDatacenter' { 'Srv_Dtc' } 'Datacenter' { 'Srv_Dtc' } + 'Standard (Desktop Experience)' { 'Srv_Std_DE' } 'Datacenter (Desktop Experience)' { 'Srv_Dtc_DE' } } return $shortenedWindowsSKU } function New-FFUFileName { - + + # $Winverinfo.name will be either Win10 or Win11 for client OSes + # Since WindowsRelease now includes dates, it breaks default name template in the config file + # This should keep in line with the naming that's done via VM Captures + if ($installationType -eq 'Client' -and $winverinfo) { + $WindowsRelease = $winverinfo.name + } + $BuildDate = Get-Date -uformat %b%Y # Replace '{WindowsRelease}' with the Windows release (e.g., 10, 11, 2016, 2019, 2022, 2025) $CustomFFUNameTemplate = $CustomFFUNameTemplate -replace '{WindowsRelease}', $WindowsRelease @@ -3406,13 +3549,31 @@ Function Get-WindowsVersionInfo { [int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild' WriteLog "Windows Build: $CurrentBuild" #DisplayVersion does not exist for 1607 builds (RS1 and Server 2016) and Server 2019 - if($CurrentBuild -notin (14393, 17763)) { - $DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' - WriteLog "Windows Version: $DisplayVersion" + if ($CurrentBuild -notin (14393, 17763)) { + $DisplayVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' + WriteLog "Windows Version: $DisplayVersion" + } + # For Windows 10 LTSC 2019, set DisplayVersion to 2019 + if ($CurrentBuild -eq 17763 -and $InstallationType -eq "Client") { + $DisplayVersion = '2019' } $BuildDate = Get-Date -uformat %b%Y + # $SKU = switch ($SKU) { + # Core { 'Home' } + # Professional { 'Pro' } + # ProfessionalEducation { 'Pro_Edu' } + # Enterprise { 'Ent' } + # EnterpriseS { 'Ent_LTSC' } + # IoTEnterpriseS { 'IoT_Ent_LTSC' } + # Education { 'Edu' } + # ProfessionalWorkstation { 'Pro_Wks' } + # ServerStandard { 'Srv_Std' } + # ServerDatacenter { 'Srv_Dtc' } + # } + # WriteLog "Windows SKU Modified to: $SKU" + # $WindowsSKU = switch ($WindowsSKU) { # Core { 'Home' } # Professional { 'Pro' } @@ -3420,7 +3581,7 @@ Function Get-WindowsVersionInfo { # Enterprise { 'Ent' } # Education { 'Edu' } # ProfessionalWorkstation { 'Pro_Wks' } - # ServerStandard { 'Srv_Std' } + # ServerStandard { 'Srv_Std' } # ServerDatacenter { 'Srv_Dtc' } # } @@ -4010,6 +4171,37 @@ function Export-ConfigFile{ # Convert to JSON and save $orderedParams | ConvertTo-Json | Out-File $ExportConfigFile -Force } +function Get-PEArchitecture { + param( + [string]$FilePath + ) + + # Read the entire file as bytes. + $bytes = [System.IO.File]::ReadAllBytes($FilePath) + + # Check for the 'MZ' signature. + if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) { + throw "The file is not a valid PE file." + } + + # The PE header offset is stored at offset 0x3C. + $peHeaderOffset = [System.BitConverter]::ToInt32($bytes, 0x3C) + + # Verify the PE signature "PE\0\0". + if ($bytes[$peHeaderOffset] -ne 0x50 -or $bytes[$peHeaderOffset + 1] -ne 0x45) { + throw "Invalid PE header." + } + + # The Machine field is located immediately after the PE signature. + $machine = [System.BitConverter]::ToUInt16($bytes, $peHeaderOffset + 4) + + switch ($machine) { + 0x014c { return "x86" } + 0x8664 { return "x64" } + 0xAA64 { return "ARM64" } + default { return ("Unknown architecture: 0x{0:X}" -f $machine) } + } +} ###END FUNCTIONS @@ -4307,9 +4499,9 @@ if ($InstallApps) { exit } WriteLog "$AppsPath\InstallAppsandSysprep.cmd found" - If (Test-Path -Path "$AppsPath\AppList.json"){ - WriteLog "$AppsPath\AppList.json found, checking for winget apps to install" - Get-Apps -AppList "$AppsPath\AppList.json" + If (Test-Path -Path $AppListPath){ + WriteLog "$AppListPath found, checking for winget apps to install" + Get-Apps -AppList "$AppListPath" } if (-not $InstallOffice) { @@ -4372,40 +4564,46 @@ if ($InstallApps) { Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent WriteLog "Update complete" - ###### 9/4/2024 - Windows Security Platform update is no longer available from Update Catalog. Will change to using + ###### 5/20/2025 - Security Platform URLs are not available for download, will go back to using the Microsoft Update Catalog in UI build ###### https://support.microsoft.com/en-us/topic/windows-security-update-a6ac7d2e-b1bf-44c0-a028-41720a242da3 #Download Windows Security Platform Update - WriteLog "Downloading Windows Security Platform Update" - if ($WindowsArch -eq 'x64') { - $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/x64/securityhealthsetup.exe' - } - if ($WindowsArch -eq 'ARM64') { - $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/arm64/securityhealthsetup.exe' - } - try { - WriteLog "Windows Security Platform Update URL is $securityPlatformURL" - Start-BitsTransferWithRetry -Source $securityPlatformURL -Destination "$DefenderPath\securityhealthsetup.exe" - WriteLog "Windows Security Platform Update downloaded to $DefenderPath\securityhealthsetup.exe" - } - catch { - Write-Host "Downloading Windows Security Platform Update Failed" - WriteLog "Downloading Windows Security Platform Update Failed with error $_" - throw $_ - } - # Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Windows Security Platform Update - WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Windows Security Platform Update" - $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" - $UpdatedcmdContent = $CmdContent -replace '^(REM Install Windows Security Platform Update)', ("REM Install Windows Security Platform Update`r`nd:\Defender\securityhealthsetup.exe") - Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent - WriteLog "Update complete" + # WriteLog "Downloading Windows Security Platform Update" + # if ($WindowsArch -eq 'x64') { + # $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/x64/securityhealthsetup.exe' + # } + # if ($WindowsArch -eq 'ARM64') { + # $securityPlatformURL = 'https://definitionupdates.microsoft.com/download/DefinitionUpdates/windowssecurity/10.0.27703.1006/arm64/securityhealthsetup.exe' + # } + # try { + # WriteLog "Windows Security Platform Update URL is $securityPlatformURL" + # Start-BitsTransferWithRetry -Source $securityPlatformURL -Destination "$DefenderPath\securityhealthsetup.exe" + # WriteLog "Windows Security Platform Update downloaded to $DefenderPath\securityhealthsetup.exe" + # } + # catch { + # Write-Host "Downloading Windows Security Platform Update Failed" + # WriteLog "Downloading Windows Security Platform Update Failed with error $_" + # throw $_ + # } + # # Modify InstallAppsandSysprep.cmd to add in $KBFilePath on the line after REM Install Windows Security Platform Update + # WriteLog "Updating $AppsPath\InstallAppsandSysprep.cmd to include Windows Security Platform Update" + # $CmdContent = Get-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" + # $UpdatedcmdContent = $CmdContent -replace '^(REM Install Windows Security Platform Update)', ("REM Install Windows Security Platform Update`r`nd:\Defender\securityhealthsetup.exe") + # Set-Content -Path "$AppsPath\InstallAppsandSysprep.cmd" -Value $UpdatedcmdContent + # WriteLog "Update complete" } if ($UpdateLatestMSRT) { WriteLog "`$UpdateLatestMSRT is set to true." if ($WindowsArch -eq 'x64') { - if ($installationType -eq 'client') { + if ($WindowsRelease -in 10, 11) { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows $WindowsRelease""" } + elseif ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 10""" + } + elseif ($WindowsRelease -in 2024 -and $isLTSC) { + $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows 11""" + } #Windows Server 2025 isn't listed as a product in the Microsoft Update Catalog, so we'll use the 2019 version elseif ($installationType -eq 'server' -and $WindowsRelease -eq '24H2') { $Name = """Windows Malicious Software Removal Tool x64""" + " " + """Windows Server 2019""" @@ -4542,7 +4740,7 @@ try { #The Windows release info page is updated later than the MU Catalog if ($UpdateLatestCU -and -not $UpdatePreviewCU) { Writelog "`$UpdateLatestCU is set to true, checking for latest CU" - if ($installationType -eq 'Client') { + if ($WindowsRelease -in 10, 11) { $Name = """Cumulative update for Windows $WindowsRelease Version $WindowsVersion for $WindowsArch""" } if ($WindowsRelease -eq 2025) { @@ -4551,30 +4749,67 @@ try { if ($WindowsRelease -eq 2022) { $Name = """Cumulative Update for Microsoft server operating system, version 21h2 for $WindowsArch""" } - if ($WindowsRelease -in 2016, 2019) { + if ($WindowsRelease -in 2016, 2019 -and $installationType -eq "Server") { $Name = """Cumulative update for Windows Server $WindowsRelease for $WindowsArch""" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + $today = Get-Date + $firstDayOfMonth = Get-Date -Year $today.Year -Month $today.Month -Day 1 + $secondTuesday = $firstDayOfMonth.AddDays(((2 - [int]$firstDayOfMonth.DayOfWeek + 7) % 7) + 7) + $updateDate = if ($today -gt $secondTuesday) { $today } else { $today.AddMonths(-1) } + # More precise search to prevent Dynamic cumulative update from being chosen. + $Name = """$($updateDate.ToString('yyyy-MM')) Cumulative update for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2024 -and $isLTSC) { + $Name = """Cumulative update for Windows 11 Version $WindowsVersion for $WindowsArch""" + } #Check if $KBPath exists, if not, create it If (-not (Test-Path -Path $KBPath)) { WriteLog "Creating $KBPath" New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } #Get latest Servicing Stack Update for Windows Server 2016 - if ($WindowsRelease -eq 2016) { + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { $SSUName = """Servicing stack update for Windows Server $WindowsRelease for $WindowsArch""" WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" $SSUFile = Save-KB -Name $SSUName -Path $KBPath $SSUFilePath = "$KBPath\$SSUFile" WriteLog "Latest SSU saved to $SSUFilePath" } + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" + WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" + $SSUFile = Save-KB -Name $SSUName -Path $KBPath + $SSUFilePath = "$KBPath\$SSUFile" + WriteLog "Latest SSU saved to $SSUFilePath" + } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest CU saved to $KBPath\$KBFilePath" + $CUFileName = Save-KB -Name $Name -Path $KBPath + # Check if $CUFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $CUFileName + # This is because checkpoint CUs download indeterministically + WriteLog "Checking if $CUFileName contains $global:LastKBArticleID" + if ($CUFileName -notmatch $global:LastKBArticleID) { + WriteLog "$CUFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $CUFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $CUFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $CUFileName) { + WriteLog "Found $CUFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $CUPath = "$KBPath\$CUFileName" + WriteLog "Latest CU saved to $CUPath" } #Update Latest Preview Cumlative Update for Client OS only #will take Precendence over $UpdateLatestCU if both were set to $true - if ($UpdatePreviewCU -and $installationType -eq 'Client') { + if ($UpdatePreviewCU -and $installationType -eq 'Client' -and $WindowsSKU -notlike "*LTSC") { Writelog "`$UpdatePreviewCU is set to true, checking for latest Preview CU" $Name = """Cumulative update Preview for Windows $WindowsRelease Version $WindowsVersion for $WindowsArch""" #Check if $KBPath exists, if not, create it @@ -4583,36 +4818,174 @@ try { New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest Preview CU saved to $KBPath\$KBFilePath" + $CUPFileName = Save-KB -Name $Name -Path $KBPath + # Check if $CUPFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $CUPFileName + # This is because checkpoint CUs download indeterministically + WriteLog "Checking if $CUPFileName contains $global:LastKBArticleID" + if ($CUPFileName -notmatch $global:LastKBArticleID) { + WriteLog "$CUPFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $CUPFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $CUPFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $CUPFileName) { + WriteLog "Found $CUPFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $CUPPath = "$KBPath\$CUPFileName" + WriteLog "Latest CU Preview saved to $CUPPath" } #Update Latest .NET Framework if ($UpdateLatestNet) { Writelog "`$UpdateLatestNet is set to true, checking for latest .NET Framework" - if ($installationType -eq 'Client') { - $Name = "Cumulative update for .net framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" - } - if ($WindowsRelease -eq 2025) { - $Name = """Cumulative Update for .NET Framework"" ""3.5 and 4.8.1"" for Windows 11 24H2 x64 -preview" - } - if ($WindowsRelease -eq 2022) { - $Name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1"" ""operating system version 21H2 for x64""" - } - if ($WindowsRelease -eq 2019) { - $Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64""" - } - if ($WindowsRelease -eq 2016) { - $Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64""" - } #Check if $KBPath exists, if not, create it - If (-not (Test-Path -Path $KBPath)) { + if (-not (Test-Path -Path $KBPath)) { WriteLog "Creating $KBPath" New-Item -Path $KBPath -ItemType Directory -Force | Out-Null } - WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" - $KBFilePath = Save-KB -Name $Name -Path $KBPath - WriteLog "Latest .NET saved to $KBPath\$KBFilePath" + + ###### + #LTSC# + ###### + + # For Windows 10 LTSC editions (2016, 2019, 2021), download and save the latest Servicing Stack Update (SSU) and .NET Framework cumulative update(s) + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + # SSU likely was downloaded via CU, but still needed here if .net is being updated, no need to download twice though + if ($null -eq $SSUFile) { + $SSUName = """Servicing Stack Update for Windows 10 Version $WindowsVersion for $WindowsArch""" + WriteLog "Searching for $SSUName from Microsoft Update Catalog and saving to $KBPath" + $SSUFile = Save-KB -Name $SSUName -Path $KBPath + $SSUFilePath = "$KBPath\$SSUFile" + WriteLog "Latest SSU saved to $SSUFilePath" + } + + # For Windows 10 LTSC editions (2016, 2019, 2021), download and save the latest .NET Framework cumulative update(s) + # to a dedicated NET subdirectory, as these editions may include multiple .NET updates that need to be installed together. + if ($WindowsRelease -in 2016) { + $name = """Cumulative Update for .NET Framework 4.8 for Windows 10 version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2019) { + $name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + if ($WindowsRelease -eq 2021){ + $name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1 for Windows 10 Version $WindowsVersion for $WindowsArch""" + } + + $NETPath = Join-Path -Path $KBPath -ChildPath "NET" + if (-not (Test-Path -Path $NETPath)) { + WriteLog "Creating $NETPath" + New-Item -Path $NETPath -ItemType Directory -Force | Out-Null + } + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $NETPath" + $NETFileName = Save-KB -Name $name -Path $NETPath + WriteLog "Latest .NET Framework cumulative update saved to $NETPath\$NETFileName" + } + + # For Windows 11 LTSC 2024, set the update name to search for the latest .NET Framework cumulative update in the Microsoft Update Catalog + if ($WindowsRelease -eq 2024 -and $isLTSC) { + $Name = "Cumulative update for .NET framework windows 11 $WindowsVersion $WindowsArch -preview" + } + + # For Windows 10 LTSC 2021, download and save the latest .NET Framework 4.8.1 feature pack to the NET subdirectory. + if ($WindowsRelease -eq 2021 -and $isLTSC) { + WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" + $NETFeatureName = """Microsoft .NET Framework 4.8.1 for Windows 10 Version 21H2 for x64""" + $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath + WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" + } + # For Windows 10 LTSC 2016 and 2019, download and save the latest .NET Framework 4.8 feature pack to the NET subdirectory. + if ($WindowsRelease -in 2016, 2019 -and $isLTSC) { + WriteLog "Checking for latest .NET Framework feature pack for Windows $WindowsRelease $WindowsSKU" + $NETFeatureName = """Microsoft .NET Framework 4.8 for Windows 10 Version $WindowsVersion and Windows Server $WindowsRelease for x64""" + $NETFeaturePackFile = Save-KB -Name $NETFeatureName -Path $NETPath + WriteLog "Latest .NET Framework Feature pack saved to $NETPath\$NETFeaturePackFile" + } + + ######## + #CLIENT# + ######## + + # For Windows 10 and 11, set the update name to search for the latest .NET Framework cumulative update (excluding preview) in the Microsoft Update Catalog + if ($WindowsRelease -in 10, 11) { + $Name = "Cumulative update for .NET framework windows $WindowsRelease $WindowsVersion $WindowsArch -preview" + } + + ######## + #SERVER# + ######## + + # For Windows Server 2025, set the update name to search for the latest .NET Framework cumulative update (excluding preview) in the Microsoft Update Catalog + if ($WindowsRelease -eq 2025 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework"" ""3.5 and 4.8.1"" for Windows 11 24H2 x64 -preview" + } + + # For Windows Server 2022, set the update name to search for the latest .NET Framework cumulative update (3.5, 4.8, and 4.8.1) for OS version 21H2 x64 + if ($WindowsRelease -eq 2022 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 3.5, 4.8 and 4.8.1"" ""operating system version 21H2 for x64""" + } + # For Windows Server 2019, set the update name to search for the latest .NET Framework cumulative update (3.5, 4.7.2, and 4.8) for x64 + if ($WindowsRelease -eq 2019 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64""" + } + + # For Windows Server 2016, set the update name to search for the latest .NET Framework 4.8 cumulative update for x64 + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { + $Name = """Cumulative Update for .NET Framework 4.8 for Windows Server 2016 for x64""" + } + + # For all editions except Windows 10 LTSC (2016, 2019, 2021), search for the latest .NET Framework cumulative update in the Microsoft Update Catalog, + # download it to $KBPath, and verify the correct file was downloaded by matching the KB article ID. If not found, search for the file by KB article ID. + if (-not ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC)) { + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $KBPath" + $NETFileName = Save-KB -Name $Name -Path $KBPath + # Check if $NETFileName contains the string in $global:LastKBArticleID + # If it does not, look in $KBPath for the file that contains the string in $global:LastKBArticleID + # and set that as the $NETFileName + WriteLog "Checking if $NETFileName contains $global:LastKBArticleID" + if ($NETFileName -notmatch $global:LastKBArticleID) { + WriteLog "$NETFileName does not contain $global:LastKBArticleID, searching for file that contains it" + $NETFileName = $null + # Get the file that contains the string in $global:LastKBArticleID + $NETFileName = (Get-ChildItem -Path $KBPath -Filter "*$global:LastKBArticleID*" | Select-Object -First 1).Name + if ($null -ne $NETFileName) { + WriteLog "Found $NETFileName" + } + else { + WriteLog "Could not find file that contains $global:LastKBArticleID" + throw "Could not find file that contains $global:LastKBArticleID" + } + } + $NETPath = "$KBPath\$NETFileName" + WriteLog "Latest .NET Framework saved to $NETPath" + } + } + # Update latest Microcode + if ($UpdateLatestMicrocode -and $WindowsRelease -in 2016, 2019) { + WriteLog "`$UpdateLatestMicrocode is set to true, checking for latest Microcode" + #Check if $MicrocodePath exists, if not, create it + If (-not (Test-Path -Path $MicrocodePath)) { + WriteLog "Creating $MicrocodePath" + New-Item -Path $MicrocodePath -ItemType Directory -Force | Out-Null + } + + # Windows 10 LTSC 2016 (1607) and Windows Server 2016 + if($WindowsRelease -eq 2016){ + $name = "KB4589210 $windowsArch" + } + + # Windows 10 LTSC 2019 (1809) and Windows Server 2019 + if($WindowsRelease -eq 2019){ + $name = "KB4589208 $windowsArch" + } + WriteLog "Searching for $name from Microsoft Update Catalog and saving to $MicrocodePath" + $MicrocodeFileName = Save-KB -Name $name -Path $MicrocodePath + WriteLog "Latest Microcode saved to $MicrocodePath\$MicrocodeFileName" } #Search for cached VHDX and skip VHDX creation if there's a cached version @@ -4623,7 +4996,7 @@ try { $vhdxJsons = @(Get-ChildItem -File -Path $VHDXCacheFolder -Filter '*_config.json' | Sort-Object -Property CreationTime -Descending) WriteLog "Found $($vhdxJsons.Count) cached VHDX files" if (Test-Path -Path $KBPath){ - $downloadedKBs = @(Get-ChildItem -File -Path $KBPath) + $downloadedKBs = @(Get-ChildItem -File -Path $KBPath -Recurse) } else { $downloadedKBs = @() @@ -4719,7 +5092,7 @@ try { WriteLog "Adding KBs to $WindowsPartition" WriteLog 'This can take 10+ minutes depending on how old the media is and the size of the KB. Please be patient' # If WindowsRelease is 2016, we need to add the SSU first - if ($WindowsRelease -eq 2016) { + if ($WindowsRelease -eq 2016 -and $installationType -eq "Server") { WriteLog 'WindowsRelease is 2016, adding SSU first' WriteLog "Adding SSU to $WindowsPartition" # Add-WindowsPackage -Path $WindowsPartition -PackagePath $SSUFilePath -PreventPending | Out-Null @@ -4730,14 +5103,41 @@ try { WriteLog "Removing $SSUFilePath" Remove-Item -Path $SSUFilePath -Force | Out-Null WriteLog 'SSU removed' - WriteLog "Adding CU to $WindowsPartition" } - # Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath -PreventPending | Out-Null - Add-WindowsPackage -Path $WindowsPartition -PackagePath $KBPath | Out-Null + if ($WindowsRelease -in 2016, 2019, 2021 -and $isLTSC) { + WriteLog "WindowsRelease is $WindowsRelease and is $WindowsSKU, adding SSU first" + WriteLog "Adding SSU to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $SSUFilePath | Out-Null + WriteLog "SSU added to $WindowsPartition" + WriteLog "Removing $SSUFilePath" + Remove-Item -Path $SSUFilePath -Force | Out-Null + WriteLog 'SSU removed' + } + # Break out CU and NET updates to be added separately to abide by Checkpoint Update recommendations + if ($UpdateLatestCU) { + WriteLog "Adding $CUPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPath | Out-Null + WriteLog "$CUPath added to $WindowsPartition" + } + if ($UpdatePreviewCU) { + WriteLog "Adding $CUPPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $CUPPath | Out-Null + WriteLog "$CUPPath added to $WindowsPartition" + } + if ($UpdateLatestNet) { + WriteLog "Adding $NETPath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $NETPath | Out-Null + WriteLog "$NETPath added to $WindowsPartition" + } + if ($UpdateLatestMicrocode -and $WindowsRelease -in 2016, 2019) { + WriteLog "Adding $MicrocodePath to $WindowsPartition" + Add-WindowsPackage -Path $WindowsPartition -PackagePath $MicrocodePath | Out-Null + WriteLog "$MicrocodePath added to $WindowsPartition" + } WriteLog "KBs added to $WindowsPartition" if ($AllowVHDXCaching) { $cachedVHDXInfo = [VhdxCacheItem]::new() - $includedUpdates = Get-ChildItem -Path $KBPath -File + $includedUpdates = Get-ChildItem -Path $KBPath -File -Recurse foreach ($includedUpdate in $includedUpdates) { $cachedVHDXInfo.IncludedUpdates += ([VhdxCacheUpdateItem]::new($includedUpdate.Name)) @@ -4942,7 +5342,7 @@ try { } else { #Shorten Windows SKU for use in FFU file name to remove spaces and long names - WriteLog 'Shortening Windows SKU for FFU file name' + WriteLog "Shortening Windows SKU: $WindowsSKU for FFU file name" $shortenedWindowsSKU = Get-ShortenedWindowsSKU -WindowsSKU $WindowsSKU WriteLog "Shortened Windows SKU: $shortenedWindowsSKU" #Create FFU file diff --git a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 index b51ba5a..cbd2d7d 100644 --- a/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 +++ b/FFUDevelopment/WinPECaptureFFUFiles/CaptureFFU.ps1 @@ -1,7 +1,6 @@ #Modify the net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user ddb1f077-3eed-433c-b4d9-7b8cd54ce727 net use W: \\192.168.1.158\FFUCaptureShare /user:ffu_user ddb1f077-3eed-433c-b4d9-7b8cd54ce727 #Custom naming placeholder - $AssignDriveLetter = 'x:\AssignDriveLetter.txt' Start-Process -FilePath diskpart.exe -ArgumentList "/S $AssignDriveLetter" -Wait -ErrorAction Stop | Out-Null #Load Registry Hive @@ -12,26 +11,39 @@ reg load "HKLM\FFU" $Software $SKU = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'EditionID' [int]$CurrentBuild = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'CurrentBuild' -if ($CurrentBuild -notin 14393, 17763) { +$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType' +if ($CurrentBuild -notin 14393, 17763 -and $InstallationType -ne "Server") { $WindowsVersion = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'DisplayVersion' } -$InstallationType = Get-ItemPropertyValue -Path 'HKLM:\FFU\Microsoft\Windows NT\CurrentVersion\' -Name 'InstallationType' +# For Windows 10 LTSB 2016, set WindowsVersion to 2016 +if ($CurrentBuild -eq 14393 -and $InstallationType -eq "Client") { + $WindowsVersion = '2016' +} +# For Windows 10 LTSC 2019, set WindowsVersion to 2019 +if ($CurrentBuild -eq 17763 -and $InstallationType -eq "Client") { + $WindowsVersion = '2019' +} + + $BuildDate = Get-Date -uformat %b%Y $SKU = switch ($SKU) { Core { 'Home' } - CoreN { 'HomeN' } - CoreSingleLanguage { 'HomeSL' } + CoreN { 'Home_N' } + CoreSingleLanguage { 'Home_SL' } Professional { 'Pro' } - ProfessionalN { 'ProN' } + ProfessionalN { 'Pro_N' } ProfessionalEducation { 'Pro_Edu' } - ProfessionalEducationN { 'Pro_EduN' } + ProfessionalEducationN { 'Pro_Edu_N' } Enterprise { 'Ent' } - EnterpriseN { 'EntN' } + EnterpriseN { 'Ent_N' } + EnterpriseS { 'Ent_LTSC' } + EnterpriseSN { 'Ent_N_LTSC' } + IoTEnterpriseS { 'IoT_Ent_LTSC' } Education { 'Edu' } - EducationN { 'EduN' } + EducationN { 'Edu_N' } ProfessionalWorkstation { 'Pro_Wks' } - ProfessionalWorkstationN { 'Pro_WksN' } + ProfessionalWorkstationN { 'Pro_Wks_N' } ServerStandard { 'Srv_Std' } ServerDatacenter { 'Srv_Dtc' } } diff --git a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 index 74746a7..8dc030c 100644 --- a/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 +++ b/FFUDevelopment/WinPEDeployFFUFiles/ApplyFFU.ps1 @@ -135,7 +135,7 @@ $LogFileName = 'ScriptLog.txt' $USBDrive = Get-USBDrive New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null $LogFile = $USBDrive + $LogFilename -$version = '2412.3' +$version = '2505.1' WriteLog 'Begin Logging' WriteLog "Script version: $version" @@ -222,6 +222,7 @@ if (Test-Path -Path $PPKGFolder){ $UnattendFolder = $USBDrive + "unattend\" $UnattendFilePath = $UnattendFolder + "unattend.xml" $UnattendPrefixPath = $UnattendFolder + "prefixes.txt" +$UnattendComputerNamePath = $UnattendFolder + "SerialComputerNames.csv" If (Test-Path -Path $UnattendFilePath){ $UnattendFile = Get-ChildItem -Path $UnattendFilePath If ($UnattendFile){ @@ -234,6 +235,12 @@ If (Test-Path -Path $UnattendPrefixPath){ $UnattendPrefix = $true } } +If (Test-Path -Path $UnattendComputerNamePath){ + $UnattendComputerNameFile = Get-ChildItem -Path $UnattendComputerNamePath + If ($UnattendComputerNameFile){ + $UnattendComputerName = $true + } +} #Ask for device name if unattend exists if ($Unattend -and $UnattendPrefix){ @@ -278,7 +285,25 @@ if ($Unattend -and $UnattendPrefix){ $computername = Set-Computername($computername) Writelog "Computer name set to $computername" } -elseif($Unattend){ +elseif($Unattend -and $UnattendComputerName){ + Writelog 'Unattend file found with SerialComputerNames.csv. Getting name for current computer.' + $SerialComputerNames = Import-Csv -Path $UnattendComputerNameFile.FullName -Delimiter "," + + $SerialNumber = (Get-CimInstance -Class Win32_Bios).SerialNumber + $SCName = $SerialComputerNames | Where-Object { $_.SerialNumber -eq $SerialNumber } + + If ($SCName) { + [string]$computername = $SCName.ComputerName + $computername = Set-Computername($computername) + Writelog "Computer name set to $computername" + } else { + Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.' + [string]$computername = ("FFU-" + (-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 11 | ForEach-Object { [char]$_ }))) + $computername = Set-Computername($computername) + Writelog "Computer name set to $computername" + } +} +elseif($Unattend) { Writelog 'Unattend file found with no prefixes.txt, asking for name' [string]$computername = Read-Host 'Enter device name' Set-Computername($computername) diff --git a/FFUDevelopment/config/Sample_default.json b/FFUDevelopment/config/Sample_default.json index 1390424..175d175 100644 Binary files a/FFUDevelopment/config/Sample_default.json and b/FFUDevelopment/config/Sample_default.json differ