Active Directory

Finding duplicate DNS entries using PowerShell

Today's blog post is about Active Directory-integrated DNS and how to find duplicate entries. By duplicate, I mean those where one DNS name matches multiple IP addresses. While some duplicate DNS entries are expected, in other cases, it may lead to problems. For example, having a static IP assigned to a hostname that later on is also updated with dynamic entries.

Duplicate DNS Entries

The command I've created and added for easy use to the ADEssentials module is called Get-WinDNSRecords. Its usage is very simple. As long as you have RSAT tools for AD and DNS, it will autodetect the required settings and display results.

Get-WinDNSRecords | Format-Table

The command show multiple columns such as HostName, Zone, RecordType (probably could remove it), ListRecordIP, ListRecordTypes and Count of IP Addresses. Count makes it easy to filter for what you are looking for, and ListRecordTypes provides a quick overview of potential issues. If there is both Static and Dynamic entry for a single hostname, a server has both static entry and DHCP IP Address.

Get-WinDNSRecords provides a way to get information from Active Directory when the record was created or modified by using the IncludeDetails switch. Remember that the DNS records stored in AD for a single name have only a single entry. This means that if you have more than one record, it's impossible to tell which information is from which record. I've also added a parameter called Prettify so that it's easier to export to CSV or HTML.

Get-WinDNSRecords -Prettify -IncludeDetails | Format-Table

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

SYNOPSIS
    Gets all the DNS records from all the zones within a forest


SYNTAX
    Get-WinDNSRecords [[-IncludeZone] <String[]>] [[-ExcludeZone] <String[]>] [-IncludeDetails] [-Prettify] [-IncludeDNSRecords] [-AsHashtable] [<CommonParameters>]


DESCRIPTION
    Gets all the DNS records from all the zones within a forest


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-WinDNSRecords -Prettify -IncludeDetails | Format-Table






    -------------------------- EXAMPLE 2 --------------------------

    PS C:\>$Output = Get-WinDNSRecords -Prettify -IncludeDetails -Verbose

    $Output.Count
    $Output | Sort-Object -Property Count -Descending | Select-Object -First 30 | Format-Table




REMARKS
    To see the examples, type: "get-help Get-WinDNSRecords -examples".
    For more information, type: "get-help Get-WinDNSRecords -detailed".
    For technical information, type: "get-help Get-WinDNSRecords -full".

Of course, if you're using PSWriteHTML, you can quickly pipe Get-WinDNSRecords to Out-HtmlView and have filters and all other goodies along with Excel, CSV, and PDF export ready.

Get-WinDNSRecords -IncludeDetails -Prettify | Out-HtmlView -Filtering

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-WinDNSRecords {
    <#
    .SYNOPSIS
    Gets all the DNS records from all the zones within a forest

    .DESCRIPTION
    Gets all the DNS records from all the zones within a forest

    .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-WinDNSRecords -Prettify -IncludeDetails | Format-Table

    .EXAMPLE
    $Output = Get-WinDNSRecords -Prettify -IncludeDetails -Verbose
    $Output.Count
    $Output | Sort-Object -Property Count -Descending | Select-Object -First 30 | 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-WinDNSRecords - 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-WinDNSRecords - 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-WinDNSRecords - 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-WinDNSRecords - 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-WinDNSRecords - Error getting AD records for ForestDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } else {
                    Write-Warning -Message "Get-WinDNSRecords - 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.HostName).$($Zone)"]) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"] = [ordered] @{
                    'HostName' = $Record.HostName
                    'Zone'     = $Zone
                    #'RecordType' = $Record.RecordType
                    RecordIP   = [System.Collections.Generic.List[Object]]::new()
                    Types      = [System.Collections.Generic.List[Object]]::new()
                    Timestamps = [System.Collections.Generic.List[Object]]::new()
                    Count      = 0
                }
                if ($ADRecordsPerZone.Keys.Count -gt 0) {
                    $DNSRecordsCached["$($Record.HostName).$($Zone)"].WhenCreated = $ADRecordsPerZone[$Zone][$Record.HostName].whenCreated
                    $DNSRecordsCached["$($Record.HostName).$($Zone)"].WhenChanged = $ADRecordsPerZone[$Zone][$Record.HostName].whenChanged
                }
                if ($IncludeDNSRecords) {
                    $DNSRecordsCached["$($Record.HostName).$($Zone)"].List = [System.Collections.Generic.List[Object]]::new()
                }
            }
            if ($IncludeDNSRecords) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].List.Add($Record)
            }
            if ($null -ne $Record.TimeStamp) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Timestamps.Add($Record.TimeStamp)
            } else {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Timestamps.Add("Not available")
            }
            $DNSRecordsCached["$($Record.HostName).$($Zone)"].RecordIP.Add($Record.RecordData.IPv4Address)
            if ($Null -ne $Record.Timestamp) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Types.Add('Dynamic')
            } else {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Types.Add('Static')
            }
            $DNSRecordsCached["$($Record.HostName).$($Zone)"] = [PSCustomObject] $DNSRecordsCached["$($Record.HostName).$($Zone)"]

        }
    }
    foreach ($DNS in $DNSRecordsCached.PSBase.Keys) {
        $DNSRecordsCached[$DNS].Count = $DNSRecordsCached[$DNS].RecordIP.Count
        if ($Prettify) {
            $DNSRecordsCached[$DNS].Types = $DNSRecordsCached[$DNS].Types -join ", "
            $DNSRecordsCached[$DNS].RecordIP = $DNSRecordsCached[$DNS].RecordIP -join ", "
            $DNSRecordsCached[$DNS].Timestamps = $DNSRecordsCached[$DNS].Timestamps -join ", "
        }
    }
    if ($AsHashtable) {
        $DNSRecordsCached
    } else {
        $DNSRecordsCached.Values
    }
}

This post was last modified on August 7, 2022 13:50

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

1 day ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

5 months ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

6 months ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

8 months ago

How to Efficiently Remove Comments from Your PowerShell Script

As part of my daily development, I create lots of code that I subsequently comment…

8 months ago

Unlocking PowerShell Magic: Different Approach to Creating ‘Empty’ PSCustomObjects

Today I saw an article from Christian Ritter, "PowerShell: Creating an "empty" PSCustomObject" on X…

8 months ago