Public/Get-ExpirationAlerts.ps1
|
function Get-ExpirationAlerts { <# .SYNOPSIS Checks for certificates, accounts, and resources approaching expiration. .DESCRIPTION Scans local/remote certificate stores, AD service accounts with expiring passwords, user accounts with approaching expiration dates, domain functional level (informational), and DHCP scope utilization. .PARAMETER DaysCertExpiry Warn when a certificate expires within this many days. Default 30. .PARAMETER DaysPasswordExpiry Warn when a service account password expires within this many days. Default 14. .PARAMETER DaysAccountExpiry Warn when a user account expiration date is within this many days. Default 14. .PARAMETER ComputerName Servers whose certificate stores should be checked. If omitted, only the local machine is checked. .EXAMPLE Get-ExpirationAlerts -DaysCertExpiry 14 -ComputerName 'WEB01','WEB02' .OUTPUTS PSCustomObject[] #> [CmdletBinding()] param( [Parameter()] [ValidateRange(1, 365)] [int]$DaysCertExpiry = 30, [Parameter()] [ValidateRange(1, 365)] [int]$DaysPasswordExpiry = 14, [Parameter()] [ValidateRange(1, 365)] [int]$DaysAccountExpiry = 14, [Parameter()] [string[]]$ComputerName ) $alerts = [System.Collections.Generic.List[PSCustomObject]]::new() $now = Get-Date # ──────────────────────────────────────────────────────────────────── # 1. Certificates expiring (CRITICAL / HIGH / MEDIUM) # ──────────────────────────────────────────────────────────────────── # Build list of machines to check $certTargets = @($env:COMPUTERNAME) if ($ComputerName) { $certTargets = $ComputerName } foreach ($target in $certTargets) { try { $certs = if ($target -eq $env:COMPUTERNAME) { Get-ChildItem -Path 'Cert:\LocalMachine\My' -ErrorAction Stop | Where-Object { $_.NotAfter -and $_.NotAfter -gt $now } } else { Invoke-Command -ComputerName $target -ScriptBlock { Get-ChildItem -Path 'Cert:\LocalMachine\My' -ErrorAction Stop | Where-Object { $_.NotAfter -and $_.NotAfter -gt (Get-Date) } | Select-Object Subject, Thumbprint, NotAfter, FriendlyName, DnsNameList } -ErrorAction Stop } foreach ($cert in $certs) { $daysLeft = ($cert.NotAfter - $now).Days if ($daysLeft -le $DaysCertExpiry) { $certName = if ($cert.FriendlyName) { $cert.FriendlyName } elseif ($cert.Subject) { $cert.Subject -replace '^CN=', '' } else { $cert.Thumbprint.Substring(0, 16) } $prio = Get-AlertPriority -AlertType 'CertificateExpiring' -Detail @{ DaysUntilExpiry = $daysLeft } $alerts.Add([PSCustomObject]@{ AlertType = 'CertificateExpiring' Priority = $prio.Priority Source = 'Certificates' AffectedObject = "$target - $certName" Detail = "Certificate expires in $daysLeft day(s) on $($cert.NotAfter.ToString('yyyy-MM-dd')) (Thumbprint: $($cert.Thumbprint))" Timestamp = $now Category = 'Expiration' ColorCode = $prio.ColorCode SortOrder = $prio.SortOrder }) } } } catch { Write-Warning "ExpirationAlerts: Certificate check failed on $target - $_" } } # ──────────────────────────────────────────────────────────────────── # 2. Service account passwords expiring (HIGH) # ──────────────────────────────────────────────────────────────────── try { $domainPolicy = Get-ADDefaultDomainPasswordPolicy -ErrorAction Stop $maxPasswordAge = $domainPolicy.MaxPasswordAge if ($maxPasswordAge -and $maxPasswordAge -ne [TimeSpan]::Zero) { # Service accounts: commonly in specific OUs or with specific naming # We look for user accounts marked as service accounts (non-interactive) $svcAccounts = Get-ADUser -Filter { Enabled -eq $true -and PasswordNeverExpires -eq $false -and ServicePrincipalName -like "*" } -Properties PasswordLastSet, DisplayName, SamAccountName, ServicePrincipalName -ErrorAction Stop foreach ($svc in $svcAccounts) { if (-not $svc.PasswordLastSet) { continue } $expiryDate = $svc.PasswordLastSet + $maxPasswordAge $daysRemaining = ($expiryDate - $now).Days if ($daysRemaining -le $DaysPasswordExpiry -and $daysRemaining -ge 0) { $prio = Get-AlertPriority -AlertType 'ServiceAccountPasswordExpiring' $alerts.Add([PSCustomObject]@{ AlertType = 'ServiceAccountPasswordExpiring' Priority = $prio.Priority Source = 'ActiveDirectory' AffectedObject = "$($svc.DisplayName) ($($svc.SamAccountName))" Detail = "Service account password expires in $daysRemaining day(s) on $($expiryDate.ToString('yyyy-MM-dd')). SPN: $($svc.ServicePrincipalName -join ', ')" Timestamp = $now Category = 'Expiration' ColorCode = $prio.ColorCode SortOrder = $prio.SortOrder }) } } } } catch { Write-Warning "ExpirationAlerts: Service account password check failed - $_" } # ──────────────────────────────────────────────────────────────────── # 3. User accounts with approaching expiration dates (MEDIUM) # ──────────────────────────────────────────────────────────────────── try { $expirationCutoff = $now.AddDays($DaysAccountExpiry) $expiringAccounts = Get-ADUser -Filter { Enabled -eq $true -and AccountExpirationDate -le $expirationCutoff -and AccountExpirationDate -ge $now } -Properties AccountExpirationDate, DisplayName, SamAccountName -ErrorAction Stop foreach ($user in $expiringAccounts) { $daysLeft = ($user.AccountExpirationDate - $now).Days $prio = Get-AlertPriority -AlertType 'AccountExpirationApproaching' $alerts.Add([PSCustomObject]@{ AlertType = 'AccountExpirationApproaching' Priority = $prio.Priority Source = 'ActiveDirectory' AffectedObject = "$($user.DisplayName) ($($user.SamAccountName))" Detail = "Account expires in $daysLeft day(s) on $($user.AccountExpirationDate.ToString('yyyy-MM-dd'))" Timestamp = $now Category = 'Expiration' ColorCode = $prio.ColorCode SortOrder = $prio.SortOrder }) } } catch { Write-Warning "ExpirationAlerts: Account expiration check failed - $_" } # ──────────────────────────────────────────────────────────────────── # 4. Domain / Forest functional level (LOW - informational) # ──────────────────────────────────────────────────────────────────── try { $domain = Get-ADDomain -ErrorAction Stop $forest = Get-ADForest -ErrorAction Stop $domainFL = $domain.DomainMode $forestFL = $forest.ForestMode # Flag if not at the latest common level (Windows Server 2016 = Windows2016Domain) $modernLevels = @('Windows2016Domain', 'Windows2016Forest') if ($domainFL -notin $modernLevels -or $forestFL -notin $modernLevels) { $prio = Get-AlertPriority -AlertType 'DomainFunctionalLevel' $alerts.Add([PSCustomObject]@{ AlertType = 'DomainFunctionalLevel' Priority = $prio.Priority Source = 'ActiveDirectory' AffectedObject = $domain.DNSRoot Detail = "Domain FL: $domainFL, Forest FL: $forestFL. Consider raising to Windows Server 2016 level." Timestamp = $now Category = 'Expiration' ColorCode = $prio.ColorCode SortOrder = $prio.SortOrder }) } } catch { Write-Warning "ExpirationAlerts: Domain functional level check failed - $_" } # ──────────────────────────────────────────────────────────────────── # 5. DHCP scope utilization > 90% (HIGH) # ──────────────────────────────────────────────────────────────────── try { # Only runs if DHCP server tools are available if (Get-Command Get-DhcpServerv4ScopeStatistics -ErrorAction SilentlyContinue) { $dhcpStats = Get-DhcpServerv4ScopeStatistics -ErrorAction Stop foreach ($scope in $dhcpStats) { if ($scope.PercentageInUse -ge 90) { $prio = Get-AlertPriority -AlertType 'DHCPScopeHighUtilization' $alerts.Add([PSCustomObject]@{ AlertType = 'DHCPScopeHighUtilization' Priority = $prio.Priority Source = 'DHCP' AffectedObject = "Scope $($scope.ScopeId)" Detail = "DHCP scope $($scope.ScopeId) is $([math]::Round($scope.PercentageInUse, 1))% utilized ($($scope.InUse) of $($scope.Free + $scope.InUse) addresses)" Timestamp = $now Category = 'Expiration' ColorCode = $prio.ColorCode SortOrder = $prio.SortOrder }) } } } } catch { Write-Warning "ExpirationAlerts: DHCP scope check failed - $_" } return , $alerts.ToArray() } |