function Get-USBDrive() { $USBDriveLetter = (Get-Volume | Where-Object { $_.DriveType -eq 'Removable' -and $_.FileSystemType -eq 'NTFS' }).DriveLetter if ($null -eq $USBDriveLetter) { #Must be using a fixed USB drive - difficult to grab drive letter from win32_diskdrive. Assume user followed instructions and used Deploy as the friendly name for partition $USBDriveLetter = (Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.FileSystemType -eq 'NTFS' -and $_.FileSystemLabel -eq 'Deploy' }).DriveLetter #If we didn't get the drive letter, stop the script. if ($null -eq $USBDriveLetter) { $errorMessage = 'Cannot find USB drive letter. If using a fixed USB drive, name the deployment partition "Deploy".' WriteLog ($errorMessage + ' Exiting.') Stop-Script -Message $errorMessage } } $USBDriveLetter = $USBDriveLetter + ":\" return $USBDriveLetter } function Get-HardDrive() { $systemInfo = Get-CimInstance -Class 'Win32_ComputerSystem' $manufacturer = $systemInfo.Manufacturer $model = $systemInfo.Model WriteLog 'Getting Hard Drive info' if ($manufacturer -eq 'Microsoft Corporation' -and $model -eq 'Virtual Machine') { WriteLog 'Running in a Hyper-V VM. Getting virtual disk on Index 0 and SCSILogicalUnit 0' $diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' ` -and $_.Model -eq 'Microsoft Virtual Disk' ` -and $_.Index -eq 0 ` -and $_.SCSILogicalUnit -eq 0 }) } else { WriteLog 'Not running in a VM. Getting physical disk drive' $diskDriveCandidates = @(Get-CimInstance -Class 'Win32_DiskDrive' | Where-Object { $_.MediaType -eq 'Fixed hard disk media' -and $_.Model -ne 'Microsoft Virtual Disk' }) } # Return the array of candidates for selection in main script return $diskDriveCandidates } function WriteLog($LogText) { Add-Content -path $LogFile -value "$((Get-Date).ToString()) $LogText" } function Set-DiskpartAnswerFiles($DiskpartFile, $DiskID) { (Get-Content $DiskpartFile).Replace('disk 0', "disk $DiskID") | Set-Content -Path $DiskpartFile } function Set-Computername($computername) { [xml]$xml = Get-Content $UnattendFile $components = $xml.unattend.settings.component $found = $false foreach ($component in $components) { if ($component.ComputerName) { $component.ComputerName = $computername $found = $true break } } if (-not $found) { WriteLog 'ComputerName element not found in unattend.xml.' throw 'ComputerName element not found in unattend.xml.' } $xml.Save($UnattendFile) return $computername } function Invoke-Process { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$FilePath, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ArgumentList, [Parameter()] [switch]$IgnoreExitCode, [Parameter()] [switch]$PassThruExitCode ) $ErrorActionPreference = 'Stop' try { $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)" $startProcessParams = @{ FilePath = $FilePath ArgumentList = $ArgumentList RedirectStandardError = $stdErrTempFile RedirectStandardOutput = $stdOutTempFile Wait = $true; PassThru = $true; NoNewWindow = $true; } if ($PSCmdlet.ShouldProcess("Process [$($FilePath)]", "Run with args: [$($ArgumentList)]")) { $cmd = Start-Process @startProcessParams $cmdOutput = Get-Content -Path $stdOutTempFile -Raw $cmdError = Get-Content -Path $stdErrTempFile -Raw if ($cmd.ExitCode -ne 0) { # Non-terminating mode: capture output to Scriptlog and continue if ($IgnoreExitCode) { if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) { WriteLog $cmdOutput } if ([string]::IsNullOrEmpty($cmdError) -eq $false) { WriteLog $cmdError } if ($PassThruExitCode) { return $cmd.ExitCode } return } if ($cmdError) { throw $cmdError.Trim() } if ($cmdOutput) { throw $cmdOutput.Trim() } throw "Process failed. ExitCode = $($cmd.ExitCode)." } else { if ([string]::IsNullOrEmpty($cmdOutput) -eq $false) { WriteLog $cmdOutput } } if ($PassThruExitCode) { return $cmd.ExitCode } } } catch { #$PSCmdlet.ThrowTerminatingError($_) WriteLog $_ Write-Host 'Script failed - check scriptlog.txt on the USB drive for more info' throw $_ } finally { Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction Ignore } } function Write-SectionHeader($Title) { $width = 51 $leftPad = [math]::Floor(($width - $Title.Length) / 2) $rightPad = $width - $Title.Length - $leftPad $centeredTitle = (' ' * $leftPad) + $Title + (' ' * $rightPad) Write-Host "`n" # Add a newline for spacing Write-Host ('-' * $width) -ForegroundColor Yellow Write-Host $centeredTitle -ForegroundColor Yellow Write-Host ('-' * $width) -ForegroundColor Yellow } function Get-NormalizedManufacturer { param( [string]$Manufacturer ) if ([string]::IsNullOrWhiteSpace($Manufacturer)) { return $null } $normalized = $Manufacturer.Trim().ToUpperInvariant() if ($normalized -like '*DELL*') { return 'Dell' } elseif ($normalized -like '*HP*' -or $normalized -like '*HEWLETT*') { return 'HP' } elseif ($normalized -like '*LENOVO*') { return 'Lenovo' } elseif ($normalized -like '*MICROSOFT*' -or $normalized -like '*SURFACE*') { return 'Microsoft' } elseif ($normalized -like '*PANASONIC*') { return 'Panasonic Corporation' } elseif ($normalized -like '*VIGLEN*') { return 'Viglen' } elseif ($normalized -like '*AZW*') { return 'AZW' } elseif ($normalized -like '*FUJITSU*') { return 'Fujitsu' } elseif ($normalized -like '*GETAC*') { return 'Getac' } elseif ($normalized -like '*BYTESPEED*') { return 'ByteSpeed' } elseif ($normalized -like '*INTEL*') { return 'Intel' } return $Manufacturer.Trim() } function Get-SystemIdentityMetadata { param( [Parameter(Mandatory = $true)] [Microsoft.Management.Infrastructure.CimInstance]$ComputerSystem, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance]$ComputerSystemProduct, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance]$MsSystemInformation ) # Consolidate manufacturer normalization so UI and driver mapping share the same identifiers. $normalizedManufacturer = Get-NormalizedManufacturer -Manufacturer $ComputerSystem.Manufacturer if (-not $ComputerSystemProduct) { $ComputerSystemProduct = Get-CimInstance -Class Win32_ComputerSystemProduct -ErrorAction SilentlyContinue } $baseBoardInfo = Get-CimInstance -Class Win32_BaseBoard -ErrorAction SilentlyContinue | Select-Object -First 1 $baseBoardSku = if ($baseBoardInfo -and -not [string]::IsNullOrWhiteSpace($baseBoardInfo.SKU)) { $baseBoardInfo.SKU.Trim() } else { $null } $msBaseBoardProduct = if ($MsSystemInformation -and $MsSystemInformation.BaseBoardProduct) { $MsSystemInformation.BaseBoardProduct.Trim() } else { $null } $modelCandidate = if ($normalizedManufacturer -eq 'Lenovo' -and $ComputerSystemProduct -and -not [string]::IsNullOrWhiteSpace($ComputerSystemProduct.Version)) { $ComputerSystemProduct.Version } else { $ComputerSystem.Model } if ([string]::IsNullOrWhiteSpace($modelCandidate)) { $modelCandidate = $ComputerSystem.Model } if ($modelCandidate) { $modelCandidate = $modelCandidate.Trim() } $identity = [pscustomobject]@{ ManufacturerOriginal = $ComputerSystem.Manufacturer ManufacturerNormalized = if ($normalizedManufacturer) { $normalizedManufacturer } else { $ComputerSystem.Manufacturer } ModelOriginal = $modelCandidate ModelNormalized = ConvertTo-ComparableModelName -Text $modelCandidate SystemSkuNormalized = $null FallbackSkuNormalized = $null MachineTypeNormalized = $null IdentifierLabel = 'System ID' IdentifierValue = $null } if ($MsSystemInformation -and $MsSystemInformation.SystemSku) { $identity.SystemSkuNormalized = $MsSystemInformation.SystemSku.Trim().ToUpperInvariant() } switch ($identity.ManufacturerNormalized) { 'Dell' { if ($MsSystemInformation -and $MsSystemInformation.SystemSku) { $identity.SystemSkuNormalized = $MsSystemInformation.SystemSku.Trim().ToUpperInvariant() } $oemStringArray = $ComputerSystem | Select-Object -ExpandProperty OEMStringArray -ErrorAction SilentlyContinue if ($oemStringArray) { $joinedOemString = ($oemStringArray -join ' ') $fallbackMatches = [regex]::Matches($joinedOemString, '\[\S*]') if ($fallbackMatches.Count -gt 0) { $identity.FallbackSkuNormalized = $fallbackMatches[0].Value.TrimStart('[').TrimEnd(']').Trim().ToUpperInvariant() } } if ($identity.FallbackSkuNormalized) { $identity.IdentifierValue = $identity.FallbackSkuNormalized } elseif ($identity.SystemSkuNormalized) { $identity.IdentifierValue = $identity.SystemSkuNormalized } break } 'HP' { if ($msBaseBoardProduct) { $identity.SystemSkuNormalized = $msBaseBoardProduct.ToUpperInvariant() } break } 'Lenovo' { $modelValue = $ComputerSystem.Model if (-not [string]::IsNullOrWhiteSpace($modelValue) -and $modelValue.Length -ge 4) { $identity.MachineTypeNormalized = $modelValue.Substring(0, 4).Trim().ToUpperInvariant() } $identity.IdentifierLabel = 'Machine Type' if ($identity.MachineTypeNormalized) { $identity.IdentifierValue = $identity.MachineTypeNormalized } break } 'Panasonic Corporation' { $identity.IdentifierLabel = 'System ID' if ($msBaseBoardProduct) { $identity.SystemSkuNormalized = $msBaseBoardProduct.ToUpperInvariant() $identity.IdentifierValue = $msBaseBoardProduct } break } 'Viglen' { $identity.IdentifierLabel = 'System ID' if ($baseBoardSku) { $identity.SystemSkuNormalized = $baseBoardSku.ToUpperInvariant() $identity.IdentifierValue = $baseBoardSku } break } 'AZW' { $identity.IdentifierLabel = 'System ID' if ($msBaseBoardProduct) { $identity.SystemSkuNormalized = $msBaseBoardProduct.ToUpperInvariant() $identity.IdentifierValue = $msBaseBoardProduct } break } 'Fujitsu' { $identity.IdentifierLabel = 'System ID' if ($baseBoardSku) { $identity.SystemSkuNormalized = $baseBoardSku.ToUpperInvariant() $identity.IdentifierValue = $baseBoardSku } break } 'Getac' { $identity.IdentifierLabel = 'System ID' if ($msBaseBoardProduct) { $identity.SystemSkuNormalized = $msBaseBoardProduct.ToUpperInvariant() $identity.IdentifierValue = $msBaseBoardProduct } break } 'Intel' { $identity.IdentifierLabel = 'Model' if ($identity.ModelOriginal) { $identity.IdentifierValue = $identity.ModelOriginal } break } 'ByteSpeed' { $modelValue = if ($ComputerSystem.Model) { $ComputerSystem.Model.Trim() } else { $null } if ($modelValue -and $modelValue -like '*NUC*') { $identity.ManufacturerNormalized = 'Intel' if ($msBaseBoardProduct) { $identity.ModelOriginal = $msBaseBoardProduct $identity.ModelNormalized = ConvertTo-ComparableModelName -Text $msBaseBoardProduct $identity.IdentifierLabel = 'Model' $identity.IdentifierValue = $msBaseBoardProduct } elseif ($modelValue) { $identity.IdentifierLabel = 'Model' $identity.IdentifierValue = $modelValue } } else { $identity.IdentifierLabel = 'Model' if ($modelValue) { $identity.IdentifierValue = $modelValue } } break } default { break } } if ($null -eq $identity.IdentifierValue) { if ($identity.MachineTypeNormalized) { $identity.IdentifierValue = $identity.MachineTypeNormalized } elseif ($identity.SystemSkuNormalized) { $identity.IdentifierValue = $identity.SystemSkuNormalized } } return $identity } function Get-SystemInformation { param( [Parameter(Mandatory = $true)] [pscustomobject]$HardDrive ) $computerSystem = Get-CimInstance -Class Win32_ComputerSystem $computerSystemProduct = Get-CimInstance -Class Win32_ComputerSystemProduct -ErrorAction SilentlyContinue $msSystemInformation = Get-CimInstance -Namespace 'root\WMI' -Class MS_SystemInformation -ErrorAction SilentlyContinue $systemIdentity = Get-SystemIdentityMetadata -ComputerSystem $computerSystem -ComputerSystemProduct $computerSystemProduct -MsSystemInformation $msSystemInformation $biosInfo = Get-CimInstance -Class Win32_Bios $processorInfo = Get-CimInstance -Class Win32_Processor | Select-Object -First 1 $processor = if ($processorInfo) { $processorInfo.Name } else { 'Unknown' } $totalMemory = (Get-CimInstance -Class Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum if ($null -eq $totalMemory) { $totalMemory = 0 } $totalMemoryGB = [math]::Round(($totalMemory / 1GB), 2) $diskSizeGB = [math]::Round(($HardDrive.DiskSize / 1GB), 2) $baseBoardManufacturer = if ($msSystemInformation -and $msSystemInformation.BaseBoardManufacturer) { $msSystemInformation.BaseBoardManufacturer.Trim() } else { $null } $baseBoardProduct = if ($msSystemInformation -and $msSystemInformation.BaseBoardProduct) { $msSystemInformation.BaseBoardProduct.Trim() } else { $null } $baseBoardVersion = if ($msSystemInformation -and $msSystemInformation.BaseBoardVersion) { $msSystemInformation.BaseBoardVersion.Trim() } else { $null } $biosMajorRelease = if ($msSystemInformation -and $null -ne $msSystemInformation.BiosMajorRelease) { [string]$msSystemInformation.BiosMajorRelease } else { $null } $biosMinorRelease = if ($msSystemInformation -and $null -ne $msSystemInformation.BiosMinorRelease) { [string]$msSystemInformation.BiosMinorRelease } else { $null } $biosReleaseDate = $null if ($msSystemInformation -and $msSystemInformation.BiosReleaseDate) { try { $biosReleaseDate = [System.Management.ManagementDateTimeConverter]::ToDateTime($msSystemInformation.BiosReleaseDate).ToString('yyyy-MM-dd HH:mm:ss') } catch { $biosReleaseDate = $msSystemInformation.BiosReleaseDate } } $biosVendor = if ($msSystemInformation -and $msSystemInformation.BiosVendor) { $msSystemInformation.BiosVendor.Trim() } else { $null } $biosVersion = if ($msSystemInformation -and $msSystemInformation.BiosVersion) { $msSystemInformation.BiosVersion.Trim() } else { $null } $ecFirmwareMajorRelease = if ($msSystemInformation -and $null -ne $msSystemInformation.ECFirmwareMajorRelease) { [string]$msSystemInformation.ECFirmwareMajorRelease } else { $null } $ecFirmwareMinorRelease = if ($msSystemInformation -and $null -ne $msSystemInformation.ECFirmwareMinorRelease) { [string]$msSystemInformation.ECFirmwareMinorRelease } else { $null } $displayManufacturer = if ($systemIdentity.ManufacturerNormalized) { $systemIdentity.ManufacturerNormalized } else { $computerSystem.Manufacturer } $displayModel = if ($systemIdentity.ModelNormalized) { $systemIdentity.ModelNormalized } else { $systemIdentity.ModelOriginal } $sysInfoData = [ordered]@{ "Manufacturer" = $displayManufacturer "Model" = $displayModel "Serial Number" = $biosInfo.SerialNumber "Processor" = $processor "Memory" = "{0} GB" -f $totalMemoryGB "Disk Size" = "{0} GB" -f $diskSizeGB "Logical Sector Size" = "$($HardDrive.BytesPerSector) Bytes" "BaseBoardManufacturer" = if ($baseBoardManufacturer) { $baseBoardManufacturer } else { 'Not Detected' } "BaseBoardProduct" = if ($baseBoardProduct) { $baseBoardProduct } else { 'Not Detected' } "BaseBoardVersion" = if ($baseBoardVersion) { $baseBoardVersion } else { 'Not Detected' } "BiosMajorRelease" = if ($biosMajorRelease) { $biosMajorRelease } else { 'Not Detected' } "BiosMinorRelease" = if ($biosMinorRelease) { $biosMinorRelease } else { 'Not Detected' } "BiosReleaseDate" = if ($biosReleaseDate) { $biosReleaseDate } else { 'Not Detected' } "BiosVendor" = if ($biosVendor) { $biosVendor } else { 'Not Detected' } "BiosVersion" = if ($biosVersion) { $biosVersion } else { 'Not Detected' } "ECFirmwareMajorRelease" = if ($ecFirmwareMajorRelease) { $ecFirmwareMajorRelease } else { 'Not Detected' } "ECFirmwareMinorRelease" = if ($ecFirmwareMinorRelease) { $ecFirmwareMinorRelease } else { 'Not Detected' } "ManufacturerNormalized" = $systemIdentity.ManufacturerNormalized "ModelNormalized" = $systemIdentity.ModelNormalized "DriverIdentifierLabel" = $systemIdentity.IdentifierLabel "DriverIdentifierValue" = $systemIdentity.IdentifierValue "SystemSkuNormalized" = $systemIdentity.SystemSkuNormalized "FallbackSkuNormalized" = $systemIdentity.FallbackSkuNormalized "MachineTypeNormalized" = $systemIdentity.MachineTypeNormalized } $sysInfoData[$systemIdentity.IdentifierLabel] = if ($systemIdentity.IdentifierValue) { $systemIdentity.IdentifierValue } else { 'Not Detected' } return [PSCustomObject]$sysInfoData } function Write-SystemInformation { param( [Parameter(Mandatory = $true)] [pscustomobject]$SystemInformation ) $hiddenProperties = @( 'SystemSkuNormalized', 'FallbackSkuNormalized', 'MachineTypeNormalized', 'ManufacturerNormalized', 'ModelNormalized', 'DriverIdentifierLabel', 'DriverIdentifierValue' ) WriteLog '--- System Information ---' foreach ($property in $SystemInformation.psobject.Properties) { if ($hiddenProperties -contains $property.Name) { continue } WriteLog "$($property.Name): $($property.Value)" } WriteLog '--- End System Information ---' Write-SectionHeader -Title 'System Information' $displayData = [ordered]@{} foreach ($property in $SystemInformation.psobject.Properties) { if ($hiddenProperties -contains $property.Name) { continue } $displayData[$property.Name] = $property.Value } $consoleOutput = ([pscustomobject]$displayData | Format-List | Out-String) Write-Host $consoleOutput.Trim() Write-Host } function Find-DriverMappingRule { param( [Parameter(Mandatory = $true)] [pscustomobject]$SystemInformation, [Parameter(Mandatory = $true)] [object[]]$DriverMappings ) $normalizedManufacturer = if ($SystemInformation.PSObject.Properties['ManufacturerNormalized']) { $SystemInformation.ManufacturerNormalized } else { Get-NormalizedManufacturer -Manufacturer $SystemInformation.Manufacturer } if ([string]::IsNullOrWhiteSpace($normalizedManufacturer)) { WriteLog 'DriverMapping: Unable to determine manufacturer for automatic matching.' return $null } $driverMappingsArray = @() foreach ($entry in @($DriverMappings)) { if ($null -ne $entry) { $driverMappingsArray += $entry } } if ($driverMappingsArray.Count -eq 0) { WriteLog 'DriverMapping: Mapping file contained no entries.' return $null } $rulesForMake = @($driverMappingsArray | Where-Object { $entryManufacturer = Get-NormalizedManufacturer -Manufacturer $_.Manufacturer $entryManufacturer -eq $normalizedManufacturer }) if ($rulesForMake.Count -eq 0) { WriteLog "DriverMapping: No entries found for manufacturer '$normalizedManufacturer'." return $null } $systemSkuNormalized = if ($SystemInformation.PSObject.Properties['SystemSkuNormalized']) { $SystemInformation.SystemSkuNormalized } else { $null } $fallbackSkuNormalized = if ($SystemInformation.PSObject.Properties['FallbackSkuNormalized']) { $SystemInformation.FallbackSkuNormalized } else { $null } $machineTypeNormalized = if ($SystemInformation.PSObject.Properties['MachineTypeNormalized']) { $SystemInformation.MachineTypeNormalized } else { $null } $normalizedModel = if ($SystemInformation.PSObject.Properties['ModelNormalized']) { $SystemInformation.ModelNormalized } else { $null } if ([string]::IsNullOrWhiteSpace($normalizedModel)) { $normalizedModel = ConvertTo-ComparableModelName -Text $SystemInformation.Model } switch ($normalizedManufacturer) { 'Dell' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Dell SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } if (-not [string]::IsNullOrWhiteSpace($fallbackSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $fallbackSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Dell fallback SKU '$fallbackSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Dell identifiers did not match any entries.' return $null } 'HP' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: HP SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: HP SystemId not detected or not present in mapping.' return $null } 'Lenovo' { if (-not [string]::IsNullOrWhiteSpace($machineTypeNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['MachineType'] -and $_.MachineType.Trim().ToUpperInvariant() -eq $machineTypeNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Lenovo MachineType '$machineTypeNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Lenovo MachineType not detected or not present in mapping.' return $null } 'Microsoft' { # Prefer System SKU matching for Microsoft/Surface when available. if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { foreach ($rule in $rulesForMake) { if ($rule.PSObject.Properties['SystemSku'] -and $null -ne $rule.SystemSku) { foreach ($sku in @($rule.SystemSku)) { if (-not [string]::IsNullOrWhiteSpace($sku) -and $sku.Trim().ToUpperInvariant() -eq $systemSkuNormalized) { WriteLog "DriverMapping: Microsoft SystemSku '$systemSkuNormalized' matched '$($rule.Model)'." return $rule } } } } } # Fallback to model string comparison (legacy behavior). foreach ($rule in $rulesForMake) { $ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) { WriteLog "DriverMapping: Microsoft model '$normalizedModel' matched '$($rule.Model)'." return $rule } } WriteLog 'DriverMapping: Microsoft model not present in mapping.' return $null } 'Panasonic Corporation' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Panasonic SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Panasonic SystemId not detected or not present in mapping.' return $null } 'Viglen' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Viglen SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Viglen SystemId not detected or not present in mapping.' return $null } 'AZW' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: AZW SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: AZW SystemId not detected or not present in mapping.' return $null } 'Fujitsu' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Fujitsu SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Fujitsu SystemId not detected or not present in mapping.' return $null } 'Getac' { if (-not [string]::IsNullOrWhiteSpace($systemSkuNormalized)) { $match = $rulesForMake | Where-Object { $_.PSObject.Properties['SystemId'] -and $_.SystemId.Trim().ToUpperInvariant() -eq $systemSkuNormalized } | Select-Object -First 1 if ($match) { WriteLog "DriverMapping: Getac SystemId '$systemSkuNormalized' matched '$($match.Model)'." return $match } } WriteLog 'DriverMapping: Getac SystemId not detected or not present in mapping.' return $null } 'Intel' { foreach ($rule in $rulesForMake) { if (-not $rule.PSObject.Properties['Model']) { continue } $ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) { WriteLog "DriverMapping: Intel model '$normalizedModel' matched '$($rule.Model)'." return $rule } } WriteLog 'DriverMapping: Intel model not detected or not present in mapping.' return $null } 'ByteSpeed' { foreach ($rule in $rulesForMake) { if (-not $rule.PSObject.Properties['Model']) { continue } $ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) { WriteLog "DriverMapping: ByteSpeed model '$normalizedModel' matched '$($rule.Model)'." return $rule } } WriteLog 'DriverMapping: ByteSpeed model not detected or not present in mapping.' return $null } default { # Generic fallback for manufacturers without explicit handling foreach ($rule in $rulesForMake) { if (-not $rule.PSObject.Properties['Model']) { continue } $ruleModelNorm = ConvertTo-ComparableModelName -Text $rule.Model if (-not [string]::IsNullOrWhiteSpace($ruleModelNorm) -and $ruleModelNorm -eq $normalizedModel) { WriteLog "DriverMapping: Manufacturer '$normalizedManufacturer' model '$normalizedModel' matched '$($rule.Model)'." return $rule } } WriteLog "DriverMapping: No generic match found for manufacturer '$normalizedManufacturer' using model '$normalizedModel'." return $null } } } function Stop-Script { param( [string]$Message ) Write-Host "`n" if (-not [string]::IsNullOrWhiteSpace($Message)) { Write-Error -Message $Message } WriteLog "Copying dism log to $USBDrive" Invoke-Process xcopy "X:\Windows\logs\dism\dism.log $USBDrive /Y" WriteLog "Copying dism log to $USBDrive succeeded" Read-Host "Press Enter to exit" Exit } function ConvertTo-ComparableModelName { [CmdletBinding()] param( [string]$Text ) # Normalize model strings with HP-specific adjustments. # Remove inch unit variants (23.8-in, 23.8 inch, 23inch, 23-in, etc.) keeping only the numeric size. # Canonicalize All-in-One variants (All in One, All-in-One, All-in-One PC, AiO, AIO) to 'AIO'. # Convert any non-alphanumeric sequence to a single space, collapse whitespace, and trim. if ($null -eq $Text) { return '' } $original = $Text # Remove inch unit variants while preserving the numeric size $Text = [regex]::Replace($Text, '(?i)(\d+(?:\.\d+)?)(?:\s*[-]?\s*)(?:in|inch)\b', '$1') # Canonicalize All-in-One variants $Text = [regex]::Replace($Text, '(?i)\bAll[\s-]*in[\s-]*One(?:\s*PC)?\b', 'AIO') $Text = [regex]::Replace($Text, '(?i)\bAiO\b', 'AIO') # Generic normalization $normalized = ($Text -replace '[^A-Za-z0-9]+', ' ') $normalized = ($normalized -replace '\s+', ' ').Trim() if ($normalized -ne $original) { WriteLog "Normalized model string: Original='$original' -> Normalized='$normalized'" } return $normalized } function Test-DriverFolderHasInstallableContent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path -Path $Path -PathType Container)) { return $false } try { $nonWimFile = Get-ChildItem -Path $Path -File -Recurse -ErrorAction Stop | Where-Object { $extension = $_.Extension if ([string]::IsNullOrWhiteSpace($extension)) { return $true } return $extension.ToLowerInvariant() -ne '.wim' } | Select-Object -First 1 if ($nonWimFile) { return $true } return $false } catch { WriteLog "Failed to inspect driver folder '$Path': $($_.Exception.Message)" return $false } } function Get-AvailableDriveLetter { $usedLetters = (Get-PSDrive -PSProvider FileSystem).Name | ForEach-Object { $_.ToUpperInvariant() } for ($ascii = [int][char]'Z'; $ascii -ge [int][char]'A'; $ascii--) { $candidate = [char]$ascii if ($usedLetters -notcontains $candidate) { return $candidate } } return $null } function New-SecureBootDiagnosticsFolder { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$UsbDrive ) # Create a per-run diagnostics folder on the deployment media. try { $diagnosticsRoot = Join-Path -Path $UsbDrive -ChildPath 'SecureBootDiagnostics' New-Item -Path $diagnosticsRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null $folderName = 'Run_' + (Get-Date -Format 'yyyyMMdd_HHmmss') $diagnosticsPath = Join-Path -Path $diagnosticsRoot -ChildPath $folderName $suffix = 1 while (Test-Path -Path $diagnosticsPath) { $diagnosticsPath = Join-Path -Path $diagnosticsRoot -ChildPath ("{0}_{1}" -f $folderName, $suffix) $suffix++ } New-Item -Path $diagnosticsPath -ItemType Directory -Force -ErrorAction Stop | Out-Null WriteLog "Secure Boot diagnostics folder: $diagnosticsPath" return $diagnosticsPath } catch { WriteLog "Warning: Failed to create Secure Boot diagnostics folder. $($_.Exception.Message)" return $null } } function New-DiagnosticsStageFolder { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DiagnosticsRoot, [Parameter(Mandatory = $true)] [string]$StageName ) # Create a stage-specific folder for collected artifacts. try { if ([string]::IsNullOrWhiteSpace($DiagnosticsRoot)) { return $null } $stagePath = Join-Path -Path $DiagnosticsRoot -ChildPath $StageName New-Item -Path $stagePath -ItemType Directory -Force -ErrorAction Stop | Out-Null return $stagePath } catch { WriteLog "Warning: Failed to create diagnostics stage folder '$StageName'. $($_.Exception.Message)" return $null } } function Write-DiagnosticsTextFile { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter()] [AllowNull()] [object]$Content ) # Persist text diagnostics without affecting deployment flow. try { $directoryPath = Split-Path -Path $FilePath -Parent if (-not [string]::IsNullOrWhiteSpace($directoryPath)) { New-Item -Path $directoryPath -ItemType Directory -Force -ErrorAction Stop | Out-Null } if ($null -eq $Content) { $Content = '' } Set-Content -Path $FilePath -Value $Content -Encoding UTF8 -Force -ErrorAction Stop } catch { WriteLog "Warning: Failed to write diagnostics file '$FilePath'. $($_.Exception.Message)" } } function Get-ByteArraySha256 { [CmdletBinding()] param( [byte[]]$Bytes ) # Calculate a stable hash for raw EFI variable data. if ($null -eq $Bytes) { return $null } $sha256 = [System.Security.Cryptography.SHA256]::Create() try { return (($sha256.ComputeHash($Bytes) | ForEach-Object { $_.ToString('x2') }) -join '') } finally { $sha256.Dispose() } } function Get-ByteArrayAsciiMarker { [CmdletBinding()] param( [byte[]]$Bytes ) # Look for obvious ASCII markers inside EFI variable data. if ($null -eq $Bytes -or $Bytes.Length -eq 0) { return $null } $asciiText = [System.Text.Encoding]::ASCII.GetString($Bytes) $markers = @( 'Windows UEFI CA 2023', 'Windows UEFI CA 2011', 'Microsoft Corporation UEFI CA 2011', 'Microsoft Corporation KEK CA 2011', 'Microsoft Windows Production PCA 2011', 'Microsoft' ) foreach ($marker in $markers) { if ($asciiText -match [regex]::Escape($marker)) { return $marker } } return $null } function Open-EspPartitionAccess { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [int]$DiskNumber ) # Assign a temporary drive letter to the EFI system partition when needed. try { $espPartition = Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop | Where-Object { $_.Type -eq 'System' -or $_.GptType -eq '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' } | Select-Object -First 1 if ($null -eq $espPartition) { WriteLog "Warning: EFI system partition not found on disk $DiskNumber." return $null } if ($espPartition.DriveLetter) { $driveLetter = $espPartition.DriveLetter.ToString().ToUpperInvariant() return [PSCustomObject]@{ DiskNumber = $DiskNumber PartitionNumber = $espPartition.PartitionNumber DriveLetter = $driveLetter DrivePath = "$driveLetter`:\" RemoveAccessPath = $false } } $driveLetter = Get-AvailableDriveLetter if ($null -eq $driveLetter) { WriteLog 'Warning: No drive letters are available to mount the EFI system partition.' return $null } Set-Partition -InputObject $espPartition -NewDriveLetter $driveLetter -ErrorAction Stop WriteLog "Assigned temporary drive letter $driveLetter`: to the EFI system partition." return [PSCustomObject]@{ DiskNumber = $DiskNumber PartitionNumber = $espPartition.PartitionNumber DriveLetter = $driveLetter DrivePath = "$driveLetter`:\" RemoveAccessPath = $true } } catch { WriteLog "Warning: Failed to access the EFI system partition on disk $DiskNumber. $($_.Exception.Message)" return $null } } function Close-EspPartitionAccess { [CmdletBinding()] param( [Parameter()] [pscustomobject]$EspAccess ) # Remove a temporary EFI access path after diagnostics complete. if ($null -eq $EspAccess -or $EspAccess.RemoveAccessPath -ne $true) { return } try { Get-Partition -DiskNumber $EspAccess.DiskNumber -ErrorAction Stop | Where-Object { $_.PartitionNumber -eq $EspAccess.PartitionNumber } | Remove-PartitionAccessPath -AccessPath "$($EspAccess.DriveLetter):" -ErrorAction Stop WriteLog "Removed temporary drive letter $($EspAccess.DriveLetter): from the EFI system partition." } catch { WriteLog "Warning: Failed to remove temporary EFI access path $($EspAccess.DriveLetter):. $($_.Exception.Message)" } } function Save-StorageSnapshot { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$StagePath, [Parameter(Mandatory = $true)] [int]$DiskNumber ) # Capture disk, partition, and volume state for the selected target disk. try { $storagePath = Join-Path -Path $StagePath -ChildPath 'Storage' New-Item -Path $storagePath -ItemType Directory -Force -ErrorAction Stop | Out-Null $disk = Get-Disk -Number $DiskNumber -ErrorAction Stop $partitions = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop | Sort-Object PartitionNumber) $partitionTable = @( $partitions | Select-Object ` PartitionNumber, DriveLetter, Type, GptType, @{ Name = 'SizeGB'; Expression = { if ($_.Size) { [math]::Round(($_.Size / 1GB), 2) } else { $null } } } ) $volumeRecords = @() foreach ($partition in $partitions) { $partitionVolume = $null if ($partition.DriveLetter) { try { $partitionVolume = Get-Volume -DriveLetter $partition.DriveLetter -ErrorAction Stop } catch { $partitionVolume = $null } } $volumeRecords += [PSCustomObject]@{ PartitionNumber = $partition.PartitionNumber DriveLetter = $partition.DriveLetter FileSystem = if ($partitionVolume) { $partitionVolume.FileSystem } else { $null } FileSystemLabel = if ($partitionVolume) { $partitionVolume.FileSystemLabel } else { $null } HealthStatus = if ($partitionVolume) { $partitionVolume.HealthStatus } else { $null } SizeGB = if ($partitionVolume -and $partitionVolume.Size) { [math]::Round(($partitionVolume.Size / 1GB), 2) } else { $null } FreeGB = if ($partitionVolume -and $partitionVolume.SizeRemaining) { [math]::Round(($partitionVolume.SizeRemaining / 1GB), 2) } else { $null } } } Write-DiagnosticsTextFile -FilePath (Join-Path -Path $storagePath -ChildPath 'disk.txt') -Content (($disk | Format-List * | Out-String).Trim()) Write-DiagnosticsTextFile -FilePath (Join-Path -Path $storagePath -ChildPath 'partitions.txt') -Content (($partitionTable | Format-Table -AutoSize | Out-String).Trim()) Write-DiagnosticsTextFile -FilePath (Join-Path -Path $storagePath -ChildPath 'volumes.txt') -Content (($volumeRecords | Format-Table -AutoSize | Out-String).Trim()) WriteLog "Storage snapshot [$StageName]: Disk=$DiskNumber; Partitions=$($partitions.Count); Volumes=$($volumeRecords.Count)." } catch { WriteLog "Warning: Failed to capture storage snapshot [$StageName] for disk $DiskNumber. $($_.Exception.Message)" } } function Get-CertificateSha256 { [CmdletBinding()] param( [Parameter()] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) # Hash a certificate's DER bytes for comparison with db/dbx entries. if ($null -eq $Certificate) { return $null } return Get-ByteArraySha256 -Bytes $Certificate.RawData } function Get-ByteArrayHexString { [CmdletBinding()] param( [byte[]]$Bytes, [string]$Delimiter = '' ) # Convert bytes to uppercase hexadecimal for readable reports and comparisons. if ($null -eq $Bytes) { return $null } return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join $Delimiter) } function Get-EfiGuidString { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [byte[]]$Bytes, [Parameter(Mandatory = $true)] [int]$Offset ) # Read an EFI_GUID from a byte array at the specified offset. if ($Offset -lt 0 -or ($Offset + 16) -gt $Bytes.Length) { return $null } $guidBytes = [byte[]]::new(16) [Array]::Copy($Bytes, $Offset, $guidBytes, 0, 16) try { return ([System.Guid]::new($guidBytes)).Guid } catch { return $null } } function Get-EfiSignatureTypeName { [CmdletBinding()] param( [string]$SignatureTypeGuid ) # Map well-known EFI signature type GUIDs to friendly names. if ([string]::IsNullOrWhiteSpace($SignatureTypeGuid)) { return 'UNKNOWN' } switch ($SignatureTypeGuid.ToLowerInvariant()) { 'a5c059a1-94e4-4aa7-87b5-ab155c2bf072' { return 'EFI_CERT_X509' } 'c1c41626-504c-4092-aca9-41f936934328' { return 'EFI_CERT_SHA256' } '3bd2a492-96c0-4079-b420-fcf98ef103ed' { return 'EFI_CERT_X509_SHA256' } '826ca512-cf10-4ac9-b187-be01496631bd' { return 'EFI_CERT_SHA1' } '67f8444f-8743-48f1-a328-1eaab8736080' { return 'EFI_CERT_SHA224' } 'ff3e5307-9fd0-48c9-85f1-8ad56c701e01' { return 'EFI_CERT_SHA384' } '093e0fae-a6c4-4f50-9f1b-d41e2b89c19a' { return 'EFI_CERT_SHA512' } default { return 'UNKNOWN' } } } function Get-EfiSignatureDatabaseEntries { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [byte[]]$Bytes, [Parameter(Mandatory = $true)] [string]$VariableName ) # Parse EFI signature database bytes into typed entries. $parsedEntries = [System.Collections.Generic.List[object]]::new() if ($null -eq $Bytes -or $Bytes.Length -lt 28) { return @() } $offset = 0 $listIndex = 0 while (($offset + 28) -le $Bytes.Length) { $listIndex++ $signatureTypeGuid = Get-EfiGuidString -Bytes $Bytes -Offset $offset $signatureListSize = [BitConverter]::ToUInt32($Bytes, $offset + 16) $signatureHeaderSize = [BitConverter]::ToUInt32($Bytes, $offset + 20) $signatureSize = [BitConverter]::ToUInt32($Bytes, $offset + 24) if ([string]::IsNullOrWhiteSpace($signatureTypeGuid) -or $signatureListSize -lt 28 -or $signatureSize -lt 16 -or ($offset + $signatureListSize) -gt $Bytes.Length) { break } $signatureTypeName = Get-EfiSignatureTypeName -SignatureTypeGuid $signatureTypeGuid $entryStartOffset = $offset + 28 + $signatureHeaderSize $usableBytes = $signatureListSize - 28 - $signatureHeaderSize if ($usableBytes -lt 0) { break } $entryCount = if ($signatureSize -gt 0) { [int][math]::Floor($usableBytes / $signatureSize) } else { 0 } for ($entryIndex = 0; $entryIndex -lt $entryCount; $entryIndex++) { $currentOffset = $entryStartOffset + ($entryIndex * $signatureSize) if (($currentOffset + $signatureSize) -gt ($offset + $signatureListSize)) { break } $signatureOwnerGuid = Get-EfiGuidString -Bytes $Bytes -Offset $currentOffset $signatureDataLength = [int]$signatureSize - 16 if ($signatureDataLength -lt 0) { continue } $signatureData = [byte[]]::new($signatureDataLength) [Array]::Copy($Bytes, $currentOffset + 16, $signatureData, 0, $signatureDataLength) $entry = [ordered]@{ VariableName = $VariableName SignatureListIndex = $listIndex SignatureEntryIndex = $entryIndex + 1 SignatureTypeGuid = $signatureTypeGuid SignatureTypeName = $signatureTypeName SignatureOwnerGuid = $signatureOwnerGuid SignatureDataLength = $signatureDataLength HashHex = $null CertificateSubject = $null CertificateIssuer = $null CertificateNotBefore = $null CertificateNotAfter = $null CertificateThumbprint = $null CertificateSha256 = $null DataSha256 = $null EntrySummary = $null } switch ($signatureTypeName) { 'EFI_CERT_X509' { try { $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($signatureData) $entry.CertificateSubject = $certificate.Subject $entry.CertificateIssuer = $certificate.Issuer $entry.CertificateNotBefore = $certificate.NotBefore.ToString('yyyy-MM-dd HH:mm:ss') $entry.CertificateNotAfter = $certificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') $entry.CertificateThumbprint = $certificate.Thumbprint $entry.CertificateSha256 = Get-CertificateSha256 -Certificate $certificate $entry.EntrySummary = "CertificateSubject=$($entry.CertificateSubject)" } catch { $entry.DataSha256 = Get-ByteArraySha256 -Bytes $signatureData $entry.EntrySummary = "CertificateParseError=$($_.Exception.Message)" } } 'EFI_CERT_SHA256' { $entry.HashHex = Get-ByteArrayHexString -Bytes $signatureData $entry.EntrySummary = "ImageHash=$($entry.HashHex)" } 'EFI_CERT_X509_SHA256' { $entry.HashHex = Get-ByteArrayHexString -Bytes $signatureData $entry.EntrySummary = "CertificateHash=$($entry.HashHex)" } default { $entry.DataSha256 = Get-ByteArraySha256 -Bytes $signatureData $entry.EntrySummary = "DataSha256=$($entry.DataSha256)" } } $parsedEntries.Add([PSCustomObject]$entry) | Out-Null } if ($signatureListSize -le 0) { break } $offset += [int]$signatureListSize } return @($parsedEntries) } function Write-EfiSignatureDatabaseReport { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$VariableName, [Parameter(Mandatory = $true)] [object[]]$Entries, [Parameter(Mandatory = $true)] [string]$ReportPath ) # Write a readable parsed report and a smaller summary for EFI signature database entries. $summaryPath = Join-Path -Path (Split-Path -Path $ReportPath -Parent) -ChildPath "${VariableName}_summary.txt" $typeGroups = @($Entries | Group-Object SignatureTypeName | Sort-Object Name) $ownerGroups = @($Entries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.SignatureOwnerGuid) } | Group-Object SignatureOwnerGuid | Sort-Object Name) $hashEntries = @($Entries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.HashHex) }) $uniqueHashCount = @($hashEntries | Select-Object -ExpandProperty HashHex -Unique).Count $duplicateHashGroups = @( $hashEntries | Group-Object HashHex | Where-Object { $_.Count -gt 1 } | Sort-Object -Property @{ Expression = 'Count'; Descending = $true }, @{ Expression = 'Name'; Descending = $false } ) $duplicateHashValueCount = $duplicateHashGroups.Count $duplicateHashEntryCount = if ($duplicateHashGroups.Count -gt 0) { ($duplicateHashGroups | Measure-Object -Property Count -Sum).Sum } else { 0 } $reportLines = @( "VariableName: $VariableName" "ParsedEntryCount: $($Entries.Count)" "UniqueHashCount: $uniqueHashCount" "DuplicateHashValueCount: $duplicateHashValueCount" "DuplicateHashEntryCount: $duplicateHashEntryCount" "OwnerGuidCount: $($ownerGroups.Count)" ) $summaryLines = @( "VariableName: $VariableName" "ParsedEntryCount: $($Entries.Count)" "UniqueHashCount: $uniqueHashCount" "DuplicateHashValueCount: $duplicateHashValueCount" "DuplicateHashEntryCount: $duplicateHashEntryCount" "OwnerGuidCount: $($ownerGroups.Count)" ) if ($Entries.Count -gt 0) { $reportLines += '' $reportLines += 'ParsedTypeCounts:' $summaryLines += '' $summaryLines += 'ParsedTypeCounts:' foreach ($typeGroup in $typeGroups) { $reportLines += "$($typeGroup.Name): $($typeGroup.Count)" $summaryLines += "$($typeGroup.Name): $($typeGroup.Count)" } $reportLines += '' $reportLines += 'OwnerGuidCounts:' $summaryLines += '' $summaryLines += 'OwnerGuidCounts:' if ($ownerGroups.Count -gt 0) { foreach ($ownerGroup in $ownerGroups) { $reportLines += "$($ownerGroup.Name): $($ownerGroup.Count)" $summaryLines += "$($ownerGroup.Name): $($ownerGroup.Count)" } } else { $reportLines += '' $summaryLines += '' } $reportLines += '' $reportLines += 'DuplicateHashes:' $summaryLines += '' $summaryLines += 'DuplicateHashes:' if ($duplicateHashGroups.Count -gt 0) { foreach ($duplicateHashGroup in $duplicateHashGroups) { $reportLines += "$($duplicateHashGroup.Name): $($duplicateHashGroup.Count)" $summaryLines += "$($duplicateHashGroup.Name): $($duplicateHashGroup.Count)" } } else { $reportLines += '' $summaryLines += '' } $reportLines += '' $reportLines += 'Entries:' foreach ($entry in $Entries) { $reportLines += "[List $($entry.SignatureListIndex), Entry $($entry.SignatureEntryIndex)] Type=$($entry.SignatureTypeName) ($($entry.SignatureTypeGuid))" $reportLines += "SignatureOwnerGuid: $($entry.SignatureOwnerGuid)" $reportLines += "SignatureDataLength: $($entry.SignatureDataLength)" if ($entry.CertificateSubject) { $reportLines += "CertificateSubject: $($entry.CertificateSubject)" $reportLines += "CertificateIssuer: $($entry.CertificateIssuer)" $reportLines += "CertificateNotBefore: $($entry.CertificateNotBefore)" $reportLines += "CertificateNotAfter: $($entry.CertificateNotAfter)" $reportLines += "CertificateThumbprint: $($entry.CertificateThumbprint)" $reportLines += "CertificateSha256: $($entry.CertificateSha256)" } elseif ($entry.HashHex) { $reportLines += "HashHex: $($entry.HashHex)" } else { $reportLines += "DataSha256: $($entry.DataSha256)" } if ($entry.EntrySummary) { $reportLines += "EntrySummary: $($entry.EntrySummary)" } $reportLines += '' } } else { $reportLines += '' $reportLines += 'ParsedTypeCounts: ' $reportLines += '' $reportLines += 'OwnerGuidCounts: ' $reportLines += '' $reportLines += 'DuplicateHashes: ' $reportLines += '' $reportLines += 'Entries: ' $summaryLines += '' $summaryLines += 'ParsedTypeCounts: ' $summaryLines += '' $summaryLines += 'OwnerGuidCounts: ' $summaryLines += '' $summaryLines += 'DuplicateHashes: ' } Write-DiagnosticsTextFile -FilePath $ReportPath -Content $reportLines Write-DiagnosticsTextFile -FilePath $summaryPath -Content $summaryLines } function Save-SecureBootVariableDiagnostics { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$FirmwarePath, [Parameter(Mandatory = $true)] [string]$VariableName ) # Export raw Secure Boot variable data and parse EFI signature database entries when available. $textPath = Join-Path -Path $FirmwarePath -ChildPath "$VariableName.txt" $parsedReportPath = Join-Path -Path $FirmwarePath -ChildPath "${VariableName}_parsed.txt" if ($null -eq (Get-Command -Name Get-SecureBootUEFI -ErrorAction SilentlyContinue)) { Write-DiagnosticsTextFile -FilePath $textPath -Content 'Get-SecureBootUEFI is not available.' Write-DiagnosticsTextFile -FilePath $parsedReportPath -Content 'Get-SecureBootUEFI is not available.' WriteLog "Secure Boot variable [$StageName] $VariableName unavailable: Get-SecureBootUEFI not available." return $null } try { try { $variable = Get-SecureBootUEFI -Name $VariableName -ErrorAction Stop } catch [System.Management.Automation.ParameterBindingException] { $variable = Get-SecureBootUEFI $VariableName -ErrorAction Stop } $bytes = [byte[]]@() if ($variable -and $variable.PSObject.Properties['Bytes']) { $bytes = [byte[]]$variable.Bytes } elseif ($variable -and $variable.PSObject.Properties['Content']) { $bytes = [byte[]]$variable.Content } $binPath = Join-Path -Path $FirmwarePath -ChildPath "$VariableName.bin" [System.IO.File]::WriteAllBytes($binPath, $bytes) $sha256 = Get-ByteArraySha256 -Bytes $bytes $marker = Get-ByteArrayAsciiMarker -Bytes $bytes $markerText = if ($marker) { $marker } else { '' } $parsedEntries = @(Get-EfiSignatureDatabaseEntries -Bytes $bytes -VariableName $VariableName) $typeSummaryText = if ($parsedEntries.Count -gt 0) { (($parsedEntries | Group-Object SignatureTypeName | Sort-Object Name | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join '; ') } else { '' } $hashEntries = @($parsedEntries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.HashHex) }) $uniqueHashCount = @($hashEntries | Select-Object -ExpandProperty HashHex -Unique).Count $duplicateHashValueCount = @($hashEntries | Group-Object HashHex | Where-Object { $_.Count -gt 1 }).Count $ownerGuidCount = @($parsedEntries | Where-Object { -not [string]::IsNullOrWhiteSpace($_.SignatureOwnerGuid) } | Group-Object SignatureOwnerGuid).Count $summaryLines = @( "VariableName: $VariableName" "ByteCount: $($bytes.Length)" "SHA256: $sha256" "AsciiMarker: $markerText" "ParsedEntryCount: $($parsedEntries.Count)" "ParsedTypeCounts: $typeSummaryText" "UniqueHashCount: $uniqueHashCount" "DuplicateHashValueCount: $duplicateHashValueCount" "OwnerGuidCount: $ownerGuidCount" '' 'Details:' ($variable | Format-List * | Out-String).TrimEnd() ) Write-DiagnosticsTextFile -FilePath $textPath -Content $summaryLines Write-EfiSignatureDatabaseReport -VariableName $VariableName -Entries $parsedEntries -ReportPath $parsedReportPath WriteLog "Secure Boot variable [$StageName] $($VariableName): Bytes=$($bytes.Length); SHA256=$sha256; Marker=$markerText; ParsedEntries=$($parsedEntries.Count); ParsedTypes=$typeSummaryText; UniqueHashes=$uniqueHashCount; DuplicateHashValues=$duplicateHashValueCount; OwnerGuidCount=$ownerGuidCount." return [PSCustomObject]@{ VariableName = $VariableName Bytes = $bytes Sha256 = $sha256 Marker = $markerText ParsedEntries = $parsedEntries } } catch { Write-DiagnosticsTextFile -FilePath $textPath -Content "Error: $($_.Exception.Message)" Write-DiagnosticsTextFile -FilePath $parsedReportPath -Content "Error: $($_.Exception.Message)" WriteLog "Secure Boot variable [$StageName] $($VariableName) unavailable: $($_.Exception.Message)" return $null } } function Save-FirmwareSecureBootDiagnostics { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$StagePath ) # Collect firmware and Secure Boot state without affecting deployment flow. $firmwarePath = Join-Path -Path $StagePath -ChildPath 'Firmware' try { New-Item -Path $firmwarePath -ItemType Directory -Force -ErrorAction Stop | Out-Null } catch { WriteLog "Warning: Failed to create firmware diagnostics folder for stage $StageName. $($_.Exception.Message)" return $null } $summaryLines = @( "Stage: $StageName" "Timestamp: $(Get-Date -Format 's')" ) try { $controlValues = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name PEFirmwareType -ErrorAction Stop $peFirmwareType = $controlValues.PEFirmwareType $peFirmwareTypeText = switch ($peFirmwareType) { 1 { 'BIOS' } 2 { 'UEFI' } default { 'Unknown' } } $summaryLines += "PEFirmwareType: $peFirmwareType ($peFirmwareTypeText)" WriteLog "Firmware state [$StageName]: PEFirmwareType=$peFirmwareType ($peFirmwareTypeText)." } catch { $summaryLines += 'PEFirmwareType: ' WriteLog "Firmware state [$StageName]: PEFirmwareType unavailable. $($_.Exception.Message)" } if ($null -ne (Get-Command -Name Confirm-SecureBootUEFI -ErrorAction SilentlyContinue)) { try { $confirmResult = Confirm-SecureBootUEFI -ErrorAction Stop $summaryLines += "Confirm-SecureBootUEFI: $confirmResult" WriteLog "Firmware state [$StageName]: Confirm-SecureBootUEFI=$confirmResult." } catch { $summaryLines += "Confirm-SecureBootUEFI: $($_.Exception.Message)" WriteLog "Firmware state [$StageName]: Confirm-SecureBootUEFI failed. $($_.Exception.Message)" } } else { $summaryLines += 'Confirm-SecureBootUEFI: ' WriteLog "Firmware state [$StageName]: Confirm-SecureBootUEFI not available." } Write-DiagnosticsTextFile -FilePath (Join-Path -Path $firmwarePath -ChildPath 'firmware-summary.txt') -Content $summaryLines $variableEvidence = [ordered]@{} foreach ($variableName in @('PK', 'KEK', 'db', 'dbx')) { $currentVariableEvidence = Save-SecureBootVariableDiagnostics -StageName $StageName -FirmwarePath $firmwarePath -VariableName $variableName if ($null -ne $currentVariableEvidence) { $variableEvidence[$variableName] = $currentVariableEvidence } } return [PSCustomObject]$variableEvidence } function Get-CertificateChainEvidence { [CmdletBinding()] param( [Parameter()] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) # Build a best-effort certificate chain and look for 2011 or 2023 markers. $result = [PSCustomObject]@{ Marker = '' BuildSucceeded = $false ChainStatusText = '' ChainElementText = @() ChainCertificateHashes = @() } if ($null -eq $Certificate) { return $result } $markerCandidates = @( 'Windows UEFI CA 2023', 'Windows UEFI CA 2011', 'Microsoft Corporation UEFI CA 2011', 'Microsoft Corporation KEK CA 2011', 'Microsoft Windows Production PCA 2011', 'Microsoft Windows' ) $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() try { # Avoid revocation/network dependencies in WinPE while still building the local chain. $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck $chain.ChainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::IgnoreEndRevocationUnknown $result.BuildSucceeded = $chain.Build($Certificate) $chainStatusLines = @() foreach ($chainStatus in $chain.ChainStatus) { $statusInformation = $chainStatus.StatusInformation if (-not [string]::IsNullOrWhiteSpace($statusInformation)) { $chainStatusLines += "$($chainStatus.Status): $($statusInformation.Trim())" } } if ($chainStatusLines.Count -gt 0) { $result.ChainStatusText = $chainStatusLines -join ' | ' } $elementLines = @() $chainCertificateHashes = @() $index = 0 foreach ($chainElement in $chain.ChainElements) { $index++ $chainCertificate = $chainElement.Certificate $certificateSha256 = Get-CertificateSha256 -Certificate $chainCertificate if ($certificateSha256) { $chainCertificateHashes += $certificateSha256 } $elementLines += "[{0}] Subject={1}; Issuer={2}; Thumbprint={3}; NotBefore={4}; NotAfter={5}; CertificateSha256={6}" -f ` $index, ` $chainCertificate.Subject, ` $chainCertificate.Issuer, ` $chainCertificate.Thumbprint, ` $chainCertificate.NotBefore.ToString('yyyy-MM-dd HH:mm:ss'), ` $chainCertificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'), ` $certificateSha256 if ($result.Marker -eq '') { foreach ($markerCandidate in $markerCandidates) { if ($chainCertificate.Subject -match [regex]::Escape($markerCandidate) -or $chainCertificate.Issuer -match [regex]::Escape($markerCandidate)) { $result.Marker = $markerCandidate break } } } } $result.ChainElementText = $elementLines $result.ChainCertificateHashes = @($chainCertificateHashes) } catch { $result.ChainStatusText = "Error: $($_.Exception.Message)" } finally { $chain.Dispose() } return $result } function Save-BootFileArtifact { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$BootFilesPath, [Parameter(Mandatory = $true)] [pscustomobject]$BootEntry ) # Capture metadata and a copy of each requested boot-chain file when present. $metadataRoot = Join-Path -Path $BootFilesPath -ChildPath 'Metadata' $safeFileName = ($BootEntry.CopyRelativePath -replace '[\\/:*?"<>| ]', '_') + '.txt' $metadataPath = Join-Path -Path $metadataRoot -ChildPath $safeFileName $summaryLines = @( "Label: $($BootEntry.Label)" "Path: $($BootEntry.Path)" ) if ([string]::IsNullOrWhiteSpace($BootEntry.Path) -or -not (Test-Path -Path $BootEntry.Path -PathType Leaf)) { $summaryLines += 'Exists: False' Write-DiagnosticsTextFile -FilePath $metadataPath -Content $summaryLines WriteLog "Boot file [$StageName] $($BootEntry.CopyRelativePath): Missing." return [PSCustomObject]@{ Label = $BootEntry.Label CopyRelativePath = $BootEntry.CopyRelativePath Exists = $false FileHash = $null SignatureStatus = 'Missing' CertificateMarker = '' ChainCertificateHashes = @() } } try { $fileItem = Get-Item -Path $BootEntry.Path -ErrorAction Stop $summaryLines += 'Exists: True' $summaryLines += "Size: $($fileItem.Length)" $hashValue = $null try { $hashValue = (Get-FileHash -Path $BootEntry.Path -Algorithm SHA256 -ErrorAction Stop).Hash } catch { $hashValue = $null } $summaryLines += "SHA256: $(if ($hashValue) { $hashValue } else { '' })" $fileVersion = $null $productVersion = $null if ($fileItem.VersionInfo) { $fileVersion = $fileItem.VersionInfo.FileVersion $productVersion = $fileItem.VersionInfo.ProductVersion } $summaryLines += "FileVersion: $(if ($fileVersion) { $fileVersion } else { '' })" $summaryLines += "ProductVersion: $(if ($productVersion) { $productVersion } else { '' })" $skipSignatureCheck = if ($BootEntry.PSObject.Properties['SkipSignatureCheck']) { [bool]$BootEntry.SkipSignatureCheck } else { $false } $signatureStatus = if ($skipSignatureCheck) { '' } else { '' } $signerSubject = '' $signerIssuer = '' $signerThumbprint = '' $signerNotBefore = '' $signerNotAfter = '' $certificateMarker = '' $chainBuildSucceeded = $false $chainStatusText = '' $chainElementText = @() $chainCertificateHashes = @() if (-not $skipSignatureCheck -and $null -ne (Get-Command -Name Get-AuthenticodeSignature -ErrorAction SilentlyContinue)) { try { $signature = Get-AuthenticodeSignature -FilePath $BootEntry.Path -ErrorAction Stop $signatureStatus = [string]$signature.Status if ($signature.SignerCertificate) { $signerSubject = $signature.SignerCertificate.Subject $signerIssuer = $signature.SignerCertificate.Issuer $signerThumbprint = $signature.SignerCertificate.Thumbprint $signerNotBefore = $signature.SignerCertificate.NotBefore.ToString('yyyy-MM-dd HH:mm:ss') $signerNotAfter = $signature.SignerCertificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') $chainEvidence = Get-CertificateChainEvidence -Certificate $signature.SignerCertificate $certificateMarker = $chainEvidence.Marker $chainBuildSucceeded = $chainEvidence.BuildSucceeded $chainStatusText = $chainEvidence.ChainStatusText $chainElementText = $chainEvidence.ChainElementText $chainCertificateHashes = @($chainEvidence.ChainCertificateHashes | ForEach-Object { $_.ToUpperInvariant() }) } } catch { $signatureStatus = " $($_.Exception.Message)" } } $summaryLines += "AuthenticodeStatus: $signatureStatus" $summaryLines += "SignerSubject: $signerSubject" $summaryLines += "SignerIssuer: $signerIssuer" $summaryLines += "SignerThumbprint: $signerThumbprint" $summaryLines += "SignerNotBefore: $signerNotBefore" $summaryLines += "SignerNotAfter: $signerNotAfter" $summaryLines += "CertificateMarker: $certificateMarker" $summaryLines += "ChainBuildSucceeded: $chainBuildSucceeded" $summaryLines += "ChainStatus: $chainStatusText" if ($chainElementText.Count -gt 0) { $summaryLines += '' $summaryLines += 'CertificateChain:' $summaryLines += $chainElementText } $copyPath = Join-Path -Path (Join-Path -Path $BootFilesPath -ChildPath 'Files') -ChildPath $BootEntry.CopyRelativePath try { New-Item -Path (Split-Path -Path $copyPath -Parent) -ItemType Directory -Force -ErrorAction Stop | Out-Null Copy-Item -Path $BootEntry.Path -Destination $copyPath -Force -ErrorAction Stop $summaryLines += "CopiedTo: $copyPath" } catch { $summaryLines += 'CopiedTo: ' $summaryLines += "CopyError: $($_.Exception.Message)" } Write-DiagnosticsTextFile -FilePath $metadataPath -Content $summaryLines $logHash = if ($hashValue) { $hashValue } else { '' } WriteLog "Boot file [$StageName] $($BootEntry.CopyRelativePath): Size=$($fileItem.Length); SHA256=$logHash; Signature=$signatureStatus; Marker=$certificateMarker; Signer=$signerSubject." return [PSCustomObject]@{ Label = $BootEntry.Label CopyRelativePath = $BootEntry.CopyRelativePath Exists = $true FileHash = if ($hashValue) { $hashValue.ToUpperInvariant() } else { $null } SignatureStatus = $signatureStatus CertificateMarker = $certificateMarker ChainCertificateHashes = @($chainCertificateHashes) } } catch { $summaryLines += "Error: $($_.Exception.Message)" Write-DiagnosticsTextFile -FilePath $metadataPath -Content $summaryLines WriteLog "Warning: Failed to inspect boot file [$StageName] $($BootEntry.CopyRelativePath). $($_.Exception.Message)" return [PSCustomObject]@{ Label = $BootEntry.Label CopyRelativePath = $BootEntry.CopyRelativePath Exists = $false FileHash = $null SignatureStatus = " $($_.Exception.Message)" CertificateMarker = '' ChainCertificateHashes = @() } } } function Save-BootFileDiagnostics { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$StagePath, [Parameter(Mandatory = $true)] [int]$DiskNumber, [Parameter()] [string]$WindowsDrivePath = 'W:\' ) # Inspect EFI and OS boot files using a temporary ESP access path when needed. $bootFilesPath = Join-Path -Path $StagePath -ChildPath 'BootFiles' try { New-Item -Path $bootFilesPath -ItemType Directory -Force -ErrorAction Stop | Out-Null } catch { WriteLog "Warning: Failed to create boot-file diagnostics folder for stage $StageName. $($_.Exception.Message)" return @() } $bootEvidence = @() $espAccess = Open-EspPartitionAccess -DiskNumber $DiskNumber try { $espRoot = if ($null -ne $espAccess) { $espAccess.DrivePath } else { $null } $bootEntries = @( [PSCustomObject]@{ Label = 'ESP Microsoft Boot Manager' Path = if ($espRoot) { Join-Path -Path $espRoot -ChildPath 'EFI\Microsoft\Boot\bootmgfw.efi' } else { $null } CopyRelativePath = 'ESP\EFI\Microsoft\Boot\bootmgfw.efi' }, [PSCustomObject]@{ Label = 'ESP fallback bootx64' Path = if ($espRoot) { Join-Path -Path $espRoot -ChildPath 'EFI\Boot\bootx64.efi' } else { $null } CopyRelativePath = 'ESP\EFI\Boot\bootx64.efi' }, [PSCustomObject]@{ Label = 'ESP BCD store' Path = if ($espRoot) { Join-Path -Path $espRoot -ChildPath 'EFI\Microsoft\Boot\BCD' } else { $null } CopyRelativePath = 'ESP\EFI\Microsoft\Boot\BCD' SkipSignatureCheck = $true } ) if (-not [string]::IsNullOrWhiteSpace($WindowsDrivePath) -and (Test-Path -Path $WindowsDrivePath)) { $bootEntries += [PSCustomObject]@{ Label = 'Offline Windows winload' Path = Join-Path -Path $WindowsDrivePath -ChildPath 'Windows\System32\winload.efi' CopyRelativePath = 'Windows\Windows\System32\winload.efi' } } else { WriteLog "Boot file [$StageName]: Skipping offline Windows loader inspection because $WindowsDrivePath is not accessible." } foreach ($bootEntry in $bootEntries) { $bootEvidence += Save-BootFileArtifact -StageName $StageName -BootFilesPath $bootFilesPath -BootEntry $bootEntry } } finally { Close-EspPartitionAccess -EspAccess $espAccess } return @($bootEvidence) } function Invoke-DiagnosticsCommand { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter()] [string]$ArgumentList, [Parameter(Mandatory = $true)] [string]$OutputFilePath, [Parameter(Mandatory = $true)] [string]$LogLabel ) # Run a diagnostics command, save the full output, and mirror it to ScriptLog. $stdOutTempFile = "$env:TEMP\$((New-Guid).Guid)" $stdErrTempFile = "$env:TEMP\$((New-Guid).Guid)" try { $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -RedirectStandardOutput $stdOutTempFile -RedirectStandardError $stdErrTempFile -Wait -PassThru -NoNewWindow -ErrorAction Stop $stdOutContent = if (Test-Path -Path $stdOutTempFile) { Get-Content -Path $stdOutTempFile -Raw } else { '' } $stdErrContent = if (Test-Path -Path $stdErrTempFile) { Get-Content -Path $stdErrTempFile -Raw } else { '' } $outputParts = @() if (-not [string]::IsNullOrWhiteSpace($stdOutContent)) { $outputParts += $stdOutContent.TrimEnd() } if (-not [string]::IsNullOrWhiteSpace($stdErrContent)) { $outputParts += "STDERR:`r`n$($stdErrContent.TrimEnd())" } if ($outputParts.Count -eq 0) { $outputParts += '' } $combinedOutput = $outputParts -join "`r`n`r`n" Write-DiagnosticsTextFile -FilePath $OutputFilePath -Content $combinedOutput WriteLog "$LogLabel exit code: $($process.ExitCode)" foreach ($outputLine in ($combinedOutput -split "`r?`n")) { if (-not [string]::IsNullOrWhiteSpace($outputLine)) { WriteLog $outputLine } } return $process.ExitCode } catch { $errorMessage = $_.Exception.Message Write-DiagnosticsTextFile -FilePath $OutputFilePath -Content @( "Command: $FilePath $ArgumentList" "Error: $errorMessage" ) WriteLog "Warning: $LogLabel failed. $errorMessage" return $null } finally { Remove-Item -Path $stdOutTempFile, $stdErrTempFile -Force -ErrorAction SilentlyContinue } } function Invoke-SecureBootDiagnostics { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter()] [string]$DiagnosticsRoot, [Parameter(Mandatory = $true)] [int]$DiskNumber, [Parameter()] [string]$WindowsDrivePath = 'W:\', [Parameter()] [bool]$IncludeBootFiles = $true ) # Collect firmware and storage telemetry, and optionally collect boot-file telemetry. if ([string]::IsNullOrWhiteSpace($DiagnosticsRoot)) { WriteLog "Secure Boot diagnostics [$StageName] skipped: diagnostics folder unavailable." return $null } $stagePath = New-DiagnosticsStageFolder -DiagnosticsRoot $DiagnosticsRoot -StageName $StageName if ([string]::IsNullOrWhiteSpace($stagePath)) { WriteLog "Secure Boot diagnostics [$StageName] skipped: stage folder unavailable." return $null } $firmwareEvidence = Save-FirmwareSecureBootDiagnostics -StageName $StageName -StagePath $stagePath Save-StorageSnapshot -StageName $StageName -StagePath $stagePath -DiskNumber $DiskNumber $bootFileEvidence = @() if ($IncludeBootFiles) { $bootFileEvidence = @(Save-BootFileDiagnostics -StageName $StageName -StagePath $stagePath -DiskNumber $DiskNumber -WindowsDrivePath $WindowsDrivePath) } else { WriteLog "Secure Boot diagnostics [$StageName]: Boot-file inspection skipped." } return [PSCustomObject]@{ StageName = $StageName StagePath = $stagePath FirmwareEvidence = $firmwareEvidence BootFileEvidence = @($bootFileEvidence) } } function Get-BcdSettingValue { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$BcdOutputPath, [Parameter(Mandatory = $true)] [string]$SettingName ) # Extract a single setting value from a saved bcdedit output file. if (-not (Test-Path -Path $BcdOutputPath -PathType Leaf)) { return $null } try { $pattern = '^\s*' + [regex]::Escape($SettingName) + '\s+(.+)$' foreach ($line in Get-Content -Path $BcdOutputPath -ErrorAction Stop) { $match = [regex]::Match($line, $pattern) if ($match.Success) { return $match.Groups[1].Value.Trim() } } } catch { WriteLog "Warning: Failed to parse BCD setting '$SettingName' from $BcdOutputPath. $($_.Exception.Message)" } return $null } function Write-BcdSummary { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter(Mandatory = $true)] [string]$BcdPath ) # Surface the highest value BCD fields directly in ScriptLog and a summary file. $bootMgrPath = Join-Path -Path $BcdPath -ChildPath 'bcdedit_bootmgr_v.txt' $defaultPath = Join-Path -Path $BcdPath -ChildPath 'bcdedit_default_v.txt' $bootMgrDevice = Get-BcdSettingValue -BcdOutputPath $bootMgrPath -SettingName 'device' $bootMgrLoaderPath = Get-BcdSettingValue -BcdOutputPath $bootMgrPath -SettingName 'path' $defaultDevice = Get-BcdSettingValue -BcdOutputPath $defaultPath -SettingName 'device' $defaultOsDevice = Get-BcdSettingValue -BcdOutputPath $defaultPath -SettingName 'osdevice' $defaultLoaderPath = Get-BcdSettingValue -BcdOutputPath $defaultPath -SettingName 'path' $summaryObject = [PSCustomObject]@{ StageName = $StageName BootMgrDevice = $bootMgrDevice BootMgrPath = $bootMgrLoaderPath DefaultDevice = $defaultDevice DefaultOsDevice = $defaultOsDevice DefaultPath = $defaultLoaderPath } $summaryLines = @( "Stage: $StageName" "BootMgrDevice: $(if ($bootMgrDevice) { $bootMgrDevice } else { '' })" "BootMgrPath: $(if ($bootMgrLoaderPath) { $bootMgrLoaderPath } else { '' })" "DefaultDevice: $(if ($defaultDevice) { $defaultDevice } else { '' })" "DefaultOsDevice: $(if ($defaultOsDevice) { $defaultOsDevice } else { '' })" "DefaultPath: $(if ($defaultLoaderPath) { $defaultLoaderPath } else { '' })" ) Write-DiagnosticsTextFile -FilePath (Join-Path -Path $BcdPath -ChildPath 'bcd_summary.txt') -Content $summaryLines WriteLog "BCD [$StageName] Summary: {bootmgr}.device=$(if ($bootMgrDevice) { $bootMgrDevice } else { '' }); {bootmgr}.path=$(if ($bootMgrLoaderPath) { $bootMgrLoaderPath } else { '' }); {default}.device=$(if ($defaultDevice) { $defaultDevice } else { '' }); {default}.osdevice=$(if ($defaultOsDevice) { $defaultOsDevice } else { '' }); {default}.path=$(if ($defaultLoaderPath) { $defaultLoaderPath } else { '' })." return $summaryObject } function Write-BootExpectationSummary { [CmdletBinding()] param( [Parameter()] [pscustomobject]$PostApplyDiagnostics, [Parameter()] [pscustomobject]$BcdDiagnostics ) # Decide whether the device should boot based on parsed dbx, boot files, and BCD evidence. if ($null -eq $PostApplyDiagnostics -or $null -eq $BcdDiagnostics) { WriteLog 'Boot expectation: Unable to evaluate because required diagnostics were not available.' return } $bootEvidence = @($PostApplyDiagnostics.BootFileEvidence) $bootMgrEvidence = @($bootEvidence | Where-Object { $_.CopyRelativePath -eq 'ESP\EFI\Microsoft\Boot\bootmgfw.efi' }) | Select-Object -First 1 $fallbackEvidence = @($bootEvidence | Where-Object { $_.CopyRelativePath -eq 'ESP\EFI\Boot\bootx64.efi' }) | Select-Object -First 1 $winloadEvidence = @($bootEvidence | Where-Object { $_.CopyRelativePath -eq 'Windows\Windows\System32\winload.efi' }) | Select-Object -First 1 $dbxVariableEvidence = if ($PostApplyDiagnostics.FirmwareEvidence -and $PostApplyDiagnostics.FirmwareEvidence.PSObject.Properties['dbx']) { $PostApplyDiagnostics.FirmwareEvidence.dbx } else { $null } $dbxEntries = if ($dbxVariableEvidence -and $dbxVariableEvidence.PSObject.Properties['ParsedEntries']) { @($dbxVariableEvidence.ParsedEntries) } else { @() } $dbxImageHashEntries = @($dbxEntries | Where-Object { $_.SignatureTypeName -eq 'EFI_CERT_SHA256' -and -not [string]::IsNullOrWhiteSpace($_.HashHex) }) $dbxCertificateHashEntries = @($dbxEntries | Where-Object { ($_.SignatureTypeName -eq 'EFI_CERT_X509_SHA256' -and -not [string]::IsNullOrWhiteSpace($_.HashHex)) -or ($_.SignatureTypeName -eq 'EFI_CERT_X509' -and -not [string]::IsNullOrWhiteSpace($_.CertificateSha256)) }) $dbxImageHashes = @($dbxImageHashEntries | ForEach-Object { $_.HashHex.ToUpperInvariant() } | Select-Object -Unique) $dbxCertificateHashes = @($dbxCertificateHashEntries | ForEach-Object { if (-not [string]::IsNullOrWhiteSpace($_.HashHex)) { $_.HashHex.ToUpperInvariant() } elseif (-not [string]::IsNullOrWhiteSpace($_.CertificateSha256)) { $_.CertificateSha256.ToUpperInvariant() } } | Select-Object -Unique) $bootExpectation = 'ExpectedToBootBasedOnCollectedEvidence' $reasonLines = [System.Collections.Generic.List[string]]::new() $warningLines = [System.Collections.Generic.List[string]]::new() $bootComparisonLines = [System.Collections.Generic.List[string]]::new() if ($null -eq $bootMgrEvidence -or -not $bootMgrEvidence.Exists) { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add('ESP bootmgfw.efi is missing.') | Out-Null } elseif ($bootMgrEvidence.SignatureStatus -ne 'Valid') { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add("ESP bootmgfw.efi signature status is $($bootMgrEvidence.SignatureStatus).") | Out-Null } if ($null -eq $winloadEvidence -or -not $winloadEvidence.Exists) { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add('Windows winload.efi is missing.') | Out-Null } elseif ($winloadEvidence.SignatureStatus -ne 'Valid') { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add("Windows winload.efi signature status is $($winloadEvidence.SignatureStatus).") | Out-Null } if ($null -ne $fallbackEvidence -and -not $fallbackEvidence.Exists) { $warningLines.Add('ESP bootx64.efi is missing, so fallback boot relies on the Windows Boot Manager firmware entry.') | Out-Null } foreach ($currentEvidence in @($bootMgrEvidence, $fallbackEvidence, $winloadEvidence)) { if ($null -eq $currentEvidence) { continue } $matchingImageEntries = @() $matchingImageEntryRefs = @() if ($currentEvidence.Exists -and -not [string]::IsNullOrWhiteSpace($currentEvidence.FileHash)) { $matchingImageEntries = @($dbxImageHashEntries | Where-Object { $_.HashHex.ToUpperInvariant() -eq $currentEvidence.FileHash.ToUpperInvariant() }) $matchingImageEntryRefs = @($matchingImageEntries | ForEach-Object { "List $($_.SignatureListIndex), Entry $($_.SignatureEntryIndex)" } | Select-Object -Unique) } $matchingCertificateEntries = @() $matchingCertificateEntryRefs = @() foreach ($chainCertificateHash in @($currentEvidence.ChainCertificateHashes)) { if ([string]::IsNullOrWhiteSpace($chainCertificateHash)) { continue } $matchingCertificateEntries += @($dbxCertificateHashEntries | Where-Object { $candidateHash = if (-not [string]::IsNullOrWhiteSpace($_.HashHex)) { $_.HashHex } else { $_.CertificateSha256 } -not [string]::IsNullOrWhiteSpace($candidateHash) -and $candidateHash.ToUpperInvariant() -eq $chainCertificateHash.ToUpperInvariant() }) } if ($matchingCertificateEntries.Count -gt 0) { $matchingCertificateEntryRefs = @($matchingCertificateEntries | ForEach-Object { "List $($_.SignatureListIndex), Entry $($_.SignatureEntryIndex)" } | Select-Object -Unique) } $matchedDbxImage = $matchingImageEntryRefs.Count -gt 0 $matchedDbxCertificate = $matchingCertificateEntryRefs.Count -gt 0 if ($matchedDbxImage) { $bootExpectation = 'LikelyBlockedByDbx' $reasonLines.Add("$($currentEvidence.CopyRelativePath) hash matches dbx EFI_CERT_SHA256 entry or entries: $($matchingImageEntryRefs -join '; ').") | Out-Null } if ($matchedDbxCertificate) { $bootExpectation = 'LikelyBlockedByDbx' $reasonLines.Add("$($currentEvidence.CopyRelativePath) certificate chain hash matches dbx certificate entry or entries: $($matchingCertificateEntryRefs -join '; ').") | Out-Null } $bootComparisonLines.Add("$($currentEvidence.CopyRelativePath): Exists=$($currentEvidence.Exists); SignatureStatus=$($currentEvidence.SignatureStatus); FileHash=$(if ($currentEvidence.FileHash) { $currentEvidence.FileHash } else { '' }); MatchedDbxImage=$matchedDbxImage; MatchedDbxCertificate=$matchedDbxCertificate; MatchingDbxImageEntries=$(if ($matchingImageEntryRefs.Count -gt 0) { $matchingImageEntryRefs -join '; ' } else { '' }); MatchingDbxCertificateEntries=$(if ($matchingCertificateEntryRefs.Count -gt 0) { $matchingCertificateEntryRefs -join '; ' } else { '' })") | Out-Null } $bcdSummary = if ($BcdDiagnostics.PSObject.Properties['Summary']) { $BcdDiagnostics.Summary } else { $null } if ($null -eq $bcdSummary) { if ($bootExpectation -eq 'ExpectedToBootBasedOnCollectedEvidence') { $bootExpectation = 'Inconclusive' } $reasonLines.Add('BCD summary was not available.') | Out-Null } else { if ([string]::IsNullOrWhiteSpace($bcdSummary.BootMgrDevice)) { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add('{bootmgr}.device is missing.') | Out-Null } if ([string]::IsNullOrWhiteSpace($bcdSummary.BootMgrPath) -or $bcdSummary.BootMgrPath -ne '\EFI\Microsoft\Boot\bootmgfw.efi') { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add("{bootmgr}.path is '$($bcdSummary.BootMgrPath)' instead of '\EFI\Microsoft\Boot\bootmgfw.efi'.") | Out-Null } if ([string]::IsNullOrWhiteSpace($bcdSummary.DefaultDevice)) { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add('{default}.device is missing.') | Out-Null } if ([string]::IsNullOrWhiteSpace($bcdSummary.DefaultOsDevice)) { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add('{default}.osdevice is missing.') | Out-Null } if ([string]::IsNullOrWhiteSpace($bcdSummary.DefaultPath) -or $bcdSummary.DefaultPath -ne '\Windows\system32\winload.efi') { $bootExpectation = 'LikelyNotBootable' $reasonLines.Add("{default}.path is '$($bcdSummary.DefaultPath)' instead of '\Windows\system32\winload.efi'.") | Out-Null } } if ($reasonLines.Count -eq 0) { $reasonLines.Add('No direct dbx image-hash or certificate-hash matches were found for bootmgfw.efi, bootx64.efi, or winload.efi, and the collected BCD paths point to bootmgfw.efi and winload.efi.') | Out-Null $warningLines.Add('OEM-specific UEFI behavior can still prevent boot even when the collected evidence looks correct.') | Out-Null } $expectationLines = @( "ExpectedBootOutcome: $bootExpectation" "DbxImageHashEntryCount: $($dbxImageHashes.Count)" "DbxCertificateEntryCount: $($dbxCertificateHashes.Count)" "WindowsBootManagerSignatureStatus: $(if ($bootMgrEvidence) { $bootMgrEvidence.SignatureStatus } else { '' })" "WindowsLoaderSignatureStatus: $(if ($winloadEvidence) { $winloadEvidence.SignatureStatus } else { '' })" "WindowsFallbackBootSignatureStatus: $(if ($fallbackEvidence) { $fallbackEvidence.SignatureStatus } else { '' })" ) if ($bootComparisonLines.Count -gt 0) { $expectationLines += '' $expectationLines += 'BootFileComparison:' $expectationLines += @($bootComparisonLines) } if ($reasonLines.Count -gt 0) { $expectationLines += '' $expectationLines += 'Reasons:' $expectationLines += @($reasonLines) } if ($warningLines.Count -gt 0) { $expectationLines += '' $expectationLines += 'Warnings:' $expectationLines += @($warningLines) } Write-DiagnosticsTextFile -FilePath (Join-Path -Path $BcdDiagnostics.BcdPath -ChildPath 'boot_expectation.txt') -Content $expectationLines WriteLog "Boot expectation: $bootExpectation. $($reasonLines -join ' ')" } function Save-BcdDiagnostics { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$StageName, [Parameter()] [string]$DiagnosticsRoot, [Parameter(Mandatory = $true)] [int]$DiskNumber ) # Capture the final BCD state after boot order configuration. if ([string]::IsNullOrWhiteSpace($DiagnosticsRoot)) { WriteLog "BCD diagnostics [$StageName] skipped: diagnostics folder unavailable." return $null } $stagePath = New-DiagnosticsStageFolder -DiagnosticsRoot $DiagnosticsRoot -StageName $StageName if ([string]::IsNullOrWhiteSpace($stagePath)) { WriteLog "BCD diagnostics [$StageName] skipped: stage folder unavailable." return $null } $bcdPath = Join-Path -Path $stagePath -ChildPath 'BCD' try { New-Item -Path $bcdPath -ItemType Directory -Force -ErrorAction Stop | Out-Null } catch { WriteLog "Warning: Failed to create BCD diagnostics folder for stage $StageName. $($_.Exception.Message)" return $null } $commands = @( [PSCustomObject]@{ Label = 'fwbootmgr' Arguments = '/enum {fwbootmgr} /v' FileName = 'bcdedit_fwbootmgr_v.txt' }, [PSCustomObject]@{ Label = 'bootmgr' Arguments = '/enum {bootmgr} /v' FileName = 'bcdedit_bootmgr_v.txt' }, [PSCustomObject]@{ Label = 'default' Arguments = '/enum {default} /v' FileName = 'bcdedit_default_v.txt' }, [PSCustomObject]@{ Label = 'firmware' Arguments = '/enum firmware /v' FileName = 'bcdedit_firmware_v.txt' } ) foreach ($command in $commands) { Invoke-DiagnosticsCommand -FilePath 'bcdedit.exe' -ArgumentList $command.Arguments -OutputFilePath (Join-Path -Path $bcdPath -ChildPath $command.FileName) -LogLabel "BCD [$StageName] $($command.Label)" | Out-Null } $espAccess = Open-EspPartitionAccess -DiskNumber $DiskNumber try { $espBcdPath = if ($null -ne $espAccess) { Join-Path -Path $espAccess.DrivePath -ChildPath 'EFI\Microsoft\Boot\BCD' } else { $null } $storeOutputPath = Join-Path -Path $bcdPath -ChildPath 'bcdedit_store_all_v.txt' if ($espBcdPath -and (Test-Path -Path $espBcdPath -PathType Leaf)) { $storeArguments = "/store `"$espBcdPath`" /enum all /v" Invoke-DiagnosticsCommand -FilePath 'bcdedit.exe' -ArgumentList $storeArguments -OutputFilePath $storeOutputPath -LogLabel "BCD [$StageName] store_all" | Out-Null } else { Write-DiagnosticsTextFile -FilePath $storeOutputPath -Content 'ESP BCD store not found.' WriteLog "BCD [$StageName] ESP store enumeration skipped: offline ESP BCD not found." } } finally { Close-EspPartitionAccess -EspAccess $espAccess } $summaryObject = Write-BcdSummary -StageName $StageName -BcdPath $bcdPath return [PSCustomObject]@{ StageName = $StageName StagePath = $stagePath BcdPath = $bcdPath Summary = $summaryObject } } function New-DriverSubstMapping { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$SourcePath ) $resolvedPath = (Resolve-Path -Path $SourcePath -ErrorAction Stop).Path $driveLetter = Get-AvailableDriveLetter if ($null -eq $driveLetter) { throw 'No drive letters are available for SUBST mapping.' } $driveName = "$driveLetter`:" $mappedPath = "$driveLetter`:\" WriteLog "Mapping driver folder '$resolvedPath' to $driveName with SUBST." $escapedPath = $resolvedPath -replace '"', '""' $arguments = "/c subst $driveName `"$escapedPath`"" Invoke-Process -FilePath cmd.exe -ArgumentList $arguments return [PSCustomObject]@{ DriveLetter = $driveLetter DriveName = $driveName DrivePath = $mappedPath } } function Remove-DriverSubstMapping { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DriveLetter ) $driveName = "$DriveLetter`:" WriteLog "Removing SUBST drive $driveName" try { $arguments = "/c subst $driveName /d" Invoke-Process -FilePath cmd.exe -ArgumentList $arguments } catch { WriteLog "Failed to remove SUBST drive $($driveName): $_" } } #Get USB Drive and create log file $LogFileName = 'ScriptLog.txt' $USBDrive = Get-USBDrive New-item -Path $USBDrive -Name $LogFileName -ItemType "file" -Force | Out-Null $LogFile = $USBDrive + $LogFilename $version = '2603.2' WriteLog 'Begin Logging' WriteLog "Script version: $version" # Create the per-run diagnostics folder used for Secure Boot telemetry. $secureBootDiagnosticsPath = New-SecureBootDiagnosticsFolder -UsbDrive $USBDrive # Display banner and version $banner = @" ███████╗███████╗██╗ ██╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ███████╗██████╗ ██╔════╝██╔════╝██║ ██║ ██╔══██╗██║ ██║██║██║ ██╔══██╗██╔════╝██╔══██╗ █████╗ █████╗ ██║ ██║ ██████╔╝██║ ██║██║██║ ██║ ██║█████╗ ██████╔╝ ██╔══╝ ██╔══╝ ██║ ██║ ██╔══██╗██║ ██║██║██║ ██║ ██║██╔══╝ ██╔══██╗ ██║ ██║ ╚██████╔╝ ██████╔╝╚██████╔╝██║███████╗██████╔╝███████╗██║ ██║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝ ╚═╝ "@ Write-Host $banner -ForegroundColor Cyan Write-Host "Version $version" -ForegroundColor Cyan #Find PhysicalDrive Write-SectionHeader -Title 'Target Disk Selection' $diskDriveCandidates = @(Get-HardDrive) $diskCount = $diskDriveCandidates.Count if ($diskCount -eq 0) { $errorMessage = 'No hard drive found. You may need to add storage drivers to the WinPE image.' WriteLog ($errorMessage + ' Exiting.') WriteLog 'To add drivers, place them in the PEDrivers folder and re-run the creation script with -CopyPEDrivers $true, or add them manually via DISM.' Stop-Script -Message $errorMessage } # Select target disk - prompt user if multiple disks found if ($diskCount -eq 1) { $selectedDisk = $diskDriveCandidates[0] WriteLog "Single fixed disk detected: DiskNumber=$($selectedDisk.Index), Model=$($selectedDisk.Model)" Write-Host "Single fixed disk detected: $($selectedDisk.Model)" } else { WriteLog "Found $diskCount fixed disks. Prompting for selection." Write-Host "Found $diskCount fixed disks" # Build list of available disk indexes for validation $validDiskIndexes = @($diskDriveCandidates | ForEach-Object { $_.Index }) # Display disk list using actual disk index as the selection value $displayList = @() foreach ($currentDisk in $diskDriveCandidates) { $sizeGB = [math]::Round(($currentDisk.Size / 1GB), 2) $displayList += [PSCustomObject]@{ Disk = $currentDisk.Index 'Size (GB)' = $sizeGB 'Sector' = $currentDisk.BytesPerSector 'Bus Type' = $currentDisk.InterfaceType Model = $currentDisk.Model } } $displayList | Format-Table -AutoSize -Property Disk, 'Size (GB)', Sector, 'Bus Type', Model do { try { $var = $true [int]$diskSelection = Read-Host 'Enter the disk number to apply the FFU to' } catch { Write-Host 'Input was not in correct format. Please enter a valid disk number' $var = $false } # Validate selected disk is in the list of available disks if ($var -and $validDiskIndexes -notcontains $diskSelection) { Write-Host "Invalid disk number. Please select from the available disks." $var = $false } } until ($var) $selectedDisk = $diskDriveCandidates | Where-Object { $_.Index -eq $diskSelection } WriteLog "Disk selection: DiskNumber=$($selectedDisk.Index), Model=$($selectedDisk.Model), SizeGB=$([math]::Round(($selectedDisk.Size / 1GB), 2)), BusType=$($selectedDisk.InterfaceType)" Write-Host "`nDisk $($selectedDisk.Index) selected: $($selectedDisk.Model)" } # Set variables from selected disk $PhysicalDeviceID = $selectedDisk.DeviceID $BytesPerSector = $selectedDisk.BytesPerSector $DiskID = $selectedDisk.Index $diskSizeGB = [math]::Round(($selectedDisk.Size / 1GB), 2) # Create hardDrive object for Get-SystemInformation compatibility $hardDrive = [PSCustomObject]@{ DeviceID = $PhysicalDeviceID BytesPerSector = $BytesPerSector DiskSize = $selectedDisk.Size DiskNumber = $DiskID } WriteLog "Physical DeviceID is $PhysicalDeviceID" WriteLog "DiskNumber is $DiskID with size $diskSizeGB GB" # Gather and write system information $sysInfoObject = Get-SystemInformation -HardDrive $hardDrive Write-SystemInformation -SystemInformation $sysInfoObject # Capture baseline Secure Boot and storage diagnostics before the target disk is wiped. $null = Invoke-SecureBootDiagnostics -StageName 'Baseline' -DiagnosticsRoot $secureBootDiagnosticsPath -DiskNumber $DiskID -IncludeBootFiles $false #Find FFU Files Write-SectionHeader 'FFU File Selection' [array]$FFUFiles = @(Get-ChildItem -Path $USBDrive*.ffu) $FFUCount = $FFUFiles.Count #If multiple FFUs found, ask which to install If ($FFUCount -gt 1) { WriteLog "Found $FFUCount FFU Files" Write-Host "Found $FFUCount FFU Files" $array = @() for ($i = 0; $i -le $FFUCount - 1; $i++) { $Properties = [ordered]@{Number = $i + 1 ; FFUFile = $FFUFiles[$i].FullName } $array += New-Object PSObject -Property $Properties } $array | Format-Table -AutoSize -Property Number, FFUFile | Out-Host do { try { $var = $true [int]$FFUSelected = Read-Host 'Enter the FFU number to install' $FFUSelected = $FFUSelected - 1 } catch { Write-Host 'Input was not in correct format. Please enter a valid FFU number' $var = $false } } until (($FFUSelected -le $FFUCount - 1) -and $var) $FFUFileToInstall = $array[$FFUSelected].FFUFile WriteLog "$FFUFileToInstall was selected" } elseif ($FFUCount -eq 1) { WriteLog "Found $FFUCount FFU File" Write-Host "Found $FFUCount FFU File" $FFUFileToInstall = $FFUFiles[0].FullName WriteLog "$FFUFileToInstall will be installed" Write-Host "$FFUFileToInstall will be installed" } else { $errorMessage = 'No FFU files found.' Writelog $errorMessage Stop-Script -Message $errorMessage } #FindAP $APFolder = $USBDrive + "Autopilot\" If (Test-Path -Path $APFolder) { [array]$APFiles = @(Get-ChildItem -Path $APFolder*.json) $APFilesCount = $APFiles.Count if ($APFilesCount -ge 1) { $autopilot = $true } } #FindPPKG $PPKGFolder = $USBDrive + "PPKG\" if (Test-Path -Path $PPKGFolder) { [array]$PPKGFiles = @(Get-ChildItem -Path $PPKGFolder*.ppkg) $PPKGFilesCount = $PPKGFiles.Count if ($PPKGFilesCount -ge 1) { $PPKG = $true } } #FindUnattend $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) { $Unattend = $true } } If (Test-Path -Path $UnattendPrefixPath) { $UnattendPrefixFile = Get-ChildItem -Path $UnattendPrefixPath If ($UnattendPrefixFile) { $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 -or $UnattendPrefix -or $UnattendComputerName) { Write-SectionHeader 'Device Name Selection' if ($Unattend -and $UnattendPrefix) { Writelog 'Unattend file found with prefixes.txt. Getting prefixes.' $UnattendPrefixes = @(Get-content $UnattendPrefixFile) $UnattendPrefixCount = $UnattendPrefixes.Count If ($UnattendPrefixCount -gt 1) { WriteLog "Found $UnattendPrefixCount Prefixes" $array = @() for ($i = 0; $i -le $UnattendPrefixCount - 1; $i++) { $Properties = [ordered]@{Number = $i + 1 ; DeviceNamePrefix = $UnattendPrefixes[$i] } $array += New-Object PSObject -Property $Properties } $array | Format-Table -AutoSize -Property Number, DeviceNamePrefix do { try { $var = $true [int]$PrefixSelected = Read-Host 'Enter the prefix number to use for the device name' $PrefixSelected = $PrefixSelected - 1 } catch { Write-Host 'Input was not in correct format. Please enter a valid prefix number' $var = $false } } until (($PrefixSelected -le $UnattendPrefixCount - 1) -and $var) $PrefixToUse = $array[$PrefixSelected].DeviceNamePrefix WriteLog "$PrefixToUse was selected" Write-Host "`n$PrefixToUse was selected as device name prefix" } elseif ($UnattendPrefixCount -eq 1) { WriteLog "Found $UnattendPrefixCount Prefix" Write-Host "Found $UnattendPrefixCount Prefix" $PrefixToUse = $UnattendPrefixes[0] WriteLog "Will use $PrefixToUse as device name prefix" Write-Host "Will use $PrefixToUse as device name prefix" } #Get serial number to append. This can make names longer than 15 characters. Trim any leading or trailing whitespace $serial = (Get-CimInstance -ClassName win32_bios).SerialNumber.Trim() #Combine prefix with serial $computername = ($PrefixToUse + $serial) -replace "\s", "" # Remove spaces because windows does not support spaces in the computer names #If computername is longer than 15 characters, reduce to 15. Sysprep/unattend doesn't like ComputerName being longer than 15 characters even though Windows accepts it If ($computername.Length -gt 15) { $computername = $computername.substring(0, 15) } $computername = Set-Computername($computername) Writelog "Computer name will be set to $computername" Write-Host "Computer name will be set to $computername" } 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 will be set to $computername" Write-Host "Computer name will be set to $computername" } else { Writelog 'No matching serial number found in SerialComputerNames.csv. Setting random computer name to complete setup.' Write-Host '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 will be set to $computername" Write-Host "Computer name will be set to $computername" } } elseif ($Unattend) { Writelog 'Unattend file found with no prefixes.txt, asking for name' Write-Host 'Unattend file found but no prefixes.txt. Please enter a device name.' [string]$computername = Read-Host 'Enter device name' $computername = Set-Computername($computername) Writelog "Computer name will be set to $computername" Write-Host "Computer name will be set to $computername" } else { WriteLog 'Device naming assets detected without unattend.xml. Skipping device naming prompts.' } } else { WriteLog 'No unattend folder found. Device name will be set via PPKG, AP JSON, or default OS name.' } #If both AP and PPKG folder found with files, ask which to use. If ($autopilot -eq $true -and $PPKG -eq $true) { WriteLog 'Both PPKG and Autopilot json files found' Write-Host 'Both Autopilot JSON files and Provisioning packages were found.' do { try { $var = $true [int]$APorPPKG = Read-Host 'Enter 1 for Autopilot or 2 for Provisioning Package' } catch { Write-Host 'Incorrect value. Please enter 1 for Autopilot or 2 for Provisioning Package' $var = $false } } until (($APorPPKG -gt 0 -and $APorPPKG -lt 3) -and $var) If ($APorPPKG -eq 1) { $PPKG = $false } else { $autopilot = $false } } #If multiple AP json files found, ask which to install If ($APFilesCount -gt 1 -and $autopilot -eq $true) { WriteLog "Found $APFilesCount Autopilot json Files" $array = @() for ($i = 0; $i -le $APFilesCount - 1; $i++) { $Properties = [ordered]@{Number = $i + 1 ; APFile = $APFiles[$i].FullName; APFileName = $APFiles[$i].Name } $array += New-Object PSObject -Property $Properties } $array | Format-Table -AutoSize -Property Number, APFileName do { try { $var = $true [int]$APFileSelected = Read-Host 'Enter the AP json file number to install' $APFileSelected = $APFileSelected - 1 } catch { Write-Host 'Input was not in correct format. Please enter a valid AP json file number' $var = $false } } until (($APFileSelected -le $APFilesCount - 1) -and $var) $APFileToInstall = $array[$APFileSelected].APFile $APFileName = $array[$APFileSelected].APFileName WriteLog "$APFileToInstall was selected" } elseif ($APFilesCount -eq 1 -and $autopilot -eq $true) { WriteLog "Found $APFilesCount AP File" $APFileToInstall = $APFiles[0].FullName $APFileName = $APFiles[0].Name WriteLog "$APFileToInstall will be copied" } else { Writelog 'No AP files found or AP was not selected' } #If multiple PPKG files found, ask which to install If ($PPKGFilesCount -gt 1 -and $PPKG -eq $true) { Write-SectionHeader -Title 'Provisioning Package Selection' WriteLog "Found $PPKGFilesCount PPKG Files" $array = @() for ($i = 0; $i -le $PPKGFilesCount - 1; $i++) { $Properties = [ordered]@{Number = $i + 1 ; PPKGFile = $PPKGFiles[$i].FullName; PPKGFileName = $PPKGFiles[$i].Name } $array += New-Object PSObject -Property $Properties } $array | Format-Table -AutoSize -Property Number, PPKGFileName do { try { $var = $true [int]$PPKGFileSelected = Read-Host 'Enter the PPKG file number to install' $PPKGFileSelected = $PPKGFileSelected - 1 } catch { Write-Host 'Input was not in correct format. Please enter a valid PPKG file number' $var = $false } } until (($PPKGFileSelected -le $PPKGFilesCount - 1) -and $var) $PPKGFileToInstall = $array[$PPKGFileSelected].PPKGFile WriteLog "$PPKGFileToInstall was selected" Write-Host "`n$PPKGFileToInstall will be used" } elseif ($PPKGFilesCount -eq 1 -and $PPKG -eq $true) { Write-SectionHeader -Title 'Provisioning Package Selection' WriteLog "Found $PPKGFilesCount PPKG File" Write-Host "Found $PPKGFilesCount PPKG File" $PPKGFileToInstall = $PPKGFiles[0].FullName WriteLog "$PPKGFileToInstall will be used" Write-Host "`n$PPKGFileToInstall will be used" } else { Writelog 'No PPKG files found or PPKG not selected.' } #Find Drivers $DriversPath = $USBDrive + "Drivers" $DriverSourcePath = $null $DriverSourceType = $null # Will be 'WIM' or 'Folder' $driverMappingPath = Join-Path -Path $DriversPath -ChildPath "DriverMapping.json" If (Test-Path -Path $DriversPath) { Write-SectionHeader -Title 'Drivers Selection' } # --- Automatic Driver Detection using DriverMapping.json --- if (Test-Path -Path $driverMappingPath -PathType Leaf) { WriteLog "DriverMapping.json found at $driverMappingPath. Attempting automatic driver selection." Write-Host "DriverMapping.json found. Attempting automatic driver selection." try { $driverMappings = Get-Content -Path $driverMappingPath | Out-String | ConvertFrom-Json -ErrorAction Stop $driverMappings = @($driverMappings) | Where-Object { $null -ne $_ } if ($driverMappings.Count -eq 0) { throw "DriverMapping.json does not contain any entries." } if ($null -eq $sysInfoObject) { $sysInfoObject = Get-SystemInformation -HardDrive $hardDrive } $identifierLabelForLog = $null $identifierValueForLog = $null if ($sysInfoObject.PSObject.Properties['Machine Type'] -and -not [string]::IsNullOrWhiteSpace($sysInfoObject.'Machine Type')) { $identifierLabelForLog = 'Machine Type' $identifierValueForLog = $sysInfoObject.'Machine Type' } elseif ($sysInfoObject.PSObject.Properties['System ID'] -and -not [string]::IsNullOrWhiteSpace($sysInfoObject.'System ID')) { $identifierLabelForLog = 'System ID' $identifierValueForLog = $sysInfoObject.'System ID' } else { $identifierLabelForLog = 'System ID' $identifierValueForLog = 'Not Detected' } WriteLog ("Detected System: Manufacturer='{0}', Model='{1}', {2}='{3}'" -f $sysInfoObject.Manufacturer, $sysInfoObject.Model, $identifierLabelForLog, $identifierValueForLog) Write-Host ("Detected System: Manufacturer='{0}', Model='{1}'" -f $sysInfoObject.Manufacturer, $sysInfoObject.Model) $matchedRule = Find-DriverMappingRule -SystemInformation $sysInfoObject -DriverMappings $driverMappings if ($null -ne $matchedRule) { WriteLog "Automatic match found: Manufacturer='$($matchedRule.Manufacturer)', Model='$($matchedRule.Model)'" Write-Host "Automatic match found: Manufacturer='$($matchedRule.Manufacturer)', Model='$($matchedRule.Model)'" $potentialDriverPath = Join-Path -Path $DriversPath -ChildPath $matchedRule.DriverPath if (Test-Path -Path $potentialDriverPath) { $DriverSourcePath = $potentialDriverPath if ($DriverSourcePath -like '*.wim') { $DriverSourceType = 'WIM' } else { $DriverSourceType = 'Folder' } WriteLog "Automatically selected driver source. Type: $DriverSourceType, Path: $DriverSourcePath" Write-Host "Automatically selected driver source. Type: $DriverSourceType, Path: $DriverSourcePath" } else { WriteLog "Matched driver path '$potentialDriverPath' not found. Falling back to manual selection." Write-Host "Matched driver path '$potentialDriverPath' not found. Falling back to manual selection." } } else { WriteLog "No automatic driver mapping rule matched identifiers for this system. Falling back to manual selection." Write-Host "No matching driver mapping rule was found for this system. Falling back to manual selection." } } catch { WriteLog "An error occurred during automatic driver detection: $($_.Exception.Message). Falling back to manual selection." Write-Host "An error occurred during automatic driver detection: $($_.Exception.Message). Falling back to manual selection." } } else { WriteLog "DriverMapping.json not found. Proceeding with manual driver selection." } # --- Manual Driver Selection (Fallback) --- if ($null -eq $DriverSourcePath) { If (Test-Path -Path $DriversPath) { WriteLog "Searching for driver WIMs and folders in $DriversPath" # Collect all WIM-based driver sources anywhere under Drivers $wimFiles = Get-ChildItem -Path $DriversPath -Filter *.wim -File -Recurse -ErrorAction SilentlyContinue # Treat each immediate child folder as a manufacturer container (supports known and unknown vendors) $manufacturerFolders = Get-ChildItem -Path $DriversPath -Directory -ErrorAction SilentlyContinue $driversRootFullPath = (Get-Item -Path $DriversPath).FullName.TrimEnd('\') $relativePathResolver = { param( [string]$candidatePath, [string]$rootPath ) try { $normalizedPath = [System.IO.Path]::GetFullPath($candidatePath) if ($normalizedPath.StartsWith($rootPath, [System.StringComparison]::OrdinalIgnoreCase)) { $relativeSegment = $normalizedPath.Substring($rootPath.Length).TrimStart('\', '/') if ([string]::IsNullOrWhiteSpace($relativeSegment)) { return Split-Path -Path $normalizedPath -Leaf } return $relativePath = $relativeSegment } return $normalizedPath } catch { return $candidatePath } } # Create a combined list $DriverSources = @() foreach ($wimFile in $wimFiles) { $relativePath = & $relativePathResolver -candidatePath $wimFile.FullName -rootPath $driversRootFullPath $DriverSources += [PSCustomObject]@{ Type = 'WIM' Path = $wimFile.FullName RelativePath = $relativePath } } foreach ($manufacturerFolder in $manufacturerFolders) { $modelFolders = Get-ChildItem -Path $manufacturerFolder.FullName -Directory -ErrorAction SilentlyContinue if ($null -eq $modelFolders -or $modelFolders.Count -eq 0) { if (Test-DriverFolderHasInstallableContent -Path $manufacturerFolder.FullName) { $relativePath = & $relativePathResolver -candidatePath $manufacturerFolder.FullName -rootPath $driversRootFullPath $DriverSources += [PSCustomObject]@{ Type = 'Folder' Path = $manufacturerFolder.FullName RelativePath = $relativePath } WriteLog "Using manufacturer folder '$($manufacturerFolder.FullName)' as a driver source because it contains installable content." } else { WriteLog "Skipping '$($manufacturerFolder.FullName)' because it has no model folders with installable content." } continue } foreach ($modelFolder in $modelFolders) { if (-not (Test-DriverFolderHasInstallableContent -Path $modelFolder.FullName)) { WriteLog "Skipping driver folder '$($modelFolder.FullName)' because no installable files were found." continue } $relativePath = & $relativePathResolver -candidatePath $modelFolder.FullName -rootPath $driversRootFullPath $DriverSources += [PSCustomObject]@{ Type = 'Folder' Path = $modelFolder.FullName RelativePath = $relativePath } } } $DriverSourcesCount = $DriverSources.Count if ($DriverSourcesCount -gt 0) { WriteLog "Found $DriverSourcesCount total driver sources (WIMs and folders)." if ($DriverSourcesCount -eq 1) { $DriverSourcePath = $DriverSources[0].Path $DriverSourceType = $DriverSources[0].Type $selectedRelativePath = $DriverSources[0].RelativePath WriteLog "Single driver source found. Type: $DriverSourceType, Path: $DriverSourcePath, RelativePath: $selectedRelativePath" Write-Host "Single driver source found. Type: $DriverSourceType, RelativePath: $selectedRelativePath" } else { # Multiple sources found, prompt user WriteLog "Multiple driver sources found. Prompting for selection." $displayArray = @() for ($i = 0; $i -lt $DriverSourcesCount; $i++) { $displayArray += [PSCustomObject]@{ Number = $i + 1 Type = $DriverSources[$i].Type RelativePath = $DriverSources[$i].RelativePath Path = $DriverSources[$i].Path } } $displayArray | Format-Table -Property Number, Type, RelativePath -AutoSize $DriverSelected = -1 $skipDriverInstall = $false do { try { $var = $true [int]$userSelection = Read-Host 'Enter the number of the driver source to install (0 to skip)' if ($userSelection -eq 0) { $skipDriverInstall = $true break } $DriverSelected = $userSelection - 1 } catch { Write-Host 'Input was not in correct format. Please enter a valid number.' $var = $false } } until ((($DriverSelected -ge 0 -and $DriverSelected -lt $DriverSourcesCount) -or $skipDriverInstall) -and $var) if ($skipDriverInstall) { $DriverSourcePath = $null $DriverSourceType = $null $selectedRelativePath = $null WriteLog 'User chose to skip driver installation.' Write-Host "`nDriver installation was skipped." } else { $DriverSourcePath = $DriverSources[$DriverSelected].Path $DriverSourceType = $DriverSources[$DriverSelected].Type $selectedRelativePath = $DriverSources[$DriverSelected].RelativePath WriteLog "User selected Type: $DriverSourceType, Path: $DriverSourcePath, RelativePath: $selectedRelativePath" Write-Host "`nUser selected Type: $DriverSourceType, RelativePath: $selectedRelativePath" } } } else { WriteLog "No driver WIMs or folders found in Drivers directory." Write-Host "No driver WIMs or folders found in Drivers directory." } } else { WriteLog "Drivers folder not found at $DriversPath. Skipping driver installation." } } #Partition drive Writelog 'Clean Disk' $originalProgressPreference = $ProgressPreference try { $ProgressPreference = 'SilentlyContinue' $Disk = Get-Disk -Number $DiskID if ($Disk.PartitionStyle -ne "RAW") { $Disk | clear-disk -RemoveData -RemoveOEM -Confirm:$false } } catch { WriteLog 'Cleaning disk failed. Exiting' throw $_ } finally { $ProgressPreference = $originalProgressPreference } Writelog 'Cleaning Disk succeeded' #Apply FFU Write-SectionHeader -Title 'Applying FFU' WriteLog "Applying FFU to $PhysicalDeviceID" WriteLog "Running command dism /apply-ffu /ImageFile:$FFUFileToInstall /ApplyDrive:$PhysicalDeviceID" #In order for Applying Image progress bar to show up, need to call dism directly. Might be a better way to handle, but must have progress bar show up on screen. dism /apply-ffu /ImageFile:$FFUFileToInstall /ApplyDrive:$PhysicalDeviceID $dismExitCode = $LASTEXITCODE if ($dismExitCode -ne 0) { $errorMessage = "Failed to apply FFU. LastExitCode = $dismExitCode." if ($dismExitCode -eq 1393) { WriteLog "Failed to apply FFU - LastExitCode = $dismExitCode" WriteLog "This is likely due to a mismatched LogicalSectorSizeBytes" WriteLog "BytesPerSector value from Win32_Diskdrive is $BytesPerSector" if ($BytesPerSector -eq 4096) { WriteLog "The FFU build process by default uses a 512 LogicalSectorSizeBytes. Rebuild the FFU by adding -LogicalSectorSizeBytes 4096 to the command line" } elseif ($BytesPerSector -eq 512) { WriteLog "This FFU was likely built with a LogicalSectorSizeBytes of 4096. Rebuild the FFU by adding -LogicalSectorSizeBytes 512 to the command line" } $errorMessage += " This is likely due to a mismatched logical sector size. Check logs for details." } else { Writelog "Failed to apply FFU - LastExitCode = $dismExitCode also check dism.log on the USB drive for more info" $errorMessage += " Check dism.log on the USB drive for more info." } Stop-Script -Message $errorMessage } WriteLog 'Successfully applied FFU' # Verify Windows partition exists and assign drive letter $windowsPartition = Get-Partition -DiskNumber $DiskID | Where-Object { $_.PartitionNumber -eq 3 } if ($null -eq $windowsPartition) { $errorMessage = "Windows partition (Partition 3) not found after applying FFU, even though DISM reported success." WriteLog $errorMessage Stop-Script -Message $errorMessage } WriteLog "Assigning drive letter 'W' to Windows partition." Set-Partition -InputObject $windowsPartition -NewDriveLetter W # Verify the drive letter was set $windowsVolume = Get-Volume -DriveLetter W -ErrorAction SilentlyContinue if ($null -eq $windowsVolume) { $errorMessage = "Failed to assign drive letter 'W' to the Windows partition after applying FFU." WriteLog $errorMessage Stop-Script -Message $errorMessage } WriteLog "Successfully assigned drive letter 'W'." $recoveryPartition = Get-Partition -DiskNumber $DiskID | Where-Object PartitionNumber -eq 4 if ($recoveryPartition) { WriteLog 'Setting recovery partition attributes' $diskpartScript = @( "SELECT DISK $($Disk.Number)", "SELECT PARTITION $($recoveryPartition.PartitionNumber)", "GPT ATTRIBUTES=0x8000000000000001", "EXIT" ) $diskpartScript | diskpart.exe | Out-Null WriteLog 'Setting recovery partition attributes complete' } #Copy modified WinRE if folder exists, else copy inbox WinRE $WinRE = $USBDrive + "WinRE\winre.wim" If (Test-Path -Path $WinRE) { WriteLog 'Copying modified WinRE to Recovery directory' Get-Disk | Where-Object Number -eq $DiskID | Get-Partition | Where-Object Type -eq Recovery | Set-Partition -NewDriveLetter R Invoke-Process xcopy.exe "/h $WinRE R:\Recovery\WindowsRE\ /Y" WriteLog 'Copying WinRE to Recovery directory succeeded' WriteLog 'Registering location of recovery tools' Invoke-Process W:\Windows\System32\Reagentc.exe "/Setreimage /Path R:\Recovery\WindowsRE /Target W:\Windows" Get-Disk | Where-Object Number -eq $DiskID | Get-Partition | Where-Object Type -eq Recovery | Remove-PartitionAccessPath -AccessPath R: WriteLog 'Registering location of recovery tools succeeded' } # Capture post-apply Secure Boot, storage, and boot-chain diagnostics. $postApplyDiagnostics = Invoke-SecureBootDiagnostics -StageName 'PostApply' -DiagnosticsRoot $secureBootDiagnosticsPath -DiskNumber $DiskID -WindowsDrivePath 'W:\' #Autopilot JSON If ($APFileToInstall) { Write-SectionHeader -Title 'Applying Autopilot Configuration' WriteLog "Copying $APFileToInstall to W:\windows\provisioning\autopilot" Invoke-process xcopy.exe "$APFileToInstall W:\Windows\provisioning\autopilot\" WriteLog "Copying $APFileToInstall to W:\windows\provisioning\autopilot succeeded" # Rename file in W:\Windows\Provisioning\Autopilot to AutoPilotConfigurationFile.json try { Rename-Item -Path "W:\Windows\Provisioning\Autopilot\$APFileName" -NewName 'W:\Windows\Provisioning\Autopilot\AutoPilotConfigurationFile.json' WriteLog "Renamed W:\Windows\Provisioning\Autopilot\$APFilename to W:\Windows\Provisioning\Autopilot\AutoPilotConfigurationFile.json" } catch { Writelog "Copying $APFileToInstall to W:\windows\provisioning\autopilot failed with error: $_" throw $_ } } #Apply PPKG If ($PPKGFileToInstall) { Write-SectionHeader -Title 'Applying Provisioning Package' try { #Make sure to delete any existing PPKG on the USB drive Get-Childitem -Path $USBDrive\*.ppkg | ForEach-Object { Remove-item -Path $_.FullName } WriteLog "Copying $PPKGFileToInstall to $USBDrive" Write-Host "Copying $PPKGFileToInstall to $USBDrive" # Quote paths to handle PPKG filenames with spaces Invoke-process xcopy.exe """$PPKGFileToInstall"" ""$USBDrive""" WriteLog "Copying $PPKGFileToInstall to $USBDrive succeeded" Write-Host "Copying $PPKGFileToInstall to $USBDrive succeeded" } catch { Writelog "Copying $PPKGFileToInstall to $USBDrive failed with error: $_" Write-Host "Copying $PPKGFileToInstall to $USBDrive failed with error: $_" throw $_ } } #Set DeviceName If ($computername) { Write-SectionHeader -Title 'Applying Computer Name and Unattend Configuration' try { $PantherDir = 'w:\windows\panther' If (Test-Path -Path $PantherDir) { Writelog "Copying $UnattendFile to $PantherDir" Write-Host "Copying $UnattendFile to $PantherDir" Invoke-process xcopy "$UnattendFile $PantherDir /Y" WriteLog "Copying $UnattendFile to $PantherDir succeeded" Write-Host "Copying $UnattendFile to $PantherDir succeeded" } else { Writelog "$PantherDir doesn't exist, creating it" New-Item -Path $PantherDir -ItemType Directory -Force Writelog "Copying $UnattendFile to $PantherDir" Write-Host "Copying $UnattendFile to $PantherDir" Invoke-Process xcopy.exe "$UnattendFile $PantherDir" WriteLog "Copying $UnattendFile to $PantherDir succeeded" Write-Host "Copying $UnattendFile to $PantherDir succeeded" } } catch { WriteLog "Copying Unattend.xml to name device failed" Stop-Script -Message "Copying Unattend.xml to name device failed with error: $_" } } # Add Drivers if ($null -ne $DriverSourcePath) { Write-SectionHeader -Title 'Installing Drivers' if ($DriverSourceType -eq 'WIM') { WriteLog "Installing drivers from WIM: $DriverSourcePath" Write-Host "Installing drivers from WIM: $DriverSourcePath" $TempDriverDir = "W:\TempDrivers" try { # Create working folder for WIM-based drivers WriteLog "Creating temporary directory for drivers at $TempDriverDir" New-Item -Path $TempDriverDir -ItemType Directory -Force | Out-Null # Mount the driver WIM read-only so DISM can recurse the extracted INF tree WriteLog "Mounting WIM contents to $TempDriverDir" Write-Host "Mounting WIM contents to $TempDriverDir" # For some reason can't use /mount-image with invoke-process, so using dism.exe directly dism.exe /Mount-Image /ImageFile:$DriverSourcePath /Index:1 /MountDir:$TempDriverDir /ReadOnly /optimize $mountExitCode = $LASTEXITCODE if ($mountExitCode -ne 0) { throw "DISM WIM mount failed. LastExitCode = $mountExitCode." } WriteLog "WIM mount successful." # Inject drivers into the offline Windows image; failures here should not stop deployment WriteLog "Injecting drivers from $TempDriverDir" Write-Host "Injecting drivers from $TempDriverDir" Write-Host "This may take a while, please be patient." $driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:""$TempDriverDir"" /Recurse" -IgnoreExitCode -PassThruExitCode if ($driverInjectExitCode -ne 0) { $warningMessage = "Warning: One or more drivers failed to inject from WIM. ExitCode = $driverInjectExitCode. Continuing deployment." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow # Copy setupapi.offline.log to the USB drive when driver injection fails $setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log' if (Test-Path -Path $setupApiLogPath) { try { Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y" } catch { WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. " } } else { WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath" } } else { WriteLog "Driver injection from WIM succeeded." Write-Host "Driver injection from WIM succeeded." } } catch { $warningMessage = "Warning: An error occurred during WIM driver installation. Continuing deployment." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow # Copy troubleshooting logs to the USB drive when driver installation fails try { Invoke-Process cmd.exe "/c copy /Y ""X:\Windows\logs\dism\dism.log"" ""$($USBDrive)dism_driverinject.log""" } catch { WriteLog "Warning: Failed to copy dism.log to $USBDrive." } $setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log' if (Test-Path -Path $setupApiLogPath) { try { Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y" } catch { WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive." } } else { WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath" } } finally { if (Test-Path -Path $TempDriverDir) { # Always attempt to unmount and clean up; unmount failures should not stop deployment WriteLog "Unmounting WIM from $TempDriverDir" Write-Host "Unmounting WIM from $TempDriverDir" try { Invoke-Process dism.exe "/Unmount-Image /MountDir:""$TempDriverDir"" /Discard" WriteLog "Unmount successful." Write-Host "Unmount successful." } catch { $warningMessage = "Warning: Failed to unmount WIM from $TempDriverDir. Continuing cleanup." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow } WriteLog "Cleaning up temporary driver directory: $TempDriverDir" Write-Host "Cleaning up temporary driver directory: $TempDriverDir" try { Remove-Item -Path $TempDriverDir -Recurse -Force WriteLog "Cleanup successful." Write-Host "Cleanup successful." } catch { $warningMessage = "Warning: Failed to clean up temporary driver directory: $TempDriverDir." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow } } } } elseif ($DriverSourceType -eq 'Folder') { $substMapping = $null try { # Use SUBST to shorten long paths for DISM /Add-Driver $substMapping = New-DriverSubstMapping -SourcePath $DriverSourcePath $shortDriverPath = $substMapping.DrivePath WriteLog "Injecting drivers from folder via SUBST. Source: $DriverSourcePath, Mapped: $($substMapping.DriveName)" Write-Host "Injecting drivers from folder: $shortDriverPath" Write-Host "This may take a while, please be patient." # Inject drivers into the offline Windows image; failures here should not stop deployment $driverInjectExitCode = Invoke-Process -FilePath dism.exe -ArgumentList "/image:W:\ /Add-Driver /Driver:$shortDriverPath /Recurse" -IgnoreExitCode -PassThruExitCode if ($driverInjectExitCode -ne 0) { $warningMessage = "Warning: One or more drivers failed to inject from folder. ExitCode = $driverInjectExitCode. Continuing deployment." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow # Copy setupapi.offline.log to the USB drive when driver injection fails $setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log' if (Test-Path -Path $setupApiLogPath) { try { Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y" } catch { WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive. " } } else { WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath" } } else { WriteLog "Driver injection from folder succeeded." Write-Host "Driver injection from folder succeeded." } } catch { $warningMessage = "Warning: An error occurred during folder driver installation. Continuing deployment." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow # Copy troubleshooting logs to the USB drive when driver installation fails try { Invoke-Process xcopy.exe "X:\Windows\logs\dism\dism.log $USBDrive /Y" } catch { WriteLog "Warning: Failed to copy dism.log to $USBDrive." } $setupApiLogPath = 'W:\Windows\INF\setupapi.offline.log' if (Test-Path -Path $setupApiLogPath) { try { Invoke-Process xcopy.exe """$setupApiLogPath"" ""$USBDrive"" /Y" } catch { WriteLog "Warning: Failed to copy setupapi.offline.log to $USBDrive." } } else { WriteLog "Warning: setupapi.offline.log not found at $setupApiLogPath" } } finally { # Always attempt to remove SUBST mapping; failures here should not stop deployment if ($null -ne $substMapping) { try { Remove-DriverSubstMapping -DriveLetter $substMapping.DriveLetter } catch { $warningMessage = "Warning: Failed to remove SUBST mapping $($substMapping.DriveLetter). Continuing deployment." WriteLog $warningMessage Write-Host $warningMessage -ForegroundColor Yellow } } } } } else { WriteLog "No drivers to install." } Write-SectionHeader -Title 'Setting Boot Configuration' WriteLog "Setting Windows Boot Manager to be first in the firmware display order." Write-Host "Setting Windows Boot Manager to be first in the firmware display order." Invoke-Process bcdedit.exe "/set {fwbootmgr} displayorder {bootmgr} /addfirst" WriteLog "Setting Windows Boot Manager to be first in the default display order." Write-Host "Setting Windows Boot Manager to be first in the default display order." Invoke-Process bcdedit.exe "/set {bootmgr} displayorder {default} /addfirst" # Capture final BCD telemetry after the display order changes are applied. $finalBcdDiagnostics = Save-BcdDiagnostics -StageName 'FinalBcd' -DiagnosticsRoot $secureBootDiagnosticsPath -DiskNumber $DiskID Write-BootExpectationSummary -PostApplyDiagnostics $postApplyDiagnostics -BcdDiagnostics $finalBcdDiagnostics #Copy DISM log to USBDrive WriteLog "Copying dism log to $USBDrive" invoke-process xcopy "X:\Windows\logs\dism\dism.log $USBDrive /Y" WriteLog "Copying dism log to $USBDrive succeeded"