Audit-ADSubnets.ps1
Description
@GitHub Copilot - Welcome @BanterBoy, I’m your Copilot and I’m here to help you get things done faster. I can identify issues, explain and even improve code.
You can ask generic questions, but what I’m really good at is helping you with your code. For example:
Generate unit tests for my code Explain the selected code Propose a fix for the bugs in my code If you want to learn more about my capabilities and limitations, check out the Copilot documentation.
I’m powered by AI, so surprises and mistakes are possible. Make sure to verify any generated code or suggestions, and share feedback so that we can learn and improve.
@BanterBoy: - Explain what the script does and why it is useful.
@GitHub Copilot -
Script
<#
.SYNOPSIS
The script is exporting all Active Directory subnets to a CSV file with the following datas:
Name : subnet name
Location : location attribute
Type : IPv4/IPv6
IpStart : first IP of the subnet
IpEnd : last IP of the subnet
RangeStart : first IP of the subnet (decimal format)
RangeEnd : last IP of the subnet (decimal format)
The script is exporting the list of all subnet overlaps within the forest with the following datas:
Subnet1 : the 'CN' attribute of the first subnet
Site1 : the associated site of the first subnet
Subnet2 : the 'CN' attribute of the second subnet
Site2 : the associated site of the second subnet
IsSameSite : check if site1 and site2 are the same (boolean)
The script is exporting the list of possible superscopes within a site with the following datas:
Site : concerned site
Superscope : IP and Mask of the superscope
IpStart : first IP of the superscope
IpEnd : last IP of the superscope
Subnets : list of concerned subnets
.NOTES
Author : Alexandre Augagneur (www.alexwinner.com)
File Name : Audit-ADSubnets.ps1
.EXAMPLE
.\Audit-ADSubnets.ps1 -Path C:\Temp -CheckOverlap $true -CheckSuperscope $true
.PARAMETER Path
Path for the exported files
.PARAMETER CheckOverlap
If set to $true, the script is checking the existence of subnet overlaps
.PARAMETER CheckSuperScope
If set to $true, the script is evaluating superscope creation within AD sites
#>
param (
[Parameter(Mandatory = $true)]
[ValidateScript( { Test-Path $_ -PathType Container })]
[String] $Path,
[Parameter()]
[Boolean] $CheckOverlap = $false,
[Parameter()]
[Boolean] $CheckSuperscope = $false
)
#Region Functions
####################################################
# Functions
####################################################
#---------------------------------------------------
# Converting an array of bytes to IPv6 format
#---------------------------------------------------
function Convert-BytesToIPv6 ( $arrayBytes ) {
$String = $null
$j = 0
foreach ( $Item in $arrayBytes ) {
if ( $j -eq 2) {
$String += ":" + [system.bitconverter]::Tostring($Item)
$j = 1
}
else {
$String += [system.bitconverter]::Tostring($Item)
$j++
}
}
Return $String
}
#---------------------------------------------------
# Create the IPv6 object
#---------------------------------------------------
function Compute-IPv6 ( $Obj, $ObjInputAddress, $Prefix ) {
try {
# Compute IP length
[int] $IntIPLength = 128 - $Prefix
$Obj | Add-Member -type NoteProperty -name Type -value "IPv6"
# Compute available IPs based on the IPv6 Prefix
[System.Numerics.BigInteger] $NumberOfIPs = [System.Math]::Pow(2, $IntIPLength)
$Obj | Add-Member -type NoteProperty -name NumberOfIPs -value $NumberOfIPs
# Convert InputAddress to decimal value
$ArrBytesInputAddress = $ObjInputAddress.GetAddressBytes()
[array]::Reverse($ArrBytesInputAddress)
$BigIntRangeStart = [System.Numerics.BigInteger] $ArrBytesInputAddress
$Obj | Add-Member -type NoteProperty -name RangeStart -value $BigIntRangeStart
# Compute the lastest available IP in decimal value
[System.Numerics.BigInteger] $BigIntRangeEnd = ($BigIntRangeStart + $NumberOfIPs) - 1
$Obj | Add-Member -type NoteProperty -name RangeEnd -value $BigIntRangeEnd
# Convert the decimal value of the range start to IPv6 format
$Obj | Add-Member -type NoteProperty -name IpStart -value $ObjInputAddress.IPAddressToString
# Convert the lastest available IP in IPv6 format
$ArrBytesRangeEnd = $BigIntRangeEnd.ToByteArray()
[array]::Reverse($ArrBytesRangeEnd)
$IpEnd = Convert-BytesToIpv6 $ArrBytesRangeEnd
$Obj | Add-Member -type NoteProperty -name IpEnd -value $IpEnd
return $Obj
}
catch {
Write-Host $_.Exception.Message
}
}
#---------------------------------------------------
# Create the IPv4 object
#---------------------------------------------------
function Compute-IPv4 ( $Obj, $ObjInputAddress, $Prefix ) {
$Obj | Add-Member -type NoteProperty -name Type -value "IPv4"
# Compute IP length
[int] $IntIPLength = 32 - $Prefix
$NumberOfIPs = ([System.Math]::Pow(2, $IntIPLength)) - 1
$ArrBytesInputAddress = $ObjInputAddress.GetAddressBytes()
[Array]::Reverse($ArrBytesInputAddress)
$IpStart = ([System.Net.IPAddress]($ArrBytesInputAddress -join ".")).Address
If (($IpStart.Gettype()).Name -ine "double") {
$IpStart = [Convert]::ToDouble($IpStart)
}
$IpStart = [System.Net.IPAddress] $IpStart
$Obj | Add-Member -type NoteProperty -name IpStart -value $IpStart
$ArrBytesIpStart = $IpStart.GetAddressBytes()
[array]::Reverse($ArrBytesIpStart)
$RangeStart = [system.bitconverter]::ToUInt32($ArrBytesIpStart, 0)
$IpEnd = $RangeStart + $NumberOfIPs
If (($IpEnd.Gettype()).Name -ine "double") {
$IpEnd = [Convert]::ToDouble($IpEnd)
}
$IpEnd = [System.Net.IPAddress] $IpEnd
$Obj | Add-Member -type NoteProperty -name IpEnd -value $IpEnd
$Obj | Add-Member -type NoteProperty -name RangeStart -value $RangeStart
$ArrBytesIpEnd = $IpEnd.GetAddressBytes()
[array]::Reverse($ArrBytesIpEnd)
$Obj | Add-Member -type NoteProperty -name RangeEnd -value ([system.bitconverter]::ToUInt32($ArrBytesIpEnd, 0))
Return $Obj
}
#---------------------------------------------------
# Compute the subnet/prefix length
#---------------------------------------------------
function Compute-Prefix ( $IntStart, $IntEnd, $SubnetType ) {
if ( $SubnetType -eq "IPv4" ) {
[Double] $NumberOfIPs = [Double] $IntEnd - [Double] $IntStart
}
else {
[System.Numerics.BigInteger] $NumberOfIPs = [System.Numerics.BigInteger] $IntEnd - [System.Numerics.BigInteger] $IntStart
}
$IPlength = [Math]::Ceiling([Math]::Log($NumberOfIPs, 2))
$Prefix = 32 - $IPlength
Return $Prefix
}
#EndRegion
$Message =
@"
---------------------------------------------------------------------------------------------------
The script will not treat IPv6 subnets.
The script needs to use the CLR .Net Framework 4.0 to manage IPv6 subnets.
If you have already installed it, you have to indicate to PowerShell to use it.
Commands to run:
reg add hklm\software\microsoft\.netframework /v OnlyUseLatestCLR /t REG_DWORD /d 1
reg add hklm\software\wow6432node\microsoft\.netframework /v OnlyUseLatestCLR /t REG_DWORD /d 1
---------------------------------------------------------------------------------------------------
"@
# Check the CLR version (only version 4 can be used)
if ( ([environment]::Version).Major -ne 4 ) {
Write-Host $Message -Foreground 'Yellow'
$script:CLRVersion4 = $false
}
else {
try {
# Add dll System.Numerics to use the BigInteger structure
Add-Type -Path "C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Numerics.dll"
$script:CLRVersion4 = $true
}
catch {
Write-Error "Adding DLL Sytem.Numerics: $($_.Exception.Message)"
$script:CLRVersion4 = $false
}
}
Write-Host "Retrieving AD subnets..."
# Connect to Active Directory and retrieve subnet objects
$objRootDSE = [System.DirectoryServices.DirectoryEntry] "LDAP://rootDSE"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
$Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry("LDAP://cn=subnets,cn=sites," + $objRootDSE.ConfigurationNamingContext)
$Searcher.PageSize = 10000
$Searcher.SearchScope = "Subtree"
$Searcher.Filter = "(objectClass=subnet)"
$Properties = @("cn", "location", "siteobject")
$Searcher.PropertiesToLoad.AddRange(@($Properties))
$Subnets = $Searcher.FindAll()
$selectedProperties = $Properties | ForEach-Object { @{name = "$_"; expression = $ExecutionContext.InvokeCommand.NewScriptBlock("`$_['$_']") } }
[Regex] $RegexCN = "CN=(.*?),.*"
$SubnetsArray = @()
foreach ( $Subnet in $Subnets ) {
# Construct the subnet object
$SubnetObj = New-Object -TypeName PsObject
$SubnetObj | Add-Member -type NoteProperty -name Name -value ([string] $Subnet.Properties['cn'])
$SubnetObj | Add-Member -type NoteProperty -name Location -value ([string] $Subnet.Properties['location'])
$SubnetObj | Add-Member -type NoteProperty -name Site -value ([string] $RegexCN.Match( $Subnet.Properties['siteobject']).Groups[1].Value)
$InputAddress = (($SubnetObj.Name).Split("/"))[0]
$Prefix = (($SubnetObj.Name).Split("/"))[1]
# Construct System.Net.IPAddress
$ObjInputAddress = [System.Net.IPAddress] $InputAddress
# Check if IP is a IPv4 or IPv6
if ( ($ObjInputAddress.AddressFamily -match "InterNetworkV6") ) {
if ($script:CLRVersion4) {
# Compute network address and IP ranges
$SubnetObj = Compute-IPv6 $SubnetObj $ObjInputAddress $Prefix
$SubnetsArray += $SubnetObj
}
}
elseif ( $ObjInputAddress.AddressFamily -match "InterNetwork" ) {
$SubnetObj = Compute-IPv4 $SubnetObj $ObjInputAddress $Prefix
$SubnetsArray += $SubnetObj
}
}
$SubnetsArray | Export-Csv "$($Path)\ADSubnets-Export.csv" -Delimiter ";" -NoTypeInformation -Force
Write-Host "List of AD subnets exported to file : $($Path)\ADSubnets-Export.csv" -ForegroundColor Green
$ReportObj = New-Object -TypeName PsObject
$ReportObj | Add-Member -type NoteProperty -name 'Number of subnets' -value ($SubnetsArray.Count)
$ReportObj | Add-Member -type NoteProperty -name 'Number of subnets with no site' -value (($SubnetsArray | Where-Object { [string]::IsNullOrEmpty($_.Site) }).Count)
# Check if overlaps are existing between every subnets of the forest
if ( $CheckOverlap ) {
Write-Host "Checking subnets overlap..."
# Working of the existing array
$Subnets = $SubnetsArray | Sort-Object -Property RangeStart
$OverlapsArray = @()
# Compare a subnet against all the others to check if it is overlapping one of them
for ( $i = 0; $i -lt $Subnets.Count; $i++ ) {
foreach ( $Item in $Subnets ) {
# Compare subnets ranges (decimal values of first IP and last IP) of the same type (IPv4/IPv6)
if (($Item.Type -match $Subnets[$i].Type) -and ($Item.rangeStart -ge $Subnets[$i].rangeStart) -and ($Item.rangeEnd -le $Subnets[$i].rangeEnd) -and ($Item.Name -notmatch $Subnets[$i].Name) ) {
$OverlapObj = New-Object -TypeName PsObject
$OverlapObj | Add-Member -type NoteProperty -name Subnet1 -value $Subnets[$i].Name
$OverlapObj | Add-Member -type NoteProperty -name Site1 -value $Subnets[$i].Site
$OverlapObj | Add-Member -type NoteProperty -name Subnet2 -value $Item.Name
$OverlapObj | Add-Member -type NoteProperty -name Site2 -value $Item.Site
if ( $OverlapObj.Site1 -eq $OverlapObj.Site2 ) {
$OverlapObj | Add-Member -type NoteProperty -name IsSameSite -value $true
}
else {
$OverlapObj | Add-Member -type NoteProperty -name IsSameSite -value $false
}
$OverlapsArray += $OverlapObj
}
}
}
$OverlapsArray | Export-Csv "$($Path)\ADSubnets-Overlaps.csv" -Delimiter ";" -NoTypeInformation -Force
Write-Host "List of overlapped AD subnets exported to file : $($Path)\ADSubnets-Overlaps.csv" -ForegroundColor Green
$ReportObj | Add-Member -type NoteProperty -name 'Number of overlaps' -value ($OverlapsArray.Count)
}
# Check superscope creation per site. The script doesn't check the "location" attribute
if ( $CheckSuperscope ) {
Write-Host "Checking subnets superscope..."
$Subnets = $SubnetsArray | Sort-Object -Property Site, RangeStart
$Sites = $Subnets | Select-Object Site -Unique
$ArraySuperScopes = @()
foreach ( $Site in $Sites ) {
# Treatment of IPv6 if CLR 4.0 is used
if ( $script:CLRVersion4 ) {
$SubnetsOfSite = $Subnets | Where-Object { $_.Site -eq $Site.Site } | Sort-Object RangeStart
}
else {
$SubnetsOfSite = $Subnets | Where-Object { ($_.Site -eq $Site.Site) -and ($_.Type -eq "IPv4") } | Sort-Object RangeStart
}
# Check if there is more than 1 subnet associated to a site
if ( $SubnetsOfSite.Count -gt 1 ) {
# Treatment of each subnet in a site
for ( $i = 0; $i -lt ($SubnetsOfSite.Count - 1) ; $i++ ) {
# Cast the last IP of the current subnet and the last IP of the next subnet
if ( $SubnetsOfSite[$i].Type -eq "IPv4" ) {
[double] $LastIP = $SubnetsOfSite[$i].RangeEnd
$LastIP++
[double] $FirstIP = $SubnetsOfSite[$i + 1].RangeStart
}
else {
[System.Numerics.BigInteger] $LastIP = $SubnetsOfSite[$i].RangeEnd
$LastIP += 1
[System.Numerics.BigInteger] $FirstIP = $SubnetsOfSite[$i + 1].RangeStart
}
# Check if we can merge the current subnet with the next subnet
if ( ($LastIP -ge $FirstIP) -and ($SubnetsOfSite[$i].Type -eq $SubnetsOfSite[$i + 1].Type) ) {
$SubnetType = $SubnetsOfSite[$i].Type
# Check if we have to create a new superscope object
if ( !($SuperScopeObj) ) {
$SuperScopeObj = New-Object -TypeName PsObject
$SuperScopeObj | Add-Member -type NoteProperty -name Site -value $Site.Site
$SuperScopeObj | Add-Member -type NoteProperty -name RangeStart -value ($SubnetsOfSite[$i].RangeStart)
if ( $SubnetType -eq "IPv4" ) {
$SuperScopeObj | Add-Member -type NoteProperty -name IpStart -value ([System.Net.IPAddress] "$($SubnetsOfSite[$i].RangeStart)")
}
else {
[System.Numerics.BigInteger] $BigIntIP = $SubnetsOfSite[$i].RangeStart
$ArrBytesIP = $BigIntIP.ToByteArray()
[array]::Reverse($ArrBytesIP)
$IP = Convert-BytesToIpv6 $ArrBytesIP
$SuperScopeObj | Add-Member -type NoteProperty -name IpStart -value ([System.Net.IPAddress] $IP)
}
$arrSubnets = @()
}
$arrSubnets += $SubnetsOfSite[$i].Name
$SuperScopeObj | Add-Member -type NoteProperty -name RangeEnd -value ($SubnetsOfSite[$i + 1].RangeEnd) -Force
if ( $SubnetType -eq "IPv4" ) {
$SuperScopeObj | Add-Member -type NoteProperty -name IpEnd -value ([System.Net.IPAddress] "$($SubnetsOfSite[$i+1].RangeEnd)") -Force
}
else {
[System.Numerics.BigInteger] $BigIntIP = $SubnetsOfSite[$i + 1].RangeEnd
$ArrBytesIP = $BigIntIP.ToByteArray()
[array]::Reverse($ArrBytesIP)
$IP = Convert-BytesToIpv6 $ArrBytesIP
$SuperScopeObj | Add-Member -type NoteProperty -name IpEnd -value ([System.Net.IPAddress] $IP) -Force
}
}
# Current subnet can not be merge with the next one
else {
# If property RangeEnd is not null then add the superscope to the superscopes array
if ( $SuperScopeObj.RangeEnd ) {
$arrSubnets += $SubnetsOfSite[$i].Name
$SuperScopeObj | Add-Member -type NoteProperty -name Subnets -value ([string] $arrSubnets)
$SubnetLength = Compute-Prefix $SuperScopeObj.RangeStart $SuperScopeObj.RangeEnd $SubnetType
$SuperScopeObj | Add-Member -type NoteProperty -name Superscope -value "$($SuperScopeObj.IpStart)/$Subnetlength"
$ArraySuperScopes += $SuperScopeObj
Remove-Variable -Name SuperScopeObj
}
}
}
# Special treatment for the lastest subnet which is part of a superscope
if ( $SuperScopeObj.RangeEnd ) {
$arrSubnets += $SubnetsOfSite[$i].Name
$SuperScopeObj | Add-Member -type NoteProperty -name Subnets -value ([string] $arrSubnets)
$SubnetLength = Compute-Prefix $SuperScopeObj.RangeStart $SuperScopeObj.RangeEnd $SubnetType
$SuperScopeObj | Add-Member -type NoteProperty -name Superscope -value "$($SuperScopeObj.IpStart)/$Subnetlength"
$ArraySuperScopes += $SuperScopeObj
Remove-Variable -Name SuperScopeObj
}
}
}
$ArraySuperScopes | Select-Object Site, Superscope, IpStart, IpEnd, Subnets | Export-Csv "$($Path)\ADSubnets-Superscopes.csv" -Delimiter ";" -NoTypeInformation
Write-Host "AD subnet superscopes evaluation exported to file : $($Path)\ADSubnets-Superscopes.csv" -ForegroundColor Green
$ReportObj | Add-Member -type NoteProperty -name 'Number of superscopes' -value ($ArraySuperScopes.Count)
}
$ReportObj | Format-List
Download
Please feel free to copy parts of the script or if you would like to download the entire script, simple click the download button. You can download the complete repository in a zip file by clicking the Download link in the menu bar on the left hand side of the page.
Report Issues
You can report an issue or contribute to this site on GitHub. Simply click the button below and add any relevant notes. I will attempt to respond to all issues as soon as possible.