mirror of https://github.com/raandree/NTFSSecurity
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
105 lines
6.6 KiB
105 lines
6.6 KiB
[CmdletBinding()]
|
|
param (
|
|
[Parameter(Mandatory)] [string] $LogPath,
|
|
[Parameter(Mandatory)] [ValidatePattern('^[A-Za-z][A-Za-z0-9-]{0,14}$')] [string] $VmName,
|
|
[string] $LabName = 'NtfsSecurityOsMatrixLab',
|
|
[switch] $Start
|
|
)
|
|
|
|
# Repairs the boot files of one virtual machine of the matrix lab. AutomatedLab builds a base image with the bcdboot of the host and
|
|
# ignores its exit code; when the host's bcdboot can't process the boot files of an older image (it fails with exit code 193 on Windows
|
|
# Server 2019 and on Windows 11 22H2), the EFI system partition stays empty and the generation 2 virtual machine fails with Hyper-V event
|
|
# 18603. This script turns the machine off, mounts the machine's own differencing disk (never the shared base image), runs the bcdboot of
|
|
# the image itself, adds the removable-media path EFI\Boot\bootx64.efi that a new virtual machine boots from, checks the files, and
|
|
# dismounts the disk. It refuses a machine that isn't connected to the switch of the lab. Windows PowerShell 5.1 on the host, elevated.
|
|
$ErrorActionPreference = 'Stop'
|
|
$stamp = '[{0:yyyy-MM-dd HH:mm:ss}Z]'
|
|
function Write-Step { param ([string] $Message) ($stamp -f [DateTime]::UtcNow) + ' ' + $Message | Add-Content -LiteralPath $LogPath }
|
|
|
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script must run in an elevated PowerShell session.' }
|
|
|
|
($stamp -f [DateTime]::UtcNow) + " START repair-os-matrix-boot vm=$VmName lab=$LabName" | Set-Content -LiteralPath $LogPath
|
|
$diskPath = $null
|
|
$letters = @()
|
|
try {
|
|
$vm = Get-VM -Name $VmName
|
|
if ($vm.Generation -ne 2) { throw "$VmName isn't a generation 2 machine." }
|
|
$switches = @(Get-VMNetworkAdapter -VMName $VmName | ForEach-Object -Process { $_.SwitchName })
|
|
# An array comparison with -ne returns the elements that differ, and an empty result is false: a machine without an adapter, or with an
|
|
# adapter that has no switch, would pass, so the guard counts.
|
|
if ($switches.Count -eq 0 -or @($switches | Where-Object -FilterScript { $_ -ne $LabName }).Count -gt 0) { throw "$VmName isn't connected only to the switch '$LabName' (switches: $($switches -join ', ')). Refusing." }
|
|
if ($vm.State -ne 'Off') {
|
|
Stop-VM -Name $VmName -TurnOff -Force
|
|
Write-Step "$VmName turned off"
|
|
}
|
|
|
|
$drive = Get-VMHardDiskDrive -VMName $VmName | Select-Object -First 1
|
|
$diskPath = $drive.Path
|
|
$vhd = Get-VHD -Path $diskPath
|
|
if ($vhd.VhdType -ne 'Differencing') { throw "$diskPath isn't a differencing disk; refusing to change a base image." }
|
|
Write-Step "disk $diskPath (parent $($vhd.ParentPath))"
|
|
|
|
$image = Mount-VHD -Path $diskPath -Passthru
|
|
$disk = $image | Get-Disk
|
|
$partitions = @(Get-Partition -DiskNumber $disk.Number)
|
|
$esp = $partitions | Where-Object -FilterScript { $_.GptType -eq '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' } | Select-Object -First 1
|
|
$system = $partitions | Where-Object -FilterScript { $_.Type -eq 'Basic' } | Sort-Object -Property Size -Descending | Select-Object -First 1
|
|
if (-not $esp -or -not $system) { throw 'The disk has no EFI system partition or no Windows partition.' }
|
|
foreach ($partition in $esp, $system) {
|
|
# The host may have assigned a letter to the Windows partition on mount already.
|
|
if (-not (Get-Partition -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber).DriveLetter) {
|
|
Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AssignDriveLetter
|
|
}
|
|
|
|
$letters += (Get-Partition -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber).DriveLetter
|
|
}
|
|
|
|
$espLetter = (Get-Partition -DiskNumber $disk.Number -PartitionNumber $esp.PartitionNumber).DriveLetter
|
|
$systemLetter = (Get-Partition -DiskNumber $disk.Number -PartitionNumber $system.PartitionNumber).DriveLetter
|
|
$windows = '{0}:\Windows' -f $systemLetter
|
|
$bcdboot = Join-Path -Path $windows -ChildPath 'System32\bcdboot.exe'
|
|
if (-not (Test-Path -LiteralPath $bcdboot)) { throw "$bcdboot is missing." }
|
|
$before = @(Get-ChildItem -LiteralPath ('{0}:\' -f $espLetter) -Recurse -Force -File -ErrorAction SilentlyContinue).Count
|
|
Write-Step ("system partition {0}: {1}; ESP {2}: holds {3} files; image build {4}" -f $systemLetter, $windows, $espLetter, $before, (Get-Item -LiteralPath $bcdboot).VersionInfo.ProductVersion)
|
|
|
|
$output = & $bcdboot $windows /s ('{0}:' -f $espLetter) /f UEFI 2>&1 | Out-String
|
|
Write-Step ("bcdboot of the image: exit code {0}: {1}" -f $LASTEXITCODE, ($output -replace '\s+', ' ').Trim())
|
|
if ($LASTEXITCODE -ne 0) { throw "bcdboot of the image failed with exit code $LASTEXITCODE." }
|
|
|
|
$bootManager = '{0}:\EFI\Microsoft\Boot\bootmgfw.efi' -f $espLetter
|
|
if (-not (Test-Path -LiteralPath $bootManager)) { throw "$bootManager is missing after bcdboot." }
|
|
$removable = '{0}:\EFI\Boot' -f $espLetter
|
|
$null = New-Item -ItemType Directory -Path $removable -Force
|
|
Copy-Item -LiteralPath $bootManager -Destination (Join-Path -Path $removable -ChildPath 'bootx64.efi') -Force
|
|
$after = @(Get-ChildItem -LiteralPath ('{0}:\' -f $espLetter) -Recurse -Force -File -ErrorAction SilentlyContinue).Count
|
|
$hasBcd = Test-Path -LiteralPath ('{0}:\EFI\Microsoft\Boot\BCD' -f $espLetter)
|
|
Write-Step ("ESP now holds {0} files; BCD present: {1}; bootx64.efi present: {2}" -f $after, $hasBcd, (Test-Path -LiteralPath (Join-Path -Path $removable -ChildPath 'bootx64.efi')))
|
|
if ($after -lt 20 -or -not $hasBcd) { throw "The EFI system partition still looks empty ($after files, BCD $hasBcd)." }
|
|
}
|
|
catch {
|
|
Write-Step ('repair-os-matrix-boot-FAILED: {0}' -f $_)
|
|
$_ | Format-List -Property * -Force | Out-String | Add-Content -LiteralPath $LogPath
|
|
$failed = $true
|
|
}
|
|
finally {
|
|
if ($diskPath) {
|
|
try {
|
|
foreach ($partition in @(Get-Partition -DiskNumber (Get-VHD -Path $diskPath).DiskNumber -ErrorAction SilentlyContinue | Where-Object -FilterScript { $_.DriveLetter })) {
|
|
Remove-PartitionAccessPath -DiskNumber $partition.DiskNumber -PartitionNumber $partition.PartitionNumber -AccessPath ('{0}:\' -f $partition.DriveLetter) -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
catch { Write-Step ('access path cleanup: {0}' -f $_) }
|
|
Dismount-VHD -Path $diskPath -ErrorAction SilentlyContinue
|
|
Write-Step 'disk dismounted'
|
|
}
|
|
}
|
|
|
|
if ($failed) { exit 1 }
|
|
if ($Start) {
|
|
Start-VM -Name $VmName
|
|
Write-Step "$VmName started"
|
|
}
|
|
|
|
Write-Step 'repair-os-matrix-boot-DONE'
|
|
exit 0
|
|
|