PowerShell DNS Record Audit: Export All Zone Records to CSV
─□✕

PowerShell DNS Record Audit: Export All Zone Records to CSV

PowerShell Tips Editor 5 min read
PowerShell DNS Record Audit: Export All Zone Records to CSV

Why DNS Hygiene Matters

DNS zones accumulate records for years without cleanup. Decommissioned servers leave orphaned A records that cause name resolution failures when new machines reuse the same IP. CNAME chains grow circular. MX records point at servers that were retired in a previous migration. In large organisations, a DNS zone can hold thousands of records and nobody has a complete picture of what is current and what is stale. Exporting all records to a structured CSV on a regular schedule enables change tracking, stale record identification, and the kind of before/after comparison that makes DNS migrations manageable rather than terrifying.

Quick Answer

Use Get-DnsServerZone to enumerate all zones, Get-DnsServerResourceRecord per zone to retrieve all records, normalize the type-specific data into a flat table, and export with Export-Csv. Compare today’s export against a previous baseline using Compare-Object to surface changes.

Getting All Zones with Get-DnsServerZone

Get-DnsServerZone lists every zone hosted on the DNS server. Filter to primary and secondary zones to exclude the built-in root hints and cache zones that are not useful for auditing. The -ComputerName parameter lets you query remote DNS servers without an interactive session, provided the DnsServer module and appropriate firewall rules are in place.

Import-Module DnsServer -ErrorAction Stop

$dnsServer = "DC01"

$zones = Get-DnsServerZone -ComputerName $dnsServer |
    Where-Object {
        $_.ZoneType -in @("Primary","Secondary") -and
        -not $_.IsAutoCreated -and
        $_.ZoneName -ne "TrustAnchors"
    }

Write-Host "Auditable zones found: $($zones.Count)"
$zones | Select-Object ZoneName, ZoneType, IsDsIntegrated, IsReverseLookupZone |
    Format-Table -AutoSize

Exporting Records per Zone

Get-DnsServerResourceRecord retrieves all resource records for a named zone. For large zones, always specify -ZoneName rather than querying without a filter — some zones contain tens of thousands of records and an unfiltered query can be slow and memory-intensive. Loop over the zone list and collect all records into a single array for unified processing.

$allRecords = foreach ($zone in $zones) {
    Write-Verbose "Querying zone: $($zone.ZoneName)"
    try {
        Get-DnsServerResourceRecord -ZoneName $zone.ZoneName `
            -ComputerName $dnsServer -ErrorAction Stop |
            Select-Object @{N="Zone"; E={ $zone.ZoneName }}, *
    }
    catch {
        Write-Warning "Failed to query zone $($zone.ZoneName): $_"
    }
}

Write-Host "Total records retrieved: $($allRecords.Count)"

Handling Different Record Types

DNS resource records have type-specific data structures. An A record’s IP address lives in RecordData.IPv4Address, while a CNAME’s target is in RecordData.HostNameAlias and an MX’s exchange server is in RecordData.MailExchange. To create a flat exportable table, you need a type switch that extracts the relevant data field for each record type. A single RecordValue column that normalizes all types to a string makes the CSV universally readable.

$normalizedRecords = $allRecords | ForEach-Object {
    $value = switch ($_.RecordType) {
        "A"     { $_.RecordData.IPv4Address.ToString() }
        "AAAA"  { $_.RecordData.IPv6Address.ToString() }
        "CNAME" { $_.RecordData.HostNameAlias }
        "MX"    { "$($_.RecordData.Preference) $($_.RecordData.MailExchange)" }
        "TXT"   { ($_.RecordData.DescriptiveText -join " ") }
        "PTR"   { $_.RecordData.PtrDomainName }
        "NS"    { $_.RecordData.NameServer }
        "SOA"   { "$($_.RecordData.PrimaryServer) / $($_.RecordData.ResponsiblePerson)" }
        default { $_.RecordData.ToString() }
    }

    [PSCustomObject]@{
        Zone        = $_.Zone
        Name        = $_.HostName
        RecordType  = $_.RecordType
        TTL         = $_.TimeToLive.TotalSeconds
        RecordValue = $value
        Timestamp   = $_.Timestamp
    }
}

Write-Host "Record types present: $(($normalizedRecords | Select-Object -Unique RecordType).RecordType -join ', ')"

Running Across Multiple DNS Servers

Enterprise environments typically have multiple DNS servers — primary and secondary, sometimes across sites. Querying all of them reveals discrepancies between zone copies, where a secondary might be out of sync with the primary. Loop over your DNS server list, tag each record with its source server, and combine the results into a unified dataset for comparison.

$dnsServers = @("DC01","DC02","DNS-SECONDARY")
$combinedRecords = [System.Collections.Generic.List[object]]::new()

foreach ($server in $dnsServers) {
    Write-Host "Querying DNS server: $server"
    $serverZones = Get-DnsServerZone -ComputerName $server -ErrorAction SilentlyContinue |
        Where-Object { $_.ZoneType -eq "Primary" -and -not $_.IsAutoCreated }

    foreach ($zone in $serverZones) {
        $recs = Get-DnsServerResourceRecord -ZoneName $zone.ZoneName `
            -ComputerName $server -ErrorAction SilentlyContinue
        foreach ($r in $recs) {
            $combinedRecords.Add([PSCustomObject]@{
                Server    = $server
                Zone      = $zone.ZoneName
                HostName  = $r.HostName
                RecordType = $r.RecordType
            })
        }
    }
}

Write-Host "Total records across all servers: $($combinedRecords.Count)"

Comparing Current Export to Previous Baseline for Changes

A single export is a snapshot. The real value comes from comparing today’s export to the previous one. Compare-Object surfaces records that were added (=>) or removed (<=) between runs. Schedule the export nightly and keep 30 days of baselines. Changes outside of approved maintenance windows are immediate candidates for review.

$today = "C:\DNSAudit\DNS_Export_$(Get-Date -Format yyyyMMdd).csv"
$yesterday = "C:\DNSAudit\DNS_Export_$((Get-Date).AddDays(-1) | Get-Date -Format yyyyMMdd).csv"

$normalizedRecords | Export-Csv $today -NoTypeInformation
Write-Host "Today's export saved: $today"

if (Test-Path $yesterday) {
    $baseline = Import-Csv $yesterday
    $current  = Import-Csv $today

    $diff = Compare-Object $baseline $current `
        -Property Zone, Name, RecordType, RecordValue

    if ($diff) {
        Write-Host "DNS changes detected since yesterday:"
        $diff | Format-Table -AutoSize
    }
    else {
        Write-Host "No DNS record changes since yesterday."
    }
}
else {
    Write-Host "No baseline found for comparison — today's export will serve as tomorrow's baseline."
}

Common Errors

  • DnsServer module not installed on non-DC machines. The DnsServer module is part of RSAT DNS Tools, not installed by default on management workstations. Install it with Add-WindowsCapability -Name Rsat.Dns.Tools~~~~0.0.1.0 -Online on Windows 10/11, or via Install-WindowsFeature RSAT-DNS-Server on Server editions. Without it, Import-Module DnsServer fails silently or throws an error depending on the PowerShell version.
  • Get-DnsServerResourceRecord can be slow on large zones. Querying a zone with 50,000+ records without filtering takes significant time and memory. Use -ZoneName to scope queries to specific zones rather than querying the server root. If you need only specific record types, the -RRType parameter filters at the server side before data is returned, dramatically reducing transfer size.

Related Cmdlets / See Also

Wrapping Up

Regular DNS zone exports provide the change visibility that manual DNS management never can. Export all zones nightly, normalize all record types to a flat table, and run a Compare-Object diff against the previous baseline to surface unauthorised or unexpected changes. The CSV archive doubles as a disaster-recovery reference if you ever need to rebuild a zone from scratch.

Send-Item -To