Private/AD/Core/Get-ADDomainInfo.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-ADDomainInfo { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) # ── Functional level mapping ────────────────────────────────────── $functionalLevelMap = @{ 0 = '2000' 1 = '2003Interim' 2 = '2003' 3 = '2008' 4 = '2008R2' 5 = '2012' 6 = '2012R2' 7 = '2016' 10 = '2025' } # ── Schema version mapping ──────────────────────────────────────── $schemaVersionMap = @{ 13 = '2000' 30 = '2003' 31 = '2003R2' 44 = '2008' 47 = '2008R2' 56 = '2012' 69 = '2012R2' 87 = '2016' 88 = '2019' 90 = '2022' 91 = '2025' } $result = @{ ForestFunctionalLevel = 0 ForestFunctionalLevelName = 'Unknown' DomainFunctionalLevel = 0 DomainFunctionalLevelName = 'Unknown' SchemaVersion = 0 SchemaVersionName = 'Unknown' DomainName = '' ForestName = '' DomainDN = '' ForestDN = '' DomainSID = '' Sites = @() RecycleBinEnabled = $false TombstoneLifetime = 0 FSMORoles = @{ SchemaMaster = '' DomainNamingMaster = '' PDCEmulator = '' RIDMaster = '' InfrastructureMaster = '' } DnsZones = @() Errors = @{} } $domainDN = $Connection.DomainDN $configDN = $Connection.ConfigDN $schemaDN = $Connection.SchemaDN $forestDN = $Connection.ForestDN # ── 1. Functional levels ────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Collecting domain functional levels' } $result.ForestFunctionalLevel = $Connection.ForestFunctionality $result.DomainFunctionalLevel = $Connection.DomainFunctionality if ($functionalLevelMap.ContainsKey($Connection.ForestFunctionality)) { $result.ForestFunctionalLevelName = $functionalLevelMap[$Connection.ForestFunctionality] } if ($functionalLevelMap.ContainsKey($Connection.DomainFunctionality)) { $result.DomainFunctionalLevelName = $functionalLevelMap[$Connection.DomainFunctionality] } # ── 2. Schema version ───────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Reading schema version' } try { $schemaRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $schemaDN $schemaResults = @(Invoke-LdapQuery -SearchRoot $schemaRoot ` -Filter '(objectClass=dMD)' ` -Properties @('objectVersion') ` -Scope Base) if ($schemaResults.Count -gt 0 -and $schemaResults[0].ContainsKey('objectversion')) { $result.SchemaVersion = [int]$schemaResults[0]['objectversion'] if ($schemaVersionMap.ContainsKey($result.SchemaVersion)) { $result.SchemaVersionName = $schemaVersionMap[$result.SchemaVersion] } } } catch { Write-Verbose "Failed to read schema version: $_" $result.Errors['SchemaVersion'] = $_.Exception.Message } # ── 3. Domain and forest names ──────────────────────────────────── $result.DomainDN = $domainDN $result.ForestDN = $forestDN # Convert DN to DNS-style name: DC=contoso,DC=com -> contoso.com $result.DomainName = ($domainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower() $result.ForestName = ($forestDN -replace '^DC=', '' -replace ',DC=', '.').ToLower() # ── 4. Domain SID ───────────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Retrieving domain SID' } try { $domainRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $domainObj = @(Invoke-LdapQuery -SearchRoot $domainRoot ` -Filter '(objectClass=domainDNS)' ` -Properties @('objectSid') ` -Scope Base) if ($domainObj.Count -gt 0 -and $domainObj[0].ContainsKey('objectsid')) { $result.DomainSID = $domainObj[0]['objectsid'] } } catch { Write-Verbose "Failed to retrieve domain SID: $_" $result.Errors['DomainSID'] = $_.Exception.Message } # ── 5. Sites, subnets, site links, and servers ──────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Enumerating AD sites and subnets' } try { $sitesContainerDN = "CN=Sites,$configDN" $sitesRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $sitesContainerDN # Get all site objects $siteResults = Invoke-LdapQuery -SearchRoot $sitesRoot ` -Filter '(objectClass=site)' ` -Properties @('cn', 'distinguishedName', 'description', 'whenCreated') ` -Scope OneLevel Write-Verbose "Found $($siteResults.Count) site(s)" # Get all subnet objects $subnetsContainerDN = "CN=Subnets,CN=Sites,$configDN" $subnetResults = @() try { $subnetsRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $subnetsContainerDN $subnetResults = Invoke-LdapQuery -SearchRoot $subnetsRoot ` -Filter '(objectClass=subnet)' ` -Properties @('cn', 'siteObject', 'description') ` -Scope OneLevel } catch { Write-Verbose "Failed to enumerate subnets: $_" } # Build subnet-to-site lookup $subnetsBySite = @{} foreach ($subnet in $subnetResults) { $siteRef = if ($subnet.ContainsKey('siteobject')) { $subnet['siteobject'] } else { '' } if ($siteRef) { if (-not $subnetsBySite.ContainsKey($siteRef)) { $subnetsBySite[$siteRef] = [System.Collections.Generic.List[string]]::new() } $subnetsBySite[$siteRef].Add($subnet['cn']) } } # Get all site link objects $siteLinksContainerDN = "CN=IP,CN=Inter-Site Transports,CN=Sites,$configDN" $siteLinkResults = @() try { $siteLinksRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $siteLinksContainerDN $siteLinkResults = Invoke-LdapQuery -SearchRoot $siteLinksRoot ` -Filter '(objectClass=siteLink)' ` -Properties @('cn', 'siteList', 'cost', 'replInterval', 'description') ` -Scope OneLevel } catch { Write-Verbose "Failed to enumerate site links: $_" } # Build site link lookup: site DN -> list of site link names $siteLinksBySite = @{} foreach ($link in $siteLinkResults) { $linkedSites = @() if ($link.ContainsKey('sitelist')) { $linkedSites = if ($link['sitelist'] -is [array]) { $link['sitelist'] } else { @($link['sitelist']) } } foreach ($linkedSiteDN in $linkedSites) { if (-not $siteLinksBySite.ContainsKey($linkedSiteDN)) { $siteLinksBySite[$linkedSiteDN] = [System.Collections.Generic.List[string]]::new() } $siteLinksBySite[$linkedSiteDN].Add($link['cn']) } } # Build sites array with servers $sites = [System.Collections.Generic.List[hashtable]]::new() foreach ($site in $siteResults) { $siteDN = $site['distinguishedname'] $siteName = $site['cn'] # Get servers (DCs) in this site $siteServers = @() try { $serversContainerDN = "CN=Servers,$siteDN" $serversRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $serversContainerDN $serverResults = Invoke-LdapQuery -SearchRoot $serversRoot ` -Filter '(objectClass=server)' ` -Properties @('cn', 'dNSHostName') ` -Scope OneLevel $siteServers = @($serverResults | ForEach-Object { @{ Name = $_['cn'] FQDN = if ($_.ContainsKey('dnshostname')) { $_['dnshostname'] } else { $_['cn'] } } }) } catch { Write-Verbose "Failed to enumerate servers in site $siteName`: $_" } $siteObj = @{ Name = $siteName Description = if ($site.ContainsKey('description')) { $site['description'] } else { '' } Subnets = @(if ($subnetsBySite.ContainsKey($siteDN)) { $subnetsBySite[$siteDN] } else { @() }) SiteLinks = @(if ($siteLinksBySite.ContainsKey($siteDN)) { $siteLinksBySite[$siteDN] } else { @() }) Servers = $siteServers } $sites.Add($siteObj) } $result.Sites = @($sites) } catch { Write-Verbose "Failed to enumerate sites: $_" $result.Errors['Sites'] = $_.Exception.Message } # ── 6. Recycle Bin feature ──────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Checking AD Recycle Bin status' } try { $recycleBinDN = "CN=Recycle Bin Feature,CN=Optional Features,CN=Directory Service,CN=Windows NT,CN=Services,$configDN" $recycleBinRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $recycleBinDN $recycleBinResults = Invoke-LdapQuery -SearchRoot $recycleBinRoot ` -Filter '(objectClass=msDS-OptionalFeature)' ` -Properties @('msDS-EnabledFeatureBL') ` -Scope Base if ($recycleBinResults.Count -gt 0 -and $recycleBinResults[0].ContainsKey('msds-enabledfeaturebl')) { $enabledBL = $recycleBinResults[0]['msds-enabledfeaturebl'] $result.RecycleBinEnabled = ($null -ne $enabledBL -and @($enabledBL).Count -gt 0) } } catch { Write-Verbose "AD Recycle Bin feature not found or not accessible: $_" # Not an error condition — feature may not exist on older schemas } # ── 7. Tombstone lifetime ───────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Reading tombstone lifetime' } try { $dirServiceDN = "CN=Directory Service,CN=Windows NT,CN=Services,$configDN" $dirServiceRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $dirServiceDN $dirServiceResults = Invoke-LdapQuery -SearchRoot $dirServiceRoot ` -Filter '(objectClass=nTDSService)' ` -Properties @('tombstoneLifetime') ` -Scope Base if ($dirServiceResults.Count -gt 0 -and $dirServiceResults[0].ContainsKey('tombstonelifetime')) { $result.TombstoneLifetime = [int]$dirServiceResults[0]['tombstonelifetime'] } else { # Default tombstone lifetime is 60 days for forests upgraded from 2000/2003, 180 for newer $result.TombstoneLifetime = 180 Write-Verbose 'tombstoneLifetime attribute not set; defaulting to 180 days' } } catch { Write-Verbose "Failed to read tombstone lifetime: $_" $result.Errors['TombstoneLifetime'] = $_.Exception.Message } # ── 8. FSMO roles ───────────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Locating FSMO role holders' } # Helper to extract server name from NTDS Settings DN # CN=NTDS Settings,CN=SERVERNAME,CN=Servers,CN=SiteName,CN=Sites,... $extractServerFromNtds = { param([string]$NtdsDN) if ([string]::IsNullOrWhiteSpace($NtdsDN)) { return '' } # Remove "CN=NTDS Settings," prefix, then take the first CN value $remainder = $NtdsDN -replace '^CN=NTDS Settings,', '' if ($remainder -match '^CN=([^,]+)') { return $Matches[1] } return $NtdsDN } # Schema Master — fSMORoleOwner on the Schema container try { $schemaRoot2 = New-LdapSearchRoot -Connection $Connection -SearchBase $schemaDN $schemaFsmo = @(Invoke-LdapQuery -SearchRoot $schemaRoot2 ` -Filter '(objectClass=dMD)' ` -Properties @('fSMORoleOwner') ` -Scope Base) if ($schemaFsmo.Count -gt 0 -and $schemaFsmo[0].ContainsKey('fsmoroleowner')) { $result.FSMORoles.SchemaMaster = & $extractServerFromNtds $schemaFsmo[0]['fsmoroleowner'] } } catch { Write-Verbose "Failed to determine Schema Master: $_" $result.Errors['FSMO_SchemaMaster'] = $_.Exception.Message } # Domain Naming Master — fSMORoleOwner on CN=Partitions,<ConfigDN> try { $partitionsDN = "CN=Partitions,$configDN" $partitionsRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $partitionsDN $partitionsFsmo = @(Invoke-LdapQuery -SearchRoot $partitionsRoot ` -Filter '(objectClass=crossRefContainer)' ` -Properties @('fSMORoleOwner') ` -Scope Base) if ($partitionsFsmo.Count -gt 0 -and $partitionsFsmo[0].ContainsKey('fsmoroleowner')) { $result.FSMORoles.DomainNamingMaster = & $extractServerFromNtds $partitionsFsmo[0]['fsmoroleowner'] } } catch { Write-Verbose "Failed to determine Domain Naming Master: $_" $result.Errors['FSMO_DomainNamingMaster'] = $_.Exception.Message } # PDC Emulator, RID Master, Infrastructure Master — fSMORoleOwner on the domain head try { $domainHeadRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $domainHeadFsmo = @(Invoke-LdapQuery -SearchRoot $domainHeadRoot ` -Filter '(objectClass=domainDNS)' ` -Properties @('fSMORoleOwner') ` -Scope Base) if ($domainHeadFsmo.Count -gt 0 -and $domainHeadFsmo[0].ContainsKey('fsmoroleowner')) { $result.FSMORoles.PDCEmulator = & $extractServerFromNtds $domainHeadFsmo[0]['fsmoroleowner'] } } catch { Write-Verbose "Failed to determine PDC Emulator: $_" $result.Errors['FSMO_PDCEmulator'] = $_.Exception.Message } # RID Master — fSMORoleOwner on CN=RID Manager$,CN=System,<DomainDN> try { $ridManagerDN = "CN=RID Manager`$,CN=System,$domainDN" $ridRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $ridManagerDN $ridFsmo = @(Invoke-LdapQuery -SearchRoot $ridRoot ` -Filter '(objectClass=rIDManager)' ` -Properties @('fSMORoleOwner') ` -Scope Base) if ($ridFsmo.Count -gt 0 -and $ridFsmo[0].ContainsKey('fsmoroleowner')) { $result.FSMORoles.RIDMaster = & $extractServerFromNtds $ridFsmo[0]['fsmoroleowner'] } } catch { Write-Verbose "Failed to determine RID Master: $_" $result.Errors['FSMO_RIDMaster'] = $_.Exception.Message } # Infrastructure Master — fSMORoleOwner on CN=Infrastructure,<DomainDN> try { $infraDN = "CN=Infrastructure,$domainDN" $infraRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $infraDN $infraFsmo = @(Invoke-LdapQuery -SearchRoot $infraRoot ` -Filter '(objectClass=infrastructureUpdate)' ` -Properties @('fSMORoleOwner') ` -Scope Base) if ($infraFsmo.Count -gt 0 -and $infraFsmo[0].ContainsKey('fsmoroleowner')) { $result.FSMORoles.InfrastructureMaster = & $extractServerFromNtds $infraFsmo[0]['fsmoroleowner'] } } catch { Write-Verbose "Failed to determine Infrastructure Master: $_" $result.Errors['FSMO_InfrastructureMaster'] = $_.Exception.Message } # ── 9. DNS zones ────────────────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase AUDITING -Message 'Enumerating DNS zones' } $dnsZones = [System.Collections.Generic.List[hashtable]]::new() # Try Application Directory Partition first (DC=DomainDnsZones), then fallback to CN=System $dnsContainers = @( "CN=MicrosoftDNS,DC=DomainDnsZones,$domainDN" "CN=MicrosoftDNS,DC=ForestDnsZones,$forestDN" "CN=MicrosoftDNS,CN=System,$domainDN" ) foreach ($dnsContainer in $dnsContainers) { try { $dnsRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $dnsContainer $zoneResults = Invoke-LdapQuery -SearchRoot $dnsRoot ` -Filter '(objectClass=dnsZone)' ` -Properties @( 'dc', 'name', 'dnsProperty', 'whenCreated', 'whenChanged', 'distinguishedName' ) ` -Scope OneLevel foreach ($zone in $zoneResults) { $zoneName = if ($zone.ContainsKey('dc')) { $zone['dc'] } elseif ($zone.ContainsKey('name')) { $zone['name'] } else { '' } # Skip the cache zone and RootDNSServers if ($zoneName -eq '..Cache' -or $zoneName -eq 'RootDNSServers') { continue } # Determine zone type from the container $zoneType = if ($dnsContainer -match 'DC=DomainDnsZones') { 'AD-Domain' } elseif ($dnsContainer -match 'DC=ForestDnsZones') { 'AD-Forest' } else { 'AD-Legacy' } $zoneObj = @{ Name = $zoneName ZoneType = $zoneType Container = $dnsContainer WhenCreated = if ($zone.ContainsKey('whencreated')) { $zone['whencreated'] } else { $null } WhenChanged = if ($zone.ContainsKey('whenchanged')) { $zone['whenchanged'] } else { $null } } # Check for duplicate zone names already collected $existing = $dnsZones | Where-Object { $_.Name -eq $zoneName -and $_.ZoneType -eq $zoneType } if (-not $existing) { $dnsZones.Add($zoneObj) } } } catch { Write-Verbose "DNS zone container not accessible: $dnsContainer - $_" # Not an error — container may not exist in this environment } } $result.DnsZones = @($dnsZones) # ── Summary ─────────────────────────────────────────────────────── if (-not $Quiet) { $summary = "Domain info collected: $($result.DomainName) " + "(FL $($result.DomainFunctionalLevelName)), " + "$($result.Sites.Count) site(s), " + "$($result.DnsZones.Count) DNS zone(s)" if ($result.Errors.Count -gt 0) { $summary += " ($($result.Errors.Count) error(s))" } Write-ProgressLine -Phase AUDITING -Message $summary } return $result } |