From 1f847ef5de13bc36000911def6773bc4511c8d43 Mon Sep 17 00:00:00 2001 From: Claudio Mendes Date: Wed, 7 Oct 2026 18:05:16 +0200 Subject: [PATCH] Harden disk safety and integrity validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 6 +++ CHECKSUMS.txt | 57 ++++++++++++++------------- CONTRIBUTING.md | 2 + Invoke-OSDDiskLayout.ps1 | 59 +++++++++++++++++++++++++++- README.md | 2 +- Scripts/Invoke-OSDDiskLayout.ps1 | 59 +++++++++++++++++++++++++++- Tests/Invoke-OSDDiskLayout.Tests.ps1 | 30 ++++++++++++++ build/Invoke-Validation.ps1 | 40 +++++++++++++++++++ docs/COMPATIBILITY-MATRIX.md | 16 +++++--- 9 files changed, 232 insertions(+), 39 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..584747b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf +*.ps1 text eol=crlf +*.psd1 text eol=crlf +*.psm1 text eol=crlf +*.md text eol=lf +*.yml text eol=lf diff --git a/CHECKSUMS.txt b/CHECKSUMS.txt index 809267f..521f8e6 100644 --- a/CHECKSUMS.txt +++ b/CHECKSUMS.txt @@ -1,30 +1,31 @@ -13E18CEACF8F4DB244D86D522AADFDA94BF4BE7A05856ACC710018BCCA03ED0C AUTHORS.md -20C672DDECFFF00B284AD71032A0A617851606032EEA7360E687A2D54EF0149C docs/COMPATIBILITY-MATRIX.md -22AC84BD655BD8BD71F70538734BE7AAE84342AA561EC40AB07770B9584811C3 .github/PULL_REQUEST_TEMPLATE.md -252390C5F743D5D35FDE2A4861A178ADA49B7FD5CE79D3E3C13201D1FF93ED43 assets/banner.svg -409D6AFCD93585021F1A554A63EFB8C67D07987820E1D0D8ED6BE0B474E5C426 .github/FUNDING.yml -477BBF58F7FC6A1E422F3D61D5489B2ED27B8D391D9DFBCA997D693704A068FE CODE_OF_CONDUCT.md -4BC67EFF11DBE142783D49018F13C3D7B19859B9FF66051CCA430F639BB16A89 .github/workflows/ci.yml -6CB6B870AFE8694EE75FD9643089D64B856A0A7ABD73D4829140AB0838277C67 build/Invoke-Validation.ps1 -6FD3AB9304532C159C00EE07D11ECEB28FE5AF656A9E7AD9664C7AB27BA6ABBD .github/ISSUE_TEMPLATE/config.yml -712D7EF74DB0E9E87AE14D6F5891CEB6FC6C3C38DDE4D6E331F1E77AF889B2FF assets/css/style.scss -82E480FD2D1C96D08DAA7A5573B52C1F1A33F14BEF4DDDEE9F322FB4C4B464B5 .gitignore -8819917BBF41564EE48BDC9CA6169DB3768A14FCFB09E823BCFBF8D0C59CE7CD README.md -88F4422226987D7A16D4D9B53DD7192EC28D9EC942906F7706E86B949D923A19 assets/favicon.svg -9E0334C5EFC6369D003C920E58F075EB7E54A386EA5B7472A5A127DBDAD5545A TASK-SEQUENCE.md -AAEE3EB231274105D19F95D14CCFC81D6A783FEC30D41ABECE356AC3075CFAB0 assets/banner-compact.svg -ADB0FF6DF54DE634079C2F363FD6D6062A3F7559D011DCE52FD13F0784592774 Tests/Invoke-OSDDiskLayout.Tests.ps1 -AFCE72268C9532C4D571A39BB354DAF5565EBC721533CD0E7D0093DC40B7A4C2 CONTRIBUTING.md +0CD7480196145CF2F0D8105383E2882B8F3517B1D2798183BD714D9BA419CEAA AUTHORS.md +0DDDC8E079182C8275174F70D9CFAE5776B7CF5480A8A0E34335317085351930 .github/ISSUE_TEMPLATE/feature_request.yml +1339D0B0D32E3797DD9B9BE2553D77A2DA8C837632A95EC1BB017528E20C7FE3 .github/PULL_REQUEST_TEMPLATE.md +1BC6AAA2FB498119688D176E33F92125C98C21C05BCCEA92ABBE2E4976F8C0B7 _config.yml +1FE38F9954722F66DF7C06FC1A93AA5AAB4B28272DD7D316BE352E162BCCF890 .gitattributes +229B99CB364794905604F3DF1AF743818B87BEF3285AEA2D8F9E2EF7110BEBE6 LICENSE +33E160714B21F67E4CF6A4758756277A6DB8EFA7F5FD181A1176C1D159B7D2C4 _layouts/default.html +3D38B56C06B43FB35BB74B2B86AF1668353EC7918689ADFD73394E5C8A0287E3 Tests/Invoke-OSDDiskLayout.Tests.ps1 +43CE8E1E50DCAFE1505F85B588405CBE9BDF870C660B95BCE8B72657BDE5D0A8 Invoke-OSDDiskLayout.ps1 +43CE8E1E50DCAFE1505F85B588405CBE9BDF870C660B95BCE8B72657BDE5D0A8 Scripts/Invoke-OSDDiskLayout.ps1 +4BE89F930738813EDBEAF483B71263D7C93CF04C090EB7F5722E9BAA91E3339E TASK-SEQUENCE.md +72113F7FDCEC707FDBCC0825F696EE67C419B0BF01963A2A48755C582047FA14 CODE_OF_CONDUCT.md +77EFEA7044A969E429538D0863AFD5C05B481A4E97AD7B4B437F5E1CAA43950C assets/README.md +785F1C9CD791EA87EA53E0E4C058D26C0499AD44B614FB5CDFB5D56623A509F2 RELEASE-NOTES.md +8173AF643AB11708ADB706F8EE258B64F697A500951E67ED3BA8B4C26B9CD140 .gitignore +8BB5536BD0A7529406371E507C01F76757A84EB52453E71F600E5E35AF7AB73D docs/COMPATIBILITY-MATRIX.md +8C5A69942136345CF522BD11A014104AA8D4FE6FE80EFC335C731A923158F99C SECURITY.md +8CF174BF5D03B758D008AEFCC2E8C9F29279DD20C5160C53850BFE045478925F .github/ISSUE_TEMPLATE/config.yml +91240F8C92EA0BEBD31134FE7D8847B3B61FDAF59AF3E97846D556185ACA114B index.md +9A5E1A65E8DC2031345D290DF29A32EF61C84F835C98E6ED7B96EDA1D835F9AC README.md +A3BB0407535BA00026D48887490B19FE74DC04615112599E174BD19E5DC832DD assets/favicon.svg +A6BB46E5BEB1DAF1DB2FD029548CFFA3A85BF995CF68177F1B8299E4DF6F3A0D .github/workflows/ci.yml B3FA5B1E4D7F52FEF40C54F52D4B0C6D63EED3F3193E99976D8EDE75DFD87482 PSScriptAnalyzerSettings.psd1 -B6818E77A77718C171BAA15D003A9E67F9A9B6CAB42F9EADD3068F5491F5ACB0 LICENSE +B4052A8246C92104D479C9760DFC99D79B710AE0EDCA21866585C94CAF2B5B7C assets/banner.svg +B7469B888920F0CC7AFE47067BE298E4FA5576094CACEEEDB44A102E1A9389AD build/Invoke-Validation.ps1 +B950F2103942CE90ACCDA5CBAA1DC1403D8080DE244649EE1873757187BCC3C4 CONTRIBUTING.md B9E64B826F91346D3E32844DC123263582DE7ECB8692112BE19FDAFA981F894E Tests/Branding.Tests.ps1 -C04D3716FFBE1B1E44C02F244D8378137EDCA2C44D7F87A9A7A49E7CF267FB50 assets/README.md -C784516C6D8945C6F11C9AB93B80C3C92B7B397FFF59C6188B6E313C63C79F72 .github/ISSUE_TEMPLATE/bug_report.yml -CAEC6646EA6042A17E283F86545A5E591CACF8876C60740FB5436D9F32171D2B .github/ISSUE_TEMPLATE/feature_request.yml -D1D27B54A16CC7C714F34F2D57D3D796160AA74868A34288802E6BD2DAD27C5E Invoke-OSDDiskLayout.ps1 -D1D27B54A16CC7C714F34F2D57D3D796160AA74868A34288802E6BD2DAD27C5E Scripts/Invoke-OSDDiskLayout.ps1 -DB2ECC787E09B4F361DF6B7DFE917D216EDE424D918AD612A6B3D102274EB749 SECURITY.md -E4ED3A5D957732F39605FB05A06E0A4386453B770BE82BCCFB10B4617B28CF65 _layouts/default.html -EB5660739C7FDB81C9B9FEF58B05A71F504743DCE5599B58A9AE8F71929092F0 index.md -ECBFF2063237206547F105EB1D155833C0DA70DEF69A3BF09CCFF8D0242EC69C _config.yml -F57D39E02B58937C127B01FA7CBF635005EAED3CDDB43A9DBA9C084F8C81D63E RELEASE-NOTES.md +BFE846987094295386DD402ABB9114B5D104C70FAF7615AB48CCA02A9E35BB63 .github/ISSUE_TEMPLATE/bug_report.yml +CE16C3B2F70009ECC731E23ED6168AC7F99693D60DF794327DCBEFB7596610C0 assets/css/style.scss +DF3A074261544800B739DEE93B071DE5383EFE796B874D7673AD69A5548F7A4A .github/FUNDING.yml +EE0AE7CB2CDBA1FCE9931C070088CB6111695425DAD8C5EDF42522695B018B05 assets/banner-compact.svg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b614986..8802cf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,3 +18,5 @@ Contributions that improve safety, compatibility, tests, or documentation are we 6. Update user-facing documentation when support boundaries, parameters, task-sequence integration, or release behavior change. Pull requests should describe the risk addressed, the validation performed, and any remaining hardware-validation boundary. Automated tests do not establish field certification; test destructive behavior only on disposable disks in a controlled ConfigMgr/WinPE pilot. + +Regenerate `CHECKSUMS.txt` after all maintained-file changes and before final validation. The manifest hashes checked-out bytes after the repository `.gitattributes` rules are applied: PowerShell files use CRLF and other maintained text uses LF. diff --git a/Invoke-OSDDiskLayout.ps1 b/Invoke-OSDDiskLayout.ps1 index 3eeb9b8..ba1242b 100644 --- a/Invoke-OSDDiskLayout.ps1 +++ b/Invoke-OSDDiskLayout.ps1 @@ -636,6 +636,53 @@ function Test-SameDisk { } } +function Resolve-OSDHostAddresses { + param( + [Parameter(Mandatory)][string] $HostName + ) + try { + return @([System.Net.Dns]::GetHostAddresses($HostName) | + ForEach-Object { $_.ToString() } | Select-Object -Unique) + } + catch { + throw "Network host '$HostName' cannot be resolved; target safety cannot be checked." + } +} + +function Get-OSDLocalNetworkIdentity { + $Names = @( + [string]$env:COMPUTERNAME + [System.Net.Dns]::GetHostName() + ) + try { + $Names += [System.Net.Dns]::GetHostEntry( + [System.Net.Dns]::GetHostName()).HostName + } + catch { + Write-Verbose "Unable to resolve the local fully qualified host name: $($_.Exception.Message)" + } + + $Addresses = @('127.0.0.1', '::1') + $Addresses += Resolve-OSDHostAddresses -HostName ( + [System.Net.Dns]::GetHostName()) + $Interfaces = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() + foreach ($Interface in $Interfaces) { + $Addresses += @($Interface.GetIPProperties().UnicastAddresses | + ForEach-Object { $_.Address.ToString() }) + } + + [pscustomobject]@{ + Names = @($Names | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } | ForEach-Object { + $_.Trim().TrimEnd('.').ToLowerInvariant() + } | Select-Object -Unique) + Addresses = @($Addresses | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } | Select-Object -Unique) + } +} + function Assert-OSDPathOffTargetDisk { param( [AllowEmptyString()][string] $Path, @@ -650,10 +697,18 @@ function Assert-OSDPathOffTargetDisk { throw "$Description uses a device path that cannot be mapped safely." } $Server = ($Expanded.Substring(2) -split '[\\/]', 2)[0] - if ($Server -in @('', '.', 'localhost', '127.0.0.1', - '[::1]', [string]$env:COMPUTERNAME)) { + $NetworkHost = $Server.Trim().Trim('[', ']').TrimEnd('.').ToLowerInvariant() + $LocalIdentity = Get-OSDLocalNetworkIdentity + if ($NetworkHost -in @('', '.', 'localhost') -or + $NetworkHost -in $LocalIdentity.Names) { throw "$Description uses a local network alias that may point to the selected disk." } + $RemoteAddresses = @(Resolve-OSDHostAddresses -HostName $NetworkHost) + if (@($RemoteAddresses | Where-Object { + $_ -in $LocalIdentity.Addresses + }).Count -gt 0) { + throw "$Description resolves to this computer and may point to the selected disk." + } return } if ($Expanded -notmatch '^([A-Za-z]):[\\/]') { diff --git a/README.md b/README.md index 01b2f0a..f92d910 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ An optional fixed-size Windows plus Data profile is available with `-WindowsSize ## Validation and rollout -The repository validation runs Windows PowerShell 5.1 parsing, PSScriptAnalyzer, and Pester tests. The tests exercise pure selection/planning/postcondition logic and static entry-point safety checks without cleaning disks; they do not prove that a real ConfigMgr/WinPE deployment succeeds. +The repository validation runs Windows PowerShell 5.1 parsing, PSScriptAnalyzer, Pester tests, root/package byte-parity checks, and complete `CHECKSUMS.txt` verification. The tests exercise pure selection/planning/postcondition logic and static entry-point safety checks without cleaning disks; they do not prove that a real ConfigMgr/WinPE deployment succeeds. **Live ConfigMgr, WinPE, firmware, storage-driver, OS-image, and disposable-disk validation has not been completed for this release.** Use the [compatibility matrix](docs/COMPATIBILITY-MATRIX.md) to plan a controlled pilot. Check [CI](https://github.com/vartaxe/ConfigMgr-OSD-DiskPartitionLayout/actions/workflows/ci.yml) for the status of a specific revision. diff --git a/Scripts/Invoke-OSDDiskLayout.ps1 b/Scripts/Invoke-OSDDiskLayout.ps1 index 3eeb9b8..ba1242b 100644 --- a/Scripts/Invoke-OSDDiskLayout.ps1 +++ b/Scripts/Invoke-OSDDiskLayout.ps1 @@ -636,6 +636,53 @@ function Test-SameDisk { } } +function Resolve-OSDHostAddresses { + param( + [Parameter(Mandatory)][string] $HostName + ) + try { + return @([System.Net.Dns]::GetHostAddresses($HostName) | + ForEach-Object { $_.ToString() } | Select-Object -Unique) + } + catch { + throw "Network host '$HostName' cannot be resolved; target safety cannot be checked." + } +} + +function Get-OSDLocalNetworkIdentity { + $Names = @( + [string]$env:COMPUTERNAME + [System.Net.Dns]::GetHostName() + ) + try { + $Names += [System.Net.Dns]::GetHostEntry( + [System.Net.Dns]::GetHostName()).HostName + } + catch { + Write-Verbose "Unable to resolve the local fully qualified host name: $($_.Exception.Message)" + } + + $Addresses = @('127.0.0.1', '::1') + $Addresses += Resolve-OSDHostAddresses -HostName ( + [System.Net.Dns]::GetHostName()) + $Interfaces = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() + foreach ($Interface in $Interfaces) { + $Addresses += @($Interface.GetIPProperties().UnicastAddresses | + ForEach-Object { $_.Address.ToString() }) + } + + [pscustomobject]@{ + Names = @($Names | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } | ForEach-Object { + $_.Trim().TrimEnd('.').ToLowerInvariant() + } | Select-Object -Unique) + Addresses = @($Addresses | Where-Object { + -not [string]::IsNullOrWhiteSpace($_) + } | Select-Object -Unique) + } +} + function Assert-OSDPathOffTargetDisk { param( [AllowEmptyString()][string] $Path, @@ -650,10 +697,18 @@ function Assert-OSDPathOffTargetDisk { throw "$Description uses a device path that cannot be mapped safely." } $Server = ($Expanded.Substring(2) -split '[\\/]', 2)[0] - if ($Server -in @('', '.', 'localhost', '127.0.0.1', - '[::1]', [string]$env:COMPUTERNAME)) { + $NetworkHost = $Server.Trim().Trim('[', ']').TrimEnd('.').ToLowerInvariant() + $LocalIdentity = Get-OSDLocalNetworkIdentity + if ($NetworkHost -in @('', '.', 'localhost') -or + $NetworkHost -in $LocalIdentity.Names) { throw "$Description uses a local network alias that may point to the selected disk." } + $RemoteAddresses = @(Resolve-OSDHostAddresses -HostName $NetworkHost) + if (@($RemoteAddresses | Where-Object { + $_ -in $LocalIdentity.Addresses + }).Count -gt 0) { + throw "$Description resolves to this computer and may point to the selected disk." + } return } if ($Expanded -notmatch '^([A-Za-z]):[\\/]') { diff --git a/Tests/Invoke-OSDDiskLayout.Tests.ps1 b/Tests/Invoke-OSDDiskLayout.Tests.ps1 index 29b6037..66971a9 100644 --- a/Tests/Invoke-OSDDiskLayout.Tests.ps1 +++ b/Tests/Invoke-OSDDiskLayout.Tests.ps1 @@ -25,6 +25,8 @@ BeforeAll { 'New-OSDPartitionPlan', 'New-OSDDiskPartCommands', 'Assert-OSDPartitionStructure', + 'Resolve-OSDHostAddresses', + 'Get-OSDLocalNetworkIdentity', 'Assert-OSDPathOffTargetDisk', 'Assert-OSDWinPEDriveSafety' )) { @@ -643,6 +645,22 @@ Describe 'Partition postconditions (synthetic, no hardware writes)' { Describe 'Task-sequence source protection (no disk writes)' { BeforeAll { + Mock Get-OSDLocalNetworkIdentity { + [pscustomobject]@{ + Names = @('testhost', 'testhost.example.test') + Addresses = @('127.0.0.1', '::1', '10.0.0.5') + } + } + Mock Resolve-OSDHostAddresses { + switch ($HostName) { + 'server' { return @('192.0.2.10') } + 'local-alias.example.test' { return @('10.0.0.5') } + 'unresolved.example.test' { + throw "Network host '$HostName' cannot be resolved; target safety cannot be checked." + } + default { return @('192.0.2.11') } + } + } Mock Get-Partition { if ($DriveLetter -eq 'C') { return [pscustomobject]@{ DiskNumber = 2 } @@ -699,6 +717,18 @@ Describe 'Task-sequence source protection (no disk writes)' { { Assert-OSDPathOffTargetDisk -Path '\\localhost\C$\script.ps1' ` -TargetDiskNumber 2 -Description 'Running script' } | Should -Throw + { Assert-OSDPathOffTargetDisk ` + -Path '\\testhost.example.test\C$\script.ps1' ` + -TargetDiskNumber 2 -Description 'Running script' } | + Should -Throw + { Assert-OSDPathOffTargetDisk ` + -Path '\\local-alias.example.test\C$\script.ps1' ` + -TargetDiskNumber 2 -Description 'Running script' } | + Should -Throw + { Assert-OSDPathOffTargetDisk ` + -Path '\\unresolved.example.test\share\script.ps1' ` + -TargetDiskNumber 2 -Description 'Running script' } | + Should -Throw } } diff --git a/build/Invoke-Validation.ps1 b/build/Invoke-Validation.ps1 index bc3faa0..d0365bb 100644 --- a/build/Invoke-Validation.ps1 +++ b/build/Invoke-Validation.ps1 @@ -50,3 +50,43 @@ if ($null -eq $Result -or $Result.Result -ne 'Passed' -or $Result.TotalCount -eq $Result.NotRunCount -gt 0) { throw 'Pester did not complete with a fully passing, nonempty test suite.' } + +$Expected = @{} +Get-ChildItem -LiteralPath $Root -File -Recurse -Force | + Where-Object { + $_.FullName -notlike "$Root\.git\*" -and + $_.Name -ne 'CHECKSUMS.txt' + } | + ForEach-Object { + $RelativePath = $_.FullName.Substring($Root.Length + 1).Replace('\', '/') + $Expected[$RelativePath] = $_.FullName + } + +$Manifest = @{} +foreach ($Line in Get-Content -LiteralPath (Join-Path $Root 'CHECKSUMS.txt')) { + if ($Line -notmatch '^([0-9A-Fa-f]{64}) (.+)$') { + throw "Malformed checksum entry: $Line" + } + $Hash = $Matches[1] + $RelativePath = $Matches[2] + if ($Manifest.ContainsKey($RelativePath)) { + throw "Duplicate checksum entry: $RelativePath" + } + $Manifest[$RelativePath] = $Hash +} + +foreach ($RelativePath in $Expected.Keys) { + if (-not $Manifest.ContainsKey($RelativePath)) { + throw "Missing checksum entry: $RelativePath" + } + $ActualHash = (Get-FileHash -LiteralPath $Expected[$RelativePath] ` + -Algorithm SHA256).Hash + if ($ActualHash -ine $Manifest[$RelativePath]) { + throw "Checksum mismatch: $RelativePath" + } +} +foreach ($RelativePath in $Manifest.Keys) { + if (-not $Expected.ContainsKey($RelativePath)) { + throw "Unexpected checksum entry: $RelativePath" + } +} diff --git a/docs/COMPATIBILITY-MATRIX.md b/docs/COMPATIBILITY-MATRIX.md index c3d04e9..ebc62f8 100644 --- a/docs/COMPATIBILITY-MATRIX.md +++ b/docs/COMPATIBILITY-MATRIX.md @@ -45,6 +45,12 @@ device that the operating system does not enumerate. - **Current Windows 11:** use UEFI boot, GPT and `-RequireUEFI`. Validate TPM, Secure Boot capability, supported CPU, image architecture, ADK/ConfigMgr support and storage drivers separately. +- **Windows 11 26H1:** treat this as a specialized new-hardware release, not a + general in-place-upgrade target. Use only the OEM image and supported Arm64 + deployment stack for the applicable device. +- **Windows 11 26H2:** Configuration Manager 2509 does not support it as a + client OS. Use Configuration Manager 2603 or later and a supported + `10.1.26100.x` ADK/WinPE combination. - **Windows 10 and other current UEFI-capable images:** use the Modern UEFI profile unless the image and hardware policy intentionally permits BIOS. - **Older BIOS-based Windows images:** use `-RequireBIOS` and the MBR profile. @@ -58,10 +64,9 @@ device that the operating system does not enumerate. creates a correctly typed partition but does not populate or register `winre.wim`. -Older x86 WinPE and older ConfigMgr sites may work when PowerShell 5.1, -Storage WMI, ConfigMgr task-sequence COM support, DiskPart and the required -storage drivers are present, but they are not certified by this project. -PowerShell 2/3/4 is outside the current compatibility boundary because the +Current Windows 11 ADKs do not include an x86 WinPE image. Do not build a new +deployment around 32-bit boot media; use x64 or a supported Arm64 deployment +stack. PowerShell 2/3/4 is outside the compatibility boundary because the script requires Windows PowerShell 5.1. ## Custom configuration contract @@ -101,7 +106,7 @@ Before calling a profile production-ready, test at least one representative device and the exact boot image for each deployed family: - Dell Command Configure/driver-pack-managed systems; -- Lenovo SCCM/MDT driver-pack systems; +- Lenovo enterprise driver-pack-managed systems; - HP Image Assistant/SoftPaq-managed systems; - Microsoft Surface, including affected Storage Spaces models; - Intel NUC and current Intel VMD/RST platforms; @@ -112,4 +117,3 @@ sector size, visible disks, selected disk identity, partition postconditions, Apply Operating System behavior, first boot and `reagentc /info`. After adding WinPE drivers or optional components, update and redistribute the boot image and recreate affected media before retesting. -