Public/Get-CIHealthScore.ps1
|
function Get-CIHealthScore { <# .SYNOPSIS Calculates an aggregate health score (0-100) for one or more configuration items. .DESCRIPTION Evaluates the health of servers by aggregating multiple data sources: - Recent runbook execution failures - Recent configuration changes (via Infra-ChangeTracker if available) - Open ITSM tickets - Recurring issues from the learning system - Certificate expiry proximity (via Certificate-LifecycleMonitor if available) - Service health checks Score formula: Start at 100, subtract for each negative factor. Grading: A=90-100, B=80-89, C=70-79, D=60-69, F=below 60. .PARAMETER ComputerName One or more computer names to score. .PARAMETER IncludeRunbookHistory Include recent runbook execution results in the score calculation. .PARAMETER IncludeChangeHistory Include recent configuration changes (requires Infra-ChangeTracker module). .PARAMETER IncludeTicketHistory Include open ticket count from an ITSM provider. .PARAMETER ITSMProvider ITSM provider type for ticket lookup: ServiceNow, Jira, or CSV. .PARAMETER ITSMEndpoint URL or path for the ITSM provider. .PARAMETER ITSMCredential Credential for ITSM authentication. .PARAMETER DaysBack Number of days of history to consider. Default is 30. .PARAMETER OutputPath Path to save an HTML health score report. .EXAMPLE Get-CIHealthScore -ComputerName 'SERVER01' -IncludeRunbookHistory Get health score for SERVER01 including runbook execution history. .EXAMPLE Get-CIHealthScore -ComputerName 'DC01','DC02' -IncludeRunbookHistory -IncludeChangeHistory -DaysBack 7 -OutputPath 'C:\Reports\health.html' Score two DCs with full history and generate an HTML report. .EXAMPLE Get-CIHealthScore -ComputerName 'WEB01' -IncludeTicketHistory -ITSMProvider ServiceNow -ITSMEndpoint 'https://instance.service-now.com' -ITSMCredential $cred Score including open ServiceNow tickets. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string[]]$ComputerName, [Parameter()] [switch]$IncludeRunbookHistory, [Parameter()] [switch]$IncludeChangeHistory, [Parameter()] [switch]$IncludeTicketHistory, [Parameter()] [ValidateSet('ServiceNow', 'Jira', 'CSV')] [string]$ITSMProvider = 'ServiceNow', [Parameter()] [string]$ITSMEndpoint, [Parameter()] [PSCredential]$ITSMCredential, [Parameter()] [int]$DaysBack = 30, [Parameter()] [string]$OutputPath ) $results = foreach ($target in $ComputerName) { $score = 100 $factors = [System.Collections.Generic.List[object]]::new() $recommendations = [System.Collections.Generic.List[string]]::new() $cutoffDate = (Get-Date).AddDays(-$DaysBack) # Factor 1: Runbook execution history if ($IncludeRunbookHistory) { $executionsPath = Join-Path $env:USERPROFILE '.runbookengine\executions' if (Test-Path $executionsPath) { $execFiles = Get-ChildItem -Path $executionsPath -Filter '*.json' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $cutoffDate } $totalExecs = 0 $failedExecs = 0 $escalatedExecs = 0 foreach ($file in $execFiles) { try { $exec = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json -ErrorAction Stop $execTarget = if ($exec.ComputerName) { $exec.ComputerName } elseif ($exec.Parameters -and $exec.Parameters.ComputerName) { $exec.Parameters.ComputerName } else { $null } if ($execTarget -eq $target) { $totalExecs++ if ($exec.Status -eq 'Failed') { $failedExecs++ } if ($exec.Status -eq 'Escalated') { $escalatedExecs++ } } } catch { Write-Verbose "Failed to parse execution file: $_" } } if ($failedExecs -gt 0) { $penalty = [math]::Min($failedExecs * 5, 25) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Failed Runbooks' Impact = -$penalty Details = "$failedExecs failed executions in the last $DaysBack days" }) $recommendations.Add("Investigate $failedExecs failed runbook executions on $target") } if ($escalatedExecs -gt 0) { $penalty = [math]::Min($escalatedExecs * 3, 15) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Escalated Issues' Impact = -$penalty Details = "$escalatedExecs escalated issues in the last $DaysBack days" }) } if ($totalExecs -gt 0 -and $failedExecs -eq 0) { $factors.Add([PSCustomObject]@{ Name = 'Runbook Success' Impact = 0 Details = "$totalExecs successful runbook executions" }) } } } # Factor 2: Learning system - recurring failures $learningsPath = Join-Path $env:USERPROFILE '.runbookengine\learnings.json' if (Test-Path $learningsPath) { try { $rawLearnings = Get-Content -Path $learningsPath -Raw -ErrorAction Stop if ($rawLearnings -and $rawLearnings.Trim().Length -gt 2) { $learnings = @($rawLearnings | ConvertFrom-Json -ErrorAction Stop) $lowSuccessSteps = $learnings | Where-Object { $_.SuccessRate -lt 50 -and $_.TotalRuns -ge 3 -and ($_.RecentExecutions | Where-Object { $_.ComputerName -eq $target }) } if ($lowSuccessSteps -and @($lowSuccessSteps).Count -gt 0) { $penalty = [math]::Min(@($lowSuccessSteps).Count * 5, 20) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Recurring Failures' Impact = -$penalty Details = "$(@($lowSuccessSteps).Count) runbook steps with <50% success rate on $target" }) $recommendations.Add("Review runbook steps with low success rates: $($lowSuccessSteps | ForEach-Object { "$($_.RunbookName)/$($_.StepId)" } | Select-Object -First 3 | ForEach-Object { $_ })") } } } catch { Write-Verbose "Failed to parse learnings: $_" } } # Factor 3: Change history (via Infra-ChangeTracker integration) if ($IncludeChangeHistory) { try { if (Get-Command 'Get-ServerConfigChanges' -ErrorAction SilentlyContinue) { $changes = Get-ServerConfigChanges -ComputerName $target -HoursBack ($DaysBack * 24) -ErrorAction Stop if ($changes -and @($changes).Count -gt 0) { $changeCount = @($changes).Count $penalty = if ($changeCount -gt 20) { 10 } elseif ($changeCount -gt 10) { 5 } else { 2 } $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Recent Changes' Impact = -$penalty Details = "$changeCount configuration changes in the last $DaysBack days" }) if ($changeCount -gt 15) { $recommendations.Add("High change velocity on $target ($changeCount changes). Review change management process.") } } } else { Write-Verbose "Infra-ChangeTracker not available. Skipping change history." } } catch { Write-Verbose "Change history lookup failed: $_" } } # Factor 4: ITSM ticket count if ($IncludeTicketHistory -and $ITSMEndpoint) { $openTickets = 0 try { switch ($ITSMProvider) { 'ServiceNow' { if ($ITSMCredential) { $encodedCI = [System.Uri]::EscapeDataString($target) $uri = "$($ITSMEndpoint.TrimEnd('/'))/api/now/table/incident?sysparm_query=cmdb_ci.name=$encodedCI^state!=7&sysparm_count=true" $response = Invoke-RestMethod -Uri $uri -Method Get -Credential $ITSMCredential ` -Headers @{ 'Accept' = 'application/json' } -ErrorAction Stop $openTickets = if ($response.result) { @($response.result).Count } else { 0 } } } 'CSV' { if (Test-Path $ITSMEndpoint) { $csv = Import-Csv -Path $ITSMEndpoint -ErrorAction Stop $openTickets = @($csv | Where-Object { ($_.CI -eq $target -or $_.ComputerName -eq $target -or $_.Server -eq $target) -and ($_.State -notin @('Closed', 'Resolved', 'Done')) }).Count } } } } catch { Write-Verbose "ITSM ticket lookup failed: $_" } if ($openTickets -gt 0) { $penalty = [math]::Min($openTickets * 3, 15) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Open Tickets' Impact = -$penalty Details = "$openTickets open tickets for $target" }) $recommendations.Add("Resolve $openTickets open tickets for $target") } } # Factor 5: Service health (live check) try { $criticalServices = Get-CimInstance -ClassName Win32_Service -ComputerName $target -ErrorAction Stop | Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } if ($criticalServices -and @($criticalServices).Count -gt 0) { $stoppedCount = @($criticalServices).Count $penalty = [math]::Min($stoppedCount * 4, 20) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Stopped Services' Impact = -$penalty Details = "$stoppedCount auto-start services are not running" }) $serviceNames = ($criticalServices | Select-Object -First 5 | ForEach-Object { $_.Name }) -join ', ' $recommendations.Add("Investigate stopped services on ${target}: $serviceNames") } } catch { Write-Verbose "Service health check failed for ${target}: $_" $factors.Add([PSCustomObject]@{ Name = 'Health Check' Impact = -5 Details = "Unable to perform live service health check on $target" }) $score -= 5 } # Factor 6: Certificate expiry (via Certificate-LifecycleMonitor integration) try { if (Get-Command 'Get-CertificateExpiry' -ErrorAction SilentlyContinue) { $certs = Get-CertificateExpiry -ComputerName $target -ErrorAction Stop $expiringCerts = @($certs | Where-Object { $_.DaysUntilExpiry -lt 30 -and $_.DaysUntilExpiry -ge 0 }) $expiredCerts = @($certs | Where-Object { $_.DaysUntilExpiry -lt 0 }) if ($expiredCerts.Count -gt 0) { $penalty = [math]::Min($expiredCerts.Count * 10, 20) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Expired Certificates' Impact = -$penalty Details = "$($expiredCerts.Count) expired certificates on $target" }) $recommendations.Add("URGENT: Renew $($expiredCerts.Count) expired certificates on $target") } if ($expiringCerts.Count -gt 0) { $penalty = [math]::Min($expiringCerts.Count * 3, 10) $score -= $penalty $factors.Add([PSCustomObject]@{ Name = 'Expiring Certificates' Impact = -$penalty Details = "$($expiringCerts.Count) certificates expiring within 30 days on $target" }) $recommendations.Add("Plan renewal for $($expiringCerts.Count) certificates expiring soon on $target") } } } catch { Write-Verbose "Certificate check not available: $_" } # Ensure score stays within bounds $score = [math]::Max(0, [math]::Min(100, $score)) # Determine grade $grade = if ($score -ge 90) { 'A' } elseif ($score -ge 80) { 'B' } elseif ($score -ge 70) { 'C' } elseif ($score -ge 60) { 'D' } else { 'F' } # Determine trend (compare to previous scores if available) $trend = 'Stable' $executionsPath = Join-Path $env:USERPROFILE '.runbookengine\executions' if (Test-Path $executionsPath) { $recentFiles = Get-ChildItem -Path $executionsPath -Filter '*.json' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge (Get-Date).AddDays(-7) } $olderFiles = Get-ChildItem -Path $executionsPath -Filter '*.json' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge (Get-Date).AddDays(-14) -and $_.LastWriteTime -lt (Get-Date).AddDays(-7) } $recentFailures = 0 $olderFailures = 0 foreach ($f in $recentFiles) { try { $e = Get-Content -Path $f.FullName -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue $eTarget = if ($e.ComputerName) { $e.ComputerName } else { $null } if ($eTarget -eq $target -and $e.Status -eq 'Failed') { $recentFailures++ } } catch { } } foreach ($f in $olderFiles) { try { $e = Get-Content -Path $f.FullName -Raw | ConvertFrom-Json -ErrorAction SilentlyContinue $eTarget = if ($e.ComputerName) { $e.ComputerName } else { $null } if ($eTarget -eq $target -and $e.Status -eq 'Failed') { $olderFailures++ } } catch { } } if ($recentFailures -lt $olderFailures) { $trend = 'Improving' } elseif ($recentFailures -gt $olderFailures) { $trend = 'Declining' } } # Add general recommendations based on grade if ($grade -eq 'F') { $recommendations.Add("CRITICAL: $target health is severely degraded. Immediate attention required.") } elseif ($grade -eq 'D') { $recommendations.Add("$target health is below acceptable threshold. Schedule maintenance window for remediation.") } [PSCustomObject]@{ ComputerName = $target Score = $score Grade = $grade Trend = $trend Factors = $factors.ToArray() Recommendations = $recommendations.ToArray() DaysBack = $DaysBack AssessedAt = (Get-Date).ToString('o') } } # Generate HTML report if requested if ($OutputPath -and $results) { $reportData = if (@($results).Count -eq 1) { $results } else { $results[0] } $htmlPath = New-HtmlDashboard -ReportType 'HealthScore' -Data $reportData -OutputPath $OutputPath Write-Host "Health score report saved: $htmlPath" -ForegroundColor Green } return $results } |