# DeployCenter - Discover and generate Windows answer-file profiles. # PSD1 profiles remain data files; answer files are built as XML. function Get-DeployCenterUnattendProfileNames { [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$ProfilesRoot) if (-not (Test-Path -LiteralPath $ProfilesRoot -PathType Container)) { return @() } $names = @() foreach ($file in (Get-ChildItem -LiteralPath $ProfilesRoot -Filter '*.psd1' -File -ErrorAction SilentlyContinue)) { if ($file.BaseName -match '^[A-Za-z0-9_-]+$') { $names += $file.BaseName } } return @($names | Sort-Object -Unique) } function Import-DeployCenterUnattendProfile { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ProfilesRoot, [Parameter(Mandatory=$true)][string]$ProfileName ) if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid Unattend profile name: $ProfileName" } $path = Join-Path $ProfilesRoot ($ProfileName + '.psd1') if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Unattend profile not found: $ProfileName" } $profile = Import-PowerShellDataFile -LiteralPath $path foreach ($key in @('Id','Name','Description','Target','Template','ComputerName','LocalAccount')) { if (-not $profile.ContainsKey($key)) { throw ('Unattend profile {0} does not contain key {1}.' -f $ProfileName, $key) } } if ([string]$profile.Id -ne $ProfileName) { throw ('Unattend profile ID {0} does not match the file name.' -f $ProfileName) } if ([string]$profile.Target -notin @('Workstation', 'Server')) { throw ('The Target property of Unattend profile {0} must be Workstation or Server.' -f $ProfileName) } if ([string]$profile.Template -notmatch '^[A-Za-z0-9_-]+\.xml$') { throw ('Invalid XML template name in profile {0}.' -f $ProfileName) } $templatesRoot = Join-Path (Split-Path -Parent $ProfilesRoot) 'Templates' $templatePath = Join-Path $templatesRoot ([string]$profile.Template) if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) { throw "XML template not found for profile '$ProfileName': $templatePath" } if (-not [string]$profile.Name -or [string]::IsNullOrWhiteSpace([string]$profile.Name)) { throw ('Unattend profile {0} must define a display name.' -f $ProfileName) } $profile.TemplatePath = $templatePath return $profile } function Test-DeployCenterUnattendConfiguration { [CmdletBinding()] param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Configuration, [Parameter(Mandatory=$true)][string]$HostName ) if ($Profile.ComputerName -and $HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') { throw 'The computer name must follow NetBIOS rules and not exceed 15 characters.' } if ($Profile.LocalAccount) { $accountName = [string]$Configuration.UnattendAdminName $displayName = [string]$Configuration.UnattendAdminDisplayName $groupName = [string]$Configuration.UnattendAdminGroup $passwordBase64 = [string]$Configuration.UnattendAdminPasswordBase64 if ($accountName -notmatch '^[^"/\[\]:;|=,+*?<>\\]{1,20}$') { throw '[Unattend] AdminName must be 1 to 20 characters and must not contain characters prohibited by Windows.' } if ([string]::IsNullOrWhiteSpace($displayName)) { throw '[Unattend] AdminDisplayName cannot be empty.' } if ([string]::IsNullOrWhiteSpace($groupName)) { throw '[Unattend] AdminGroup cannot be empty.' } if ([string]::IsNullOrWhiteSpace($passwordBase64)) { throw 'The [Unattend] AdminPasswordBase64 setting is required for this profile.' } try { $passwordBytes = [Convert]::FromBase64String($passwordBase64) if ($passwordBytes.Length -eq 0 -or ($passwordBytes.Length % 2) -ne 0 -or [string]::IsNullOrEmpty([Text.Encoding]::Unicode.GetString($passwordBytes))) { throw 'Invalid UTF-16LE Base64 format.' } } catch { throw '[Unattend] AdminPasswordBase64 must be a UTF-16LE password encoded as Base64.' } } } function New-DeployCenterUnattendXml { [CmdletBinding()] param( [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Configuration, [Parameter(Mandatory=$true)][string]$HostName, [Parameter(Mandatory=$true)][string]$Architecture, [AllowEmptyString()][string]$WallpaperPath = '' ) Test-DeployCenterUnattendConfiguration -Profile $Profile -Configuration $Configuration -HostName $HostName $document = New-Object System.Xml.XmlDocument $document.PreserveWhitespace = $true $document.Load([string]$Profile.TemplatePath) $namespaces = New-Object System.Xml.XmlNamespaceManager($document.NameTable) $namespaces.AddNamespace('u', 'urn:schemas-microsoft-com:unattend') $namespaces.AddNamespace('wcm', 'http://schemas.microsoft.com/WMIConfig/2002/State') $processorArchitecture = switch -Regex ($Architecture.ToLowerInvariant()) { 'arm64' { 'arm64'; break } 'x86|32' { 'x86'; break } default { 'amd64' } } foreach ($component in $document.SelectNodes('//u:component', $namespaces)) { $component.SetAttribute('processorArchitecture', $processorArchitecture) } if ($Profile.ComputerName) { $node = $document.SelectSingleNode('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Shell-Setup"]/u:ComputerName', $namespaces) if ($null -eq $node) { throw ('The template for profile {0} does not define the ComputerName node in specialize.' -f $Profile.Id) } $node.InnerText = $HostName } if ($Profile.LocalAccount) { $values = @{ Name = [string]$Configuration.UnattendAdminName DisplayName = [string]$Configuration.UnattendAdminDisplayName Group = [string]$Configuration.UnattendAdminGroup Password = [string]$Configuration.UnattendAdminPasswordBase64 } foreach ($name in $values.Keys) { $field = if ($name -eq 'Password') { 'Password/u:Value' } else { $name } $node = $document.SelectSingleNode("/u:unattend/u:settings[@pass='oobeSystem']/u:component[@name='Microsoft-Windows-Shell-Setup']/u:UserAccounts/u:LocalAccounts/u:LocalAccount/u:$field", $namespaces) if ($null -eq $node) { throw ('The template for profile {0} does not define the account field {1}.' -f $Profile.Id, $name) } $node.InnerText = $values[$name] } } $supportAppUrl = [string]$Configuration.UnattendSupportAppURL $supportUrl = [string]$Configuration.UnattendSupportURL # Compatibility: before SupportURL was added, a web URL could be provided in SupportAppURL. if ($supportAppUrl -match '^https?://') { if ([string]::IsNullOrWhiteSpace($supportUrl)) { $supportUrl = $supportAppUrl } $supportAppUrl = '' } $supportSettings = @( @{ Name = 'SupportProvider'; Value = [string]$Configuration.UnattendSupportProvider }, @{ Name = 'SupportURL'; Value = $supportUrl }, @{ Name = 'SupportAppURL'; Value = $supportAppUrl } ) $oemInformationPath = '/u:unattend/u:settings[@pass="oobeSystem"]/u:component[@name="Microsoft-Windows-Shell-Setup"]/u:OEMInformation' foreach ($supportSetting in $supportSettings) { $supportNode = $document.SelectSingleNode(($oemInformationPath + '/u:' + $supportSetting.Name), $namespaces) if ($null -eq $supportNode) { if (-not [string]::IsNullOrWhiteSpace($supportSetting.Value)) { throw ('The template for profile {0} does not define the {1} node.' -f $Profile.Id, $supportSetting.Name) } continue } if ([string]::IsNullOrWhiteSpace($supportSetting.Value)) { [void]$supportNode.ParentNode.RemoveChild($supportNode) } else { $supportNode.InnerText = $supportSetting.Value } } $oemInformationNode = $document.SelectSingleNode($oemInformationPath, $namespaces) if ($null -ne $oemInformationNode -and $oemInformationNode.SelectNodes('u:*', $namespaces).Count -eq 0) { [void]$oemInformationNode.ParentNode.RemoveChild($oemInformationNode) } if (-not [string]::IsNullOrWhiteSpace($WallpaperPath)) { $shellSetupNode = $document.SelectSingleNode('/u:unattend/u:settings[@pass="oobeSystem"]/u:component[@name="Microsoft-Windows-Shell-Setup"]', $namespaces) if ($null -eq $shellSetupNode) { throw ('The template for profile {0} does not define the Shell-Setup component in oobeSystem.' -f $Profile.Id) } $themesNode = $shellSetupNode.SelectSingleNode('u:Themes', $namespaces) if ($null -eq $themesNode) { $themesNode = $document.CreateElement('Themes', 'urn:schemas-microsoft-com:unattend') [void]$shellSetupNode.AppendChild($themesNode) } foreach ($themeSetting in @( @{ Name = 'ThemeName'; Value = 'Blorand' }, @{ Name = 'DesktopBackground'; Value = $WallpaperPath }, @{ Name = 'UWPAppsUseLightTheme'; Value = 'true' } )) { $themeNode = $themesNode.SelectSingleNode(('u:' + $themeSetting.Name), $namespaces) if ($null -eq $themeNode) { $themeNode = $document.CreateElement($themeSetting.Name, 'urn:schemas-microsoft-com:unattend') [void]$themesNode.AppendChild($themeNode) } $themeNode.InnerText = [string]$themeSetting.Value } } return $document.OuterXml } Export-ModuleMember -Function Get-DeployCenterUnattendProfileNames, Import-DeployCenterUnattendProfile, Test-DeployCenterUnattendConfiguration, New-DeployCenterUnattendXml