# Apply-ADBaseline.ps1 # Orchestration script for managing AD objects (OUs, groups, users). # # Reads declarative definitions from ous.ps1, groups.ps1, users.ps1, # delegations.ps1, and password-policies.ps1, then compares against # live AD and applies changes. Works from any machine with RSAT. # # Usage: # .\Apply-ADBaseline.ps1 # Apply all AD objects # .\Apply-ADBaseline.ps1 -TestOnly # Compare desired vs. current (no changes) [CmdletBinding()] param( [switch]$TestOnly ) $ErrorActionPreference = 'Stop' $ScriptRoot = $PSScriptRoot # ------------------------------------------------------------------- # Load helper functions # ------------------------------------------------------------------- . (Join-Path $ScriptRoot 'lib\ADHelper.ps1') # ------------------------------------------------------------------- # Load definitions # ------------------------------------------------------------------- $ous = & (Join-Path $ScriptRoot 'ous.ps1') $groups = & (Join-Path $ScriptRoot 'groups.ps1') $users = & (Join-Path $ScriptRoot 'users.ps1') $delegations = & (Join-Path $ScriptRoot 'delegations.ps1') $passwordPolicies = & (Join-Path $ScriptRoot 'password-policies.ps1') $totalDiffs = 0 $createdUsers = @() # ------------------------------------------------------------------- # Pre-flight: Warn about stale credential files (older than 24 hours) # ------------------------------------------------------------------- $credDir = Join-Path $ScriptRoot '.credentials' if (Test-Path $credDir) { $staleFiles = Get-ChildItem -Path $credDir -Filter '*.txt' | Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-24) } if ($staleFiles.Count -gt 0) { Write-Host '========================================================' -ForegroundColor Red Write-Host ' WARNING: Stale credential files detected (>24 hours)' -ForegroundColor Red Write-Host '========================================================' -ForegroundColor Red foreach ($f in $staleFiles) { $age = [math]::Round(((Get-Date) - $f.LastWriteTime).TotalHours, 1) Write-Host " - $($f.Name) (${age}h old)" -ForegroundColor Red } Write-Host ' These should have been handed off and deleted.' -ForegroundColor Red Write-Host '========================================================' -ForegroundColor Red Write-Host '' } } # ------------------------------------------------------------------- # Step 1: Organizational Units (must exist before groups/users) # ------------------------------------------------------------------- Write-Host "=== Organizational Units ===" -ForegroundColor White foreach ($ou in $ous) { if ($TestOnly) { $diff = Compare-ADOU -Name $ou.Name -Path $ou.Path if ($diff) { $totalDiffs++ } } else { Ensure-ADOU -Name $ou.Name -Path $ou.Path -Description $ou.Description | Out-Null } } Write-Host '' # ------------------------------------------------------------------- # Step 2: Security Groups (must exist before users can join them) # ------------------------------------------------------------------- Write-Host "=== Security Groups ===" -ForegroundColor White foreach ($group in $groups) { if ($TestOnly) { $diffs = Compare-ADSecurityGroup -Name $group.Name -Members $group.Members $totalDiffs += @($diffs).Count } else { $params = @{ Name = $group.Name Path = $group.Path Description = $group.Description Scope = $group.Scope } # Don't sync members yet - users may not exist Ensure-ADSecurityGroup @params | Out-Null } } Write-Host '' # ------------------------------------------------------------------- # Step 3: User Accounts # ------------------------------------------------------------------- Write-Host "=== User Accounts ===" -ForegroundColor White foreach ($user in $users) { # Extract optional properties (anything not in the core schema) $coreKeys = @('SamAccountName','Name','GivenName','Surname','Path','Enabled','MemberOf') $optionalProps = @{} foreach ($key in $user.Keys) { if ($key -notin $coreKeys) { $optionalProps[$key] = $user[$key] } } if ($TestOnly) { $diffs = Compare-ADUserAccount ` -SamAccountName $user.SamAccountName ` -Path $user.Path ` -Enabled $user.Enabled ` -MemberOf $user.MemberOf ` -Properties $optionalProps $totalDiffs += @($diffs).Count } else { $wasCreated = Ensure-ADUserAccount ` -SamAccountName $user.SamAccountName ` -Name $user.Name ` -GivenName $user.GivenName ` -Surname $user.Surname ` -Path $user.Path ` -Enabled $user.Enabled ` -MemberOf $user.MemberOf ` -Properties $optionalProps if ($wasCreated) { $createdUsers += $user.SamAccountName } } } Write-Host '' # ------------------------------------------------------------------- # Step 4: Sync group membership (now that users exist) # ------------------------------------------------------------------- if (-not $TestOnly) { Write-Host "=== Group Membership Sync ===" -ForegroundColor White foreach ($group in $groups) { if ($group.Members.Count -gt 0) { Ensure-ADSecurityGroup -Name $group.Name -Path $group.Path -Scope $group.Scope -Members $group.Members | Out-Null } } Write-Host '' } # ------------------------------------------------------------------- # Step 5: OU Delegation (ACLs) # ------------------------------------------------------------------- Write-Host "=== OU Delegation ===" -ForegroundColor White foreach ($delegation in $delegations) { foreach ($ou in $delegation.TargetOUs) { if ($TestOnly) { $diffs = Compare-OUDelegation ` -GroupName $delegation.GroupName ` -TargetOU $ou ` -Rights $delegation.Rights $totalDiffs += @($diffs).Count } else { Ensure-OUDelegation ` -GroupName $delegation.GroupName ` -TargetOU $ou ` -Rights $delegation.Rights | Out-Null } } } Write-Host '' # ------------------------------------------------------------------- # Step 6: Fine-Grained Password Policies (PSOs) # ------------------------------------------------------------------- Write-Host "=== Password Policies ===" -ForegroundColor White foreach ($pso in $passwordPolicies) { if ($TestOnly) { $diffs = Compare-ADPasswordPolicy -Definition $pso $totalDiffs += @($diffs).Count } else { Ensure-ADPasswordPolicy -Definition $pso } } Write-Host '' # ------------------------------------------------------------------- # Summary # ------------------------------------------------------------------- if ($TestOnly) { if ($totalDiffs -eq 0) { Write-Host 'All AD objects match desired state.' -ForegroundColor Green } else { Write-Host "$totalDiffs difference(s) found." -ForegroundColor Red } Write-Host 'Compliance test complete. No changes were made.' -ForegroundColor Yellow } else { Write-Host 'All AD objects applied.' -ForegroundColor Green # --- Credential handoff warning --- if ($createdUsers.Count -gt 0) { $credDir = Join-Path $ScriptRoot '.credentials' Write-Host '' Write-Host '========================================================' -ForegroundColor Magenta Write-Host ' ACTION REQUIRED: New user credentials pending handoff' -ForegroundColor Magenta Write-Host '========================================================' -ForegroundColor Magenta Write-Host '' Write-Host " $($createdUsers.Count) user(s) were created with temporary passwords." -ForegroundColor Yellow Write-Host ' Passwords are stored in:' -ForegroundColor Yellow Write-Host " $credDir" -ForegroundColor White Write-Host '' foreach ($u in $createdUsers) { Write-Host " - $u.txt" -ForegroundColor White } Write-Host '' Write-Host ' You must:' -ForegroundColor Yellow Write-Host ' 1. Read each credential file' -ForegroundColor White Write-Host ' 2. Securely share the password with the end user' -ForegroundColor White Write-Host ' 3. Delete the credential file after handoff' -ForegroundColor White Write-Host '' Write-Host ' These files are ACL-locked to your account only.' -ForegroundColor DarkGray Write-Host ' Users must change their password on first login.' -ForegroundColor DarkGray Write-Host '========================================================' -ForegroundColor Magenta } }