Private/AD/Core/Get-ADDomainControllers.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-ADDomainControllers { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) # ── Obsolete/unsupported OS patterns ────────────────────────────── # OS names that indicate end-of-support or obsolete Windows Server versions. # Windows Server 2012 R2 extended support ended Oct 2023. # Windows Server 2012 (non-R2) ended Oct 2023. # Anything older is long out of support. $obsoleteOsPatterns = @( 'Windows Server 2012 R2' 'Windows Server 2012' 'Windows Server 2008 R2' 'Windows Server 2008' 'Windows Server 2003' 'Windows 2000' ) # "Unsupported" means entirely out of extended support (2012 and older, excluding 2012 R2 which # left support at the same time but is tracked separately for organizations that may have ESU). $unsupportedOsPatterns = @( 'Windows Server 2012' # non-R2 only — matched before 2012 R2 check below 'Windows Server 2008 R2' 'Windows Server 2008' 'Windows Server 2003' 'Windows 2000' ) $domainDN = $Connection.DomainDN $configDN = $Connection.ConfigDN if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Enumerating domain controllers' } # ── 1. Query DC computer objects ────────────────────────────────── # SERVER_TRUST_ACCOUNT (0x2000 = 8192) identifies domain controller computer accounts $dcFilter = '(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))' $dcProperties = @( 'cn', 'dNSHostName', 'distinguishedName', 'objectSid', 'operatingSystem', 'operatingSystemVersion', 'operatingSystemServicePack', 'userAccountControl', 'lastLogonTimestamp', 'whenCreated', 'msDS-isRODC', 'primaryGroupID' ) $dcResults = @() try { $domainRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $dcResults = Invoke-LdapQuery -SearchRoot $domainRoot ` -Filter $dcFilter ` -Properties $dcProperties } catch { Write-Warning "Failed to enumerate domain controllers: $_" return @() } Write-Verbose "Found $($dcResults.Count) domain controller(s)" # ── 2. Determine FSMO role holders for cross-reference ──────────── $fsmoHolders = @{ SchemaMaster = '' DomainNamingMaster = '' PDCEmulator = '' RIDMaster = '' InfrastructureMaster = '' } $extractServerFromNtds = { param([string]$NtdsDN) if ([string]::IsNullOrWhiteSpace($NtdsDN)) { return '' } $remainder = $NtdsDN -replace '^CN=NTDS Settings,', '' if ($remainder -match '^CN=([^,]+)') { return $Matches[1] } return '' } # Schema Master try { $schemaRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $Connection.SchemaDN $r = @(Invoke-LdapQuery -SearchRoot $schemaRoot -Filter '(objectClass=dMD)' -Properties @('fSMORoleOwner') -Scope Base) if ($r.Count -gt 0 -and $r[0].ContainsKey('fsmoroleowner')) { $fsmoHolders.SchemaMaster = & $extractServerFromNtds $r[0]['fsmoroleowner'] } } catch { Write-Verbose "Could not resolve Schema Master: $_" } # Domain Naming Master try { $partRoot = New-LdapSearchRoot -Connection $Connection -SearchBase "CN=Partitions,$configDN" $r = @(Invoke-LdapQuery -SearchRoot $partRoot -Filter '(objectClass=crossRefContainer)' -Properties @('fSMORoleOwner') -Scope Base) if ($r.Count -gt 0 -and $r[0].ContainsKey('fsmoroleowner')) { $fsmoHolders.DomainNamingMaster = & $extractServerFromNtds $r[0]['fsmoroleowner'] } } catch { Write-Verbose "Could not resolve Domain Naming Master: $_" } # PDC Emulator try { $domHead = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $r = @(Invoke-LdapQuery -SearchRoot $domHead -Filter '(objectClass=domainDNS)' -Properties @('fSMORoleOwner') -Scope Base) if ($r.Count -gt 0 -and $r[0].ContainsKey('fsmoroleowner')) { $fsmoHolders.PDCEmulator = & $extractServerFromNtds $r[0]['fsmoroleowner'] } } catch { Write-Verbose "Could not resolve PDC Emulator: $_" } # RID Master try { $ridRoot = New-LdapSearchRoot -Connection $Connection -SearchBase "CN=RID Manager`$,CN=System,$domainDN" $r = @(Invoke-LdapQuery -SearchRoot $ridRoot -Filter '(objectClass=rIDManager)' -Properties @('fSMORoleOwner') -Scope Base) if ($r.Count -gt 0 -and $r[0].ContainsKey('fsmoroleowner')) { $fsmoHolders.RIDMaster = & $extractServerFromNtds $r[0]['fsmoroleowner'] } } catch { Write-Verbose "Could not resolve RID Master: $_" } # Infrastructure Master try { $infraRoot = New-LdapSearchRoot -Connection $Connection -SearchBase "CN=Infrastructure,$domainDN" $r = @(Invoke-LdapQuery -SearchRoot $infraRoot -Filter '(objectClass=infrastructureUpdate)' -Properties @('fSMORoleOwner') -Scope Base) if ($r.Count -gt 0 -and $r[0].ContainsKey('fsmoroleowner')) { $fsmoHolders.InfrastructureMaster = & $extractServerFromNtds $r[0]['fsmoroleowner'] } } catch { Write-Verbose "Could not resolve Infrastructure Master: $_" } # ── 3. Determine Global Catalog servers from NTDS Settings ──────── $gcServers = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) try { $sitesRoot = New-LdapSearchRoot -Connection $Connection -SearchBase "CN=Sites,$configDN" $ntdsResults = Invoke-LdapQuery -SearchRoot $sitesRoot ` -Filter '(&(objectClass=nTDSDSA)(options:1.2.840.113556.1.4.803:=1))' ` -Properties @('distinguishedName') foreach ($ntds in $ntdsResults) { $dn = $ntds['distinguishedname'] $serverName = & $extractServerFromNtds $dn if ($serverName) { [void]$gcServers.Add($serverName) } } Write-Verbose "Found $($gcServers.Count) Global Catalog server(s)" } catch { Write-Verbose "Failed to enumerate GC servers via NTDS Settings: $_" } # ── 4. Build DC result objects ──────────────────────────────────── $domainControllers = [System.Collections.Generic.List[hashtable]]::new() foreach ($dc in $dcResults) { $dcName = if ($dc.ContainsKey('cn')) { $dc['cn'] } else { '' } $dcFQDN = if ($dc.ContainsKey('dnshostname')) { $dc['dnshostname'] } else { '' } $os = if ($dc.ContainsKey('operatingsystem')) { $dc['operatingsystem'] } else { '' } $osVer = if ($dc.ContainsKey('operatingsystemversion')) { $dc['operatingsystemversion'] } else { '' } $osSP = if ($dc.ContainsKey('operatingsystemservicepack')) { $dc['operatingsystemservicepack'] } else { '' } $uac = if ($dc.ContainsKey('useraccountcontrol')) { [int]$dc['useraccountcontrol'] } else { 0 } # Determine if this is an RODC # Method 1: msDS-isRODC attribute (Server 2008+) # Method 2: PARTIAL_SECRETS_ACCOUNT UAC flag (0x04000000) # Method 3: primaryGroupID = 521 (Read-only Domain Controllers group) $isRODC = $false if ($dc.ContainsKey('msds-isrodc')) { $isRODC = [bool]$dc['msds-isrodc'] } if (-not $isRODC) { $isRODC = ($uac -band 0x04000000) -ne 0 } if (-not $isRODC -and $dc.ContainsKey('primarygroupid')) { $isRODC = ([int]$dc['primarygroupid'] -eq 521) } # Determine if this is a Global Catalog $isGC = $gcServers.Contains($dcName) # Resolve IPv4 address from DNS hostname $ipv4 = '' if ($dcFQDN) { try { $dnsEntry = [System.Net.Dns]::GetHostAddresses($dcFQDN) $ipv4Addr = $dnsEntry | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if ($ipv4Addr) { $ipv4 = $ipv4Addr.ToString() } } catch { Write-Verbose "DNS resolution failed for $dcFQDN`: $_" } } # Determine FSMO roles held by this DC $dcFsmoRoles = [System.Collections.Generic.List[string]]::new() foreach ($role in $fsmoHolders.GetEnumerator()) { if ($role.Value -and $role.Value -eq $dcName) { $dcFsmoRoles.Add($role.Key) } } # Determine obsolete/unsupported OS $isObsoleteOS = $false $isUnsupportedOS = $false if ($os) { foreach ($pattern in $obsoleteOsPatterns) { if ($os -like "*$pattern*") { $isObsoleteOS = $true break } } # For unsupported check, we need to be careful: "Windows Server 2012" should not # match "Windows Server 2012 R2". Check 2012 R2 first, then plain 2012. if ($os -like '*Windows Server 2012 R2*') { $isUnsupportedOS = $true # 2012 R2 is also out of support } elseif ($os -like '*Windows Server 2012*') { $isUnsupportedOS = $true } elseif ($os -like '*Windows Server 2008 R2*' -or $os -like '*Windows Server 2008*' -or $os -like '*Windows Server 2003*' -or $os -like '*Windows 2000*') { $isUnsupportedOS = $true } } $dcObj = @{ Name = $dcName FQDN = $dcFQDN DistinguishedName = if ($dc.ContainsKey('distinguishedname')) { $dc['distinguishedname'] } else { '' } OperatingSystem = $os OperatingSystemVersion = $osVer OperatingSystemServicePack = $osSP IsGlobalCatalog = $isGC IsRODC = $isRODC IPv4Address = $ipv4 LastLogon = if ($dc.ContainsKey('lastlogontimestamp')) { $dc['lastlogontimestamp'] } else { $null } WhenCreated = if ($dc.ContainsKey('whencreated')) { $dc['whencreated'] } else { $null } SID = if ($dc.ContainsKey('objectsid')) { $dc['objectsid'] } else { '' } UserAccountControl = $uac FSMORoles = @($dcFsmoRoles) ObsoleteOS = $isObsoleteOS UnsupportedOS = $isUnsupportedOS } $domainControllers.Add($dcObj) } # ── Summary ─────────────────────────────────────────────────────── if (-not $Quiet) { $gcCount = @($domainControllers | Where-Object { $_.IsGlobalCatalog }).Count $rodcCount = @($domainControllers | Where-Object { $_.IsRODC }).Count $obsoleteCount = @($domainControllers | Where-Object { $_.ObsoleteOS }).Count $summary = "Found $($domainControllers.Count) DC(s): $gcCount GC, $rodcCount RODC" if ($obsoleteCount -gt 0) { $summary += ", $obsoleteCount obsolete OS" } Write-ProgressLine -Phase AUDITING -Message $summary } return @($domainControllers) } |