Public/Get-VBPTRRecord.ps1
|
function Get-VBPTRRecord { <# .SYNOPSIS Resolve an IP address via DNS PTR lookup with forward-confirmation (Layer 3). .DESCRIPTION Performs a reverse DNS (PTR) lookup for the supplied IP. On success, the resolved hostname is forward-confirmed by querying the A record for that name and verifying that at least one resolved IP matches the original. Stale PTR records that no longer resolve back to the same IP are returned with ForwardConfirmed = $false and Confidence = 'Low'. PS 5.1 fallback: uses [System.Net.Dns]::GetHostEntry() when Resolve-DnsName is not available (unlikely on WS2012+ but handled defensively). TLS 1.2 is forced on PS 5.1 to ensure the session is not degraded by default system settings (belt-and-suspenders -- DNS itself doesn't use TLS, but other layers in the session might). Prerequisites: $Context.DNSAvailable must be $true. .PARAMETER IPAddress The RFC1918 / CGNAT / link-local IP address to look up. .PARAMETER Context Environment context object from Get-VBEnrichmentContext. Provides DNSAvailable. .OUTPUTS [PSCustomObject] -- base layer result fields plus: Hostname [string] PTRRecord [string] ForwardConfirmed [bool] Confidence [string] High | Low .EXAMPLE $ctx = Get-VBEnrichmentContext Get-VBPTRRecord -IPAddress '192.168.1.45' -Context $ctx .EXAMPLE '192.168.1.45','192.168.1.46' | Get-VBPTRRecord -Context $ctx .NOTES Version: 1.0.0 MinPSVersion: 5.1 Author: VB ChangeLog: 1.0.0 -- 2026-05-11 -- Initial release #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [string]$IPAddress, [Parameter()] [PSCustomObject]$Context ) begin { $LAYER_NUM = 3 $LAYER_NAME = 'PTR' if (-not $Context) { Write-Warning "[$LAYER_NAME] No context provided -- running without prerequisite validation." } $hasResolveDnsName = [bool](Get-Command -Name Resolve-DnsName -ErrorAction SilentlyContinue) } process { $sw = [System.Diagnostics.Stopwatch]::StartNew() if ($Context -and -not $Context.DNSAvailable) { $sw.Stop() return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Skipped' -ExecutionMs $sw.ElapsedMilliseconds ` -SkipReason 'DNSUnavailable' ` -Impact 'PTR-based hostname resolution unavailable' } try { $ptrRecord = $null $hostname = $null if ($hasResolveDnsName) { $ptrResult = Resolve-DnsName -Name $IPAddress -Type PTR -ErrorAction Stop $ptrRecord = $ptrResult | Where-Object { $_.Type -eq 'PTR' } | Select-Object -ExpandProperty NameHost -First 1 } else { # PS 5.1 fallback -- [System.Net.Dns]::GetHostEntry does PTR + A in one call $entry = [System.Net.Dns]::GetHostEntry($IPAddress) $ptrRecord = $entry.HostName } if ([string]::IsNullOrWhiteSpace($ptrRecord)) { $sw.Stop() return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'NoResult' -ExecutionMs $sw.ElapsedMilliseconds } $hostname = $ptrRecord.TrimEnd('.') # Forward-confirm: resolve A record and check for IP match $forwardConfirmed = $false try { if ($hasResolveDnsName) { $aResult = Resolve-DnsName -Name $hostname -Type A -ErrorAction Stop $resolvedIPs = @($aResult | Where-Object { $_.Type -eq 'A' } | Select-Object -ExpandProperty IPAddress) } else { $fwdEntry = [System.Net.Dns]::GetHostAddresses($hostname) $resolvedIPs = @($fwdEntry | ForEach-Object { $_.ToString() }) } $parsedTarget = [System.Net.IPAddress]::Parse($IPAddress) $forwardConfirmed = [bool]($resolvedIPs | Where-Object { try { [System.Net.IPAddress]::Parse($_).Equals($parsedTarget) } catch { $false } } | Select-Object -First 1) } catch { Write-Verbose "[$LAYER_NAME] Forward-confirm failed for $hostname`: $($_.Exception.Message)" $forwardConfirmed = $false } $confidence = if ($forwardConfirmed) { 'High' } else { 'Low' } $sw.Stop() New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Success' -ExecutionMs $sw.ElapsedMilliseconds ` -ExtraFields @{ Hostname = $hostname PTRRecord = $ptrRecord ForwardConfirmed = $forwardConfirmed Confidence = $confidence } } catch { # NXDOMAIN and SERVFAIL both throw from Resolve-DnsName -- distinguish them $msg = $_.Exception.Message if ($msg -match 'DNS name does not exist|NXDOMAIN|no such host') { $sw.Stop() return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'NoResult' -ExecutionMs $sw.ElapsedMilliseconds } $sw.Stop() New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME ` -Status 'Failed' -ExecutionMs $sw.ElapsedMilliseconds ` -ErrorDetail $msg } } } |