Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
57 changes: 29 additions & 28 deletions CHECKSUMS.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
59 changes: 57 additions & 2 deletions Invoke-OSDDiskLayout.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]):[\\/]') {
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
59 changes: 57 additions & 2 deletions Scripts/Invoke-OSDDiskLayout.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]):[\\/]') {
Expand Down
30 changes: 30 additions & 0 deletions Tests/Invoke-OSDDiskLayout.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ BeforeAll {
'New-OSDPartitionPlan',
'New-OSDDiskPartCommands',
'Assert-OSDPartitionStructure',
'Resolve-OSDHostAddresses',
'Get-OSDLocalNetworkIdentity',
'Assert-OSDPathOffTargetDisk',
'Assert-OSDWinPEDriveSafety'
)) {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
}
}

Expand Down
40 changes: 40 additions & 0 deletions build/Invoke-Validation.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
16 changes: 10 additions & 6 deletions docs/COMPATIBILITY-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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.