diff --git a/Private/ConvertTo-DHCPOptionIssueRecord.ps1 b/Private/ConvertTo-DHCPOptionIssueRecord.ps1 new file mode 100644 index 0000000..18da9a0 --- /dev/null +++ b/Private/ConvertTo-DHCPOptionIssueRecord.ps1 @@ -0,0 +1,71 @@ +function ConvertTo-DHCPOptionIssueRecord { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object] $Issue + ) + + if ($null -eq $Issue) { + return $null + } + + if ($Issue -is [string] -and [string]::IsNullOrWhiteSpace($Issue)) { + return $null + } + + if ($Issue -isnot [string]) { + $text = if ($Issue.PSObject.Properties['Issue']) { [string] $Issue.Issue } else { [string] $Issue } + $serverName = if ($Issue.PSObject.Properties['ServerName']) { [string] $Issue.ServerName } else { $null } + $scopeId = if ($Issue.PSObject.Properties['ScopeId']) { [string] $Issue.ScopeId } else { $null } + $category = if ($Issue.PSObject.Properties['Category']) { [string] $Issue.Category } else { 'Other' } + $recommendation = if ($Issue.PSObject.Properties['Recommendation']) { [string] $Issue.Recommendation } else { 'Review the DHCP option configuration and align it with the approved standard.' } + + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + + return [PSCustomObject]@{ + Category = $category + ServerName = $serverName + ScopeId = $scopeId + Details = $text + Recommendation = $recommendation + } + } + + $text = [string] $Issue + $category = 'Other' + $serverName = $null + $scopeId = $null + $recommendation = 'Review the DHCP option configuration and align it with the approved standard.' + + if ($text -match '^Public DNS servers configured in scope (?.+?) on (?.+)$') { + $category = 'Public DNS' + $scopeId = $Matches.scope + $serverName = $Matches.server + $recommendation = 'Replace public DNS servers with approved internal DNS servers or document the exception.' + } elseif ($text -match '^Very long lease time \((?\d+) hours\) in scope (?.+?) on (?.+)$') { + $category = 'Lease Time' + $scopeId = $Matches.scope + $serverName = $Matches.server + $recommendation = 'Reduce the lease duration to 168 hours or less unless the longer value is explicitly approved.' + } elseif ($text -match '^Empty domain name in scope (?.+?) on (?.+)$') { + $category = 'Domain Name' + $scopeId = $Matches.scope + $serverName = $Matches.server + $recommendation = 'Configure DHCP option 15 with the expected DNS domain name.' + } elseif ($text -match '^Invalid lease time format in scope (?.+?) on (?.+)$') { + $category = 'Lease Time Format' + $scopeId = $Matches.scope + $serverName = $Matches.server + $recommendation = 'Verify DHCP option 51 uses a valid numeric value expressed in seconds.' + } + + [PSCustomObject]@{ + Category = $category + ServerName = $serverName + ScopeId = $scopeId + Details = $text + Recommendation = $recommendation + } +} diff --git a/Private/Get-WinADDHCPScopeValidation.ps1 b/Private/Get-WinADDHCPScopeValidation.ps1 index 554967c..a6c8c57 100644 --- a/Private/Get-WinADDHCPScopeValidation.ps1 +++ b/Private/Get-WinADDHCPScopeValidation.ps1 @@ -1,67 +1,74 @@ -function Get-WinADDHCPScopeValidation { - [CmdletBinding()] - param( - [Object] $Scope, - [PSCustomObject] $ScopeObject - ) - +function Get-WinADDHCPScopeValidation { + [CmdletBinding()] + param( + [Object] $Scope, + [PSCustomObject] $ScopeObject + ) + # Validate scope configuration # Check lease duration (should not exceed 48 hours unless explicitly documented) if ($Scope.LeaseDuration.TotalHours -gt 48) { - # Check for documented exceptions (like V2 validator's "DHCP lease time" check) - if ($Scope.Description -notlike "*DHCP lease time*") { - # Use consistent string for both reports - $ScopeObject.Issues.Add("Lease duration exceeds 48 hours ($([Math]::Round($Scope.LeaseDuration.TotalHours, 1)) hours)") - $ScopeObject.Issues.Add("Lease duration greater than 48 hours") # For minimal report matching - $ScopeObject.HasIssues = $true - } + # Check for documented exceptions (like V2 validator's "DHCP lease time" check) + if ($Scope.Description -notlike "*DHCP lease time*") { + # Use consistent string for both reports + $ScopeObject.Issues.Add("Lease duration exceeds 48 hours ($([Math]::Round($Scope.LeaseDuration.TotalHours, 1)) hours)") + $ScopeObject.Issues.Add("Lease duration greater than 48 hours") # For minimal report matching + $ScopeObject.HasIssues = $true + } + } + + # PTR registration should stay enabled + if ($ScopeObject.DisableDnsPtrRRUpdate -eq $true) { + $ScopeObject.Issues.Add("DisableDnsPtrRRUpdate is enabled") + $ScopeObject.Issues.Add("PTR registration disabled") + $ScopeObject.HasIssues = $true } # Check for dynamic DNS updates with public DNS servers if ($ScopeObject.DNSSettings -and $ScopeObject.DNSSettings.DynamicUpdates -ne 'Never') { if ($ScopeObject.DNSServers) { - # Check for non-private DNS servers (V2 validator's ^10. check) - $DNSServerArray = $ScopeObject.DNSServers -split ',' | ForEach-Object { $_.Trim() } - $NonPrivateDNS = $DNSServerArray | Where-Object { - $_ -notmatch "^10\." -and - $_ -notmatch "^192\.168\." -and - $_ -notmatch "^172\.(1[6-9]|2[0-9]|3[0-1])\." - } - if ($NonPrivateDNS) { - $ScopeObject.Issues.Add("DNS updates enabled with non-private DNS servers: $($NonPrivateDNS -join ', ')") - $ScopeObject.Issues.Add("DNS updates enabled with public DNS servers") # For minimal report matching - $ScopeObject.HasIssues = $true - } - } - - # Enhanced DNS update validation (from V2 validator) - if (-not $ScopeObject.UpdateDnsRRForOlderClients -and -not $ScopeObject.DeleteDnsRROnLeaseExpiry) { - $ScopeObject.Issues.Add("Both UpdateDnsRRForOlderClients and DeleteDnsRROnLeaseExpiry are disabled") - $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching - $ScopeObject.HasIssues = $true - } elseif (-not $ScopeObject.UpdateDnsRRForOlderClients) { - $ScopeObject.Issues.Add("UpdateDnsRRForOlderClients is disabled") - $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching - $ScopeObject.HasIssues = $true - } elseif (-not $ScopeObject.DeleteDnsRROnLeaseExpiry) { - $ScopeObject.Issues.Add("DeleteDnsRROnLeaseExpiry is disabled") - $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching - $ScopeObject.HasIssues = $true - } - + # Check for non-private DNS servers (V2 validator's ^10. check) + $DNSServerArray = $ScopeObject.DNSServers -split ',' | ForEach-Object { $_.Trim() } + $NonPrivateDNS = $DNSServerArray | Where-Object { + $_ -notmatch "^10\." -and + $_ -notmatch "^192\.168\." -and + $_ -notmatch "^172\.(1[6-9]|2[0-9]|3[0-1])\." + } + if ($NonPrivateDNS) { + $ScopeObject.Issues.Add("DNS updates enabled with non-private DNS servers: $($NonPrivateDNS -join ', ')") + $ScopeObject.Issues.Add("DNS updates enabled with public DNS servers") # For minimal report matching + $ScopeObject.HasIssues = $true + } + } + + # Enhanced DNS update validation (from V2 validator) + if (-not $ScopeObject.UpdateDnsRRForOlderClients -and -not $ScopeObject.DeleteDnsRROnLeaseExpiry) { + $ScopeObject.Issues.Add("Both UpdateDnsRRForOlderClients and DeleteDnsRROnLeaseExpiry are disabled") + $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching + $ScopeObject.HasIssues = $true + } elseif (-not $ScopeObject.UpdateDnsRRForOlderClients) { + $ScopeObject.Issues.Add("UpdateDnsRRForOlderClients is disabled") + $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching + $ScopeObject.HasIssues = $true + } elseif (-not $ScopeObject.DeleteDnsRROnLeaseExpiry) { + $ScopeObject.Issues.Add("DeleteDnsRROnLeaseExpiry is disabled") + $ScopeObject.Issues.Add("DNS update settings misconfigured") # For minimal report matching + $ScopeObject.HasIssues = $true + } + if (-not $ScopeObject.DomainNameOption -or [string]::IsNullOrEmpty($ScopeObject.DomainNameOption)) { $ScopeObject.Issues.Add("Domain name option (015) is empty") $ScopeObject.Issues.Add("DNS updates enabled but missing domain name option") # For minimal report matching $ScopeObject.HasIssues = $true } - } - - # Check for missing failover configuration - if (-not $ScopeObject.FailoverPartner) { - $ScopeObject.Issues.Add("DHCP Failover not configured") - $ScopeObject.Issues.Add("Missing DHCP failover configuration") # For minimal report matching - $ScopeObject.HasIssues = $true - } - - return $ScopeObject.HasIssues -} \ No newline at end of file + } + + # Check for missing failover configuration + if (-not $ScopeObject.FailoverPartner) { + $ScopeObject.Issues.Add("DHCP Failover not configured") + $ScopeObject.Issues.Add("Missing DHCP failover configuration") # For minimal report matching + $ScopeObject.HasIssues = $true + } + + return $ScopeObject.HasIssues +} diff --git a/Private/Get-WinADDHCPValidationResults.ps1 b/Private/Get-WinADDHCPValidationResults.ps1 index 00d259d..f0f322d 100644 --- a/Private/Get-WinADDHCPValidationResults.ps1 +++ b/Private/Get-WinADDHCPValidationResults.ps1 @@ -1,197 +1,196 @@ -function Get-WinADDHCPValidationResults { - [CmdletBinding()] - param( - [System.Collections.IDictionary] $DHCPSummary, - [switch] $SkipScopeDetails, - [switch] $ConsiderMissingFailoverCritical, - [switch] $ConsiderDNSConfigCritical, - [switch] $IncludeServerAvailabilityIssues - ) - - Write-Verbose "Get-WinADDHCPValidationResults - Categorizing validation results" - - # Initialize validation collections - $PublicDNSWithUpdates = [System.Collections.Generic.List[Object]]::new() - $HighUtilization = [System.Collections.Generic.List[Object]]::new() - $ServersOffline = [System.Collections.Generic.List[Object]]::new() - $ServersDNSFailed = [System.Collections.Generic.List[Object]]::new() - $ServersPingFailed = [System.Collections.Generic.List[Object]]::new() - $ServersDHCPNotResponding = [System.Collections.Generic.List[Object]]::new() - $MissingFailover = [System.Collections.Generic.List[Object]]::new() - $FailoverOnlyOnPrimary = [System.Collections.Generic.List[Object]]::new() - $FailoverOnlyOnSecondary = [System.Collections.Generic.List[Object]]::new() - $FailoverMissingOnBoth = [System.Collections.Generic.List[Object]]::new() - $ExtendedLeaseDuration = [System.Collections.Generic.List[Object]]::new() - $ModerateUtilization = [System.Collections.Generic.List[Object]]::new() - $DNSRecordManagement = [System.Collections.Generic.List[Object]]::new() - $MissingDomainName = [System.Collections.Generic.List[Object]]::new() - $InactiveScopes = [System.Collections.Generic.List[Object]]::new() - - # Server availability categorization - foreach ($Server in $DHCPSummary.Servers) { - switch ($Server.Status) { - 'DNS resolution failed' { $ServersDNSFailed.Add($Server) } - 'DNS OK but unreachable' { $ServersPingFailed.Add($Server) } - 'Reachable but DHCP not responding' { $ServersDHCPNotResponding.Add($Server) } - } - } - if ($IncludeServerAvailabilityIssues) { - foreach ($s in $ServersDNSFailed) { $ServersOffline.Add($s) } - foreach ($s in $ServersPingFailed) { $ServersOffline.Add($s) } - foreach ($s in $ServersDHCPNotResponding) { $ServersOffline.Add($s) } - } - - # Scope issues categorization - foreach ($Scope in $DHCPSummary.ScopesWithIssues) { - foreach ($Issue in $Scope.Issues) { - if ($Issue -like "*public DNS servers*" -or $Issue -like "*non-private DNS servers*") { - if ($PublicDNSWithUpdates -notcontains $Scope) { $PublicDNSWithUpdates.Add($Scope) } - } - if ($Issue -like "*Failover not configured*") { - if ($MissingFailover -notcontains $Scope) { $MissingFailover.Add($Scope) } - } - if ($Issue -like "*exceeds 48 hours*") { - if ($ExtendedLeaseDuration -notcontains $Scope) { $ExtendedLeaseDuration.Add($Scope) } - } - if ($Issue -like "*UpdateDnsRRForOlderClients*" -or $Issue -like "*DeleteDnsRROnLeaseExpiry*") { +function Get-WinADDHCPValidationResults { + [CmdletBinding()] + param( + [System.Collections.IDictionary] $DHCPSummary, + [switch] $SkipScopeDetails, + [switch] $ConsiderMissingFailoverCritical, + [switch] $ConsiderDNSConfigCritical, + [switch] $IncludeServerAvailabilityIssues + ) + + Write-Verbose "Get-WinADDHCPValidationResults - Categorizing validation results" + + # Initialize validation collections + $PublicDNSWithUpdates = [System.Collections.Generic.List[Object]]::new() + $HighUtilization = [System.Collections.Generic.List[Object]]::new() + $ServersOffline = [System.Collections.Generic.List[Object]]::new() + $ServersDNSFailed = [System.Collections.Generic.List[Object]]::new() + $ServersPingFailed = [System.Collections.Generic.List[Object]]::new() + $ServersDHCPNotResponding = [System.Collections.Generic.List[Object]]::new() + $MissingFailover = [System.Collections.Generic.List[Object]]::new() + $FailoverOnlyOnPrimary = [System.Collections.Generic.List[Object]]::new() + $FailoverOnlyOnSecondary = [System.Collections.Generic.List[Object]]::new() + $FailoverMissingOnBoth = [System.Collections.Generic.List[Object]]::new() + $ExtendedLeaseDuration = [System.Collections.Generic.List[Object]]::new() + $ModerateUtilization = [System.Collections.Generic.List[Object]]::new() + $DNSRecordManagement = [System.Collections.Generic.List[Object]]::new() + $MissingDomainName = [System.Collections.Generic.List[Object]]::new() + $InactiveScopes = [System.Collections.Generic.List[Object]]::new() + + # Server availability categorization + foreach ($Server in $DHCPSummary.Servers) { + switch ($Server.Status) { + 'DNS resolution failed' { $ServersDNSFailed.Add($Server) } + 'DNS OK but unreachable' { $ServersPingFailed.Add($Server) } + 'Reachable but DHCP not responding' { $ServersDHCPNotResponding.Add($Server) } + } + } + if ($IncludeServerAvailabilityIssues) { + foreach ($s in $ServersDNSFailed) { $ServersOffline.Add($s) } + foreach ($s in $ServersPingFailed) { $ServersOffline.Add($s) } + foreach ($s in $ServersDHCPNotResponding) { $ServersOffline.Add($s) } + } + + # Scope issues categorization + foreach ($Scope in $DHCPSummary.ScopesWithIssues) { + foreach ($Issue in $Scope.Issues) { + if ($Issue -like "*public DNS servers*" -or $Issue -like "*non-private DNS servers*") { + if ($PublicDNSWithUpdates -notcontains $Scope) { $PublicDNSWithUpdates.Add($Scope) } + } + if ($Issue -like "*Failover not configured*") { + if ($MissingFailover -notcontains $Scope) { $MissingFailover.Add($Scope) } + } + if ($Issue -like "*exceeds 48 hours*") { + if ($ExtendedLeaseDuration -notcontains $Scope) { $ExtendedLeaseDuration.Add($Scope) } + } + if ($Issue -like "*UpdateDnsRRForOlderClients*" -or $Issue -like "*DeleteDnsRROnLeaseExpiry*" -or $Issue -like "*DisableDnsPtrRRUpdate*" -or $Issue -like "*PTR registration disabled*") { if ($DNSRecordManagement -notcontains $Scope) { $DNSRecordManagement.Add($Scope) } } - if ($Issue -like "*Domain name option*") { - if ($MissingDomainName -notcontains $Scope) { $MissingDomainName.Add($Scope) } - } - } - } - - # Failover mismatches from precomputed analysis - if ($DHCPSummary.FailoverAnalysis) { - if ($DHCPSummary.FailoverAnalysis.OnlyOnPrimary) { foreach ($i in $DHCPSummary.FailoverAnalysis.OnlyOnPrimary) { $FailoverOnlyOnPrimary.Add($i) } } - if ($DHCPSummary.FailoverAnalysis.OnlyOnSecondary) { foreach ($i in $DHCPSummary.FailoverAnalysis.OnlyOnSecondary) { $FailoverOnlyOnSecondary.Add($i) } } - if ($DHCPSummary.FailoverAnalysis.MissingOnBoth) { foreach ($i in $DHCPSummary.FailoverAnalysis.MissingOnBoth) { $FailoverMissingOnBoth.Add($i) } } - } - - # Inactive scopes - foreach ($Scope in $DHCPSummary.Scopes) { if ($Scope.State -eq 'Inactive') { $InactiveScopes.Add($Scope) } } - - # Utilization checks - if (-not $SkipScopeDetails) { - foreach ($Scope in $DHCPSummary.Scopes) { - if ($Scope.State -eq 'Active') { - if ($Scope.PercentageInUse -gt 90) { $HighUtilization.Add($Scope) } - elseif ($Scope.PercentageInUse -gt 75) { $ModerateUtilization.Add($Scope) } - } - } - } else { - Write-Verbose "Get-WinADDHCPValidationResults - Utilization validations skipped due to SkipScopeDetails parameter" - } - - # Build result structure - $ValidationResults = [ordered] @{ - CriticalIssues = [ordered] @{ - PublicDNSWithUpdates = $PublicDNSWithUpdates - DNSConfigurationProblems = @() - ServersOffline = $ServersOffline - ServersDNSFailed = $ServersDNSFailed - ServersPingFailed = $ServersPingFailed - ServersDHCPNotResponding = $ServersDHCPNotResponding - # Reclassified failover risks per request - FailoverOnlyOnPrimary = $FailoverOnlyOnPrimary # "missing on secondary" => critical - FailoverMissingOnBoth = $FailoverMissingOnBoth # "missing on both" => critical - } - UtilizationIssues = [ordered] @{ - HighUtilization = $HighUtilization - ModerateUtilization = $ModerateUtilization - } - WarningIssues = [ordered] @{ - MissingFailover = $MissingFailover - # Keep as warning: "missing on primary" => present only on secondary - FailoverOnlyOnSecondary = $FailoverOnlyOnSecondary - ExtendedLeaseDuration = $ExtendedLeaseDuration - DNSRecordManagement = $DNSRecordManagement - } - InfoIssues = [ordered] @{ - MissingDomainName = $MissingDomainName - InactiveScopes = $InactiveScopes - } - Summary = [ordered] @{ - TotalCriticalIssues = 0 - TotalUtilizationIssues = 0 - TotalWarningIssues = 0 - TotalInfoIssues = 0 - ScopesWithCritical = 0 - ScopesWithUtilization = 0 - ScopesWithWarnings = 0 - ScopesWithInfo = 0 - } - } - - # Escalate DNS config issues to critical when requested (readable & efficient) - if ($ConsiderDNSConfigCritical) { - # Aggregate - $dnsAgg = New-Object 'System.Collections.Generic.List[object]' - if ($PublicDNSWithUpdates -and $PublicDNSWithUpdates.Count -gt 0) { [void] $dnsAgg.AddRange($PublicDNSWithUpdates) } - if ($DNSRecordManagement -and $DNSRecordManagement.Count -gt 0) { [void] $dnsAgg.AddRange($DNSRecordManagement) } - if ($MissingDomainName -and $MissingDomainName.Count -gt 0) { [void] $dnsAgg.AddRange($MissingDomainName) } - - # Deduplicate by (ServerName|ScopeId) - $seen = @{} + if ($Issue -like "*Domain name option*") { + if ($MissingDomainName -notcontains $Scope) { $MissingDomainName.Add($Scope) } + } + } + } + + # Failover mismatches from precomputed analysis + if ($DHCPSummary.FailoverAnalysis) { + if ($DHCPSummary.FailoverAnalysis.OnlyOnPrimary) { foreach ($i in $DHCPSummary.FailoverAnalysis.OnlyOnPrimary) { $FailoverOnlyOnPrimary.Add($i) } } + if ($DHCPSummary.FailoverAnalysis.OnlyOnSecondary) { foreach ($i in $DHCPSummary.FailoverAnalysis.OnlyOnSecondary) { $FailoverOnlyOnSecondary.Add($i) } } + if ($DHCPSummary.FailoverAnalysis.MissingOnBoth) { foreach ($i in $DHCPSummary.FailoverAnalysis.MissingOnBoth) { $FailoverMissingOnBoth.Add($i) } } + } + + # Inactive scopes + foreach ($Scope in $DHCPSummary.Scopes) { if ($Scope.State -eq 'Inactive') { $InactiveScopes.Add($Scope) } } + + # Utilization checks + if (-not $SkipScopeDetails) { + foreach ($Scope in $DHCPSummary.Scopes) { + if ($Scope.State -eq 'Active') { + if ($Scope.PercentageInUse -gt 90) { $HighUtilization.Add($Scope) } + elseif ($Scope.PercentageInUse -gt 75) { $ModerateUtilization.Add($Scope) } + } + } + } else { + Write-Verbose "Get-WinADDHCPValidationResults - Utilization validations skipped due to SkipScopeDetails parameter" + } + + # Build result structure + $ValidationResults = [ordered] @{ + CriticalIssues = [ordered] @{ + PublicDNSWithUpdates = $PublicDNSWithUpdates + DNSConfigurationProblems = @() + ServersOffline = $ServersOffline + ServersDNSFailed = $ServersDNSFailed + ServersPingFailed = $ServersPingFailed + ServersDHCPNotResponding = $ServersDHCPNotResponding + # Reclassified failover risks per request + FailoverOnlyOnPrimary = $FailoverOnlyOnPrimary # "missing on secondary" => critical + FailoverMissingOnBoth = $FailoverMissingOnBoth # "missing on both" => critical + } + UtilizationIssues = [ordered] @{ + HighUtilization = $HighUtilization + ModerateUtilization = $ModerateUtilization + } + WarningIssues = [ordered] @{ + MissingFailover = $MissingFailover + # Keep as warning: "missing on primary" => present only on secondary + FailoverOnlyOnSecondary = $FailoverOnlyOnSecondary + ExtendedLeaseDuration = $ExtendedLeaseDuration + DNSRecordManagement = $DNSRecordManagement + } + InfoIssues = [ordered] @{ + MissingDomainName = $MissingDomainName + InactiveScopes = $InactiveScopes + } + Summary = [ordered] @{ + TotalCriticalIssues = 0 + TotalUtilizationIssues = 0 + TotalWarningIssues = 0 + TotalInfoIssues = 0 + ScopesWithCritical = 0 + ScopesWithUtilization = 0 + ScopesWithWarnings = 0 + ScopesWithInfo = 0 + } + } + + # Escalate DNS config issues to critical when requested (readable & efficient) + if ($ConsiderDNSConfigCritical) { + # Aggregate + $dnsAgg = New-Object 'System.Collections.Generic.List[object]' + if ($PublicDNSWithUpdates -and $PublicDNSWithUpdates.Count -gt 0) { [void] $dnsAgg.AddRange($PublicDNSWithUpdates) } + if ($DNSRecordManagement -and $DNSRecordManagement.Count -gt 0) { [void] $dnsAgg.AddRange($DNSRecordManagement) } + if ($MissingDomainName -and $MissingDomainName.Count -gt 0) { [void] $dnsAgg.AddRange($MissingDomainName) } + + # Deduplicate by (ServerName|ScopeId) + $seen = [System.Collections.Generic.HashSet[string]]::new() $dnsUnique = New-Object 'System.Collections.Generic.List[object]' foreach ($item in $dnsAgg) { $id = "$($item.ServerName)|$($item.ScopeId)" - if (-not $seen.ContainsKey($id)) { - $seen[$id] = $true + if ($seen.Add($id)) { [void] $dnsUnique.Add($item) } } - - $ValidationResults.CriticalIssues.DNSConfigurationProblems = $dnsUnique - # Prevent double-counting when summarizing - $DNSRecordManagement = [System.Collections.Generic.List[Object]]::new() - $MissingDomainName = [System.Collections.Generic.List[Object]]::new() - } - - # Counters - $ValidationResults.Summary.TotalCriticalIssues = ( - $ValidationResults.CriticalIssues.PublicDNSWithUpdates.Count + - $ValidationResults.CriticalIssues.DNSConfigurationProblems.Count + - $ValidationResults.CriticalIssues.ServersOffline.Count + - $ValidationResults.CriticalIssues.FailoverOnlyOnPrimary.Count + - $ValidationResults.CriticalIssues.FailoverMissingOnBoth.Count + - $(if ($ConsiderMissingFailoverCritical) { $MissingFailover.Count } else { 0 }) - ) - - $ValidationResults.Summary.TotalUtilizationIssues = ($HighUtilization.Count + $ModerateUtilization.Count) - - $ValidationResults.Summary.TotalWarningIssues = ( - $(if ($ConsiderMissingFailoverCritical) { 0 } else { $MissingFailover.Count }) + - $FailoverOnlyOnSecondary.Count + - $ExtendedLeaseDuration.Count + - $DNSRecordManagement.Count - ) - - $ValidationResults.Summary.TotalInfoIssues = ($MissingDomainName.Count + $InactiveScopes.Count) - - # Unique scope counters - $CriticalScopes = @( - $ValidationResults.CriticalIssues.PublicDNSWithUpdates; - $ValidationResults.CriticalIssues.DNSConfigurationProblems; - $ValidationResults.CriticalIssues.FailoverOnlyOnPrimary; - $ValidationResults.CriticalIssues.FailoverMissingOnBoth - ) - $ValidationResults.Summary.ScopesWithCritical = ($CriticalScopes | Sort-Object -Property ScopeId -Unique).Count - - $UtilizationScopes = @($HighUtilization; $ModerateUtilization) - $ValidationResults.Summary.ScopesWithUtilization = ($UtilizationScopes | Sort-Object -Property ScopeId -Unique).Count - - $WarningScopes = @( - $(if ($ConsiderMissingFailoverCritical) { @() } else { $MissingFailover }) - $FailoverOnlyOnSecondary - $ExtendedLeaseDuration - $DNSRecordManagement - ) - $ValidationResults.Summary.ScopesWithWarnings = ($WarningScopes | Sort-Object -Property ScopeId -Unique).Count - - $InfoScopes = @($MissingDomainName; $InactiveScopes) - $ValidationResults.Summary.ScopesWithInfo = ($InfoScopes | Sort-Object -Property ScopeId -Unique).Count - - return $ValidationResults -} + + $ValidationResults.CriticalIssues.DNSConfigurationProblems = $dnsUnique + # Prevent double-counting when summarizing + $DNSRecordManagement = [System.Collections.Generic.List[Object]]::new() + $MissingDomainName = [System.Collections.Generic.List[Object]]::new() + } + + # Counters + $ValidationResults.Summary.TotalCriticalIssues = ( + $ValidationResults.CriticalIssues.PublicDNSWithUpdates.Count + + $ValidationResults.CriticalIssues.DNSConfigurationProblems.Count + + $ValidationResults.CriticalIssues.ServersOffline.Count + + $ValidationResults.CriticalIssues.FailoverOnlyOnPrimary.Count + + $ValidationResults.CriticalIssues.FailoverMissingOnBoth.Count + + $(if ($ConsiderMissingFailoverCritical) { $MissingFailover.Count } else { 0 }) + ) + + $ValidationResults.Summary.TotalUtilizationIssues = ($HighUtilization.Count + $ModerateUtilization.Count) + + $ValidationResults.Summary.TotalWarningIssues = ( + $(if ($ConsiderMissingFailoverCritical) { 0 } else { $MissingFailover.Count }) + + $FailoverOnlyOnSecondary.Count + + $ExtendedLeaseDuration.Count + + $DNSRecordManagement.Count + ) + + $ValidationResults.Summary.TotalInfoIssues = ($MissingDomainName.Count + $InactiveScopes.Count) + + # Unique scope counters + $CriticalScopes = @( + $ValidationResults.CriticalIssues.PublicDNSWithUpdates; + $ValidationResults.CriticalIssues.DNSConfigurationProblems; + $ValidationResults.CriticalIssues.FailoverOnlyOnPrimary; + $ValidationResults.CriticalIssues.FailoverMissingOnBoth + ) + $ValidationResults.Summary.ScopesWithCritical = ($CriticalScopes | Sort-Object -Property ScopeId -Unique).Count + + $UtilizationScopes = @($HighUtilization; $ModerateUtilization) + $ValidationResults.Summary.ScopesWithUtilization = ($UtilizationScopes | Sort-Object -Property ScopeId -Unique).Count + + $WarningScopes = @( + $(if ($ConsiderMissingFailoverCritical) { @() } else { $MissingFailover }) + $FailoverOnlyOnSecondary + $ExtendedLeaseDuration + $DNSRecordManagement + ) + $ValidationResults.Summary.ScopesWithWarnings = ($WarningScopes | Sort-Object -Property ScopeId -Unique).Count + + $InfoScopes = @($MissingDomainName; $InactiveScopes) + $ValidationResults.Summary.ScopesWithInfo = ($InfoScopes | Sort-Object -Property ScopeId -Unique).Count + + return $ValidationResults +} diff --git a/Private/New-DHCPFailoverTab.ps1 b/Private/New-DHCPFailoverTab.ps1 index 5e7e229..0da2150 100644 --- a/Private/New-DHCPFailoverTab.ps1 +++ b/Private/New-DHCPFailoverTab.ps1 @@ -1,148 +1,148 @@ -function New-DHCPFailoverTab { - <# - .SYNOPSIS - Creates the Failover tab content for DHCP HTML report. - - .DESCRIPTION - This private function generates the Failover tab which focuses on high availability, - failover relationships, and redundancy analysis. - - .PARAMETER DHCPData - The DHCP data object containing all server and scope information. - - .OUTPUTS - New-HTMLTab object containing the Failover tab content. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [hashtable] $DHCPData - ) - - New-HTMLTab -TabName 'Failover' { - # Failover Overview at the top - New-HTMLSection -HeaderText "🔄 High Availability Overview" { - New-HTMLPanel -Invisible { - # Calculate failover statistics - $TotalFailoverRelationships = $DHCPData.FailoverRelationships.Count - $ActiveFailovers = ($DHCPData.FailoverRelationships | Where-Object { $_.State -eq 'Normal' }).Count - $FailoverIssues = ($DHCPData.FailoverRelationships | Where-Object { $_.State -ne 'Normal' }).Count - $ScopesWithFailover = ($DHCPData.Scopes | Where-Object { $null -ne $_.FailoverPartner -and $_.FailoverPartner -ne '' }).Count - $ScopesWithoutFailover = ($DHCPData.Scopes | Where-Object { $_.State -eq 'Active' -and ($null -eq $_.FailoverPartner -or $_.FailoverPartner -eq '') }).Count - - # Failover enumeration issues (from centralized error log) - $FailoverEnumWarnings = @($DHCPData.Warnings | Where-Object { $_.Component -eq 'Failover Relationships' -and $_.Operation -eq 'Get-DhcpServerv4Failover' }) - $FailoverEnumErrors = @($DHCPData.Errors | Where-Object { $_.Component -eq 'Failover Relationships' -and $_.Operation -eq 'Get-DhcpServerv4Failover' }) - - New-HTMLSection -HeaderText "Failover Health Dashboard" -Invisible -Density Compact { - New-HTMLInfoCard -Title "Failover Relationships" -Number $TotalFailoverRelationships -Subtitle "Configured" -Icon "🔄" -TitleColor Blue -NumberColor DarkBlue - New-HTMLInfoCard -Title "Active Failovers" -Number $ActiveFailovers -Subtitle "Normal State" -Icon "✅" -TitleColor Green -NumberColor DarkGreen - New-HTMLInfoCard -Title "Failover Issues" -Number $FailoverIssues -Subtitle "Need Attention" -Icon "⚠️" -TitleColor $(if ($FailoverIssues -gt 0) { "Red" } else { "Green" }) -NumberColor $(if ($FailoverIssues -gt 0) { "DarkRed" } else { "DarkGreen" }) - New-HTMLInfoCard -Title "Unprotected Scopes" -Number $ScopesWithoutFailover -Subtitle "No Failover" -Icon "🚨" -TitleColor $(if ($ScopesWithoutFailover -gt 0) { "Orange" } else { "Green" }) -NumberColor $(if ($ScopesWithoutFailover -gt 0) { "DarkOrange" } else { "DarkGreen" }) - if ($DHCPData.FailoverAnalysis) { - New-HTMLInfoCard -Title "Missing on Partner B" -Number $($DHCPData.FailoverAnalysis.OnlyOnPrimary.Count) -Subtitle "Scopes assigned on A only" -Icon "🟠" -TitleColor 'DarkOrange' -NumberColor 'DarkOrange' - New-HTMLInfoCard -Title "Missing on Partner A" -Number $($DHCPData.FailoverAnalysis.OnlyOnSecondary.Count) -Subtitle "Scopes assigned on B only" -Icon "🟠" -TitleColor 'DarkOrange' -NumberColor 'DarkOrange' - New-HTMLInfoCard -Title "Missing on Both" -Number $($DHCPData.FailoverAnalysis.MissingOnBoth.Count) -Subtitle "Gap" -Icon "⚠️" -TitleColor 'OrangeRed' -NumberColor 'OrangeRed' - if ($FailoverEnumWarnings.Count -gt 0 -or $FailoverEnumErrors.Count -gt 0) { - $warnColor = if ($FailoverEnumWarnings.Count -gt 0) { 'Orange' } else { 'Green' } - $errColor = if ($FailoverEnumErrors.Count -gt 0) { 'Red' } else { 'Green' } - New-HTMLInfoCard -Title "Enum Warnings" -Number $FailoverEnumWarnings.Count -Subtitle "Get-DhcpServerv4Failover" -Icon "⚠️" -TitleColor $warnColor -NumberColor $warnColor - New-HTMLInfoCard -Title "Enum Errors" -Number $FailoverEnumErrors.Count -Subtitle "Get-DhcpServerv4Failover" -Icon "❌" -TitleColor $errColor -NumberColor $errColor - } - } - } - - # Failover coverage chart - if ($DHCPData.Scopes.Count -gt 0) { - New-HTMLChart -Title "Failover Coverage Analysis" { - New-ChartPie -Name "With Failover" -Value $ScopesWithFailover -Color '#00FF00' - New-ChartPie -Name "Without Failover" -Value $ScopesWithoutFailover -Color '#FF6347' - } -Height 300 - } - } - } - - # Normalized (deduped) view per partner pair for clarity - New-HTMLSection -HeaderText "🤝 Failover Relationships (normalized per partner pair)" -CanCollapse { - $rels = $DHCPData.FailoverRelationships - $pairs = @{} - foreach ($rel in $rels) { - # Canonicalize names (prefer FQDN) to avoid short/FQDN mismatches - $a = Resolve-DHCPServerName -Name $rel.ServerName -DHCPSummary $DHCPData - $b = Resolve-DHCPServerName -Name $rel.PartnerServer -DHCPSummary $DHCPData - $sorted = @($a, $b) | Sort-Object - $key = $sorted -join '↔' - if (-not $pairs.ContainsKey($key)) { - $pairs[$key] = [ordered]@{ - NameSet = New-Object System.Collections.Generic.HashSet[string] - ServerA = $sorted[0] - ServerB = $sorted[1] - Modes = New-Object System.Collections.Generic.HashSet[string] - States = New-Object System.Collections.Generic.HashSet[string] - ScopesUnion = New-Object System.Collections.Generic.HashSet[string] - Sources = New-Object System.Collections.Generic.HashSet[string] - } - } - if ($rel.Name) { [void]$pairs[$key].NameSet.Add([string]$rel.Name) } - if ($rel.Mode) { [void]$pairs[$key].Modes.Add([string]$rel.Mode) } - if ($rel.State) { [void]$pairs[$key].States.Add([string]$rel.State) } - foreach ($sid in @($rel.ScopeId)) { if ($sid) { [void]$pairs[$key].ScopesUnion.Add(([string]$sid).Trim()) } } - if ($rel.GatheredFrom) { [void]$pairs[$key].Sources.Add((([string]$rel.GatheredFrom).Trim().ToLower())) } - } - - $FailoverSummary = foreach ($p in $pairs.Values) { - $state = if ($p.States.Count -eq 1) { @($p.States)[0] } elseif ($p.States.Count -eq 0) { '' } else { 'Mixed' } - $complete = ($p.Sources.Contains($p.ServerA) -and $p.Sources.Contains($p.ServerB)) - $dataSrc = if ($complete) { 'Both partners' } elseif ($p.Sources.Contains($p.ServerA)) { "Only $($p.ServerA)" } elseif ($p.Sources.Contains($p.ServerB)) { "Only $($p.ServerB)" } else { 'Unknown' } - [PSCustomObject]@{ - Name = (@($p.NameSet) -join ', ') - PartnerA = $p.ServerA - PartnerB = $p.ServerB - Mode = (@($p.Modes) -join ', ') - State = $state - ScopeCount = @($p.ScopesUnion).Count - DataSource = $dataSrc - } - } - - New-HTMLTable -DataTable $FailoverSummary -Filtering { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Normal' -BackgroundColor LightGreen -FailBackgroundColor Orange - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator ne -Value 'Normal' -BackgroundColor Yellow - New-HTMLTableCondition -Name 'ScopeCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'DataSource' -ComparisonType string -Operator like -Value 'Only *' -BackgroundColor LightYellow - } -DataStore JavaScript -ScrollX -Title "Failover Relationships by Partner Pair" - } - - # Consolidated per-subnet failover issues (simplified view) - if ($DHCPData.FailoverAnalysis -and $DHCPData.FailoverAnalysis.PerSubnetIssues -and $DHCPData.FailoverAnalysis.PerSubnetIssues.Count -gt 0) { - New-HTMLSection -HeaderText "🚦 Per-Subnet Failover Issues" { - $perSubnet = $DHCPData.FailoverAnalysis.PerSubnetIssues | ForEach-Object { - [PSCustomObject]@{ - ScopeId = $_.ScopeId - PartnerA = $_.PrimaryServer - PartnerB = $_.SecondaryServer - Relationship = if ($_.Relationship) { $_.Relationship } else { '' } - Status = $_.Issue - } - } - New-HTMLTable -DataTable $perSubnet -ScrollX -Filtering { - # Status now includes server names (e.g., "Missing on dhcp01") - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator like -Value 'Missing on *' -BackgroundColor Orange - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Missing from both partners' -BackgroundColor Salmon -Color White - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'No failover configured' -BackgroundColor Salmon -Color White - } -Title 'Subnets requiring attention' - } - } - - # Stale failover relationships (no subnets) - if ($DHCPData.FailoverAnalysis -and $DHCPData.FailoverAnalysis.StaleRelationships -and $DHCPData.FailoverAnalysis.StaleRelationships.Count -gt 0) { - New-HTMLSection -HeaderText "🧹 Stale Failover Relationships (no subnets)" { - New-HTMLTable -DataTable $DHCPData.FailoverAnalysis.StaleRelationships -ScrollX -Filtering { - New-HTMLTableCondition -Name 'ScopeCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor Yellow - } - } - } - +function New-DHCPFailoverTab { + <# + .SYNOPSIS + Creates the Failover tab content for DHCP HTML report. + + .DESCRIPTION + This private function generates the Failover tab which focuses on high availability, + failover relationships, and redundancy analysis. + + .PARAMETER DHCPData + The DHCP data object containing all server and scope information. + + .OUTPUTS + New-HTMLTab object containing the Failover tab content. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable] $DHCPData + ) + + New-HTMLTab -TabName 'Failover' { + # Failover Overview at the top + New-HTMLSection -HeaderText "🔄 High Availability Overview" { + New-HTMLPanel -Invisible { + # Calculate failover statistics + $TotalFailoverRelationships = $DHCPData.FailoverRelationships.Count + $ActiveFailovers = ($DHCPData.FailoverRelationships | Where-Object { $_.State -eq 'Normal' }).Count + $FailoverIssues = ($DHCPData.FailoverRelationships | Where-Object { $_.State -ne 'Normal' }).Count + $ScopesWithFailover = ($DHCPData.Scopes | Where-Object { $null -ne $_.FailoverPartner -and $_.FailoverPartner -ne '' }).Count + $ScopesWithoutFailover = ($DHCPData.Scopes | Where-Object { $_.State -eq 'Active' -and ($null -eq $_.FailoverPartner -or $_.FailoverPartner -eq '') }).Count + + # Failover enumeration issues (from centralized error log) + $FailoverEnumWarnings = @($DHCPData.Warnings | Where-Object { $_.Component -eq 'Failover Relationships' -and $_.Operation -eq 'Get-DhcpServerv4Failover' }) + $FailoverEnumErrors = @($DHCPData.Errors | Where-Object { $_.Component -eq 'Failover Relationships' -and $_.Operation -eq 'Get-DhcpServerv4Failover' }) + + New-HTMLSection -HeaderText "Failover Health Dashboard" -Invisible -Density Compact { + New-HTMLInfoCard -Title "Failover Relationships" -Number $TotalFailoverRelationships -Subtitle "Configured" -Icon "🔄" -TitleColor Blue -NumberColor DarkBlue + New-HTMLInfoCard -Title "Active Failovers" -Number $ActiveFailovers -Subtitle "Normal State" -Icon "✅" -TitleColor Green -NumberColor DarkGreen + New-HTMLInfoCard -Title "Failover Issues" -Number $FailoverIssues -Subtitle "Need Attention" -Icon "⚠️" -TitleColor $(if ($FailoverIssues -gt 0) { "Red" } else { "Green" }) -NumberColor $(if ($FailoverIssues -gt 0) { "DarkRed" } else { "DarkGreen" }) + New-HTMLInfoCard -Title "Unprotected Scopes" -Number $ScopesWithoutFailover -Subtitle "No Failover" -Icon "🚨" -TitleColor $(if ($ScopesWithoutFailover -gt 0) { "Orange" } else { "Green" }) -NumberColor $(if ($ScopesWithoutFailover -gt 0) { "DarkOrange" } else { "DarkGreen" }) + if ($DHCPData.FailoverAnalysis) { + New-HTMLInfoCard -Title "Missing on Partner B" -Number $($DHCPData.FailoverAnalysis.OnlyOnPrimary.Count) -Subtitle "Scopes assigned on A only" -Icon "🟠" -TitleColor 'DarkOrange' -NumberColor 'DarkOrange' + New-HTMLInfoCard -Title "Missing on Partner A" -Number $($DHCPData.FailoverAnalysis.OnlyOnSecondary.Count) -Subtitle "Scopes assigned on B only" -Icon "🟠" -TitleColor 'DarkOrange' -NumberColor 'DarkOrange' + New-HTMLInfoCard -Title "Missing on Both" -Number $($DHCPData.FailoverAnalysis.MissingOnBoth.Count) -Subtitle "Gap" -Icon "⚠️" -TitleColor 'OrangeRed' -NumberColor 'OrangeRed' + if ($FailoverEnumWarnings.Count -gt 0 -or $FailoverEnumErrors.Count -gt 0) { + $warnColor = if ($FailoverEnumWarnings.Count -gt 0) { 'Orange' } else { 'Green' } + $errColor = if ($FailoverEnumErrors.Count -gt 0) { 'Red' } else { 'Green' } + New-HTMLInfoCard -Title "Enum Warnings" -Number $FailoverEnumWarnings.Count -Subtitle "Get-DhcpServerv4Failover" -Icon "⚠️" -TitleColor $warnColor -NumberColor $warnColor + New-HTMLInfoCard -Title "Enum Errors" -Number $FailoverEnumErrors.Count -Subtitle "Get-DhcpServerv4Failover" -Icon "❌" -TitleColor $errColor -NumberColor $errColor + } + } + } + + # Failover coverage chart + if ($DHCPData.Scopes.Count -gt 0) { + New-HTMLChart -Title "Failover Coverage Analysis" { + New-ChartPie -Name "With Failover" -Value $ScopesWithFailover -Color '#00FF00' + New-ChartPie -Name "Without Failover" -Value $ScopesWithoutFailover -Color '#FF6347' + } -Height 300 + } + } + } + + # Normalized (deduped) view per partner pair for clarity + New-HTMLSection -HeaderText "🤝 Failover Relationships (normalized per partner pair)" -CanCollapse { + $rels = $DHCPData.FailoverRelationships + $pairs = @{} + foreach ($rel in $rels) { + # Canonicalize names (prefer FQDN) to avoid short/FQDN mismatches + $a = Resolve-DHCPServerName -Name $rel.ServerName -DHCPSummary $DHCPData + $b = Resolve-DHCPServerName -Name $rel.PartnerServer -DHCPSummary $DHCPData + $sorted = @($a, $b) | Sort-Object + $key = $sorted -join '↔' + if (-not $pairs.ContainsKey($key)) { + $pairs[$key] = [ordered]@{ + NameSet = New-Object System.Collections.Generic.HashSet[string] + ServerA = $sorted[0] + ServerB = $sorted[1] + Modes = New-Object System.Collections.Generic.HashSet[string] + States = New-Object System.Collections.Generic.HashSet[string] + ScopesUnion = New-Object System.Collections.Generic.HashSet[string] + Sources = New-Object System.Collections.Generic.HashSet[string] + } + } + if ($rel.Name) { [void]$pairs[$key].NameSet.Add([string]$rel.Name) } + if ($rel.Mode) { [void]$pairs[$key].Modes.Add([string]$rel.Mode) } + if ($rel.State) { [void]$pairs[$key].States.Add([string]$rel.State) } + foreach ($sid in @($rel.ScopeId)) { if ($sid) { [void]$pairs[$key].ScopesUnion.Add(([string]$sid).Trim()) } } + if ($rel.GatheredFrom) { [void]$pairs[$key].Sources.Add((([string]$rel.GatheredFrom).Trim().ToLower())) } + } + + $FailoverSummary = foreach ($p in $pairs.Values) { + $state = if ($p.States.Count -eq 1) { @($p.States)[0] } elseif ($p.States.Count -eq 0) { '' } else { 'Mixed' } + $complete = ($p.Sources.Contains($p.ServerA) -and $p.Sources.Contains($p.ServerB)) + $dataSrc = if ($complete) { 'Both partners' } elseif ($p.Sources.Contains($p.ServerA)) { "Only $($p.ServerA)" } elseif ($p.Sources.Contains($p.ServerB)) { "Only $($p.ServerB)" } else { 'Unknown' } + [PSCustomObject]@{ + Name = (@($p.NameSet) -join ', ') + PartnerA = $p.ServerA + PartnerB = $p.ServerB + Mode = (@($p.Modes) -join ', ') + State = $state + ScopeCount = @($p.ScopesUnion).Count + DataSource = $dataSrc + } + } + + New-HTMLTable -DataTable $FailoverSummary -Filtering { + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Normal' -BackgroundColor LightGreen -FailBackgroundColor Orange + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator ne -Value 'Normal' -BackgroundColor Yellow + New-HTMLTableCondition -Name 'ScopeCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'DataSource' -ComparisonType string -Operator like -Value 'Only *' -BackgroundColor LightYellow + } -DataStore JavaScript -ScrollX -Title "Failover Relationships by Partner Pair" + } + + # Consolidated per-subnet failover issues (simplified view) + if ($DHCPData.FailoverAnalysis -and $DHCPData.FailoverAnalysis.PerSubnetIssues -and $DHCPData.FailoverAnalysis.PerSubnetIssues.Count -gt 0) { + New-HTMLSection -HeaderText "🚦 Per-Subnet Failover Issues" { + $perSubnet = $DHCPData.FailoverAnalysis.PerSubnetIssues | ForEach-Object { + [PSCustomObject]@{ + ScopeId = $_.ScopeId + PartnerA = $_.PrimaryServer + PartnerB = $_.SecondaryServer + Relationship = if ($_.Relationship) { $_.Relationship } else { '' } + Status = $_.Issue + } + } + New-HTMLTable -DataTable $perSubnet -ScrollX -Filtering { + # Status now includes server names (e.g., "Missing on dhcp01") + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator like -Value 'Missing on *' -BackgroundColor Orange + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Missing from both partners' -BackgroundColor Salmon -Color White + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'No failover configured' -BackgroundColor Salmon -Color White + } -Title 'Subnets requiring attention' + } + } + + # Stale failover relationships (no subnets) + if ($DHCPData.FailoverAnalysis -and $DHCPData.FailoverAnalysis.StaleRelationships -and $DHCPData.FailoverAnalysis.StaleRelationships.Count -gt 0) { + New-HTMLSection -HeaderText "🧹 Stale Failover Relationships (no subnets)" { + New-HTMLTable -DataTable $DHCPData.FailoverAnalysis.StaleRelationships -ScrollX -Filtering { + New-HTMLTableCondition -Name 'ScopeCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor Yellow + } + } + } + # Pair-wise analysis views New-HTMLSection -HeaderText "🔎 Relationship Pair View" -CanCollapse { # Build pairs keyed by normalized server tuple + relationship name @@ -164,7 +164,7 @@ if ($rel.ServerName.ToLower() -eq $Pairs[$key].ServerA) { $Pairs[$key].RelA = $rel } else { $Pairs[$key].RelB = $rel } } - foreach ($pair in $Pairs.Values) { + $pairRows = foreach ($pair in $Pairs.Values) { $relA = $pair.RelA $relB = $pair.RelB $scopesA = if ($relA -and $relA.ScopeId) { @($relA.ScopeId) } else { @() } @@ -194,147 +194,161 @@ $commonScopes = @($scopesOnA | Where-Object { $scopesOnB -contains $_ }) $allScopes = @($scopesA + $scopesB + $commonScopes) | Select-Object -Unique - $rows = foreach ($s in $allScopes) { + foreach ($s in $allScopes) { $onA = $scopesA -contains $s $onB = $scopesB -contains $s $statusBase = if ($onA -and $onB) { 'On both partners' } elseif ($onA) { "Missing on $($pair.ServerB)" } elseif ($onB) { "Missing on $($pair.ServerA)" } else { 'Missing on both' } $status = $statusBase + $(if (-not $verified -and $statusBase -ne 'On both partners') { ' (Unverified)' } else { '' }) - $failoverConfig = switch ($status) { + $failoverConfig = switch ($statusBase) { { $_ -like 'Missing on *' } { 'missing on one partner' } 'Missing on both' { 'missing on both' } default { 'configured' } } [PSCustomObject]@{ Relationship = $pair.Name + Pair = "$($pair.ServerA) ↔ $($pair.ServerB)" PartnerA = $pair.ServerA PartnerB = $pair.ServerB ScopeId = $s OnPartnerA = $onA OnPartnerB = $onB + VerifiedFromBothSides = $verified Status = $status FailoverConfiguration = $failoverConfig } } - - New-HTMLSection -HeaderText "$($pair.Name) — $($pair.ServerA) ↔ $($pair.ServerB)" { - New-HTMLTable -DataTable $rows -Filtering { - # Critical - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator like -Value 'Missing on *' -BackgroundColor Salmon - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Missing on both' -BackgroundColor Salmon - } -DataStore JavaScript -ScrollX -Title "Scope Assignment Comparison" - } } - } - # High Availability Recommendations - New-HTMLSection -HeaderText "💡 High Availability Recommendations" { - New-HTMLPanel -Invisible { - # Critical recommendations for scopes without failover - if ($ScopesWithoutFailover -gt 0) { - New-HTMLText -Text "🚨 Critical Recommendations:" -FontSize 16pt -FontWeight bold -Color Red - New-HTMLList { - New-HTMLListItem -Text "$ScopesWithoutFailover active scope(s) have no failover configuration" - New-HTMLListItem -Text "Configure DHCP failover for all production scopes to ensure service availability" - New-HTMLListItem -Text "Consider Load Balance mode for even distribution or Hot Standby for primary/backup scenarios" - } - } - - # Best practices - # New-HTMLText -Text "✅ DHCP Failover Best Practices:" -FontSize 16pt -FontWeight bold -Color Blue - # New-HTMLList { - # New-HTMLListItem -Text "Use Load Balance mode (50/50) for most scenarios to distribute load evenly" - # New-HTMLListItem -Text "Configure Hot Standby mode for scenarios requiring a primary/backup configuration" - # New-HTMLListItem -Text "Enable authentication between failover partners for security" - # New-HTMLListItem -Text "Monitor failover state regularly and configure alerts for state changes" - # New-HTMLListItem -Text "Test failover scenarios periodically to ensure proper configuration" - # New-HTMLListItem -Text "Keep Maximum Client Lead Time (MCLT) at default (1 hour) unless specific requirements exist" - # New-HTMLListItem -Text "Configure State Switchover Interval based on network reliability (default: 5 minutes)" - # } - - # Failover modes explanation - # New-HTMLText -Text "📚 Failover Modes Explained:" -FontSize 16pt -FontWeight bold -Color Blue + if (@($pairRows).Count -gt 0) { New-HTMLSection -Invisible { New-HTMLPanel { - New-HTMLText -Text "Load Balance Mode" -FontSize 14pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "Both servers actively respond to DHCP requests based on configured percentage" -FontSize 12pt - New-HTMLList { - New-HTMLListItem -Text "Default: 50/50 split of client requests" - New-HTMLListItem -Text "Provides active-active configuration" - New-HTMLListItem -Text "Best for redundancy and load distribution" - } - } -Width '48%' - - New-HTMLPanel { - New-HTMLText -Text "Hot Standby Mode" -FontSize 14pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "Primary server handles all requests; standby activates on failure" -FontSize 12pt - New-HTMLList { - New-HTMLListItem -Text "Primary server handles 100% of requests" - New-HTMLListItem -Text "Standby server activates only on primary failure" - New-HTMLListItem -Text "Best for specific server preference scenarios" - } - } -Width '48%' + New-HTMLText -Text "Detailed scope comparison across all failover pairs. Use filtering on Relationship, Pair, or Status to narrow the view." -FontSize 12pt -Color DarkSlateGray + } -Width '100%' } - } - } - - # Failover enumeration issues table (if any) - if (($FailoverEnumWarnings.Count -gt 0) -or ($FailoverEnumErrors.Count -gt 0)) { - New-HTMLSection -HeaderText "🚨 Failover Enumeration Issues" -CanCollapse { - $enumIssues = @($FailoverEnumWarnings + $FailoverEnumErrors) | Sort-Object -Property Timestamp - $enumDisplay = $enumIssues | ForEach-Object { - [PSCustomObject]@{ - Time = $_.Timestamp - Server = $_.ServerName - Severity = $_.Severity - Component = $_.Component - Operation = $_.Operation - Category = $_.Category - Reason = $_.Reason - ErrorId = $_.ErrorId - Target = $_.Target - HResult = $_.HResult - Message = $_.ErrorMessage - } + New-HTMLSection -Invisible { + New-HTMLPanel { + New-HTMLTable -DataTable $pairRows -Filtering { + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'On both partners' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator like -Value 'Missing on *' -BackgroundColor LightYellow + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Missing on both' -BackgroundColor Salmon -Color White + New-HTMLTableCondition -Name 'VerifiedFromBothSides' -ComparisonType bool -Operator eq -Value $false -BackgroundColor Lavender -HighlightHeaders 'VerifiedFromBothSides' + } -DataStore JavaScript -ScrollX -Title 'Scope Assignment Comparison by Relationship Pair' + } -Width '100%' } - New-HTMLTable -DataTable $enumDisplay -Filtering -ScrollX { - New-HTMLTableCondition -Name 'Severity' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Yellow - New-HTMLTableCondition -Name 'Severity' -ComparisonType string -Operator eq -Value 'Error' -BackgroundColor Red -Color White - } -Title 'Enumeration errors/warnings when calling Get-DhcpServerv4Failover' + } else { + New-HTMLText -Text "No failover pair comparison data is available." -Color Gray -FontSize 12pt } } - - # Standalone DHCP servers (no failover relationships and no enumeration error recorded) - $allServers = @($DHCPData.Servers | ForEach-Object { ([string]$_.ServerName).Trim().ToLower() }) - $mentioned = @() - foreach ($rel in $DHCPData.FailoverRelationships) { - if ($rel.ServerName) { $mentioned += ([string]$rel.ServerName).Trim().ToLower() } - if ($rel.PartnerServer){ $mentioned += ([string]$rel.PartnerServer).Trim().ToLower() } - } - $mentioned = $mentioned | Select-Object -Unique - $enumFailed = @(@($FailoverEnumWarnings + $FailoverEnumErrors) | ForEach-Object { ([string]$_.ServerName).Trim().ToLower() }) | Select-Object -Unique - $standalone = @($allServers | Where-Object { ($_ -notin $mentioned) -and ($_ -notin $enumFailed) }) - if ($standalone.Count -gt 0) { - New-HTMLSection -HeaderText "ℹ️ Servers Without Any Failover Relationships (Standalone)" -CanCollapse { - $rows = foreach ($srv in $standalone) { - $active = @($DHCPData.Scopes | Where-Object { $_.ServerName -and $_.ServerName.ToLower() -eq $srv -and $_.State -eq 'Active' }).Count - [PSCustomObject]@{ Server = $srv; ActiveScopes = $active } + + # High Availability Recommendations + New-HTMLSection -HeaderText "💡 High Availability Recommendations" { + New-HTMLPanel -Invisible { + # Critical recommendations for scopes without failover + if ($ScopesWithoutFailover -gt 0) { + New-HTMLText -Text "🚨 Critical Recommendations:" -FontSize 16pt -FontWeight bold -Color Red + New-HTMLList { + New-HTMLListItem -Text "$ScopesWithoutFailover active scope(s) have no failover configuration" + New-HTMLListItem -Text "Configure DHCP failover for all production scopes to ensure service availability" + New-HTMLListItem -Text "Consider Load Balance mode for even distribution or Hot Standby for primary/backup scenarios" + } + } + + # Best practices + # New-HTMLText -Text "✅ DHCP Failover Best Practices:" -FontSize 16pt -FontWeight bold -Color Blue + # New-HTMLList { + # New-HTMLListItem -Text "Use Load Balance mode (50/50) for most scenarios to distribute load evenly" + # New-HTMLListItem -Text "Configure Hot Standby mode for scenarios requiring a primary/backup configuration" + # New-HTMLListItem -Text "Enable authentication between failover partners for security" + # New-HTMLListItem -Text "Monitor failover state regularly and configure alerts for state changes" + # New-HTMLListItem -Text "Test failover scenarios periodically to ensure proper configuration" + # New-HTMLListItem -Text "Keep Maximum Client Lead Time (MCLT) at default (1 hour) unless specific requirements exist" + # New-HTMLListItem -Text "Configure State Switchover Interval based on network reliability (default: 5 minutes)" + # } + + # Failover modes explanation + # New-HTMLText -Text "📚 Failover Modes Explained:" -FontSize 16pt -FontWeight bold -Color Blue + New-HTMLSection -Invisible { + New-HTMLPanel { + New-HTMLText -Text "Load Balance Mode" -FontSize 14pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "Both servers actively respond to DHCP requests based on configured percentage" -FontSize 12pt + New-HTMLList { + New-HTMLListItem -Text "Default: 50/50 split of client requests" + New-HTMLListItem -Text "Provides active-active configuration" + New-HTMLListItem -Text "Best for redundancy and load distribution" + } + } -Width '100%' + + New-HTMLPanel { + New-HTMLText -Text "Hot Standby Mode" -FontSize 14pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "Primary server handles all requests; standby activates on failure" -FontSize 12pt + New-HTMLList { + New-HTMLListItem -Text "Primary server handles 100% of requests" + New-HTMLListItem -Text "Standby server activates only on primary failure" + New-HTMLListItem -Text "Best for specific server preference scenarios" + } + } -Width '100%' } - New-HTMLTable -DataTable $rows -Filtering -ScrollX -Title 'Standalone DHCP Servers' - } - } - - # Scope Redundancy Analysis - if ($DHCPData.ScopeRedundancyAnalysis.Count -gt 0) { - New-HTMLSection -HeaderText "📊 Scope Redundancy Analysis" { - New-HTMLTable -DataTable $DHCPData.ScopeRedundancyAnalysis -Filtering { - New-HTMLTableCondition -Name 'RedundancyStatus' -ComparisonType string -Operator eq -Value 'Failover Configured' -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'RedundancyStatus' -ComparisonType string -Operator contains -Value 'Risk' -BackgroundColor Salmon - New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'High' -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'Medium' -BackgroundColor Orange - New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'Low' -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 80 -BackgroundColor Orange -HighlightHeaders 'UtilizationPercent' - } -DataStore JavaScript -ScrollX -Title "Scope-Level Redundancy Status" } } - } -} + + # Failover enumeration issues table (if any) + if (($FailoverEnumWarnings.Count -gt 0) -or ($FailoverEnumErrors.Count -gt 0)) { + New-HTMLSection -HeaderText "🚨 Failover Enumeration Issues" -CanCollapse { + $enumIssues = @($FailoverEnumWarnings + $FailoverEnumErrors) | Sort-Object -Property Timestamp + $enumDisplay = $enumIssues | ForEach-Object { + [PSCustomObject]@{ + Time = $_.Timestamp + Server = $_.ServerName + Severity = $_.Severity + Component = $_.Component + Operation = $_.Operation + Category = $_.Category + Reason = $_.Reason + ErrorId = $_.ErrorId + Target = $_.Target + HResult = $_.HResult + Message = $_.ErrorMessage + } + } + New-HTMLTable -DataTable $enumDisplay -Filtering -ScrollX { + New-HTMLTableCondition -Name 'Severity' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Yellow + New-HTMLTableCondition -Name 'Severity' -ComparisonType string -Operator eq -Value 'Error' -BackgroundColor Red -Color White + } -Title 'Enumeration errors/warnings when calling Get-DhcpServerv4Failover' + } + } + + # Standalone DHCP servers (no failover relationships and no enumeration error recorded) + $allServers = @($DHCPData.Servers | ForEach-Object { ([string]$_.ServerName).Trim().ToLower() }) + $mentioned = @() + foreach ($rel in $DHCPData.FailoverRelationships) { + if ($rel.ServerName) { $mentioned += ([string]$rel.ServerName).Trim().ToLower() } + if ($rel.PartnerServer){ $mentioned += ([string]$rel.PartnerServer).Trim().ToLower() } + } + $mentioned = $mentioned | Select-Object -Unique + $enumFailed = @(@($FailoverEnumWarnings + $FailoverEnumErrors) | ForEach-Object { ([string]$_.ServerName).Trim().ToLower() }) | Select-Object -Unique + $standalone = @($allServers | Where-Object { ($_ -notin $mentioned) -and ($_ -notin $enumFailed) }) + if ($standalone.Count -gt 0) { + New-HTMLSection -HeaderText "ℹ️ Servers Without Any Failover Relationships (Standalone)" -CanCollapse { + $rows = foreach ($srv in $standalone) { + $active = @($DHCPData.Scopes | Where-Object { $_.ServerName -and $_.ServerName.ToLower() -eq $srv -and $_.State -eq 'Active' }).Count + [PSCustomObject]@{ Server = $srv; ActiveScopes = $active } + } + New-HTMLTable -DataTable $rows -Filtering -ScrollX -Title 'Standalone DHCP Servers' + } + } + + # Scope Redundancy Analysis + if ($DHCPData.ScopeRedundancyAnalysis.Count -gt 0) { + New-HTMLSection -HeaderText "📊 Scope Redundancy Analysis" { + New-HTMLTable -DataTable $DHCPData.ScopeRedundancyAnalysis -Filtering { + New-HTMLTableCondition -Name 'RedundancyStatus' -ComparisonType string -Operator eq -Value 'Failover Configured' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'RedundancyStatus' -ComparisonType string -Operator contains -Value 'Risk' -BackgroundColor Salmon + New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'High' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'Medium' -BackgroundColor Orange + New-HTMLTableCondition -Name 'RiskLevel' -ComparisonType string -Operator eq -Value 'Low' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 80 -BackgroundColor Orange -HighlightHeaders 'UtilizationPercent' + } -DataStore JavaScript -ScrollX -Title "Scope-Level Redundancy Status" + } + } + } +} diff --git a/Private/New-DHCPMinimalAllScopesTab.ps1 b/Private/New-DHCPMinimalAllScopesTab.ps1 index e15c1db..a3a0b5c 100644 --- a/Private/New-DHCPMinimalAllScopesTab.ps1 +++ b/Private/New-DHCPMinimalAllScopesTab.ps1 @@ -1,81 +1,82 @@ -function New-DHCPMinimalAllScopesTab { - [CmdletBinding()] - param( - [Parameter(Mandatory)][System.Collections.IDictionary] $DHCPData - ) - - New-HTMLTab -TabName 'All Scopes' { - New-HTMLSection -HeaderText "Complete Scope Inventory" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "All DHCP Scopes" -FontSize 18px -FontWeight bold - New-HTMLText -Text "This view shows all scopes including both properly configured and problematic ones." -FontSize 12px - - # Summary counts - $GoodScopes = $DHCPData.Scopes | Where-Object { -not $_.HasIssues } - $BadScopes = $DHCPData.Scopes | Where-Object { $_.HasIssues } - - New-HTMLList { - New-HTMLListItem -Text "Total Scopes: ", "$($DHCPData.Scopes.Count)" -FontWeight bold, normal -Color Black, Blue - New-HTMLListItem -Text "Properly Configured: ", "$($GoodScopes.Count)" -FontWeight bold, normal -Color Black, Green - New-HTMLListItem -Text "With Issues: ", "$($BadScopes.Count)" -FontWeight bold, normal -Color Black, Red - } -FontSize 12px - } - } - - # Good Scopes Section - if ($GoodScopes.Count -gt 0) { - New-HTMLSection -HeaderText "✅ Properly Configured Scopes ($($GoodScopes.Count))" -CanCollapse { - New-HTMLPanel -Invisible { +function New-DHCPMinimalAllScopesTab { + [CmdletBinding()] + param( + [Parameter(Mandatory)][System.Collections.IDictionary] $DHCPData + ) + + New-HTMLTab -TabName 'All Scopes' { + New-HTMLSection -HeaderText "Complete Scope Inventory" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "All DHCP Scopes" -FontSize 18px -FontWeight bold + New-HTMLText -Text "This view shows all scopes including both properly configured and problematic ones." -FontSize 12px + + # Summary counts + $GoodScopes = $DHCPData.Scopes | Where-Object { -not $_.HasIssues } + $BadScopes = $DHCPData.Scopes | Where-Object { $_.HasIssues } + + New-HTMLList { + New-HTMLListItem -Text "Total Scopes: ", "$($DHCPData.Scopes.Count)" -FontWeight bold, normal -Color Black, Blue + New-HTMLListItem -Text "Properly Configured: ", "$($GoodScopes.Count)" -FontWeight bold, normal -Color Black, Green + New-HTMLListItem -Text "With Issues: ", "$($BadScopes.Count)" -FontWeight bold, normal -Color Black, Red + } -FontSize 12px + } + } + + # Good Scopes Section + if ($GoodScopes.Count -gt 0) { + New-HTMLSection -HeaderText "✅ Properly Configured Scopes ($($GoodScopes.Count))" -CanCollapse { + New-HTMLPanel -Invisible { New-HTMLTable -DataTable $GoodScopes { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Inactive' -BackgroundColor LightGray - New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 80 -BackgroundColor Yellow -HighlightHeaders 'PercentageInUse' - New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Orange -HighlightHeaders 'PercentageInUse' + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Inactive' -BackgroundColor LightGray + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 80 -BackgroundColor Yellow -HighlightHeaders 'PercentageInUse' + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Orange -HighlightHeaders 'PercentageInUse' New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator le -Value 48 -BackgroundColor LightGreen -HighlightHeaders 'LeaseDurationHours' } -ScrollX -IncludeProperty @( 'ServerName', 'ScopeId', 'Name', 'State', 'LeaseDurationHours', 'DNSServers', 'DomainNameOption', 'DynamicUpdates', - 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', + 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', 'DisableDnsPtrRRUpdate', 'FailoverPartner', 'HasFailover', 'FailoverConfiguration' ) } } } - - # Bad Scopes Section - if ($BadScopes.Count -gt 0) { - New-HTMLSection -HeaderText "⚠️ Scopes with Issues ($($BadScopes.Count))" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLTable -DataTable $BadScopes { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Inactive' -BackgroundColor LightGray - New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Salmon -HighlightHeaders 'Issues' - New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Red -Color White -HighlightHeaders 'PercentageInUse' - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Orange -HighlightHeaders 'LeaseDurationHours' + + # Bad Scopes Section + if ($BadScopes.Count -gt 0) { + New-HTMLSection -HeaderText "⚠️ Scopes with Issues ($($BadScopes.Count))" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLTable -DataTable $BadScopes { + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Inactive' -BackgroundColor LightGray + New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Salmon -HighlightHeaders 'Issues' + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Red -Color White -HighlightHeaders 'PercentageInUse' + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Orange -HighlightHeaders 'LeaseDurationHours' New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '8.8' -BackgroundColor Red -Color White New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '1.1' -BackgroundColor Red -Color White New-HTMLTableCondition -Name 'UpdateDnsRRForOlderClients' -ComparisonType bool -Operator eq -Value $false -BackgroundColor Yellow New-HTMLTableCondition -Name 'DeleteDnsRROnLeaseExpiry' -ComparisonType bool -Operator eq -Value $false -BackgroundColor Yellow + New-HTMLTableCondition -Name 'DisableDnsPtrRRUpdate' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Orange } -ScrollX -IncludeProperty @( 'ServerName', 'ScopeId', 'Name', 'State', 'LeaseDurationHours', 'DNSServers', 'DomainNameOption', 'DynamicUpdates', - 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', + 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', 'DisableDnsPtrRRUpdate', 'FailoverPartner', 'HasFailover', 'FailoverConfiguration', 'Issues' ) } } - } - - # Complete raw data (collapsed by default) - New-HTMLSection -HeaderText '📊 Complete Scope Data' -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text 'Raw scope data for detailed analysis' -FontSize 12px - New-HTMLTable -DataTable $DHCPData.Scopes { - New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Salmon - New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $false -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightBlue -HighlightHeaders 'State' - } -ScrollX -Filtering -PagingLength 25 -ExcludeProperty 'AddressesInUse', 'AddressesFree', 'PercentageInUse', 'Reserved', 'HasUtilizationIssues', 'UtilizationIssues', 'TotalAddresses', 'DefinedRange', 'UtilizationEfficiency', 'DNSSettings' - } - } - } -} + } + + # Complete raw data (collapsed by default) + New-HTMLSection -HeaderText '📊 Complete Scope Data' -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text 'Raw scope data for detailed analysis' -FontSize 12px + New-HTMLTable -DataTable $DHCPData.Scopes { + New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Salmon + New-HTMLTableCondition -Name 'HasIssues' -ComparisonType bool -Operator eq -Value $false -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightBlue -HighlightHeaders 'State' + } -ScrollX -Filtering -PagingLength 25 -ExcludeProperty 'AddressesInUse', 'AddressesFree', 'PercentageInUse', 'Reserved', 'HasUtilizationIssues', 'UtilizationIssues', 'TotalAddresses', 'DefinedRange', 'UtilizationEfficiency', 'DNSSettings' + } + } + } +} diff --git a/Private/New-DHCPMinimalValidationTab.ps1 b/Private/New-DHCPMinimalValidationTab.ps1 index b0f4c03..960c2f0 100644 --- a/Private/New-DHCPMinimalValidationTab.ps1 +++ b/Private/New-DHCPMinimalValidationTab.ps1 @@ -1,104 +1,104 @@ -function New-DHCPMinimalValidationTab { - [CmdletBinding()] - param( - [Parameter(Mandatory)][System.Collections.IDictionary] $DHCPData - ) - - New-HTMLTab -TabName 'Validation Results' { - if ($DHCPData.ScopesWithIssues.Count -gt 0) { - # Group issues by type for summary (align with full report categories) - $LeaseDurationIssues = $DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration - $PublicDNSWithUpdates = $DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates - $DNSConfigurationProblems = $DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems - $DNSRecordManagement = $DHCPData.ValidationResults.WarningIssues.DNSRecordManagement - $MissingDomainName = $DHCPData.ValidationResults.InfoIssues.MissingDomainName - $FailoverIssues = $DHCPData.ValidationResults.WarningIssues.MissingFailover - - $LeaseDurationCount = @($LeaseDurationIssues).Count - $PublicDNSCount = @($PublicDNSWithUpdates).Count - $DNSConfigProblemsCount = @($DNSConfigurationProblems).Count - $DNSRecordMgmtCount = @($DNSRecordManagement).Count - $MissingDomainNameCount = @($MissingDomainName).Count - $FailoverCount = @($FailoverIssues).Count - - # Summary Section - New-HTMLSection -Invisible { - New-HTMLSection -HeaderText "Configuration Validation Summary" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Issues Detected" -FontSize 18px -FontWeight bold -Color Red - New-HTMLText -Text "The following configuration issues were found during validation:" -FontSize 12px - New-HTMLList { - New-HTMLListItem -Text "Total Issues: ", "$($DHCPData.ScopesWithIssues.Count) scope(s) with problems" -FontWeight bold, normal -Color Red, Black - if ($LeaseDurationCount -gt 0) { - New-HTMLListItem -Text "Lease Duration: ", "$LeaseDurationCount scope(s) exceed 48 hours" -FontWeight bold, normal -Color Orange, Black - } - if ($PublicDNSCount -gt 0) { - New-HTMLListItem -Text "Public DNS + Updates: ", "$PublicDNSCount scope(s)" -FontWeight bold, normal -Color Orange, Black - } - if ($DNSConfigProblemsCount -gt 0) { - New-HTMLListItem -Text "DNS Configuration Problems: ", "$DNSConfigProblemsCount scope(s)" -FontWeight bold, normal -Color Orange, Black - } - if ($DNSRecordMgmtCount -gt 0) { - New-HTMLListItem -Text "DNS Record Management: ", "$DNSRecordMgmtCount scope(s)" -FontWeight bold, normal -Color Orange, Black - } - if ($MissingDomainNameCount -gt 0) { - New-HTMLListItem -Text "Missing Domain Name Option: ", "$MissingDomainNameCount scope(s)" -FontWeight bold, normal -Color Orange, Black - } - if ($FailoverCount -gt 0) { - New-HTMLListItem -Text "Failover Missing: ", "$FailoverCount scope(s) without redundancy" -FontWeight bold, normal -Color Orange, Black - } - } -FontSize 14px - } - } - } - - # Lease Duration Issues - if ($LeaseDurationCount -gt 0) { - New-HTMLSection -HeaderText "⏱️ Lease Duration Issues (> 48 hours)" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Scopes with excessive lease duration:" -FontSize 14px -FontWeight bold - New-HTMLTable -DataTable $LeaseDurationIssues { - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 168 -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 96 -BackgroundColor Orange - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Yellow - } -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'LeaseDurationHours', 'Description' - ) -Filtering - New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue - New-HTMLText -Text "Reduce lease duration to ≤ 48 hours or add 'DHCP lease time=Xd' to scope description for approved exceptions." -FontSize 11px -Color Blue - } - } - } - - # Public DNS with updates (critical) - if ($PublicDNSCount -gt 0) { - New-HTMLSection -HeaderText "🌐 Public DNS Servers with Dynamic Updates Enabled" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Scopes using public DNS servers while dynamic updates are enabled:" -FontSize 14px -FontWeight bold - New-HTMLTable -DataTable $PublicDNSWithUpdates { - New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '8.8.4.4' -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '1.0.0.1' -BackgroundColor Red -Color White - } -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'DNSServers', 'DynamicUpdates' - ) -Filtering - New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue - New-HTMLText -Text "Replace public DNS servers with internal DNS servers (10.x.x.x)." -FontSize 11px -Color Blue - } - } - } - - # DNS configuration problems (aggregated when policy enabled) - if ($DNSConfigProblemsCount -gt 0) { - New-HTMLSection -HeaderText "⚠️ Scopes with DNS Configuration Problems" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLTable -DataTable $DNSConfigurationProblems -Filtering -ScrollX - } - } - } - - # DNS record management issues (warning) +function New-DHCPMinimalValidationTab { + [CmdletBinding()] + param( + [Parameter(Mandatory)][System.Collections.IDictionary] $DHCPData + ) + + New-HTMLTab -TabName 'Validation Results' { + if ($DHCPData.ScopesWithIssues.Count -gt 0) { + # Group issues by type for summary (align with full report categories) + $LeaseDurationIssues = $DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration + $PublicDNSWithUpdates = $DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates + $DNSConfigurationProblems = $DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems + $DNSRecordManagement = $DHCPData.ValidationResults.WarningIssues.DNSRecordManagement + $MissingDomainName = $DHCPData.ValidationResults.InfoIssues.MissingDomainName + $FailoverIssues = $DHCPData.ValidationResults.WarningIssues.MissingFailover + + $LeaseDurationCount = @($LeaseDurationIssues).Count + $PublicDNSCount = @($PublicDNSWithUpdates).Count + $DNSConfigProblemsCount = @($DNSConfigurationProblems).Count + $DNSRecordMgmtCount = @($DNSRecordManagement).Count + $MissingDomainNameCount = @($MissingDomainName).Count + $FailoverCount = @($FailoverIssues).Count + + # Summary Section + New-HTMLSection -Invisible { + New-HTMLSection -HeaderText "Configuration Validation Summary" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Issues Detected" -FontSize 18px -FontWeight bold -Color Red + New-HTMLText -Text "The following configuration issues were found during validation:" -FontSize 12px + New-HTMLList { + New-HTMLListItem -Text "Total Issues: ", "$($DHCPData.ScopesWithIssues.Count) scope(s) with problems" -FontWeight bold, normal -Color Red, Black + if ($LeaseDurationCount -gt 0) { + New-HTMLListItem -Text "Lease Duration: ", "$LeaseDurationCount scope(s) exceed 48 hours" -FontWeight bold, normal -Color Orange, Black + } + if ($PublicDNSCount -gt 0) { + New-HTMLListItem -Text "Public DNS + Updates: ", "$PublicDNSCount scope(s)" -FontWeight bold, normal -Color Orange, Black + } + if ($DNSConfigProblemsCount -gt 0) { + New-HTMLListItem -Text "DNS Configuration Problems: ", "$DNSConfigProblemsCount scope(s)" -FontWeight bold, normal -Color Orange, Black + } + if ($DNSRecordMgmtCount -gt 0) { + New-HTMLListItem -Text "DNS Record Management: ", "$DNSRecordMgmtCount scope(s)" -FontWeight bold, normal -Color Orange, Black + } + if ($MissingDomainNameCount -gt 0) { + New-HTMLListItem -Text "Missing Domain Name Option: ", "$MissingDomainNameCount scope(s)" -FontWeight bold, normal -Color Orange, Black + } + if ($FailoverCount -gt 0) { + New-HTMLListItem -Text "Failover Missing: ", "$FailoverCount scope(s) without redundancy" -FontWeight bold, normal -Color Orange, Black + } + } -FontSize 14px + } + } + } + + # Lease Duration Issues + if ($LeaseDurationCount -gt 0) { + New-HTMLSection -HeaderText "⏱️ Lease Duration Issues (> 48 hours)" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Scopes with excessive lease duration:" -FontSize 14px -FontWeight bold + New-HTMLTable -DataTable $LeaseDurationIssues { + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 168 -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 96 -BackgroundColor Orange + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Yellow + } -ScrollX -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'LeaseDurationHours', 'Description' + ) -Filtering + New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue + New-HTMLText -Text "Reduce lease duration to ≤ 48 hours or add 'DHCP lease time=Xd' to scope description for approved exceptions." -FontSize 11px -Color Blue + } + } + } + + # Public DNS with updates (critical) + if ($PublicDNSCount -gt 0) { + New-HTMLSection -HeaderText "🌐 Public DNS Servers with Dynamic Updates Enabled" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Scopes using public DNS servers while dynamic updates are enabled:" -FontSize 14px -FontWeight bold + New-HTMLTable -DataTable $PublicDNSWithUpdates { + New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '8.8.4.4' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'DNSServers' -ComparisonType string -Operator contains -Value '1.0.0.1' -BackgroundColor Red -Color White + } -ScrollX -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'DNSServers', 'DynamicUpdates' + ) -Filtering + New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue + New-HTMLText -Text "Replace public DNS servers with internal DNS servers (10.x.x.x)." -FontSize 11px -Color Blue + } + } + } + + # DNS configuration problems (aggregated when policy enabled) + if ($DNSConfigProblemsCount -gt 0) { + New-HTMLSection -HeaderText "⚠️ Scopes with DNS Configuration Problems" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLTable -DataTable $DNSConfigurationProblems -Filtering -ScrollX + } + } + } + + # DNS record management issues (warning) if ($DNSRecordMgmtCount -gt 0) { New-HTMLSection -HeaderText "DNS Record Management Issues" -CanCollapse { New-HTMLPanel -Invisible { @@ -106,73 +106,74 @@ New-HTMLTable -DataTable $DNSRecordManagement { New-HTMLTableCondition -Name 'UpdateDnsRRForOlderClients' -ComparisonType bool -Operator eq -Value $false -BackgroundColor Yellow New-HTMLTableCondition -Name 'DeleteDnsRROnLeaseExpiry' -ComparisonType bool -Operator eq -Value $false -BackgroundColor Yellow + New-HTMLTableCondition -Name 'DisableDnsPtrRRUpdate' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Orange -Color Black } -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', 'DynamicUpdates' + 'ServerName', 'ScopeId', 'Name', 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', 'DisableDnsPtrRRUpdate', 'DynamicUpdates' ) -Filtering New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue - New-HTMLText -Text "Enable 'Update DNS RR for Older Clients' and 'Delete DNS RR on Lease Expiry'." -FontSize 11px -Color Blue + New-HTMLText -Text "Enable 'Update DNS RR for Older Clients', enable 'Delete DNS RR on Lease Expiry', and keep 'Disable DNS PTR RR Update' set to FALSE." -FontSize 11px -Color Blue } } } - - # Missing domain name (info) - if ($MissingDomainNameCount -gt 0) { - New-HTMLSection -HeaderText "Scopes Missing Domain Name Option" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLTable -DataTable $MissingDomainName -Filtering -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'DomainNameOption', 'DynamicUpdates' - ) - New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue - New-HTMLText -Text "Configure Domain Name option (015) when DNS updates are enabled." -FontSize 11px -Color Blue - } - } - } - - # Failover Issues - if ($FailoverCount -gt 0) { - New-HTMLSection -HeaderText "🔄 Failover Configuration Issues" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Scopes without proper failover configuration:" -FontSize 14px -FontWeight bold - New-HTMLTable -DataTable $FailoverIssues { - New-HTMLTableCondition -Name 'FailoverPartner' -ComparisonType string -Operator eq -Value '' -BackgroundColor Red -Color White - } -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'State', 'FailoverPartner' - ) -Filtering - New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue - New-HTMLText -Text "Configure DHCP failover for high availability and redundancy." -FontSize 11px -Color Blue - } - } - } - - # Complete Issues Table - New-HTMLSection -HeaderText "📄 Complete Issue List" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLTable -DataTable $DHCPData.ScopesWithIssues { - New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'duration' -BackgroundColor Yellow - New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'DNS' -BackgroundColor Orange - New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'failover' -BackgroundColor Red -Color White - } -ScrollX -IncludeProperty @( - 'ServerName', 'ScopeId', 'Name', 'State', 'Issues' - ) -Filtering - } - } - } else { - # No issues found - New-HTMLSection -Invisible { - New-HTMLSection -HeaderText "✅ Validation Results" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "All Validations Passed" -FontSize 20px -FontWeight bold -Color Green -TextAlign center - New-HTMLText -Text "No configuration issues detected in your DHCP infrastructure." -FontSize 14px -TextAlign center - - New-HTMLText -Text "Validation Checks Performed:" -FontSize 14px -FontWeight bold - New-HTMLList { - New-HTMLListItem -Text "Lease Duration: ", "All scopes ≤ 48 hours or have approved exceptions" -FontWeight bold, normal -Color Green, Black - New-HTMLListItem -Text "DNS Configuration: ", "All scopes use internal DNS servers with proper settings" -FontWeight bold, normal -Color Green, Black - New-HTMLListItem -Text "Failover Status: ", "All required scopes have failover configured" -FontWeight bold, normal -Color Green, Black - } -FontSize 12px - } - } - } - } - } -} + + # Missing domain name (info) + if ($MissingDomainNameCount -gt 0) { + New-HTMLSection -HeaderText "Scopes Missing Domain Name Option" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLTable -DataTable $MissingDomainName -Filtering -ScrollX -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'DomainNameOption', 'DynamicUpdates' + ) + New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue + New-HTMLText -Text "Configure Domain Name option (015) when DNS updates are enabled." -FontSize 11px -Color Blue + } + } + } + + # Failover Issues + if ($FailoverCount -gt 0) { + New-HTMLSection -HeaderText "🔄 Failover Configuration Issues" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Scopes without proper failover configuration:" -FontSize 14px -FontWeight bold + New-HTMLTable -DataTable $FailoverIssues { + New-HTMLTableCondition -Name 'FailoverPartner' -ComparisonType string -Operator eq -Value '' -BackgroundColor Red -Color White + } -ScrollX -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'State', 'FailoverPartner' + ) -Filtering + New-HTMLText -Text "Recommendation:" -FontSize 12px -FontWeight bold -Color Blue + New-HTMLText -Text "Configure DHCP failover for high availability and redundancy." -FontSize 11px -Color Blue + } + } + } + + # Complete Issues Table + New-HTMLSection -HeaderText "📄 Complete Issue List" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLTable -DataTable $DHCPData.ScopesWithIssues { + New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'duration' -BackgroundColor Yellow + New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'DNS' -BackgroundColor Orange + New-HTMLTableCondition -Name 'Issues' -ComparisonType string -Operator contains -Value 'failover' -BackgroundColor Red -Color White + } -ScrollX -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'State', 'Issues' + ) -Filtering + } + } + } else { + # No issues found + New-HTMLSection -Invisible { + New-HTMLSection -HeaderText "✅ Validation Results" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "All Validations Passed" -FontSize 20px -FontWeight bold -Color Green -TextAlign center + New-HTMLText -Text "No configuration issues detected in your DHCP infrastructure." -FontSize 14px -TextAlign center + + New-HTMLText -Text "Validation Checks Performed:" -FontSize 14px -FontWeight bold + New-HTMLList { + New-HTMLListItem -Text "Lease Duration: ", "All scopes ≤ 48 hours or have approved exceptions" -FontWeight bold, normal -Color Green, Black + New-HTMLListItem -Text "DNS Configuration: ", "All scopes use internal DNS servers with proper settings" -FontWeight bold, normal -Color Green, Black + New-HTMLListItem -Text "Failover Status: ", "All required scopes have failover configured" -FontWeight bold, normal -Color Green, Black + } -FontSize 12px + } + } + } + } + } +} diff --git a/Private/New-DHCPOptionsClassesTab.ps1 b/Private/New-DHCPOptionsClassesTab.ps1 index 77d4f52..c0cbf2a 100644 --- a/Private/New-DHCPOptionsClassesTab.ps1 +++ b/Private/New-DHCPOptionsClassesTab.ps1 @@ -1,167 +1,188 @@ -function New-DHCPOptionsClassesTab { - <# - .SYNOPSIS - Creates the Options & Classes tab content for DHCP HTML report with nested tabs. - #> - [CmdletBinding()] - param([Parameter(Mandatory = $true)][hashtable] $DHCPData) - - New-HTMLTab -TabName 'Options & Classes' { - # Create nested tabs for Options and Classes - New-HTMLTabPanel { - # DHCP Options Tab - New-HTMLTab -TabName 'Options' { - # Options Health Dashboard at the top - if ($DHCPData.OptionsAnalysis.Count -gt 0) { - New-HTMLSection -HeaderText "⚙️ DHCP Options Health Dashboard" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "DHCP Options Configuration Analysis" -FontSize 18pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "Critical analysis of DHCP options configuration across all servers and scopes." -FontSize 12pt -Color DarkGray - - foreach ($Analysis in $DHCPData.OptionsAnalysis) { - # Health Overview Cards - New-HTMLSection -HeaderText "Configuration Health Overview" -Invisible -Density Compact { - New-HTMLInfoCard -Title "Total Servers" -Number $Analysis.TotalServersAnalyzed -Subtitle "Analyzed" -Icon "🖥️" -TitleColor DodgerBlue -NumberColor Navy - New-HTMLInfoCard -Title "Options Configured" -Number $Analysis.TotalOptionsConfigured -Subtitle "Total Settings" -Icon "⚙️" -TitleColor Purple -NumberColor DarkMagenta - New-HTMLInfoCard -Title "Option Types" -Number $Analysis.UniqueOptionTypes -Subtitle "Different Options" -Icon "🔧" -TitleColor Orange -NumberColor DarkOrange - - if ($Analysis.CriticalOptionsCovered -ge 4) { - New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Good Coverage" -Icon "✅" -TitleColor LimeGreen -NumberColor DarkGreen -ShadowColor 'rgba(50, 205, 50, 0.15)' - } else { - New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Needs Attention" -Icon "⚠️" -TitleColor Crimson -NumberColor DarkRed -ShadowColor 'rgba(220, 20, 60, 0.2)' -ShadowIntensity Bold +function New-DHCPOptionsClassesTab { + <# + .SYNOPSIS + Creates the Options & Classes tab content for DHCP HTML report with nested tabs. + #> + [CmdletBinding()] + param([Parameter(Mandatory = $true)][hashtable] $DHCPData) + + New-HTMLTab -TabName 'Options & Classes' { + # Create nested tabs for Options and Classes + New-HTMLTabPanel { + # DHCP Options Tab + New-HTMLTab -TabName 'Options' { + # Options Health Dashboard at the top + if ($DHCPData.OptionsAnalysis.Count -gt 0) { + New-HTMLSection -HeaderText "⚙️ DHCP Options Health Dashboard" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "DHCP Options Configuration Analysis" -FontSize 18pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "Critical analysis of DHCP options configuration across all servers and scopes." -FontSize 12pt -Color DarkGray + + foreach ($Analysis in $DHCPData.OptionsAnalysis) { + # Health Overview Cards + New-HTMLSection -HeaderText "Configuration Health Overview" -Invisible -Density Compact { + New-HTMLInfoCard -Title "Total Servers" -Number $Analysis.TotalServersAnalyzed -Subtitle "Analyzed" -Icon "🖥️" -TitleColor DodgerBlue -NumberColor Navy + New-HTMLInfoCard -Title "Options Configured" -Number $Analysis.TotalOptionsConfigured -Subtitle "Total Settings" -Icon "⚙️" -TitleColor Purple -NumberColor DarkMagenta + New-HTMLInfoCard -Title "Option Types" -Number $Analysis.UniqueOptionTypes -Subtitle "Different Options" -Icon "🔧" -TitleColor Orange -NumberColor DarkOrange + + if ($Analysis.CriticalOptionsCovered -ge 4) { + New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Good Coverage" -Icon "✅" -TitleColor LimeGreen -NumberColor DarkGreen -ShadowColor 'rgba(50, 205, 50, 0.15)' + } else { + New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Needs Attention" -Icon "⚠️" -TitleColor Crimson -NumberColor DarkRed -ShadowColor 'rgba(220, 20, 60, 0.2)' -ShadowIntensity Bold + } + } + + # Missing Critical Options + if ($Analysis.MissingCriticalOptions.Count -gt 0) { + New-HTMLSection -HeaderText "🚨 Missing Critical Options" -CanCollapse { + New-HTMLPanel { + New-HTMLText -Text "The following critical DHCP options are not configured:" -FontSize 12pt -Color DarkRed -FontWeight bold + foreach ($MissingOption in $Analysis.MissingCriticalOptions) { + New-HTMLText -Text "🔴 $MissingOption" -Color Red -FontSize 14px + } + } + } + } + + # Configuration Issues + if ($Analysis.OptionIssues.Count -gt 0) { + $IssueDetails = foreach ($Issue in $Analysis.OptionIssues) { + ConvertTo-DHCPOptionIssueRecord -Issue $Issue } - } - - # Missing Critical Options - if ($Analysis.MissingCriticalOptions.Count -gt 0) { - New-HTMLSection -HeaderText "🚨 Missing Critical Options" -CanCollapse { - New-HTMLPanel { - New-HTMLText -Text "The following critical DHCP options are not configured:" -FontSize 12pt -Color DarkRed -FontWeight bold - foreach ($MissingOption in $Analysis.MissingCriticalOptions) { - New-HTMLText -Text "🔴 $MissingOption" -Color Red -FontSize 14px - } + $IssueSummary = $IssueDetails | Group-Object -Property Category | Sort-Object Count -Descending | ForEach-Object { + $sample = $_.Group | Select-Object -First 1 + [PSCustomObject]@{ + Category = $_.Name + IssueCount = $_.Count + AffectedScopes = (@($_.Group.ScopeId | Where-Object { $_ } | Sort-Object -Unique)).Count + AffectedServers= (@($_.Group.ServerName | Where-Object { $_ } | Sort-Object -Unique)).Count + Recommendation = $sample.Recommendation } } - } - # Configuration Issues - if ($Analysis.OptionIssues.Count -gt 0) { New-HTMLSection -HeaderText "⚠️ Configuration Issues Found" -CanCollapse { New-HTMLPanel { - foreach ($Issue in $Analysis.OptionIssues) { - New-HTMLText -Text "⚠️ $Issue" -Color Orange -FontSize 14px - } + New-HTMLText -Text "The same issues are grouped by category first, then listed in a detailed table." -Color DarkOrange -FontSize 12pt -FontWeight bold + New-HTMLTable -DataTable $IssueSummary -Filtering { + New-HTMLTableCondition -Name 'IssueCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightYellow -HighlightHeaders 'IssueCount' + } -DataStore JavaScript -ScrollX -Title 'Issue Summary by Category' + + New-HTMLTable -DataTable $IssueDetails -Filtering { + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Public DNS' -BackgroundColor LightYellow + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Lease Time' -BackgroundColor Moccasin + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Domain Name' -BackgroundColor Lavender + } -DataStore JavaScript -ScrollX -Title 'Issue Details' } } } - } - } - } - } - - # Server-Level Options - if ($DHCPData.DHCPOptions.Count -gt 0) { - New-HTMLSection -HeaderText "🖥️ Server-Level DHCP Options" -CanCollapse { - $ServerOptions = $DHCPData.DHCPOptions | Group-Object ServerName - foreach ($ServerGroup in $ServerOptions) { - New-HTMLPanel { - New-HTMLTable -DataTable $ServerGroup.Group -Filtering { - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' - } -Title "Server Options for $($ServerGroup.Name)" - } - } - } - } - - # Scope-Level Options - if ($DHCPData.Options.Count -gt 0) { - New-HTMLSection -HeaderText "📁 Scope-Level DHCP Options" -CanCollapse { - New-HTMLTable -DataTable $DHCPData.Options -Filtering { - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' - } -Title "All Scope-Level Options" -DataStore JavaScript -ScrollX - } - } - } - - # DHCP Classes Tab - New-HTMLTab -TabName 'Classes' { - # Classes Overview at the top - if ($DHCPData.DHCPClasses.Count -gt 0) { - New-HTMLSection -HeaderText "📋 DHCP Classes Overview" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Vendor & User Classes Configuration" -FontSize 16pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "DHCP classes allow different configuration based on client type." -FontSize 12pt -Color DarkGray - - # Summary statistics - $VendorClasses = ($DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'Vendor' }).Count - $UserClasses = ($DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'User' }).Count - $TotalServers = ($DHCPData.DHCPClasses | Group-Object ServerName).Count - - New-HTMLSection -HeaderText "Classes Summary" -Invisible -Density Compact { - New-HTMLInfoCard -Title "Total Classes" -Number $DHCPData.DHCPClasses.Count -Subtitle "Configured" -Icon "📋" -TitleColor Purple -NumberColor DarkMagenta - New-HTMLInfoCard -Title "Vendor Classes" -Number $VendorClasses -Subtitle "Device Manufacturers" -Icon "🏭" -TitleColor Blue -NumberColor DarkBlue - New-HTMLInfoCard -Title "User Classes" -Number $UserClasses -Subtitle "Custom Categories" -Icon "👥" -TitleColor Green -NumberColor DarkGreen - New-HTMLInfoCard -Title "Servers" -Number $TotalServers -Subtitle "With Classes" -Icon "🖥️" -TitleColor Orange -NumberColor DarkOrange - } - } - } - } - - # Vendor Classes - $VendorClassData = $DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'Vendor' } - if ($VendorClassData.Count -gt 0) { - New-HTMLSection -HeaderText "🏭 Vendor Classes" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Vendor classes identify device manufacturers and types" -FontSize 12pt - New-HTMLTable -DataTable $VendorClassData -Filtering { - New-HTMLTableCondition -Name 'Name' -ComparisonType string -Operator contains -Value 'Microsoft' -BackgroundColor LightYellow -HighlightHeaders 'Name' - New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'Vendor' -BackgroundColor LightBlue -HighlightHeaders 'Type' - } -DataStore JavaScript - } - } - } - - # User Classes - $UserClassData = $DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'User' } - if ($UserClassData.Count -gt 0) { - New-HTMLSection -HeaderText "👥 User Classes" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "User classes provide custom device categorization" -FontSize 12pt - New-HTMLTable -DataTable $UserClassData -Filtering { - New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'User' -BackgroundColor LightGreen -HighlightHeaders 'Type' - } -DataStore JavaScript - } - } - } - - # All Classes Table - if ($DHCPData.DHCPClasses.Count -gt 0) { - New-HTMLSection -HeaderText "📊 All DHCP Classes" -CanCollapse { - New-HTMLTable -DataTable $DHCPData.DHCPClasses -Filtering { - New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'Vendor' -BackgroundColor LightBlue -HighlightHeaders 'Type' - New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'User' -BackgroundColor LightGreen -HighlightHeaders 'Type' - New-HTMLTableCondition -Name 'Name' -ComparisonType string -Operator contains -Value 'Microsoft' -BackgroundColor LightYellow -HighlightHeaders 'Name' - } -DataStore JavaScript -ScrollX -Title "Complete Classes Configuration" - } - } else { - New-HTMLPanel -Invisible { - New-HTMLText -Text "No DHCP classes configured" -FontSize 14pt -Color Gray - New-HTMLText -Text "DHCP classes can be used for:" -FontSize 12pt -FontWeight bold - New-HTMLList { - New-HTMLListItem -Text "Different configurations for different device types" - New-HTMLListItem -Text "Vendor-specific options (e.g., VoIP phones)" - New-HTMLListItem -Text "Custom grouping of devices" - New-HTMLListItem -Text "Policy-based option assignment" - } - } - } - } - } - } -} \ No newline at end of file + } + } + } + } + + # Server-Level Options + if ($DHCPData.DHCPOptions.Count -gt 0) { + New-HTMLSection -HeaderText "🖥️ Server-Level DHCP Options" -CanCollapse { + $ServerOptions = $DHCPData.DHCPOptions | Group-Object ServerName + foreach ($ServerGroup in $ServerOptions) { + New-HTMLPanel { + New-HTMLTable -DataTable $ServerGroup.Group -Filtering { + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' + } -Title "Server Options for $($ServerGroup.Name)" + } + } + } + } + + # Scope-Level Options + if ($DHCPData.Options.Count -gt 0) { + New-HTMLSection -HeaderText "📁 Scope-Level DHCP Options" -CanCollapse { + New-HTMLTable -DataTable $DHCPData.Options -Filtering { + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' + } -Title "All Scope-Level Options" -DataStore JavaScript -ScrollX + } + } + } + + # DHCP Classes Tab + New-HTMLTab -TabName 'Classes' { + # Classes Overview at the top + if ($DHCPData.DHCPClasses.Count -gt 0) { + New-HTMLSection -HeaderText "📋 DHCP Classes Overview" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Vendor & User Classes Configuration" -FontSize 16pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "DHCP classes allow different configuration based on client type." -FontSize 12pt -Color DarkGray + + # Summary statistics + $VendorClasses = ($DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'Vendor' }).Count + $UserClasses = ($DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'User' }).Count + $TotalServers = ($DHCPData.DHCPClasses | Group-Object ServerName).Count + + New-HTMLSection -HeaderText "Classes Summary" -Invisible -Density Compact { + New-HTMLInfoCard -Title "Total Classes" -Number $DHCPData.DHCPClasses.Count -Subtitle "Configured" -Icon "📋" -TitleColor Purple -NumberColor DarkMagenta + New-HTMLInfoCard -Title "Vendor Classes" -Number $VendorClasses -Subtitle "Device Manufacturers" -Icon "🏭" -TitleColor Blue -NumberColor DarkBlue + New-HTMLInfoCard -Title "User Classes" -Number $UserClasses -Subtitle "Custom Categories" -Icon "👥" -TitleColor Green -NumberColor DarkGreen + New-HTMLInfoCard -Title "Servers" -Number $TotalServers -Subtitle "With Classes" -Icon "🖥️" -TitleColor Orange -NumberColor DarkOrange + } + } + } + } + + # Vendor Classes + $VendorClassData = $DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'Vendor' } + if ($VendorClassData.Count -gt 0) { + New-HTMLSection -HeaderText "🏭 Vendor Classes" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Vendor classes identify device manufacturers and types" -FontSize 12pt + New-HTMLTable -DataTable $VendorClassData -Filtering { + New-HTMLTableCondition -Name 'Name' -ComparisonType string -Operator contains -Value 'Microsoft' -BackgroundColor LightYellow -HighlightHeaders 'Name' + New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'Vendor' -BackgroundColor LightBlue -HighlightHeaders 'Type' + } -DataStore JavaScript + } + } + } + + # User Classes + $UserClassData = $DHCPData.DHCPClasses | Where-Object { $_.Type -eq 'User' } + if ($UserClassData.Count -gt 0) { + New-HTMLSection -HeaderText "👥 User Classes" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "User classes provide custom device categorization" -FontSize 12pt + New-HTMLTable -DataTable $UserClassData -Filtering { + New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'User' -BackgroundColor LightGreen -HighlightHeaders 'Type' + } -DataStore JavaScript + } + } + } + + # All Classes Table + if ($DHCPData.DHCPClasses.Count -gt 0) { + New-HTMLSection -HeaderText "📊 All DHCP Classes" -CanCollapse { + New-HTMLTable -DataTable $DHCPData.DHCPClasses -Filtering { + New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'Vendor' -BackgroundColor LightBlue -HighlightHeaders 'Type' + New-HTMLTableCondition -Name 'Type' -ComparisonType string -Operator eq -Value 'User' -BackgroundColor LightGreen -HighlightHeaders 'Type' + New-HTMLTableCondition -Name 'Name' -ComparisonType string -Operator contains -Value 'Microsoft' -BackgroundColor LightYellow -HighlightHeaders 'Name' + } -DataStore JavaScript -ScrollX -Title "Complete Classes Configuration" + } + } else { + New-HTMLPanel -Invisible { + New-HTMLText -Text "No DHCP classes configured" -FontSize 14pt -Color Gray + New-HTMLText -Text "DHCP classes can be used for:" -FontSize 12pt -FontWeight bold + New-HTMLList { + New-HTMLListItem -Text "Different configurations for different device types" + New-HTMLListItem -Text "Vendor-specific options (e.g., VoIP phones)" + New-HTMLListItem -Text "Custom grouping of devices" + New-HTMLListItem -Text "Policy-based option assignment" + } + } + } + } + } + } +} diff --git a/Private/New-DHCPOptionsTab.ps1 b/Private/New-DHCPOptionsTab.ps1 index 1c04897..6c261e1 100644 --- a/Private/New-DHCPOptionsTab.ps1 +++ b/Private/New-DHCPOptionsTab.ps1 @@ -1,131 +1,142 @@ -function New-DHCPOptionsTab { - <# - .SYNOPSIS - Creates the Options tab content for DHCP HTML report. - #> - [CmdletBinding()] - param([Parameter(Mandatory = $true)][hashtable] $DHCPData) - - New-HTMLTab -TabName '⚙️ Options' { - # Options Health Dashboard at the top - if ($DHCPData.OptionsAnalysis.Count -gt 0) { - New-HTMLSection -HeaderText "⚙️ DHCP Options Health Dashboard" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "DHCP Options Configuration Analysis" -FontSize 18pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "Critical analysis of DHCP options configuration across all servers and scopes." -FontSize 12pt -Color DarkGray - - foreach ($Analysis in $DHCPData.OptionsAnalysis) { - # Health Overview Cards - New-HTMLSection -HeaderText "Configuration Health Overview" -Invisible -Density Compact { - New-HTMLInfoCard -Title "Total Servers" -Number $Analysis.TotalServersAnalyzed -Subtitle "Analyzed" -Icon "🖥️" -TitleColor DodgerBlue -NumberColor Navy - New-HTMLInfoCard -Title "Options Configured" -Number $Analysis.TotalOptionsConfigured -Subtitle "Total Settings" -Icon "⚙️" -TitleColor Purple -NumberColor DarkMagenta - New-HTMLInfoCard -Title "Option Types" -Number $Analysis.UniqueOptionTypes -Subtitle "Different Options" -Icon "🔧" -TitleColor Orange -NumberColor DarkOrange - - if ($Analysis.CriticalOptionsCovered -ge 4) { - New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Good Coverage" -Icon "✅" -TitleColor LimeGreen -NumberColor DarkGreen -ShadowColor 'rgba(50, 205, 50, 0.15)' - } else { - New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Needs Attention" -Icon "⚠️" -TitleColor Crimson -NumberColor DarkRed -ShadowColor 'rgba(220, 20, 60, 0.2)' -ShadowIntensity Bold +function New-DHCPOptionsTab { + <# + .SYNOPSIS + Creates the Options tab content for DHCP HTML report. + #> + [CmdletBinding()] + param([Parameter(Mandatory = $true)][hashtable] $DHCPData) + + New-HTMLTab -TabName '⚙️ Options' { + # Options Health Dashboard at the top + if ($DHCPData.OptionsAnalysis.Count -gt 0) { + New-HTMLSection -HeaderText "⚙️ DHCP Options Health Dashboard" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "DHCP Options Configuration Analysis" -FontSize 18pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "Critical analysis of DHCP options configuration across all servers and scopes." -FontSize 12pt -Color DarkGray + + foreach ($Analysis in $DHCPData.OptionsAnalysis) { + # Health Overview Cards + New-HTMLSection -HeaderText "Configuration Health Overview" -Invisible -Density Compact { + New-HTMLInfoCard -Title "Total Servers" -Number $Analysis.TotalServersAnalyzed -Subtitle "Analyzed" -Icon "🖥️" -TitleColor DodgerBlue -NumberColor Navy + New-HTMLInfoCard -Title "Options Configured" -Number $Analysis.TotalOptionsConfigured -Subtitle "Total Settings" -Icon "⚙️" -TitleColor Purple -NumberColor DarkMagenta + New-HTMLInfoCard -Title "Option Types" -Number $Analysis.UniqueOptionTypes -Subtitle "Different Options" -Icon "🔧" -TitleColor Orange -NumberColor DarkOrange + + if ($Analysis.CriticalOptionsCovered -ge 4) { + New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Good Coverage" -Icon "✅" -TitleColor LimeGreen -NumberColor DarkGreen -ShadowColor 'rgba(50, 205, 50, 0.15)' + } else { + New-HTMLInfoCard -Title "Critical Options" -Number "$($Analysis.CriticalOptionsCovered)/6" -Subtitle "Needs Attention" -Icon "⚠️" -TitleColor Crimson -NumberColor DarkRed -ShadowColor 'rgba(220, 20, 60, 0.2)' -ShadowIntensity Bold + } + } + + # Missing Critical Options + if ($Analysis.MissingCriticalOptions.Count -gt 0) { + New-HTMLSection -HeaderText "🚨 Missing Critical Options" -CanCollapse { + New-HTMLPanel { + New-HTMLText -Text "The following critical DHCP options are not configured:" -FontSize 12pt -Color DarkRed -FontWeight bold + foreach ($MissingOption in $Analysis.MissingCriticalOptions) { + New-HTMLText -Text "🔴 $MissingOption" -Color Red -FontSize 14px + } + } + } + } + + # Configuration Issues + if ($Analysis.OptionIssues.Count -gt 0) { + $IssueDetails = foreach ($Issue in $Analysis.OptionIssues) { + ConvertTo-DHCPOptionIssueRecord -Issue $Issue } - } - - # Missing Critical Options - if ($Analysis.MissingCriticalOptions.Count -gt 0) { - New-HTMLSection -HeaderText "🚨 Missing Critical Options" -CanCollapse { - New-HTMLPanel { - New-HTMLText -Text "The following critical DHCP options are not configured:" -FontSize 12pt -Color DarkRed -FontWeight bold - foreach ($MissingOption in $Analysis.MissingCriticalOptions) { - New-HTMLText -Text "🔴 $MissingOption" -Color Red -FontSize 14px - } + $IssueSummary = $IssueDetails | Group-Object -Property Category | Sort-Object Count -Descending | ForEach-Object { + $sample = $_.Group | Select-Object -First 1 + [PSCustomObject]@{ + Category = $_.Name + IssueCount = $_.Count + AffectedScopes = (@($_.Group.ScopeId | Where-Object { $_ } | Sort-Object -Unique)).Count + AffectedServers= (@($_.Group.ServerName | Where-Object { $_ } | Sort-Object -Unique)).Count + Recommendation = $sample.Recommendation } } - } - # Configuration Issues - if ($Analysis.OptionIssues.Count -gt 0) { New-HTMLSection -HeaderText "⚠️ Configuration Issues Found" -CanCollapse { New-HTMLPanel { - foreach ($Issue in $Analysis.OptionIssues) { - New-HTMLContainer { - if ($Issue -is [string]) { - # Handle string format (test mode) - New-HTMLText -Text "⚠️ $Issue" -Color Orange -FontSize 14px -FontWeight bold - } else { - # Handle object format (production) - New-HTMLText -Text "⚠️ $($Issue.Issue)" -Color Orange -FontSize 14px -FontWeight bold - New-HTMLText -Text "Servers affected: $($Issue.ServersAffected -join ', ')" -Color DarkOrange -FontSize 12px - New-HTMLText -Text "Recommendation: $($Issue.Recommendation)" -Color DarkGray -FontSize 12px -FontStyle italic - } - } - } - } - } - } - - # Recommendations - if ($Analysis.OptionRecommendations.Count -gt 0) { - New-HTMLSection -HeaderText "💡 Recommendations" -CanCollapse { - New-HTMLPanel { - New-HTMLList { - foreach ($Recommendation in $Analysis.OptionRecommendations) { - New-HTMLListItem -Text $Recommendation - } - } + New-HTMLText -Text "The same issues are now grouped by category first, with detailed scope-level records below." -Color DarkOrange -FontSize 12pt -FontWeight bold + New-HTMLTable -DataTable $IssueSummary -Filtering { + New-HTMLTableCondition -Name 'IssueCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightYellow -HighlightHeaders 'IssueCount' + } -DataStore JavaScript -ScrollX -Title 'Issue Summary by Category' + + New-HTMLTable -DataTable $IssueDetails -Filtering { + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Public DNS' -BackgroundColor LightYellow + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Lease Time' -BackgroundColor Moccasin + New-HTMLTableCondition -Name 'Category' -ComparisonType string -Operator eq -Value 'Domain Name' -BackgroundColor Lavender + } -DataStore JavaScript -ScrollX -Title 'Issue Details' } } } - } - } - } - } - - # Server Options Table - Check both DHCPOptions and Options - $ServerOptions = @() - if ($DHCPData.DHCPOptions) { - $ServerOptions = $DHCPData.DHCPOptions | Where-Object { $_.Level -eq 'Server' } - } - if ($ServerOptions.Count -eq 0 -and $DHCPData.Options) { - # If no server-level options in DHCPOptions, check Options - $ServerOptions = $DHCPData.Options | Where-Object { $_.ScopeId -eq 'Server-Level' } - } - if ($ServerOptions.Count -gt 0) { - New-HTMLSection -HeaderText "🖥️ Server-Level Options" -CanCollapse { - New-HTMLTable -DataTable $ServerOptions -Filtering { - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' - } -DataStore JavaScript -ScrollX -Title "Server-Level DHCP Options" - } - } - - # Scope Options Table - $ScopeOptions = $DHCPData.Options | Where-Object { $_.ScopeId -ne 'Server-Level' } - if ($ScopeOptions.Count -gt 0) { - New-HTMLSection -HeaderText "📊 Scope-Level Options" -CanCollapse { - New-HTMLTable -DataTable $ScopeOptions -Filtering { - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' - New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' - } -DataStore JavaScript -ScrollX -Title "Scope-Level DHCP Options" - } - } - - # No Options Configured - if ($DHCPData.DHCPOptions.Count -eq 0 -and $DHCPData.Options.Count -eq 0) { - New-HTMLPanel -Invisible { - New-HTMLText -Text "No DHCP options configured" -FontSize 16pt -Color Gray - New-HTMLText -Text "DHCP options provide essential network configuration to clients:" -FontSize 12pt -FontWeight bold - New-HTMLList { - New-HTMLListItem -Text "Option 3: Router (Default Gateway)" - New-HTMLListItem -Text "Option 6: DNS Servers" - New-HTMLListItem -Text "Option 15: Domain Name" - New-HTMLListItem -Text "Option 42: NTP Servers" - New-HTMLListItem -Text "Option 66/67: TFTP Server and Boot File" - } - } - } - } -} \ No newline at end of file + + # Recommendations + if ($Analysis.OptionRecommendations.Count -gt 0) { + New-HTMLSection -HeaderText "💡 Recommendations" -CanCollapse { + New-HTMLPanel { + New-HTMLList { + foreach ($Recommendation in $Analysis.OptionRecommendations) { + New-HTMLListItem -Text $Recommendation + } + } + } + } + } + } + } + } + } + + # Server Options Table - Check both DHCPOptions and Options + $ServerOptions = @() + if ($DHCPData.DHCPOptions) { + $ServerOptions = $DHCPData.DHCPOptions | Where-Object { $_.Level -eq 'Server' } + } + if ($ServerOptions.Count -eq 0 -and $DHCPData.Options) { + # If no server-level options in DHCPOptions, check Options + $ServerOptions = $DHCPData.Options | Where-Object { $_.ScopeId -eq 'Server-Level' } + } + if ($ServerOptions.Count -gt 0) { + New-HTMLSection -HeaderText "🖥️ Server-Level Options" -CanCollapse { + New-HTMLTable -DataTable $ServerOptions -Filtering { + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' + } -DataStore JavaScript -ScrollX -Title "Server-Level DHCP Options" + } + } + + # Scope Options Table + $ScopeOptions = $DHCPData.Options | Where-Object { $_.ScopeId -ne 'Server-Level' } + if ($ScopeOptions.Count -gt 0) { + New-HTMLSection -HeaderText "📊 Scope-Level Options" -CanCollapse { + New-HTMLTable -DataTable $ScopeOptions -Filtering { + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 3 -BackgroundColor LightGreen -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 6 -BackgroundColor LightBlue -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'OptionId' -ComparisonType number -Operator eq -Value 15 -BackgroundColor LightYellow -HighlightHeaders 'OptionId', 'Name' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '8.8.8.8' -BackgroundColor Orange -HighlightHeaders 'Value' + New-HTMLTableCondition -Name 'Value' -ComparisonType string -Operator contains -Value '1.1.1.1' -BackgroundColor Orange -HighlightHeaders 'Value' + } -DataStore JavaScript -ScrollX -Title "Scope-Level DHCP Options" + } + } + + # No Options Configured + if ($DHCPData.DHCPOptions.Count -eq 0 -and $DHCPData.Options.Count -eq 0) { + New-HTMLPanel -Invisible { + New-HTMLText -Text "No DHCP options configured" -FontSize 16pt -Color Gray + New-HTMLText -Text "DHCP options provide essential network configuration to clients:" -FontSize 12pt -FontWeight bold + New-HTMLList { + New-HTMLListItem -Text "Option 3: Router (Default Gateway)" + New-HTMLListItem -Text "Option 6: DNS Servers" + New-HTMLListItem -Text "Option 15: Domain Name" + New-HTMLListItem -Text "Option 42: NTP Servers" + New-HTMLListItem -Text "Option 66/67: TFTP Server and Boot File" + } + } + } + } +} diff --git a/Private/New-DHCPUtilizationTab.ps1 b/Private/New-DHCPUtilizationTab.ps1 index 038df14..b59c0fc 100644 --- a/Private/New-DHCPUtilizationTab.ps1 +++ b/Private/New-DHCPUtilizationTab.ps1 @@ -1,25 +1,39 @@ -function New-DHCPUtilizationTab { - <# - .SYNOPSIS - Creates the Utilization tab content for DHCP HTML report. - - .DESCRIPTION - This private function generates the Utilization tab which focuses on capacity planning, - utilization trends, growth analysis, and forecasting. - - .PARAMETER DHCPData - The DHCP data object containing all server and scope information. - - .OUTPUTS - New-HTMLTab object containing the Utilization tab content. - #> - [CmdletBinding()] +function New-DHCPUtilizationTab { + <# + .SYNOPSIS + Creates the Utilization tab content for DHCP HTML report. + + .DESCRIPTION + This private function generates the Utilization tab which focuses on capacity planning, + utilization trends, growth analysis, and forecasting. + + .PARAMETER DHCPData + The DHCP data object containing all server and scope information. + + .OUTPUTS + New-HTMLTab object containing the Utilization tab content. + #> + [CmdletBinding()] param( [Parameter(Mandatory = $true)] [hashtable] $DHCPData ) New-HTMLTab -TabName 'Utilization' { + $OnlineServerPerformance = @($DHCPData.ServerPerformanceAnalysis | Where-Object { $_.Status -eq 'Online' } | Sort-Object UtilizationPercent -Descending) + $TopUtilizedScopes = @($DHCPData.Scopes | Where-Object { $_.State -eq 'Active' } | Sort-Object PercentageInUse -Descending | Select-Object -First 10) + $TopServerPerformance = @($OnlineServerPerformance | Select-Object -First 10) + $CriticalUtilizationCount = @($DHCPData.ValidationResults.UtilizationIssues.HighUtilization).Count + $ModerateUtilizationCount = @($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization).Count + $OverallUtilization = [Math]::Round([double]$DHCPData.Statistics.OverallPercentageInUse, 2) + $OverallUtilizationColor = if ($OverallUtilization -gt 90) { + 'Crimson' + } elseif ($OverallUtilization -gt 75) { + 'DarkOrange' + } else { + 'DodgerBlue' + } + # Overall Utilization Summary New-HTMLSection -HeaderText "📊 Overall DHCP Utilization Summary" { New-HTMLPanel -Invisible { @@ -28,58 +42,53 @@ } else { New-HTMLText -Text "Utilization is based on DHCP scope statistics, which include inactive reservations." -FontSize 11pt -Color DarkGray } - # Create utilization gauges - New-HTMLSection -Density Compact -Invisible { - New-HTMLPanel { - #New-HTMLText -Text "Overall IP Utilization" -FontSize 14pt -FontWeight bold -Alignment center - New-HTMLChart { - New-ChartRadial -Name "Used" -Value $DHCPData.Statistics.OverallPercentageInUse - New-ChartRadialOptions -CircleType SemiCircleGauge - } -Title "Overall IP Utilization" - } - - New-HTMLPanel { - New-HTMLText -Text "Address Distribution" -FontSize 14pt -FontWeight bold -Alignment center - New-HTMLChart { - New-ChartDonut -Name "In Use" -Value $DHCPData.Statistics.AddressesInUse -Color '#FF6347' - New-ChartDonut -Name "Available" -Value $DHCPData.Statistics.AddressesFree -Color '#00FF00' - } - } + New-HTMLSection -Invisible -Density Compact { + New-HTMLInfoCard -Title "Overall Utilization" -Number "$OverallUtilization%" -Subtitle "Across all active scopes" -Icon "📊" -TitleColor $OverallUtilizationColor -NumberColor $OverallUtilizationColor + New-HTMLInfoCard -Title "Addresses In Use" -Number ("{0:N0}" -f $DHCPData.Statistics.AddressesInUse) -Subtitle "Currently assigned" -Icon "🔴" -TitleColor 'Crimson' -NumberColor 'DarkRed' + New-HTMLInfoCard -Title "Addresses Available" -Number ("{0:N0}" -f $DHCPData.Statistics.AddressesFree) -Subtitle "Remaining capacity" -Icon "🟢" -TitleColor 'LimeGreen' -NumberColor 'DarkGreen' + New-HTMLInfoCard -Title "Critical Scopes" -Number $CriticalUtilizationCount -Subtitle ">90% utilization" -Icon "🚨" -TitleColor $(if ($CriticalUtilizationCount -gt 0) { 'Crimson' } else { 'LimeGreen' }) -NumberColor $(if ($CriticalUtilizationCount -gt 0) { 'DarkRed' } else { 'DarkGreen' }) + New-HTMLInfoCard -Title "Warning Scopes" -Number $ModerateUtilizationCount -Subtitle "75-90% utilization" -Icon "⚠️" -TitleColor $(if ($ModerateUtilizationCount -gt 0) { 'DarkOrange' } else { 'LimeGreen' }) -NumberColor $(if ($ModerateUtilizationCount -gt 0) { 'DarkOrange' } else { 'DarkGreen' }) + } - if (($DHCPData.ServerPerformanceAnalysis | Where-Object { $_.Status -eq 'Online' }).Count -gt 0) { - New-HTMLPanel -Invisible { - New-HTMLChart -Title "Server Utilization Comparison" { - New-ChartBarOptions -Type bar -Distributed - foreach ($Server in ($DHCPData.ServerPerformanceAnalysis | Where-Object { $_.Status -eq 'Online' })) { - New-ChartBar -Name $Server.ServerName -Value $Server.UtilizationPercent - } - } - } - } - New-HTMLSection -Invisible { - New-HTMLPanel { - New-HTMLText -Text "Capacity Metrics" -FontSize 14pt -FontWeight bold -Alignment center - $CapacityMetrics = [PSCustomObject]@{ - 'Total Addresses' = "{0:N0}" -f $DHCPData.Statistics.TotalAddresses - 'In Use' = "{0:N0}" -f $DHCPData.Statistics.AddressesInUse - 'Available' = "{0:N0}" -f $DHCPData.Statistics.AddressesFree - 'Critical Scopes' = ($DHCPData.ValidationResults.UtilizationIssues.HighUtilization.Count) - 'Warning Scopes' = ($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count) - } - New-HTMLTable -DataTable $CapacityMetrics -HideFooter -DisableSearch -DisablePaging -DisableOrdering - } - } + $CapacityMetrics = [PSCustomObject]@{ + 'Total Addresses' = "{0:N0}" -f $DHCPData.Statistics.TotalAddresses + 'In Use' = "{0:N0}" -f $DHCPData.Statistics.AddressesInUse + 'Available' = "{0:N0}" -f $DHCPData.Statistics.AddressesFree + 'Critical Scopes' = $CriticalUtilizationCount + 'Warning Scopes' = $ModerateUtilizationCount } + New-HTMLTable -DataTable $CapacityMetrics -HideFooter -DisableSearch -DisablePaging -DisableOrdering -Buttons @() -Title 'Capacity Metrics' } } # Utilization by Server if ($DHCPData.ServerPerformanceAnalysis.Count -gt 0) { New-HTMLSection -HeaderText "🖥️ Server Utilization Analysis" -Wrap wrap { - # Server utilization chart - New-HTMLPanel -Invisible { + if ($TopServerPerformance.Count -gt 0) { + $serverRanking = for ($index = 0; $index -lt $TopServerPerformance.Count; $index++) { + [PSCustomObject]@{ + Rank = $index + 1 + ServerName = $TopServerPerformance[$index].ServerName + UtilizationPercent = [Math]::Round([double]$TopServerPerformance[$index].UtilizationPercent, 2) + ActiveScopes = $TopServerPerformance[$index].ActiveScopes + ScopesWithIssues = $TopServerPerformance[$index].ScopesWithIssues + PerformanceRating = $TopServerPerformance[$index].PerformanceRating + CapacityStatus = $TopServerPerformance[$index].CapacityStatus + } + } + + New-HTMLText -Text "Top online servers by utilization, shown as a ranking table to keep the layout readable and avoid chart overflow." -FontSize 11pt -Color DarkSlateGray + New-HTMLTable -DataTable $serverRanking -HideFooter -DisableSearch -DisablePaging -DisableOrdering -Buttons @() -Title 'Top 10 Server Utilization' { + New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Red -Color White -HighlightHeaders 'UtilizationPercent', 'CapacityStatus' + New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 75 -BackgroundColor Orange -HighlightHeaders 'UtilizationPercent' + New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 50 -BackgroundColor Yellow -HighlightHeaders 'UtilizationPercent' + New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Critical' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Orange + } + } + New-HTMLTable -DataTable $DHCPData.ServerPerformanceAnalysis -Filtering { New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Online' -BackgroundColor LightGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Red -Color White -HighlightHeaders 'UtilizationPercent' @@ -87,26 +96,37 @@ New-HTMLTableCondition -Name 'UtilizationPercent' -ComparisonType number -Operator gt -Value 50 -BackgroundColor Yellow -HighlightHeaders 'UtilizationPercent' New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Critical' -BackgroundColor Red -Color White New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Orange - } -DataStore JavaScript -Title "Server Capacity and Performance Metrics" + } -DataStore JavaScript -ScrollX -Title "Server Capacity and Performance Metrics" } } } - + # Scope Utilization Details New-HTMLSection -HeaderText "📈 Scope Utilization Analysis" -Wrap wrap { New-HTMLPanel -Invisible { - # Top 10 utilized scopes chart - $TopUtilizedScopes = $DHCPData.Scopes | Where-Object { $_.State -eq 'Active' } | Sort-Object PercentageInUse -Descending | Select-Object -First 10 + # Top 10 utilized scopes table if ($TopUtilizedScopes.Count -gt 0) { - New-HTMLChart -Title "Top 10 Most Utilized Scopes" { - New-ChartBarOptions -Type bar -Distributed - foreach ($Scope in $TopUtilizedScopes) { - $Color = if ($Scope.PercentageInUse -gt 90) { '#FF0000' } - elseif ($Scope.PercentageInUse -gt 75) { '#FFA500' } - else { '#00FF00' } - New-ChartBar -Name "$($Scope.Name) ($($Scope.ScopeId))" -Value $Scope.PercentageInUse -Color $Color + $scopeRanking = for ($index = 0; $index -lt $TopUtilizedScopes.Count; $index++) { + [PSCustomObject]@{ + Rank = $index + 1 + ServerName = $TopUtilizedScopes[$index].ServerName + ScopeId = [string]$TopUtilizedScopes[$index].ScopeId + Name = $TopUtilizedScopes[$index].Name + PercentageInUse = [Math]::Round([double]$TopUtilizedScopes[$index].PercentageInUse, 2) + AddressesInUse = $TopUtilizedScopes[$index].AddressesInUse + AddressesFree = $TopUtilizedScopes[$index].AddressesFree + CapacityStatus = if ($TopUtilizedScopes[$index].PercentageInUse -gt 90) { 'Critical' } elseif ($TopUtilizedScopes[$index].PercentageInUse -gt 75) { 'Warning' } else { 'Healthy' } } - } -Height 400 + } + + New-HTMLText -Text "Top 10 most utilized scopes, shown as a compact ranking table instead of a chart to keep the layout stable." -FontSize 11pt -Color DarkSlateGray + New-HTMLTable -DataTable $scopeRanking -HideFooter -DisableSearch -DisablePaging -DisableOrdering -Title 'Top 10 Most Utilized Scopes' -Buttons @() { + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Salmon -HighlightHeaders 'PercentageInUse', 'CapacityStatus' + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 75 -BackgroundColor LightYellow -HighlightHeaders 'PercentageInUse' + New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Critical' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Orange + New-HTMLTableCondition -Name 'CapacityStatus' -ComparisonType string -Operator eq -Value 'Healthy' -BackgroundColor LightGreen + } } } # High utilization scopes @@ -116,118 +136,118 @@ New-HTMLTable -DataTable $DHCPData.ValidationResults.UtilizationIssues.HighUtilization -Filtering { New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 95 -BackgroundColor Red -Color White -HighlightHeaders 'PercentageInUse' New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 90 -BackgroundColor Salmon -HighlightHeaders 'PercentageInUse' - } -DataStore JavaScript + } -DataStore JavaScript -ScrollX } } - - # Moderate utilization scopes - if ($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count -gt 0) { + + # Moderate utilization scopes + if ($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count -gt 0) { New-HTMLPanel -Invisible { New-HTMLText -Text "🟠 High Utilization Scopes (75-90%)" -FontSize 16pt -FontWeight bold -Color DarkOrange New-HTMLTable -DataTable $DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization -Filtering { New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 85 -BackgroundColor Orange -HighlightHeaders 'PercentageInUse' New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 75 -BackgroundColor Yellow -HighlightHeaders 'PercentageInUse' - } -DataStore JavaScript + } -DataStore JavaScript -ScrollX } } - - - } - - # Growth Trend Analysis (moved from Scale Analysis) - New-HTMLSection -HeaderText "📈 Capacity Forecasting & Growth Planning" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Growth Trend Analysis" -FontSize 16pt -FontWeight bold -Color DarkBlue - New-HTMLText -Text "Based on current utilization patterns and environment size, plan capacity expansion appropriately." -FontSize 12pt -Color DarkGray - - # Calculate forecasting metrics - $CurrentCapacity = $DHCPData.Statistics.TotalAddresses - $CurrentUsage = $DHCPData.Statistics.AddressesInUse - $CurrentUtilization = $DHCPData.Statistics.OverallPercentageInUse - - # Determine growth rate based on current utilization - $MonthlyGrowthRate = if ($CurrentUtilization -gt 80) { 5 } - elseif ($CurrentUtilization -gt 60) { 3 } - else { 2 } - - # Calculate projections - $Projections = @() - for ($months = 3; $months -le 24; $months += 3) { - $ProjectedUsage = [Math]::Round($CurrentUsage * [Math]::Pow(1 + ($MonthlyGrowthRate / 100), $months)) - $ProjectedUtilization = [Math]::Round(($ProjectedUsage / $CurrentCapacity) * 100, 2) - - $Projections += [PSCustomObject]@{ - 'Time Period' = "+$months months" - 'Projected Usage' = "{0:N0}" -f $ProjectedUsage - 'Projected Utilization' = "$ProjectedUtilization%" - 'Status' = if ($ProjectedUtilization -gt 95) { 'Critical' } - elseif ($ProjectedUtilization -gt 85) { 'Warning' } - else { 'OK' } - 'Action Required' = if ($ProjectedUtilization -gt 95) { 'Immediate expansion needed' } - elseif ($ProjectedUtilization -gt 85) { 'Plan expansion' } - else { 'Monitor' } - } - } - - New-HTMLTable -DataTable $Projections -Filtering { - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Critical' -BackgroundColor Red -Color White - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Orange - New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'OK' -BackgroundColor LightGreen - } -Title "Growth Projections (Monthly Growth Rate: $MonthlyGrowthRate%)" - - # Recommendations - New-HTMLText -Text "Capacity Planning Recommendations:" -FontSize 14pt -FontWeight bold -Color Blue - New-HTMLList { - if ($CurrentUtilization -gt 85) { - New-HTMLListItem -Text "🚨 Critical: Current utilization exceeds 85%. Immediate capacity expansion required." - } - if (($Projections | Where-Object { $_.Status -eq 'Critical' }).Count -gt 0) { - New-HTMLListItem -Text "⚠️ Based on growth projections, capacity will be exhausted within $((($Projections | Where-Object { $_.Status -eq 'Critical' })[0]).'Time Period')" - } - if ($DHCPData.ValidationResults.UtilizationIssues.HighUtilization.Count -gt 0) { - New-HTMLListItem -Text "📊 $($DHCPData.ValidationResults.UtilizationIssues.HighUtilization.Count) scope(s) require immediate expansion" - } - if ($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count -gt 0) { - New-HTMLListItem -Text "📈 $($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count) scope(s) need expansion planning" - } - New-HTMLListItem -Text "💡 Consider implementing DHCP split-scope configuration for load balancing" - New-HTMLListItem -Text "🔄 Review and optimize lease duration settings to improve address recycling" - } - } - } - - # Utilization Heatmap - if ($DHCPData.Scopes.Count -gt 0) { - New-HTMLSection -HeaderText "🗺️ Utilization Heatmap" -CanCollapse { - New-HTMLPanel -Invisible { - New-HTMLText -Text "Visual representation of scope utilization across the infrastructure" -FontSize 12pt -Color DarkGray - - # Create heatmap data - $HeatmapData = foreach ($Scope in ($DHCPData.Scopes | Where-Object { $_.State -eq 'Active' })) { - [PSCustomObject]@{ - 'Scope' = "$($Scope.Name) ($($Scope.ScopeId))" - 'Server' = $Scope.ServerName - 'Utilization' = $Scope.PercentageInUse - 'Status' = if ($Scope.PercentageInUse -gt 90) { '🔴 Critical' } - elseif ($Scope.PercentageInUse -gt 75) { '🟠 High' } - elseif ($Scope.PercentageInUse -gt 50) { '🟡 Moderate' } - else { '🟢 Low' } - } - } - - New-HTMLTable -DataTable $HeatmapData -Filtering { - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 90 -BackgroundColor '#8B0000' -Color White - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 80 -BackgroundColor '#FF0000' -Color White - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 70 -BackgroundColor '#FF4500' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 60 -BackgroundColor '#FFA500' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 50 -BackgroundColor '#FFD700' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 40 -BackgroundColor '#FFFF00' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 30 -BackgroundColor '#ADFF2F' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 20 -BackgroundColor '#00FF00' - New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator le -Value 20 -BackgroundColor '#006400' -Color White - } -DataStore JavaScript - } - } - } - } -} + + + } + + # Growth Trend Analysis (moved from Scale Analysis) + New-HTMLSection -HeaderText "📈 Capacity Forecasting & Growth Planning" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Growth Trend Analysis" -FontSize 16pt -FontWeight bold -Color DarkBlue + New-HTMLText -Text "Based on current utilization patterns and environment size, plan capacity expansion appropriately." -FontSize 12pt -Color DarkGray + + # Calculate forecasting metrics + $CurrentCapacity = $DHCPData.Statistics.TotalAddresses + $CurrentUsage = $DHCPData.Statistics.AddressesInUse + $CurrentUtilization = $DHCPData.Statistics.OverallPercentageInUse + + # Determine growth rate based on current utilization + $MonthlyGrowthRate = if ($CurrentUtilization -gt 80) { 5 } + elseif ($CurrentUtilization -gt 60) { 3 } + else { 2 } + + # Calculate projections + $Projections = @() + for ($months = 3; $months -le 24; $months += 3) { + $ProjectedUsage = [Math]::Round($CurrentUsage * [Math]::Pow(1 + ($MonthlyGrowthRate / 100), $months)) + $ProjectedUtilization = [Math]::Round(($ProjectedUsage / $CurrentCapacity) * 100, 2) + + $Projections += [PSCustomObject]@{ + 'Time Period' = "+$months months" + 'Projected Usage' = "{0:N0}" -f $ProjectedUsage + 'Projected Utilization' = "$ProjectedUtilization%" + 'Status' = if ($ProjectedUtilization -gt 95) { 'Critical' } + elseif ($ProjectedUtilization -gt 85) { 'Warning' } + else { 'OK' } + 'Action Required' = if ($ProjectedUtilization -gt 95) { 'Immediate expansion needed' } + elseif ($ProjectedUtilization -gt 85) { 'Plan expansion' } + else { 'Monitor' } + } + } + + New-HTMLTable -DataTable $Projections -Filtering { + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Critical' -BackgroundColor Red -Color White + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'Warning' -BackgroundColor Orange + New-HTMLTableCondition -Name 'Status' -ComparisonType string -Operator eq -Value 'OK' -BackgroundColor LightGreen + } -Title "Growth Projections (Monthly Growth Rate: $MonthlyGrowthRate%)" + + # Recommendations + New-HTMLText -Text "Capacity Planning Recommendations:" -FontSize 14pt -FontWeight bold -Color Blue + New-HTMLList { + if ($CurrentUtilization -gt 85) { + New-HTMLListItem -Text "🚨 Critical: Current utilization exceeds 85%. Immediate capacity expansion required." + } + if (($Projections | Where-Object { $_.Status -eq 'Critical' }).Count -gt 0) { + New-HTMLListItem -Text "⚠️ Based on growth projections, capacity will be exhausted within $((($Projections | Where-Object { $_.Status -eq 'Critical' })[0]).'Time Period')" + } + if ($DHCPData.ValidationResults.UtilizationIssues.HighUtilization.Count -gt 0) { + New-HTMLListItem -Text "📊 $($DHCPData.ValidationResults.UtilizationIssues.HighUtilization.Count) scope(s) require immediate expansion" + } + if ($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count -gt 0) { + New-HTMLListItem -Text "📈 $($DHCPData.ValidationResults.UtilizationIssues.ModerateUtilization.Count) scope(s) need expansion planning" + } + New-HTMLListItem -Text "💡 Consider implementing DHCP split-scope configuration for load balancing" + New-HTMLListItem -Text "🔄 Review and optimize lease duration settings to improve address recycling" + } + } + } + + # Utilization Heatmap + if ($DHCPData.Scopes.Count -gt 0) { + New-HTMLSection -HeaderText "🗺️ Utilization Heatmap" -CanCollapse { + New-HTMLPanel -Invisible { + New-HTMLText -Text "Visual representation of scope utilization across the infrastructure" -FontSize 12pt -Color DarkGray + + # Create heatmap data + $HeatmapData = foreach ($Scope in ($DHCPData.Scopes | Where-Object { $_.State -eq 'Active' })) { + [PSCustomObject]@{ + 'Scope' = "$($Scope.Name) ($($Scope.ScopeId))" + 'Server' = $Scope.ServerName + 'Utilization' = $Scope.PercentageInUse + 'Status' = if ($Scope.PercentageInUse -gt 90) { '🔴 Critical' } + elseif ($Scope.PercentageInUse -gt 75) { '🟠 High' } + elseif ($Scope.PercentageInUse -gt 50) { '🟡 Moderate' } + else { '🟢 Low' } + } + } + + New-HTMLTable -DataTable $HeatmapData -Filtering { + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 90 -BackgroundColor '#8B0000' -Color White + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 80 -BackgroundColor '#FF0000' -Color White + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 70 -BackgroundColor '#FF4500' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 60 -BackgroundColor '#FFA500' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 50 -BackgroundColor '#FFD700' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 40 -BackgroundColor '#FFFF00' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 30 -BackgroundColor '#ADFF2F' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator gt -Value 20 -BackgroundColor '#00FF00' + New-HTMLTableCondition -Name 'Utilization' -ComparisonType number -Operator le -Value 20 -BackgroundColor '#006400' -Color White + } -DataStore JavaScript + } + } + } + } +} diff --git a/Private/New-DHCPValidationIssuesTab.ps1 b/Private/New-DHCPValidationIssuesTab.ps1 index 00cc159..e240d01 100644 --- a/Private/New-DHCPValidationIssuesTab.ps1 +++ b/Private/New-DHCPValidationIssuesTab.ps1 @@ -1,178 +1,184 @@ -function New-DHCPValidationIssuesTab { - <# - .SYNOPSIS - Creates the Validation Issues tab content for DHCP HTML report. - - .DESCRIPTION - This private function generates the Validation Issues tab which includes critical issues, - warning issues, and informational issues. Utilization issues are shown in the dedicated - Utilization tab for comprehensive analysis. - - .PARAMETER DHCPData - The DHCP data object containing all server and scope information. - - .OUTPUTS - New-HTMLTab object containing the Validation Issues tab content. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [hashtable] $DHCPData - ) - - New-HTMLTab -TabName 'Validation Issues' { - # Calculate total issues from validation results (excluding utilization which has its own tab) - $TotalIssuesCount = $DHCPData.ValidationResults.Summary.TotalCriticalIssues + - $DHCPData.ValidationResults.Summary.TotalWarningIssues + - $DHCPData.ValidationResults.Summary.TotalInfoIssues - - if ($TotalIssuesCount -eq 0) { - New-HTMLSection -HeaderText "Validation Status" { - New-HTMLPanel -Invisible { - New-HTMLText -Text "✅ No validation issues found" -Color Green -FontSize 16pt -FontWeight bold - New-HTMLText -Text "All DHCP servers and scopes appear to be properly configured and operating within normal parameters." -FontSize 12pt - New-HTMLText -Text "Note: Check the Utilization tab for capacity planning and utilization analysis." -FontSize 11pt -Color Blue - } - } - } - - # Critical Issues Section - if ($DHCPData.ValidationResults.Summary.TotalCriticalIssues -gt 0) { - New-HTMLSection -HeaderText "Critical Issues" -HeaderTextColor '#8b0000' -Density Compact { - # Public DNS with Updates - if ($DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates.Count -gt 0) { - # New-HTMLSection -Invisible { - New-HTMLText -Text "⚠️ Public DNS Servers with Dynamic Updates Enabled" -FontSize 14pt -FontWeight bold -Color '#cc0000' - New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates -Filtering { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen -FailBackgroundColor Orange - } -DataStore JavaScript -ScrollX - # } - } - - # DNS configuration problems (aggregated when policy enabled) - if ($DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems.Count -gt 0) { - #New-HTMLSection -Invisible { - New-HTMLText -Text "⚠️ Scopes with DNS Configuration Problems" -FontSize 14pt -FontWeight bold -Color '#cc0000' - New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems -Filtering -DataStore JavaScript -ScrollX - #} - } - - # Servers Offline - if ($DHCPData.ValidationResults.CriticalIssues.ServersOffline.Count -gt 0) { - New-HTMLContainer { - New-HTMLText -Text "⚠️ Offline DHCP Servers" -FontSize 14pt -FontWeight bold -Color '#cc0000' - New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.ServersOffline -Filtering -DataStore JavaScript - } - } - - # Scopes assigned only to Partner A (missing on Partner B) — critical - if ($DHCPData.ValidationResults.CriticalIssues.FailoverOnlyOnPrimary.Count -gt 0) { - New-HTMLSection -HeaderText "🔴 Failover Scope Mismatches: Assigned on Partner A only (missing on Partner B)" -CanCollapse { - $data = $DHCPData.ValidationResults.CriticalIssues.FailoverOnlyOnPrimary | ForEach-Object { - [PSCustomObject]@{ - Relationship = $_.Relationship - PartnerA = $_.PrimaryServer - PartnerB = $_.SecondaryServer - ScopeId = $_.ScopeId - FailoverConfiguration = 'missing on secondary' - Issue = $_.Issue - } - } - New-HTMLTable -DataTable $data -Filtering { - New-HTMLTableCondition -Name 'FailoverConfiguration' -ComparisonType string -Operator contains -Value 'missing' -BackgroundColor Salmon - } -DataStore JavaScript -ScrollX - } - } - - # Missing on both partners — critical - if ($DHCPData.ValidationResults.CriticalIssues.FailoverMissingOnBoth.Count -gt 0) { - New-HTMLSection -HeaderText "🔴 Scopes Missing from Failover on Both Partners" -CanCollapse { - New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.FailoverMissingOnBoth -Filtering { - New-HTMLTableCondition -Name 'Issue' -ComparisonType string -Operator contains -Value 'both' -BackgroundColor Salmon -HighlightHeaders 'Issue' - } -DataStore JavaScript -ScrollX - } - } - } - } - - # Warning Issues Section - if ($DHCPData.ValidationResults.Summary.TotalWarningIssues -gt 0) { - New-HTMLSection -HeaderText "Warning Issues" -HeaderTextColor '#cc8800' -Density Compact { - # Missing Failover - if ($DHCPData.ValidationResults.WarningIssues.MissingFailover.Count -gt 0) { - New-HTMLSection -HeaderText "⚡ Active Scopes without Failover Configuration" -CanCollapse { - New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.MissingFailover -Filtering { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen - New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 75 -BackgroundColor Orange -HighlightHeaders 'PercentageInUse' - } -DataStore JavaScript -ScrollX - } - } - - # NOTE: 'Failover only on primary' moved to Critical section - - # Scopes assigned only to Partner B (missing on Partner A) - if ($DHCPData.ValidationResults.WarningIssues.FailoverOnlyOnSecondary.Count -gt 0) { - New-HTMLSection -HeaderText "🔄 Failover Scope Mismatches: Assigned on Partner B only (missing on Partner A)" -CanCollapse { - $data = $DHCPData.ValidationResults.WarningIssues.FailoverOnlyOnSecondary | ForEach-Object { - [PSCustomObject]@{ - Relationship = $_.Relationship - PartnerA = $_.PrimaryServer - PartnerB = $_.SecondaryServer - ScopeId = $_.ScopeId - FailoverConfiguration = 'missing on primary' - Issue = $_.Issue - } - } - New-HTMLTable -DataTable $data -Filtering { - New-HTMLTableCondition -Name 'FailoverConfiguration' -ComparisonType string -Operator contains -Value 'missing' -BackgroundColor LightYellow - } -DataStore JavaScript -ScrollX - } - } - - # NOTE: 'Missing on both' moved to Critical section - - # Extended Lease Duration - if ($DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration.Count -gt 0) { - New-HTMLSection -HeaderText "Scopes with Extended Lease Duration (>48 hours)" -CanCollapse { - # Title moved to section header - New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration -Filtering { - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 168 -BackgroundColor Salmon -HighlightHeaders 'LeaseDurationHours' - New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Orange -HighlightHeaders 'LeaseDurationHours' - } -DataStore JavaScript -ScrollX - } - } - +function New-DHCPValidationIssuesTab { + <# + .SYNOPSIS + Creates the Validation Issues tab content for DHCP HTML report. + + .DESCRIPTION + This private function generates the Validation Issues tab which includes critical issues, + warning issues, and informational issues. Utilization issues are shown in the dedicated + Utilization tab for comprehensive analysis. + + .PARAMETER DHCPData + The DHCP data object containing all server and scope information. + + .OUTPUTS + New-HTMLTab object containing the Validation Issues tab content. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [hashtable] $DHCPData + ) + + New-HTMLTab -TabName 'Validation Issues' { + # Calculate total issues from validation results (excluding utilization which has its own tab) + $TotalIssuesCount = $DHCPData.ValidationResults.Summary.TotalCriticalIssues + + $DHCPData.ValidationResults.Summary.TotalWarningIssues + + $DHCPData.ValidationResults.Summary.TotalInfoIssues + + if ($TotalIssuesCount -eq 0) { + New-HTMLSection -HeaderText "Validation Status" { + New-HTMLPanel -Invisible { + New-HTMLText -Text "✅ No validation issues found" -Color Green -FontSize 16pt -FontWeight bold + New-HTMLText -Text "All DHCP servers and scopes appear to be properly configured and operating within normal parameters." -FontSize 12pt + New-HTMLText -Text "Note: Check the Utilization tab for capacity planning and utilization analysis." -FontSize 11pt -Color Blue + } + } + } + + # Critical Issues Section + if ($DHCPData.ValidationResults.Summary.TotalCriticalIssues -gt 0) { + New-HTMLSection -HeaderText "Critical Issues" -HeaderTextColor '#8b0000' -Density Compact { + # Public DNS with Updates + if ($DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates.Count -gt 0) { + # New-HTMLSection -Invisible { + New-HTMLText -Text "⚠️ Public DNS Servers with Dynamic Updates Enabled" -FontSize 14pt -FontWeight bold -Color '#cc0000' + New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.PublicDNSWithUpdates -Filtering { + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen -FailBackgroundColor Orange + } -DataStore JavaScript -ScrollX + # } + } + + # DNS configuration problems (aggregated when policy enabled) + if ($DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems.Count -gt 0) { + #New-HTMLSection -Invisible { + New-HTMLText -Text "⚠️ Scopes with DNS Configuration Problems" -FontSize 14pt -FontWeight bold -Color '#cc0000' + New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.DNSConfigurationProblems -Filtering -DataStore JavaScript -ScrollX + #} + } + + # Servers Offline + if ($DHCPData.ValidationResults.CriticalIssues.ServersOffline.Count -gt 0) { + New-HTMLContainer { + New-HTMLText -Text "⚠️ Offline DHCP Servers" -FontSize 14pt -FontWeight bold -Color '#cc0000' + New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.ServersOffline -Filtering -DataStore JavaScript + } + } + + # Scopes assigned only to Partner A (missing on Partner B) — critical + if ($DHCPData.ValidationResults.CriticalIssues.FailoverOnlyOnPrimary.Count -gt 0) { + New-HTMLSection -HeaderText "🔴 Failover Scope Mismatches: Assigned on Partner A only (missing on Partner B)" -CanCollapse { + $data = $DHCPData.ValidationResults.CriticalIssues.FailoverOnlyOnPrimary | ForEach-Object { + [PSCustomObject]@{ + Relationship = $_.Relationship + PartnerA = $_.PrimaryServer + PartnerB = $_.SecondaryServer + ScopeId = $_.ScopeId + FailoverConfiguration = 'missing on secondary' + Issue = $_.Issue + } + } + New-HTMLTable -DataTable $data -Filtering { + New-HTMLTableCondition -Name 'FailoverConfiguration' -ComparisonType string -Operator contains -Value 'missing' -BackgroundColor Salmon + } -DataStore JavaScript -ScrollX + } + } + + # Missing on both partners — critical + if ($DHCPData.ValidationResults.CriticalIssues.FailoverMissingOnBoth.Count -gt 0) { + New-HTMLSection -HeaderText "🔴 Scopes Missing from Failover on Both Partners" -CanCollapse { + New-HTMLTable -DataTable $DHCPData.ValidationResults.CriticalIssues.FailoverMissingOnBoth -Filtering { + New-HTMLTableCondition -Name 'Issue' -ComparisonType string -Operator contains -Value 'both' -BackgroundColor Salmon -HighlightHeaders 'Issue' + } -DataStore JavaScript -ScrollX + } + } + } + } + + # Warning Issues Section + if ($DHCPData.ValidationResults.Summary.TotalWarningIssues -gt 0) { + New-HTMLSection -HeaderText "Warning Issues" -HeaderTextColor '#cc8800' -Density Compact { + # Missing Failover + if ($DHCPData.ValidationResults.WarningIssues.MissingFailover.Count -gt 0) { + New-HTMLSection -HeaderText "⚡ Active Scopes without Failover Configuration" -CanCollapse { + New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.MissingFailover -Filtering { + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator eq -Value 'Active' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'PercentageInUse' -ComparisonType number -Operator gt -Value 75 -BackgroundColor Orange -HighlightHeaders 'PercentageInUse' + } -DataStore JavaScript -ScrollX + } + } + + # NOTE: 'Failover only on primary' moved to Critical section + + # Scopes assigned only to Partner B (missing on Partner A) + if ($DHCPData.ValidationResults.WarningIssues.FailoverOnlyOnSecondary.Count -gt 0) { + New-HTMLSection -HeaderText "🔄 Failover Scope Mismatches: Assigned on Partner B only (missing on Partner A)" -CanCollapse { + $data = $DHCPData.ValidationResults.WarningIssues.FailoverOnlyOnSecondary | ForEach-Object { + [PSCustomObject]@{ + Relationship = $_.Relationship + PartnerA = $_.PrimaryServer + PartnerB = $_.SecondaryServer + ScopeId = $_.ScopeId + FailoverConfiguration = 'missing on primary' + Issue = $_.Issue + } + } + New-HTMLTable -DataTable $data -Filtering { + New-HTMLTableCondition -Name 'FailoverConfiguration' -ComparisonType string -Operator contains -Value 'missing' -BackgroundColor LightYellow + } -DataStore JavaScript -ScrollX + } + } + + # NOTE: 'Missing on both' moved to Critical section + + # Extended Lease Duration + if ($DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration.Count -gt 0) { + New-HTMLSection -HeaderText "Scopes with Extended Lease Duration (>48 hours)" -CanCollapse { + # Title moved to section header + New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.ExtendedLeaseDuration -Filtering { + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 168 -BackgroundColor Salmon -HighlightHeaders 'LeaseDurationHours' + New-HTMLTableCondition -Name 'LeaseDurationHours' -ComparisonType number -Operator gt -Value 48 -BackgroundColor Orange -HighlightHeaders 'LeaseDurationHours' + } -DataStore JavaScript -ScrollX + } + } + # DNS Record Management Issues if ($DHCPData.ValidationResults.WarningIssues.DNSRecordManagement.Count -gt 0) { New-HTMLSection -HeaderText "DNS Record Management Issues" -CanCollapse { - # Title moved to section header - New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.DNSRecordManagement -Filtering -DataStore JavaScript -ScrollX - } - } - } - } - - # Information Issues Section - if ($DHCPData.ValidationResults.Summary.TotalInfoIssues -gt 0) { - New-HTMLSection -HeaderText "Information Issues" -BackgroundColor '#e6f3ff' -HeaderTextColor '#0066cc' -Density Compact { - # Missing Domain Name - if ($DHCPData.ValidationResults.InfoIssues.MissingDomainName.Count -gt 0) { - New-HTMLContainer { - New-HTMLText -Text "ℹ️ Scopes Missing Domain Name Option" -FontSize 14pt -FontWeight bold -Color DarkBlue - New-HTMLTable -DataTable $DHCPData.ValidationResults.InfoIssues.MissingDomainName -Filtering -DataStore JavaScript -ScrollX - } - } - - # Inactive Scopes - if ($DHCPData.ValidationResults.InfoIssues.InactiveScopes.Count -gt 0) { - New-HTMLContainer { - New-HTMLText -Text "💤 Inactive DHCP Scopes" -FontSize 14pt -FontWeight bold -Color DarkBlue - New-HTMLTable -DataTable $DHCPData.ValidationResults.InfoIssues.InactiveScopes -Filtering { - New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator ne -Value 'Active' -BackgroundColor LightYellow -HighlightHeaders 'State' - } -DataStore JavaScript -ScrollX + New-HTMLText -Text "Scopes with DNS record management problems:" -FontSize 14pt -FontWeight bold -Color '#cc8800' + New-HTMLTable -DataTable $DHCPData.ValidationResults.WarningIssues.DNSRecordManagement -Filtering -DataStore JavaScript -ScrollX { + New-HTMLTableCondition -Name 'UpdateDnsRRForOlderClients' -ComparisonType bool -Operator eq -Value $false -BackgroundColor LightYellow + New-HTMLTableCondition -Name 'DeleteDnsRROnLeaseExpiry' -ComparisonType bool -Operator eq -Value $false -BackgroundColor LightYellow + New-HTMLTableCondition -Name 'DisableDnsPtrRRUpdate' -ComparisonType bool -Operator eq -Value $true -BackgroundColor Orange + } -IncludeProperty @( + 'ServerName', 'ScopeId', 'Name', 'UpdateDnsRRForOlderClients', 'DeleteDnsRROnLeaseExpiry', 'DisableDnsPtrRRUpdate', 'DynamicUpdates' + ) } } - } - } - } -} + } + } + + # Information Issues Section + if ($DHCPData.ValidationResults.Summary.TotalInfoIssues -gt 0) { + New-HTMLSection -HeaderText "Information Issues" -BackgroundColor '#e6f3ff' -HeaderTextColor '#0066cc' -Density Compact { + # Missing Domain Name + if ($DHCPData.ValidationResults.InfoIssues.MissingDomainName.Count -gt 0) { + New-HTMLContainer { + New-HTMLText -Text "ℹ️ Scopes Missing Domain Name Option" -FontSize 14pt -FontWeight bold -Color DarkBlue + New-HTMLTable -DataTable $DHCPData.ValidationResults.InfoIssues.MissingDomainName -Filtering -DataStore JavaScript -ScrollX + } + } + + # Inactive Scopes + if ($DHCPData.ValidationResults.InfoIssues.InactiveScopes.Count -gt 0) { + New-HTMLContainer { + New-HTMLText -Text "💤 Inactive DHCP Scopes" -FontSize 14pt -FontWeight bold -Color DarkBlue + New-HTMLTable -DataTable $DHCPData.ValidationResults.InfoIssues.InactiveScopes -Filtering { + New-HTMLTableCondition -Name 'State' -ComparisonType string -Operator ne -Value 'Active' -BackgroundColor LightYellow -HighlightHeaders 'State' + } -DataStore JavaScript -ScrollX + } + } + } + } + } +} diff --git a/Tests/DHCP.Failover.Tests.ps1 b/Tests/DHCP.Failover.Tests.ps1 index c2ed403..aa866d7 100644 --- a/Tests/DHCP.Failover.Tests.ps1 +++ b/Tests/DHCP.Failover.Tests.ps1 @@ -2,30 +2,57 @@ Describe 'DHCP Failover Analysis (TestMode)' { BeforeAll { Import-Module "$PSScriptRoot/../ADEssentials.psm1" -Force } + + It 'Unions scopes across multiple relationships per partner pair' { + $s = Get-WinADDHCPSummary -TestMode -Minimal + + # Baseline counts from the bundled TestMode data + $s.FailoverRelationships.Count | Should -BeGreaterThan 0 + $s.FailoverAnalysis | Should -Not -BeNullOrEmpty + + # These expectations are tied to Private/Get-TestModeDHCPData.ps1 + # and validate that pair-wise union + normalization works. + $s.FailoverRelationships.Count | Should -Be 7 + $s.FailoverAnalysis.PerSubnetIssues.Count | Should -Be 6 + $s.FailoverAnalysis.OnlyOnPrimary.Count | Should -Be 1 + $s.FailoverAnalysis.OnlyOnSecondary.Count | Should -Be 1 + $s.FailoverAnalysis.MissingOnBoth.Count | Should -Be 1 + } + + It 'Does not duplicate per-subnet issues across relationships' { + $s = Get-WinADDHCPSummary -TestMode -Minimal + $set = New-Object 'System.Collections.Generic.HashSet[string]' + foreach ($i in $s.FailoverAnalysis.PerSubnetIssues) { + $key = "$($i.PrimaryServer)↔$($i.SecondaryServer)|$($i.ScopeId)|$($i.Issue)" + $added = $set.Add($key) + $added | Should -BeTrue + } + } +} - It 'Unions scopes across multiple relationships per partner pair' { +Describe 'DHCP DNS record management validation (TestMode)' { + BeforeAll { + Import-Module "$PSScriptRoot/../ADEssentials.psm1" -Force + } + + It 'Flags scopes when PTR registration updates are disabled' { $s = Get-WinADDHCPSummary -TestMode -Minimal - # Baseline counts from the bundled TestMode data - $s.FailoverRelationships.Count | Should -BeGreaterThan 0 - $s.FailoverAnalysis | Should -Not -BeNullOrEmpty + $ptrIssue = $s.ValidationResults.WarningIssues.DNSRecordManagement | Where-Object { [string] $_.ScopeId -eq '10.1.0.0' } | Select-Object -First 1 + $ptrIssue | Should -Not -BeNullOrEmpty + $ptrIssue.DisableDnsPtrRRUpdate | Should -BeTrue + $ptrIssue.Issues | Should -Contain 'PTR registration disabled' + } +} - # These expectations are tied to Private/Get-TestModeDHCPData.ps1 - # and validate that pair-wise union + normalization works. - $s.FailoverRelationships.Count | Should -Be 7 - $s.FailoverAnalysis.PerSubnetIssues.Count | Should -Be 6 - $s.FailoverAnalysis.OnlyOnPrimary.Count | Should -Be 1 - $s.FailoverAnalysis.OnlyOnSecondary.Count | Should -Be 1 - $s.FailoverAnalysis.MissingOnBoth.Count | Should -Be 1 +Describe 'DHCP option issue parsing' { + BeforeAll { + Import-Module "$PSScriptRoot/../ADEssentials.psm1" -Force } - It 'Does not duplicate per-subnet issues across relationships' { - $s = Get-WinADDHCPSummary -TestMode -Minimal - $set = New-Object 'System.Collections.Generic.HashSet[string]' - foreach ($i in $s.FailoverAnalysis.PerSubnetIssues) { - $key = "$($i.PrimaryServer)↔$($i.SecondaryServer)|$($i.ScopeId)|$($i.Issue)" - $added = $set.Add($key) - $added | Should -BeTrue + It 'Returns null for empty or whitespace issue text' { + InModuleScope ADEssentials { + ConvertTo-DHCPOptionIssueRecord -Issue ' ' | Should -BeNullOrEmpty } } } @@ -34,11 +61,11 @@ 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 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' { @@ -282,4 +309,4 @@ Describe 'DHCP Server Exclusions Affect All Outcomes (TestMode)' { ($analysisServers -join ',') | Should -Not -Match 'usfsm-|it-' } } - + diff --git a/Website/README.md b/Website/README.md new file mode 100644 index 0000000..66138d1 --- /dev/null +++ b/Website/README.md @@ -0,0 +1,19 @@ +# ADEssentials Website Content + +This folder contains curated source content that the Evotec website imports for the ADEssentials project page. + +## Layout + +- `content/project-docs/` contains short project documentation pages shown under /projects/adessentials/docs/. +- `content/examples/` contains curated website examples shown under /projects/adessentials/examples/. +- API artifacts are not enabled from this folder yet. Add `WebsiteArtifacts/apidocs` and wire it in the website catalog only when external help/API metadata is generated and reviewed. + +## Editing Rules + +- Keep this folder intentional and small. +- Do not mirror raw `Examples/`, `Example/`, or generated output folders into the public website. +- Add only examples that have a clear explanation, a small code sample, and a link back to the original source file. +- Keep links rooted at /projects/adessentials/ so the same content works on localhost, evotec.xyz, and evotec.pl. + +The website pipeline prefers this `Website/content/...` layout over legacy root-level content folders. + diff --git a/Website/content/examples/_index.md b/Website/content/examples/_index.md new file mode 100644 index 0000000..d7cfce0 --- /dev/null +++ b/Website/content/examples/_index.md @@ -0,0 +1,22 @@ +--- +title: "ADEssentials Examples" +description: "Curated examples for Active Directory and infrastructure diagnostics with ADEssentials." +layout: docs +--- + +These ADEssentials examples focus on targeted Active Directory checks that are useful before moving into a larger reporting workflow. + +## Featured examples + + diff --git a/Website/content/examples/check-active-directory-backup-age.md b/Website/content/examples/check-active-directory-backup-age.md new file mode 100644 index 0000000..d2ad2de --- /dev/null +++ b/Website/content/examples/check-active-directory-backup-age.md @@ -0,0 +1,39 @@ +--- +title: "Check Active Directory backup age" +description: "Use ADEssentials to review the last known Active Directory backup state across a forest or selected domains." +layout: docs +--- + +This example is useful when you need a quick recovery-readiness check before broader Active Directory troubleshooting or change work. + +It comes from the source example at `Examples/Example.06-CheckLastBackup.ps1`. + +## When to use this pattern + +- You need to confirm whether Active Directory backups are recent enough. +- You want a forest-wide check before planning changes. +- You need a narrower check for one or more selected domains. + +## Example + +```powershell +Import-Module .\ADEssentials.psd1 -Force + +# Check the whole forest +$LastBackup = Get-WinADLastBackup +$LastBackup | Format-Table -AutoSize + +# Check selected domains +$LastBackup = Get-WinADLastBackup -Domain 'corp.example.com', 'child.corp.example.com' +$LastBackup | Format-Table -AutoSize +``` + +## What this demonstrates + +- checking recovery readiness before deeper work +- running both broad and targeted domain checks +- producing a compact table that can be shared with the directory team + +## Source + +- [Example.06-CheckLastBackup.ps1](https://github.com/EvotecIT/ADEssentials/blob/master/Examples/Example.06-CheckLastBackup.ps1) diff --git a/Website/content/examples/verify-ldap-connectivity-and-certificates.md b/Website/content/examples/verify-ldap-connectivity-and-certificates.md new file mode 100644 index 0000000..1926d65 --- /dev/null +++ b/Website/content/examples/verify-ldap-connectivity-and-certificates.md @@ -0,0 +1,35 @@ +--- +title: "Verify LDAP connectivity and certificates" +description: "Test LDAP endpoints and certificate validation from a PowerShell-based AD health workflow." +layout: docs +--- + +This example is useful when you need a quick LDAP verification pass before deeper Active Directory troubleshooting. + +It comes from the source example at `Examples/Example.06-TestLDAPPorts.ps1`. + +## When to use this pattern + +- You need to confirm LDAP connectivity to domain controllers. +- You want to validate certificate-backed LDAP communication. +- You are checking whether a directory issue is network, certificate, or service related. + +## Example + +```powershell +Import-Module .\ADEssentials.psd1 -Force + +Test-LDAP -IncludeDomainControllers 'dc01.corp.example.com' -Identity "krbtgt" -Verbose | Format-List * +Test-LDAP -ComputerName 'corp.example.com' -VerifyCertificate -Verbose | Format-Table +Test-LDAP -VerifyCertificate -Verbose -IncludeDomains 'corp.example.com' | Format-Table +``` + +## What this demonstrates + +- testing specific domain controllers directly +- validating LDAP over certificate-backed channels +- comparing narrow and broad validation runs from the same command family + +## Source + +- [Example.06-TestLDAPPorts.ps1](https://github.com/EvotecIT/ADEssentials/blob/master/Examples/Example.06-TestLDAPPorts.ps1) diff --git a/Website/content/project-docs/docs/_index.md b/Website/content/project-docs/docs/_index.md new file mode 100644 index 0000000..8348296 --- /dev/null +++ b/Website/content/project-docs/docs/_index.md @@ -0,0 +1,13 @@ +--- +title: "ADEssentials Docs" +description: "Curated documentation workspace for ADEssentials." +layout: docs +--- + +ADEssentials collects practical Active Directory helper functions for diagnostics, cleanup, and operational checks. + +## Start here + +- [Installation](./install/) +- [Project overview](./overview/) +- [Back to project overview](/projects/adessentials/) diff --git a/Website/content/project-docs/docs/install.md b/Website/content/project-docs/docs/install.md new file mode 100644 index 0000000..713659c --- /dev/null +++ b/Website/content/project-docs/docs/install.md @@ -0,0 +1,18 @@ +--- +title: "Install ADEssentials" +description: "Install ADEssentials from the package source used by this project." +layout: docs +--- + +Install ADEssentials before trying the curated Active Directory examples. + +## PowerShell Gallery + +```powershell +Install-Module ADEssentials -Scope CurrentUser +``` + +## Next steps + +- Review the [project overview](../overview/) +- Browse the [curated examples](/projects/adessentials/examples/) diff --git a/Website/content/project-docs/docs/overview.md b/Website/content/project-docs/docs/overview.md new file mode 100644 index 0000000..8b32a05 --- /dev/null +++ b/Website/content/project-docs/docs/overview.md @@ -0,0 +1,18 @@ +--- +title: "ADEssentials overview" +description: "ADEssentials collects practical Active Directory helper functions for diagnostics, cleanup, and operational checks." +layout: docs +--- + +Use ADEssentials when Active Directory work needs focused checks rather than a full reporting framework. It is useful for targeted health, connectivity, and recovery-readiness tasks. + +## Typical use + +- LDAP and domain controller connectivity checks +- Active Directory backup and recovery readiness +- small helper commands used by larger Evotec automation modules + +## Related project pages + +- [Project overview](/projects/adessentials/) +- [Examples](/projects/adessentials/examples/) diff --git a/Website/content/project-docs/docs/toc.yml b/Website/content/project-docs/docs/toc.yml new file mode 100644 index 0000000..b2a8c8b --- /dev/null +++ b/Website/content/project-docs/docs/toc.yml @@ -0,0 +1,6 @@ +- title: Docs Home + href: ./ +- title: Install + href: install/ +- title: Overview + href: overview/