a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
428 lines
17 KiB
PowerShell
428 lines
17 KiB
PowerShell
###############################################################################
|
|
# windows-dns-smoke-tests.ps1 - Verify Windows DNS Server health
|
|
#
|
|
# Checks DNS service status, zone loading, AD-integrated zone replication,
|
|
# conditional forwarders, forward and reverse resolution, scavenging
|
|
# configuration, root hints, forwarder connectivity, zone delegation,
|
|
# cache statistics, and event log errors.
|
|
#
|
|
# Author: Phil Connor
|
|
# Contact: contact@mylinux.work
|
|
# License: MIT
|
|
# Version 1.0
|
|
#
|
|
# Usage:
|
|
# .\windows-dns-smoke-tests.ps1
|
|
# .\windows-dns-smoke-tests.ps1 -DnsServer 10.0.0.53 -Domain corp.local
|
|
# .\windows-dns-smoke-tests.ps1 -OutputFormat tap
|
|
# .\windows-dns-smoke-tests.ps1 -Help
|
|
###############################################################################
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$DnsServer = "localhost",
|
|
[string]$Domain = $(if ($env:USERDNSDOMAIN) { $env:USERDNSDOMAIN } else { "" }),
|
|
[string]$TestDomain = "example.com",
|
|
[ValidateSet("text","tap")]
|
|
[string]$OutputFormat = "text",
|
|
[switch]$NoColor,
|
|
[switch]$Help
|
|
)
|
|
|
|
$ErrorActionPreference = "Continue"
|
|
|
|
$script:Pass = 0; $script:Fail = 0; $script:Skip = 0; $script:Total = 0
|
|
$script:Results = @(); $script:StartTime = $null
|
|
|
|
# ============================================================================
|
|
# COLORS
|
|
# ============================================================================
|
|
|
|
function Write-Color {
|
|
param([string]$Text, [string]$Color = "White")
|
|
if ($NoColor) { Write-Host $Text } else { Write-Host $Text -ForegroundColor $Color }
|
|
}
|
|
|
|
# ============================================================================
|
|
# TEST RESULT RECORDING
|
|
# ============================================================================
|
|
|
|
function Record-Pass {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Pass++; $script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name"
|
|
} else {
|
|
$msg = " $(if($NoColor){'[PASS]'}else{[char]0x2713}) $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Green"
|
|
}
|
|
}
|
|
|
|
function Record-Fail {
|
|
param([string]$Name, [string]$Detail = "")
|
|
$script:Fail++; $script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "not ok $($script:Total) - $Name"
|
|
if ($Detail) { Write-Host " # $Detail" }
|
|
} else {
|
|
$msg = " $(if($NoColor){'[FAIL]'}else{[char]0x2717}) $Name"
|
|
if ($Detail) { $msg += " - $Detail" }
|
|
Write-Color $msg "Red"
|
|
}
|
|
}
|
|
|
|
function Record-Skip {
|
|
param([string]$Name, [string]$Reason = "")
|
|
$script:Skip++; $script:Total++
|
|
$script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
|
|
} else {
|
|
$msg = " $(if($NoColor){'[SKIP]'}else{[char]0x2298}) $Name"
|
|
if ($Reason) { $msg += " - $Reason" }
|
|
Write-Color $msg "Yellow"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# HELPERS
|
|
# ============================================================================
|
|
|
|
function Get-DnsComputerParam {
|
|
if ($DnsServer -eq "localhost" -or $DnsServer -eq "127.0.0.1" -or $DnsServer -eq "::1") {
|
|
return @{}
|
|
}
|
|
return @{ ComputerName = $DnsServer }
|
|
}
|
|
|
|
function Write-Section {
|
|
param([string]$Title)
|
|
if ($OutputFormat -ne "tap") { Write-Host ""; Write-Color $Title "White" }
|
|
}
|
|
|
|
# ============================================================================
|
|
# TESTS
|
|
# ============================================================================
|
|
|
|
function Test-DnsService {
|
|
Write-Section "DNS Service"
|
|
try {
|
|
$params = @{ Name = "DNS"; ErrorAction = "Stop" }
|
|
if ($DnsServer -ne "localhost" -and $DnsServer -ne "127.0.0.1" -and $DnsServer -ne "::1") {
|
|
$params["ComputerName"] = $DnsServer
|
|
}
|
|
$svc = Get-Service @params
|
|
if ($svc.Status -eq "Running") { Record-Pass "DNS service running" }
|
|
else { Record-Fail "DNS service running" "status: $($svc.Status)" }
|
|
} catch { Record-Fail "DNS service running" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-DnsResponds {
|
|
try {
|
|
$result = Resolve-DnsName -Name $TestDomain -Server $DnsServer -Type A -DnsOnly -ErrorAction Stop
|
|
if ($result) { Record-Pass "DNS server responds" "$TestDomain resolved" }
|
|
else { Record-Fail "DNS server responds" "no result for $TestDomain" }
|
|
} catch { Record-Fail "DNS server responds" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ZonesLoaded {
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$zones = @(Get-DnsServerZone @cp -ErrorAction Stop |
|
|
Where-Object { $_.ZoneType -ne "Forwarder" -and $_.ZoneName -ne "TrustAnchors" })
|
|
if ($zones.Count -gt 0) { Record-Pass "zones loaded" "$($zones.Count) zones" }
|
|
else { Record-Fail "zones loaded" "no zones found" }
|
|
} catch { Record-Fail "zones loaded" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ADIntegratedZones {
|
|
if (-not $Domain) { Record-Skip "AD-integrated zones" "Domain not set"; return }
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$zones = @(Get-DnsServerZone @cp -ErrorAction Stop |
|
|
Where-Object { $_.IsAutoCreated -eq $false -and $_.IsDsIntegrated -eq $true })
|
|
if ($zones.Count -eq 0) { Record-Skip "AD-integrated zones" "none found"; return }
|
|
foreach ($z in $zones) {
|
|
$scope = if ($z.DirectoryPartitionName) { $z.DirectoryPartitionName } else { "legacy" }
|
|
Record-Pass "AD-integrated zone ($($z.ZoneName))" "replication: $scope"
|
|
}
|
|
} catch { Record-Fail "AD-integrated zones" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ForwardLookup {
|
|
Write-Section "DNS Resolution"
|
|
if (-not $Domain) { Record-Skip "forward lookup" "Domain not set"; return }
|
|
try {
|
|
$result = Resolve-DnsName -Name $Domain -Server $DnsServer -Type A -DnsOnly -ErrorAction Stop
|
|
if ($result) {
|
|
$addrs = ($result | Where-Object { $_.QueryType -eq "A" } |
|
|
Select-Object -ExpandProperty IPAddress) -join ", "
|
|
if ($addrs) { Record-Pass "forward lookup ($Domain)" $addrs }
|
|
else { Record-Pass "forward lookup ($Domain)" "resolved (non-A records)" }
|
|
} else { Record-Fail "forward lookup ($Domain)" "no result" }
|
|
} catch { Record-Fail "forward lookup ($Domain)" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ReverseLookup {
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$rz = @(Get-DnsServerZone @cp -ErrorAction Stop |
|
|
Where-Object { $_.IsReverseLookupZone -eq $true -and $_.ZoneType -ne "Forwarder" })
|
|
if ($rz.Count -eq 0) { Record-Skip "reverse lookup" "no reverse zones found"; return }
|
|
$found = $false
|
|
foreach ($zone in $rz) {
|
|
try {
|
|
$ptrs = Get-DnsServerResourceRecord -ZoneName $zone.ZoneName -RRType PTR @cp -ErrorAction Stop
|
|
if ($ptrs) {
|
|
$first = $ptrs | Select-Object -First 1
|
|
Record-Pass "reverse lookup" "PTR $($first.HostName) -> $($first.RecordData.PtrDomainName)"
|
|
$found = $true; break
|
|
}
|
|
} catch { }
|
|
}
|
|
if (-not $found) { Record-Skip "reverse lookup" "no PTR records in reverse zones" }
|
|
} catch { Record-Fail "reverse lookup" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ConditionalForwarders {
|
|
Write-Section "Forwarders"
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$fzones = @(Get-DnsServerZone @cp -ErrorAction Stop |
|
|
Where-Object { $_.ZoneType -eq "Forwarder" })
|
|
if ($fzones.Count -eq 0) { Record-Skip "conditional forwarders" "none configured"; return }
|
|
foreach ($fz in $fzones) {
|
|
$targets = $fz.MasterServers
|
|
if (-not $targets -or @($targets).Count -eq 0) {
|
|
Record-Fail "conditional forwarder ($($fz.ZoneName))" "no targets"; continue
|
|
}
|
|
$ok = $false
|
|
foreach ($t in $targets) {
|
|
try {
|
|
$r = Resolve-DnsName -Name $fz.ZoneName -Server $t.IPAddressToString -Type SOA -DnsOnly -ErrorAction Stop
|
|
if ($r) { $ok = $true; break }
|
|
} catch { }
|
|
}
|
|
if ($ok) { Record-Pass "conditional forwarder ($($fz.ZoneName))" "target reachable" }
|
|
else { Record-Fail "conditional forwarder ($($fz.ZoneName))" "no targets responded" }
|
|
}
|
|
} catch { Record-Fail "conditional forwarders" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-RootHints {
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$hints = @(Get-DnsServerRootHint @cp -ErrorAction Stop)
|
|
if ($hints.Count -eq 0) { Record-Skip "root hints" "none configured"; return }
|
|
Record-Pass "root hints configured" "$($hints.Count) root servers"
|
|
$tested = $false
|
|
foreach ($h in $hints) {
|
|
foreach ($ip in ($h.IPAddress | Where-Object { $_.RecordType -eq "A" })) {
|
|
try {
|
|
$addr = $ip.RecordData.IPv4Address.IPAddressToString
|
|
$r = Resolve-DnsName -Name "." -Server $addr -Type NS -DnsOnly -ErrorAction Stop
|
|
if ($r) { Record-Pass "root hint reachable" $addr; $tested = $true; break }
|
|
} catch { }
|
|
}
|
|
if ($tested) { break }
|
|
}
|
|
if (-not $tested) { Record-Fail "root hint reachable" "no root servers responded" }
|
|
} catch { Record-Fail "root hints" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-Forwarders {
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$fwd = Get-DnsServerForwarder @cp -ErrorAction Stop
|
|
$ips = $fwd.IPAddress
|
|
if (-not $ips -or @($ips).Count -eq 0) {
|
|
Record-Skip "forwarder configuration" "no forwarders configured"; return
|
|
}
|
|
foreach ($ip in $ips) {
|
|
$addr = $ip.IPAddressToString
|
|
try {
|
|
$r = Resolve-DnsName -Name $TestDomain -Server $addr -Type A -DnsOnly -ErrorAction Stop
|
|
if ($r) { Record-Pass "forwarder responds ($addr)" }
|
|
else { Record-Fail "forwarder responds ($addr)" "no response" }
|
|
} catch { Record-Fail "forwarder responds ($addr)" $_.Exception.Message }
|
|
}
|
|
} catch { Record-Fail "forwarder configuration" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-Scavenging {
|
|
Write-Section "Server Configuration"
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$scav = Get-DnsServerScavenging @cp -ErrorAction Stop
|
|
if ($scav.ScavengingState -eq $true) {
|
|
$detail = "interval: $($scav.ScavengingInterval)"
|
|
$last = $scav.LastScavengeTime
|
|
if ($last -and $last -ne [DateTime]::MinValue) {
|
|
$days = [math]::Floor(((Get-Date) - $last).TotalDays)
|
|
$detail += ", last run: $($last.ToString('yyyy-MM-dd')) (${days}d ago)"
|
|
if ($days -gt 14) { Record-Fail "scavenging" "$detail — over 14 days ago"; return }
|
|
} else { $detail += ", last run: never" }
|
|
Record-Pass "scavenging enabled" $detail
|
|
} else {
|
|
Record-Pass "scavenging disabled" "not enabled on this server"
|
|
}
|
|
} catch { Record-Fail "scavenging" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-ZoneDelegation {
|
|
if (-not $Domain) { Record-Skip "zone delegation" "Domain not set"; return }
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$ns = @(Get-DnsServerResourceRecord -ZoneName $Domain -RRType NS @cp -ErrorAction Stop |
|
|
Where-Object { $_.HostName -ne "@" })
|
|
if ($ns.Count -eq 0) { Record-Skip "zone delegation" "no delegated subdomains"; return }
|
|
foreach ($r in $ns) {
|
|
Record-Pass "zone delegation ($($r.HostName))" "NS -> $($r.RecordData.NameServer)"
|
|
}
|
|
} catch {
|
|
if ($_.Exception.Message -match "not found") {
|
|
Record-Skip "zone delegation" "zone $Domain not hosted on this server"
|
|
} else { Record-Fail "zone delegation" $_.Exception.Message }
|
|
}
|
|
}
|
|
|
|
function Test-CacheStats {
|
|
Write-Section "Statistics"
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$stats = Get-DnsServerStatistics @cp -ErrorAction Stop
|
|
$cs = $stats.CacheStatistics
|
|
if ($cs) {
|
|
$hits = $cs.CacheHits; $misses = $cs.CacheMisses; $total = $hits + $misses
|
|
if ($total -gt 0) {
|
|
$ratio = [math]::Round(($hits / $total) * 100, 1)
|
|
Record-Pass "cache statistics" "hits: $hits, misses: $misses, hit ratio: ${ratio}%"
|
|
} else { Record-Pass "cache statistics" "no queries processed yet" }
|
|
} elseif ($stats.QueryStatistics) {
|
|
Record-Pass "cache statistics" "total queries: $($stats.QueryStatistics.TotalQueries)"
|
|
} else { Record-Skip "cache statistics" "not available" }
|
|
} catch { Record-Fail "cache statistics" $_.Exception.Message }
|
|
}
|
|
|
|
function Test-EventLog {
|
|
$cp = Get-DnsComputerParam
|
|
try {
|
|
$filter = @{ LogName = "DNS Server"; Level = 2; StartTime = (Get-Date).AddHours(-24) }
|
|
if ($cp.ContainsKey("ComputerName")) {
|
|
$events = @(Get-WinEvent -FilterHashtable $filter -ComputerName $cp.ComputerName -ErrorAction Stop)
|
|
} else {
|
|
$events = @(Get-WinEvent -FilterHashtable $filter -ErrorAction Stop)
|
|
}
|
|
if ($events.Count -gt 0) {
|
|
$latest = $events[0]
|
|
$msg = $latest.Message.Substring(0, [Math]::Min(80, $latest.Message.Length))
|
|
Record-Fail "event log" "$($events.Count) error(s) in last 24h — latest: $msg"
|
|
} else { Record-Pass "event log clean" "0 errors in last 24h" }
|
|
} catch {
|
|
if ($_.Exception.Message -match "No events were found") {
|
|
Record-Pass "event log clean" "0 errors in last 24h"
|
|
} elseif ($_.Exception.Message -match "not found|does not exist") {
|
|
Record-Skip "event log" "DNS Server event log not available"
|
|
} else { Record-Fail "event log" $_.Exception.Message }
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# OUTPUT
|
|
# ============================================================================
|
|
|
|
function Write-Summary {
|
|
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
|
|
Write-Host ""
|
|
$sep = [string]::new([char]0x2500, 40)
|
|
Write-Color $sep "White"
|
|
Write-Color "Summary $DnsServer" "White"
|
|
Write-Host " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)"
|
|
Write-Color $sep "White"
|
|
if ($script:Fail -eq 0) { Write-Color "All tests passed." "Green" }
|
|
else { Write-Color "$($script:Fail) test(s) failed." "Red" }
|
|
}
|
|
|
|
function Write-TapHeader { Write-Host "TAP version 13" }
|
|
|
|
function Write-TapFooter {
|
|
Write-Host "1..$($script:Total)"
|
|
Write-Host "# pass $($script:Pass)"
|
|
Write-Host "# fail $($script:Fail)"
|
|
Write-Host "# skip $($script:Skip)"
|
|
}
|
|
|
|
# ============================================================================
|
|
# HELP
|
|
# ============================================================================
|
|
|
|
function Show-Usage {
|
|
@"
|
|
Usage: .\windows-dns-smoke-tests.ps1 [OPTIONS]
|
|
|
|
Smoke-test a Windows DNS Server. PowerShell 5.1+, DnsServer module only.
|
|
|
|
Parameters:
|
|
-DnsServer SERVER DNS server to test (default: localhost)
|
|
-Domain DOMAIN AD domain name (default: auto-detect from environment)
|
|
-TestDomain DOMAIN Domain for resolution tests (default: example.com)
|
|
-OutputFormat FMT Output format: text (default), tap
|
|
-NoColor Disable colored output
|
|
-Help Show this help message
|
|
|
|
Examples:
|
|
.\windows-dns-smoke-tests.ps1
|
|
.\windows-dns-smoke-tests.ps1 -DnsServer 10.0.0.53 -Domain corp.local
|
|
.\windows-dns-smoke-tests.ps1 -OutputFormat tap
|
|
.\windows-dns-smoke-tests.ps1 -NoColor | Out-File dns-results.txt
|
|
|
|
Requirements:
|
|
- PowerShell 5.1+ or PowerShell 7+
|
|
- DnsServer PowerShell module (DNS Server role or RSAT)
|
|
- Administrator privileges for statistics and event log access
|
|
"@
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN
|
|
# ============================================================================
|
|
|
|
if ($Help) { Show-Usage; exit 0 }
|
|
|
|
$script:StartTime = Get-Date
|
|
|
|
if ($OutputFormat -eq "tap") {
|
|
Write-TapHeader
|
|
} else {
|
|
Write-Host ""
|
|
Write-Color "Windows DNS Smoke Tests" "White"
|
|
Write-Host "Server: $DnsServer"
|
|
if ($Domain) { Write-Host "Domain: $Domain" }
|
|
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
|
|
}
|
|
|
|
try { Import-Module DnsServer -ErrorAction Stop } catch {
|
|
Write-Color "[ERROR] DnsServer module not available. Install the DNS Server role or RSAT DNS tools." "Red"
|
|
exit 1
|
|
}
|
|
|
|
Test-DnsService
|
|
Test-DnsResponds
|
|
Test-ZonesLoaded
|
|
Test-ADIntegratedZones
|
|
Test-ForwardLookup
|
|
Test-ReverseLookup
|
|
Test-ConditionalForwarders
|
|
Test-RootHints
|
|
Test-Forwarders
|
|
Test-Scavenging
|
|
Test-ZoneDelegation
|
|
Test-CacheStats
|
|
Test-EventLog
|
|
|
|
if ($OutputFormat -eq "tap") { Write-TapFooter } else { Write-Summary }
|
|
if ($script:Fail -eq 0) { exit 0 } else { exit 1 }
|