Private/helper-functions.ps1

function Write-ToReport {
    param(
        [string]$Message
    )
    # if ($Global:Report) {
    Add-Content -Path $ReportFile -Value $Message
    # }
}

function Build-ChecksFromReport {
    param([string]$ReportFile)

    $reportLines = Get-Content $ReportFile
    $checks = @{}

    # Parse NodeConditions
    $notReadyLine = $reportLines | Where-Object { $_ -match 'Not Ready Nodes.*:\s*(\d+)' }
    if ($notReadyLine -match '(\d+)') {
        $checks["nodeConditions"] = @{ Total = 4; NotReady = [int]$matches[1] }
    }

    # Parse NodeResourceUsage
    $resourceWarningLine = $reportLines | Where-Object { $_ -match 'Total Resource Warnings.*:\s*(\d+)' }
    if ($resourceWarningLine -match '(\d+)') {
        $checks["nodeResources"] = @{ Total = 4; Warnings = [int]$matches[1] }
    }

    $sectionPatterns = @{
        "missingProbes"           = 'Missing Health Probes'
        "missingResourceLimits"   = 'Missing Resource Limits'
        "daemonSetIssues"         = 'DaemonSets Not Fully Running'
        "emptyNamespace"          = 'Empty Namespaces'
        "namespaceLimitRanges"    = 'Missing LimitRanges'
        "resourceQuotas"          = 'Missing or Weak ResourceQuotas'
        "HPA"                     = 'HorizontalPodAutoscalers'
        "PDB"                     = 'PodDisruptionBudget Coverage Check'
        "podsRestart"             = 'Pods With High Restarts'
        "podLongRunning"          = 'Long Running Pods'
        "podFail"                 = 'Failed Pods'
        "podPending"              = 'Pending Pods'
        "crashloop"               = 'CrashLoopBackOff Pods'
        "leftoverDebug"           = 'Leftover Debug Pods'
        "stuckJobs"               = 'Stuck Jobs'
        "jobFail"                 = 'Failed Jobs'
        "servicesWithoutEndpoints"= 'Services Without Endpoints'
        "publicServices"          = 'Publicly Accessible Services'
        "ingressHealth"           = 'Ingress Health'
        "unmountedPV"             = 'Unused PVCs'
        "rbacMisconfig"           = 'RBAC Misconfigurations'
        "rbacOverexposure"        = 'RBAC Overexposure'
        "orphanedRoles"           = 'Unused Roles & ClusterRoles'
        "orphanedServiceAccounts" = 'Orphaned ServiceAccounts'
        "orphanedConfigMaps"      = 'Orphaned ConfigMaps'
        "orphanedSecrets"         = 'Orphaned Secrets'
        "podsRoot"                = 'Pods Running as Root'
        "privilegedContainers"    = 'Privileged Containers'
        "hostPidNet"              = 'Pods with hostPID / hostNetwork'
        "deploymentIssues"        = 'Deployment Issues'
        "statefulSetIssues"       = 'StatefulSet Issues'
        "eventSummary"            = 'Kubernetes Warnings'
    }

    foreach ($key in $sectionPatterns.Keys) {
        $pattern = [regex]::Escape($sectionPatterns[$key])
        $sectionIndex = $reportLines | ForEach-Object { if ($_ -match "$pattern") { $reportLines.IndexOf($_) } } | Select-Object -First 1
        if ($null -ne $sectionIndex) {
            $nextSectionIndex = ($reportLines | ForEach-Object { if ($_.StartsWith('[') -and $reportLines.IndexOf($_) -gt $sectionIndex) { $reportLines.IndexOf($_) } } | Select-Object -First 1) ?? $reportLines.Count
            $sectionContent = $reportLines[$sectionIndex..($nextSectionIndex - 1)]
            $totalLine = $sectionContent | Where-Object { $_ -match '⚠️\s*Total.*:\s*(\d+)' } | Select-Object -Last 1
            if ($totalLine -match '(\d+)') {
                $checks[$key] = @{ Total = [int]$matches[1]; Items = @(1..$matches[1]) }
            }
            elseif ($sectionContent | Where-Object { $_ -match '✅' }) {
                $checks[$key] = @{ Total = 1; Items = @() }
            }
        }
    }

    return $checks
}

function Generate-K8sTextReport {
    param (
        [string]$ReportFile = "$pwd/kubebuddy-report.txt",
        [switch]$ExcludeNamespaces,
        [object]$KubeData,
        [switch]$Aks,
        [string]$SubscriptionId,
        [string]$ResourceGroup,
        [string]$ClusterName
    )

    $Global:MakeReport = $true

    if (Test-Path $ReportFile) {
        Remove-Item $ReportFile -Force
    }

    Write-ToReport "--- Kubernetes Cluster Report ---"
    Write-ToReport "Timestamp: $(Get-Date)"
    Write-ToReport "---------------------------------"

    Write-ToReport "`n[🌐 Cluster Summary]`n"
    Show-ClusterSummary
    Write-Host "`n🤖 Cluster Summary fetched. " -ForegroundColor Green

    # Checks with camelCase keys
    $checks = @{}

    $checks["nodeConditions"] = Show-NodeConditions -KubeData:$KubeData
    $checks["nodeResources"] = Show-NodeResourceUsage -KubeData:$KubeData
    Write-Host "`n🤖 Node Information fetched. " -ForegroundColor Green

    $checks["emptyNamespace"] = Show-EmptyNamespaces -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["resourceQuotas"] = Check-ResourceQuotas -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["namespaceLimitRanges"] = Check-NamespaceLimitRanges -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Namespace Information fetched. " -ForegroundColor Green

    $checks["daemonSetIssues"] = Show-DaemonSetIssues -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["deploymentIssues"] = Check-DeploymentIssues -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["statefulSetIssues"] = Check-StatefulSetIssues -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["HPA"] = Check-HPAStatus -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["missingResourceLimits"] = Check-MissingResourceLimits -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["PDB"] = Check-PodDisruptionBudgets -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["missingProbes"] = Check-MissingHealthProbes -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Workload Information fetched. " -ForegroundColor Green

    $checks["podsRestart"] = Show-PodsWithHighRestarts -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["podLongRunning"] = Show-LongRunningPods -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["podFail"] = Show-FailedPods -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["podPending"] = Show-PendingPods -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["crashloop"] = Show-CrashLoopBackOffPods -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["leftoverDebug"] = Show-LeftoverDebugPods -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Pod Information fetched. " -ForegroundColor Green

    $checks["stuckJobs"] = Show-StuckJobs -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["jobFail"] = Show-FailedJobs -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Job Information fetched. " -ForegroundColor Green

    $checks["servicesWithoutEndpoints"] = Show-ServicesWithoutEndpoints -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["publicServices"] = Check-PubliclyAccessibleServices -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["ingressHealth"] = Check-IngressHealth -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Service Information fetched. " -ForegroundColor Green

    $checks["unmountedPV"] = Show-UnusedPVCs -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Storage Information fetched. " -ForegroundColor Green

    $checks["rbacMisconfig"] = Check-RBACMisconfigurations -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["rbacOverexposure"] = Check-RBACOverexposure -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["orphanedRoles"] = Check-OrphanedRoles -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["orphanedServiceAccounts"] = Check-OrphanedServiceAccounts -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["orphanedConfigMaps"] = Check-OrphanedConfigMaps -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["orphanedSecrets"] = Check-OrphanedSecrets -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["podsRoot"] = Check-PodsRunningAsRoot -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["privilegedContainers"] = Check-PrivilegedContainers -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    $checks["hostPidNet"] = Check-HostPidAndNetwork -ExcludeNamespaces:$ExcludeNamespaces -KubeData:$KubeData
    Write-Host "`n🤖 Security Information fetched. " -ForegroundColor Green

    $checks["eventSummary"] = Show-KubeEvents -KubeData:$KubeData
    Write-Host "`n🤖 Kube Events fetched. " -ForegroundColor Green

    if ($aks) {
        Invoke-AKSBestPractices -SubscriptionId $SubscriptionId -ResourceGroup $ResourceGroup -ClusterName $ClusterName -KubeData:$KubeData
        Write-Host "`n🤖 AKS Information fetched. " -ForegroundColor Green
    }

    # Cluster score
    $parsedChecks = Build-ChecksFromReport -ReportFile $ReportFile
    $clusterScore = Get-ClusterHealthScore -Checks $parsedChecks
    Write-ToReport "`n🩺 Cluster Health Score: $clusterScore / 100"      

    $Global:MakeReport = $false
}

function Get-KubeBuddyThresholds {
    param(
        [switch]$Silent  # Suppress output when set
    )

    $configPath = "$HOME/.kube/kubebuddy-config.yaml"

    if (Test-Path $configPath) {
        try {
            # Read the YAML file and convert it to a PowerShell object
            $configContent = Get-Content -Raw $configPath | ConvertFrom-Yaml
            
            if ($configContent -and $configContent.thresholds) {
                return $configContent.thresholds
            }
            else {
                if (-not $Silent) {
                    Write-Host "`n⚠️ Config found, but missing 'thresholds' section. Using defaults..." -ForegroundColor Yellow
                }
            }
        }
        catch {
            if (-not $Silent) {
                Write-Host "`n❌ Failed to parse config file. Using defaults..." -ForegroundColor Red
            }
        }
    }
    else {
        if (-not $Silent) {
            Write-Host "`n⚠️ No config found. Using default thresholds..." -ForegroundColor Yellow
        }
    }

    # Return default thresholds if no valid config is found
    return @{
        cpu_warning       = 50
        cpu_critical      = 75
        mem_warning       = 50
        mem_critical      = 75
        restarts_warning  = 3
        restarts_critical = 5
        pod_age_warning   = 15
        pod_age_critical  = 40
        stuck_job_hours   = 2
        failed_job_hours  = 2
        event_errors_warning    = 10
        event_errors_critical   = 20
        event_warnings_warning  = 50
        event_warnings_critical = 100
    }
}

function Get-ExcludedNamespaces {
    $config = Get-KubeBuddyThresholds -Silent
    if ($config -and $config.ContainsKey("excluded_namespaces")) {
        return $config["excluded_namespaces"]
    }

    return @(
        "kube-system", "kube-public", "kube-node-lease",
        "local-path-storage", "kube-flannel",
        "tigera-operator", "calico-system", "coredns", "aks-istio-system"
    )
}

function Exclude-Namespaces {
    param([array]$items)

    $excludedNamespaces = Get-ExcludedNamespaces
    $excludedSet = $excludedNamespaces | ForEach-Object { $_.ToLowerInvariant() }

    return $items | Where-Object {
        if ($_ -is [string]) {
            $_.ToLowerInvariant() -notin $excludedSet
        } elseif ($_.metadata) {
            $ns = if ($_.metadata.namespace) {
                $_.metadata.namespace
            } elseif ($_.metadata.name) {
                $_.metadata.name
            } else {
                $null
            }

            $ns -and $ns.ToLowerInvariant() -notin $excludedSet
        } else {
            $true
        }
    }
}

function Show-Pagination {
    param(
        [int]$currentPage,
        [int]$totalPages
    )

    Write-Host "`nPage $($currentPage + 1) of $totalPages"

    $options = @()
    if ($currentPage -lt ($totalPages - 1)) { $options += "N = Next" }
    if ($currentPage -gt 0) { $options += "P = Previous" }
    $options += "C = Continue"

    # Ensure 'P' does not appear on the first page
    if ($currentPage -eq 0) { $options = $options -notmatch "P = Previous" }

    # Ensure 'N' does not appear on the last page
    if ($currentPage -eq ($totalPages - 1)) { $options = $options -notmatch "N = Next" }

    # Display available options
    Write-Host ($options -join ", ") -ForegroundColor Yellow

    do {
        $paginationInput = Read-Host "Enter your choice"
    } while ($paginationInput -notmatch "^[NnPpCc]$" -or 
             ($paginationInput -match "^[Nn]$" -and $currentPage -eq ($totalPages - 1)) -or 
             ($paginationInput -match "^[Pp]$" -and $currentPage -eq 0))

    if ($paginationInput -match "^[Nn]$") { return $currentPage + 1 }
    elseif ($paginationInput -match "^[Pp]$") { return $currentPage - 1 }
    elseif ($paginationInput -match "^[Cc]$") { return -1 }  # Exit pagination
}

# Function to detect if running in any container
function Test-IsContainer {
    if ((Test-Path "/.dockerenv") -or (Test-Path "/run/.containerenv")) {
        return $true
    }

    try {
        $cgroup = Get-Content "/proc/1/cgroup" -ErrorAction SilentlyContinue
        if ($cgroup -match "docker|kubepods|crio|containerd") {
            return $true
        }
    } catch {}

    if ($env:container) { return $true }

    return $false
}