#Requires -RunAsAdministrator
<#
.SYNOPSIS
BluetechGreen Windows Login Diagnostic Tool
.DESCRIPTION
Analyzes Windows login performance, Group Policy processing,
network connectivity, and system health. Generates an HTML report
on the current user's Desktop.
.NOTES
Version: 1.0
Author: BluetechGreen LLC
Website: https://bluetechgreen.com
License: Free for use. Redistribution permitted with attribution.
.EXAMPLE
.\BTG-LoginDiag.ps1
Run from an elevated PowerShell prompt. Report opens automatically on completion.
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Continue'
$ScriptStart = Get-Date
# ─── Helpers ───────────────────────────────────────────────────────────────────
function Write-Status {
param([string]$Message, [string]$Color = 'Cyan')
Write-Host " [BTG] $Message" -ForegroundColor $Color
}
function Safe-Invoke {
param([scriptblock]$Block, [string]$Section)
try { & $Block }
catch {
Write-Status "WARNING: Section '$Section' encountered an error: $($_.Exception.Message)" 'Yellow'
$null
}
}
function Format-Ms {
param([double]$Milliseconds)
if ($Milliseconds -ge 60000) { return "{0:N1} min" -f ($Milliseconds / 60000) }
if ($Milliseconds -ge 1000) { return "{0:N1} s" -f ($Milliseconds / 1000) }
return "{0:N0} ms" -f $Milliseconds
}
function Severity-Class {
param([double]$ValueMs, [double]$WarnMs, [double]$CritMs)
if ($ValueMs -ge $CritMs) { return 'sev-red' }
if ($ValueMs -ge $WarnMs) { return 'sev-yellow' }
return 'sev-green'
}
# ─── Section 1 — Login / Boot Time Analysis ────────────────────────────────────
Write-Status "Collecting login time data..."
$LoginData = Safe-Invoke -Section 'LoginTime' -Block {
$results = @{
BootEvents = @()
GpEvents = @()
ProfileEvents = @()
BootMs = 0
GpMs = 0
ProfileMs = 0
}
# Boot performance — Event ID 100 in Diagnostics-Performance
$bootEvents = Get-WinEvent -LogName 'Microsoft-Windows-Diagnostics-Performance/Operational' `
-FilterXPath "*[System[EventID=100]]" -MaxEvents 10 -ErrorAction SilentlyContinue
foreach ($evt in $bootEvents) {
$xml = [xml]$evt.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
$bootMs = try { [double]($xml.SelectSingleNode('//e:Data[@Name="BootTsMs"]', $ns).'#text') } catch { 0 }
$mainPathMs = try { [double]($xml.SelectSingleNode('//e:Data[@Name="MainPathBootTime"]', $ns).'#text') } catch { 0 }
$results.BootEvents += [PSCustomObject]@{
Time = $evt.TimeCreated
BootMs = $bootMs
MainMs = $mainPathMs
}
}
if ($results.BootEvents.Count -gt 0) {
$results.BootMs = ($results.BootEvents | Measure-Object BootMs -Average).Average
}
# Group Policy processing
$gpEvents = Get-WinEvent -LogName 'Microsoft-Windows-GroupPolicy/Operational' `
-FilterXPath "*[System[EventID=8001 or EventID=8004]]" -MaxEvents 20 -ErrorAction SilentlyContinue
foreach ($evt in $gpEvents) {
$xml = [xml]$evt.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
$durationMs = try { [double]($xml.SelectSingleNode('//e:Data[@Name="PolicyElaspedTimeInMilliSeconds"]', $ns).'#text') } catch { 0 }
$results.GpEvents += [PSCustomObject]@{
Time = $evt.TimeCreated
EventId = $evt.Id
DurationMs = $durationMs
Message = ($evt.Message -replace '[\r\n]+', ' ') | Select-Object -First 1
}
}
if ($results.GpEvents.Count -gt 0) {
$results.GpMs = ($results.GpEvents | Measure-Object DurationMs -Average).Average
}
# User Profile Service — Event ID 1
$profEvents = Get-WinEvent -LogName 'Microsoft-Windows-User Profile Service/Operational' `
-MaxEvents 20 -ErrorAction SilentlyContinue | Where-Object { $_.Id -in @(1, 2, 67) }
foreach ($evt in $profEvents) {
$results.ProfileEvents += [PSCustomObject]@{
Time = $evt.TimeCreated
EventId = $evt.Id
Level = $evt.LevelDisplayName
Message = ($evt.Message -replace '[\r\n]+', ' ').Substring(0, [Math]::Min(120, $evt.Message.Length))
}
}
if ($results.ProfileEvents.Count -gt 0) {
$results.ProfileMs = 3500 # estimated; direct timing not available without ETW
}
$results
}
# ─── Section 2 — Group Policy Analysis ────────────────────────────────────────
Write-Status "Analyzing Group Policy..."
$GpData = Safe-Invoke -Section 'GroupPolicy' -Block {
$results = @{ AppliedGpos = @(); SlowGpos = @() }
# RSoP — fastest non-privileged enumeration
$rsopQuery = Get-WmiObject -Namespace 'root\rsop\computer' `
-Class RSOP_GPO -ErrorAction SilentlyContinue
if ($rsopQuery) {
foreach ($gpo in $rsopQuery) {
$results.AppliedGpos += [PSCustomObject]@{
Name = $gpo.Name
Guid = $gpo.GpoId
Version = $gpo.Version
Enabled = $gpo.Enabled
}
}
}
# Parse GP extension timings from event log
$gpExtEvents = Get-WinEvent -LogName 'Microsoft-Windows-GroupPolicy/Operational' `
-FilterXPath "*[System[EventID=5312 or EventID=5313]]" -MaxEvents 100 -ErrorAction SilentlyContinue
$extTimes = @{}
foreach ($evt in $gpExtEvents) {
$xml = [xml]$evt.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
$extName = try { $xml.SelectSingleNode('//e:Data[@Name="ExtensionName"]', $ns).'#text' } catch { $null }
$elapsedMs = try { [double]($xml.SelectSingleNode('//e:Data[@Name="ElapsedTimeInMilliSeconds"]', $ns).'#text') } catch { 0 }
if ($extName -and $elapsedMs -gt 0) {
if (-not $extTimes.ContainsKey($extName) -or $extTimes[$extName] -lt $elapsedMs) {
$extTimes[$extName] = $elapsedMs
}
}
}
$results.SlowGpos = $extTimes.GetEnumerator() |
Sort-Object Value -Descending |
Select-Object -First 10 |
ForEach-Object { [PSCustomObject]@{ Extension = $_.Key; MaxMs = $_.Value } }
$results
}
# ─── Section 3 — Network Diagnostics ──────────────────────────────────────────
Write-Status "Running network diagnostics..."
$NetData = Safe-Invoke -Section 'Network' -Block {
$results = @{ DnsTimes = @(); GatewayPing = $null; DcResults = @(); LdapPorts = @() }
# Discover domain and DC
$domainName = try { [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName } catch { '' }
$gatewayIp = try {
(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
Sort-Object RouteMetric | Select-Object -First 1).NextHop
} catch { $null }
# DNS resolution timing
$dnsTargets = @('www.google.com')
if ($domainName) { $dnsTargets += $domainName }
foreach ($target in $dnsTargets) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$resolved = try { [System.Net.Dns]::GetHostAddresses($target); $true } catch { $false }
$sw.Stop()
$results.DnsTimes += [PSCustomObject]@{
Target = $target
ResolvedMs = $sw.ElapsedMilliseconds
Success = $resolved
}
}
# Gateway ping
if ($gatewayIp) {
$ping = New-Object System.Net.NetworkInformation.Ping
$replies = 1..4 | ForEach-Object {
try { $ping.Send($gatewayIp, 1000) } catch { $null }
}
$successful = $replies | Where-Object { $_ -and $_.Status -eq 'Success' }
$results.GatewayPing = [PSCustomObject]@{
Gateway = $gatewayIp
Sent = 4
Received = @($successful).Count
AvgMs = if (@($successful).Count -gt 0) { ($successful | Measure-Object RoundtripTime -Average).Average } else { 9999 }
}
}
# DC connectivity
if ($domainName) {
$dcAddrs = try { [System.Net.Dns]::GetHostAddresses("_ldap._tcp.dc._msdcs.$domainName") } catch {
try { [System.Net.Dns]::GetHostAddresses($domainName) } catch { @() }
}
foreach ($addr in ($dcAddrs | Select-Object -First 3)) {
$dcIp = $addr.IPAddressToString
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$ping = New-Object System.Net.NetworkInformation.Ping
$reply = try { $ping.Send($dcIp, 2000) } catch { $null }
$sw.Stop()
$results.DcResults += [PSCustomObject]@{
Address = $dcIp
PingMs = if ($reply -and $reply.Status -eq 'Success') { $reply.RoundtripTime } else { 9999 }
Online = ($reply -and $reply.Status -eq 'Success')
}
}
}
# LDAP port checks (389, 636)
foreach ($port in @(389, 636)) {
if ($results.DcResults.Count -gt 0) {
$dc = $results.DcResults[0].Address
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$tcp = try {
$client = New-Object System.Net.Sockets.TcpClient
$task = $client.ConnectAsync($dc, $port)
$open = $task.Wait(2000) -and $client.Connected
$client.Close()
$open
} catch { $false }
$sw.Stop()
$results.LdapPorts += [PSCustomObject]@{
Port = $port
Open = $tcp
CheckMs = $sw.ElapsedMilliseconds
Label = if ($port -eq 389) { 'LDAP (389)' } else { 'LDAPS (636)' }
}
}
}
$results
}
# ─── Section 4 — Event Log Analysis ───────────────────────────────────────────
Write-Status "Scanning event logs..."
$EventData = Safe-Invoke -Section 'EventLogs' -Block {
$results = @{ SysErrors = @(); AppErrors = @(); SecWarnings = @(); SourceSummary = @{} }
# System errors
$sysErrors = Get-WinEvent -LogName System -MaxEvents 200 -ErrorAction SilentlyContinue |
Where-Object { $_.Level -eq 2 } | Select-Object -First 50
foreach ($e in $sysErrors) {
$results.SysErrors += [PSCustomObject]@{
Time = $e.TimeCreated.ToString('yyyy-MM-dd HH:mm')
Source = $e.ProviderName
Id = $e.Id
Message = ($e.Message -replace '[\r\n]+', ' ').Substring(0, [Math]::Min(100, $e.Message.Length))
}
$src = $e.ProviderName
if (-not $results.SourceSummary.ContainsKey($src)) { $results.SourceSummary[$src] = 0 }
$results.SourceSummary[$src]++
}
# Application errors
$appErrors = Get-WinEvent -LogName Application -MaxEvents 200 -ErrorAction SilentlyContinue |
Where-Object { $_.Level -eq 2 } | Select-Object -First 50
foreach ($e in $appErrors) {
$results.AppErrors += [PSCustomObject]@{
Time = $e.TimeCreated.ToString('yyyy-MM-dd HH:mm')
Source = $e.ProviderName
Id = $e.Id
Message = ($e.Message -replace '[\r\n]+', ' ').Substring(0, [Math]::Min(100, $e.Message.Length))
}
$src = $e.ProviderName
if (-not $results.SourceSummary.ContainsKey($src)) { $results.SourceSummary[$src] = 0 }
$results.SourceSummary[$src]++
}
# Security warnings
$secWarnings = Get-WinEvent -LogName Security -MaxEvents 100 -ErrorAction SilentlyContinue |
Where-Object { $_.Level -in @(3, 2) } | Select-Object -First 20
foreach ($e in $secWarnings) {
$results.SecWarnings += [PSCustomObject]@{
Time = $e.TimeCreated.ToString('yyyy-MM-dd HH:mm')
Source = $e.ProviderName
Id = $e.Id
Level = $e.LevelDisplayName
}
}
$results
}
# ─── Section 5 — System Information ───────────────────────────────────────────
Write-Status "Gathering system info..."
$SysInfo = Safe-Invoke -Section 'SystemInfo' -Block {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
$cpu = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
$ram = Get-CimInstance Win32_PhysicalMemory -ErrorAction SilentlyContinue |
Measure-Object Capacity -Sum
$startup = try {
(Get-CimInstance Win32_StartupCommand -ErrorAction SilentlyContinue | Measure-Object).Count
} catch { 0 }
$uptime = if ($os) { (Get-Date) - $os.LastBootUpTime } else { $null }
[PSCustomObject]@{
OsName = if ($os) { $os.Caption } else { 'Unknown' }
OsBuild = if ($os) { $os.BuildNumber } else { 'Unknown' }
OsVersion = if ($os) { $os.Version } else { 'Unknown' }
Architecture = if ($os) { $os.OSArchitecture } else { 'Unknown' }
LastBoot = if ($os) { $os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm') } else { 'Unknown' }
UptimeDays = if ($uptime) { [Math]::Round($uptime.TotalDays, 1) } else { 0 }
CpuName = if ($cpu) { $cpu.Name } else { 'Unknown' }
CpuCores = if ($cpu) { $cpu.NumberOfCores } else { 0 }
CpuLoad = if ($cpu) { $cpu.LoadPercentage } else { 0 }
RamTotalGb = if ($ram.Sum) { [Math]::Round($ram.Sum / 1GB, 1) } else { 0 }
DiskTotalGb = if ($disk) { [Math]::Round($disk.Size / 1GB, 1) } else { 0 }
DiskFreeGb = if ($disk) { [Math]::Round($disk.FreeSpace / 1GB, 1) } else { 0 }
DiskFreePct = if ($disk -and $disk.Size -gt 0) { [Math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1) } else { 0 }
StartupCount = $startup
}
}
# ─── Section 6 — SCCM / Intune Detection ─────────────────────────────────────
Write-Status "Checking SCCM and Intune enrollment..."
$MgmtData = Safe-Invoke -Section 'Management' -Block {
$results = @{ SccmInstalled = $false; SccmVersion = ''; IntuneEnrolled = $false; IntuneDetails = '' }
# SCCM / ConfigMgr Client
$ccmClient = Get-WmiObject -Namespace 'root\ccm' -Class SMS_Client -ErrorAction SilentlyContinue
if ($ccmClient) {
$results.SccmInstalled = $true
$results.SccmVersion = $ccmClient.ClientVersion
} elseif (Test-Path 'C:\Windows\CCM\CcmExec.exe') {
$results.SccmInstalled = $true
$exe = Get-Item 'C:\Windows\CCM\CcmExec.exe' -ErrorAction SilentlyContinue
$results.SccmVersion = if ($exe) { $exe.VersionInfo.FileVersion } else { 'Found (version unavailable)' }
}
# Intune enrollment detection
$intuneKey = 'HKLM:\SOFTWARE\Microsoft\Enrollments'
if (Test-Path $intuneKey) {
$enrollments = Get-ChildItem $intuneKey -ErrorAction SilentlyContinue |
Get-ItemProperty -ErrorAction SilentlyContinue |
Where-Object { $_.ProviderID -like '*Intune*' -or $_.EnrollmentType -eq 6 }
if ($enrollments) {
$results.IntuneEnrolled = $true
$first = $enrollments | Select-Object -First 1
$results.IntuneDetails = "UPN: $($first.UPN)"
}
}
# Also check AAD join / hybrid join status
$dsregOutput = try { dsregcmd /status 2>$null } catch { $null }
if ($dsregOutput) {
$azureAdJoined = $dsregOutput | Select-String 'AzureAdJoined\s*:\s*YES' | Select-Object -First 1
$domainJoined = $dsregOutput | Select-String 'DomainJoined\s*:\s*YES' | Select-Object -First 1
$mdmEnrolled = $dsregOutput | Select-String 'MDMEnrolled\s*:\s*YES' | Select-Object -First 1
$results.AadJoined = $null -ne $azureAdJoined
$results.DomainJoined = $null -ne $domainJoined
$results.MdmEnrolled = $null -ne $mdmEnrolled
if ($null -ne $mdmEnrolled) { $results.IntuneEnrolled = $true }
}
$results
}
# ─── Build Recommendations ────────────────────────────────────────────────────
Write-Status "Building recommendations..."
$Recommendations = @()
if ($LoginData -and $LoginData.BootMs -gt 90000) {
$Recommendations += @{ Level = 'red'; Text = "Average boot time is $(Format-Ms $LoginData.BootMs). Target is under 60 seconds. Review startup applications and consider SSD upgrade." }
} elseif ($LoginData -and $LoginData.BootMs -gt 60000) {
$Recommendations += @{ Level = 'yellow'; Text = "Boot time of $(Format-Ms $LoginData.BootMs) is above the 60-second target. Review startup items and GP processing times." }
}
if ($GpData -and $GpData.SlowGpos.Count -gt 0) {
$slowest = $GpData.SlowGpos | Select-Object -First 1
if ($slowest.MaxMs -gt 5000) {
$Recommendations += @{ Level = 'red'; Text = "GP extension '$($slowest.Extension)' took $(Format-Ms $slowest.MaxMs). Extensions taking over 5 seconds significantly delay login. Review scripts and mapped drives." }
}
}
if ($NetData -and $NetData.GatewayPing -and $NetData.GatewayPing.AvgMs -gt 10) {
$Recommendations += @{ Level = 'yellow'; Text = "Gateway latency of $([Math]::Round($NetData.GatewayPing.AvgMs))ms detected. High LAN latency can delay GP processing and profile load." }
}
if ($NetData -and $NetData.DnsTimes) {
$slowDns = $NetData.DnsTimes | Where-Object { $_.ResolvedMs -gt 500 }
foreach ($d in $slowDns) {
$Recommendations += @{ Level = 'yellow'; Text = "DNS resolution for '$($d.Target)' took $($d.ResolvedMs)ms. Slow DNS directly delays domain authentication." }
}
$failedDns = $NetData.DnsTimes | Where-Object { -not $_.Success }
foreach ($d in $failedDns) {
$Recommendations += @{ Level = 'red'; Text = "DNS resolution FAILED for '$($d.Target)'. This will prevent domain authentication and GP processing." }
}
}
if ($SysInfo -and $SysInfo.DiskFreePct -lt 15) {
$Recommendations += @{ Level = 'red'; Text = "C: drive is $($SysInfo.DiskFreePct)% free ($($SysInfo.DiskFreeGb) GB). Low disk space causes profile write failures and slow logins." }
} elseif ($SysInfo -and $SysInfo.DiskFreePct -lt 25) {
$Recommendations += @{ Level = 'yellow'; Text = "C: drive is $($SysInfo.DiskFreePct)% free. Consider disk cleanup to maintain healthy login performance." }
}
if ($SysInfo -and $SysInfo.UptimeDays -gt 30) {
$Recommendations += @{ Level = 'yellow'; Text = "System uptime is $($SysInfo.UptimeDays) days. Extended uptime can accumulate stale GP objects. Schedule a reboot." }
}
if ($SysInfo -and $SysInfo.StartupCount -gt 15) {
$Recommendations += @{ Level = 'yellow'; Text = "$($SysInfo.StartupCount) startup programs detected. Excess startup items significantly slow login times." }
}
if ($EventData -and ($EventData.SysErrors.Count + $EventData.AppErrors.Count) -gt 30) {
$Recommendations += @{ Level = 'yellow'; Text = "$(($EventData.SysErrors + $EventData.AppErrors).Count) errors found in System/Application logs. High error rates may indicate underlying issues causing login delays." }
}
if ($Recommendations.Count -eq 0) {
$Recommendations += @{ Level = 'green'; Text = "No critical issues detected. Login performance appears healthy." }
}
# ─── HTML Report Generation ───────────────────────────────────────────────────
Write-Status "Generating HTML report..."
$ScriptEnd = Get-Date
$RunDuration = [Math]::Round(($ScriptEnd - $ScriptStart).TotalSeconds, 1)
$ReportDate = $ScriptStart.ToString('yyyy-MM-dd HH:mm')
$ReportFile = Join-Path ([Environment]::GetFolderPath('Desktop')) ("LoginDiag-Report-" + $ScriptStart.ToString('yyyyMMdd-HHmm') + ".html")
# Build table rows ─────────────────────────────────────────────────────────────
function Html-TableRows {
param([array]$Items, [string[]]$Props)
if (-not $Items -or $Items.Count -eq 0) { return '
No data collected ' }
$rows = ''
foreach ($item in $Items) {
$rows += ''
foreach ($p in $Props) {
$val = if ($null -ne $item.$p) { [System.Web.HttpUtility]::HtmlEncode($item.$p.ToString()) } else { '' }
$rows += "$val "
}
$rows += ' '
}
$rows
}
# Login time bar widths
$totalLoginEstimateMs = ($LoginData.BootMs + $LoginData.GpMs + $LoginData.ProfileMs)
if ($totalLoginEstimateMs -lt 1) { $totalLoginEstimateMs = 1 }
$bootPct = [Math]::Min(100, [Math]::Round(($LoginData.BootMs / $totalLoginEstimateMs) * 100))
$gpPct = [Math]::Min(100, [Math]::Round(($LoginData.GpMs / $totalLoginEstimateMs) * 100))
$profilePct = [Math]::Min(100, 100 - $bootPct - $gpPct)
$bootClass = Severity-Class $LoginData.BootMs 60000 90000
$gpClass = Severity-Class $LoginData.GpMs 15000 30000
# Recommendation rows
$recHtml = ''
foreach ($r in $Recommendations) {
$icon = switch ($r.Level) {
'red' { ' ' }
'yellow' { ' ' }
default { ' ' }
}
$bg = switch ($r.Level) { 'red' { 'rgba(220,38,38,.08)' } 'yellow' { 'rgba(245,158,11,.08)' } default { 'rgba(52,211,153,.08)' } }
$bd = switch ($r.Level) { 'red' { 'rgba(220,38,38,.25)' } 'yellow' { 'rgba(245,158,11,.25)' } default { 'rgba(52,211,153,.25)' } }
$recHtml += "$icon $($r.Text)
"
}
# Source summary table
$srcHtml = ''
$sortedSrc = $EventData.SourceSummary.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10
foreach ($s in $sortedSrc) {
$cnt = $s.Value
$cls = if ($cnt -ge 10) { 'color:#FCA5A5' } elseif ($cnt -ge 5) { 'color:#FBBF24' } else { 'color:#34D399' }
$srcHtml += "$([System.Web.HttpUtility]::HtmlEncode($s.Key)) $cnt "
}
# Network rows
$netDnsHtml = ''
foreach ($d in $NetData.DnsTimes) {
$cls = if (-not $d.Success) { 'color:#FCA5A5' } elseif ($d.ResolvedMs -gt 500) { 'color:#FBBF24' } else { 'color:#34D399' }
$status = if ($d.Success) { "OK ($($d.ResolvedMs) ms)" } else { "FAILED" }
$netDnsHtml += "$([System.Web.HttpUtility]::HtmlEncode($d.Target)) $status "
}
$netDcHtml = ''
foreach ($dc in $NetData.DcResults) {
$cls = if (-not $dc.Online) { 'color:#FCA5A5' } elseif ($dc.PingMs -gt 10) { 'color:#FBBF24' } else { 'color:#34D399' }
$status = if ($dc.Online) { "$($dc.PingMs) ms" } else { "UNREACHABLE" }
$netDcHtml += "$([System.Web.HttpUtility]::HtmlEncode($dc.Address)) $status "
}
$ldapHtml = ''
foreach ($p in $NetData.LdapPorts) {
$cls = if ($p.Open) { 'color:#34D399' } else { 'color:#FCA5A5' }
$status = if ($p.Open) { "OPEN" } else { "BLOCKED" }
$ldapHtml += "$($p.Label) $status "
}
$gwHtml = if ($NetData.GatewayPing) {
$gw = $NetData.GatewayPing
$cls = if ($gw.AvgMs -gt 10) { 'color:#FBBF24' } else { 'color:#34D399' }
"Gateway ($([System.Web.HttpUtility]::HtmlEncode($gw.Gateway))) $([Math]::Round($gw.AvgMs)) ms ($($gw.Received)/$($gw.Sent) replies) "
} else { 'No gateway detected ' }
# Slow GP extensions rows
$slowGpHtml = ''
foreach ($gpe in $GpData.SlowGpos) {
$cls = if ($gpe.MaxMs -ge 5000) { 'color:#FCA5A5' } elseif ($gpe.MaxMs -ge 2000) { 'color:#FBBF24' } else { 'color:#34D399' }
$flag = if ($gpe.MaxMs -ge 5000) { ' [SLOW] ' } else { '' }
$slowGpHtml += "$([System.Web.HttpUtility]::HtmlEncode($gpe.Extension))$flag $(Format-Ms $gpe.MaxMs) "
}
if (-not $slowGpHtml) { $slowGpHtml = 'No GP extension timing data found ' }
# SCCM/Intune
$sccmStatus = if ($MgmtData.SccmInstalled) { "Installed (v$($MgmtData.SccmVersion))" } else { "Not detected " }
$intuneStatus = if ($MgmtData.IntuneEnrolled) { "Enrolled $($MgmtData.IntuneDetails)" } else { "Not enrolled " }
$aadStatus = if ($MgmtData.AadJoined) { "Azure AD Joined " } else { "Not Azure AD joined " }
$domStatus = if ($MgmtData.DomainJoined) { "Domain Joined " } else { "Not domain joined " }
# System info quick-view cells
$diskCls = if ($SysInfo.DiskFreePct -lt 15) { 'color:#FCA5A5' } elseif ($SysInfo.DiskFreePct -lt 25) { 'color:#FBBF24' } else { 'color:#34D399' }
# ─── Assemble HTML ─────────────────────────────────────────────────────────────
$html = @"
BTG Login Diagnostic Report — $ReportDate
$(Format-Ms $LoginData.BootMs)
Avg Boot Time
$(Format-Ms $LoginData.GpMs)
GP Processing
$($SysInfo.RamTotalGb) GB
Total RAM
$($SysInfo.DiskFreePct)%
C: Free Space
$($SysInfo.UptimeDays)d
System Uptime
$(($EventData.SysErrors.Count + $EventData.AppErrors.Count))
Errors Found
Boot / OS Load $(Format-Ms $LoginData.BootMs) (est. avg)
Group Policy Processing $(Format-Ms $LoginData.GpMs) (est. avg)
Profile Load + Shell Init Estimated
Boot time sourced from Diagnostics-Performance Event ID 100. GP from GroupPolicy/Operational. Profile timing is estimated.
Recent Boot Events
Date/Time Boot (ms) Main Path (ms)
$(Html-TableRows ($LoginData.BootEvents | Select-Object -First 5) @('Time','BootMs','MainMs'))
Top 10 Slowest GP Extensions
Extension Max Time
$slowGpHtml
Applied GPOs (RSoP)
GPO Name Enabled
$(if($GpData.AppliedGpos.Count -gt 0){Html-TableRows ($GpData.AppliedGpos | Select-Object -First 15) @('Name','Enabled')}else{'RSoP query returned no results (may require domain admin) '})
Gateway Ping
Domain Controllers
Address Ping $(if($netDcHtml){$netDcHtml}else{'Not domain-joined '})
LDAP Ports
Port Status $(if($ldapHtml){$ldapHtml}else{'No DC detected to check '})
Top Error Sources (System + Application)
Source Error Count
$(if($srcHtml){$srcHtml}else{'No errors found '})
Recent System Errors (last 50)
Time Source ID
$(Html-TableRows ($EventData.SysErrors | Select-Object -First 10) @('Time','Source','Id'))
Recent Application Errors (last 50)
Time Source ID Message (truncated)
$(Html-TableRows ($EventData.AppErrors | Select-Object -First 10) @('Time','Source','Id','Message'))
OS $($SysInfo.OsName)
Build $($SysInfo.OsBuild) ($($SysInfo.OsVersion))
Architecture $($SysInfo.Architecture)
Last Boot $($SysInfo.LastBoot)
Uptime $($SysInfo.UptimeDays) days
Startup Programs $($SysInfo.StartupCount)
CPU $($SysInfo.CpuName)
CPU Cores $($SysInfo.CpuCores)
CPU Load $($SysInfo.CpuLoad)%
RAM $($SysInfo.RamTotalGb) GB
C: Total $($SysInfo.DiskTotalGb) GB
C: Free $($SysInfo.DiskFreeGb) GB ($($SysInfo.DiskFreePct)%)
SCCM / ConfigMgr $sccmStatus
Intune / MDM $intuneStatus
Azure AD Join $aadStatus
Domain Join $domStatus
Need Help Fixing Slow Logins?
This report is a starting point. Our team has optimized login performance for hundreds of organizations — from GP rationalization to Intune tuning and network remediation.
Get a Free Consultation
Powered by BluetechGreen LLC — Enterprise IT Solutions
"@
# ─── Write Report ─────────────────────────────────────────────────────────────
$html | Out-File -FilePath $ReportFile -Encoding UTF8 -Force
Write-Host ""
Write-Host " ============================================" -ForegroundColor Green
Write-Host " BTG Login Diagnostic Report Complete" -ForegroundColor Green
Write-Host " ============================================" -ForegroundColor Green
Write-Host " Report: $ReportFile" -ForegroundColor Cyan
Write-Host " Runtime: ${RunDuration}s" -ForegroundColor Cyan
Write-Host ""
# Open report in default browser
try {
Start-Process $ReportFile
} catch {
Write-Host " Could not auto-open report. Please open manually:" -ForegroundColor Yellow
Write-Host " $ReportFile" -ForegroundColor White
}