Src/Private/Compliance-Helpers.ps1

#region --- Compliance Framework Loader ---
#
# Compliance-Helpers.ps1
# ----------------------
# Loads ACSC E8 and CIS M365 compliance check definitions from JSON files in
# Src/Compliance/ and exposes helper functions used by each Get-AbrSP* section.
#
# Call once from Invoke-AsBuiltReport.Microsoft.SharePoint.ps1:
# Initialize-AbrSPComplianceFrameworks
#
# Then in each section:
# $checks = Get-AbrSPE8Checks -Section 'SharingPolicy'
# $checks = Get-AbrSPCISChecks -Section 'SharingPolicy'
#
#endregion

function Initialize-AbrSPComplianceFrameworks {
    <#
    .SYNOPSIS
    Loads ACSC E8 and CIS M365 compliance definitions from JSON files.
    Populates $script:SPE8Definitions and $script:SPCISDefinitions.
    Call once before the tenant report loop.
    #>

    [CmdletBinding()]
    param()

    $ModuleRoot = $null

    if ($script:SPModuleRoot -and (Test-Path $script:SPModuleRoot)) {
        $ModuleRoot = $script:SPModuleRoot
    }

    if (-not $ModuleRoot) {
        $loadedModule = Get-Module -Name 'AsBuiltReport.Microsoft.SharePoint' -ErrorAction SilentlyContinue
        if ($loadedModule -and $loadedModule.ModuleBase) {
            $ModuleRoot = $loadedModule.ModuleBase
        }
    }

    if (-not $ModuleRoot) {
        Write-Warning ' [COMPLIANCE] Cannot determine module root path -- compliance JSON will not load.'
        return
    }

    $ComplianceDir = Join-Path $ModuleRoot 'Src\Compliance'
    Write-Host " - Loading compliance definitions from: $ComplianceDir" -ForegroundColor Cyan

    # --- ACSC Essential Eight ---
    $E8Path = Join-Path $ComplianceDir 'ACSC.E8.json'
    if (Test-Path $E8Path) {
        try {
            $script:SPE8Definitions = Get-Content -Path $E8Path -Raw -ErrorAction Stop | ConvertFrom-Json
            $E8SectionCount = @($script:SPE8Definitions.PSObject.Properties | Where-Object { $_.Name -notlike '_*' }).Count
            Write-Host " - ACSC E8 definitions loaded ($E8SectionCount sections)" -ForegroundColor Cyan
        } catch {
            Write-Warning " [COMPLIANCE] Failed to load ACSC E8 definitions from '$E8Path': $($_.Exception.Message)"
            $script:SPE8Definitions = $null
        }
    } else {
        Write-Warning " [COMPLIANCE] ACSC E8 definitions file not found at: $E8Path"
        $script:SPE8Definitions = $null
    }

    # --- CIS Microsoft 365 ---
    $CISPath = Join-Path $ComplianceDir 'CIS.M365.json'
    if (Test-Path $CISPath) {
        try {
            $script:SPCISDefinitions = Get-Content -Path $CISPath -Raw -ErrorAction Stop | ConvertFrom-Json
            $CISSectionCount = @($script:SPCISDefinitions.PSObject.Properties | Where-Object { $_.Name -notlike '_*' }).Count
            Write-Host " - CIS M365 definitions loaded ($CISSectionCount sections)" -ForegroundColor Cyan
        } catch {
            Write-Warning " [COMPLIANCE] Failed to load CIS M365 definitions from '$CISPath': $($_.Exception.Message)"
            $script:SPCISDefinitions = $null
        }
    } else {
        Write-Warning " [COMPLIANCE] CIS M365 definitions file not found at: $CISPath"
        $script:SPCISDefinitions = $null
    }
}

function Get-AbrSPE8Checks {
    <#
    .SYNOPSIS
    Returns the array of ACSC E8 check definitions for a named section.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Section
    )
    if (-not $script:SPE8Definitions) {
        Write-Warning " [COMPLIANCE] E8 definitions not loaded -- JSON file may be missing."
        return @()
    }
    $sectionDef = $script:SPE8Definitions.$Section
    if (-not $sectionDef) {
        Write-Warning " [COMPLIANCE] E8 section '$Section' not found in ACSC.E8.json"
        return @()
    }
    return $sectionDef.checks
}

function Get-AbrSPCISChecks {
    <#
    .SYNOPSIS
    Returns the array of CIS M365 check definitions for a named section.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Section
    )
    if (-not $script:SPCISDefinitions) {
        Write-Warning " [COMPLIANCE] CIS definitions not loaded -- JSON file may be missing."
        return @()
    }
    $sectionDef = $script:SPCISDefinitions.$Section
    if (-not $sectionDef) {
        Write-Warning " [COMPLIANCE] CIS section '$Section' not found in CIS.M365.json"
        return @()
    }
    return $sectionDef.checks
}

function Build-AbrSPComplianceChecks {
    <#
    .SYNOPSIS
    Resolves a set of compliance check definitions into [pscustomobject] rows
    ready for New-AbrSPE8AssessmentTable or New-AbrSPCISAssessmentTable.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][object[]]$Definitions,
        [Parameter(Mandatory)][ValidateSet('E8','CIS')][string]$Framework,
        [Parameter(Mandatory)][hashtable]$CallerVariables
    )

    if (-not $Definitions -or $Definitions.Count -eq 0) {
        Write-Warning " [COMPLIANCE] Build-AbrSPComplianceChecks: no definitions supplied for Framework=$Framework"
        return @()
    }

    $result = [System.Collections.ArrayList]::new()

    foreach ($def in $Definitions) {
        # --- Resolve Status ---
        $Status = $null
        if ($def.staticStatus) {
            $Status = $def.staticStatus
        } elseif ($def.statusExpression) {
            try {
                $sb = [scriptblock]::Create($def.statusExpression)
                $Status = & {
                    foreach ($kv in $CallerVariables.GetEnumerator()) {
                        Set-Variable -Name $kv.Key -Value $kv.Value -Scope Local
                    }
                    & $sb
                }
            } catch {
                $Status = '[INFO]'
                Write-AbrDebugLog "Status expression eval failed for check '$($def.id)': $($_.Exception.Message)" 'WARN' 'COMPLIANCE'
            }
        } else {
            $Status = '[INFO]'
        }

        # --- Resolve Detail ---
        $Detail = ''
        $rawDetail = if ($def.detail) { $def.detail } elseif ($def.detailTemplate) { $def.detailTemplate } else { '' }
        if ($rawDetail) {
            $Detail = $rawDetail
            foreach ($kv in $CallerVariables.GetEnumerator()) {
                $Detail = $Detail -replace "\{$([regex]::Escape($kv.Key))\}", [string]$kv.Value
            }
        }

        # --- Build output object ---
        $row = if ($Framework -eq 'E8') {
            [pscustomobject]@{
                'ML'      = $def.ML
                'Control' = $def.control
                'Status'  = $Status
                'Detail'  = $Detail
            }
        } else {
            [pscustomobject]@{
                'CISControl' = $def.CISControl
                'Level'      = $def.Level
                'Status'     = $Status
                'Detail'     = $Detail
            }
        }

        $null = $result.Add($row)
    }

    return $result.ToArray()
}

function New-AbrSPE8AssessmentTable {
    <#
    .SYNOPSIS
    Renders a standardised ACSC Essential Eight Maturity Level assessment table.
    Only renders if Options.ComplianceFrameworks.ACSCe8 = true in the report JSON.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][AllowNull()][AllowEmptyCollection()][object[]]$Checks,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$TenantId
    )

    if (-not $script:IncludeACSCe8) { return }
    if (-not $Checks -or $Checks.Count -eq 0) { return }

    $null = (& {
        if ($HealthCheck.SharePoint.SharingPolicy -or $HealthCheck.SharePoint.Compliance -or $HealthCheck.SharePoint.ExternalAccess) {
            $null = ($Checks | Where-Object { $_.'Status' -eq '[FAIL]' } | Set-Style -Style Critical | Out-Null)
            $null = ($Checks | Where-Object { $_.'Status' -eq '[WARN]' } | Set-Style -Style Warning  | Out-Null)
            $null = ($Checks | Where-Object { $_.'Status' -eq '[OK]'   } | Set-Style -Style OK       | Out-Null)
        }
    })

    $TableParams = @{ Name = "ACSC E8 $Name - $TenantId"; List = $false; ColumnWidths = 8, 32, 9, 51 }
    if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" }
    $Checks | Table @TableParams
}

function New-AbrSPCISAssessmentTable {
    <#
    .SYNOPSIS
    Renders a standardised CIS Microsoft 365 Foundations Benchmark assessment table.
    Only renders if Options.ComplianceFrameworks.CISBaseline = true in the report JSON.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][AllowNull()][AllowEmptyCollection()][object[]]$Checks,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$TenantId
    )

    if (-not $script:IncludeCISBaseline) { return }
    if (-not $Checks -or $Checks.Count -eq 0) { return }

    $null = (& {
        if ($HealthCheck.SharePoint.SharingPolicy -or $HealthCheck.SharePoint.Compliance -or $HealthCheck.SharePoint.ExternalAccess) {
            $null = ($Checks | Where-Object { $_.'Status' -eq '[FAIL]' } | Set-Style -Style Critical | Out-Null)
            $null = ($Checks | Where-Object { $_.'Status' -eq '[WARN]' } | Set-Style -Style Warning  | Out-Null)
            $null = ($Checks | Where-Object { $_.'Status' -eq '[OK]'   } | Set-Style -Style OK       | Out-Null)
        }
    })

    $TableParams = @{ Name = "CIS Baseline $Name - $TenantId"; List = $false; ColumnWidths = 10, 7, 9, 74 }
    if ($Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" }
    $Checks | Table @TableParams
}