Src/Public/Invoke-AsBuiltReport.Microsoft.ExchangeOnline.ps1

function Invoke-AsBuiltReport.Microsoft.ExchangeOnline {
    <#
    .SYNOPSIS
        PowerShell script to document the configuration of Microsoft Exchange Online
        in Word/HTML/Text formats.
    .DESCRIPTION
        Documents the configuration of Microsoft Exchange Online in Word/HTML/Text formats
        using PScribo.

        Covers:
          - Tenant Overview (organisation config, accepted domains, remote domains)
          - Mailboxes (user, shared, resource, external forwarding gaps)
          - Exchange Online Protection (anti-spam, anti-malware, anti-phishing)
          - Email Authentication (DKIM, DMARC)
          - Mail Flow (transport rules, connectors)
          - Microsoft Defender for Office 365 (Safe Attachments, Safe Links)
          - Audit Logging

        Compliance Frameworks:
          - ACSC Essential Eight Maturity Model (ML1-ML3)
          - CIS Microsoft 365 Foundations Benchmark (L1/L2)

        Excel Export:
          After report generation, an Excel workbook is automatically exported
          containing sheets for all major data sets plus consolidated compliance
          assessment tabs (ACSC E8 Assessment, CIS Assessment).

    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
        Github: weising26

    .LINK
        https://github.com/AsBuiltReport/AsBuiltReport.Microsoft.ExchangeOnline

    .PARAMETER Target
        Specifies the Microsoft 365 tenant domain name (e.g. contoso.onmicrosoft.com).

    .PARAMETER Credential
        Optional PSCredential. The username is used as the UPN if
        Options.UserPrincipalName is not set in the report config JSON.
    #>


    param (
        [String[]] $Target,
        [PSCredential] $Credential
    )

    Write-ReportModuleInfo -ModuleName 'Microsoft.ExchangeOnline'

    #---------------------------------------------------------------------------------------------#
    # Dependency Module Version Check #
    #---------------------------------------------------------------------------------------------#
    $ModuleArray = @('AsBuiltReport.Core', 'ExchangeOnlineManagement', 'ImportExcel')

    foreach ($Module in $ModuleArray) {
        try {
            $InstalledVersion = Get-Module -ListAvailable -Name $Module -ErrorAction SilentlyContinue |
                Sort-Object -Property Version -Descending |
                Select-Object -First 1 -ExpandProperty Version

            if ($InstalledVersion) {
                Write-Host " - $Module module v$($InstalledVersion.ToString()) is currently installed."

                $PSGetAvailable = Get-Module -ListAvailable -Name PowerShellGet -ErrorAction SilentlyContinue
                if ($PSGetAvailable) {
                    try {
                        Import-Module PowerShellGet -ErrorAction SilentlyContinue
                        $LatestVersion = Find-Module -Name $Module -Repository PSGallery -ErrorAction SilentlyContinue |
                            Select-Object -ExpandProperty Version
                        if ($LatestVersion -and $InstalledVersion -lt $LatestVersion) {
                            Write-Host " - $Module module v$($LatestVersion.ToString()) is available." -ForegroundColor Red
                            Write-Host " - Run 'Update-Module -Name $Module -Force' to install the latest version." -ForegroundColor Red
                        }
                    } catch {
                        Write-PScriboMessage -IsWarning "Unable to check latest version for ${Module}: $($_.Exception.Message)"
                    }
                }
            } else {
                $Required = if ($Module -eq 'ImportExcel') { "(Optional -- required for Excel export)" } else { "" }
                Write-Host " - $Module module is NOT installed. $Required Run 'Install-Module -Name $Module -Force'." -ForegroundColor $(if ($Module -eq 'ImportExcel') { 'Yellow' } else { 'Red' })
            }
        } catch {
            Write-PScriboMessage -IsWarning $_.Exception.Message
        }
    }

    #---------------------------------------------------------------------------------------------#
    # Import Report Configuration #
    #---------------------------------------------------------------------------------------------#
    $ReportConfig.Report.Name = 'Microsoft Exchange Online As Built Report'

    $script:Report    = $ReportConfig.Report
    $script:InfoLevel = $ReportConfig.InfoLevel
    $script:Options   = $ReportConfig.Options
    $script:TextInfo  = (Get-Culture).TextInfo
    $script:SectionTimers = [System.Collections.Generic.Dictionary[string, object]]::new()

    # Compliance framework toggles
    $script:IncludeACSCe8      = ($script:Options.ComplianceFrameworks.ACSCe8 -eq $true)
    $script:IncludeCISBaseline = ($script:Options.ComplianceFrameworks.CISBaseline -eq $true)

    if ($script:IncludeACSCe8)      { Write-Host " - Compliance framework: ACSC Essential Eight enabled"   -ForegroundColor Cyan }
    if ($script:IncludeCISBaseline) { Write-Host " - Compliance framework: CIS Baseline enabled"           -ForegroundColor Cyan }
    if (-not $script:IncludeACSCe8 -and -not $script:IncludeCISBaseline) {
        Write-Host " - Compliance frameworks: none enabled (set Options.ComplianceFrameworks in JSON to enable)" -ForegroundColor DarkGray
    }

    # Load compliance check definitions from JSON
    Initialize-AbrExoComplianceFrameworks

    # Excel export sheet collector
    $script:ExcelSheets  = [System.Collections.Specialized.OrderedDictionary]::new()
    $script:E8AllChecks  = [System.Collections.ArrayList]::new()
    $script:CISAllChecks = [System.Collections.ArrayList]::new()

    #---------------------------------------------------------------------------------------------#
    # Transcript Log Setup #
    #---------------------------------------------------------------------------------------------#
    $script:TranscriptOutputFolder = $null
    if ($ReportConfig.Report.OutputFolderPath -and (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) {
        $script:TranscriptOutputFolder = $ReportConfig.Report.OutputFolderPath
    } elseif ($OutputFolderPath -and (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) {
        $script:TranscriptOutputFolder = $OutputFolderPath
    } else {
        $script:TranscriptOutputFolder = Join-Path $env:USERPROFILE 'Documents'
    }

    $TranscriptTimestamp      = Get-Date -Format 'yyyyMMdd_HHmmss'
    $script:TranscriptLogPath = Join-Path $script:TranscriptOutputFolder "ExO_Transcript_${TranscriptTimestamp}.log"
    $script:ErrorLogPath      = Join-Path $script:TranscriptOutputFolder "ExO_Errors_${TranscriptTimestamp}.log"

    try {
        Start-Transcript -Path $script:TranscriptLogPath -Append -ErrorAction Stop | Out-Null
        Write-Host " - Transcript log: $script:TranscriptLogPath" -ForegroundColor Cyan
    } catch {
        Write-Warning " - Could not start transcript: $($_.Exception.Message)"
        $script:TranscriptLogPath = $null
    }

    $script:ErrorLog = [System.Collections.Generic.List[string]]::new()

    #---------------------------------------------------------------------------------------------#
    # Debug Logger Setup #
    #---------------------------------------------------------------------------------------------#
    $script:DebugLogEnabled = ($script:Options.DebugLog -eq $true)
    $script:DebugLogEntries = [System.Collections.Generic.List[string]]::new()

    #---------------------------------------------------------------------------------------------#
    # Disclaimer (HealthCheck) #
    #---------------------------------------------------------------------------------------------#
    if ($Healthcheck) {
        Section -Style TOC -ExcludeFromTOC 'DISCLAIMER' {
            Paragraph 'This report was generated using the AsBuiltReport framework. Health check indicators are provided as guidance only and should be validated by a qualified administrator before remediation. Color coding: RED = Critical issue requiring immediate attention, YELLOW/AMBER = Warning requiring review.'
        }
        PageBreak
    }

    #---------------------------------------------------------------------------------------------#
    # Connection Section #
    #---------------------------------------------------------------------------------------------#
    foreach ($System in $Target) {

        Write-Host " "
        Write-Host "- Starting Exchange Online report for tenant: $System"
        Write-TranscriptLog "Starting Exchange Online report for tenant: $System" 'INFO' 'MAIN'
        Write-AbrDebugLog "Starting report for tenant: $System" 'INFO' 'MAIN'

        #region Resolve UPN
        $ResolvedUPN = $null
        if ($Options.UserPrincipalName -and (Test-UserPrincipalName -UserPrincipalName $Options.UserPrincipalName)) {
            $ResolvedUPN = $Options.UserPrincipalName
        } elseif ($Credential -and (Test-UserPrincipalName -UserPrincipalName $Credential.UserName)) {
            $ResolvedUPN = $Credential.UserName
        } else {
            throw "No valid UserPrincipalName found. Set 'Options.UserPrincipalName' in your report config JSON (e.g. admin@$System), or pass -Credential with a UPN-format username."
        }
        Write-TranscriptLog "Using UPN: $ResolvedUPN" 'INFO' 'AUTH'
        #endregion

        #region Connect
        try {
            Connect-ExoSession -UserPrincipalName $ResolvedUPN
        } catch {
            Write-AbrDebugLog "Connection failed: $($_.Exception.Message)" 'ERROR' 'AUTH'
            throw "Connection failed for tenant '$System'. Error: $($_.Exception.Message)"
        }
        #endregion

        #region Resolve Tenant Identity
        Write-Host " - Resolving tenant identity..."
        $script:TenantName   = $System
        $script:TenantDomain = $System

        try {
            $OrgConfig = Get-OrganizationConfig -ErrorAction SilentlyContinue
            if ($OrgConfig -and $OrgConfig.DisplayName) {
                $script:TenantName = $OrgConfig.DisplayName
            }
        } catch { }

        # Try Graph for tenant domain
        try {
            $OrgResp = Invoke-MgGraphRequest -Method GET `
                -Uri 'https://graph.microsoft.com/v1.0/organization?$select=id,displayName,verifiedDomains' `
                -ErrorAction SilentlyContinue
            $MgOrg = if ($OrgResp.value) { $OrgResp.value[0] } else { $null }
            if ($MgOrg) {
                $script:TenantDomain = ($MgOrg.verifiedDomains | Where-Object { $_.isDefault }).name
                if (-not $script:TenantDomain) { $script:TenantDomain = $System }
                if ($MgOrg.displayName -and -not $OrgConfig) { $script:TenantName = $MgOrg.displayName }
            }
        } catch { }

        Write-Host " - Tenant: $($script:TenantName) / $($script:TenantDomain)" -ForegroundColor Cyan
        Write-TranscriptLog "Tenant identified: $($script:TenantName) / $($script:TenantDomain)" 'INFO' 'MAIN'
        #endregion

        #---------------------------------------------------------------------------------------------#
        # Report Sections #
        #---------------------------------------------------------------------------------------------#
        if (-not $script:TenantName)   { $script:TenantName = $System }
        if (-not $script:TenantDomain) { $script:TenantDomain = $System }

        # 1. Tenant Overview
        if ($InfoLevel.TenantOverview -ge 1) {
            Write-Host '- Working on Tenant Overview section.'
            Write-AbrDebugLog 'Starting TenantOverview section' 'DEBUG' 'SECTION'
            Get-AbrExoTenantOverviewSection -TenantId $script:TenantName
        }

        # 2. Hybrid Configuration & Identity Integration
        if ($InfoLevel.Hybrid -ge 1) {
            Write-Host '- Working on Hybrid Configuration section.'
            Write-AbrDebugLog 'Starting Hybrid section' 'DEBUG' 'SECTION'
            Get-AbrExoHybridSection -TenantId $script:TenantName
        }

        # 3. Recipients (Mailboxes + Governance + Permissions + Distribution Groups)
        if ($InfoLevel.Mailboxes -ge 1 -or $InfoLevel.MailboxGovernance -ge 1 -or
            $InfoLevel.MailboxPermissions -ge 1 -or $InfoLevel.DistributionGroups -ge 1) {
            Write-Host '- Working on Recipients section.'
            Write-AbrDebugLog 'Starting Recipients section' 'DEBUG' 'SECTION'
            Get-AbrExoMailboxSection -TenantId $script:TenantName
        }

        # 4. Client Access & Authentication
        if ($InfoLevel.ClientAccess -ge 1) {
            Write-Host '- Working on Client Access section.'
            Write-AbrDebugLog 'Starting ClientAccess section' 'DEBUG' 'SECTION'
            Get-AbrExoClientAccessSection -TenantId $script:TenantName
        }

        # 5. Exchange Online Protection (Anti-Spam, Anti-Malware, Anti-Phishing, Quarantine)
        if ($InfoLevel.AntiSpam -ge 1 -or $InfoLevel.AntiMalware -ge 1 -or
            $InfoLevel.AntiPhishing -ge 1 -or $InfoLevel.Quarantine -ge 1) {
            Write-Host '- Working on Exchange Online Protection section.'
            Write-AbrDebugLog 'Starting Protection section' 'DEBUG' 'SECTION'
            Get-AbrExoProtectionSection -TenantId $script:TenantName
        }

        # 6. Email Authentication (DKIM + DMARC)
        if ($InfoLevel.DKIM -ge 1 -or $InfoLevel.DMARC -ge 1) {
            Write-Host '- Working on Email Authentication section.'
            Write-AbrDebugLog 'Starting EmailAuth section' 'DEBUG' 'SECTION'
            Get-AbrExoEmailAuthSection -TenantId $script:TenantName
        }

        # 7. Mail Flow & External Controls (Transport Rules + Connectors + External Sharing)
        if ($InfoLevel.TransportRules -ge 1 -or $InfoLevel.Connectors -ge 1 -or $InfoLevel.ExternalSharing -ge 1) {
            Write-Host '- Working on Mail Flow and External Controls section.'
            Write-AbrDebugLog 'Starting MailFlow section' 'DEBUG' 'SECTION'
            Get-AbrExoMailFlowSection -TenantId $script:TenantName
        }

        # 8. Microsoft Defender for Office 365 (Safe Attachments + Safe Links)
        if ($InfoLevel.SafeAttachments -ge 1 -or $InfoLevel.SafeLinks -ge 1) {
            Write-Host '- Working on Defender for Office 365 section.'
            Write-AbrDebugLog 'Starting Defender section' 'DEBUG' 'SECTION'
            Get-AbrExoDefenderSection -TenantId $script:TenantName
        }

        # 9. Compliance & Retention (Retention Policies + Journaling + Audit Logging)
        if ($InfoLevel.RetentionPolicies -ge 1 -or $InfoLevel.AuditLogging -ge 1) {
            Write-Host '- Working on Compliance and Retention section.'
            Write-AbrDebugLog 'Starting Compliance section' 'DEBUG' 'SECTION'
            Get-AbrExoComplianceSection -TenantId $script:TenantName
        }

        # 10. Mobile Device Access
        if ($InfoLevel.MobileDevices -ge 1) {
            Write-Host '- Working on Mobile Device Access section.'
            Write-AbrDebugLog 'Starting MobileDevices section' 'DEBUG' 'SECTION'
            Get-AbrExoMobileSection -TenantId $script:TenantName
        }

        # 11. Address Lists & Email Address Policies
        if ($InfoLevel.AddressLists -ge 1) {
            Write-Host '- Working on Address Lists section.'
            Write-AbrDebugLog 'Starting AddressLists section' 'DEBUG' 'SECTION'
            Get-AbrExoAddressListSection -TenantId $script:TenantName
        }

        # 12. Monitoring & Alerting
        if ($InfoLevel.Alerting -ge 1) {
            Write-Host '- Working on Monitoring and Alerting section.'
            Write-AbrDebugLog 'Starting Alerting section' 'DEBUG' 'SECTION'
            Get-AbrExoAlertingSection -TenantId $script:TenantName
        }

        #---------------------------------------------------------------------------------------------#
        # Output Folder Resolution #
        #---------------------------------------------------------------------------------------------#
        if ($ReportConfig.Report.OutputFolderPath -and
            (Test-Path $ReportConfig.Report.OutputFolderPath -ErrorAction SilentlyContinue)) {
            $script:ResolvedOutputFolder = $ReportConfig.Report.OutputFolderPath
        } elseif ($OutputFolderPath -and
            (Test-Path $OutputFolderPath -ErrorAction SilentlyContinue)) {
            $script:ResolvedOutputFolder = $OutputFolderPath
        } else {
            $script:ResolvedOutputFolder = Join-Path $env:USERPROFILE 'Documents'
        }

        Write-Host " - Output folder: $($script:ResolvedOutputFolder)" -ForegroundColor Cyan
        Write-AbrDebugLog "Output folder resolved: $($script:ResolvedOutputFolder)" 'INFO' 'MAIN'

        #---------------------------------------------------------------------------------------------#
        # Excel Export #
        #---------------------------------------------------------------------------------------------#
        # Consolidate compliance rows into summary sheets
        if ($script:E8AllChecks -and $script:E8AllChecks.Count -gt 0) {
            $script:ExcelSheets['ACSC E8 Assessment'] = $script:E8AllChecks
        }
        if ($script:CISAllChecks -and $script:CISAllChecks.Count -gt 0) {
            $script:ExcelSheets['CIS Assessment'] = $script:CISAllChecks
        }

        if ($script:ExcelSheets -and $script:ExcelSheets.Count -gt 0) {
            Write-Host " "
            Write-Host "- Exporting data to Excel workbook..."
            Write-AbrDebugLog "Exporting $($script:ExcelSheets.Count) sheets to Excel" 'INFO' 'EXPORT'

            $ExcelOutputPath = Join-Path $script:ResolvedOutputFolder `
                "ExO_AsBuilt_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').xlsx"

            try {
                Export-ExoToExcel -Sheets $script:ExcelSheets -Path $ExcelOutputPath -TenantId $script:TenantName
                Write-AbrDebugLog "Excel export complete: $ExcelOutputPath" 'SUCCESS' 'EXPORT'
            } catch {
                Write-Warning "Excel export failed: $($_.Exception.Message)"
                Write-AbrDebugLog "Excel export failed: $($_.Exception.Message)" 'ERROR' 'EXPORT'
            }
        }

        #---------------------------------------------------------------------------------------------#
        # Clean Up Connections #
        #---------------------------------------------------------------------------------------------#
        Write-Host " "
        Write-Host "- Finished report generation for tenant: $($script:TenantName)"
        Write-TranscriptLog "Report generation complete for: $($script:TenantName)" 'SUCCESS' 'MAIN'

        if ($script:ErrorLog -and $script:ErrorLog.Count -gt 0) {
            Write-Host " - $($script:ErrorLog.Count) error(s) logged to: $script:ErrorLogPath" -ForegroundColor Yellow
        }

        if ($script:Options.KeepSession -eq $true) {
            Write-Host " - KeepSession is enabled -- Exchange Online session retained." -ForegroundColor Yellow
            Write-AbrDebugLog "KeepSession=true -- skipping disconnect" 'INFO' 'AUTH'
        } else {
            Disconnect-ExoSession
        }

        #---------------------------------------------------------------------------------------------#
        # Debug Log Export #
        #---------------------------------------------------------------------------------------------#
        if ($script:DebugLogEnabled -and $script:DebugLogEntries.Count -gt 0) {
            $DebugLogPath = Join-Path $script:ResolvedOutputFolder `
                "ExO_DebugLog_$($script:TenantDomain)_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
            try {
                $script:DebugLogEntries | Set-Content -Path $DebugLogPath -Encoding UTF8
                Write-Host " - Debug log saved to: $DebugLogPath" -ForegroundColor Cyan
            } catch {
                Write-Warning "Could not save debug log: $($_.Exception.Message)"
            }
        }

        # Stop transcript
        if ($script:TranscriptLogPath) {
            try {
                Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
                Write-Host " - Transcript saved to: $script:TranscriptLogPath" -ForegroundColor Cyan
            } catch { }
        }

    } #endregion foreach Target loop
}