### FILE: README.md # Exchange Online EWS Retirement Toolkit ## Overview October 2026 marks a hard authentication cutoff for EWS in Exchange Online. This toolkit implements the full discovery-to-remediation playbook: 1. **Baseline** — audit org-wide and per-mailbox EWS configuration. 2. **Discovery** — pull 12 months of `EwsConnect` Unified Audit Log events. 3. **Correlation** — resolve `ClientAppId` GUIDs to Entra ID app registrations, owners, and secret/cert expiration. 4. **Containment** — manage `EwsAllowedAppIDs` as a time-boxed whitelist with remediation tickets. 5. **Phased Cutover** — flip mailbox-level `EwsEnabled` by cohort, validate, then lock down org-wide. 6. **Dashboard** — an M365 SaaS Engine dashboard (PHP) surfacing discovery metrics via Microsoft Graph. ## Structure ``` scripts/ 1-Get-EwsBaselineConfig.ps1 2-Export-EwsConnectAuditLogs.ps1 3-Resolve-EwsClientAppIds.ps1 4-Manage-EwsAllowedAppIDsWhitelist.ps1 5-Set-EwsMailboxCohortCutover.ps1 6-Invoke-EwsOrgWideLockdown.ps1 src/ index.php includes/graph_helpers.php ``` ## Required Scopes / Roles - Entra ID App/Delegated: `Exchange.ManageAsApp`, `AuditLog.Read.All`, `Application.Read.All` - Exchange Online Role: `View-Only Organization Management` (discovery) / `Organization Management` (remediation) ## Author **Souhaiel Morhag** — [MSEndpoint.com](https://msendpoint.com) | [Academy](https://app.msendpoint.com/academy) | [LinkedIn](https://linkedin.com/in/souhaiel-morhag) | [GitHub](https://github.com/Msendpoint) ### FILE: scripts/1-Get-EwsBaselineConfig.ps1 <# .SYNOPSIS Baselines the current Exchange Online EWS configuration at the org-wide and per-mailbox level. .DESCRIPTION Connects to Exchange Online and reports on the org-wide EwsEnabled default (Get-OrganizationConfig), the current EwsAllowedAppIDs whitelist, and a per-mailbox export of EwsEnabled overrides via Get-CASMailbox. Use this as the mandatory first step of the EWS retirement playbook before making any containment or cutover changes. Exports results to CSV for audit trail purposes. .EXAMPLE .\1-Get-EwsBaselineConfig.ps1 -OutputPath "C:\EwsAudit\Baseline" Connects to Exchange Online, pulls org-wide and per-mailbox EWS settings, and writes OrgConfig-Baseline.csv and MailboxEws-Baseline.csv to the specified folder. .NOTES Author: Souhaiel Morhag Company: MSEndpoint.com Blog: https://msendpoint.com Academy: https://app.msendpoint.com/academy LinkedIn: https://linkedin.com/in/souhaiel-morhag GitHub: https://github.com/Msendpoint License: MIT #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string]$OutputPath = ".\EwsBaseline" ) begin { if (-not (Test-Path -Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } if (-not (Get-Module -Name ExchangeOnlineManagement -ListAvailable)) { throw "ExchangeOnlineManagement module is required. Install it via: Install-Module ExchangeOnlineManagement" } } process { try { Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop Write-Host "Retrieving org-wide EWS configuration..." -ForegroundColor Cyan $orgConfig = Get-OrganizationConfig -ErrorAction Stop | Select-Object Identity, EwsEnabled, EwsAllowedAppIDs, EwsApplicationAccessPolicy $orgConfigPath = Join-Path -Path $OutputPath -ChildPath "OrgConfig-Baseline.csv" $orgConfig | Export-Csv -Path $orgConfigPath -NoTypeInformation -Encoding UTF8 Write-Host "Org-wide config exported to: $orgConfigPath" -ForegroundColor Green if ($orgConfig.EwsEnabled -eq $true -and [string]::IsNullOrEmpty($orgConfig.EwsAllowedAppIDs)) { Write-Warning "Org-wide EwsEnabled = TRUE with NO EwsAllowedAppIDs whitelist. Every registered app with a valid OAuth token can currently reach every mailbox via EWS." } Write-Host "Retrieving per-mailbox EWS overrides (this may take a while at scale)..." -ForegroundColor Cyan $mailboxes = Get-CASMailbox -ResultSize Unlimited -ErrorAction Stop | Select-Object Identity, PrimarySmtpAddress, EwsEnabled, EwsAllowedAppIDs $mailboxPath = Join-Path -Path $OutputPath -ChildPath "MailboxEws-Baseline.csv" $mailboxes | Export-Csv -Path $mailboxPath -NoTypeInformation -Encoding UTF8 Write-Host "Per-mailbox EWS baseline exported to: $mailboxPath" -ForegroundColor Green $summary = [PSCustomObject]@{ TotalMailboxes = $mailboxes.Count MailboxesWithEwsExplicitlyDisabled = ($mailboxes | Where-Object { $_.EwsEnabled -eq $false }).Count MailboxesWithNoOverride = ($mailboxes | Where-Object { $null -eq $_.EwsEnabled }).Count OrgWideEwsEnabled = $orgConfig.EwsEnabled OrgWideWhitelistCount = ($orgConfig.EwsAllowedAppIDs | Measure-Object).Count } Write-Host "`n--- Baseline Summary ---" -ForegroundColor Yellow $summary | Format-List } catch { Write-Error "Baseline collection failed: $($_.Exception.Message)" throw } finally { Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue } }