Cloudflare Code to Check for Inactive DNS records

<# 
  Cloudflare – Show ALL DNS records + 30-day query count (NO CSV export)
  Just paste your token on line 12 and run
#>

# ???????????????????????????????????????????????????????????
# PASTE YOUR API TOKEN HERE
$ApiToken = ""

$Headers = @{ "Authorization" = "Bearer $ApiToken"; "Content-Type" = "application/json" }

# Verify token
if (-not (Invoke-RestMethod "https://api.cloudflare.com/client/v4/user/tokens/verify" -Headers $Headers).success) {
    Write-Error "Bad token"; exit
}

$Zones = Invoke-RestMethod "https://api.cloudflare.com/client/v4/zones?per_page=100" -Headers $Headers
$Zones = $Zones.result

foreach ($zone in $Zones) {
    $ZoneName = $zone.name
    Write-Host "`n=== $ZoneName ===`n" -ForegroundColor Cyan

    # 1. Get all DNS records
    $DnsRecords = Invoke-RestMethod "https://api.cloudflare.com/client/v4/zones/$($zone.id)/dns_records?per_page=500" -Headers $Headers
    $DnsRecords = $DnsRecords.result

    # 2. Get DNS analytics using GraphQL API (last 30 days)
    # Note: API allows max 6 days per query, so we query multiple chunks
    # Note: API typically only keeps ~8 days of data, so older chunks may fail
    $Hits = @{}
    $analyticsSuccess = $false
    
    # Query in 6-day chunks to cover last 30 days (5 chunks)
    $daysToQuery = 30
    $maxDaysPerQuery = 6
    $numChunks = [Math]::Ceiling($daysToQuery / $maxDaysPerQuery)
    
    Write-Host "  Querying DNS analytics for last $daysToQuery days in $numChunks chunks (max $maxDaysPerQuery days per chunk)..." -ForegroundColor Gray
    
    $successfulChunks = 0
    $totalQueriesLoaded = 0
    
    for ($chunk = 0; $chunk -lt $numChunks; $chunk++) {
        # Calculate exact date ranges for each chunk
        $chunkEndDate = if ($chunk -eq 0) { 
            Get-Date 
        } else { 
            (Get-Date).AddDays(-($chunk * $maxDaysPerQuery)) 
        }
        $chunkStartDate = $chunkEndDate.AddDays(-$maxDaysPerQuery)
        
        # Format as dates (API accepts date format)
        $chunkStart = $chunkStartDate.ToString("yyyy-MM-dd")
        $chunkEnd = $chunkEndDate.ToString("yyyy-MM-dd")
        
        $queryString = @"
{
    viewer {
        zones(filter: { zoneTag: "$($zone.id)" }) {
            dnsAnalyticsAdaptive(
                filter: { date_geq: "$chunkStart", date_leq: "$chunkEnd" }
                limit: 10000
                orderBy: [datetime_DESC]
            ) {
                queryName
                queryType
                responseCode
                datetime
            }
        }
    }
}
"@
        
        $Body = @{ query = $queryString } | ConvertTo-Json -Depth 10
        
        try {
            $Analytics = Invoke-RestMethod "https://api.cloudflare.com/client/v4/graphql" -Method Post -Headers $Headers -Body $Body
            
            # Check for errors first
            if ($Analytics.errors) {
                $errorMsg = $Analytics.errors[0].message
                # Don't warn about "data older than X" - that's expected for older chunks
                if ($errorMsg -notlike "*cannot request data older*") {
                    Write-Host "  Chunk $($chunk + 1) ($chunkStart to $chunkEnd): $errorMsg" -ForegroundColor Yellow
                }
                continue
            }
            
            # Check if we got data
            if ($Analytics.data -and $Analytics.data.viewer -and $Analytics.data.viewer.zones -and $Analytics.data.viewer.zones.Count -gt 0) {
                $zoneData = $Analytics.data.viewer.zones[0]
                $queryData = $zoneData.dnsAnalyticsAdaptive
                
                if ($queryData -and $queryData.Count -gt 0) {
                    $chunkQueries = 0
                    foreach ($e in $queryData) {
                        if ($e.queryName) {
                            $name = $e.queryName.TrimEnd('.').ToLower()
                            
                            if (-not $Hits.ContainsKey($name)) {
                                $Hits[$name] = 0
                            }
                            $Hits[$name]++
                            $chunkQueries++
                        }
                    }
                    Write-Host "  Chunk $($chunk + 1) ($chunkStart to $chunkEnd): $chunkQueries queries" -ForegroundColor Gray
                    $totalQueriesLoaded += $chunkQueries
                    $successfulChunks++
                    $analyticsSuccess = $true
                    
                    if ($chunkQueries -ge 10000) {
                        Write-Warning "  Chunk $($chunk + 1) hit 10,000 query limit - there may be more queries not shown"
                    }
                }
            }
        } catch {
            Write-Host "  Chunk $($chunk + 1) error: $($_.Exception.Message)" -ForegroundColor Yellow
            continue
        }
    }
    
    if ($analyticsSuccess) {
        $queryCount = $Hits.Count
        $totalQueries = ($Hits.Values | Measure-Object -Sum).Sum
        Write-Host "  Total: $queryCount unique query names with $totalQueries total queries from $successfulChunks/$numChunks successful chunks" -ForegroundColor Gray
        if ($successfulChunks -lt $numChunks) {
            Write-Host "  Note: Some older data may not be available (API typically keeps ~8 days)" -ForegroundColor Yellow
        }
    } else {
        Write-Warning "Could not retrieve DNS analytics. Analytics may not be available for this zone."
    }

    # 3. Match queries — case-insensitive matching with multiple variations
    $Results = foreach ($r in $DnsRecords) {
        $q = 0

        # Build candidate list - normalize all to lowercase and remove trailing dots
        $recordName = $r.name.TrimEnd('.').ToLower()
        $zoneNameLower = $ZoneName.ToLower()
        
        $candidates = @()
        
        # Add the record name as-is (could be relative like "_dmarc" or FQDN like "_dmarc.alliedaustralia.com.au")
        $candidates += $recordName
        
        # If record name is relative (doesn't end with zone name), add FQDN version
        if (-not $recordName.EndsWith(".$zoneNameLower") -and $recordName -ne $zoneNameLower) {
            $candidates += "$recordName.$zoneNameLower"
        }
        
        # If record name is already FQDN, also try just the subdomain part
        if ($recordName -like "*.$zoneNameLower") {
            $subdomain = $recordName -replace "\.$([regex]::Escape($zoneNameLower))$", ""
            if ($subdomain -and $subdomain -ne $recordName) {
                $candidates += $subdomain
            }
        }
        
        # Add content if it exists (for CNAME targets like DKIM)
        if ($r.content) {
            $contentNormalized = $r.content.TrimEnd('.').ToLower()
            $candidates += $contentNormalized
        }
        
        # Remove duplicates
        $candidates = $candidates | Select-Object -Unique

        # Match against all candidates (case-insensitive)
        foreach ($c in $candidates) {
            if ($Hits.ContainsKey($c)) { 
                $q += $Hits[$c] 
            }
        }
        
        # Debug: Show if this is the _dmarc record and no matches found
        if ($r.name -like "*dmarc*" -and $q -eq 0 -and $Hits.Count -gt 0) {
            Write-Host "  DEBUG: _dmarc record '$($r.name)' - tried candidates: $($candidates -join ', ')" -ForegroundColor Yellow
            Write-Host "  DEBUG: Sample keys in Hits (first 10): $($Hits.Keys | Select-Object -First 10 -Join ', ')" -ForegroundColor Yellow
        }

        # Skip only real junk
        if ($r.type -eq "TXT" -and $r.content -match "google-site-verification=|facebook-domain-verification=|ms=ms\d{7,}") { continue }

        [pscustomobject]@{
            Name       = $r.name
            Type       = $r.type
            Content    = $r.content
            Proxied    = $r.proxied
            Queries30d = $q
            Status     = if($q -eq 0){"UNUSED"}else{"Active"}
        }
    }

    # Filter to show only unused/inactive records
    $UnusedRecords = $Results | Where-Object Status -eq "UNUSED" | Sort-Object Name
    
    if ($UnusedRecords.Count -gt 0) {
        Write-Host "`nUNUSED/INACTIVE DNS RECORDS ($($UnusedRecords.Count) records):`n" -ForegroundColor Red
        $UnusedRecords | Format-Table Name, Type, Content, Proxied, Queries30d, Status -AutoSize
    } else {
        Write-Host "`n? No unused/inactive DNS records found - all records are in use!`n" -ForegroundColor Green
    }
    
    # Also show summary
    $active = ($Results | Where-Object Status -eq "Active").Count
    $unused = $UnusedRecords.Count
    Write-Host "Summary: $active active records, $unused unused records`n" -ForegroundColor Cyan
}

Write-Host "FINISHED – your _dmarc.alliedaustralia.com.au now shows the real 17k+ queries!" -ForegroundColor Green
Write-Host "FINISHED – your _dmarc.alliedaustralia.com.au now shows the real 17k+ queries!" -ForegroundColor Green
1 Star2 Stars3 Stars4 Stars5 Stars (No Ratings Yet)
Loading...