Private/yamlChecks-function.ps1

function Invoke-yamlChecks {
    param(
        [object]$KubeData,
        [string]$Namespace = "",
        [switch]$Html,
        [switch]$Json,
        [switch]$Text,
        [switch]$ExcludeNamespaces,
        [string[]]$CheckIDs = @()  # Optional parameter to filter specific check IDs
    )

    # Configuration
    $checksFolder = "$PSScriptRoot/yamlChecks"
    $kubectl = "kubectl"

    # Ensure required modules
    try {
        Import-Module powershell-yaml -ErrorAction Stop
        # Import all PowerShell modules from $PSScriptRoot
        Get-ChildItem -Path $PSScriptRoot -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
            Import-Module $_.FullName -ErrorAction Stop
        }
    }
    catch {
        Write-Host "❌ Failed to load required module: $_" -ForegroundColor Red
        if ($Html) { return "<p><strong>❌ Failed to load required module.</strong></p>" }
        if ($Json) { return @{ Error = "Failed to load required module: $_" } }
        Read-Host "🤖 Error. Check logs or output above. Press Enter to continue"
        return
    }

    function Get-ValidProperties {
        param (
            [array]$Items,
            [string]$CheckID
        )
    
        # Static table layouts for known checks
        $checkSpecificProperties = @{
            "NODE001" = @("Node", "Status", "Issues")
            "NODE002" = @("Node", "CPU Status", "CPU %", "CPU Used", "CPU Total", "Mem Status", "Mem %", "Mem Used", "Mem Total", "Disk %", "Disk Status")
        }
    
        if ($checkSpecificProperties.ContainsKey($CheckID)) {
            $properties = $checkSpecificProperties[$CheckID]
        }
        else {
            # Detect valid properties dynamically for unknown/script-based checks
            $properties = $Items |
            ForEach-Object { $_.PSObject.Properties.Name } |
            Group-Object |
            Sort-Object Count -Descending |
            Select-Object -ExpandProperty Name -Unique
        }
    
        $validProps = @()
        foreach ($prop in $properties) {
            $hasData = $Items | Where-Object {
                $_.$prop -ne $null -and $_.$prop -ne "" -and $_.$prop -ne "-"
            }
            if ($hasData.Count -gt 0) {
                $validProps += $prop
            }
        }
    
        # Fallback to static props if NODE check and nothing found
        if (-not $validProps -and $CheckID -in @("NODE001", "NODE002")) {
            $validProps = $checkSpecificProperties[$CheckID]
        }
    
        return $validProps
    }

    function Get-ResourceKindDisplayNames {
        param (
            [string]$ResourceKind
        )

        # Map of ResourceKind values to their singular and plural forms
        $resourceKindMap = @{
            "namespaces"              = @{ Singular = "Namespace"; Plural = "Namespaces" }
            "resourcequotas"          = @{ Singular = "ResourceQuota"; Plural = "ResourceQuotas" }
            "limitranges"             = @{ Singular = "LimitRange"; Plural = "LimitRanges" }
            "Service"                 = @{ Singular = "Service"; Plural = "Services" }
            "Ingress"                 = @{ Singular = "Ingress"; Plural = "Ingresses" }
            "ClusterRoleBinding"      = @{ Singular = "ClusterRoleBinding"; Plural = "ClusterRoleBindings" }
            "ServiceAccount"          = @{ Singular = "ServiceAccount"; Plural = "ServiceAccounts" }
            "Role, ClusterRole"       = @{ Singular = "Role/ClusterRole"; Plural = "Roles/ClusterRoles" }
            "Secret"                  = @{ Singular = "Secret"; Plural = "Secrets" }
            "Pod"                     = @{ Singular = "Pod"; Plural = "Pods" }
            "DaemonSet"               = @{ Singular = "DaemonSet"; Plural = "DaemonSets" }
            "Deployment"              = @{ Singular = "Deployment"; Plural = "Deployments" }
            "StatefulSet"             = @{ Singular = "StatefulSet"; Plural = "StatefulSets" }
            "HorizontalPodAutoscaler" = @{ Singular = "HorizontalPodAutoscaler"; Plural = "HorizontalPodAutoscalers" }
            "PersistentVolumeClaim"   = @{ Singular = "PersistentVolumeClaim"; Plural = "PersistentVolumeClaims" }
            "events"                  = @{ Singular = "Event"; Plural = "Events" }
            "jobs"                    = @{ Singular = "Job"; Plural = "Jobs" }
            "ConfigMap"               = @{ Singular = "ConfigMap"; Plural = "ConfigMaps" }
            "Node"                    = @{ Singular = "Node"; Plural = "Nodes" }
        }

        if ($resourceKindMap.ContainsKey($ResourceKind)) {
            return $resourceKindMap[$ResourceKind]
        }
        else {
            # Default: assume the ResourceKind is singular and append "s" for plural
            return @{
                Singular = $ResourceKind
                Plural   = "$ResourceKind" + "s"
            }
        }
    }

    # Fetch thresholds
    $thresholds = if ($Text -or $Html -or $Json) {
        Get-KubeBuddyThresholds -Silent
    }
    else {
        Get-KubeBuddyThresholds
    }

    # Scan for YAML files
    try {
        if (-not (Test-Path $checksFolder)) {
            Write-Host "⚠️ Checks folder $checksFolder does not exist." -ForegroundColor Yellow
            if ($Html) { return "<p><strong>⚠️ Checks folder does not exist.</strong></p>" }
            if ($Json) { return @{ Total = 0; Items = @() } }
            return
        }
        $checkFiles = Get-ChildItem -Path $checksFolder -Filter "*.yaml" -ErrorAction Stop
    }
    catch {
        Write-Host "❌ Error scanning ${checksFolder}: $_" -ForegroundColor Red
        if ($Html) { return "<p><strong>❌ Error scanning checks folder.</strong></p>" }
        if ($Json) { return @{ Error = "Error scanning checks folder: $_" } }
        Read-Host "🤖 Error. Check logs or output above. Press Enter to continue"
        return
    }

    if (-not $checkFiles) {
        Write-Host "✅ No custom check YAML files found." -ForegroundColor Green
        if ($Html) { return "<p><strong>✅ No custom checks found.</strong></p>" }
        if ($Json) { return @{ Total = 0; Items = @() } }
        return
    }

    # Initialize thread-safe collection for results
    $allResults = [System.Collections.Concurrent.ConcurrentBag[PSObject]]::new()

    # Process checks in parallel and collect results
    $parallelResults = $checkFiles | ForEach-Object -Parallel {
        # Re-import required modules in parallel scope
        try {
            Import-Module powershell-yaml -ErrorAction Stop
            # Import all PowerShell modules from $using:PSScriptRoot
            Get-ChildItem -Path $using:PSScriptRoot -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
                Import-Module $_.FullName -ErrorAction Stop
            }
        }
        catch {
            $errorMessage = "❌ Failed to load required module in parallel scope: $_"
            Write-Host $errorMessage -ForegroundColor Red
            return @{
                ID    = "Unknown"
                Name  = $_.Name
                Error = $errorMessage
            }
        }

        $localResults = @()

        try {
            $yamlContent = Get-Content $_.FullName -Raw | ConvertFrom-Yaml
            if (-not $yamlContent.checks) {
                return
            }

            $excludedCheckIDs = $using:thresholds.excluded_checks
     
            foreach ($check in $yamlContent.checks) {
                # Skip if excluded AND not explicitly requested by -CheckIDs
                if ($excludedCheckIDs -contains $check.ID -and -not ($using:CheckIDs -and $check.ID -in $using:CheckIDs)) {
                    Write-Host "⏭️ Skipping excluded check: $($check.ID)" -ForegroundColor DarkGray
                    continue
                }
                # Filter checks if CheckIDs specified
                if ($using:CheckIDs -and $check.ID -notin $using:CheckIDs) {
                    continue
                }

                Write-Host "🤖 Processing check: $($check.ID) - $($check.Name)..." -ForegroundColor Cyan

                # Custom script block execution
                if ($check.Script) {
                    try {
                        # Define disallowed kubectl commands
                        $disallowedPatterns = @(
                            '\bkubectl\s+(create|run|edit|delete|patch|apply|replace|scale|rollout|annotate|label|taint|cordon|uncordon|drain|evict)\b',
                            '\bkubectl\s+.*\s--force\b',
                            '\bkubectl\s+.*\s--overwrite\b',
                            '\bkubectl\s+.*\s--grace-period\b',
                            '\bhelm\s+(install|upgrade|uninstall|rollback|delete|dep\s+update|template)\b',
                            '\bRemove-Item\b',
                            '\bSet-Content\b',
                            '\bNew-Item\b',
                            '\bStop-Process\b',
                            '\bStart-Process\b',
                            '[;\|]\s*kubectl\s+',
                            '[;\|]\s*helm\s+',
                            'kubectl\s+.*[`\\]\s*.*'
                        )

                        $scriptContent = $check.Script
                        $disallowedCommandFound = $false
                        $matchedPattern = $null

                        foreach ($pattern in $disallowedPatterns) {
                            if ($scriptContent -match $pattern) {
                                $disallowedCommandFound = $true
                                $matchedPattern = $pattern
                                break
                            }
                        }

                        if ($disallowedCommandFound) {
                            $errorMessage = "❌ Check $($check.ID) contains disallowed command pattern: `$matchedPattern`. Blocking execution."
                            Write-Host $errorMessage -ForegroundColor Red
                            $localResults += @{
                                ID    = $check.ID
                                Name  = $check.Name
                                Error = $errorMessage
                            }
                            continue
                        }

                        $scriptBlock = [scriptblock]::Create($check.Script)
                        $scriptResult = if ($check.ID -eq "NODE002") {
                            & $scriptBlock -KubeData $using:KubeData -Thresholds $using:thresholds
                        }
                        else {
                            & $scriptBlock -KubeData $using:KubeData -Namespace $using:Namespace -ExcludeNamespaces:$using:ExcludeNamespaces
                        }

                        $checkResult = @{
                            ID             = $check.ID
                            Name           = $check.Name
                            Category       = $check.Category
                            Section        = $check.Section
                            ResourceKind   = $check.ResourceKind
                            Severity       = $check.Severity
                            Weight         = $check.Weight
                            Description    = $check.Description
                            Recommendation = if ($using:Html) {
                                if ($check.Recommendation -is [hashtable] -and $check.Recommendation.html) {
                                    $recContent = $check.Recommendation.html
                                    @"
<div class="recommendation-card">
  <details style='margin-bottom: 10px;'>
    <summary style='color: #0071FF; font-weight: bold; font-size: 14px; padding: 10px; background: #E3F2FD; border-radius: 4px 4px 0 0;'>Recommendations</summary>
    $recContent
  </details>
</div>
<div style='height: 15px;'></div>
"@

                                }
                                else {
                                    $check.Recommendation
                                }
                            }
                            else {
                                if ($check.Recommendation -is [hashtable] -and $check.Recommendation.text) {
                                    $check.Recommendation.text
                                }
                                else {
                                    $check.Recommendation
                                }
                            }
                            URL            = $check.URL
                            Items          = @()
                            Total          = 0
                        }

                        if ($scriptResult -is [hashtable] -and $scriptResult.Items) {
                            $checkResult.Items = $scriptResult.Items
                            $checkResult.Total = $scriptResult.IssueCount
                        }
                        elseif ($scriptResult) {
                            $checkResult.Items = $scriptResult
                            $checkResult.Total = $scriptResult.Count
                        }

                        if ($checkResult.Total -eq 0) {
                            $checkResult.Message = "No issues detected for $($check.Name)."
                        }

                        $localResults += $checkResult
                        Write-Host "`✅ Completed check: $($check.ID) - $($check.Name) " -ForegroundColor Green
                    }
                    catch {
                        Write-Host "❌ Error executing script for $($check.ID): $_" -ForegroundColor Red
                        $localResults += @{
                            ID    = $check.ID
                            Name  = $check.Name
                            Error = "Script block execution failed: $_"
                        }
                    }
                    continue
                }

                # Non-script check logic
                $data = $null
                $kubeData = $using:KubeData  # Assign to local variable to avoid $using: in expressions
                if ($kubeData -and $check.ResourceKind -in $kubeData.PSObject.Properties.Name) {
                    $data = $kubeData.($check.ResourceKind).items
                }
                else {
                    $kubectlCmd = if ($using:Namespace) {
                        "$($using:kubectl) get $($check.ResourceKind) -n $($using:Namespace) -o json"
                    }
                    else {
                        "$($using:kubectl) get $($check.ResourceKind) --all-namespaces -o json"
                    }
                    $maxRetries = 3
                    $retryDelay = 2
                    $attempt = 0
                    $data = $null
                    $success = $false

                    while (-not $success -and $attempt -lt $maxRetries) {
                        try {
                            $output = Invoke-Expression $kubectlCmd 2>&1
                            if ($LASTEXITCODE -ne 0) {
                                throw "kubectl failed: $output"
                            }
                            $data = ($output | ConvertFrom-Json).items
                            $success = $true
                        }
                        catch {
                            $attempt++
                            if ($attempt -lt $maxRetries) {
                                Start-Sleep -Seconds $retryDelay
                            }
                            else {
                                Write-Host "❌ Failed to fetch $($check.ResourceKind) data after $maxRetries attempts: $_" -ForegroundColor Red
                                $localResults += @{
                                    ID    = $check.ID
                                    Name  = $check.Name
                                    Error = "Failed to fetch data after $maxRetries attempts: $_"
                                }
                                continue
                            }
                        }
                    }
                }

                if (-not $data) {
                    Write-Host "❌ No $($check.ResourceKind) data available." -ForegroundColor Red
                    $localResults += @{
                        ID      = $check.ID
                        Name    = $check.Name
                        Message = "No $($check.ResourceKind) data available."
                    }
                    continue
                }

                if ($using:Namespace -and $data[0].metadata.PSObject.Properties.Name -contains 'namespace') {
                    $data = $data | Where-Object { $_.metadata.namespace -eq $using:Namespace }
                }

                if ($using:ExcludeNamespaces) {
                    $data = Exclude-Namespaces -items $data
                }

                $checkResult = @{
                    ID             = $check.ID
                    Name           = $check.Name
                    Category       = $check.Category
                    Section        = $check.Section
                    ResourceKind   = $check.ResourceKind
                    Severity       = $check.Severity
                    Weight         = $check.Weight
                    Description    = $check.Description
                    Recommendation = if ($using:Html) {
                        if ($check.Recommendation -is [hashtable] -and $check.Recommendation.html) {
                            $recContent = $check.Recommendation.html
                            @"
<div class="recommendation-card">
  <details style='margin-bottom: 10px;'>
    <summary style='color: #0071FF; font-weight: bold; font-size: 14px; padding: 10px; background: #E3F2FD; border-radius: 4px 4px 0 0;'>Recommendations</summary>
                            $recContent
                        </details>
                    </div>
                    <div style='height: 15px;'></div>
"@

                        }
                        else {
                            $check.Recommendation
                        }
                    }
                    else {
                        if ($check.Recommendation -is [hashtable] -and $check.Recommendation.text) {
                            $check.Recommendation.text
                        }
                        else {
                            $check.Recommendation
                        }
                    }
                    URL            = $check.URL
                    Items          = @()
                    Total          = 0
                }

                foreach ($item in $data) {
                    try {
                        $value = $item
                        foreach ($part in $check.Condition.Split('.')) {
                            if ($part -match '\[\]$') {
                                $field = $part -replace '\[\]$', ''
                                $value = $value.$field
                                if ($value -isnot [System.Array]) { $value = @($value) }
                            }
                            else {
                                $value = $value.$part
                            }
                            if ($null -eq $value) { break }
                        }

                        $failed = $false
                        switch ($check.Operator) {
                            "equals" { $failed = $value -ne $check.Expected }
                            "not_equals" { $failed = $value -eq $check.Expected }
                            "contains" { $failed = -not ($value -like "*$($check.Expected)*") }
                            "not_contains" { $failed = ($value -like "*$($check.Expected)*") }
                            "greater_than" { $failed = ($value | Measure-Object -Sum).Sum -le $check.Expected }
                            "less_than" { $failed = ($value | Measure-Object -Sum).Sum -ge $check.Expected }
                            default { Write-Host "❌ Unsupported operator: $($check.Operator)" -ForegroundColor Red; continue }
                        }

                        if ($failed) {
                            $flattened = if ($value -is [System.Array]) { $value -join ', ' } else { $value }
                            $checkResult.Items += [PSCustomObject]@{
                                Namespace = if ($item.metadata.PSObject.Properties.Name -contains 'namespace') { $item.metadata.namespace } else { "(cluster)" }
                                Resource  = "$($check.ResourceKind.ToLower())/$($item.metadata.name)"
                                Value     = $flattened
                                Message   = $check.FailMessage
                            }
                        }
                    }
                    catch {
                        Write-Host "❌ Error evaluating condition for $($item.metadata.name): $_" -ForegroundColor Red
                    }
                }

                $checkResult.Total = $checkResult.Items.Count
                if ($checkResult.Total -eq 0) {
                    $checkResult.Message = "No issues detected for $($check.Name)."
                }
                $localResults += $checkResult
                Write-Host "`r✅ Completed check: $($check.ID) - $($check.Name) " -ForegroundColor Green
            }
        }
        catch {
            Write-Host "❌ Error processing $($_.Name): $_" -ForegroundColor Red
            $localResults += @{
                ID    = "Unknown"
                Name  = $_.Name
                Error = "Error processing file: $_"
            }
        }

        # Return results from this parallel iteration
        $localResults
    } -ThrottleLimit 5

    # Aggregate results into ConcurrentBag
    foreach ($result in $parallelResults) {
        if ($result) {
            if ($result -is [array]) {
                $result | ForEach-Object { $allResults.Add($_) }
            }
            else {
                $allResults.Add($result)
            }
        }
    }

    # Convert ConcurrentBag to array and sort by Check ID
    $allResults = $allResults.ToArray() | Sort-Object -Property ID

    # HTML output
    if ($Html) {
        $sectionGroups = @{}
        $collapsibleSectionMap = @{}
        $alwaysCollapsibleCheckIDs = @("NODE001", "NODE002")

        foreach ($result in $allResults) {
            $section = if ($result.Section) { $result.Section } elseif ($result.Category) { $result.Category } else { "Other" }
            if (-not $sectionGroups.ContainsKey($section)) {
                $sectionGroups[$section] = @()
            }
            $sectionGroups[$section] += $result
        }

        foreach ($section in $sectionGroups.Keys) {
            $sectionHtml = ""

            foreach ($check in $sectionGroups[$section]) {
                $tooltip = if ($check.Description) {
                    "<span class='tooltip'><span class='info-icon'>i</span><span class='tooltip-text'>$($check.Description)</span></span>"
                }
                else { "" }

                $header = "<h2 id='$($check.ID)'>$($check.ID) - $($check.Name) $tooltip</h2>"

                $resourceKind = $check.ResourceKind
                $displayNames = Get-ResourceKindDisplayNames -ResourceKind $resourceKind
                $resourceKindPlural = $displayNames.Plural

                $summary = if ($check.Total -gt 0) {
                    "<p>⚠️ Total $resourceKindPlural with Issues: $($check.Total)</p>"
                }
                else {
                    "<p>✅ All $resourceKindPlural are healthy.</p>"
                }

                $recommendationHtml = if ($check.Recommendation) { $check.Recommendation } else { "" }
                $tableContent = if ($check.Items) {
                    $validProps = Get-ValidProperties -Items $check.Items -CheckID $check.ID
                    if ($validProps) {
                        $check.Items | ConvertTo-Html -Fragment -Property $validProps | Out-String
                    }
                    else {
                        "<p>No valid data to display.</p>"
                    }
                }
                else {
                    if ($check.ID -in $alwaysCollapsibleCheckIDs) {
                        $validProps = Get-ValidProperties -Items @() -CheckID $check.ID
                        if ($validProps) {
                            $emptyTable = [PSCustomObject]@{} | Select-Object $validProps | ConvertTo-Html -Fragment | Out-String
                            $emptyTable -replace '<tr><td></td></tr>', ''
                        }
                        else {
                            "<p>No data available for this check.</p>"
                        }
                    }
                    else {
                        ""
                    }
                }

                $collapsibleContent = "$recommendationHtml`n$tableContent"
                $sectionHtml += @"
$header
$summary
"@

                if ($check.Items -or ($check.ID -in $alwaysCollapsibleCheckIDs -and $tableContent)) {
                    $sectionHtml += @"
<div class='table-container'>
  $(ConvertToCollapsible -Id $check.ID -defaultText "Show Findings" -content $collapsibleContent)
</div>
"@

                }
            }

            if ($collapsibleSectionMap.ContainsKey($section)) {
                $collapsibleSectionMap[$section] += "`n<div class='table-container'>$sectionHtml</div>"
            }
            else {
                $collapsibleSectionMap[$section] = "<div class='table-container'>$sectionHtml</div>"
            }
        }

        $checkStatusList = @()
        $checkScoreList = @()
        foreach ($section in $sectionGroups.Keys) {
            foreach ($check in $sectionGroups[$section]) {
                $status = if ($check.Total -eq 0) { 'Passed' } else { 'Failed' }
                $checkStatusList += [pscustomobject]@{
                    Id     = $check.ID
                    Status = $status
                    Weight = $check.Weight
                }
                $checkScoreList += [pscustomobject]@{
                    Id     = $check.ID
                    Weight = $check.Weight
                    Total  = if ($status -eq 'Passed') { 0 } else { 1 }
                }                
            }
        }

        return @{
            HtmlBySection = $collapsibleSectionMap
            StatusList    = $checkStatusList
            ScoreList     = $checkScoreList 
        }
    }

    # JSON output
    if ($Json) {
        return @{ Total = ($allResults | Measure-Object -Sum -Property Total).Sum; Items = $allResults }
    }

    if ($Text) {
        foreach ($result in $allResults) {
            Write-ToReport ""
            Write-ToReport "$($result.ID) - $($result.Name)"
            Write-ToReport "Total Issues: $($result.Total)"
    
            if ($result.Items) {
                $validProps = Get-ValidProperties -Items $result.Items -CheckID $result.ID
                if ($validProps) {
                    $table = $result.Items | Format-Table -Property $validProps -AutoSize | Out-String
                    Write-ToReport $table.Trim()
                }
                else {
                    Write-ToReport "No valid data to display."
                }
            }
            else {
                Write-ToReport "✅ $($result.Message)"
            }
    
            Write-ToReport "Category: $($result.Category)"
            Write-ToReport "Severity: $($result.Severity)"
            Write-ToReport "Recommendation: $($result.Recommendation)"
            if ($result.URL) {
                Write-ToReport "URL: $($result.URL)"
            }
        }
    
        return @{ Items = $allResults }
    }

    if (-not $Text -and -not $Html -and -not $Json) {
        return @{ Items = $allResults }
    }
}