Public/Functions/SystemFirmwareUpdate.ps1
|
function Get-SystemFirmwareUpdate { <# .SYNOPSIS Retrieves the latest system firmware update from Microsoft Update Catalog .DESCRIPTION Searches Microsoft Update Catalog directly for the latest system firmware update available for the current computer firmware resource GUID. .EXAMPLE Get-SystemFirmwareUpdate Returns the latest available firmware update .LINK https://github.com/OSDeploy/OSD/tree/master/docs .NOTES Author: David Segura - Recast Software 2026-07-10 - Added comment-based help 2026-07-11 - Removed Get-MSCatalogUpdate dependency and added direct catalog query #> [CmdLetBinding()] param() $SystemFirmwareResources = @(Get-SystemFirmwareResource | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique) if (-not $SystemFirmwareResources) { Write-Host -ForegroundColor DarkGray "Get-SystemFirmwareUpdate: Could not find a UEFI Firmware HardwareID" return } if (Test-MicrosoftUpdateCatalog) { $CatalogResults = @() foreach ($SystemFirmwareResource in $SystemFirmwareResources) { $SearchTerms = @( $SystemFirmwareResource, $SystemFirmwareResource.Trim('{','}') ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique foreach ($SearchTerm in $SearchTerms) { try { $EncodedSearch = [uri]::EscapeDataString($SearchTerm) $Uri = "https://www.catalog.update.microsoft.com/Search.aspx?q=$EncodedSearch" $CurrentWarningPreference = $WarningPreference $WarningPreference = 'SilentlyContinue' try { $Response = Invoke-CatalogRequest -Uri $Uri } finally { $WarningPreference = $CurrentWarningPreference } if ($null -eq $Response -or $null -eq $Response.Rows) { continue } foreach ($Row in $Response.Rows) { if ($Row.Id -eq 'headerRow') { continue } try { $Update = [MsUpCat]::new($Row, $false) } catch { continue } if ([string]::IsNullOrWhiteSpace($Update.Guid) -or [string]::IsNullOrWhiteSpace($Update.Title)) { continue } if ( ($Update.Title -match '(?i)firmware') -or ($Update.Classification -match '(?i)firmware') -or ($Update.Classification -match '(?i)^drivers?$') ) { $CatalogResults += $Update } } } catch { Write-Verbose "Get-SystemFirmwareUpdate: Failed catalog query for $SearchTerm. $_" } } } if ($CatalogResults) { $CatalogResults | Sort-Object LastUpdated -Descending | Select-Object LastUpdated,Title,Version,Size,Guid -First 1 return } Write-Host -ForegroundColor DarkGray "Get-SystemFirmwareUpdate: Could not find a UEFI Firmware update for this HardwareID" } else { Write-Host -ForegroundColor DarkGray "Get-SystemFirmwareUpdate: Could not reach https://www.catalog.update.microsoft.com/" } #================================================= } function Save-SystemFirmwareUpdate { <# .SYNOPSIS Downloads and extracts the latest system firmware update. .DESCRIPTION Finds the latest applicable system firmware update from Microsoft Update Catalog, downloads the package, and extracts its contents to a destination directory. .PARAMETER DestinationDirectory Directory where the firmware update package will be downloaded and extracted. .EXAMPLE Save-SystemFirmwareUpdate Downloads and extracts the latest firmware update to the default temp path. .EXAMPLE Save-SystemFirmwareUpdate -DestinationDirectory C:\Drivers\SystemFirmware Downloads and extracts the latest firmware update to C:\Drivers\SystemFirmware. .OUTPUTS PSCustomObject. Returns details about the selected update, extraction path, and discovered INF files. .LINK https://github.com/OSDeploy/OSD/tree/master/docs .NOTES Author: David Segura - Recast Software 2026-07-11 - Improved status output and error handling 2026-07-11 - Return structured save result and validate extraction exit code #> [CmdLetBinding()] param ( [ValidateNotNullOrEmpty()] [String]$DestinationDirectory = "$env:TEMP\SystemFirmwareUpdate" ) try { if (-not (Test-MicrosoftUpdateCatalog)) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to reach https://www.catalog.update.microsoft.com/" return } $SystemFirmwareUpdate = Get-SystemFirmwareUpdate if (-not $SystemFirmwareUpdate -or [string]::IsNullOrWhiteSpace($SystemFirmwareUpdate.Guid)) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unable to find a UEFI Firmware update for this HardwareID" return } # Display selected update information $SystemFirmwareUpdate $ExtractDirectory = Join-Path -Path $DestinationDirectory -ChildPath ($SystemFirmwareUpdate.Guid.Trim('{','}')) if (-not (Test-Path $ExtractDirectory -PathType Container)) { New-Item -Path $ExtractDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null } try { $SystemFirmwareUpdateFile = Save-UpdateCatalog -Guid $SystemFirmwareUpdate.Guid -DestinationDirectory $ExtractDirectory -ErrorAction Stop } catch { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed to download $($_.Exception.Message)" return } if (-not $SystemFirmwareUpdateFile -or -not (Test-Path $SystemFirmwareUpdateFile.FullName)) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Download did not produce a valid update package" return } try { & expand.exe "$($SystemFirmwareUpdateFile.FullName)" -F:* "$ExtractDirectory" | Out-Null if ($LASTEXITCODE -ne 0) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] expand.exe failed with exit code $LASTEXITCODE" return } } catch { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed to extract update package: $($_.Exception.Message)" return } try { Remove-Item $SystemFirmwareUpdateFile.FullName -ErrorAction Stop } catch { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed to remove temporary package: $($_.Exception.Message)" } $InfFiles = @(Get-ChildItem -Path $ExtractDirectory -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue) if ($InfFiles) { # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Saved and extracted firmware update to $ExtractDirectory (INF files: $($InfFiles.Count))" } else { # Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Saved and extracted firmware update to $ExtractDirectory" } [PSCustomObject]@{ Update = $SystemFirmwareUpdate DestinationDirectory = $DestinationDirectory ExtractDirectory = $ExtractDirectory InfFiles = $InfFiles } } catch { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Unexpected error: $($_.Exception.Message)" } #================================================= } function Install-SystemFirmwareUpdate { <# .SYNOPSIS Downloads and installs the system firmware update .DESCRIPTION Downloads the latest system firmware update from Microsoft Update Catalog and installs it on the running system. Requires admin rights and PowerShell 5.1. .PARAMETER DestinationDirectory Directory where the firmware update will be downloaded. Default is C:\Drivers\SystemFirmwareUpdate .PARAMETER Force Required switch to perform the firmware update. Without this switch, the function only warns and exits. .PARAMETER Restart Restarts the computer automatically when a reboot is required after installation. .EXAMPLE Install-SystemFirmwareUpdate Downloads and installs the latest firmware update .EXAMPLE Install-SystemFirmwareUpdate -DestinationDirectory 'D:\Updates' Downloads firmware update to D:\Updates and installs it .EXAMPLE Install-SystemFirmwareUpdate -Force -Restart Downloads and installs the latest firmware update and restarts if required. .LINK https://github.com/OSDeploy/OSD/tree/master/docs .NOTES Author: David Segura - Recast Software 2026-07-10 - Added comment-based help 2026-07-11 - Refactored to use Save-SystemFirmwareUpdate and improved install error handling 2026-07-11 - Added BitLocker warning, Force gate, and optional restart handling #> [CmdLetBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [ValidateNotNullOrEmpty()] [String] $DestinationDirectory = "C:\Drivers\SystemFirmwareUpdate", [Parameter()] [switch] $Force, [Parameter()] [switch] $Restart ) $CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $CurrentPrincipal = [Security.Principal.WindowsPrincipal]::new($CurrentIdentity) if (-not $CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Warning "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Administrative rights are required" return } if (-not (Test-Path 'C:\Windows' -PathType Container)) { Write-Host -ForegroundColor DarkGray "Install-SystemFirmwareUpdate: Could not locate C:\Windows" if ($env:SystemDrive -eq 'X:') { Write-Host -ForegroundColor DarkGray "Make sure that Bitlocker encrypted drives are unlocked and suspended first" } return } try { $BitLockerVolumes = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction Stop if ($BitLockerVolumes -and ($BitLockerVolumes.ProtectionStatus -contains 'On' -or $BitLockerVolumes.ProtectionStatus -contains 1)) { Write-Warning "[$($MyInvocation.MyCommand.Name)] BitLocker protection is enabled on $env:SystemDrive. Suspend BitLocker before applying firmware updates." } } catch { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Unable to query BitLocker status: $($_.Exception.Message)" } if (-not $Force) { Write-Warning "[$($MyInvocation.MyCommand.Name)] Use -Force to perform the firmware update. No changes were made." return } if (-not $PSCmdlet.ShouldProcess($DestinationDirectory, 'Download and install latest system firmware update')) { return } $SaveResult = $null try { $SaveResult = Save-SystemFirmwareUpdate -DestinationDirectory $DestinationDirectory } catch { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed while saving firmware update: $($_.Exception.Message)" return } if (-not $SaveResult -or [string]::IsNullOrWhiteSpace($SaveResult.ExtractDirectory)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Save-SystemFirmwareUpdate did not return an extraction path" return } $InstallDirectory = $SaveResult.ExtractDirectory if (-not (Test-Path $InstallDirectory -PathType Container)) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Could not locate extraction directory $InstallDirectory" return } $InfFiles = @($SaveResult.InfFiles) if (-not $InfFiles) { $InfFiles = @(Get-ChildItem -Path $InstallDirectory -Recurse -Filter '*.inf' -ErrorAction SilentlyContinue) } if (-not $InfFiles) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] No INF files found in $InstallDirectory" return } if ($env:SystemDrive -eq 'X:') { try { Add-WindowsDriver -Path 'C:\' -Driver $InstallDirectory -Recurse -ForceUnsigned -ErrorAction Stop | Out-Null Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Added firmware driver package in WinPE from $InstallDirectory" } catch { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed to add firmware driver package in WinPE: $($_.Exception.Message)" } return } $InstalledCount = 0 $FailedCount = 0 $RebootRequired = $false foreach ($InfFile in $InfFiles) { try { & pnputil.exe /Add-Driver $InfFile.FullName /install | Out-Null switch ($LASTEXITCODE) { 0 { $InstalledCount++ } 3010 { $InstalledCount++ $RebootRequired = $true } default { $FailedCount++ Write-Verbose "[$($MyInvocation.MyCommand.Name)] pnputil exit code $LASTEXITCODE for $($InfFile.FullName)" } } } catch { $FailedCount++ Write-Verbose "[$($MyInvocation.MyCommand.Name)] Failed to install $($InfFile.FullName): $($_.Exception.Message)" } } Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Firmware driver install complete. Installed: $InstalledCount Failed: $FailedCount" if ($RebootRequired) { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] A reboot is required to complete firmware update installation" if ($Restart) { if ($PSCmdlet.ShouldProcess('LocalComputer', 'Restart-Computer -Force')) { try { Restart-Computer -Force } catch { Write-Host -ForegroundColor DarkGray "[$(Get-Date -format s)] [$($MyInvocation.MyCommand.Name)] Failed to restart computer: $($_.Exception.Message)" } } } } #================================================= } |