Src/Private/Get-AbrSPCompliance.ps1

function Get-AbrSPCompliance {
    <#
    .SYNOPSIS
    Documents SharePoint and OneDrive compliance settings.
    .NOTES
        Version: 0.1.5
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting SharePoint Compliance data for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Compliance'
    }

    process {
        #region Pre-compute all values BEFORE any Section{} block
        # CRITICAL: PScribo Section{} creates a child scope. Variables assigned inside
        # a Section{} block are NOT visible outside it. All data must be gathered here,
        # in the process{} scope, before any Section{} is opened.

        # Audit log status
        $AuditEnabled = $true    # M365 enables this by default for all tenants (Oct 2023)
        $AuditNote    = 'Microsoft 365 enables unified audit logging by default for all tenants (since October 2023). Verify status and retention period in Microsoft Purview > Audit.'
        $AuditSource  = 'M365 platform default'

        if (Get-Command Get-AdminAuditLogConfig -ErrorAction SilentlyContinue) {
            try {
                $AuditConfig  = Get-AdminAuditLogConfig -ErrorAction Stop
                $AuditEnabled = [bool]$AuditConfig.UnifiedAuditLogIngestionEnabled
                $AuditNote    = if ($AuditEnabled) {
                    'Unified audit log ingestion is ENABLED (verified via Exchange Online).'
                } else {
                    'Unified audit log ingestion is DISABLED. Enable in Microsoft Purview > Audit.'
                }
                $AuditSource = 'Exchange Online (Get-AdminAuditLogConfig)'
            } catch {
                Write-AbrDebugLog "Exchange Online audit check failed: $($_.Exception.Message)" 'WARN' 'COMPLIANCE'
            }
        }

        # Recycle Bin status - get from PnP, default to $true (M365 default)
        $RecycleBinEnabled = $true
        if ($script:PnPAvailable) {
            try {
                $SPTenantForRB = Get-PnPTenant -ErrorAction Stop
                # Guard against $null -- PnP may return $null for this field
                if ($null -ne $SPTenantForRB.RecycleBinEnabled) {
                    $RecycleBinEnabled = [bool]$SPTenantForRB.RecycleBinEnabled
                }
            } catch {
                Write-AbrDebugLog "Could not retrieve Recycle Bin status: $($_.Exception.Message)" 'WARN' 'COMPLIANCE'
            }
        }

        # Sensitivity labels from pre-fetched cache
        $Labels = $script:CachedLabels
        #endregion

        Section -Style Heading2 'Compliance & Data Governance' {
            Paragraph "The following section documents the compliance and data governance configuration for SharePoint Online and OneDrive for Business in tenant $TenantId."
            BlankLine

            #region Audit Logging
            try {
                Section -Style Heading3 'Audit Logging' {
                    $AuditObj  = [System.Collections.ArrayList]::new()
                    $auditInObj = [ordered] @{
                        'Unified Audit Log Status'  = if ($AuditEnabled) { 'Enabled' } else { 'Disabled' }
                        'Verification Source'       = $AuditSource
                        'Note'                      = $AuditNote
                        'Verify in Purview'         = 'https://compliance.microsoft.com/auditlogsearch'
                    }
                    $AuditObj.Add([pscustomobject]$auditInObj) | Out-Null

                    $null = (& {
                        if ($HealthCheck.SharePoint.Compliance) {
                            $null = ($AuditObj | Where-Object { $_.'Unified Audit Log Status' -eq 'Disabled' } | Set-Style -Style Critical | Out-Null)
                            $null = ($AuditObj | Where-Object { $_.'Unified Audit Log Status' -eq 'Enabled'  } | Set-Style -Style OK       | Out-Null)
                        }
                    })

                    $AuditTableParams = @{ Name = "Audit Logging - $TenantId"; List = $true; ColumnWidths = 30, 70 }
                    if ($Report.ShowTableCaptions) { $AuditTableParams['Caption'] = "- $($AuditTableParams.Name)" }
                    $AuditObj | Table @AuditTableParams
                    $script:ExcelSheets['Audit Settings'] = $AuditObj
                }
            } catch {
                Write-AbrSectionError -Section 'Audit Logging' -Message "$($_.Exception.Message)"
            }
            #endregion

            #region Recycle Bin
            try {
                if ($script:PnPAvailable) {
                    Section -Style Heading3 'Recycle Bin' {
                        $RBObj    = [System.Collections.ArrayList]::new()
                        $rbInObj  = [ordered] @{
                            'Recycle Bin Enabled'           = $RecycleBinEnabled
                            'First Stage Retention'         = '30 days (Microsoft default)'
                            'Second Stage Retention'        = '63 days additional (93 days total)'
                            'Admin Deleted Items Retention' = '14 days (site collection recycle bin)'
                        }
                        $RBObj.Add([pscustomobject](ConvertTo-HashToYN $rbInObj)) | Out-Null

                        $null = (& {
                            if ($HealthCheck.SharePoint.Compliance) {
                                $null = ($RBObj | Where-Object { $_.'Recycle Bin Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null)
                            }
                        })

                        $RBTableParams = @{ Name = "Recycle Bin Configuration - $TenantId"; List = $true; ColumnWidths = 45, 55 }
                        if ($Report.ShowTableCaptions) { $RBTableParams['Caption'] = "- $($RBTableParams.Name)" }
                        $RBObj | Table @RBTableParams
                    }
                }
            } catch {
                Write-AbrSectionError -Section 'Recycle Bin' -Message "$($_.Exception.Message)"
            }
            #endregion

            #region DLP Policies (guidance only)
            try {
                Section -Style Heading3 'Data Loss Prevention (DLP)' {
                    Paragraph "DLP policy enumeration requires Exchange Online PowerShell (Get-DlpCompliancePolicy) or the Microsoft Purview Compliance portal. These are outside the SharePoint/Graph scopes used by this report."
                    BlankLine

                    $DLPNoteObj = [System.Collections.ArrayList]::new()
                    $dlpInObj   = [ordered] @{
                        'Review Location'     = 'Microsoft Purview > Data Loss Prevention > Policies'
                        'Purview DLP URL'     = 'https://compliance.microsoft.com/datalossprevention/policies'
                        'Recommended Actions' = 'Verify at least one DLP policy targets SharePoint and OneDrive with sensitive information types (PII, financial, health records)'
                        'PowerShell Check'    = "Connect-IPPSSession; Get-DlpCompliancePolicy | Where-Object { `$_.Workload -like '*SharePoint*' }"
                    }
                    $DLPNoteObj.Add([pscustomobject]$dlpInObj) | Out-Null
                    $DLPTableParams = @{ Name = "DLP Policy Guidance - $TenantId"; List = $true; ColumnWidths = 25, 75 }
                    if ($Report.ShowTableCaptions) { $DLPTableParams['Caption'] = "- $($DLPTableParams.Name)" }
                    $DLPNoteObj | Table @DLPTableParams
                }
            } catch {
                Write-AbrSectionError -Section 'DLP Policies' -Message "$($_.Exception.Message)"
            }
            #endregion

            #region Sensitivity Labels
            try {
                Section -Style Heading3 'Sensitivity Labels' {
                    if ($Labels -and @($Labels).Count -gt 0) {
                        $LabelObj = [System.Collections.ArrayList]::new()
                        foreach ($Label in ($Labels | Sort-Object name)) {
                            $labelInObj = [ordered] @{
                                'Label Name'         = $Label.name
                                'Sensitivity'        = if ($Label.sensitivity) { $Label.sensitivity } else { '--' }
                                'Content Marking'    = if ($Label.contentMarkings -and @($Label.contentMarkings).Count -gt 0) { 'Yes' } else { 'No' }
                                'Encryption Enabled' = if ($Label.encryptionConfiguration) { 'Yes' } else { 'No' }
                                'Color'              = if ($Label.color) { $Label.color } else { '--' }
                            }
                            $LabelObj.Add([pscustomobject]$labelInObj) | Out-Null
                        }
                        $LabelTableParams = @{ Name = "Sensitivity Labels - $TenantId"; ColumnWidths = 35, 15, 15, 15, 20 }
                        if ($Report.ShowTableCaptions) { $LabelTableParams['Caption'] = "- $($LabelTableParams.Name)" }
                        $LabelObj | Table @LabelTableParams
                        $script:ExcelSheets['Sensitivity Labels'] = $LabelObj
                    } else {
                        Paragraph "Sensitivity labels could not be retrieved (scope InformationProtectionPolicy.Read.All not requested), or no labels are configured."
                        BlankLine
                        $LabelGuideObj = [System.Collections.ArrayList]::new()
                        $lgInObj = [ordered] @{
                            'Configure Labels'    = 'Microsoft Purview > Information Protection > Labels'
                            'Purview URL'         = 'https://compliance.microsoft.com/informationprotection'
                            'Enable for SPO/OD'   = "Run: Set-SPOTenant -EnableAIPIntegration `$true (SharePoint Admin PS)"
                            'Recommended Actions' = 'Publish labels to users, configure auto-labeling for SharePoint libraries'
                        }
                        $LabelGuideObj.Add([pscustomobject]$lgInObj) | Out-Null
                        $LGTableParams = @{ Name = "Sensitivity Label Guidance - $TenantId"; List = $true; ColumnWidths = 25, 75 }
                        if ($Report.ShowTableCaptions) { $LGTableParams['Caption'] = "- $($LGTableParams.Name)" }
                        $LabelGuideObj | Table @LGTableParams
                    }
                }
            } catch {
                Write-AbrSectionError -Section 'Sensitivity Labels' -Message "$($_.Exception.Message)"
            }
            #endregion

            #region ACSC E8 Assessment
            if ($script:IncludeACSCe8) {
                BlankLine
                Paragraph "ACSC Essential Eight Assessment -- Compliance & Data Governance"
                $E8Defs   = Get-AbrSPE8Checks -Section 'Compliance'
                $E8Vars   = @{
                    AuditEnabled         = $AuditEnabled
                    AuditEnabledStr      = if ($AuditEnabled) { 'Yes' } else { 'No' }
                    RecycleBinEnabled    = $RecycleBinEnabled
                    RecycleBinEnabledStr = if ($RecycleBinEnabled) { 'Yes' } else { 'No' }
                }
                $E8Checks = Build-AbrSPComplianceChecks -Definitions $E8Defs -Framework 'E8' -CallerVariables $E8Vars
                New-AbrSPE8AssessmentTable -Checks $E8Checks -Name 'Compliance' -TenantId $TenantId
                foreach ($row in $E8Checks) {
                    $null = $script:E8AllChecks.Add([pscustomobject](@{ Section = 'Compliance' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion

            #region CIS Baseline Assessment
            if ($script:IncludeCISBaseline) {
                BlankLine
                Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Compliance & Data Governance"
                $CISDefs   = Get-AbrSPCISChecks -Section 'Compliance'
                $CISVars   = @{
                    AuditEnabled         = $AuditEnabled
                    AuditEnabledStr      = if ($AuditEnabled) { 'Yes' } else { 'No' }
                    RecycleBinEnabled    = $RecycleBinEnabled
                    RecycleBinEnabledStr = if ($RecycleBinEnabled) { 'Yes' } else { 'No' }
                }
                $CISChecks = Build-AbrSPComplianceChecks -Definitions $CISDefs -Framework 'CIS' -CallerVariables $CISVars
                New-AbrSPCISAssessmentTable -Checks $CISChecks -Name 'Compliance' -TenantId $TenantId
                foreach ($row in $CISChecks) {
                    $null = $script:CISAllChecks.Add([pscustomobject](@{ Section = 'Compliance' } + ($row | ConvertTo-HashTableSP)))
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Compliance'
    }
}