modules/DNS/DNS.psm1
|
<#
DNS.psm1 - DNS Server role discovery (read-only, summary only). Role-gated. DnsRecordsReferencingThisServer: A/CNAME records in this server's own zones whose target is this server's own hostname/IP - the records that break if this server is renamed, re-IPed, or retired. Forward zones only, capped, read-only (never touches a zone's data). #> function Get-DiscoveryModuleMetadata { [pscustomobject]@{ ModuleName='DNS'; DisplayName='DNS Server'; Category='Identity'; Version='1.0.0' DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false RequiresRole='DNS'; EstimatedImpact='Low'; CanRunAsSystem=$true ProducesDatasets=@('DnsServerSettings','DnsZones','DnsRecordsReferencingThisServer') ProducesRisks=$true; ProducesFollowUpQuestions=$false; SupportsDeepMode=$true; SupportsComplianceLens=$false } } function Test-DnsPresent { if (Get-CommandAvailable -Name 'Get-DnsServerZone') { return $true } if (Get-Service -Name 'DNS' -ErrorAction SilentlyContinue) { return $true } return $false } function Test-DiscoveryPrerequisites { param([object]$Context) if (Test-DnsPresent) { return [pscustomobject]@{ ModuleName='DNS'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() } } [pscustomobject]@{ ModuleName='DNS'; CanRun=$false; Status='NotApplicable'; Reason='DNS Server role not present.'; Limitations=@() } } function Invoke-DiscoveryCollection { param([object]$Context) # Default cmdlets resolve $env:COMPUTERNAME through DNS on every call (~1 s each; 20-30x slower than 'localhost'), and a resolution failure is swallowed by -ErrorAction SilentlyContinue as an empty result. $PSDefaultParameterValues = @{ 'Get-DnsServer*:ComputerName' = 'localhost' } $settings=[System.Collections.Generic.List[object]]::new(); $zones=[System.Collections.Generic.List[object]]::new() if (-not (Get-CommandAvailable -Name 'Get-DnsServerZone')) { Add-Limitation -Context $Context -Module 'DNS' -Message 'DnsServer module not available; DNS present but not enumerable.' | Out-Null; return ,@{ DnsServerSettings=@(); DnsZones=@(); DnsRecordsReferencingThisServer=@() } } try { $fwd = '' try { $fwd = ((Get-DnsServerForwarder -ErrorAction SilentlyContinue).IPAddress -join ', ') } catch { } $settings.Add([pscustomobject]@{ Item='Forwarders'; Value=$fwd }) } catch { } try { foreach ($z in (Get-DnsServerZone -ErrorAction SilentlyContinue)) { $zones.Add([pscustomobject]@{ ZoneName=$z.ZoneName; ZoneType=[string]$z.ZoneType; IsDsIntegrated=$z.IsDsIntegrated; IsReverse=$z.IsReverseLookupZone; DynamicUpdate=[string]$z.DynamicUpdate; IsAutoCreated=$z.IsAutoCreated }) } } catch { Add-Limitation -Context $Context -Module 'DNS' -Message 'DNS zone enumeration failed.' -Reason $_.Exception.Message | Out-Null } $selfRefs = Get-DnsRecordsReferencingThisServer -Context $Context -Zones $zones return ,@{ DnsServerSettings=@($settings); DnsZones=@($zones); DnsRecordsReferencingThisServer=@($selfRefs) } } function Get-DnsRecordsReferencingThisServer { <# A/CNAME records (forward zones only) whose target is this server's own hostname/IP - the records that break if this server is renamed, re-IPed, or retired. Bounded by $recordCap total records examined, so a very large zone cannot run away. #> param([object]$Context, [object[]]$Zones) $rows = [System.Collections.Generic.List[object]]::new() $recordCap = 3000 $examined = 0 $capped = $false $myNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) [void]$myNames.Add($env:COMPUTERNAME) if ($env:USERDNSDOMAIN) { [void]$myNames.Add(("{0}.{1}" -f $env:COMPUTERNAME, $env:USERDNSDOMAIN)) } $myIPs = [System.Collections.Generic.HashSet[string]]::new() try { if ($Context.DataSets.Contains('IPConfiguration')) { foreach ($ipc in @($Context.DataSets['IPConfiguration'].Rows)) { foreach ($ip in (([string]$ipc.AllIPv4Addresses) -split ',')) { $t = $ip.Trim(); if ($t) { [void]$myIPs.Add($t) } } if ($ipc.IPv4Address) { [void]$myIPs.Add([string]$ipc.IPv4Address) } } } } catch { } if ($myIPs.Count -eq 0) { return ,@($rows) } foreach ($z in ($Zones | Where-Object { -not $_.IsReverse })) { if ($capped) { break } try { foreach ($r in (Get-DnsServerResourceRecord -ZoneName $z.ZoneName -ErrorAction Stop)) { if ($examined -ge $recordCap) { $capped = $true; break } $examined++ $matchTarget = '' if ($r.RecordType -eq 'A' -and $r.RecordData.IPv4Address) { $ip = $r.RecordData.IPv4Address.ToString() if ($myIPs.Contains($ip)) { $matchTarget = $ip } } elseif ($r.RecordType -eq 'CNAME' -and $r.RecordData.HostNameAlias) { $alias = ([string]$r.RecordData.HostNameAlias).TrimEnd('.') if ($myNames.Contains($alias)) { $matchTarget = $alias } } if ($matchTarget) { $fqName = if ($r.HostName -eq '@') { $z.ZoneName } else { ("{0}.{1}" -f $r.HostName, $z.ZoneName) } $rows.Add([pscustomobject]@{ ZoneName=$z.ZoneName; RecordType=[string]$r.RecordType; RecordName=$fqName; PointsTo=$matchTarget }) } } } catch { Add-Limitation -Context $Context -Module 'DNS' -Message ("Record enumeration failed for zone '{0}'." -f $z.ZoneName) -Reason $_.Exception.Message | Out-Null } } if ($capped) { Add-Limitation -Context $Context -Module 'DNS' -Message ("DNS record scan stopped early (cap {0} records examined); some zones may not have been fully checked." -f $recordCap) -Impact 'Coverage capped' | Out-Null } return ,@($rows) } function ConvertTo-DiscoveryDatasets { param([object]$Context, $RawData) if (-not $RawData) { $RawData = @{} } $get = { param($k) if ($RawData[$k]) { @($RawData[$k]) } else { @() } } Add-DataSet -Context $Context -Name 'DnsServerSettings' -Description 'DNS server settings (forwarders).' -Rows (& $get 'DnsServerSettings') -Visibility 'Internal' -SourceModule 'DNS' | Out-Null Add-DataSet -Context $Context -Name 'DnsZones' -Description 'DNS zones summary.' -Rows (& $get 'DnsZones') -Visibility 'Internal' -SourceModule 'DNS' | Out-Null Add-DataSet -Context $Context -Name 'DnsRecordsReferencingThisServer' -Description 'A/CNAME records (forward zones) whose target is this server''s own hostname or IP.' -Rows (& $get 'DnsRecordsReferencingThisServer') -Visibility 'Internal' -SourceModule 'DNS' | Out-Null } Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Test-DnsPresent','Get-DnsRecordsReferencingThisServer' |