Public/Get-RegistrationHeartbeat.ps1
|
function Get-RegistrationHeartbeat { <# .SYNOPSIS Shows heartbeat status for all registered non-Windows systems. .DESCRIPTION Queries the target OU for all computer objects and checks the registration folder for each system's JSON file timestamp. Classifies each system as Active, Stale, Offline, or Never based on how recently the agent checked in. Active: Heartbeat within StaleHours (default: 2 hours) Stale: Between StaleHours and OfflineHours Offline: Beyond OfflineHours (default: 24 hours) Never: No JSON file found (manually registered system) .PARAMETER RegistrationPath Path to the shared folder where agents drop their JSON heartbeat files. .PARAMETER OrganizationalUnit OU to query for registered systems. Defaults to "OU=Non-Windows Servers" under the domain root. .PARAMETER StaleHours Number of hours without a heartbeat before a system is flagged as Stale. Default: 2 hours. .PARAMETER OfflineHours Number of hours without a heartbeat before a system is flagged as Offline. Default: 24 hours. .PARAMETER OutputPath If specified, generates an HTML dashboard report at this file path. .EXAMPLE Get-RegistrationHeartbeat -RegistrationPath '\\fileserver\registrations$' Shows heartbeat status for all registered systems. .EXAMPLE Get-RegistrationHeartbeat -RegistrationPath '\\fs01\reg$' -StaleHours 1 -OfflineHours 12 Uses tighter thresholds for stale/offline classification. .EXAMPLE Get-RegistrationHeartbeat -RegistrationPath '\\fs01\reg$' | Where-Object Status -eq 'Offline' Shows only systems that have not checked in for 24+ hours. .EXAMPLE Get-RegistrationHeartbeat -RegistrationPath '\\fs01\reg$' -OutputPath '.\heartbeat-report.html' Generates an HTML dashboard of heartbeat status. .NOTES Requires: ActiveDirectory module (RSAT). The heartbeat status is based on the file modification timestamp of each system's JSON file in the registration folder. #> [CmdletBinding()] param( [Parameter(Position = 0)] [string]$RegistrationPath, [string]$OrganizationalUnit, [ValidateRange(1, 8760)] [int]$StaleHours = 2, [ValidateRange(1, 8760)] [int]$OfflineHours = 24, [string]$OutputPath ) begin { Import-Module ActiveDirectory -ErrorAction Stop if (-not $OrganizationalUnit) { $domainDN = (Get-ADDomain).DistinguishedName $OrganizationalUnit = "OU=Non-Windows Servers,$domainDN" } } process { # Verify the OU exists try { $null = Get-ADOrganizationalUnit -Identity $OrganizationalUnit -ErrorAction Stop } catch { Write-Warning "OU not found: $OrganizationalUnit. No systems registered yet." return } # Get all computer objects from the OU $properties = @( 'Name', 'DNSHostName', 'IPv4Address', 'OperatingSystem', 'OperatingSystemVersion', 'Description', 'Created', 'Modified' ) $computers = Get-ADComputer -SearchBase $OrganizationalUnit -Filter * ` -Properties $properties -ErrorAction Stop if (-not $computers) { Write-Verbose "No computer objects found in $OrganizationalUnit" return } $now = Get-Date $results = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($comp in $computers) { $lastHeartbeat = $null $hoursSince = $null $status = 'Never' # Check for JSON file in the registration folder if ($RegistrationPath -and (Test-Path $RegistrationPath)) { $jsonFile = Join-Path -Path $RegistrationPath -ChildPath "$($comp.Name).json" if (Test-Path $jsonFile) { $fileInfo = Get-Item -Path $jsonFile $lastHeartbeat = $fileInfo.LastWriteTime $hoursSince = [math]::Round(($now - $lastHeartbeat).TotalHours, 1) if ($hoursSince -le $StaleHours) { $status = 'Active' } elseif ($hoursSince -le $OfflineHours) { $status = 'Stale' } else { $status = 'Offline' } } } $results.Add([PSCustomObject]@{ ComputerName = $comp.Name OperatingSystem = $comp.OperatingSystem OperatingSystemVersion = $comp.OperatingSystemVersion IPAddress = $comp.IPv4Address LastHeartbeat = $lastHeartbeat Status = $status HoursSinceHeartbeat = $hoursSince Description = $comp.Description }) } Write-Verbose "Heartbeat check complete: $($results.Count) systems evaluated" # Generate HTML report if requested if ($OutputPath) { $reportDir = Split-Path -Path $OutputPath -Parent if ($reportDir -and -not (Test-Path $reportDir)) { New-Item -Path $reportDir -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $totalCount = @($results).Count $activeCount = @($results | Where-Object { $_.Status -eq 'Active' }).Count $staleCount = @($results | Where-Object { $_.Status -eq 'Stale' }).Count $offlineCount = @($results | Where-Object { $_.Status -eq 'Offline' }).Count $neverCount = @($results | Where-Object { $_.Status -eq 'Never' }).Count $tableRows = ($results | ForEach-Object { $statusClass = switch ($_.Status) { 'Active' { 'finding-ok' } 'Stale' { 'finding-warn' } 'Offline' { 'finding-bad' } 'Never' { '' } default { '' } } $lastHb = if ($_.LastHeartbeat) { $_.LastHeartbeat.ToString('yyyy-MM-dd HH:mm') } else { '—' } $hoursSince = if ($null -ne $_.HoursSinceHeartbeat) { "$($_.HoursSinceHeartbeat)h" } else { '—' } $os = if ($_.OperatingSystem) { $_.OperatingSystem } else { '—' } $osVer = if ($_.OperatingSystemVersion) { $_.OperatingSystemVersion } else { '—' } $ip = if ($_.IPAddress) { $_.IPAddress } else { '—' } "<tr><td>$($_.ComputerName)</td><td>$os</td><td>$osVer</td><td>$ip</td><td>$lastHb</td><td class=`"$statusClass`">$($_.Status)</td><td>$hoursSince</td></tr>" }) -join "`n " $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Registration Heartbeat Status</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, sans-serif; background: #0d1117; color: #c9d1d9; padding: 2rem; } .header { background: linear-gradient(135deg, #1a1f2e 0%, #2a1a0a 100%); padding: 2rem; border-radius: 8px; margin-bottom: 2rem; } .header h1 { color: #d29922; font-size: 1.8rem; margin-bottom: 0.5rem; } .header .meta { color: #8b949e; font-size: 0.9rem; } .summary-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; text-align: center; } .card .number { font-size: 2.5rem; font-weight: 700; } .card .label { color: #8b949e; font-size: 0.85rem; margin-top: 0.5rem; } .card.active .number { color: #3fb950; } .card.stale .number { color: #d29922; } .card.offline .number { color: #f85149; } .card.never .number { color: #8b949e; } .card.total .number { color: #d29922; } .section { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; } .section h2 { color: #d29922; font-size: 1.3rem; margin-bottom: 1rem; } .table-wrapper { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } th { background: #21262d; color: #d29922; padding: 0.75rem; text-align: left; border-bottom: 2px solid #30363d; white-space: nowrap; } td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #21262d; } tr:hover { background: #1c2128; } .finding-bad { color: #f85149; font-weight: 600; } .finding-warn { color: #d29922; font-weight: 600; } .finding-ok { color: #3fb950; } .footer { text-align: center; color: #484f58; margin-top: 2rem; font-size: 0.8rem; } </style> </head> <body> <div class="header"> <h1>Registration Heartbeat Status</h1> <div class="meta">Generated $timestamp | OU: $OrganizationalUnit | Stale: >${StaleHours}h | Offline: >${OfflineHours}h</div> </div> <div class="summary-cards"> <div class="card total"><div class="number">$totalCount</div><div class="label">Total</div></div> <div class="card active"><div class="number">$activeCount</div><div class="label">Active</div></div> <div class="card stale"><div class="number">$staleCount</div><div class="label">Stale</div></div> <div class="card offline"><div class="number">$offlineCount</div><div class="label">Offline</div></div> <div class="card never"><div class="number">$neverCount</div><div class="label">Never</div></div> </div> <div class="section"> <h2>System Heartbeats</h2> <div class="table-wrapper"> <table> <thead><tr><th>Computer</th><th>Operating System</th><th>OS Version</th><th>IP Address</th><th>Last Heartbeat</th><th>Status</th><th>Since</th></tr></thead> <tbody> $tableRows </tbody> </table> </div> </div> <div class="footer">Generated by AD-LinuxInventory | github.com/larro1991/AD-LinuxInventory</div> </body> </html> "@ $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Verbose "HTML report saved: $OutputPath" } $results } } |