Public/Get-EFEndpointHealth.ps1
|
function Get-EFEndpointHealth { <# .SYNOPSIS Produces an operational health summary for the local Windows endpoint. .DESCRIPTION Combines inventory and pending-reboot signals into a compact health report suitable for RMM, Intune proactive remediation, scheduled tasks, and monitoring ingestion. ExitCode is 0 for Healthy, 1 for Warning, and 2 for Critical. .PARAMETER MinimumFreeSpacePercent The system-drive free-space percentage below which a warning is reported. .PARAMETER MaximumUptimeDays The uptime threshold after which a pending maintenance warning is reported. .PARAMETER IncludeSoftware Includes installed software in the nested Inventory object. .PARAMETER NoProgress Suppresses the progress display for non-interactive automation hosts. .EXAMPLE Get-EFEndpointHealth -MinimumFreeSpacePercent 15 -MaximumUptimeDays 30 #> [CmdletBinding()] param( [ValidateRange(1, 99)] [int]$MinimumFreeSpacePercent = 15, [ValidateRange(1, 3650)] [int]$MaximumUptimeDays = 30, [switch]$IncludeSoftware, [switch]$NoProgress ) $null = Test-EFWindows -Throw $inventory = Get-EFEndpointInventory -IncludeSoftware:$IncludeSoftware -NoProgress:$NoProgress $pendingReboot = Get-EFPendingReboot $checks = [Collections.Generic.List[object]]::new() $freePercent = Get-EFPropertyValue -InputObject $inventory.SystemDrive -Name 'FreeSpacePercent' if ($null -eq $freePercent) { $checks.Add([pscustomobject]@{ Id = 'DiskFreeSpace'; Severity = 'Information'; Status = 'Unknown' Message = 'System drive free space could not be collected.' ActualValue = $null; Threshold = $MinimumFreeSpacePercent }) } elseif ([double]$freePercent -lt $MinimumFreeSpacePercent) { $severity = if ([double]$freePercent -lt [math]::Max(5, $MinimumFreeSpacePercent / 2)) { 'Critical' } else { 'Warning' } $checks.Add([pscustomobject]@{ Id = 'DiskFreeSpace'; Severity = $severity; Status = 'Unhealthy' Message = "System drive free space is $freePercent%; threshold is $MinimumFreeSpacePercent%." ActualValue = $freePercent; Threshold = $MinimumFreeSpacePercent }) } else { $checks.Add([pscustomobject]@{ Id = 'DiskFreeSpace'; Severity = 'Information'; Status = 'Healthy' Message = 'System drive free space is within threshold.' ActualValue = $freePercent; Threshold = $MinimumFreeSpacePercent }) } $uptimeDays = Get-EFPropertyValue -InputObject $inventory.OperatingSystem -Name 'UptimeDays' if ($null -eq $uptimeDays) { $checks.Add([pscustomobject]@{ Id = 'Uptime'; Severity = 'Information'; Status = 'Unknown' Message = 'Endpoint uptime could not be collected.' ActualValue = $null; Threshold = $MaximumUptimeDays }) } else { $uptimeHealthy = [double]$uptimeDays -le $MaximumUptimeDays $checks.Add([pscustomobject]@{ Id = 'Uptime'; Severity = if ($uptimeHealthy) { 'Information' } else { 'Warning' } Status = if ($uptimeHealthy) { 'Healthy' } else { 'Unhealthy' } Message = if ($uptimeHealthy) { 'Endpoint uptime is within threshold.' } else { "Endpoint uptime is $uptimeDays days; threshold is $MaximumUptimeDays days." } ActualValue = $uptimeDays; Threshold = $MaximumUptimeDays }) } $checks.Add([pscustomobject]@{ Id = 'PendingReboot'; Severity = if ($pendingReboot.IsRebootPending) { 'Warning' } else { 'Information' } Status = if ($pendingReboot.IsRebootPending) { 'Unhealthy' } else { 'Healthy' } Message = if ($pendingReboot.IsRebootPending) { "Restart pending: $($pendingReboot.Reasons -join ', ')." } else { 'No pending restart was detected.' } ActualValue = $pendingReboot.IsRebootPending; Threshold = $false }) foreach ($firewallProfile in @($inventory.Security.Firewall)) { $enabled = [bool](Get-EFPropertyValue -InputObject $firewallProfile -Name 'Enabled') $profileName = [string](Get-EFPropertyValue -InputObject $firewallProfile -Name 'Name') $checks.Add([pscustomobject]@{ Id = "Firewall$profileName"; Severity = if ($enabled) { 'Information' } else { 'Critical' } Status = if ($enabled) { 'Healthy' } else { 'Unhealthy' } Message = if ($enabled) { "$profileName firewall profile is enabled." } else { "$profileName firewall profile is disabled." } ActualValue = $enabled; Threshold = $true }) } $defender = $inventory.Security.Defender if ($null -ne $defender) { $realtimeEnabled = [bool](Get-EFPropertyValue -InputObject $defender -Name 'RealTimeProtectionEnabled') $checks.Add([pscustomobject]@{ Id = 'DefenderRealTimeProtection'; Severity = if ($realtimeEnabled) { 'Information' } else { 'Critical' } Status = if ($realtimeEnabled) { 'Healthy' } else { 'Unhealthy' } Message = if ($realtimeEnabled) { 'Microsoft Defender real-time protection is enabled.' } else { 'Microsoft Defender real-time protection is disabled.' } ActualValue = $realtimeEnabled; Threshold = $true }) } $bitLocker = $inventory.Security.BitLocker if ($null -ne $bitLocker) { $protected = [string](Get-EFPropertyValue -InputObject $bitLocker -Name 'ProtectionStatus') -eq 'On' $checks.Add([pscustomobject]@{ Id = 'BitLockerProtection'; Severity = if ($protected) { 'Information' } else { 'Warning' } Status = if ($protected) { 'Healthy' } else { 'Unhealthy' } Message = if ($protected) { 'BitLocker protection is on for the system drive.' } else { 'BitLocker protection is not on for the system drive.' } ActualValue = Get-EFPropertyValue -InputObject $bitLocker -Name 'ProtectionStatus'; Threshold = 'On' }) } foreach ($inventoryError in @($inventory.Errors)) { $checks.Add([pscustomobject]@{ Id = 'InventoryCollection'; Severity = 'Information'; Status = 'Unknown' Message = [string]$inventoryError; ActualValue = $null; Threshold = $null }) } $criticalCount = @($checks | Where-Object Severity -eq 'Critical').Count $warningCount = @($checks | Where-Object Severity -eq 'Warning').Count $unknownCount = @($checks | Where-Object Status -eq 'Unknown').Count $knownCount = $checks.Count - $unknownCount $coveragePercent = if ($checks.Count -eq 0) { 0 } else { [math]::Round(($knownCount / $checks.Count) * 100, 1) } $dataStatus = if ($unknownCount -eq 0) { 'Complete' } elseif ($knownCount -eq 0) { 'Failed' } else { 'Partial' } $status = if ($criticalCount -gt 0) { 'Critical' } elseif ($warningCount -gt 0) { 'Warning' } else { 'Healthy' } $exitCode = if ($criticalCount -gt 0) { 2 } elseif ($warningCount -gt 0) { 1 } else { 0 } $score = [math]::Max(0, 100 - ($criticalCount * 25) - ($warningCount * 10)) $summaryText = switch ($status) { 'Healthy' { 'No operational health issues were detected.' } 'Warning' { "$warningCount warning(s) require attention." } 'Critical' { "$criticalCount critical issue(s) and $warningCount warning(s) require attention." } } $nextStep = if ($status -eq 'Healthy') { 'Run Get-EFEndpointSummary for a combined health and compliance view.' } else { 'Review the Checks property or run Show-EFEndpointSummary -Detailed.' } [pscustomobject]@{ PSTypeName = 'EndpointForge.EndpointHealth' ComputerName = $env:COMPUTERNAME CheckedAtUtc = [DateTime]::UtcNow Status = $status Score = $score ExitCode = $exitCode Summary = $summaryText NextStep = $nextStep CriticalCount = $criticalCount WarningCount = $warningCount UnknownCount = $unknownCount DataStatus = $dataStatus CoveragePercent = $coveragePercent CollectionErrors = @($inventory.Errors) Checks = @($checks) Inventory = $inventory PendingReboot = $pendingReboot } } |