Automate direct domain enrollment across Windows versions

This commit is contained in:
2026-09-11 17:34:19 -06:00
parent 7f8a9eed4e
commit 520b4be955
23 changed files with 1244 additions and 108 deletions
+86 -3
View File
@@ -2,6 +2,8 @@ $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$serverBootstrapPath = Join-Path $repositoryRoot 'scripts\Initialize-SguDomainController.ps1'
$clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1'
$azureClientPath = Join-Path $repositoryRoot 'scripts\Install-SguAzureP2sClient.ps1'
$domainEnrollmentPath = Join-Path $repositoryRoot 'scripts\Enroll-SguDomainClient.ps1'
$repairEnrollmentPath = Join-Path $repositoryRoot 'scripts\Repair-SguClientEnrollment.ps1'
$bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep'
$tokens = $null
@@ -16,7 +18,10 @@ if ($parseErrors.Count -gt 0) {
$networkFunctionNames = @(
'Test-PrivateIPv4Address',
'ConvertTo-NetworkCidr',
'ConvertTo-PrivateNetworkCidr'
'ConvertTo-PrivateNetworkCidr',
'ConvertTo-PublicNetworkCidr',
'Get-ActiveIPv4Adapters',
'Resolve-PrivateInterfaceAlias'
)
$networkFunctions = $serverAst.FindAll({
param($node)
@@ -39,7 +44,7 @@ $clientNetworkFunctions = $clientAst.FindAll({
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -in @('Test-IPv4AddressesSharePrefix', 'Resolve-ClientInterfaceAlias',
'Set-ClientServerRoute', 'Set-ClientDomainDns', 'Test-TcpPort', 'Assert-ClientOperatingSystem',
'Wait-ClientInterface')
'Wait-ClientInterface', 'Test-PrivateIPv4Address', 'Test-ClientDomainDns')
}, $true)
Invoke-Expression (($clientNetworkFunctions | ForEach-Object { $_.Extent.Text }) -join [Environment]::NewLine)
@@ -65,11 +70,29 @@ Describe 'SGU public-cloud network safety' {
$wasRejected | Should Be $true
}
It 'canonicalizes an explicitly authorized public enrollment network' {
ConvertTo-PublicNetworkCidr -Cidr '200.13.89.183/24' |
Should Be '200.13.89.0/24'
}
It 'rejects private space in the public enrollment allowlist' {
$wasRejected = $false
try {
ConvertTo-PublicNetworkCidr -Cidr '10.77.0.0/16' | Out-Null
}
catch {
$wasRejected = $true
}
$wasRejected | Should Be $true
}
It 'exposes explicit Azure modes on both bootstraps' {
((Get-Command $serverBootstrapPath).Parameters.Keys -contains
'NetworkConfigurationMode') | Should Be $true
((Get-Command $serverBootstrapPath).Parameters.Keys -contains
'TrustedClientNetworks') | Should Be $true
((Get-Command $serverBootstrapPath).Parameters.Keys -contains
'PublicEnrollmentNetworks') | Should Be $true
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
'ConnectivityMode') | Should Be $true
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
@@ -117,14 +140,57 @@ Describe 'SGU public-cloud network safety' {
$source | Should Match 'Add-DnsClientNrptRule'
}
It 'limits optional public administration to RDP' {
It 'keeps public enrollment closed unless explicit source CIDRs are supplied' {
$template = Get-Content -LiteralPath $bicepPath -Raw
$template | Should Match "name: 'Allow-RDP-from-administrator'"
$template | Should Match "destinationPortRange: '3389'"
$template | Should Match 'param publicEnrollmentSourceAddressPrefixes array = \[\]'
$template | Should Match "name: 'Allow-Direct-AD-TCP'"
$template | Should Match 'sourceAddressPrefixes: publicEnrollmentSourceAddressPrefixes'
$template | Should Match 'param deployVpnGateway bool = true'
$template | Should Not Match "sourceAddressPrefix: '0\.0\.0\.0/0'"
}
}
Describe 'SGU direct public enrollment discovery' {
It 'distinguishes public server addresses from LAN and VPN addresses' {
Test-PrivateIPv4Address -Address ([ipaddress]'10.77.0.4') | Should Be $true
Test-PrivateIPv4Address -Address ([ipaddress]'172.30.0.4') | Should Be $true
Test-PrivateIPv4Address -Address ([ipaddress]'192.168.50.10') | Should Be $true
Test-PrivateIPv4Address -Address ([ipaddress]'20.9.81.130') | Should Be $false
}
It 'bootstraps DoH and host mappings after authenticated server discovery' {
$source = Get-Content -LiteralPath $clientBootstrapPath -Raw
$source | Should Match 'Set-DnsServerEncryptionProtocol'
$source | Should Match 'Enable-ClientDnsOverHttps'
$source | Should Match 'Set-ClientHostMappings'
$source | Should Match 'Test-ClientDomainDns'
$source | Should Match 'Get-DnsClientDohServerAddress'
$source | Should Match 'Add-DnsClientDohServerAddress'
}
}
Describe 'Azure accelerated server adapters' {
It 'ignores an Up accelerated VF that has no IPv4 interface' {
Mock Get-NetAdapter {
[pscustomobject]@{ Name = 'Ethernet'; ifIndex = 4; Status = 'Up' }
[pscustomobject]@{ Name = 'Ethernet VF'; ifIndex = 10; Status = 'Up' }
[pscustomobject]@{ Name = 'Disconnected'; ifIndex = 12; Status = 'Disconnected' }
}
Mock Get-NetIPInterface {
if ($InterfaceIndex -eq 4) {
[pscustomobject]@{ InterfaceIndex = 4; ConnectionState = 'Connected' }
}
}
Mock Get-NetIPConfiguration { [pscustomobject]@{ IPv4DefaultGateway = '10.77.0.1' } }
$adapters = @(Get-ActiveIPv4Adapters)
$adapters.Count | Should Be 1
$adapters[0].Name | Should Be 'Ethernet'
Resolve-PrivateInterfaceAlias | Should Be 'Ethernet'
}
}
Describe 'SGU route and interface discovery' {
BeforeEach {
Mock Get-NetIPInterface {
@@ -293,6 +359,23 @@ Describe 'SGU Windows capability checks' {
}
}
Describe 'SGU repeated domain enrollment' {
It 'rejoins a same-name forest when the machine secure channel is broken' {
$source = Get-Content -LiteralPath $domainEnrollmentPath -Raw
$source | Should Match 'Test-ComputerSecureChannel'
$source | Should Match '\$computer\.PartOfDomain -and \$domainMembershipHealthy'
$source | Should Match 'Reset-ComputerMachinePassword'
$source | Should Match 'DomainControllerDnsName'
$source | Should Match 'Add-Computer @joinParams'
}
It 'defers domain-only repair until the secure channel is healthy' {
$source = Get-Content -LiteralPath $repairEnrollmentPath -Raw
$source | Should Match 'Test-ComputerSecureChannel'
$source | Should Match 'if \(\$domainReady\)'
}
}
Describe 'SGU real TCP probe' {
It 'connects with a bound source and interface without relying on ICMP' {
$listener = [Net.Sockets.TcpListener]::new([ipaddress]'127.0.0.1', 0)
+72
View File
@@ -0,0 +1,72 @@
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$wallpaperScript = Join-Path $repositoryRoot 'scripts\Set-SguWelcomeWallpaper.ps1'
$source = Get-Content -LiteralPath $wallpaperScript -Raw
$tokens = $null
$parseErrors = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($wallpaperScript, [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors -join [Environment]::NewLine) }
$lookup = $ast.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq 'Get-DirectoryWelcomeMetadata'
}, $true)
function Invoke-WelcomeFixture {
param($DirectoryGender, [string]$ExplicitGender)
# Replace only the external directory lookup. Execute the actual script,
# including its validated parameters, metadata assignment and JPEG renderer.
$fixtureJson = [pscustomobject]@{
DisplayName = 'Usuario de prueba'
Gender = $DirectoryGender
Location = 'Sala de pruebas'
OrganizationalUnit = 'Laboratorio'
} | ConvertTo-Json -Compress
$fixtureFunction = 'function Get-DirectoryWelcomeMetadata { param($UserName, $MachineName); ConvertFrom-Json ''' +
$fixtureJson.Replace("'", "''") + ''' }'
$fixtureSource = $source.Remove($lookup.Extent.StartOffset, $lookup.Extent.EndOffset - $lookup.Extent.StartOffset).
Insert($lookup.Extent.StartOffset, $fixtureFunction)
$testScript = Join-Path $TestDrive ('wallpaper-' + [Guid]::NewGuid().ToString('N') + '.ps1')
[IO.File]::WriteAllText($testScript, $fixtureSource, [Text.UTF8Encoding]::new($false))
$parameters = @{
BaseImagePath = Join-Path $repositoryRoot 'assets\branding\darkblue.jpg'
FontsPath = Join-Path $repositoryRoot 'assets\branding\fonts'
OutputPath = Join-Path $TestDrive ([IO.Path]::GetFileNameWithoutExtension($testScript) + '.jpg')
CanvasWidth = 640
CanvasHeight = 480
SkipApply = $true
}
if ($ExplicitGender) { $parameters.Gender = $ExplicitGender }
$previousLocalAppData = $env:LOCALAPPDATA
try {
$env:LOCALAPPDATA = $TestDrive
& $testScript @parameters
}
finally { $env:LOCALAPPDATA = $previousLocalAppData }
}
Describe 'Welcome wallpaper with AD metadata' {
It 'renders a neutral JPEG when AD has no gender' {
$result = Invoke-WelcomeFixture -DirectoryGender $null
$result.WelcomeHeading | Should Be 'Te damos la bienvenida,'
$result.Applied | Should Be $false
$bitmap = [Drawing.Image]::FromFile($result.OutputPath)
try { $bitmap.Width | Should Be 640; $bitmap.Height | Should Be 480 }
finally { $bitmap.Dispose() }
}
It 'uses neutral wording for empty or unrecognized metadata' {
foreach ($value in @('', 'Unknown')) {
(Invoke-WelcomeFixture -DirectoryGender $value).WelcomeHeading | Should Be 'Te damos la bienvenida,'
}
}
It 'keeps the gendered greetings for recognized directory values' {
(Invoke-WelcomeFixture -DirectoryGender 'Female').WelcomeHeading | Should Be 'Bienvenida,'
(Invoke-WelcomeFixture -DirectoryGender 'Male').WelcomeHeading | Should Be 'Bienvenido,'
}
It 'honors an explicit gender over directory metadata' {
(Invoke-WelcomeFixture -DirectoryGender 'Female' -ExplicitGender 'Male').WelcomeHeading | Should Be 'Bienvenido,'
}
}