In my earlier blog post, I showed you a way to find duplicate DNS entries using PowerShell, but the focus was on finding duplicate entries based on hostname. But what if you would like to find duplicate entries based on IP Addresses? This was the question I was asked on Reddit, and I thought it was a legitimate request, so today's focus will be on transposing table output from earlier functions to present data differently.
Duplicate DNS Entries by IP Address
Just like other function from the earlier blog Get-WinDNSRecords I've added this new command to the ADEssentials module as well. Its usage is simple. As long as you have RSAT tools for AD and DNS, it will autodetect the required settings and display results.
Get-WinDNSIPAddresses | ft
As with the earlier command, this one also has additional parameters. You can use Prettify to make the table display it correctly for CSV/HTML output. You can also use IncludeDetails to provide WhenCreated/WhenChanged properties. As before, the Count column contains information for easy sorting and finding duplicate entries.
Get-WinDNSIPAddresses -Prettify -IncludeDetails | ft
By default, this command gets all the zones, but one can modify its output using IncludeZone, and ExcludeZone depending on how large your AD is and what you are interested in. Finally, there is also the IncludeDNSRecords switch which provides the ability to work with complete DNS records instead of their text form.
NAME Get-WinDNSIPAddresses SYNOPSIS Gets all the DNS records from all the zones within a forest sorted by IPAddress SYNTAX Get-WinDNSIPAddresses [[-IncludeZone] <String[]>] [[-ExcludeZone] <String[]>] [-IncludeDetails] [-Prettify] [-IncludeDNSRecords] [-AsHashtable] [<CommonParameters>] DESCRIPTION Gets all the DNS records from all the zones within a forest sorted by IPAddress PARAMETERS -IncludeZone <String[]> Limit the output of DNS records to specific zones -ExcludeZone <String[]> Limit the output of dNS records to only zones not in the exclude list -IncludeDetails [<SwitchParameter>] Adds additional information such as creation time, changed time -Prettify [<SwitchParameter>] Converts arrays into strings connected with comma -IncludeDNSRecords [<SwitchParameter>] Include full DNS records just in case one would like to further process them -AsHashtable [<SwitchParameter>] Outputs the results as a hashtable instead of an array <CommonParameters> This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer, PipelineVariable, and OutVariable. For more information, see about_CommonParameters (https:/go.microsoft.com/fwlink/?LinkID=113216). -------------------------- EXAMPLE 1 -------------------------- PS C:\>Get-WinDNSIPAddresses | Format-Table * -------------------------- EXAMPLE 2 -------------------------- PS C:\>Get-WinDNSIPAddresses -Prettify | Format-Table * -------------------------- EXAMPLE 3 -------------------------- PS C:\>Get-WinDNSIPAddresses -Prettify -IncludeDetails -IncludeDNSRecords | Format-Table * REMARKS To see the examples, type: "get-help Get-WinDNSIPAddresses -examples". For more information, type: "get-help Get-WinDNSIPAddresses -detailed". For technical information, type: "get-help Get-WinDNSIPAddresses -full".
Get-WinDNSIPAddresses -Prettify -IncludeDetails | Out-HtmlView -Filtering
If you're using PSWriteHTML, you can quickly pipe Get-WinDNSIPAddresses to Out-HtmlView and have filters and all other goodies along with Excel, CSV, and PDF export ready. If you're into something cooler, you can get nice little HTML that will make it easy for your eyes to pick what you need.
# Install module should be only done once, unless you want to update to newest version Install-Module PSWriteHTML -Force -Verbose -Scope CurrentUser # import module should be done every time you want to use it, although PowerShell autoloads most PowerShell modules Import-Module PSWriteHTML -Force # Gather data $DNSByName = Get-WinDNSRecords -Prettify -IncludeDetails $DNSByIP = Get-WinDNSIPAddresses -Prettify -IncludeDetails # Create HTML 🙂 New-HTML { New-HTMLTab -Name "DNS by Name" { New-HTMLTable -DataTable $DNSByName -Filtering { New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -BackgroundColor LightGreen New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt -BackgroundColor Orange New-HTMLTableConditionGroup -Logic AND { New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'static' New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'dynamic' } -BackgroundColor Rouge -Row -Color White } -DataStore JavaScript } New-HTMLTab -Name 'DNS by IP' { New-HTMLTable -DataTable $DNSByIP -Filtering { New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -BackgroundColor LightGreen New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt -BackgroundColor Orange New-HTMLTableConditionGroup -Logic AND { New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'static' New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'dynamic' } -BackgroundColor Rouge -Row -Color White } -DataStore JavaScript } } -ShowHTML -Online -TitleText "DNS Records" -FilePath $PSScriptRoot\DNSRecords.html
When you run the script above, you get Tabbed HTML with a lot of DNS data.
Installing ADEssentials
Those cmdlets are part of the ADEssentials module that I've been enhancing for some time now. All you need to do, to install it is:
Install-Module ADEssentials -Force -Verbose
Many commands in the ADEssentials module require RSAT (ActiveDirectory/GroupPolicy) to be present to work. Some cmdlets are ADSI based, so they don't need RSAT to work, but others from ADEssentials do.
When you install the module, it will also install PSWriteHTML and PSEventViewer. The first one is required for displaying output in HTML (tables/diagrams). The other one is a wrapper around Get-WinEvent and is used by some of the commands available within ADEssentials. Install-Module will do all that installation for you without doing anything except for the RSAT requirement, but if you're AD Admin, you should already have those up and ready to use. If you don't have admin rights on your workstation, it's still possible to use this module.
Install-Module ADEssentials -Scope CurrentUser
As those cmdlets described above are read-only, they don't require any rights in AD. I've been digesting my production environments using my standard ID. For sources, reporting issues, or feature requests, as always, visit GitHub. All my projects are hosted on it, and it's preferred method of providing support.
If you don't want to install the full module with all other 30+ useful commands here's the code for this function:
function Get-WinDNSIPAddresses { <# .SYNOPSIS Gets all the DNS records from all the zones within a forest sorted by IPAddress .DESCRIPTION Gets all the DNS records from all the zones within a forest sorted by IPAddress .PARAMETER IncludeZone Limit the output of DNS records to specific zones .PARAMETER ExcludeZone Limit the output of dNS records to only zones not in the exclude list .PARAMETER IncludeDetails Adds additional information such as creation time, changed time .PARAMETER Prettify Converts arrays into strings connected with comma .PARAMETER IncludeDNSRecords Include full DNS records just in case one would like to further process them .PARAMETER AsHashtable Outputs the results as a hashtable instead of an array .EXAMPLE Get-WinDNSIPAddresses | Format-Table * .EXAMPLE Get-WinDNSIPAddresses -Prettify | Format-Table * .EXAMPLE Get-WinDNSIPAddresses -Prettify -IncludeDetails -IncludeDNSRecords | Format-Table * .NOTES General notes #> [cmdletbinding()] param( [string[]] $IncludeZone, [string[]] $ExcludeZone, [switch] $IncludeDetails, [switch] $Prettify, [switch] $IncludeDNSRecords, [switch] $AsHashtable ) $DNSRecordsCached = [ordered] @{} $DNSRecordsPerZone = [ordered] @{} $ADRecordsPerZone = [ordered] @{} try { $oRootDSE = Get-ADRootDSE -ErrorAction Stop } catch { Write-Warning -Message "Get-WinDNSIPAddresses - Could not get the root DSE. Make sure you're logged in to machine with Active Directory RSAT tools installed, and there's connecitivity to the domain. Error: $($_.Exception.Message)" return } $ADServer = ($oRootDSE.dnsHostName) $Exclusions = 'DomainDnsZones', 'ForestDnsZones', '@' $DNS = Get-DnsServerZone -ComputerName $ADServer [Array] $ZonesToProcess = foreach ($Zone in $DNS) { if ($Zone.ZoneType -eq 'Primary' -and $Zone.IsDsIntegrated -eq $true -and $Zone.IsReverseLookupZone -eq $false) { if ($Zone.ZoneName -notlike "*_*" -and $Zone.ZoneName -ne 'TrustAnchors') { if ($IncludeZone -and $IncludeZone -notcontains $Zone.ZoneName) { continue } if ($ExcludeZone -and $ExcludeZone -contains $Zone.ZoneName) { continue } $Zone } } } foreach ($Zone in $ZonesToProcess) { Write-Verbose -Message "Get-WinDNSIPAddresses - Processing zone for DNS records: $($Zone.ZoneName)" $DNSRecordsPerZone[$Zone.ZoneName] = Get-DnsServerResourceRecord -ComputerName $ADServer -ZoneName $Zone.ZoneName -RRType A } if ($IncludeDetails) { $Filter = { (Name -notlike "@" -and Name -notlike "_*" -and ObjectClass -eq 'dnsNode' -and Name -ne 'ForestDnsZone' -and Name -ne 'DomainDnsZone' ) } foreach ($Zone in $ZonesToProcess) { $ADRecordsPerZone[$Zone.ZoneName] = [ordered]@{} Write-Verbose -Message "Get-WinDNSIPAddresses - Processing zone for AD records: $($Zone.ZoneName)" $TempObjects = @( if ($Zone.ReplicationScope -eq 'Domain') { try { Get-ADObject -Server $ADServer -Filter $Filter -SearchBase ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones," + $oRootDSE.defaultNamingContext) -Properties CanonicalName, whenChanged, whenCreated, DistinguishedName, ProtectedFromAccidentalDeletion, dNSTombstoned } catch { Write-Warning -Message "Get-WinDNSIPAddresses - Error getting AD records for DomainDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)" } } elseif ($Zone.ReplicationScope -eq 'Forest') { try { Get-ADObject -Server $ADServer -Filter $Filter -SearchBase ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=ForestDnsZones," + $oRootDSE.defaultNamingContext) -Properties CanonicalName, whenChanged, whenCreated, DistinguishedName, ProtectedFromAccidentalDeletion, dNSTombstoned } catch { Write-Warning -Message "Get-WinDNSIPAddresses - Error getting AD records for ForestDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)" } } else { Write-Warning -Message "Get-WinDNSIPAddresses - Unknown replication scope: $($Zone.ReplicationScope)" } ) foreach ($DNSObject in $TempObjects) { $ADRecordsPerZone[$Zone.ZoneName][$DNSObject.Name] = $DNSObject } } } foreach ($Zone in $DNSRecordsPerZone.PSBase.Keys) { foreach ($Record in $DNSRecordsPerZone[$Zone]) { if ($Record.HostName -in $Exclusions) { continue } if (-not $DNSRecordsCached[$Record.RecordData.IPv4Address]) { $DNSRecordsCached[$Record.RecordData.IPv4Address] = [ordered] @{ IPAddress = $Record.RecordData.IPv4Address DnsNames = [System.Collections.Generic.List[Object]]::new() Timestamps = [System.Collections.Generic.List[Object]]::new() Types = [System.Collections.Generic.List[Object]]::new() Count = 0 } if ($ADRecordsPerZone.Keys.Count -gt 0) { $DNSRecordsCached[$Record.RecordData.IPv4Address].WhenCreated = $ADRecordsPerZone[$Zone][$Record.HostName].whenCreated $DNSRecordsCached[$Record.RecordData.IPv4Address].WhenChanged = $ADRecordsPerZone[$Zone][$Record.HostName].whenChanged } if ($IncludeDNSRecords) { $DNSRecordsCached[$Record.RecordData.IPv4Address].List = [System.Collections.Generic.List[Object]]::new() } } $DNSRecordsCached[$Record.RecordData.IPv4Address].DnsNames.Add($Record.HostName + "." + $Zone) if ($IncludeDNSRecords) { $DNSRecordsCached[$Record.RecordData.IPv4Address].List.Add($Record) } if ($null -ne $Record.TimeStamp) { $DNSRecordsCached[$Record.RecordData.IPv4Address].Timestamps.Add($Record.TimeStamp) } else { $DNSRecordsCached[$Record.RecordData.IPv4Address].Timestamps.Add("Not available") } if ($Null -ne $Record.Timestamp) { $DNSRecordsCached[$Record.RecordData.IPv4Address].Types.Add('Dynamic') } else { $DNSRecordsCached[$Record.RecordData.IPv4Address].Types.Add('Static') } $DNSRecordsCached[$Record.RecordData.IPv4Address] = [PSCustomObject] $DNSRecordsCached[$Record.RecordData.IPv4Address] } } foreach ($DNS in $DNSRecordsCached.PSBase.Keys) { $DNSRecordsCached[$DNS].Count = $DNSRecordsCached[$DNS].DnsNames.Count if ($Prettify) { $DNSRecordsCached[$DNS].DnsNames = $DNSRecordsCached[$DNS].DnsNames -join ", " $DNSRecordsCached[$DNS].Timestamps = $DNSRecordsCached[$DNS].Timestamps -join ", " $DNSRecordsCached[$DNS].Types = $DNSRecordsCached[$DNS].Types -join ", " } } if ($AsHashtable) { $DNSRecordsCached } else { $DNSRecordsCached.Values } }