Compare commits

...

19 Commits

Author SHA1 Message Date
rbalsleyMSFT d809fc021f Updated changelog.md 2025-05-21 17:29:12 -07:00
rbalsleyMSFT 582df6e3a8 Merge pull request #165 from rbalsleyMSFT/dev
2505.1
2025-05-21 17:26:25 -07:00
rbalsleyMSFT d5a4f96482 Enhance FFU Development Scripts and Configuration
BuildFFUVM.ps1
- Added parameter definitions that were missing:
  - AppListPath - Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json.
  - PEDriversFolder - Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers.
- Added two new parameters:
  - UpdateLatestMicrocode - This is used for Windows 10/Server. 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.
  - UpdateADK - Added for airgapped scenarios where you've manually updated the ADK and don't need it to continually check. 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.
- Reorganized the WindowsSKU validateset to make it easier to read and added in 2016 LTSB releases
- Changed version to 2505.1
- Reorganized the releasetoMapping SKUs to make it easier to read
- Omitted Defender/Edge from reporting KB ID since neither includes it
- Updated Save-KB with some enhancements from the UI branch which will handle KBs that don't have an architecture defined in their file name that will leverage a new function Get-PEArchitecture that can interrogate the file name and determine the correct architecture
- Updated Get-ShortenedWindowsSKU with LTSB/LTSC SKUs
- Updated New-FFUFileName to use $winverinfo.Name for $WindowsRelease for client OSes to which will set $WindowsRelease to using Win10 or Win11. This fixes a bug where you might see 10 or 11 instead of Win10 or Win11 for FFU builds that use only the VHDX (e.g. `-InstallApps $false`. This keeps the naming consistent with FFUs built via VM.
- Updated Get-WindowsVersionInfo to fix an issue with naming LTSC 2019
- Added Get-PEArchitecture function
- Commented out the Windows Security Platform Update code since the URL is dead for the content. This is fixed in the UI branch and will be reintroduced in Dev and Main at a later date when the UI work is complete.
- Created a new variable `$isLTSC`
- Modified and reorganized the search strings for the various .net framework components. LTSC introduced some complexity with handling the various .net releases.
- VHDXCaching will now recurse the KBPath folder when finding downloaded KBs to include in its config file

Sample_default.json
- Added new/missing parameters
  - ApplistPath
  - UpdateADK
  - UpdateLatestMicrocode

CaptureFFU.ps1
- `$WindowsVersion` 2016 and 2019 for LTSC releases
- Changed some SKU spacing to make things more consistent and included Enterprise N LTSC

ApplyFFU.ps1
- Updated version to 2505.1
2025-05-21 16:44:21 -07:00
rbalsleyMSFT d2fc9cf558 Merge pull request #116 from zehadialam/win-ltsc
Support for Windows LTSC
2025-05-19 11:04:51 -07:00
Zehadi Alam b530ac5a5c Added comment for .NET updates and added condition in CaptureFFU.ps1 to fix naming for LTSC 2019 2025-05-18 22:06:40 -04:00
Zehadi Alam 2659336ee9 Move .NET folder creation code 2025-05-16 00:26:06 -04:00
Zehadi Alam c32a09bfc1 Add .NET update support for Windows LTSC 2025-05-16 00:11:50 -04:00
Zehadi Alam f7ff415374 Merge branch 'dev' into win-ltsc 2025-05-15 11:55:02 -04:00
rbalsleyMSFT 9cac674d7b Marged in changes to Add-WindowsPackage that were missing 2025-05-15 08:38:01 -07:00
Zehadi Alam 2cf7da9c91 Merge branch 'dev' into win-ltsc 2025-05-15 11:02:18 -04:00
rbalsleyMSFT 0d1d3a1ed5 Fix issue with checkpoint CUs and May 2025-05B CU. Should future proof new checkpoint CUs in the future. 2025-05-14 19:59:04 -07:00
rbalsleyMSFT 7e2ebe8013 Merge pull request #150 from JonasKloseBW/2412.3-Set-Computer-Name-through-SerialNumber-List
2412.3 set computer name through serial number list
2025-02-27 10:04:08 -08:00
JonasKloseBW 0606a1278c Update ApplyFFU.ps1
- Allows setting the computer name with a predefined list (SerialComputerNames.csv) of serial numbers and matching computer names
- Defaults to FFU-{Random} if no matching serial number is found in list so FFU deployment can continue without user input
2025-02-27 18:37:29 +01:00
rbalsleyMSFT 7c9f24f695 Fixed issues
- Fixed an issue where if AppsScriptVariables was configured in a config file, the hashtable wasn't being created by the script when setting the variable.
- Fixed a crash where shortening the Windows SKU was creating duplicate shortened names for certain SKUs.
2025-01-16 18:00:08 -08:00
rbalsleyMSFT 227fb4fa94 Merge pull request #129 from JonasKloseBW/2412.3-Add-AppList-Parameter
Add $AppListPath parameter
2025-01-13 09:06:37 -08:00
JonasKloseBW 77ef154941 Add $AppListPath parameter
- Add $AppListPath parameter
- Set default value to "$AppsPath\AppList.json"
- Modify Get-Apps call to use the $AppListPath parameter
2025-01-13 12:51:32 +01:00
rbalsleyMSFT a00aa3ee02 Merge pull request #123 from rbalsleyMSFT/main
2412.3
2025-01-10 13:51:21 -08:00
Zehadi Alam e25a890946 Remove extra lines 2024-12-28 20:30:03 -05:00
Zehadi Alam bc4a181182 Add support for LTSC versions of Windows 2024-12-28 20:01:01 -05:00
5 changed files with 638 additions and 148 deletions
+53
View File
@@ -1,5 +1,58 @@
# Change Log
# 2505.1
Highly recommended that you upgrade to this release. Fixes the issue with the May 2025 cumulative update and some SKU naming issues for SKUs other than Pro.
# Support for Windows LTSB/LTSC
Thanks to @zehadialam for the code to allow support for LTSB and LTSC. This has been a requested feature from a number of customers and some might be opting for LTSC when Windows 10 support ends in October. We support LTSB 2016, LTSC 2019, 2021, 2024 including the N and IoT variants. Extensive testing has gone into validating CU and .net support. File an issue if you see any weird behavior.
# Support for automating computer naming via CSV
Thanks to @JonasKloseBW for PR #150
- Allows setting the computer name with a predefined list (SerialComputerNames.csv) of serial numbers and matching computer names
- Defaults to FFU-{Random} if no matching serial number is found in list so FFU deployment can continue without user input
# Fixes
- Thanks to @JonasKloseBW for PR #129 for adding the -AppListPath parameter
- Fixed an issue where if AppsScriptVariables was configured in a config file, the hashtable wasn't being created by the script when setting the variable.
- Fixed a crash where shortening the Windows SKU was creating duplicate shortened names for certain SKUs (EDU mainly, but others too)
- Fix an issue with checkpoint CUs and May 2025-05B CU. Should future proof new checkpoint CUs in the future.
# Additional Fixes
BuildFFUVM.ps1
- Added parameter definitions that were missing:
- AppListPath - Path to a JSON file containing a list of applications to install using WinGet. Default is $FFUDevelopmentPath\Apps\AppList.json.
- PEDriversFolder - Path to the folder containing drivers to be injected into the WinPE deployment media. Default is $FFUDevelopmentPath\PEDrivers.
- Added two new parameters:
- UpdateLatestMicrocode - This is used for Windows 10/Server. 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.
- UpdateADK - Added for airgapped scenarios where you've manually updated the ADK and don't need it to continually check. 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.
- Reorganized the WindowsSKU validateset to make it easier to read and added in 2016 LTSB releases
- Changed version to 2505.1
- Reorganized the releasetoMapping SKUs to make it easier to read
- Omitted Defender/Edge from reporting KB ID since neither includes it
- Updated Save-KB with some enhancements from the UI branch which will handle KBs that don't have an architecture defined in their file name that will leverage a new function Get-PEArchitecture that can interrogate the file name and determine the correct architecture
- Updated Get-ShortenedWindowsSKU with LTSB/LTSC SKUs
- Updated New-FFUFileName to use $winverinfo.Name for $WindowsRelease for client OSes to which will set $WindowsRelease to using Win10 or Win11. This fixes a bug where you might see 10 or 11 instead of Win10 or Win11 for FFU builds that use only the VHDX (e.g. `-InstallApps $false`. This keeps the naming consistent with FFUs built via VM.
- Updated Get-WindowsVersionInfo to fix an issue with naming LTSC 2019
- Added Get-PEArchitecture function
- Commented out the Windows Security Platform Update code since the URL is dead for the content. This is fixed in the UI branch and will be reintroduced in Dev and Main at a later date when the UI work is complete.
- Created a new variable `$isLTSC`
- Modified and reorganized the search strings for the various .net framework components. LTSC introduced some complexity with handling the various .net releases.
- VHDXCaching will now recurse the KBPath folder when finding downloaded KBs to include in its config file
Sample_default.json
- Added new/missing parameters
- ApplistPath
- UpdateADK
- UpdateLatestMicrocode
CaptureFFU.ps1
- `$WindowsVersion` 2016 and 2019 for LTSC releases
- Changed some SKU spacing to make things more consistent and included Enterprise N LTSC
ApplyFFU.ps1
- Updated version to 2505.1
# 2412.1
This is a major release with a number of quality-of-life improvements that will reduce the time it takes to create FFUs. I highly recommend you update to this release.
+534 -134
View File
@@ -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,36 +3262,43 @@ 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
@@ -3170,6 +3306,13 @@ function Get-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
@@ -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' }
}
@@ -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)
Binary file not shown.