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 } |