Public/New-Tier0MoveObject.ps1
function New-Tier0MoveObject { <# .SYNOPSIS Moves Tier0 Active Directory objects to their proper OUs as per secure tiering model. .DESCRIPTION Moves default Tier0 Active Directory objects (privileged accounts and groups) to their designated Organizational Units according to a secure tiering model. This function is typically run once during initial AD structure setup to clean up the default container locations and implement proper object segregation. .PARAMETER ConfigXMLFile [System.IO.FileInfo] Full path to the XML configuration file. Contains all naming conventions, OU structure, and security settings. Must be a valid XML file with required schema elements. Default: C:\PsScripts\Config.xml .PARAMETER DMScripts [System.String] Path to all the scripts and files needed by this function. The directory must contain a SecTmpl subfolder. Default: C:\PsScripts\ .EXAMPLE New-Tier0MoveObject -ConfigXMLFile 'C:\PsScripts\Config.xml' -Verbose Moves Tier0 objects as defined in the configuration file with verbose output. .EXAMPLE New-Tier0MoveObject -ConfigXMLFile 'C:\PsScripts\Config.xml' -DMScripts 'C:\Scripts\' Moves Tier0 objects as defined in the configuration file, using scripts from the specified path. .INPUTS System.IO.FileInfo System.String .OUTPUTS System.String .NOTES Used Functions: Name ║ Module/Namespace ═══════════════════════════════════════════╬══════════════════════════════ Import-MyModule ║ EguibarIT Get-ADUser ║ ActiveDirectory Get-ADGroup ║ ActiveDirectory Get-ADDomain ║ ActiveDirectory Get-ADDomainController ║ ActiveDirectory Rename-ADObject ║ ActiveDirectory Move-ADObject ║ ActiveDirectory Set-ADUser ║ ActiveDirectory Get-FunctionDisplay ║ EguibarIT Write-Verbose ║ Microsoft.PowerShell.Utility Write-Warning ║ Microsoft.PowerShell.Utility Write-Debug ║ Microsoft.PowerShell.Utility Write-Error ║ Microsoft.PowerShell.Utility Version: 1.1 DateModified: 29/Apr/2025 LastModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar IT http://www.eguibarit.com .LINK https://github.com/vreguibar/EguibarIT .COMPONENT Active Directory .ROLE Administrator .FUNCTIONALITY AD Object Management #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'High' )] [OutputType([System.String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ValueFromRemainingArguments = $false, HelpMessage = 'Full path to the configuration.xml file', Position = 0)] [ValidateScript({ if (-Not ($_ | Test-Path -PathType Leaf) ) { throw ('File not found: {0}' -f $_) } if ($_.Extension -ne '.xml') { throw ('File must be XML: {0}' -f $_) } try { [xml]$xml = Get-Content -Path $_ -ErrorAction Stop # Verify required XML elements are present if ($null -eq $xml.n.Admin -or $null -eq $xml.n.Admin.OUs -or $null -eq $xml.n.Admin.Users) { throw 'XML file is missing required elements (Admin, OUs or Users section)' } return $true } catch { throw ('Invalid XML file: {0}' -f $_.Exception.Message) } })] [PSDefaultValue(Help = 'Default Value is "C:\PsScripts\Config.xml"', Value = 'C:\PsScripts\Config.xml' )] [Alias('Config', 'XML', 'ConfigXml')] [System.IO.FileInfo] $ConfigXMLFile, [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ValueFromRemainingArguments = $false, HelpMessage = 'Path to all the scripts and files needed by this function', Position = 1)] [PSDefaultValue( Help = 'Default Value is "C:\PsScripts\"', Value = 'C:\PsScripts\' )] [Alias('ScriptPath')] [string] $DMScripts = 'C:\PsScripts\', [Parameter(Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false, ValueFromRemainingArguments = $false, HelpMessage = 'Start transcript logging to DMScripts path with function name', Position = 2)] [Alias('Transcript', 'Log')] [switch] $EnableTranscript ) Begin { Set-StrictMode -Version Latest If (-not $PSBoundParameters.ContainsKey('ConfigXMLFile')) { $PSBoundParameters['ConfigXMLFile'] = 'C:\PsScripts\Config.xml' } #end If If (-not $PSBoundParameters.ContainsKey('DMScripts')) { $PSBoundParameters['DMScripts'] = 'C:\PsScripts\' } #end If # If EnableTranscript is specified, start a transcript if ($EnableTranscript) { # Ensure DMScripts directory exists if (-not (Test-Path -Path $DMScripts -PathType Container)) { try { New-Item -Path $DMScripts -ItemType Directory -Force | Out-Null Write-Verbose -Message ('Created transcript directory: {0}' -f $DMScripts) } catch { Write-Warning -Message ('Failed to create transcript directory: {0}' -f $_.Exception.Message) } #end try-catch } #end if # Create transcript filename using function name and current date/time $TranscriptFile = Join-Path -Path $DMScripts -ChildPath ('{0}_{1}.LOG' -f $MyInvocation.MyCommand.Name, (Get-Date -Format 'yyyyMMdd_HHmmss')) try { Start-Transcript -Path $TranscriptFile -Force -ErrorAction Stop Write-Verbose -Message ('Transcript started: {0}' -f $TranscriptFile) } catch { Write-Warning -Message ('Failed to start transcript: {0}' -f $_.Exception.Message) } #end try-catch } #end if # Display function header if variables exist if ($null -ne $Variables -and $null -ne $Variables.Header) { $txt = ($Variables.Header -f (Get-Date).ToString('dd/MMM/yyyy'), $MyInvocation.Mycommand, (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False) ) Write-Verbose -Message $txt } #end If ############################## # Module imports Import-MyModule -Name 'ActiveDirectory' -Verbose:$false Import-MyModule -Name 'EguibarIT' -Verbose:$false Import-MyModule -Name 'EguibarIT.DelegationPS' -Verbose:$false ############################## # Variables Definition # Parameters variable for splatting CMDlets [hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase) # Load the XML configuration file try { [xml]$confXML = [xml](Get-Content $PSBoundParameters['ConfigXMLFile']) } catch { Write-Error -Message ('Error reading XML file: {0}' -f $_.Exception.Message) throw } #end Try-Catch # Get the current domain controller for all operations try { # Define current domain controller #[string]$CurrentDC = (Get-ADDomainController -Discover -NextClosestSite -ErrorAction Stop).HostName[0] [string]$CurrentDC = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().FindDomainController().Name Write-Debug -Message ('Using domain controller: {0}' -f $CurrentDC) } catch { Write-Error -Message ('Error discovering domain controller: {0}' -f $_.Exception.Message) throw } #end Try-Catch # Define OU names from XML configuration [hashtable]$OuNames = @{ # Main Admin OU ItAdminOu = $ConfXML.n.Admin.OUs.ItAdminOU.name # Admin sub-OUs ItAdminAccountsOu = $ConfXML.n.Admin.OUs.ItAdminAccountsOU.name ItAdminGroupsOU = $ConfXML.n.Admin.OUs.ItAdminGroupsOU.name ItPrivGroupsOU = $ConfXML.n.Admin.OUs.ItPrivGroupsOU.name ItRightsOu = $ConfXML.n.Admin.OUs.ItRightsOU.name } # Generate DN paths for OUs [string]$ItAdminAccountsOuDn = ('OU={0},OU={1},{2}' -f $OuNames.ItAdminAccountsOu, $OuNames.ItAdminOu, $Variables.AdDn) [string]$ItAdminGroupsOuDn = ('OU={0},OU={1},{2}' -f $OuNames.ItAdminGroupsOU, $OuNames.ItAdminOu, $Variables.AdDn) [string]$ItPrivGroupsOUDn = ('OU={0},OU={1},{2}' -f $OuNames.ItPrivGroupsOU, $OuNames.ItAdminOu, $Variables.AdDn) [string]$ItRightsOuDn = ('OU={0},OU={1},{2}' -f $OuNames.ItRightsOu, $OuNames.ItAdminOu, $Variables.AdDn) #region Users Variables $AdminName = Get-SafeVariable -Name 'AdminName' -CreateIfNotExist { try { Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-500' } } catch { Write-Debug -Message ('Failed to retrieve Administrator name: {0}' -f $_.Exception.Message) $null } } $GuestNewName = Get-SafeVariable -Name 'GuestNewName' -CreateIfNotExist { try { Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-501' } } catch { Write-Debug -Message ('Failed to retrieve Guest name: {0}' -f $_.Exception.Message) $null } } #endregion Users Variables #region Well-Known groups Variables $DomainAdmins = Get-SafeVariable -Name 'DomainAdmins' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-512' } } catch { Write-Debug -Message ('Failed to retrieve Domain Admins group: {0}' -f $_.Exception.Message) $null } } $EnterpriseAdmins = Get-SafeVariable -Name 'EnterpriseAdmins' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-519' } } catch { Write-Debug -Message ('Failed to retrieve Enterprise Admins group: {0}' -f $_.Exception.Message) $null } } $SchemaAdmins = Get-SafeVariable -Name 'SchemaAdmins' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-518' } } catch { Write-Debug -Message ('Failed to retrieve Schema Admins group: {0}' -f $_.Exception.Message) $null } } $DomainControllers = Get-SafeVariable -Name 'DomainControllers' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-516' } } catch { Write-Debug -Message ('Failed to retrieve Domain Controllers group: {0}' -f $_.Exception.Message) $null } } $RODC = Get-SafeVariable -Name 'RODC' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-521' } } catch { Write-Debug -Message ('Failed to retrieve Read Only Domain Controllers group: {0}' -f $_.Exception.Message) $null } } $GPOCreatorsOwner = Get-SafeVariable -Name 'GPOCreatorsOwner' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-520' } } catch { Write-Debug -Message ('Failed to retrieve Group Policy Creators Owner group: {0}' -f $_.Exception.Message) $null } } $DeniedRODC = Get-SafeVariable -Name 'DeniedRODC' -CreateIfNotExist { try { Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-572' } } catch { Write-Debug -Message ('Failed to retrieve Denied Read Only Domain Controllers group: {0}' -f $_.Exception.Message) $null } } #endregion Well-Known groups Variables #region Global groups Variables $DnsAdmins = Get-SafeVariable -Name 'DnsAdmins' -CreateIfNotExist { Get-AdObjectType -Identity 'DnsAdmins' } $ProtectedUsers = Get-SafeVariable -Name 'ProtectedUsers' -CreateIfNotExist { Get-AdObjectType -Identity 'Protected Users' } #endregion Global groups Variables } #end Begin Process { if ($PSCmdlet.ShouldProcess('Active Directory', 'Moving Tier0 objects')) { try { # Move, and if needed, rename the Admin account if ($null -ne $AdminName -and $null -ne $confXML.n.Admin.users.Admin.Name) { if ($AdminName.Name -ne $confXML.n.Admin.users.Admin.Name) { Write-Debug -Message ('Renaming admin account to: {0}' -f $confXML.n.Admin.users.Admin.Name) $Splat = @{ Identity = $AdminName.DistinguishedName NewName = $ConfXML.n.Admin.users.Admin.Name Server = $CurrentDC } Rename-ADObject @Splat $Splat = @{ Identity = $AdminName SamAccountName = $ConfXML.n.Admin.users.Admin.Name DisplayName = $ConfXML.n.Admin.users.Admin.Name Server = $CurrentDC } Set-ADUser @Splat } #end If Write-Debug -Message ('Moving admin account to: {0}' -f $ItAdminAccountsOuDn) $AdminName | Move-ADObject -TargetPath $ItAdminAccountsOuDn -Server $CurrentDC } #end If # Move the Guest Account if it exists if ($null -ne $GuestNewName) { Write-Debug -Message ('Moving guest account to: {0}' -f $ItAdminAccountsOuDn) $GuestNewName | Move-ADObject -TargetPath $ItAdminAccountsOuDn -Server $CurrentDC } #end If Get-ADUser -Identity 'krbtgt' | Move-ADObject -TargetPath $ItAdminAccountsOuDn -Server $CurrentDC $DomainAdmins | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC $EnterpriseAdmins | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity $SchemaAdmins | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity $DomainControllers | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity $GPOCreatorsOwner | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity $RODC | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'Enterprise Read-only Domain Controllers' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'DnsUpdateProxy' | Move-ADObject -TargetPath $ItAdminGroupsOuDn -Server $CurrentDC Get-ADGroup -Identity 'Domain Users' | Move-ADObject -TargetPath $ItAdminGroupsOuDn -Server $CurrentDC Get-ADGroup -Identity 'Domain Computers' | Move-ADObject -TargetPath $ItAdminGroupsOuDn -Server $CurrentDC Get-ADGroup -Identity 'Domain Guests' | Move-ADObject -TargetPath $ItAdminGroupsOuDn -Server $CurrentDC Get-ADGroup -Identity 'Allowed RODC Password Replication Group' | Move-ADObject -TargetPath $ItRightsOuDn -Server $CurrentDC Get-ADGroup -Identity 'RAS and IAS Servers' | Move-ADObject -TargetPath $ItRightsOuDn -Server $CurrentDC $DnsAdmins | Move-ADObject -TargetPath $ItRightsOuDn -Server $CurrentDC Get-ADGroup -Identity 'Cert Publishers' | Move-ADObject -TargetPath $ItRightsOuDn -Server $CurrentDC Get-ADGroup -Identity $DeniedRODC | Move-ADObject -TargetPath $ItRightsOuDn -Server $CurrentDC $ProtectedUsers | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'Cloneable Domain Controllers' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'Access-Denied Assistance Users' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Filter { SamAccountName -like 'WinRMRemoteWMIUsers*' } | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC # ToDo: Check for group existence before moving # Following groups only exist on Win 2019 If ([System.Environment]::OSVersion.Version.Build -ge 17763) { Get-ADGroup -Identity 'Enterprise Key Admins' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'Key Admins' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'External Trust Accounts' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC Get-ADGroup -Identity 'Forest Trust Accounts' | Move-ADObject -TargetPath $ItPrivGroupsOUDn -Server $CurrentDC #Get-ADGroup -Identity 'Windows Admin Center CredSSP Administrators' | Move-ADObject -TargetPath $ItPrivGroupsOUDn } # Get-ADGroup $Administrators | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Account Operators" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Backup Operators" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Certificate Service DCOM Access" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Cryptographic Operators" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Server Operators" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Remote Desktop Users" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Distributed COM Users" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Event Log Readers" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Guests" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "IIS_IUSRS" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Incoming Forest Trust Builders" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup $NetConfOperators | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Performance Log Users" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Performance Monitor Users" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Pre-Windows 2000 Compatible Access" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Print Operators" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Replicator" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Terminal Server License Servers" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Users" | Move-ADObject -TargetPath $ItRightsOuDn # Get-ADGroup "Windows Authorization Access Group" | Move-ADObject -TargetPath $ItRightsOuDn # REFRESH - Get the object after moving it. Write-Verbose -Message 'Refreshing security principal variables after moves' $Splat = @{ Name = 'AdminName' Value = (Get-ADUser -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-500' }) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'DomainAdmins' Value = (Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-512' }) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'EnterpriseAdmins' Value = (Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-519' }) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'GPOCreatorsOwner' Value = (Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-520' }) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'DeniedRODC' Value = (Get-ADGroup -Filter * | Where-Object { $_.SID -like 'S-1-5-21-*-572' }) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'DnsAdmins' Value = (Get-ADGroup -Identity 'DnsAdmins' -Server $CurrentDC -ErrorAction SilentlyContinue) Scope = 'Global' Force = $true } New-Variable @Splat $Splat = @{ Name = 'ProtectedUsers' Value = (Get-ADGroup -Identity 'Protected Users' -Server $CurrentDC -ErrorAction SilentlyContinue) Scope = 'Global' Force = $true } New-Variable @Splat Write-Verbose -Message 'Successfully moved all Tier0 objects to their respective OUs' return 'Tier0 objects have been successfully moved to their respective OUs' } catch { Write-Error -Message ('Error moving Tier0 objects: {0}' -f $_.Exception.Message) throw } #end Try-Catch } #end If ShouldProcess } #end Process End { # Display function footer if variables exist if ($null -ne $Variables -and $null -ne $Variables.Footer) { $txt = ($Variables.Footer -f $MyInvocation.InvocationName, 'moving Tier0 objects.' ) Write-Verbose -Message $txt } #end If # Stop transcript if it was started if ($EnableTranscript) { try { Stop-Transcript -ErrorAction Stop Write-Verbose -Message 'Transcript stopped successfully' } catch { Write-Warning -Message ('Failed to stop transcript: {0}' -f $_.Exception.Message) } #end Try-Catch } #end If } #end End } #end Function New-Tier0MoveObject |