Last active
January 8, 2024 16:09
-
-
Save thedavecarroll/ea2b4f0ba7527bd469e59e1aabf3e0b0 to your computer and use it in GitHub Desktop.
This simple PowerShell script module uses a custom class and Get-ADObject to search an Active Directory-integrated DNS Zone by name or partial name.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Version 5.1 | |
#Requires -Module ActiveDirectory | |
$script:ADRootDSE = Get-ADRootDSE | |
class ADDnsNode { | |
# AD Object Properties | |
[String]$Name | |
[String]$CanonicalName | |
[System.DateTime]$Created | |
[System.DateTime]$Modified | |
[String]$DistinguishedName | |
[System.Boolean]$IsDeleted | |
[System.Boolean]$IsTombstoned | |
[System.Boolean]$ProtectedFromAccidentalDeletion | |
# DnsRecord Properties | |
[System.UInt32]$UpdateAtSerial | |
[System.UInt32]$TTL | |
[System.UInt32]$Age | |
[Nullable[System.DateTime]]$Timestamp | |
[System.Boolean]$IsStatic | |
[String]$RRType | |
[String]$Data | |
hidden [Microsoft.ActiveDirectory.Management.ADObject]$OriginalADObject | |
# Constructors | |
ADDnsNode () { } | |
ADDnsNode ([PSCustomObject]$InputObject) { | |
$this.Name = $InputObject.Name | |
$this.CanonicalName = $InputObject.CanonicalName | |
$this.Created = $InputObject.Created | |
$this.Modified = $InputObject.Modified | |
$this.DistinguishedName = $InputObject.DistinguishedName | |
$this.IsDeleted = $InputObject.IsDeleted | |
$this.IsTombstoned = $InputObject.dNSTombstoned | |
$this.ProtectedFromAccidentalDeletion = $InputObject.ProtectedFromAccidentalDeletion | |
$this.OriginalADObject = $InputObject | |
$This.ConvertFromDnsRecord($InputObject.dnsRecord[0]) | |
} | |
# Methods | |
[void] ConvertFromDnsRecord([byte[]]$DnsRecord) { | |
try { | |
$TTLRaw = $DnsRecord[12..15] | |
$this.TTL = [BitConverter]::ToUInt32($TTLRaw, 0) | |
[void][array]::Reverse($TTLRaw) # reverse for big endian | |
$this.UpdateAtSerial = [BitConverter]::ToUInt32($DnsRecord, 8) | |
$RecordType = $null | |
switch ([BitConverter]::ToUInt16($DnsRecord, 2)) { | |
1 { | |
$RecordData = '{0}.{1}.{2}.{3}' -f $DnsRecord[24], $DnsRecord[25], $DnsRecord[26], $DnsRecord[27] | |
$RecordType = 'A' | |
} | |
16 { | |
[string]$RecordData = '' | |
[int]$SegmentLength = $DnsRecord[24] | |
$Index = 25 | |
while ($SegmentLength-- -gt 0) { | |
$RecordData += [char]$DnsRecord[$index++] | |
} | |
$RecordType = 'TXT' | |
} | |
{$_ -in 2,5,12} { | |
$RecordData = $this.GetName($DnsRecord[24..$DnsRecord.length]) | |
switch ($_) { | |
2 { $RecordType = 'NS' } | |
5 { $RecordType = 'CNAME' } | |
12 { $RecordType = 'PTR' } | |
} | |
} | |
default { | |
$RecordData = $([System.Convert]::ToBase64String($DnsRecord[24..$DnsRecord.length])) | |
switch ($_) { | |
6 { $RecordType = 'SOA' } | |
13 { $RecordType = 'HINFO' } | |
15 { $RecordType = 'MX' } | |
17 { $RecordType = 'RP' } | |
28 { $RecordType = 'AAAA' } | |
33 { $RecordType = 'SRV' } | |
default { $RecordType = 'UNKNOWN' } | |
} | |
} | |
} | |
$this.RRType = $RecordType | |
$this.Data = $RecordData | |
$this.Age = [BitConverter]::ToUInt32($DnsRecord, 20) | |
if ($this.Age -ne 0) { | |
$RecordTimestamp = ((Get-Date '01/01/1601 00:00:00').AddHours($this.Age)) | |
$RecordIsStatic = $false | |
} else { | |
$RecordTimestamp = $null | |
$RecordIsStatic = $true | |
} | |
$this.Timestamp = $RecordTimestamp | |
$this.IsStatic = $RecordIsStatic | |
} | |
catch { | |
'{0} : {1}' -f $This.Name,$This.DistinguishedName | Write-Error | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
} | |
hidden [string] GetName([byte[]]$Raw) { | |
try { | |
[Int]$Segments = $Raw[1] | |
[Int]$Index = 2 | |
[String]$GetName = '' | |
while ($Segments-- -gt 0) { | |
[Int]$SegmentLength = $Raw[$Index++] | |
while ($SegmentLength-- -gt 0) { | |
$GetName += [Char]$Raw[$Index++] | |
} | |
$GetName += "." | |
} | |
return $GetName | |
} | |
catch { | |
$_ | Write-Warning | |
return '' | |
} | |
} | |
} | |
function Search-DnsRecord { | |
[CmdletBinding(DefaultParameterSetName='ByHostName')] | |
param( | |
[Parameter(Mandatory,ParameterSetName='ByHostName')] | |
[string]$HostName, | |
[Parameter(Mandatory,ParameterSetName='ByFilter')] | |
[string]$Filter, | |
[switch]$IncludeTombstoned, | |
[Parameter(Mandatory)] | |
[string]$ZoneName, | |
[string]$Server, | |
[ValidateSet('DomainDnsZones','ForestDnsZones')] | |
[string]$ADZoneType = 'DomainDnsZones' | |
) | |
$SearchBase = 'DC={0},CN=MicrosoftDNS,{1}' -f $ZoneName,($script:ADRootDSE.namingContexts.Where{$_ -match $ADZoneType})[0] | |
$SearchFilter = @() | |
# specify the Dns-Node objectCategory which is faster than objectClass | |
$DnsNodeCategory = 'CN=Dns-Node,{0}' -f $script:ADRootDSE.schemaNamingContext | |
$SearchFilter += ('objectCategory -eq "{0}"' -f $DnsNodeCategory) | |
# add tombstone filter, if required | |
if (-Not $PSBoundParameters.ContainsKey('IncludeTombstoned')) { | |
$SearchFilter += 'dNSTombstoned -eq $false' | |
} | |
# filter by name or partial name | |
switch ($PSCmdlet.ParameterSetName) { | |
'ByHostName' { | |
$SearchFilter += 'Name -eq "{0}"' -f $HostName | |
} | |
'ByFilter' { | |
if ($Filter -eq '*') { | |
$SearchFilter += 'Name -like "*"' -f $Filter | |
} else { | |
$SearchFilter += 'Name -like "*{0}*"' -f $Filter | |
} | |
} | |
} | |
$ADObjectParams = @{ | |
SearchBase = $SearchBase | |
Properties = '*' | |
Server = $Server | |
ResultSetSize = [int32]::MaxValue | |
Filter = $SearchFilter -join ' -and ' | |
} | |
Get-ADObject @ADObjectParams | ForEach-Object { | |
[ADDnsNode]::New($_) | |
} | |
} |
There are some record types that are not correctly decoded. I am working on this in my spare time.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Caveat
I have only used this for
DomainDnsZones
partition, but it should work forForestDnsZones
. Please let me know here if it does not.