Scroll Top
Evotec Services sp. z o.o., ul. Drozdów 6, Mikołów, 43-190, Poland

Finding duplicate DNS records by IP Address using PowerShell

Duplicate DNS entries

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
    }
}

Related Posts