# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #Requires -Version 3.0 <# .SYNOPSIS This script audits mails, calendar and task items and check if PidLidReminderFileParameter property is populated or not. If required admins can use this script to cleanup the property for items they find malicious or even delete these items permanently. .DESCRIPTION There are two modes in which we can run the script Audit and Cleanup. Audit Mode: Script provides a csv to the admins with details of items that have PidLidReminderFileParameter property populated. Cleanup Mode: Script performs Cleanup action on items by either clearing the property or deleting the mail itself. Prerequisites to run the script for Exchange Server on-premises: You need to have ApplicationImpersonation role. You can create a new role group with the required permissions by running the following PowerShell command in an elevated Exchange Management Shell (EMS): `New-RoleGroup -Name "CVE-2023-23397-Script" -Roles "ApplicationImpersonation" -Description "Permission to run the CVE-2023-23397 script"` Prerequisites to run the script for Exchange Online: You need to be a member of Organization Management. The script will create an application with full access permission on all the mailboxes. The script uses EWS managed api to make ews calls in order to fetch items from user mailboxes. So the machine on which the script is run should be able to make EWS calls to Exchange Server. .PARAMETER CreateAzureApplication Use this switch to create a Azure AD application that can be used for running the script in online mode .PARAMETER DeleteAzureApplication Use this switch to delete the Azure AD application .PARAMETER UserMailboxes Use this parameter to provide list of user primary SMTP address. .PARAMETER StartTimeFilter Use this parameter to provide start time filter .PARAMETER EndTimeFilter Use this parameter to provide end time filter .PARAMETER CleanupAction Use this parameter to provide type of cleanup action you want to provide (ClearProperty/ClearItem) .PARAMETER CleanupInfoFilePath Use this parameter to provide path to the csv file containing the details of items to be cleaned up .PARAMETER EWSExchange2013 Use this switch if you are running on Exchange 2013 server mailboxes .PARAMETER MaxCSVLength This optional parameter allows you to provide maximum csv length .PARAMETER AzureEnvironment This optional parameter allows you to provide Azure environment .PARAMETER AzureApplicationName This optional parameter allows you to provide Azure application name .PARAMETER CertificateThumbprint This optional parameter allows you to provide a certificate thumbprint to use the Azure application created by the script .PARAMETER AppId This optional parameter allows you to provide the AppId of the Azure application created by the script .PARAMETER Organization This optional parameter allows you to provide the organization name e.g., contoso.onmicrosoft.com .PARAMETER AzureADEndpoint This optional parameter allows you to provide Azure AD endpoint .PARAMETER EWSServerURL This optional parameter allows you to provide EWS server URL .PARAMETER DLLPath This optional parameter allows you to provide the path to Microsoft.Exchange.WebServices.dll .PARAMETER ScriptUpdateOnly This optional parameter allows you to only update the script without performing any other actions. .PARAMETER SkipVersionCheck This optional parameter allows you to skip the automatic version check and script update. .PARAMETER IgnoreCertificateMismatch This optional parameter lets you ignore TLS certificate mismatch errors. .PARAMETER Credential This optional parameter lets you pass administrator credential object while running on Exchange Server on-premises. .PARAMETER UseSearchFolders This switch causes the script to use deep-traversal search folders, significantly improving performance. .PARAMETER SearchFolderCleanup Clean up any search folders left behind by the -UseSearchFolders switch. .PARAMETER SkipSearchFolderCreation Skip the first pass of -UseSearchFolders and just check the existing search folders for results. .PARAMETER TimeoutSeconds This optional parameter lets you specify the timeout value for the ExchangeService object. Defaults to 5 minutes. .EXAMPLE PS C:\> .\CVE-2023-23397.ps1 -CreateAzureApplication This will run the tool to create a new Azure application with required permissions .EXAMPLE PS C:\> (Get-Mailbox).PrimarySMTPAddress | .\CVE-2023-23397.ps1 -Environment This will run the tool in audit mode on all the users present .EXAMPLE PS C:\> .\CVE-2023-23397.ps1 -Environment -CleanupAction ClearItem -CleanupInfoFilePath This will run the tool in clean up mode and clear all the items mentioned in the csv file #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [ValidateSet("Online", "Onprem")] [Parameter(Mandatory = $true, ParameterSetName = "Audit")] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [string]$Environment, [Parameter(Mandatory = $true, ParameterSetName = "CreateAzureApplication")] [switch]$CreateAzureApplication, [Parameter(Mandatory = $true, ParameterSetName = "DeleteAzureApplication")] [switch]$DeleteAzureApplication, [Parameter(Mandatory = $true, ParameterSetName = "Audit", ValueFromPipelineByPropertyName = $true)] [Alias("PrimarySmtpAddress")] [string[]]$UserMailboxes, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [DateTime]$StartTimeFilter, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [DateTime]$EndTimeFilter, [ValidateSet("ClearProperty", "ClearItem")] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [string]$CleanupAction, [ValidateScript({ Test-Path -Path $_ -PathType leaf })] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [ValidatePattern("(.*?)\.(csv)$")] [string]$CleanupInfoFilePath, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$EWSExchange2013, [ValidateRange(1, [Int]::MaxValue)] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [int]$MaxCSVLength = 200000, [ValidateSet("Global", "USGovernmentL4", "USGovernmentL5", "ChinaCloud")] [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AzureEnvironment = "Global", [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AzureApplicationName = "CVE-2023-23397Application", [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$CertificateThumbprint, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AppId, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$Organization, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [uri]$EWSServerURL, [ValidateScript({ Test-Path $_ })] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$DLLPath, [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] [switch]$ScriptUpdateOnly, [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$SkipVersionCheck, [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$IgnoreCertificateMismatch, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$UseSearchFolders, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$SearchFolderCleanup, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$SkipSearchFolderCreation, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [ValidateRange(1, 2147483)] [int]$TimeoutSeconds = 300 ) dynamicparam { if ($Environment -eq "Onprem") { $auditParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Audit" Mandatory = $true } $cleanupParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Cleanup" Mandatory = $true } } else { $auditParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Audit" Mandatory = $false } $cleanupParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Cleanup" Mandatory = $false } } $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $attributeCollection.Add($auditParameterAttribute) $attributeCollection.Add($cleanupParameterAttribute) $credentialType = [System.Management.Automation.PSCredential] $dynParameter = New-Object System.Management.Automation.RuntimeDefinedParameter 'Credential', $credentialType, $attributeCollection $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $paramDictionary.Add('Credential', $dynParameter) return $paramDictionary } begin { $BuildVersion = "" $searchFolderName = "ReminderFileParameterItems" $credential = $PSBoundParameters['Credential'] . $PSScriptRoot\WriteFunctions.ps1 . $PSScriptRoot\..\..\..\Shared\CertificateFunctions\Enable-TrustAnyCertificateCallback.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Host.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Verbose.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Warning.ps1 . $PSScriptRoot\..\..\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 . $PSScriptRoot\..\..\..\Shared\Get-NuGetPackage.ps1 . $PSScriptRoot\..\..\..\Shared\Invoke-ExtractArchive.ps1 . $PSScriptRoot\..\..\..\Shared\LoggerFunctions.ps1 . $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Test-ADCredentials.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-GraphAccessToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-NewJsonWebToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-NewOAuthToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Invoke-GraphApiRequest.ps1 . $PSScriptRoot\..\..\..\Shared\Show-Disclaimer.ps1 $loggerParams = @{ LogName = "CVE-2023-23397-$((Get-Date).ToString("yyyyMMddhhmmss"))-Debug" AppendDateTimeToFileName = $false ErrorAction = "SilentlyContinue" } $Script:Logger = Get-NewLoggerInstance @loggerParams SetWriteHostAction ${Function:Write-HostLog} SetWriteVerboseAction ${Function:Write-VerboseLog} SetWriteWarningAction ${Function:Write-HostLog} $mode = $PsCmdlet.ParameterSetName $cloudService = Get-CloudServiceEndpoint $AzureEnvironment # Define the endpoints that we need for the various calls to the Azure AD Graph API and EWS $ewsOnlineURL = "$($cloudService.ExchangeOnlineEndpoint)/EWS/Exchange.asmx" $ewsOnlineScope = "$($cloudService.ExchangeOnlineEndpoint)/.default" $autoDSecureName = $cloudService.AutoDiscoverSecureName $azureADEndpoint = $cloudService.AzureADEndpoint $graphApiEndpoint = $cloudService.GraphApiEndpoint ## function to create ews managed api service object function EWSAuth { param( [string]$Environment, $Token, $EWSOnlineURL, $EWSServerURL ) ## Create the Exchange Service object with credentials if ($EWSExchange2013) { $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013) } else { $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2016) } $Service.Timeout = $TimeoutSeconds * 1000 if ($Environment -eq "Onprem") { $Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($credential.UserName, [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password))) } else { $Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($Token.access_token) } if ($Environment -eq "Onprem") { if (-not([System.String]::IsNullOrEmpty($EWSServerURL))) { $Service.Url = New-Object Uri($EWSServerURL) CheckOnpremCredentials -EWSService $Service } else { try { $credentialTestResult = Test-ADCredentials -Credentials $credential if ($credentialTestResult.CredentialsValid) { Write-Host "Credentials validated successfully." -ForegroundColor Green } elseif ($credentialTestResult.CredentialsValid -eq $false) { Write-Host "Credentials that were provided are incorrect." -ForegroundColor Red exit } else { Write-Host "Credentials couldn't be validated. Trying to use the credentials anyway." -ForegroundColor Yellow } if (($credentialTestResult.UsernameFormat -eq "downlevel") -or ($credentialTestResult.UsernameFormat -eq "local")) { Write-Host "Username: $($Credential.UserName) was passed in $($credentialTestResult.UsernameFormat) format" -ForegroundColor Red Write-Host "You must use the -EWSServerURL parameter if the username is not in UPN (username@domain) format." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red exit } $redirectionCallback = { param([string]$url) return $url.ToLower().StartsWith($autoDSecureName) } $Service.AutodiscoverUrl($Credential.UserName, $redirectionCallback) } catch [Microsoft.Exchange.WebServices.Data.AutodiscoverLocalException] { Write-Host "Unable to locate the Autodiscover service by using username $($Credential.UserName)" -ForegroundColor Red Write-Host "A reason could be that the username that was passed uses a domain which is not an accepted domain by Exchange Server." -ForegroundColor Red Write-Host "You must use the -EWSServerURL parameter if the Autodiscover service cannot be located." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red Write-Host "Inner Exception:`n$_" -ForegroundColor Red exit } catch [System.FormatException] { # We should no longer get here as we are validating the username format above, however, keeping this as failsafe for now Write-Host "Username: $($Credential.UserName) was passed in an unexpected format" -ForegroundColor Red Write-Host "Please try again or use the -EWSServerURL parameter to provide the EWS url." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red Write-Host "Inner Exception:`n$_" -ForegroundColor Red exit } catch { Write-Host "Unable to make Autodiscover call to fetch EWS endpoint details. Please make sure you have enter valid credentials. Inner Exception`n`n$_" -ForegroundColor Red exit } } } else { $Service.Url = $EWSOnlineURL } return $Service } ## function to validate onprem credential function CheckOnpremCredentials { param( $EWSService ) try { $null = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($EWSService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot) } catch [Microsoft.Exchange.WebServices.Data.ServiceRequestException] { Write-Host "Unable to connect to EWS endpoint. Please make sure you have enter valid credentials. Inner Exception`n`n$_" -ForegroundColor Red exit } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { if ($_.Exception.ErrorCode -eq "ErrorNonExistentMailbox") { # This is fine. This means this account is not mail-enabled, but credentials worked. return } elseif ($_.Exception.ErrorCode -eq "ErrorMissingEmailAddress") { # On Exchange 2013, even if we handle this error, it doesn't work. Write-Host "Could not open mailbox due to ErrorMissingEmailAddress. If running on Exchange 2013, the impersonation user must have a mailbox on prem. Inner Exception`n`n$_" -ForegroundColor Red exit } else { Write-Host "Could not open mailbox. Inner Exception`n`n$_" -ForegroundColor Red exit } } catch { Write-Host "Could not open mailbox. Inner Exception`n`n$_" -ForegroundColor Red exit } } ## function to add a row to the csv function CreateCustomCSV { param( $Mailbox, $Data, [string]$CsvPath ) $ItemType = $Data.ItemClass if ($Data.ItemClass.StartsWith("IPM.Note")) { $ItemType = "E-Mail" } elseif ($Data.ItemClass.StartsWith("IPM.Appointment")) { $ItemType = "Calendar" } elseif ($Data.ItemClass.StartsWith("IPM.Task")) { $ItemType = "Task" } <# Make sure to update the $headerLine.Count check if you add more columns or remove some. We do the check after running Import-Csv call before processing a csv file that was passed via CleanupInfoFilePath parameter. #> $row = [PSCustomObject]@{ "Mailbox" = $Mailbox "Id" = $Data.Id "ItemType" = $ItemType "Sender" = ($Data.From | Select-Object -ExpandProperty Address) -join "," "Recipient" = ($Data.ToRecipients | Select-Object -ExpandProperty Address) -join "," "Subject" = $Data.Subject "DateReceived" = $Data.DateTimeReceived "PidLidReminderFileParameter" = $Data.ExtendedProperties[0].Value "Cleanup" = "N" } $row | Export-Csv -Path $CsvPath -Delimiter "," -NoTypeInformation -Append -Encoding utf8 -Force } # Define a function to get all the SubFolders of a given folder function GetSubFolders { param( $Folder, $FoldersList ) # Get the SubFolders of the folder $folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(100) do { $Folder.FindFolders($folderView) | ForEach-Object { # Add the folder path to the list $null = $FoldersList.Add($_) # Recursively get the SubFolders of this folder GetSubFolders -folder $_ -foldersList $FoldersList } $folderView.Offset += $result.Folders.Count } while ($result.MoreAvailable -eq $true) } ## function to find item on basis of store ID function FindItem { param( [Microsoft.Exchange.WebServices.Data.ExchangeService]$ExchangeService, [string]$Id ) $ps = New-Object Microsoft.Exchange.WebServices.Data.PropertySet(New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common, 0x0000851F, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String)) return [Microsoft.Exchange.WebServices.Data.Item]::Bind($ExchangeService, $Id, $ps) } function GetAzureApplication { param( $AzAccountsObject, $AzureApplicationName, $GraphApiUrl ) <# Get the Azure AD Application ID for the given Azure Application Name https://learn.microsoft.com/graph/api/application-list?view=graph-rest-1.0&tabs=http#request #> $listAadApplicationParams = @{ Query = ("applications?`$filter=displayName eq '{0}'" -f $AzureApplicationName) AccessToken = $AzAccountsObject.AccessToken GraphApiUrl = $GraphApiUrl } $listAadApplicationResponse = Invoke-GraphApiRequest @listAadApplicationParams if ($listAadApplicationResponse.Successful) { return $listAadApplicationResponse.Content } return $null } function NewAzureApplication { param( $AzAccountsObject, $AzureApplicationName, $GraphApiUrl ) <# This function will create an Azure AD Application by calling the Graph API https://docs.microsoft.com/graph/api/application-post-applications?view=graph-rest-1.0&tabs=http #> $getAadApplicationParams = @{ AzAccountsObject = $AzAccountsObject AzureApplicationName = $AzureApplicationName GraphApiUrl = $GraphApiUrl } $getAadApplicationResponse = GetAzureApplication @getAadApplicationParams if ($null -eq $getAadApplicationResponse) { Write-Host "Something went wrong while querying the Azure AD Application" -ForegroundColor Red exit } elseif (-not([System.String]::IsNullOrEmpty($getAadApplicationResponse.Value.id))) { Write-Host "Application with name: $AzureApplicationName already exists..." Write-Host "Client ID: $($getAadApplicationResponse.Value.AppId)" -ForegroundColor Green exit } # Graph API call to create a new Azure AD Application $newAzureAdApplicationParams = @{ Query = "applications" Body = @{ "displayName" = $AzureApplicationName; "signInAudience" = "AzureADMyOrg" } | ConvertTo-Json AccessToken = $AzAccountsObject.AccessToken Method = "Post" ExpectedStatusCode = 201 GraphApiUrl = $GraphApiUrl } $createAzureApplicationResponse = Invoke-GraphApiRequest @newAzureAdApplicationParams if ($createAzureApplicationResponse.Successful -eq $false) { Write-Host "Something went wrong while creating the Azure AD Application: $AzureApplicationName" -ForegroundColor Red exit } $aadApplication = $createAzureApplicationResponse.Content # Graph API call to get the current logged in user $loggedInUserParams = @{ Query = "me" AccessToken = $AzAccountsObject.AccessToken GraphApiUrl = $GraphApiUrl } $loggedInUserResponse = Invoke-GraphApiRequest @loggedInUserParams if ($loggedInUserResponse.Successful -eq $false) { Write-Host "Unable to query the logged in user. Please try again." -ForegroundColor Red exit } $currentUser = $loggedInUserResponse.Content # Graph API call to query the owners of the Azure AD Application $listOwnerParams = @{ Query = "applications/$($aadApplication.id)/owners" AccessToken = $AzAccountsObject.AccessToken GraphApiUrl = $GraphApiUrl } $listOwnerResponse = Invoke-GraphApiRequest @listOwnerParams if ($listOwnerResponse.Successful -eq $false) { Write-Host "Something went wrong while querying the owners of the Azure application: $AzureApplicationName" -ForegroundColor Red exit } if ($listOwnerResponse.Content.Value.id.Contains($currentUser.id)) { Write-Host "User: $($currentUser.userPrincipalName) is already an owner of application: $AzureApplicationName" } else { Write-Host "User: $($currentUser.userPrincipalName) is not an owner of application: $AzureApplicationName" # Graph API call to add the current user as owner of the Azure AD Application $addUserAsOwnerParams = @{ Query = "applications/$($aadApplication.id)/owners/`$ref" AccessToken = $AzAccountsObject.AccessToken Body = @{ "@odata.id" = "$GraphApiUrl/v1.0/directoryObjects/$($currentUser.id)" } | ConvertTo-Json Method = "Post" ExpectedStatusCode = 204 GraphApiUrl = $GraphApiUrl } $addUserAsOwnerResponse = Invoke-GraphApiRequest @addUserAsOwnerParams if ($addUserAsOwnerResponse.Successful -eq $false) { Write-Host "Something went wrong while adding the user as owner of the Azure application: $AzureApplicationName" -ForegroundColor Red exit } } # Graph API call to update the Azure AD Application and add the required permissions # https://learn.microsoft.com/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth#configure-for-delegated-authentication # https://learn.microsoft.com/troubleshoot/azure/active-directory/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications $updateApplicationParams = @{ Query = "applications/$($aadApplication.id)" AccessToken = $AzAccountsObject.AccessToken Body = '{ "requiredResourceAccess": [ { "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { "id": "dc890d15-9560-4a4c-9b7f-a736ec74ec40", "type": "Role" } ] } ] }' Method = "Patch" ExpectedStatusCode = 204 GraphApiUrl = $GraphApiUrl } $updateApplicationResponse = Invoke-GraphApiRequest @updateApplicationParams if ($updateApplicationResponse.Successful -eq $false) { Write-Host "Something went wrong while adding the required permission to application: $AzureApplicationName" -ForegroundColor Red exit } # Graph API call to create a new service principal for the Azure AD Application $newServicePrincipalParams = @{ Query = "servicePrincipals" AccessToken = $AzAccountsObject.AccessToken Body = @{ "appId" = $aadApplication.appId; "accountEnabled" = $true } | ConvertTo-Json Method = "Post" ExpectedStatusCode = 201 GraphApiUrl = $GraphApiUrl } $newServicePrincipalResponse = Invoke-GraphApiRequest @newServicePrincipalParams if ($newServicePrincipalResponse.Successful -eq $false) { Write-Host "Something went wrong while creating the service principal." -ForegroundColor Red exit } $servicePrincipal = $newServicePrincipalResponse.Content # Graph API call to update the service principal and add the required tags $updateServicePrincipalParams = @{ Query = "servicePrincipals/$($servicePrincipal.id)" AccessToken = $AzAccountsObject.AccessToken Body = @{ "tags" = @("WindowsAzureActiveDirectoryIntegratedApp") } | ConvertTo-Json Method = "Patch" ExpectedStatusCode = 204 GraphApiUrl = $GraphApiUrl } $updateServicePrincipalResponse = Invoke-GraphApiRequest @updateServicePrincipalParams if ($updateServicePrincipalResponse.Successful -eq $false) { Write-Host "Something went wrong while updating the service principal." -ForegroundColor Red exit } # Graph API call to query the Office 365 Exchange Online service principal (as we need the object id) $o365ExchangeOnlineServicePrincipalParams = @{ Query = "servicePrincipals?`$filter=appId eq '00000002-0000-0ff1-ce00-000000000000'" AccessToken = $AzAccountsObject.AccessToken GraphApiUrl = $GraphApiUrl } $o365ExchangeOnlineServicePrincipalResponse = Invoke-GraphApiRequest @o365ExchangeOnlineServicePrincipalParams if ($o365ExchangeOnlineServicePrincipalResponse.Successful -eq $false) { Write-Host "Something went wrong while querying the Office 365 Exchange Online service principal." -ForegroundColor Red exit } $o365ExchangeOnlineObjectId = $o365ExchangeOnlineServicePrincipalResponse.Content.value[0].id # Graph API call to provide admin consent to the application $adminConsentParams = @{ Query = "servicePrincipals/$($servicePrincipal.id)/appRoleAssignments" AccessToken = $AzAccountsObject.AccessToken Body = @{ "principalId" = $servicePrincipal.id; "resourceId" = $o365ExchangeOnlineObjectId; "appRoleId" = "dc890d15-9560-4a4c-9b7f-a736ec74ec40" } | ConvertTo-Json Method = "Post" ExpectedStatusCode = 201 GraphApiUrl = $GraphApiUrl } $adminConsentResponse = Invoke-GraphApiRequest @adminConsentParams if ($adminConsentResponse.Successful -eq $false) { Write-Host "Something went wrong while providing admin consent to application: $AzureApplicationName" -ForegroundColor Red exit } Write-Host "Application: $AzureApplicationName created with required permissions. Client ID: $($aadApplication.appId)" -ForegroundColor Green } function NewAzureApplicationAppSecret { param( $AzAccountsObject, $AzureApplicationName, $GraphApiUrl ) <# This function creates an App secret for a given application and return it. The assigned App Password is valid for 7 days. https://learn.microsoft.com/graph/api/application-addpassword?view=graph-rest-1.0&tabs=http#request #> $getAadApplicationParams = @{ AzAccountsObject = $AzAccountsObject AzureApplicationName = $AzureApplicationName GraphApiUrl = $GraphApiUrl } $getAadApplicationResponse = GetAzureApplication @getAadApplicationParams if ($null -eq $getAadApplicationResponse) { Write-Host "Something went wrong while querying the Azure application" -ForegroundColor Red exit } elseif ([System.String]::IsNullOrEmpty($getAadApplicationResponse.value.id)) { Write-Host "No Azure application found with the name: $AzureApplicationName. Please re-run the script with -CreateAzureApplication to create the application." -ForegroundColor Red exit } # Garbage collect expired secrets if ($getAadApplicationResponse.value.passwordCredentials.Count -gt 0) { Write-Host "The Azure application already has application secrets - checking for expired ones..." foreach ($password in $getAadApplicationResponse.value.passwordCredentials) { $endDateTime = [DateTime]::Parse($password.endDateTime).ToUniversalTime() if ($endDateTime -lt (Get-Date).ToUniversalTime()) { Write-Host "Secret with id: $($password.keyId) has expired since: $endDateTime - deleting it now..." $deleteAadApplicationPasswordParams = @{ Query = "applications/$($getAadApplicationResponse.value.id)/removePassword" AccessToken = $AzAccountsObject.AccessToken Body = @{ "keyId" = $password.keyId } | ConvertTo-Json Method = "POST" ExpectedStatusCode = 204 GraphApiUrl = $GraphApiUrl } $deleteAadApplicationPasswordResponse = Invoke-GraphApiRequest @deleteAadApplicationPasswordParams if ($deleteAadApplicationPasswordResponse.Successful -eq $false) { Write-Host "Unable to delete secret with id: $($password.keyId) - please delete it manually" -ForegroundColor Yellow } } } } # Specify secret expiration time which must be in ISO 8601 format and is always in UTC time $pwdEndDateTime = ([DateTime]::UtcNow).AddDays(7).ToString("o") # Graph API call to create a new application password $newAadApplicationPasswordParams = @{ Query = "applications/$($getAadApplicationResponse.value.id)/addPassword" AccessToken = $AzAccountsObject.AccessToken Body = @{ "passwordCredential" = @{ "displayName" = "AppAccessKey" "endDateTime" = $pwdEndDateTime } } | ConvertTo-Json Method = "POST" GraphApiUrl = $GraphApiUrl } $newAadApplicationPasswordResponse = Invoke-GraphApiRequest @newAadApplicationPasswordParams if ($newAadApplicationPasswordResponse.Successful -eq $false) { Write-Host "Unable to create the Azure application password" -ForegroundColor Red exit } Write-Host "Secret created for Azure application: $AzureApplicationName - waiting 60 seconds for replication..." Start-Sleep -Seconds 60 Write-Host "Continuing..." return $newAadApplicationPasswordResponse.Content.secretText } function DeleteAzureApplication { param( $AzAccountsObject, $AzureApplicationName, $GraphApiUrl ) <# This function will delete the specified Azure AD application https://docs.microsoft.com/graph/api/application-delete?view=graph-rest-1.0&tabs=http #> $getAadApplicationParams = @{ AzAccountsObject = $AzAccountsObject AzureApplicationName = $AzureApplicationName GraphApiUrl = $GraphApiUrl } $getAadApplicationResponse = GetAzureApplication @getAadApplicationParams if ($null -eq $getAadApplicationResponse) { Write-Host "Something went wrong while querying the Azure AD Application" -ForegroundColor Red exit } elseif ([System.String]::IsNullOrEmpty($getAadApplicationResponse.Value.id)) { Write-Host "No application with name: $AzureApplicationName found" -ForegroundColor Red exit } $deleteAadApplicationParams = @{ Query = "applications/$($getAadApplicationResponse.value[0].id)" AccessToken = $AzAccountsObject.AccessToken Method = "DELETE" ExpectedStatusCode = 204 GraphApiUrl = $GraphApiUrl } $deleteAzureApplicationResponse = Invoke-GraphApiRequest @deleteAadApplicationParams if ($deleteAzureApplicationResponse.Successful -eq $false) { Write-Host "Unable to delete the Azure AD application. Please try again or delete it manually." -ForegroundColor Red exit } Write-Host "Deleted the Azure application: $AzureApplicationName successfully" -ForegroundColor Green } ## function to check if a given token is expired and renew it function CheckTokenExpiry { param( $ApplicationInfo, [ref]$EWSService, [ref]$Token, [string]$Environment, $EWSOnlineURL, $EWSOnlineScope, $AzureADEndpoint ) if ($Environment -eq "Onprem") { return } # if token is going to expire in next 5 min then refresh it if ($null -eq $script:tokenLastRefreshTime -or $script:tokenLastRefreshTime.AddMinutes(55) -lt (Get-Date)) { $createOAuthTokenParams = @{ TenantID = $ApplicationInfo.TenantID ClientID = $ApplicationInfo.ClientID Endpoint = $AzureADEndpoint CertificateBasedAuthentication = (-not([System.String]::IsNullOrEmpty($ApplicationInfo.CertificateThumbprint))) Scope = $EWSOnlineScope } # Check if we use an app secret or certificate by using regex to match Json Web Token (JWT) if ($ApplicationInfo.AppSecret -match "^([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)") { $jwtParams = @{ CertificateThumbprint = $ApplicationInfo.CertificateThumbprint CertificateStore = "CurrentUser" Issuer = $ApplicationInfo.ClientID Audience = "$AzureADEndpoint/$($ApplicationInfo.TenantID)/oauth2/v2.0/token" Subject = $ApplicationInfo.ClientID } $jwt = Get-NewJsonWebToken @jwtParams if ($null -eq $jwt) { Write-Host "Unable to sign a new Json Web Token by using certificate: $($ApplicationInfo.CertificateThumbprint)" -ForegroundColor Red exit } $createOAuthTokenParams.Add("Secret", $jwt) } else { $createOAuthTokenParams.Add("Secret", $ApplicationInfo.AppSecret) } $oAuthReturnObject = Get-NewOAuthToken @createOAuthTokenParams if ($oAuthReturnObject.Successful -eq $false) { Write-Host "" Write-Host "Unable to refresh EWS OAuth token. Please review the error message below and re-run the script:" -ForegroundColor Red Write-Host $oAuthReturnObject.ExceptionMessage -ForegroundColor Red exit } $Token.Value = $oAuthReturnObject.OAuthToken $script:tokenLastRefreshTime = $oAuthReturnObject.LastTokenRefreshTime $EWSService.Value = EWSAuth -Environment $Environment -Token $Token.Value -EWSOnlineURL $EWSOnlineURL } } ## function to get our named search folder function GetSearchFolder { param($ewsService, $userMailbox) $searchFoldersId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root, $userMailbox) $searchFoldersFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ewsService, $searchFoldersId) $searchFoldersView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) $searchFoldersFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $searchFolderName) $results = $searchFoldersFolder.FindFolders($searchFoldersFilter, $searchFoldersView) return $results } ## function to create our named search folder function NewSearchFolder { param($ewsService, $userMailbox) $rootFolderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $userMailbox) $searchFoldersId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root, $userMailbox) $mySearchFolder = New-Object Microsoft.Exchange.WebServices.Data.SearchFolder($ewsService) $mySearchFolder.DisplayName = $searchFolderName $mySearchFolder.SearchParameters.SearchFilter = $searchFilterCollection $mySearchFolder.SearchParameters.RootFolderIds.Add($rootFolderId) $mySearchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep [void]$mySearchFolder.Save($searchFoldersId) } function RemoveSearchFolder { param($ewsService, $userMailbox) $searchFolders = @(GetSearchFolder $ewsService $userMailbox) if ($searchFolders.Count -gt 0) { Write-Host " Cleaning up search folder." [void]$searchFolders[0].Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) } } function GetSearchFolderItemResults { param($ewsService, $userMailbox) $searchFolders = @(GetSearchFolder $ewsService $userMailbox) if ($searchFolders.Count -lt 1) { Write-Warning "Search folder missing. Could not get results." return } $offset = 0 $pageSize = 100 $findItemsResults = $null do { $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($pageSize, $offset) $findItemsResults = $searchFolders[0].FindItems($itemView) foreach ($item in $findItemsResults.Items) { $item } $offset = $findItemsResults.NextPageOffset } while ($findItemsResults.MoreAvailable) } $mailAddresses = New-Object System.Collections.ArrayList } process { foreach ($address in $UserMailboxes) { [void]$mailAddresses.Add($address) } } end { Write-Host ("CVE-2023-23397 script version $($BuildVersion)") -ForegroundColor Green Write-Verbose "PowerShell version: $($PSVersionTable.PSVersion)" # Using either of these switches implies -UseSearchFolders if ($SearchFolderCleanup -or $SkipSearchFolderCreation) { # But using them together is not valid if ($SearchFolderCleanup -and $SkipSearchFolderCreation) { Write-Host "The -SearchFolderCleanup and -SkipSearchFolderCreation switches cannot be used together." exit } $UseSearchFolders = $true } if (([System.String]::IsNullOrEmpty($CleanupInfoFilePath)) -and ($ScriptUpdateOnly -eq $false) -and ($UseSearchFolders -eq $false) -and ($SearchFolderCleanup -eq $false)) { $newSearchFeatureWording = "Did you know?" + "`nThe new asynchronous search feature is now general available (GA)!" + "`nWe recommend using it because the search performance is significantly increased." + "`nYou can enable it by using the the following parameters:" + "`n" + "`nUseSearchFolders: Enable deep-traversal SearchFolders search to significantly improve performance" + "`nSearchFolderCleanup: Clean up any SearchFolders left behind by the -UseSearchFolders switch" + "`n" + "`nThis version of the script also supports Certificate Based Authentication (CBA)" + "`n" + "`nMore information: https://aka.ms/CVE-2023-23397ScriptDocFAQ" Write-Host "" Write-Host $newSearchFeatureWording -ForegroundColor Cyan Write-Host "" Start-Sleep -Seconds 3 } if ($ScriptUpdateOnly) { switch (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/CVE-2023-23397-VersionsUrl" -Confirm:$false) { ($true) { Write-Host ("Script was successfully updated") -ForegroundColor Green } ($false) { Write-Host ("No update of the script performed") -ForegroundColor Yellow } default { Write-Host ("Unable to perform ScriptUpdateOnly operation") -ForegroundColor Red } } return } if ((-not($SkipVersionCheck)) -and (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/CVE-2023-23397-VersionsUrl" -Confirm:$false)) { Write-Host ("Script was updated. Please rerun the command") -ForegroundColor Yellow return } if ($IgnoreCertificateMismatch) { Write-Verbose ("IgnoreCertificateMismatch was used -policy will be set to: TrustAllCertsPolicy") Enable-TrustAnyCertificateCallback } if ($CreateAzureApplication) { $createAzureApplicationParams = @{ AzAccountsObject = (Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint) AzureApplicationName = $AzureApplicationName GraphApiUrl = $graphApiEndpoint } NewAzureApplication @createAzureApplicationParams exit } if ($DeleteAzureApplication) { $deleteAzureApplicationParams = @{ AzAccountsObject = (Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint) AzureApplicationName = $AzureApplicationName GraphApiUrl = $graphApiEndpoint } DeleteAzureApplication @deleteAzureApplicationParams exit } $path = $DLLPath if ([System.String]::IsNullOrEmpty($path)) { Write-Host "Trying to find Microsoft.Exchange.WebServices.dll in the script folder" $path = (Get-ChildItem -LiteralPath "$PSScriptRoot\EWS" -Recurse -Filter "Microsoft.Exchange.WebServices.dll" -ErrorAction SilentlyContinue | Select-Object -First 1).FullName if ([System.String]::IsNullOrEmpty($path)) { Write-Host "Microsoft.Exchange.WebServices.dll wasn't found - attempting to download it from the internet" -ForegroundColor Yellow $nuGetPackage = Get-NuGetPackage -PackageId "Microsoft.Exchange.WebServices" -Author "Microsoft" if ($nuGetPackage.DownloadSuccessful) { $unzipNuGetPackage = Invoke-ExtractArchive -CompressedFilePath $nuGetPackage.NuGetPackageFullPath -TargetFolder "$PSScriptRoot\EWS" if ($unzipNuGetPackage.DecompressionSuccessful) { $path = (Get-ChildItem -Path $unzipNuGetPackage.FullPathToDecompressedFiles -Recurse -Filter "Microsoft.Exchange.WebServices.dll" | Select-Object -First 1).FullName } else { Write-Host "Failed to unzip Microsoft.Exchange.WebServices.dll. Please unzip the package manually." -ForegroundColor Red exit } } else { Write-Host "Failed to download Microsoft.Exchange.WebServices.dll from the internet. Please download the package manually and extract the dll. Provide the path to dll using DLLPath parameter." -ForegroundColor Red exit } } else { Write-Host "Microsoft.Exchange.WebServices.dll was found in the script folder" -ForegroundColor Green } } try { Import-Module -Name $path -ErrorAction Stop } catch { Write-Host "Failed to import Microsoft.Exchange.WebServices.dll Inner Exception`n`n$_" -ForegroundColor Red exit } $failedMailboxes = New-Object 'System.Collections.Generic.List[string]' $invalidEntries = New-Object 'System.Collections.Generic.List[string]' $removedEntries = New-Object 'System.Collections.Generic.List[string]' #MailInfo $mailInfo = @{ "Id" = [Microsoft.Exchange.WebServices.Data.ItemSchema]::Id "Sender" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::From "Recipient" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ToRecipients "Subject" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject "DateReceived" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::DateTimeReceived "PidLidReminderFileParameter" = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common, 0x0000851F, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String) "ItemClass" = [Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass } if ($Environment -eq "Online") { if (([System.String]::IsNullOrEmpty($AppId)) -or ([System.String]::IsNullOrEmpty($Organization)) -or ([System.String]::IsNullOrEmpty($CertificateThumbprint))) { # We need to query the Azure application information from the Azure AD if not explicitly provided via parameter $azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint $application = GetAzureApplication -AzAccountsObject $azAccountsObject -AzureApplicationName $AzureApplicationName -GraphApiUrl $graphApiEndpoint $tenantID = $azAccountsObject.TenantId $clientID = $application.value.appId } else { $tenantID = $Organization $clientID = $AppId } $applicationInfo = @{ "TenantID" = $tenantID "ClientID" = $clientID } if ([System.String]::IsNullOrEmpty($CertificateThumbprint)) { $secret = NewAzureApplicationAppSecret -AzAccountsObject $azAccountsObject -AzureApplicationName $AzureApplicationName -GraphApiUrl $graphApiEndpoint if ([System.String]::IsNullOrEmpty($secret)) { Write-Host "Unable to generate application secret for Azure application: $AzureApplicationName" -ForegroundColor Red exit } $applicationInfo.Add("AppSecret", $secret) } else { $jwtParams = @{ CertificateThumbprint = $CertificateThumbprint CertificateStore = "CurrentUser" Issuer = $clientID Audience = "$azureADEndpoint/$tenantID/oauth2/v2.0/token" Subject = $clientID } $jwt = Get-NewJsonWebToken @jwtParams if ($null -eq $jwt) { Write-Host "Unable to generate Json Web Token by using certificate: $CertificateThumbprint" -ForegroundColor Red exit } $applicationInfo.Add("AppSecret", $jwt) $applicationInfo.Add("CertificateThumbprint", $CertificateThumbprint) } $createOAuthTokenParams = @{ TenantID = $tenantID ClientID = $clientID Secret = $applicationInfo.AppSecret Scope = $ewsOnlineScope Endpoint = $azureADEndpoint CertificateBasedAuthentication = (-not([System.String]::IsNullOrEmpty($CertificateThumbprint))) } #Create OAUTH token $oAuthReturnObject = Get-NewOAuthToken @createOAuthTokenParams if ($oAuthReturnObject.Successful -eq $false) { Write-Host "" Write-Host "Unable to fetch an OAuth token for accessing EWS. Please review the error message below and re-run the script:" -ForegroundColor Red Write-Host $oAuthReturnObject.ExceptionMessage -ForegroundColor Red exit } $EWSToken = $oAuthReturnObject.OAuthToken $script:tokenLastRefreshTime = $oAuthReturnObject.LastTokenRefreshTime $ewsService = EWSAuth -Environment $Environment -Token $EWSToken -EWSOnlineURL $ewsOnlineURL } else { #Server $EWSToken = $null $ewsService = EWSAuth -Environment $Environment -EWSServerURL $EWSServerURL } if ($PSCmdlet.ParameterSetName -eq "Audit") { if ($null -eq $mailAddresses -or $mailAddresses.Count -eq 0) { Write-Host "No mailbox provided" -ForegroundColor Red exit } $csvFileName = ("AuditResults_$(Get-Date -Format "yyyyMMdd_HHmmss").csv") $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView([int]::MaxValue) $searchFilterCollection = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection if ($null -ne $StartTimeFilter -and -not $UseSearchFolders) { $searchFilterStartTime = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeCreated, $StartTimeFilter) $searchFilterCollection.Add($searchFilterStartTime) } if ($null -ne $EndTimeFilter -and -not $UseSearchFolders) { $searchFilterEndTime = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeCreated, $EndTimeFilter) $searchFilterCollection.Add($searchFilterEndTime) } $searchFilterPidLidReminderFileParameterExists = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($mailInfo["PidLidReminderFileParameter"]) $searchFilterCollection.Add($searchFilterPidLidReminderFileParameterExists) $PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet foreach ($key in $mailInfo.Keys) { $PropertySet.Add($mailInfo[$key]) } $mailboxProcessed = 0 $rowCount = 0 if ($UseSearchFolders) { $actionText = "Creating" if ($SearchFolderCleanup) { $actionText = "Removing" } $searchFolderCreationTimer = $null if (-not $SkipSearchFolderCreation) { foreach ($mailAddress in $mailAddresses) { $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("$actionText search folders in $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) if ($null -eq $userMailbox) { Write-Host ("Unable to get mailbox associated with mail address $mailAddress") $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) if ($SearchFolderCleanup) { RemoveSearchFolder $ewsService $userMailbox $mailboxProcessed += 1 continue } $searchFolders = GetSearchFolder $ewsService $userMailbox if ($searchFolders.Count -gt 0) { Write-Host " Search folder already exists in this mailbox." $mailboxProcessed += 1 continue } # Create a new search folder if ($null -eq $searchFolderCreationTimer) { $searchFolderCreationTimer = New-Object System.Diagnostics.Stopwatch $searchFolderCreationTimer.Start() } NewSearchFolder $ewsService $userMailbox $mailboxProcessed += 1 } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } } } if (-not $SearchFolderCleanup) { if ($null -ne $SearchFolderCreationTimer) { $waitSecondsAfterCreatingFirstSearchFolder = 60 if ($SearchFolderCreationTimer.ElapsedMilliseconds -lt $waitSecondsAfterCreatingFirstSearchFolder * 1000) { $secondsElapsed = $SearchFolderCreationTimer.ElapsedMilliseconds / 1000 $secondsRemaining = $waitSecondsAfterCreatingFirstSearchFolder - $secondsElapsed Write-Host "First search folder created $secondsElapsed seconds ago. Waiting $secondsRemaining seconds to allow for population." Start-Sleep -Seconds $secondsRemaining } } $mailboxProcessed = 0 foreach ($mailAddress in $mailAddresses) { if ($failedMailboxes.Contains($mailAddress)) { $mailboxProcessed += 1 continue } $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("Getting search results in $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) $itemResults = @() try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) [Microsoft.Exchange.WebServices.Data.Item[]]$itemResults = @(GetSearchFolderItemResults $ewsService $userMailbox) } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } $IdsProcessed = New-Object 'System.Collections.Generic.HashSet[string]' if ($itemResults.Count -lt 1) { $mailboxProcessed += 1 continue } $items = $ewsService.LoadPropertiesForItems($itemResults, $PropertySet) foreach ($item in $items) { if ($null -ne $StartTimeFilter -and $item.Item.DateTimeReceived -lt $StartTimeFilter) { continue } if ($null -ne $EndTimeFilter -and $item.Item.DateTimeReceived -gt $EndTimeFilter) { continue } if (-not $IdsProcessed.Contains($item.Item.Id) -and -not [System.String]::IsNullOrEmpty($item.Item.ExtendedProperties[0].Value)) { CreateCustomCSV -Mailbox $mailAddress -Data $item.Item -CsvPath $csvFileName $rowCount ++ if ($rowCount -ge $MaxCSVLength) { Write-Host ("The csv file has reached it's maximum limit of $MaxCSVLength rows... aborting... Please apply appropriate filters to reduce the result size") Write-Host ("Please find the audit results in $csvFileName created in the current folder.") exit } [void]$IdsProcessed.Add($item.Item.Id) } } $mailboxProcessed ++ } } } else { foreach ($mailAddress in $mailAddresses) { $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("Scanning $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) if ($null -eq $userMailbox) { Write-Host ("Unable to get mailbox associated with mail address $mailAddress") $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) $rootFolderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $userMailbox) $rootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ewsService, $rootFolderId) # Create a new ArrayList to hold the folder $foldersList = New-Object System.Collections.ArrayList GetSubFolders -Folder $rootFolder -FoldersList $foldersList } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } $IdsProcessed = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($folder in $foldersList) { try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) $results = $ewsService.FindItems($folder.Id, $searchFilterCollection, $itemView) if ($null -ne $results -and $null -ne $results.Items -and $results.Items.Count -gt 0) { $items = $ewsService.LoadPropertiesForItems($results.Items, $PropertySet) } else { continue } foreach ($item in $items) { if (-not $IdsProcessed.Contains($item.Item.Id) -and -not [System.String]::IsNullOrEmpty($item.Item.ExtendedProperties[0].Value)) { CreateCustomCSV -Mailbox $mailAddress -Data $item.Item -CsvPath $csvFileName $rowCount ++ if ($rowCount -ge $MaxCSVLength) { Write-Host ("The csv file has reached it's maximum limit of $MaxCSVLength rows... aborting... Please apply appropriate filters to reduce the result size") Write-Host ("Please find the audit results in $csvFileName created in the current folder.") exit } [void]$IdsProcessed.Add($item.Item.Id) } } } catch { Write-Host "Error while scanning $($folder.DisplayName) of the mailbox $mailAddress. Inner Exception:`n`n$_" -ForegroundColor Red } } $mailboxProcessed ++ } } if (-not $SearchFolderCleanup) { if ($rowCount -eq 0) { Write-Host "No vulnerable item found" -ForegroundColor Green } else { Write-Host ("Please find the audit results in $csvFileName created in the current folder.") } } } else { $params = @{ Message = "Display Warning about Store operation" Target = "The script will perform store operation on mailboxes using EWS" Operation = "" } if ($CleanupAction -eq "ClearProperty") { $params.Operation = "Clear the PidLidReminderFileParameter property of mail items" } if ($CleanupAction -eq "ClearItem") { $params.Operation = "Delete items" } Show-Disclaimer @params $importedCsvData = Import-Csv $CleanupInfoFilePath -Delimiter "," if ($null -ne $importedCsvData) { $headerLine = ($importedCsvData[0] | Get-Member | Where-Object { ($_.MemberType -eq "NoteProperty") }).Name if ($headerLine.Count -eq 1) { # Csv file which isn't in the expected format has only 1 NoteProperty after being imported Write-Host "Csv file isn't in the expected format and must be normalized" try { # Process line by line and pipe to ConvertFrom-Csv to convert data to PSObject type $cleanupCSV = $importedCsvData | ForEach-Object { ($_.$headerLine) } | ConvertFrom-Csv -Header ((($headerLine).Replace('"', "")).Split(",")) -ErrorAction Stop } catch { Write-Host "Something went wrong while normalizing the csv file. Inner Exception:`n`n$_" -ForegroundColor Red exit } } elseif ($headerLine.Count -eq 9) { # Csv file which is in the expected format has 9 NoteProperties after being imported Write-Host "Csv file is in the expected format and can be used for cleanup" $cleanupCSV = $importedCsvData } else { # Everything else is unexpected and we stop processing here Write-Host "Unable to normalize the csv file please reach out to Microsoft support" -ForegroundColor Red exit } } else { Write-Host "Something went wrong while importing the csv file" -ForegroundColor Red exit } $entryCount = 0 foreach ($entry in $cleanupCSV) { $entryCount ++ if ([System.String]::IsNullOrEmpty($entry.Id)) { Write-Host ("No Id present for entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } if ([System.String]::IsNullOrEmpty($entry.Mailbox)) { Write-Host ("No Mailbox address present for entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } if ($null -ne $entry.Cleanup -and $entry.Cleanup.ToLower() -eq "y") { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $entry.Mailbox) $item = FindItem -ExchangeService $ewsService -Id $entry.Id if ($null -ne $item) { try { if ($CleanupAction -eq "ClearItem") { $item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) } else { if (-not $item.RemoveExtendedProperty($mailInfo["PidLidReminderFileParameter"])) { Write-Host ("Property already cleared on entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Yellow $invalidEntries.Add($entryCount) continue } CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $entry.Mailbox) if ($entry.ItemType -eq "Calendar") { $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite, [Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone) } else { $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) } } # If we get here, it should be successful. $removedEntries.Add($entryCount) } catch { Write-Host ("Unable to perform cleanup action on entry number: $entryCount, Line number: $($entryCount + 1) Inner Exception`n`n$_") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } } else { Write-Host ("Unable to find item associated to entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } } } Write-Host "Successfully removed $($removedEntries.Count) of $($cleanupCSV.Count)" if ($removedEntries.Count -eq 0) { Write-Host "No entries were removed. Please update the Cleanup column for the items you wish to cleanup." -ForegroundColor Yellow } else { Write-Host "Completed cleanup operation!" } } if ($mode -eq "Audit" -and $null -ne $failedMailboxes -and $failedMailboxes.Count -gt 0) { Write-Host ("Couldn't Audit mailboxes: {0}" -f [string]::Join(", ", $failedMailboxes)) } if ($mode -eq "Cleanup" -and $null -ne $invalidEntries -and $invalidEntries.Count -gt 0) { Write-Host ("Couldn't Cleanup the entries: {0}" -f [string]::Join(", ", $invalidEntries)) } Write-Host "" Write-Host ("Do you have feedback regarding the script? Please email ExToolsFeedback@microsoft.com.") -ForegroundColor Green Write-Host "" Remove-Module -Name "Microsoft.Exchange.WebServices" -ErrorAction SilentlyContinue }