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
}