#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
BG
BluetechGreen Login Diagnostic Report
Report Generated: $ReportDate
Hostname: $env:COMPUTERNAME  |  User: $env:USERNAME
Script Runtime: ${RunDuration}s  |  bluetechgreen.com
$(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
Recommendations ($($Recommendations.Count))
$recHtml
Login Time Breakdown
Boot / OS Load$(Format-Ms $LoginData.BootMs) (est. avg)
Group Policy Processing$(Format-Ms $LoginData.GpMs) (est. avg)
Profile Load + Shell InitEstimated

Boot time sourced from Diagnostics-Performance Event ID 100. GP from GroupPolicy/Operational. Profile timing is estimated.

Recent Boot Events

$(Html-TableRows ($LoginData.BootEvents | Select-Object -First 5) @('Time','BootMs','MainMs'))
Date/TimeBoot (ms)Main Path (ms)
Group Policy Analysis

Top 10 Slowest GP Extensions

$slowGpHtml
ExtensionMax Time

Applied GPOs (RSoP)

$(if($GpData.AppliedGpos.Count -gt 0){Html-TableRows ($GpData.AppliedGpos | Select-Object -First 15) @('Name','Enabled')}else{''})
GPO NameEnabled
RSoP query returned no results (may require domain admin)
Network Diagnostics

DNS Resolution

$netDnsHtml
TargetResult

Gateway Ping

$gwHtml
HostLatency

Domain Controllers

$(if($netDcHtml){$netDcHtml}else{''})
AddressPing
Not domain-joined

LDAP Ports

$(if($ldapHtml){$ldapHtml}else{''})
PortStatus
No DC detected to check
Event Log Analysis

Top Error Sources (System + Application)

$(if($srcHtml){$srcHtml}else{''})
SourceError Count
No errors found

Recent System Errors (last 50)

$(Html-TableRows ($EventData.SysErrors | Select-Object -First 10) @('Time','Source','Id'))
TimeSourceID

Recent Application Errors (last 50)

$(Html-TableRows ($EventData.AppErrors | Select-Object -First 10) @('Time','Source','Id','Message'))
TimeSourceIDMessage (truncated)
System Information
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)%)
Endpoint Management Detection
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

BTG-LoginDiag v1.0  •  Generated on $env:COMPUTERNAME  •  $ReportDate  •  Script runtime: ${RunDuration}s  •  bluetechgreen.com/tools
"@ # ─── 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 }