Src/Private/Connect-ExoSession.ps1

function Connect-ExoSession {
    <#
    .SYNOPSIS
    Establishes authenticated connections to Exchange Online for reporting.
    .DESCRIPTION
        Connects to:
          1. Exchange Online PowerShell (EXO v3 module) - for mailbox, transport, EOP cmdlets
          2. Security & Compliance PowerShell (IPPSSession) - for Get-ProtectionAlert,
             Get-CompliancePolicy, Get-RetentionCompliancePolicy, Get-LabelPolicy, etc.
          3. Microsoft Graph - for supplementary tenant identity data

        Required Roles (any one of):
          - Global Administrator
          - Global Reader
          - Exchange Administrator + Security Reader
          - Security Administrator
          - Compliance Administrator

    .NOTES
        Version: 0.1.1
        Author: Pai Wei Sing

    .EXAMPLE
        Connect-ExoSession -UserPrincipalName 'admin@contoso.onmicrosoft.com'
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$UserPrincipalName
    )

    if (-not (Test-UserPrincipalName -UserPrincipalName $UserPrincipalName)) {
        $errorMsg = "Invalid User Principal Name format: '$UserPrincipalName'. Expected format: user@domain.com"
        Write-TranscriptLog $errorMsg 'ERROR' 'AUTH'
        throw $errorMsg
    }

    Write-TranscriptLog "Starting connection to Exchange Online: $UserPrincipalName" 'INFO' 'AUTH'

    #region Exchange Online PowerShell
    $ExistingExo = $null
    try { $ExistingExo = Get-ConnectionInformation -ErrorAction SilentlyContinue } catch { }

    if ($ExistingExo) {
        Write-TranscriptLog "Reusing existing Exchange Online session" 'SUCCESS' 'AUTH'
        Write-Host " - Exchange Online: existing session reused." -ForegroundColor Cyan
    } else {
        Write-Host " - Connecting to Exchange Online..."
        Write-TranscriptLog "Connecting to Exchange Online PowerShell (EXO v3)" 'INFO' 'AUTH'

        Invoke-WithRetry -ScriptBlock {
            Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowBanner:$false -ErrorAction Stop
        } -OperationName 'Connect to Exchange Online'

        Write-Host " - Exchange Online connected successfully." -ForegroundColor Green
        Write-TranscriptLog "Exchange Online connection established" 'SUCCESS' 'AUTH'
    }
    #endregion

    #region Security & Compliance PowerShell (IPPSSession)
    # Required for: Get-ProtectionAlert, Get-CompliancePolicy, Get-RetentionCompliancePolicy,
    # Get-LabelPolicy, Get-DlpCompliancePolicy and other Purview/Defender cmdlets.
    # Uses the same UPN/interactive auth as Exchange Online.
    # If this fails we warn but do not abort — EXO-only sections will still work.
    $script:IPPSConnected = $false
    try {
        # Check if IPPSSession is already active by testing a compliance cmdlet
        $null = Get-ProtectionAlert -ResultSize 1 -ErrorAction Stop
        $script:IPPSConnected = $true
        Write-Host " - Security & Compliance (IPPSSession): existing session detected." -ForegroundColor Cyan
        Write-TranscriptLog "Reusing existing IPPSSession" 'SUCCESS' 'AUTH'
    } catch {
        # Not connected yet — attempt Connect-IPPSSession
        try {
            Write-Host " - Connecting to Security and Compliance PowerShell (IPPSSession)..."
            Write-TranscriptLog "Connecting to IPPSSession for compliance cmdlets" 'INFO' 'AUTH'

            Invoke-WithRetry -ScriptBlock {
                Connect-IPPSSession -UserPrincipalName $UserPrincipalName -ShowBanner:$false -ErrorAction Stop
            } -OperationName 'Connect to IPPSSession (Security & Compliance)'

            $script:IPPSConnected = $true
            Write-Host " - Security and Compliance PowerShell connected successfully." -ForegroundColor Green
            Write-TranscriptLog "IPPSSession established successfully" 'SUCCESS' 'AUTH'
        } catch {
            $script:IPPSConnected = $false
            Write-Host " - Security and Compliance PowerShell connection failed (alert policies and compliance data will be skipped): $($_.Exception.Message)" -ForegroundColor Yellow
            Write-TranscriptLog "IPPSSession connection failed: $($_.Exception.Message)" 'WARNING' 'AUTH'
        }
    }
    #endregion

    #region Microsoft Graph (for supplementary data)
    $ExistingGraph = $null
    try { $ExistingGraph = Get-MgContext -ErrorAction SilentlyContinue } catch { }

    if ($ExistingGraph -and $ExistingGraph.TenantId) {
        Write-TranscriptLog "Reusing existing Microsoft Graph session (TenantId: $($ExistingGraph.TenantId))" 'SUCCESS' 'AUTH'
    } else {
        Write-Host " - Connecting to Microsoft Graph (supplementary)..."
        $GraphScopes = @(
            'Organization.Read.All'
            'Domain.Read.All'
            'Policy.Read.All'
            'Reports.Read.All'
        )
        try {
            Invoke-WithRetry -ScriptBlock {
                Connect-MgGraph -Scopes $GraphScopes -ErrorAction Stop
            } -OperationName 'Connect to Microsoft Graph (EXO supplementary)'
            Write-Host " - Microsoft Graph connected successfully." -ForegroundColor Green
        } catch {
            Write-Warning " - Microsoft Graph connection failed (some data may be unavailable): $($_.Exception.Message)"
        }
    }
    #endregion
}


function Disconnect-ExoSession {
    <#
    .SYNOPSIS
    Cleanly disconnects Exchange Online, IPPSSession, and Microsoft Graph sessions.
    #>

    [CmdletBinding()]
    param()

    Write-TranscriptLog "Disconnecting Exchange Online, IPPSSession, and Graph sessions" 'INFO' 'AUTH'

    try {
        $null = Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
        Write-TranscriptLog "Exchange Online session disconnected" 'SUCCESS' 'AUTH'
    } catch {
        Write-TranscriptLog "Exchange Online disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH'
    }

    # Disconnect IPPSSession if it was established
    if ($script:IPPSConnected) {
        try {
            $null = Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
            Write-TranscriptLog "IPPSSession disconnected" 'SUCCESS' 'AUTH'
        } catch {
            Write-TranscriptLog "IPPSSession disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH'
        }
    }

    try {
        $GraphCtx = Get-MgContext -ErrorAction SilentlyContinue
        if ($GraphCtx) {
            $null = Disconnect-MgGraph -ErrorAction SilentlyContinue
            Write-TranscriptLog "Microsoft Graph session disconnected" 'SUCCESS' 'AUTH'
        }
    } catch {
        Write-TranscriptLog "Graph disconnect warning: $($_.Exception.Message)" 'WARNING' 'AUTH'
    }

    Write-Host " - Session disconnected." -ForegroundColor Green
}