diff --git a/ADEssentials.psd1 b/ADEssentials.psd1 index 94011f1b..59a8387b 100644 --- a/ADEssentials.psd1 +++ b/ADEssentials.psd1 @@ -8,7 +8,7 @@ Description = 'Helper module for Active Directory with lots of useful functions that simplify supporting Active Directory.' FunctionsToExport = @('Add-ADACL', 'Compare-PingCastleReport', 'Compare-WinADGlobalCatalogObjects', 'Convert-ADSecurityDescriptor', 'Copy-ADOUSecurity', 'Disable-ADACLInheritance', 'Enable-ADACLInheritance', 'Export-ADACLObject', 'Find-WinADObjectDifference', 'Get-ADACL', 'Get-ADACLOwner', 'Get-DNSServerIP', 'Get-PingCastleReport', 'Get-WinADACLConfiguration', 'Get-WinADACLForest', 'Get-WinADBitlockerLapsSummary', 'Get-WinADBrokenProtectedFromDeletion', 'Get-WinADComputerACLLAPS', 'Get-WinADComputers', 'Get-WinADDelegatedAccounts', 'Get-WinADDFSHealth', 'Get-WinADDFSTopology', 'Get-WinADDHCP', 'Get-WinADDHCPHealthCheck', 'Get-WinADDHCPSummary', 'Get-WinADDiagnostics', 'Get-WinADDnsInformation', 'Get-WinADDnsIPAddresses', 'Get-WinADDnsRecords', 'Get-WinADDnsServerForwarder', 'Get-WinADDnsServerScavenging', 'Get-ADWinDnsServerZones', 'Get-WinADDNSZones', 'Get-WinADDomain', 'Get-WinADDomainControllerGenerationId', 'Get-WinADDomainControllerNetLogonSettings', 'Get-WinADDomainControllerNTDSSettings', 'Get-WinADDomainControllerOption', 'Get-WinADDuplicateObject', 'Get-WinADDuplicateSPN', 'Get-WinADForest', 'Get-WinADForestControllerInformation', 'Get-WinADForestOptionalFeatures', 'Get-WinADForestReplication', 'Get-WinADForestReplicationSummary', 'Get-WinADForestRoles', 'Get-WinADForestSchemaDetails', 'Get-WinADForestSchemaProperties', 'Get-WinADForestSites', 'Get-WinADForestSubnet', 'Get-WinADGroupMember', 'Get-WinADGroupMemberOf', 'Get-WinADGroups', 'Get-WinADKerberosAccount', 'Get-WinADLastBackup', 'Get-WinADLDAPBindingsSummary', 'Get-WinADLDAPSummary', 'Get-WinADLMSettings', 'Get-WinADObject', 'Get-WinADOrganization', 'Get-WinADPasswordPolicy', 'Get-WinADPrivilegedObjects', 'Get-WinADProtocol', 'Get-WinADProxyAddresses', 'Get-WinADServiceAccount', 'Get-WinADSharePermission', 'Get-WinADSIDHistory', 'Get-WinADSiteConnections', 'Get-WinADSiteCoverage', 'Get-WinADSiteLinks', 'Get-WinADSiteOptions', 'Get-WinADTombstoneLifetime', 'Get-WinADTrust', 'Get-WinADTrustLegacy', 'Get-WinADUserPrincipalName', 'Get-WinADUsers', 'Get-WinADUsersForeignSecurityPrincipalList', 'Get-WinADWellKnownFolders', 'Invoke-ADEssentials', 'Invoke-PingCastle', 'Invoke-WinADDHCPHealthCheck', 'New-ADACLObject', 'New-ADSite', 'Remove-ADACL', 'Remove-WinADDFSTopology', 'Remove-WinADDuplicateObject', 'Remove-WinADSharePermission', 'Rename-WinADUserPrincipalName', 'Repair-WinADACLConfigurationOwner', 'Repair-WinADBrokenProtectedFromDeletion', 'Repair-WinADEmailAddress', 'Repair-WinADForestControllerInformation', 'Request-ChangePasswordAtLogon', 'Request-DisableOnAccountExpiration', 'Restore-ADACLDefault', 'Set-ADACL', 'Set-ADACLInheritance', 'Set-ADACLOwner', 'Set-DnsServerIP', 'Set-WinADDiagnostics', 'Set-WinADDomainControllerNetLogonSettings', 'Set-WinADDomainControllerOption', 'Set-WinADForestACLOwner', 'Set-WinADReplication', 'Set-WinADReplicationConnections', 'Set-WinADShare', 'Set-WinADTombstoneLifetime', 'Show-WinADDHCPSummary', 'Show-WinADDNSRecords', 'Show-WinADForestReplicationSummary', 'Show-WinADGroupCritical', 'Show-WinADGroupMember', 'Show-WinADGroupMemberOf', 'Show-WinADKerberosAccount', 'Show-WinADLdapSummary', 'Show-WinADObjectDifference', 'Show-WinADOrganization', 'Show-WinADSIDHistory', 'Show-WinADSites', 'Show-WinADSitesCoverage', 'Show-WinADTrust', 'Show-WinADUserSecurity', 'Sync-WinADDomainController', 'Test-ADDomainController', 'Test-ADRolesAvailability', 'Test-ADSiteLinks', 'Test-DNSNameServers', 'Test-FSMORolesAvailability', 'Test-LDAP', 'Test-WinADDNSResolving', 'Test-WinADObjectReplicationStatus', 'Test-WinADVulnerableSchemaClass', 'Update-LastLogonTimestamp') GUID = '9fc9fd61-7f11-4f4b-a527-084086f1905f' - ModuleVersion = '0.0.268' + ModuleVersion = '0.0.269' PowerShellVersion = '5.1' PrivateData = @{ PSData = @{ diff --git a/Public/Get-WinADDHCPSummary.ps1 b/Public/Get-WinADDHCPSummary.ps1 index 3df4ce75..72b273bd 100644 --- a/Public/Get-WinADDHCPSummary.ps1 +++ b/Public/Get-WinADDHCPSummary.ps1 @@ -70,10 +70,16 @@ Excludes components from being gathered. Same accepted values as IncludeComponents. .PARAMETER IncludeServers - Only analyze these DHCP servers (FQDN or short names). Applied to discovery and detailed processing. + Only analyze these DHCP servers (case-insensitive). Matches exact FQDN or IP, and short names when short form is provided. .PARAMETER ExcludeServers - Exclude these DHCP servers from discovery/processing. + Exclude these DHCP servers (case-insensitive). Matches exact FQDN or IP, and short names when short form is provided. + + .PARAMETER IncludeServerPrefix + Only analyze DHCP servers whose short names start with these prefixes (case-insensitive). + + .PARAMETER ExcludeServerPrefix + Exclude DHCP servers whose short names start with these prefixes (case-insensitive). .PARAMETER IncludeScopeId Only process these IPv4 scope IDs (e.g., 10.10.1.0). Applied per-server after scope discovery. @@ -106,6 +112,11 @@ Excludes DHCPv6, options/classes, and scope utilization for a lighter, faster collection. + .EXAMPLE + Get-WinADDHCPSummary -IncludeServerPrefix "NYC","LON" + + Includes servers whose short names start with NYC or LON (case-insensitive), e.g., NYC01, nyc-dhcp, LON-DHCP01. + .NOTES This function requires the DHCP PowerShell module and appropriate permissions to query DHCP servers. The function performs validation checks for common DHCP misconfigurations including: @@ -160,6 +171,8 @@ # New: Control which servers/scopes are processed [alias('IncludeHost','IncludeDHCPServers')][string[]] $IncludeServers, [alias('ExcludeHost','ExcludeDHCPServers')][string[]] $ExcludeServers, + [string[]] $IncludeServerPrefix, + [string[]] $ExcludeServerPrefix, [alias('IncludeScope','IncludeScopes')][string[]] $IncludeScopeId, [alias('ExcludeScope','ExcludeScopes')][string[]] $ExcludeScopeId ) @@ -178,6 +191,10 @@ Write-Verbose "Get-WinADDHCPSummary - Running in TEST MODE - using mock data for DHCP operations" } + if ($IncludeServerPrefix -and $ExcludeServerPrefix) { + Write-Warning "Get-WinADDHCPSummary - Both IncludeServerPrefix and ExcludeServerPrefix specified; include is applied first, then exclude." + } + # Accurate utilization is expensive; allow only when scope is limited if ($AccurateUtilization) { if ($ExcludeComponents -and $ExcludeComponents -contains 'ScopeStatistics') { @@ -277,15 +294,79 @@ # Apply include/exclude filters to discovered servers if requested if ($IncludeServers -and $IncludeServers.Count -gt 0) { - $set = @{}; foreach ($n in $IncludeServers) { $set[$n.ToLower()] = $true } - $DHCPServersFromAD = @($DHCPServersFromAD | Where-Object { $set.ContainsKey($_.DnsName.ToLower()) }) + $setExact = @{} + $setShort = @{} + foreach ($n in $IncludeServers) { + if ([string]::IsNullOrWhiteSpace($n)) { continue } + $name = $n.Trim().ToLower() + $setExact[$name] = $true + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($name, [ref]$ipValue) + if (-not $isIp -and $name -notmatch '\.') { + $setShort[$name] = $true + } + } + $DHCPServersFromAD = @($DHCPServersFromAD | Where-Object { + $dns = ([string]$_.DnsName).ToLower() + $short = if ($dns -match '\.') { $dns.Split('.')[0] } else { $dns } + $ip = if ($_.IPAddress) { ([string]$_.IPAddress).ToLower() } else { $null } + $setExact.ContainsKey($dns) -or ($ip -and $setExact.ContainsKey($ip)) -or ($short -and $setShort.ContainsKey($short)) + }) Write-Verbose "Get-WinADDHCPSummary - After IncludeServers filter: $($DHCPServersFromAD.Count) servers" } + if ($IncludeServerPrefix -and $IncludeServerPrefix.Count -gt 0) { + $prefixes = @($IncludeServerPrefix | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim().ToLower() }) + if ($prefixes.Count -gt 0) { + $DHCPServersFromAD = @( + $DHCPServersFromAD | Where-Object { + if (-not $_.DnsName) { return $false } + $short = ([string]$_.DnsName).Split('.')[0].ToLower() + foreach ($p in $prefixes) { + if ($short.StartsWith($p)) { return $true } + } + return $false + } + ) + Write-Verbose "Get-WinADDHCPSummary - After IncludeServerPrefix filter: $($DHCPServersFromAD.Count) servers" + } + } if ($ExcludeServers -and $ExcludeServers.Count -gt 0) { - $setX = @{}; foreach ($n in $ExcludeServers) { $setX[$n.ToLower()] = $true } - $DHCPServersFromAD = @($DHCPServersFromAD | Where-Object { -not $setX.ContainsKey($_.DnsName.ToLower()) }) + $setXExact = @{} + $setXShort = @{} + foreach ($n in $ExcludeServers) { + if ([string]::IsNullOrWhiteSpace($n)) { continue } + $name = $n.Trim().ToLower() + $setXExact[$name] = $true + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($name, [ref]$ipValue) + if (-not $isIp -and $name -notmatch '\.') { + $setXShort[$name] = $true + } + } + $DHCPServersFromAD = @($DHCPServersFromAD | Where-Object { + $dns = ([string]$_.DnsName).ToLower() + $short = if ($dns -match '\.') { $dns.Split('.')[0] } else { $dns } + $ip = if ($_.IPAddress) { ([string]$_.IPAddress).ToLower() } else { $null } + -not ($setXExact.ContainsKey($dns) -or ($ip -and $setXExact.ContainsKey($ip)) -or ($short -and $setXShort.ContainsKey($short))) + }) Write-Verbose "Get-WinADDHCPSummary - After ExcludeServers filter: $($DHCPServersFromAD.Count) servers" } + if ($ExcludeServerPrefix -and $ExcludeServerPrefix.Count -gt 0) { + $prefixesX = @($ExcludeServerPrefix | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim().ToLower() }) + if ($prefixesX.Count -gt 0) { + $DHCPServersFromAD = @( + $DHCPServersFromAD | Where-Object { + if (-not $_.DnsName) { return $false } + $short = ([string]$_.DnsName).Split('.')[0].ToLower() + foreach ($p in $prefixesX) { + if ($short.StartsWith($p)) { return $false } + } + return $true + } + ) + Write-Verbose "Get-WinADDHCPSummary - After ExcludeServerPrefix filter: $($DHCPServersFromAD.Count) servers" + } + } } catch { Add-DHCPError -Summary $DHCPSummary -ServerName 'AD Discovery' -Component 'DHCP Server Discovery' -Operation 'Get-DhcpServerInDC' -ErrorMessage $_.Exception.Message -Severity 'Error' return $DHCPSummary @@ -300,10 +381,76 @@ # If specific servers provided, only analyze those $ServersToAnalyze = $ComputerName if ($IncludeServers -and $IncludeServers.Count -gt 0) { - $ServersToAnalyze = @($ServersToAnalyze | Where-Object { $_ -in $IncludeServers }) + $setExact = @{} + $setShort = @{} + foreach ($n in $IncludeServers) { + if ([string]::IsNullOrWhiteSpace($n)) { continue } + $name = $n.Trim().ToLower() + $setExact[$name] = $true + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($name, [ref]$ipValue) + if (-not $isIp -and $name -notmatch '\.') { + $setShort[$name] = $true + } + } + $ServersToAnalyze = @($ServersToAnalyze | Where-Object { + $n = ([string]$_).ToLower() + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($n, [ref]$ipValue) + $short = if (-not $isIp -and $n -match '\.') { $n.Split('.')[0] } elseif (-not $isIp) { $n } else { $null } + $setExact.ContainsKey($n) -or ($short -and $setShort.ContainsKey($short)) + }) + } + if ($IncludeServerPrefix -and $IncludeServerPrefix.Count -gt 0) { + $prefixes = @($IncludeServerPrefix | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim().ToLower() }) + if ($prefixes.Count -gt 0) { + $ServersToAnalyze = @( + $ServersToAnalyze | Where-Object { + $n = ([string]$_).ToLower() + $short = if ($n -match '\.') { $n.Split('.')[0] } else { $n } + foreach ($p in $prefixes) { + if ($short.StartsWith($p)) { return $true } + } + return $false + } + ) + } } if ($ExcludeServers -and $ExcludeServers.Count -gt 0) { - $ServersToAnalyze = @($ServersToAnalyze | Where-Object { $_ -notin $ExcludeServers }) + $setXExact = @{} + $setXShort = @{} + foreach ($n in $ExcludeServers) { + if ([string]::IsNullOrWhiteSpace($n)) { continue } + $name = $n.Trim().ToLower() + $setXExact[$name] = $true + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($name, [ref]$ipValue) + if (-not $isIp -and $name -notmatch '\.') { + $setXShort[$name] = $true + } + } + $ServersToAnalyze = @($ServersToAnalyze | Where-Object { + $n = ([string]$_).ToLower() + $ipValue = $null + $isIp = [System.Net.IPAddress]::TryParse($n, [ref]$ipValue) + $short = if (-not $isIp -and $n -match '\.') { $n.Split('.')[0] } elseif (-not $isIp) { $n } else { $null } + -not ($setXExact.ContainsKey($n) -or ($short -and $setXShort.ContainsKey($short))) + }) + } + if ($ExcludeServerPrefix -and $ExcludeServerPrefix.Count -gt 0) { + $prefixesX = @($ExcludeServerPrefix | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim().ToLower() }) + if ($prefixesX.Count -gt 0) { + $ServersToAnalyze = @( + $ServersToAnalyze | Where-Object { + $n = ([string]$_).ToLower() + $short = if ($n -match '\.') { $n.Split('.')[0] } else { $n } + foreach ($p in $prefixesX) { + if ($short.StartsWith($p)) { return $false } + } + return $true + } + ) + } } Write-Verbose "Get-WinADDHCPSummary - Will perform detailed analysis on $($ServersToAnalyze.Count) specified servers" } @@ -391,6 +538,30 @@ } [void] $FailoverByServer[$key].Add($rel) } + + # Filter failover relationships for reporting/analysis to only include allowed servers + if ($DHCPServersFromAD -and $DHCPServersFromAD.Count -gt 0) { + $allowed = @{} + foreach ($s in $DHCPServersFromAD) { + if (-not $s -or -not $s.DnsName) { continue } + $n = ([string]$s.DnsName).Trim().ToLower() + if ($n) { $allowed[$n] = $true } + if ($n -match '\.') { + $short = $n.Split('.')[0] + if ($short) { $allowed[$short] = $true } + } + } + $filteredFailoverRelationships = New-Object System.Collections.Generic.List[object] + foreach ($rel in $DHCPSummary.FailoverRelationships) { + if (-not $rel) { continue } + $a = if ($rel.ServerName) { ([string]$rel.ServerName).Trim().ToLower() } else { $null } + $b = if ($rel.PartnerServer) { ([string]$rel.PartnerServer).Trim().ToLower() } else { $null } + if (($a -and $allowed.ContainsKey($a)) -and ($b -and $allowed.ContainsKey($b))) { + [void]$filteredFailoverRelationships.Add($rel) + } + } + $DHCPSummary.FailoverRelationships = $filteredFailoverRelationships + } } foreach ($DHCPServer in $DHCPServersFromAD) { diff --git a/Public/Show-WinADDHCPSummary.ps1 b/Public/Show-WinADDHCPSummary.ps1 index 1ddc3904..cb7aa477 100644 --- a/Public/Show-WinADDHCPSummary.ps1 +++ b/Public/Show-WinADDHCPSummary.ps1 @@ -74,10 +74,16 @@ Pass-through exclusion of components. Also auto-hides related tabs (e.g., excluding ScopeStatistics removes the Utilization tab). .PARAMETER IncludeServers - Only analyze these DHCP servers (filters discovery and detailed processing). + Only analyze these DHCP servers (case-insensitive). Matches exact FQDN or IP, and short names when short form is provided. .PARAMETER ExcludeServers - Exclude these DHCP servers from discovery/processing. + Exclude these DHCP servers (case-insensitive). Matches exact FQDN or IP, and short names when short form is provided. + + .PARAMETER IncludeServerPrefix + Only analyze DHCP servers whose short names start with these prefixes (case-insensitive). + + .PARAMETER ExcludeServerPrefix + Exclude DHCP servers whose short names start with these prefixes (case-insensitive). .PARAMETER IncludeScopeId Only process these IPv4 scope IDs across the queried servers. @@ -125,6 +131,11 @@ Deep-dive report for specific scope IDs on a single server with full per-scope details. + .EXAMPLE + Show-WinADDHCPSummary -IncludeServerPrefix "NYC","LON" -Minimal -Online + + Minimal report only for servers whose short names start with NYC or LON (case-insensitive). + .NOTES This function requires the DHCP PowerShell module and PSWriteHTML module for report generation. The generated report includes: @@ -168,6 +179,8 @@ [string[]] $ExcludeComponents, [alias('IncludeHost','IncludeDHCPServers')][string[]] $IncludeServers, [alias('ExcludeHost','ExcludeDHCPServers')][string[]] $ExcludeServers, + [string[]] $IncludeServerPrefix, + [string[]] $ExcludeServerPrefix, [alias('IncludeScope','IncludeScopes')][string[]] $IncludeScopeId, [alias('ExcludeScope','ExcludeScopes')][string[]] $ExcludeScopeId, [string[]] $IncludeTabs = @('Overview', 'IPv4/IPv6', 'Utilization', 'ValidationIssues', 'Infrastructure', 'Options&Classes', 'Failover', 'NetworkSegmentation', 'Performance', 'SecurityCompliance'), @@ -248,8 +261,10 @@ if ($AccurateUtilization) { $GetWinADDHCPSummarySplat.AccurateUtilization = $true } if ($IncludeComponents) { $GetWinADDHCPSummarySplat.IncludeComponents = $IncludeComponents } if ($ExcludeComponents) { $GetWinADDHCPSummarySplat.ExcludeComponents = $ExcludeComponents } - if ($IncludeServers) { $GetWinADDHCPSummarySplat.IncludeServers = $IncludeServers } - if ($ExcludeServers) { $GetWinADDHCPSummarySplat.ExcludeServers = $ExcludeServers } + if ($IncludeServers) { $GetWinADDHCPSummarySplat.IncludeServers = $IncludeServers } + if ($ExcludeServers) { $GetWinADDHCPSummarySplat.ExcludeServers = $ExcludeServers } + if ($IncludeServerPrefix) { $GetWinADDHCPSummarySplat.IncludeServerPrefix = $IncludeServerPrefix } + if ($ExcludeServerPrefix) { $GetWinADDHCPSummarySplat.ExcludeServerPrefix = $ExcludeServerPrefix } if ($IncludeScopeId) { $GetWinADDHCPSummarySplat.IncludeScopeId = $IncludeScopeId } if ($ExcludeScopeId) { $GetWinADDHCPSummarySplat.ExcludeScopeId = $ExcludeScopeId } diff --git a/Tests/DHCP.Failover.Tests.ps1 b/Tests/DHCP.Failover.Tests.ps1 index 618fdced..e4cf5a78 100644 --- a/Tests/DHCP.Failover.Tests.ps1 +++ b/Tests/DHCP.Failover.Tests.ps1 @@ -30,3 +30,21 @@ Describe 'DHCP Failover Analysis (TestMode)' { } } +Describe 'DHCP Server Prefix Filters (TestMode)' { + BeforeAll { + Import-Module "$PSScriptRoot/../ADEssentials.psm1" -Force + } + + It 'Filters servers by IncludeServerPrefix (case-insensitive)' { + $s = Get-WinADDHCPSummary -TestMode -Minimal -IncludeServerPrefix 'DHCP' + $s.Servers.Count | Should -Be 2 + $s.Servers.ServerName | Should -Not -Contain 'dc01.domain.com' + } + + It 'Filters servers by ExcludeServerPrefix and clears unrelated failover relationships' { + $s = Get-WinADDHCPSummary -TestMode -Minimal -ExcludeServerPrefix 'dhcp' + $s.Servers.Count | Should -Be 1 + $s.FailoverRelationships.Count | Should -Be 0 + } +} +