diff --git a/.build/Build.ps1 b/.build/Build.ps1 index b9eb11e2df..ee98dd59db 100644 --- a/.build/Build.ps1 +++ b/.build/Build.ps1 @@ -91,7 +91,7 @@ $unreferencedScriptFiles = @($allScriptFiles | Where-Object { } return $true - }); + }) $unreferencedSharedScriptFiles = @($unreferencedScriptFiles | Where-Object { $_.StartsWith("$repoRoot\Shared\") diff --git a/.build/CodeFormatterChecks/CheckScriptFileHasBOM.ps1 b/.build/CodeFormatterChecks/CheckScriptFileHasBOM.ps1 index 90c480e7c8..984c4f03b7 100644 --- a/.build/CodeFormatterChecks/CheckScriptFileHasBOM.ps1 +++ b/.build/CodeFormatterChecks/CheckScriptFileHasBOM.ps1 @@ -38,7 +38,7 @@ function CheckScriptFileHasBOM { Write-Host "Added BOM: $($FileInfo.FullName)" $false } catch { - Write-Warning "File has no BOM and couldn't be fixed automatically: $($FileInfo.FullName). Exception: $($_.Exception)"; + Write-Warning "File has no BOM and couldn't be fixed automatically: $($FileInfo.FullName). Exception: $($_.Exception)" $true } } else { diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index bbbacd3166..5b9a2bff5f 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -158,4 +158,5 @@ vuln wbxml Webex Weve +wevtutil windir diff --git a/Admin/Find-AmbiguousSids.ps1 b/Admin/Find-AmbiguousSids.ps1 index 79eb24d734..a4f51aca75 100644 --- a/Admin/Find-AmbiguousSids.ps1 +++ b/Admin/Find-AmbiguousSids.ps1 @@ -61,8 +61,8 @@ begin { Write-Host "Using GC $GCName" $ldapConn = New-Object System.DirectoryServices.Protocols.LdapConnection("$($GCName):3268") $searchReq = New-Object System.DirectoryServices.Protocols.SearchRequest("", $filter, "Subtree", $null) - $prc = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize); - [void]$searchReq.Controls.Add($prc); + $prc = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize) + [void]$searchReq.Controls.Add($prc) $sidProperties = @("objectSid", "sidHistory", "msExchMasterAccountSid") $containersToIgnore = @(",CN=WellKnown Security Principals,", ",CN=Builtin,", ",CN=ForeignSecurityPrincipals,") @@ -99,7 +99,7 @@ process { $objectsProcessed++ } - } while ($prc.Cookie.Length -gt 0); + } while ($prc.Cookie.Length -gt 0) } end { diff --git a/Admin/Update-Engines.ps1 b/Admin/Update-Engines.ps1 index 5d83a10675..c8e92c0dc1 100644 --- a/Admin/Update-Engines.ps1 +++ b/Admin/Update-Engines.ps1 @@ -108,7 +108,7 @@ function ExtractCab($sourceCabPath, $destinationDirectory) { # Process each item in the cab. Determine if the destination # is a sub directory and create if necessary. for ($i=0; $i -lt $itemCount; $i++) { - $lastPathIndex = $source.item($i).Path.LastIndexOf("\"); + $lastPathIndex = $source.item($i).Path.LastIndexOf("\") # If the file inside the zip file should be extracted # to a subfolder, then we need to reset the destination diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 new file mode 100644 index 0000000000..66273ff5e7 --- /dev/null +++ b/Calendar/Check-SharingStatus.ps1 @@ -0,0 +1,326 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# .DESCRIPTION +# This script runs a variety of cmdlets to establish a baseline of the sharing status of a Calendar. +# +# .PARAMETER Identity +# Owner Mailbox to query, owner of the Mailbox sharing the calendar. +# Receiver of the shared mailbox, often the Delegate. +# +# .EXAMPLE +# Check-SharingStatus.ps1 -Owner Owner@contoso.com -Receiver Receiver@contoso.com + +# Define the parameters +[CmdletBinding()] +param( + [Parameter(Mandatory=$true)] + [string]$Owner, + [Parameter(Mandatory=$true)] + [string]$Receiver +) + +$BuildVersion = "" + +. $PSScriptRoot\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 + +if (Test-ScriptVersion -AutoUpdate) { + # Update was downloaded, so stop here. + Write-Host "Script was updated. Please rerun the command." -ForegroundColor Yellow + return +} + +Write-Verbose "Script Versions: $BuildVersion" + +$script:PIIAccess = $true #Assume we have PII access until we find out otherwise + +<# +.SYNOPSIS + Formats the CalendarSharingInvite logs from Export-MailboxDiagnosticLogs for a given identity. +.DESCRIPTION + This function processes calendar sharing accept logs for a given identity and outputs the most recent update for each recipient. +.PARAMETER Identity + The SMTP Address for which to process calendar sharing accept logs. +#> +function ProcessCalendarSharingInviteLogs { + param ( + [string]$Identity + ) + + # Define the header row + $header = "Timestamp", "Mailbox", "Entry MailboxOwner", "Recipient", "RecipientType", "SharingType", "DetailLevel" + $csvString = @() + $csvString = $header -join "," + $csvString += "`n" + + Write-Output "------------------------" + Write-Output "Looking for modern calendar sharing accept data for [$Identity]." + + # Call the Export-MailboxDiagnosticLogs cmdlet and store the output in a variable + try { + # Call the Export-MailboxDiagnosticLogs cmdlet and store the output in a variable + # -ErrorAction is not supported on Export-MailboxDiagnosticLogs + # $logOutput = Export-MailboxDiagnosticLogs $Identity -ComponentName CalendarSharingInvite -ErrorAction SilentlyContinue + + $logOutput = Export-MailboxDiagnosticLogs $Identity -ComponentName CalendarSharingInvite + } catch { + # Code to run if an error occurs + Write-Error "An error occurred: $_" + } + + # check if the output is empty + if ($null -eq $logOutput.MailboxLog) { + Write-Output "No data found for [$Identity]." + return + } + + $logLines =@() + # Split the output into an array of lines + $logLines = $logOutput.MailboxLog -split "`r`n" + + # Loop through each line of the output + foreach ($line in $logLines) { + if ($line -like "*RecipientType*") { + $csvString += $line + "`n" + } + } + + # Clean up output + $csvString = $csvString.Replace("Mailbox: ", "") + $csvString = $csvString.Replace("Entry MailboxOwner:", "") + $csvString = $csvString.Replace("Recipient:", "") + $csvString = $csvString.Replace("RecipientType:", "") + $csvString = $csvString.Replace("Handler=", "") + $csvString = $csvString.Replace("ms-exchange-", "") + $csvString = $csvString.Replace("DetailLevel=", "") + + # Convert the CSV string to an object + $csvObject = $csvString | ConvertFrom-Csv + + # Access the values as properties of the object + foreach ($row in $csvObject) { + Write-Debug "$($row.Recipient) - $($row.SharingType) - $($row.detailLevel)" + } + + #Filter the output to get the most recent update foreach recipient + $mostRecentRecipients = $csvObject | Sort-Object Recipient -Unique | Sort-Object Timestamp -Descending + + # Output the results to the console + Write-Output "User [$Identity] has shared their calendar with the following recipients:" + Write-Output $mostRecentRecipients | Format-Table -a Timestamp, Recipient, SharingType, DetailLevel +} + +<# +.SYNOPSIS + Formats the AcceptCalendarSharingInvite logs from Export-MailboxDiagnosticLogs for a given identity. +.DESCRIPTION + This function processes calendar sharing invite logs. +.PARAMETER Identity + The SMTP Address for which to process calendar sharing accept logs. +#> +function ProcessCalendarSharingAcceptLogs { + param ( + [string]$Identity + ) + + # Define the header row + $header = "Timestamp", "Mailbox", "SharedCalendarOwner", "FolderName" + $csvString = @() + $csvString = $header -join "," + $csvString += "`n" + + Write-Output "------------------------" + Write-Output "Looking for Modern Calendar Sharing Accept data for [$Identity]." + + # Call the Export-MailboxDiagnosticLogs cmdlet and store the output in a variable + try { + # Call the Export-MailboxDiagnosticLogs cmdlet and store the output in a variable + # -ErrorAction is not supported on Export-MailboxDiagnosticLogs + # $logOutput = Export-MailboxDiagnosticLogs $Identity -ComponentName AcceptCalendarSharingInvite -ErrorAction SilentlyContinue + + $logOutput = Export-MailboxDiagnosticLogs $Identity -ComponentName AcceptCalendarSharingInvite + } catch { + # Code to run if an error occurs + Write-Error "An error occurred: $_" + } + + # check if the output is empty + if ($null -eq $logOutput.MailboxLog) { + Write-Output "No AcceptCalendarSharingInvite Logs found for [$Identity]." + return + } + + $logLines =@() + # Split the output into an array of lines + $logLines = $logOutput.MailboxLog -split "`r`n" + + # Loop through each line of the output + foreach ($line in $logLines) { + if ($line -like "*CreateInternalSharedCalendarGroupEntry*") { + $csvString += $line + "`n" + } + } + + # Clean up output + $csvString = $csvString.Replace("Mailbox: ", "") + $csvString = $csvString.Replace("Entry MailboxOwner:", "") + $csvString = $csvString.Replace("Entry CreateInternalSharedCalendarGroupEntry: ", "") + $csvString = $csvString.Replace("Creating a shared calendar for ", "") + $csvString = $csvString.Replace("calendar name ", "") + + # Convert the CSV string to an object + $csvObject = $csvString | ConvertFrom-Csv + + # Access the values as properties of the object + foreach ($row in $csvObject) { + Write-Debug "$($row.Timestamp) - $($row.SharedCalendarOwner) - $($row.FolderName) " + } + + # Filter the output to get the most recent update for each recipient + # $mostRecentSharedCalendars = $csvObject |sort-object SharedCalendarOwner -Unique | Sort-Object Timestamp -Descending + + # Output the results to the console + Write-Host "User [$Identity] has accepted copies of the shared calendar from the following recipients on these dates:" + #Write-Host $csvObject | Format-Table -a Timestamp, SharedCalendarOwner, FolderName + $csvObject | Format-Table -a Timestamp, SharedCalendarOwner, FolderName +} + +<# +.SYNOPSIS + Display Calendar Owner information. +.DESCRIPTION + This function displays key Calendar Owner information. +.PARAMETER Identity + The SMTP Address for Owner of the shared calendar. +#> +function GetOwnerInformation { + param ( + [string]$Owner + ) + #Standard Owner information + Write-Host -ForegroundColor DarkYellow "------------------------------------------------" + Write-Host -ForegroundColor DarkYellow "Key Owner Mailbox Information:" + $script:OwnerMB = Get-Mailbox $Owner + # Write-Host "`t DisplayName:" $script:OwnerMB.DisplayName + # Write-Host "`t Database:" $script:OwnerMB.Database + # Write-Host "`t ServerName:" $script:OwnerMB.ServerName + # Write-Host "`t LitigationHoldEnabled:" $script:OwnerMB.LitigationHoldEnabled + # Write-Host "`t CalendarVersionStoreDisabled:" $script:OwnerMB.CalendarVersionStoreDisabled + # Write-Host "`t CalendarRepairDisabled:" $script:OwnerMB.CalendarRepairDisabled + # Write-Host "`t RecipientTypeDetails:" $script:OwnerMB.RecipientTypeDetails + # Write-Host "`t RecipientType:" $script:OwnerMB.RecipientType + Get-Mailbox $Owner | Format-List DisplayName, Database, ServerName, LitigationHoldEnabled, CalendarVersionStoreDisabled, CalendarRepairDisabled, RecipientType* + + if ($null -eq $script:OwnerMB) { + Write-Host -ForegroundColor Red "Could not find Owner Mailbox [$Owner]." + exit + } + + Write-Host -ForegroundColor DarkYellow "Send on Behalf Granted to :" + foreach ($del in $($script:OwnerMB.GrantSendOnBehalfTo)) { + Write-Host -ForegroundColor Blue "`t$($del)" + } + Write-Host "`n`n`n" + + if ($script:OwnerMB.DisplayName -like "Redacted*") { + Write-Host -ForegroundColor Yellow "Do Not have PII information for the Owner." + Write-Host -ForegroundColor Yellow "Get PII Access for $($script:OwnerMB.Database)." + $script:PIIAccess = $false + } + + Write-Host -ForegroundColor DarkYellow "Owner Calendar Folder Statistics:" + $OwnerCalendar = Get-MailboxFolderStatistics -Identity $Owner -FolderScope Calendar + $OwnerCalendarName = ($OwnerCalendar | Where-Object FolderPath -EQ "/Calendar").Name + + Get-MailboxFolderStatistics -Identity $Owner -FolderScope Calendar | Format-Table -a FolderPath, ItemsInFolder, FolderAndSubfolderSize + + Write-Host -ForegroundColor DarkYellow "Owner Calendar Permissions:" + Get-mailboxFolderPermission "${Owner}:\$OwnerCalendarName" | Format-Table -a User, AccessRights, SharingPermissionFlags + + Write-Host -ForegroundColor DarkYellow "Owner Root MB Permissions:" + Get-mailboxPermission $Owner | Format-Table -a User, AccessRights, SharingPermissionFlags + + # Write-Host -ForegroundColor DarkYellow "Owner Recoverable Items Folder Statistics: " + # Get-MailboxFolderStatistics -Identity $Owner -FolderScope RecoverableItems | Where-Object FolderPath -Like *Calendar* | Format-Table FolderPath, ItemsInFolder, FolderAndSubfolderSize + + Write-Host -ForegroundColor DarkYellow "Owner Modern Sharing Sent Invites" + ProcessCalendarSharingInviteLogs -Identity $Owner +} + +<# +.SYNOPSIS + Displays key information from the receiver of the shared Calendar. +.DESCRIPTION + This function displays key Calendar Receiver information. +.PARAMETER Identity + The SMTP Address for Receiver of the shared calendar. +#> +function GetReceiverInformation { + param ( + [string]$Receiver + ) + #Standard Receiver information + Write-Host -ForegroundColor Cyan "`r`r`r------------------------------------------------" + Write-Host -ForegroundColor Cyan "Key Receiver Information: [$Receiver]" + Get-Mailbox $Receiver | Format-List DisplayName, Database, LitigationHoldEnabled, CalendarVersionStoreDisabled, CalendarRepairDisabled, RecipientType* + + Write-Host -ForegroundColor Cyan "Receiver Calendar Folders (look for a copy of [$Owner] Calendar):" + $CalStats = Get-MailboxFolderStatistics -Identity $Receiver -FolderScope Calendar + $CalStats | Format-Table -a FolderPath, ItemsInFolder, FolderAndSubfolderSize + $ReceiverCalendarName = ($CalStats | Where-Object FolderType -EQ "Calendar").Name + + if ($CalStats | Where-Object Name -Like $owner* ) { + Write-Host -ForegroundColor Yellow "Looks like we might have found a copy of the Owner Calendar in the Receiver Calendar." + $CalStats | Where-Object Name -Like $owner* | Format-Table -a FolderPath, ItemsInFolder, FolderAndSubfolderSize + if (($CalStats | Where-Object Name -Like $owner*).count -gt 1) { + Write-Host -ForegroundColor Yellow "Warning :Might have found more than one copy of the Owner Calendar in the Receiver Calendar." + } + } else { + Write-Host -ForegroundColor Yellow "Warning: Could not Identify the Owner Calendar in the Receiver Calendar." + } + + if ($ReceiverCalendarName -like "REDACTED-*" ) { + Write-Host -ForegroundColor Yellow "Do Not have PII information for the Receiver" + $script:PIIAccess = $false + } + + Write-Host -ForegroundColor Cyan "`n`nReceiver Accepted the Following Modern Calendar Sharing Accept Logs:" + ProcessCalendarSharingAcceptLogs -Identity $Receiver + + if (Get-Command -Name Get-CalendarEntries -ErrorAction SilentlyContinue) { + Write-Verbose "Found Get-CalendarEntries cmdlet. Running cmdlet: Get-CalendarEntries -Identity $Receiver" + # ToDo: Check each value for proper sharing permissions (i.e. $X.CalendarSharingPermissionLevel -eq "ReadWrite" ) + $ReceiverCalEntries = Get-CalendarEntries -Identity $Receiver + Write-Host "CalendarGroupName : $($ReceiverCalEntries.CalendarGroupName)" + Write-Host "CalendarName : $($ReceiverCalEntries.CalendarName)" + Write-Host "OwnerEmailAddress : $($ReceiverCalEntries.OwnerEmailAddress)" + Write-Host "SharingModelType: $($ReceiverCalEntries.SharingModelType)" + Write-Host "IsOrphanedEntry: $($ReceiverCalEntries.IsOrphanedEntry)" + + # need to check if Get-CalendarValidationResult in the PS Workspace + if ((Get-Command -Name Get-CalendarValidationResult -ErrorAction SilentlyContinue) -and + $null -ne $ReceiverCalEntries) { + Write-Host "Running cmdlet: Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $($ReceiverCalEntries[0].LocalFolderId) -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1" + $ewsId_del= $ReceiverCalEntries[0].LocalFolderId + Get-CalendarValidationResult -Version V2 -Identity $Receiver -SourceCalendarId $ewsId_del -TargetUserId $Owner -IncludeAnalysis 1 -OnlyReportErrors 1 + } + } + + if ($script:PIIAccess) { + Write-Host "Checking for Owner copy Calendar in Receiver Calendar:" + Write-Host "Running cmdlet:" + Write-Host -NoNewline -ForegroundColor Yellow "Get-MailboxCalendarFolder -Identity ${Receiver}:\$ReceiverCalendarName\$($script:OwnerMB.DisplayName)" + try { + Get-MailboxCalendarFolder -Identity "${Receiver}:\$ReceiverCalendarName\$($script:OwnerMB.DisplayName)" | Format-List Identity, CreationTime, ExtendedFolderFlags, ExtendedFolderFlags2, CalendarSharingFolderFlags, CalendarSharingOwnerSmtpAddress, CalendarSharingPermissionLevel, SharingLevelOfDetails, SharingPermissionFlags, LastAttemptedSyncTime, LastSuccessfulSyncTime, SharedCalendarSyncStartDate + } catch { + Write-Error "Failed to get the Owner Calendar from the Receiver Mailbox. This is fine if not using Modern Sharing." + } + } else { + Write-Host "Do Not have PII information for the Receiver." + Write-Host "Get PII Access for $($script:OwnerMB.Database)." + } +} + +# Main +GetOwnerInformation -Owner $Owner +GetReceiverInformation -Receiver $Receiver diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index f5f6a1691d..f41d90668f 100644 --- a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 +++ b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 @@ -6,25 +6,26 @@ # as well as the Calendar Diagnostic Objects in CSV format. # # .PARAMETER Identity -# Address of EXO User Mailbox to query +# One or more SMTP Address of EXO User Mailbox to query. # # .PARAMETER Subject -# Subject of the meeting to query +# Subject of the meeting to query, only valid if Identity is a single user. # # .PARAMETER MeetingID -# The MeetingID of the meeting to query +# The MeetingID of the meeting to query. # # .EXAMPLE # Get-CalendarDiagnosticObjectsSummary.ps1 -Identity someuser@microsoft.com -MeetingID 040000008200E00074C5B7101A82E008000000008063B5677577D9010000000000000000100000002FCDF04279AF6940A5BFB94F9B9F73CD # # Get-CalendarDiagnosticObjectsSummary.ps1 -Identity someuser@microsoft.com -Subject "Test OneTime Meeting Subject" # +# Get-CalendarDiagnosticObjectsSummary.ps1 -Identity User1, User2, Delegate -MeetingID $MeetingID # [CmdletBinding(DefaultParameterSetName = 'Subject')] param ( [Parameter(Mandatory, Position = 0)] - [string]$Identity, + [string[]]$Identity, [Parameter(Mandatory, ParameterSetName = 'Subject', Position = 1)] [string]$Subject, @@ -43,7 +44,7 @@ $BuildVersion = "" if (Test-ScriptVersion -AutoUpdate -Confirm:$false) { # Update was downloaded, so stop here. - Write-Host "Script was updated. Please rerun the command." -ForegroundColor Yellow + Write-Host "Script was updated. Please rerun the command." -ForegroundColor Yellow return } @@ -74,9 +75,9 @@ $CustomPropertyNameList = "MapiStartTime", "NormalizedSubject", "SentRepresentingDisplayName", -"SentRepresentingEmailAddress"; +"SentRepresentingEmailAddress" -$LogLimit = 2000; +$LogLimit = 2000 $WellKnownCN_CA = "MICROSOFT SYSTEM ATTENDANT" $CalAttendant = "Calendar Assistant" @@ -138,28 +139,36 @@ $ResponseTypeOptions = @{ Run Get-CalendarDiagnosticObjects for passed in User with Subject or MeetingID. #> function GetCalendarDiagnosticObjects { + param( + [string]$Identity, + [string]$Subject, + [string]$MeetingID + ) - # Use MeetingID if we have it. - if ($Identity -and $MeetingID) { - Write-Verbose "Getting CalLogs for [$Identity] with MeetingID [$MeetingID]." - $script:InitialCDOs = Get-CalendarDiagnosticObjects -Identity $Identity -MeetingID $MeetingID -CustomPropertyNames $CustomPropertyNameList -WarningAction Ignore -MaxResults $LogLimit -ResultSize $LogLimit -ShouldBindToItem $true; + $params = @{ + Identity = $Identity + CustomPropertyName = $CustomPropertyNameList + WarningAction = "Ignore" + MaxResults = $LogLimit + ResultSize = $LogLimit + ShouldBindToItem = $true } - # Otherwise do a search on the subject. - if ($Identity -and $Subject -and !$MeetingID) { + if ($Identity -and $MeetingID) { + Write-Verbose "Getting CalLogs for [$Identity] with MeetingID [$MeetingID]." + $CalLogs = Get-CalendarDiagnosticObjects @params -MeetingID $MeetingID + } elseif ($Identity -and $Subject ) { Write-Verbose "Getting CalLogs for [$Identity] with Subject [$Subject]." - $script:InitialCDOs = Get-CalendarDiagnosticObjects -Identity $Identity -Subject $Subject -CustomPropertyNames $CustomPropertyNameList -WarningAction Ignore -MaxResults $LogLimit -ResultSize $LogLimit -ShouldBindToItem $true; + $CalLogs = Get-CalendarDiagnosticObjects @params -Subject $Subject # No Results, do a Deep search with ExactMatch. - if ($script:InitialCDOs.count -lt 1) { - $script:InitialCDOs = Get-CalendarDiagnosticObjects -Identity $Identity -Subject $Subject -ExactMatch $true -CustomPropertyNames $CustomPropertyNameList -WarningAction Ignore -MaxResults $LogLimit -ResultSize $LogLimit -ShouldBindToItem $true; + if ($CalLogs.count -lt 1) { + $CalLogs = Get-CalendarDiagnosticObjects @Params -Subject $Subject -ExactMatch $true } } - if ($Identity -and !$Subject -and !$MeetingID) { - Write-Warning "Can't run command with just Identity, either Subject or MeetingID must be provided."; - exit; - } + Write-Host "Found $($CalLogs.count) Calendar Logs for [$Identity]" + return $CalLogs } function FindMatch { @@ -168,7 +177,7 @@ function FindMatch { ) foreach ($Val in $PassedHash.keys) { if ($KeyInput -like "*$Val*") { - return $PassedHash[$Val]; + return $PassedHash[$Val] } } } @@ -189,27 +198,30 @@ function GetMailbox { ) try { - Write-Verbose "Searching Get-Mailbox $(if ($Organization -ne `"`" ) {"with Org: $Organization"}) for $Identity." - - # See if it is a Customer Tenant running the cmdlet. (They will not have access to Organization parameter) - $MSSupport = [Bool](Get-Help Get-Mailbox -Parameter Organization -ErrorAction SilentlyContinue) - Write-Verbose "MSSupport: $MSSupport" + Write-Verbose "Searching Get-Mailbox $(if (-not ([string]::IsNullOrEmpty($Organization))) {"with Org: $Organization"}) for $Identity." if ($Identity -and $Organization) { - if ($MSSupport) { - Write-Verbose "Using Organization parameter" - $GetMailboxOutput = Get-Mailbox -Identity $Identity -Organization $Organization -ErrorAction SilentlyContinue; + if ($script:MSSupport) { + Write-Verbose "Using Organization parameter" + $GetMailboxOutput = Get-Mailbox -Identity $Identity -Organization $Organization -ErrorAction SilentlyContinue } else { - Write-Verbose "Using -OrganizationalUnit parameter" - $GetMailboxOutput = Get-Mailbox -Identity $Identity -OrganizationalUnit $Organization -ErrorAction SilentlyContinue; + Write-Verbose "Using -OrganizationalUnit parameter" + $GetMailboxOutput = Get-Mailbox -Identity $Identity -OrganizationalUnit $Organization -ErrorAction SilentlyContinue } } else { - $GetMailboxOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue; + $GetMailboxOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue } if (!$GetMailboxOutput) { - Write-Host "Unable to find [$Identity] in Organization:[$Organization]" - return $null + Write-Host "Unable to find [$Identity]$(if ($Organization -ne `"`" ) {" in Organization:[$Organization]"})." + Write-Host "Trying to find a Group Mailbox for [$Identity]..." + $GetMailboxOutput = Get-Mailbox -Identity $Identity -ErrorAction SilentlyContinue -GroupMailbox + if (!$GetMailboxOutput) { + Write-Host "Unable to find a Group Mailbox for [$Identity] either." + return $null + } else { + Write-Verbose "Found GroupMailbox [$($GetMailboxOutput.DisplayName)]" + } } else { Write-Verbose "Found [$($GetMailboxOutput.DisplayName)]" } @@ -219,9 +231,9 @@ function GetMailbox { } else { Write-Verbose "Found [$($GetMailboxOutput.DisplayName)]" } - return $GetMailboxOutput; + return $GetMailboxOutput } catch { - Write-Error "An error occurred while running Get-Mailbox: [$_]"; + Write-Error "An error occurred while running Get-Mailbox: [$_]" } } @@ -231,32 +243,32 @@ function Convert-Data { [string[]] $ArrayNames, [switch ] $NoWarnings = $False ) - $ValidArrays = @(); - $ItemCounts = @(); - $VariableLookup = @{}; + $ValidArrays = @() + $ItemCounts = @() + $VariableLookup = @{} foreach ($Array in $ArrayNames) { try { - $VariableData = Get-Variable -Name $Array -ErrorAction Stop; - $VariableLookup[$Array] = $VariableData.Value; - $ValidArrays += $Array; - $ItemCounts += ($VariableData.Value | Measure-Object).Count; + $VariableData = Get-Variable -Name $Array -ErrorAction Stop + $VariableLookup[$Array] = $VariableData.Value + $ValidArrays += $Array + $ItemCounts += ($VariableData.Value | Measure-Object).Count } catch { if (!$NoWarnings) { - Write-Warning -Message "No variable found for [$Array]"; + Write-Warning -Message "No variable found for [$Array]" } } } - $MaxItemCount = ($ItemCounts | Measure-Object -Maximum).Maximum; - $FinalArray = @(); + $MaxItemCount = ($ItemCounts | Measure-Object -Maximum).Maximum + $FinalArray = @() for ($Inc = 0; $Inc -lt $MaxItemCount; $Inc++) { - $FinalObj = New-Object PsObject; + $FinalObj = New-Object PsObject foreach ($Item in $ValidArrays) { - $FinalObj | Add-Member -MemberType NoteProperty -Name $Item -Value $VariableLookup[$Item][$Inc]; + $FinalObj | Add-Member -MemberType NoteProperty -Name $Item -Value $VariableLookup[$Item][$Inc] } - $FinalArray += $FinalObj; + $FinalArray += $FinalObj } - return $FinalArray; - $FinalArray = @(); + return $FinalArray + $FinalArray = @() } <# @@ -292,7 +304,7 @@ function GetMailboxProp { } default { if ($null -ne $script:MailboxList[$PassedCN]) { - $ReturnValue = $script:MailboxList[$PassedCN].$Prop; + $ReturnValue = $script:MailboxList[$PassedCN].$Prop if ($null -eq $ReturnValue) { Write-Error "`t GetMailboxProp:$Prop :NotFound for ::[$PassedCN]" @@ -304,7 +316,7 @@ function GetMailboxProp { Write-Verbose "No PII Access for [$ReturnValue]" return BetterThanNothingCNConversion($PassedCN) } - return $ReturnValue; + return $ReturnValue } else { Write-Verbose "`t GetMailboxProp:$Prop :NotFound::$PassedCN" return BetterThanNothingCNConversion($PassedCN) @@ -338,19 +350,19 @@ function BetterThanNothingCNConversion { } if ($PassedCN -match 'cn=([\w,\s.@-]*[^/])$') { - $cNameMatch = $PassedCN -split "cn="; + $cNameMatch = $PassedCN -split "cn=" # Normally a readable name is sectioned off with a "-" at the end. # example /o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=d61149258ba04404adda42f336b504ed-Delegate if ($cNameMatch[-1] -match "-[\w* -.]*") { Write-Verbose "BetterThanNothingCNConversion: Returning : [$($cNameMatch[-1])]" - return $cNameMatch.split('-')[-1]; + return $cNameMatch.split('-')[-1] } # Sometimes we do not have the "-" in front of the Name. # example: "/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=user123" if ($cNameMatch[-1] -match "[\w* -.]*") { Write-Verbose "BetterThanNothingCNConversion: Returning : [$($cNameMatch[-1])]" - return $cNameMatch.split('-')[-1]; + return $cNameMatch.split('-')[-1] } } } @@ -420,8 +432,8 @@ function CreateExternalMasterIDMap { $AllFolderNames = @($script:GCDO | Where-Object { $_.ExternalSharingMasterId -eq $ExternalID } | Select-Object -ExpandProperty OriginalParentDisplayName | Select-Object -Unique) if ($AllFolderNames.count -gt 1) { - # We have 2+ FolderNames, Need to find the best one. #remove Calendar - $AllFolderNames = $AllFolderNames | Where-Object { $_ -notmatch 'Calendar' } # This will not work for non-english + # We have 2+ FolderNames, Need to find the best one. #remove Calendar + $AllFolderNames = $AllFolderNames | Where-Object { $_ -notmatch 'Calendar' } # This will not work for non-english } if ($AllFolderNames.Count -eq 0) { @@ -438,9 +450,9 @@ function CreateExternalMasterIDMap { Write-Host -ForegroundColor Red "Found $($AllFolderNames.count) possible folders" if ($AllFolderNames.Count -eq 2) { - $SharedFolders[$ExternalID] = $AllFolderNames[0] + $AllFolderNames[1] + $SharedFolders[$ExternalID] = $AllFolderNames[0] + $AllFolderNames[1] } else { - $SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy" + $SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy" } } } @@ -454,15 +466,15 @@ Creates a list of CN that are used in the Calendar Logs, Looks up the Mailboxes #> function ConvertCNtoSMTP { # Creates a list of CN's that we will do MB look up on - $CNEntries = @(); + $CNEntries = @() $CNEntries += ($script:GCDO.SentRepresentingEmailAddress.ToUpper() | Select-Object -Unique) $CNEntries += ($script:GCDO.ResponsibleUserName.ToUpper() | Select-Object -Unique) $CNEntries += ($script:GCDO.SenderEmailAddress.ToUpper() | Select-Object -Unique) $CNEntries = $CNEntries | Select-Object -Unique - Write-Verbose " Have $($CNEntries.count) CNEntries to look for..." + Write-Verbose "`t Have $($CNEntries.count) CNEntries to look for..." Write-Verbose "CNEntries: "; foreach ($CN in $CNEntries) { Write-Verbose $CN } - $Org = $script:MB.OrganizationalUnit.split('/')[-1]; + $Org = $script:MB.OrganizationalUnit.split('/')[-1] # Creates a Dictionary of MB's that we will use to look up the CN's Write-Verbose "Converting CN entries into SMTP Addresses..." @@ -473,7 +485,7 @@ function ConvertCNtoSMTP { } elseif ($CNEntry -match $WellKnownCN_Trans) { $MailboxList[$CNEntry] = $Transport } else { - $MailboxList[$CNEntry] = (GetMailbox -Identity $CNEntry -Organization $Org); + $MailboxList[$CNEntry] = (GetMailbox -Identity $CNEntry -Organization $Org) } } } @@ -486,29 +498,29 @@ function ConvertCNtoSMTP { <# .SYNOPSIS -Creates Friendly / short client names +Creates friendly / short client names from the ClientInfoString #> function CreateShortClientName { param( $ClientInfoString ) - $ShortClientName= @(); + $ShortClientName= @() # Map ClientInfoString to ShortClientName if (!$ClientInfoString) { - $ShortClientName = "NotFound"; + $ShortClientName = "NotFound" } if ($ClientInfoString -like "Client=EBA*" -or $ClientInfoString -like "Client=TBA*") { if ($ClientInfoString -like "*ResourceBookingAssistant*") { - $ShortClientName = "ResourceBookingAssistant"; + $ShortClientName = "ResourceBookingAssistant" } elseif ($ClientInfoString -like "*CalendarRepairAssistant*") { - $ShortClientName = "CalendarRepairAssistant"; + $ShortClientName = "CalendarRepairAssistant" } else { - $client = $ClientInfoString.Split(';')[0].Split('=')[-1]; - $Action = $ClientInfoString.Split(';')[1].Split('=')[-1]; - $Data = $ClientInfoString.Split(';')[-1]; - $ShortClientName = $client+":"+$Action+";"+$Data; + $client = $ClientInfoString.Split(';')[0].Split('=')[-1] + $Action = $ClientInfoString.Split(';')[1].Split('=')[-1] + $Data = $ClientInfoString.Split(';')[-1] + $ShortClientName = $client+":"+$Action+";"+$Data } } elseif ($ClientInfoString -like "Client=ActiveSync*") { if ($ClientInfoString -match 'UserAgent=(\w*-\w*)') { @@ -520,50 +532,50 @@ function CreateShortClientName { } } elseif ($ClientInfoString -like "Client=Rest*") { if ($ClientInfoString -like "*LocationAssistantProcessor*") { - $ShortClientName = "LocationProcessor"; + $ShortClientName = "LocationProcessor" } elseif ($ClientInfoString -like "*AppId=6326e366-9d6d-4c70-b22a-34c7ea72d73d*") { - $ShortClientName = "CalendarReplication"; + $ShortClientName = "CalendarReplication" } elseif ($ClientInfoString -like "*AppId=1e3faf23-d2d2-456a-9e3e-55db63b869b0*") { - $ShortClientName = "CiscoWebex"; + $ShortClientName = "CiscoWebex" } elseif ($ClientInfoString -like "*AppId=1c3a76cc-470a-46d7-8ba9-713cfbb2c01f*") { - $ShortClientName = "TimeService"; + $ShortClientName = "TimeService" } elseif ($ClientInfoString -like "*AppId=48af08dc-f6d2-435f-b2a7-069abd99c086*") { - $ShortClientName = "RestConnector"; + $ShortClientName = "RestConnector" } elseif ($ClientInfoString -like "*GriffinRestClient*") { - $ShortClientName = "GriffinRestClient"; + $ShortClientName = "GriffinRestClient" } elseif ($ClientInfoString -like "*NoUserAgent*") { - $ShortClientName = "RestUnknown"; + $ShortClientName = "RestUnknown" } elseif ($ClientInfoString -like "*MacOutlook*") { - $ShortClientName = "MacOutlookRest"; + $ShortClientName = "MacOutlookRest" } elseif ($ClientInfoString -like "*Microsoft Outlook 16*") { - $ShortClientName = "Outlook-ModernCalendarSharing"; + $ShortClientName = "Outlook-ModernCalendarSharing" } else { - $ShortClientName = "Rest"; + $ShortClientName = "Rest" } } else { - $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor; + $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor } if ($ClientInfoString -like "*InternalCalendarSharing*" -and $ClientInfoString -like "*OWA*") { - $ShortClientName = "Owa-ModernCalendarSharing"; + $ShortClientName = "Owa-ModernCalendarSharing" } if ($ClientInfoString -like "*InternalCalendarSharing*" -and $ClientInfoString -like "*MacOutlook*") { - $ShortClientName = "MacOutlook-ModernCalendarSharing"; + $ShortClientName = "MacOutlook-ModernCalendarSharing" } if ($ClientInfoString -like "*InternalCalendarSharing*" -and $ClientInfoString -like "*Outlook*") { - $ShortClientName = "Outlook-ModernCalendarSharing"; + $ShortClientName = "Outlook-ModernCalendarSharing" } if ($ClientInfoString -like "Client=ActiveSync*" -and $ClientInfoString -like "*Outlook*") { - $ShortClientName = "Outlook-ModernCalendarSharing"; + $ShortClientName = "Outlook-ModernCalendarSharing" } - return $ShortClientName; + return $ShortClientName } <# .SYNOPSIS Checks to see if the Calendar Log is Ignorable. -Many updates are not interesting in the Calendar Log, marking these as ignorable. 99% of the time this is correct. +Many updates are not interesting in the Calendar Log, marking these as ignorable. 99% of the time this is correct. #> function SetIsIgnorable { param( @@ -581,9 +593,9 @@ function SetIsIgnorable { -or $CalendarItemTypes.($CalLog.ItemClass) -eq "SharingDelete" ` -or $CalendarItemTypes.($CalLog.ItemClass) -eq "AttendeeList" ` -or $CalendarItemTypes.($CalLog.ItemClass) -eq "RespAny") { - return "True"; + return "True" } else { - return "False"; + return "False" } } @@ -644,44 +656,48 @@ function MapSharedFolder { Builds the CSV output from the Calendar Diagnostic Objects #> function BuildCSV { + param( + $Identity + ) + Write-Host "Starting to Process Calendar Logs..." - $GCDOResults = @(); - $IsFromSharedCalendar = @(); - $IsIgnorable = @(); - $script:MailboxList = @{}; + $GCDOResults = @() + $IsFromSharedCalendar = @() + $IsIgnorable = @() + $script:MailboxList = @{} Write-Host "Creating Map of Mailboxes to CN's..." - CreateExternalMasterIDMap; + CreateExternalMasterIDMap - $ThisMeetingID = $script:GCDO.CleanGlobalObjectId | Select-Object -Unique; - $ShortMeetingID = $ThisMeetingID.Substring($ThisMeetingID.length - 6); + $ThisMeetingID = $script:GCDO.CleanGlobalObjectId | Select-Object -Unique + $ShortMeetingID = $ThisMeetingID.Substring($ThisMeetingID.length - 6) - ConvertCNtoSMTP; + ConvertCNtoSMTP Write-Host "Making Calendar Logs more readable..." - $Index = 0; + $Index = 0 foreach ($CalLog in $script:GCDO) { - $CalLogACP = $CalLog.AppointmentCounterProposal.ToString(); - $Index++; - $ItemType = $CalendarItemTypes.($CalLog.ItemClass); - $ShortClientName = @(); - $script:KeyInput = $CalLog.ClientInfoString; - $ResponseType = $ResponseTypeOptions.($CalLog.ResponseType.ToString()); + $CalLogACP = $CalLog.AppointmentCounterProposal.ToString() + $Index++ + $ItemType = $CalendarItemTypes.($CalLog.ItemClass) + $ShortClientName = @() + $script:KeyInput = $CalLog.ClientInfoString + $ResponseType = $ResponseTypeOptions.($CalLog.ResponseType.ToString()) - $ShortClientName = CreateShortClientName($CalLog.ClientInfoString); + $ShortClientName = CreateShortClientName($CalLog.ClientInfoString) $IsIgnorable = SetIsIgnorable($CalLog) # CleanNotFounds; $PropsToClean = "FreeBusyStatus", "ClientIntent", "AppointmentLastSequenceNumber", "RecurrencePattern", "AppointmentAuxiliaryFlags", "IsOrganizerProperty", "EventEmailReminderTimer", "IsSeriesCancelled", "AppointmentCounterProposal", "MeetingRequestType" foreach ($Prop in $PropsToClean) { - $CalLog.$Prop = ReplaceNotFound($CalLog.$Prop); + $CalLog.$Prop = ReplaceNotFound($CalLog.$Prop) } if ($CalLogACP -eq "NotFound") { - $CalLogACP = ''; + $CalLogACP = '' } - $IsFromSharedCalendar = ($null -ne $CalLog.externalSharingMasterId -and $CalLog.externalSharingMasterId -ne "NotFound"); + $IsFromSharedCalendar = ($null -ne $CalLog.externalSharingMasterId -and $CalLog.externalSharingMasterId -ne "NotFound") # Need to ask about this $GetIsOrganizer = ($CalendarItemTypes.($CalLog.ItemClass) -eq "IpmAppointment" -and @@ -700,7 +716,7 @@ function BuildCSV { 'ItemClass' = $CalLog.ItemClass 'ItemVersion' = $CalLog.ItemVersion 'AppointmentSequenceNumber' = $CalLog.AppointmentSequenceNumber - 'AppointmentLastSequenceNumber' = $CalLog.AppointmentLastSequenceNumber # Need to find out how we can combine these two... + 'AppointmentLastSequenceNumber' = $CalLog.AppointmentLastSequenceNumber # Need to find out how we can combine these two... 'Organizer' = $CalLog.From.FriendlyDisplayName 'From' = GetBestFromAddress($CalLog.From) 'FreeBusyStatus' = $CalLog.FreeBusyStatus @@ -750,19 +766,27 @@ function BuildCSV { 'CleanGlobalObjectId' = $CalLog.CleanGlobalObjectId } } - $script:Results = $GCDOResults; + $script:Results = $GCDOResults # Automation won't have access to this file - will add code in next version to save contents to a variable #$Filename = "$($Results[0].ReceivedBy)_$ShortMeetingID.csv"; - $Filename = "$($Identity)_$ShortMeetingID.csv"; - $GCDOResults | Export-Csv -Path $Filename -NoTypeInformation - Write-Host "Calendar Logs for $Identity have been saved to $Filename." + + if ($Identity -like "*@*") { + $ShortName = $Identity.Split('@')[0] + } + $ShortName = $ShortName.Substring(0, [System.Math]::Min(20, $ShortName.Length)) + $Filename = "$($ShortName)_$ShortMeetingID.csv" + Write-Host -ForegroundColor Cyan -NoNewline "Calendar Logs for [$Identity] have been saved to :" + Write-Host -ForegroundColor Yellow "$Filename" $GCDOResults | Export-Csv -Path $Filename -NoTypeInformation -Encoding UTF8 - $MeetingTimeLine = $Results | Where-Object { $_.IsIgnorable -eq "False" } ; + $MeetingTimeLine = $Results | Where-Object { $_.IsIgnorable -eq "False" } Write-Host "`n`n`nThis is the meetingID $ThisMeetingID`nThis is Short MeetingID $ShortMeetingID" - Write-Host "Found $($script:GCDO.count) Log entries, Only $($MeetingTimeLine.count) entries will be analyzed."; - return; + if ($MeetingTimeLine.count -eq 0) { + Write-Host "All CalLogs are Ignorable, nothing to create a timeline with, displaying initial values." + } else { + Write-Host "Found $($script:GCDO.count) Log entries, only the $($MeetingTimeLine.count) Non-Ignorable entries will be analyzed in the TimeLine." + } } # =================================================================================================== @@ -777,53 +801,53 @@ function MeetingSummary { [switch] $ShortVersion ) - $InitialSubject = "Subject: " + $Entry.NormalizedSubject; - $InitialOrganizer = "Organizer: " + $Entry.SentRepresentingDisplayName; - $InitialSender = "Sender: " + $Entry.SentRepresentingDisplayName; - $InitialToList = "To List: " + $Entry.DisplayAttendeesAll; - $InitialLocation = "Location: " + $Entry.Location; + $InitialSubject = "Subject: " + $Entry.NormalizedSubject + $InitialOrganizer = "Organizer: " + $Entry.SentRepresentingDisplayName + $InitialSender = "Sender: " + $Entry.SentRepresentingDisplayName + $InitialToList = "To List: " + $Entry.DisplayAttendeesAll + $InitialLocation = "Location: " + $Entry.Location if ($ShortVersion -or $LongVersion) { - $InitialStartTime = "StartTime: " + $Entry.StartTime.ToString(); - $InitialEndTime = "EndTime: " + $Entry.EndTime.ToString(); + $InitialStartTime = "StartTime: " + $Entry.StartTime.ToString() + $InitialEndTime = "EndTime: " + $Entry.EndTime.ToString() } if ($longVersion -and ($Entry.Timezone -ne "")) { - $InitialTimeZone = "Time Zone: " + $Entry.Timezone; + $InitialTimeZone = "Time Zone: " + $Entry.Timezone } else { $InitialTimeZone = "Time Zone: Not Populated" } if ($Entry.AppointmentRecurring) { - $InitialRecurring = "Recurring: Yes - Recurring"; + $InitialRecurring = "Recurring: Yes - Recurring" } else { - $InitialRecurring = "Recurring: No - Single instance"; + $InitialRecurring = "Recurring: No - Single instance" } if ($longVersion -and $Entry.AppointmentRecurring) { - $InitialRecurrencePattern = "RecurrencePattern: " + $Entry.RecurrencePattern; - $InitialSeriesStartTime = "Series StartTime: " + $Entry.StartTime.ToString() + "Z"; - $InitialSeriesEndTime = "Series EndTime: " + $Entry.StartTime.ToString() + "Z"; + $InitialRecurrencePattern = "RecurrencePattern: " + $Entry.RecurrencePattern + $InitialSeriesStartTime = "Series StartTime: " + $Entry.StartTime.ToString() + "Z" + $InitialSeriesEndTime = "Series EndTime: " + $Entry.StartTime.ToString() + "Z" if (!$Entry.ViewEndTime) { - $InitialEndDate = "Meeting Series does not have an End Date."; + $InitialEndDate = "Meeting Series does not have an End Date." } } if (!$Time) { - $Time = $CalLog.LastModifiedTime.ToString(); + $Time = $CalLog.LastModifiedTime.ToString() } if (!$MeetingChanges) { - $MeetingChanges = @(); - $MeetingChanges += $InitialSubject, $InitialOrganizer, $InitialSender, $InitialToList, $InitialLocation, $InitialStartTime, $InitialEndTime, $InitialTimeZone, $InitialRecurring, $InitialRecurrencePattern, $InitialSeriesStartTime , $InitialSeriesEndTime , $InitialEndDate; + $MeetingChanges = @() + $MeetingChanges += $InitialSubject, $InitialOrganizer, $InitialSender, $InitialToList, $InitialLocation, $InitialStartTime, $InitialEndTime, $InitialTimeZone, $InitialRecurring, $InitialRecurrencePattern, $InitialSeriesStartTime , $InitialSeriesEndTime , $InitialEndDate } if ($ShortVersion) { - $MeetingChanges = @(); - $MeetingChanges += $InitialToList, $InitialLocation, $InitialStartTime, $InitialEndTime, $InitialRecurring; + $MeetingChanges = @() + $MeetingChanges += $InitialToList, $InitialLocation, $InitialStartTime, $InitialEndTime, $InitialRecurring } - Convert-Data -ArrayNames "Time", "MeetingChanges"; + Convert-Data -ArrayNames "Time", "MeetingChanges" } # =================================================================================================== @@ -839,7 +863,7 @@ function MeetingSummary { overview of what happened to the meeting. This can be use to get a quick overview of the meeting and then you can look into the CalLog in Excel to get more details. - The timeline will skip a lot of the noise (isIgnorable) in the CalLogs. It skips EBA (Event Based Assistants), + The timeline will skip a lot of the noise (isIgnorable) in the CalLogs. It skips EBA (Event Based Assistants), and other EXO internal processes, which are (99% of the time) not interesting to the end user and just setting hidden internal properties (i.e. things like HasBeenIndex, etc.) @@ -853,14 +877,21 @@ function MeetingSummary { I use a iterative approach to building this, so it will get better over time. #> function BuildTimeline { - [Array]$Header = ("Subject: " + ($script:GCDO[0].NormalizedSubject) + " | Display Name: " + ($script:GCDO[0].SentRepresentingDisplayName) + " | MeetingID: "+ ($script:GCDO[0].CleanGlobalObjectId)); - MeetingSummary -Time "Calendar Logs for Meeting with" -MeetingChanges $Header; - MeetingSummary -Time "Initial Message Values" -Entry $script:GCDO[0] -LongVersion; - $MeetingTimeLine = $Results | Where-Object { $_.IsIgnorable -eq "False" }; + param ( + [string] $Identity + ) + Write-DashLineBoxColor " TimeLine for [$Identity]:", + " Subject: $($script:GCDO[0].NormalizedSubject)", + " Organizer: $($script:GCDO[0].SentRepresentingDisplayName)", + " MeetingID: $($script:GCDO[0].CleanGlobalObjectId)" + [Array]$Header = ("Subject: " + ($script:GCDO[0].NormalizedSubject) + " | MeetingID: "+ ($script:GCDO[0].CleanGlobalObjectId)) + MeetingSummary -Time "Calendar Log Timeline for Meeting with" -MeetingChanges $Header + MeetingSummary -Time "Initial Message Values" -Entry $script:GCDO[0] -LongVersion + $MeetingTimeLine = $Results | Where-Object { $_.IsIgnorable -eq "False" } foreach ($CalLog in $MeetingTimeLine) { - [bool] $MeetingSummaryNeeded = $False; - [bool] $AddChangedProperties = $False; + [bool] $MeetingSummaryNeeded = $False + [bool] $AddChangedProperties = $False <# .SYNOPSIS @@ -875,23 +906,23 @@ function BuildTimeline { if ($CalLog.Client -ne "LocationProcessor" -or $CalLog.Client -notlike "EBA:*" -or $CalLog.Client -notlike "TBA:*") { if ($PreviousCalLog -and $AddChangedProperties) { if ($CalLog.MapiStartTime.ToString() -ne $PreviousCalLog.MapiStartTime.ToString()) { - [Array]$TimeLineText = "The StartTime changed from [$($PreviousCalLog.MapiStartTime)] to: [$($CalLog.MapiStartTime)]"; - MeetingSummary -Time " " -MeetingChanges $TimeLineText; + [Array]$TimeLineText = "The StartTime changed from [$($PreviousCalLog.MapiStartTime)] to: [$($CalLog.MapiStartTime)]" + MeetingSummary -Time " " -MeetingChanges $TimeLineText } if ($CalLog.MapiEndTime.ToString() -ne $PreviousCalLog.MapiEndTime.ToString()) { - [Array]$TimeLineText = "The EndTime changed from [$($PreviousCalLog.MapiEndTime)] to: [$($CalLog.MapiEndTime)]"; - MeetingSummary -Time " " -MeetingChanges $TimeLineText; + [Array]$TimeLineText = "The EndTime changed from [$($PreviousCalLog.MapiEndTime)] to: [$($CalLog.MapiEndTime)]" + MeetingSummary -Time " " -MeetingChanges $TimeLineText } if ($CalLog.SubjectProperty -ne $PreviousCalLog.SubjectProperty) { [Array]$TimeLineText = "The EndTime changed from [$($PreviousCalLog.SubjectProperty)] to: [$($CalLog.SubjectProperty)]" - MeetingSummary -Time " " -MeetingChanges $TimeLineText; + MeetingSummary -Time " " -MeetingChanges $TimeLineText } if ($CalLog.NormalizedSubject -ne $PreviousCalLog.NormalizedSubject) { [Array]$TimeLineText = "The EndTime changed from [$($PreviousCalLog.NormalizedSubject)] to: [$($CalLog.NormalizedSubject)]" - MeetingSummary -Time " " -MeetingChanges $TimeLineText; + MeetingSummary -Time " " -MeetingChanges $TimeLineText } if ($CalLog.Location -ne $PreviousCalLog.Location) { [Array]$TimeLineText = "The Location changed from [$($PreviousCalLog.Location)] to: [$($CalLog.Location)]" @@ -1006,40 +1037,40 @@ function BuildTimeline { Create { if ($CalLog.IsOrganizer) { if ($CalLog.IsException) { - $Output1 = "A new Exception $($CalLog.MeetingRequestType.Value) Meeting Request was created with $($CalLog.Client)"; + $Output1 = "A new Exception $($CalLog.MeetingRequestType.Value) Meeting Request was created with $($CalLog.Client)" } else { - $Output1 = "A new $($CalLog.MeetingRequestType.Value) Meeting Request was created with $($CalLog.Client)"; + $Output1 = "A new $($CalLog.MeetingRequestType.Value) Meeting Request was created with $($CalLog.Client)" } if ($CalLog.SentRepresentingEmailAddress -eq $CalLog.SenderEmailAddress) { - $Output2 = " by the Organizer $($CalLog.ResponsibleUser)."; + $Output2 = " by the Organizer $($CalLog.ResponsibleUser)." } else { - $Output2 = " by the Delegate."; + $Output2 = " by the Delegate." } - [array] $Output = $Output1+$Output2; - [bool] $MeetingSummaryNeeded = $True; + [array] $Output = $Output1+$Output2 + [bool] $MeetingSummaryNeeded = $True } else { if ($CalLog.DisplayAttendeesTo -ne $PreviousCalLog.DisplayAttendeesTo -or $CalLog.DisplayAttendeesCc -ne $PreviousCalLog.DisplayAttendeesCc) { - [array] $Output = "The user Forwarded a Meeting Request with $($CalLog.Client)."; + [array] $Output = "The user Forwarded a Meeting Request with $($CalLog.Client)." } else { if ($CalLog.Client -eq "Transport") { - [array] $Output = "Transport delivered a new Meeting Request from $($CalLog.SentRepresentingDisplayName)."; - [bool] $MeetingSummaryNeeded = $True; + [array] $Output = "Transport delivered a new Meeting Request from $($CalLog.SentRepresentingDisplayName)." + [bool] $MeetingSummaryNeeded = $True } else { - [array] $Output = "$($CalLog.ResponsibleUserName) sent a $($CalLog.MeetingRequestType.Value) update for the Meeting Request and was processed by $($CalLog.Client)."; + [array] $Output = "$($CalLog.ResponsibleUserName) sent a $($CalLog.MeetingRequestType.Value) update for the Meeting Request and was processed by $($CalLog.Client)." } } } } Update { - [array] $Output = "$($CalLog.ResponsibleUserName) updated on the $($CalLog.MeetingRequestType) Meeting Request with $($CalLog.Client)."; + [array] $Output = "$($CalLog.ResponsibleUserName) updated on the $($CalLog.MeetingRequestType) Meeting Request with $($CalLog.Client)." } MoveToDeletedItems { - [array] $Output = "$($CalLog.ResponsibleUserName) deleted the Meeting Request with $($CalLog.Client)."; + [array] $Output = "$($CalLog.ResponsibleUserName) deleted the Meeting Request with $($CalLog.Client)." } default { - [array] $Output = "$($CalLog.TriggerAction) was performed on the $($CalLog.MeetingRequestType) Meeting Request by $($CalLog.ResponsibleUserName) with $($CalLog.Client)."; + [array] $Output = "$($CalLog.TriggerAction) was performed on the $($CalLog.MeetingRequestType) Meeting Request by $($CalLog.ResponsibleUserName) with $($CalLog.Client)." } } } @@ -1051,7 +1082,7 @@ function BuildTimeline { } if ($CalLog.AppointmentCounterProposal -eq "True") { - [array] $Output = "$($CalLog.SentRepresentingDisplayName) send a $($MeetingRespType) response message with a New Time Proposal: $($CalLog.MapiStartTime) to $($CalLog.MapiEndTime)"; + [array] $Output = "$($CalLog.SentRepresentingDisplayName) send a $($MeetingRespType) response message with a New Time Proposal: $($CalLog.MapiStartTime) to $($CalLog.MapiEndTime)" } else { switch -Wildcard ($CalLog.TriggerAction) { "Update" { $Action = "updated" } @@ -1062,43 +1093,42 @@ function BuildTimeline { } } + $Extra = "" if ($CalLog.IsException) { $Extra = " to the meeting starting $($CalLog.StartTime)" - Write-Host -ForegroundColor Cyan "Extra: $Extra" } elseif ($CalLog.AppointmentRecurring) { $Extra = " to the meeting series" - Write-Host -ForegroundColor Cyan "Extra: $Extra" } if ($CalLog.IsOrganizer) { - [array] $Output = "$($CalLog.SentRepresentingDisplayName) $($Action) a $($MeetingRespType) Meeting Response message$($Extra)."; + [array] $Output = "$($CalLog.SentRepresentingDisplayName) $($Action) a $($MeetingRespType) Meeting Response message$($Extra)." } else { switch ($CalLog.Client) { RBA { - [array] $Output = "RBA $($Action) a $($MeetingRespType) Meeting Response message."; + [array] $Output = "RBA $($Action) a $($MeetingRespType) Meeting Response message." } Transport { - [array] $Output = "$($CalLog.SentRepresentingDisplayName) $($Action) $($MeetingRespType) Meeting Response message."; + [array] $Output = "$($CalLog.SentRepresentingDisplayName) $($Action) $($MeetingRespType) Meeting Response message." } default { - [array] $Output = "Meeting Response $($MeetingRespType) from [$($CalLog.SentRepresentingDisplayName)] was $($Action) by $($CalLog.ResponsibleUserName) with $($CalLog.Client)."; + [array] $Output = "Meeting Response $($MeetingRespType) from [$($CalLog.SentRepresentingDisplayName)] was $($Action) by $($CalLog.ResponsibleUserName) with $($CalLog.Client)." } } } } } ForwardNotification { - [array] $Output = "The meeting was FORWARDED by $($CalLog.SentRepresentingDisplayName)."; + [array] $Output = "The meeting was FORWARDED by $($CalLog.SentRepresentingDisplayName)." } ExceptionMsgClass { if ($CalLog.TriggerAction -eq "Create") { - $Action = "New"; + $Action = "New" } else { - $Action = "$($CalLog.TriggerAction)"; + $Action = "$($CalLog.TriggerAction)" } if ($CalLog.ResponsibleUser -ne "Calendar Assistant") { - [array] $Output = "$($Action) Exception to the meeting series added by $($CalLog.ResponsibleUser) with $($CalLog.Client)."; + [array] $Output = "$($Action) Exception to the meeting series added by $($CalLog.ResponsibleUser) with $($CalLog.Client)." } } IpmAppointment { @@ -1106,20 +1136,20 @@ function BuildTimeline { Create { if ($CalLog.IsOrganizer) { if ($CalLog.Client -eq "Transport") { - [array] $Output = "Transport created a new meeting."; + [array] $Output = "Transport created a new meeting." } else { - [array] $Output = "$($CalLog.SentRepresentingDisplayName) created a new Meeting with $($CalLog.Client)."; + [array] $Output = "$($CalLog.SentRepresentingDisplayName) created a new Meeting with $($CalLog.Client)." } } else { switch ($CalLog.Client) { Transport { - [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar."; + [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar." } RBA { - [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar."; + [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar." } default { - [array] $Output = "Meeting was created by [$($CalLog.ResponsibleUser)] with $($CalLog.Client)."; + [array] $Output = "Meeting was created by [$($CalLog.ResponsibleUser)] with $($CalLog.Client)." } } } @@ -1127,87 +1157,87 @@ function BuildTimeline { Update { switch ($CalLog.Client) { Transport { - [array] $Output = "Transport $($CalLog.TriggerAction)d the meeting from $($CalLog.SentRepresentingDisplayName)."; + [array] $Output = "Transport $($CalLog.TriggerAction)d the meeting from $($CalLog.SentRepresentingDisplayName)." } LocationProcessor { - [array] $Output = ""; + [array] $Output = "" } RBA { - [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting."; + [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting." } default { if ($CalLog.ResponsibleUser -eq "Calendar Assistant") { - [array] $Output = "The Exchange System $($CalLog.TriggerAction)d the meeting via the Calendar Assistant."; + [array] $Output = "The Exchange System $($CalLog.TriggerAction)d the meeting via the Calendar Assistant." } else { - [array] $Output = "$($CalLog.TriggerAction) to the Meeting by [$($CalLog.ResponsibleUserName)] with $($CalLog.Client)."; - $AddChangedProperties = $True; + [array] $Output = "$($CalLog.TriggerAction) to the Meeting by [$($CalLog.ResponsibleUserName)] with $($CalLog.Client)." + $AddChangedProperties = $True } } } if ($CalLog.FreeBusyStatus -eq 2 -and $PreviousCalLog.FreeBusyStatus -ne 2) { - [array] $Output = "$($CalLog.ResponsibleUserName) Accepted the meeting with $($CalLog.Client)."; - $AddChangedProperties = $False; + [array] $Output = "$($CalLog.ResponsibleUserName) Accepted the meeting with $($CalLog.Client)." + $AddChangedProperties = $False } elseif ($CalLog.FreeBusyStatus -ne 2 -and $PreviousCalLog.FreeBusyStatus -eq 2) { - [array] $Output = "$($CalLog.ResponsibleUserName) Declined the Meeting with $($CalLog.Client)."; - $AddChangedProperties = $False; + [array] $Output = "$($CalLog.ResponsibleUserName) Declined the Meeting with $($CalLog.Client)." + $AddChangedProperties = $False } } SoftDelete { switch ($CalLog.Client) { Transport { - [array] $Output = "Transport $($CalLog.TriggerAction)d the Meeting from $($CalLog.SentRepresentingDisplayName)."; + [array] $Output = "Transport $($CalLog.TriggerAction)d the Meeting from $($CalLog.SentRepresentingDisplayName)." } LocationProcessor { - [array] $Output = ""; + [array] $Output = "" } RBA { - [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting."; + [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting." } default { if ($CalLog.ResponsibleUser -eq "Calendar Assistant") { - [array] $Output = "The Exchange System $($CalLog.TriggerAction)s the meeting via the Calendar Assistant."; + [array] $Output = "The Exchange System $($CalLog.TriggerAction)s the meeting via the Calendar Assistant." } else { - [array] $Output = "The Meeting was $($CalLog.TriggerAction) by [$($CalLog.ResponsibleUserName)] with $($CalLog.Client)."; - $AddChangedProperties = $True; + [array] $Output = "The Meeting was $($CalLog.TriggerAction) by [$($CalLog.ResponsibleUserName)] with $($CalLog.Client)." + $AddChangedProperties = $True } } } if ($CalLog.FreeBusyStatus -eq 2 -and $PreviousCalLog.FreeBusyStatus -ne 2) { - [array] $Output = "The $($CalLog.ResponsibleUser) accepted the Meeting with $($CalLog.Client)."; - $AddChangedProperties = $False; + [array] $Output = "The $($CalLog.ResponsibleUser) accepted the Meeting with $($CalLog.Client)." + $AddChangedProperties = $False } elseif ($CalLog.FreeBusyStatus -ne 2 -and $PreviousCalLog.FreeBusyStatus -eq 2) { - [array] $Output = "The $($CalLog.ResponsibleUser) declined the Meeting with $($CalLog.Client)."; - $AddChangedProperties = $False; + [array] $Output = "The $($CalLog.ResponsibleUser) declined the Meeting with $($CalLog.Client)." + $AddChangedProperties = $False } } MoveToDeletedItems { - [array] $Output = "[$($CalLog.ResponsibleUser)] moved the Meeting to the Deleted Items with $($CalLog.Client)."; + [array] $Output = "[$($CalLog.ResponsibleUser)] moved the Meeting to the Deleted Items with $($CalLog.Client)." } default { - [array] $Output = "[$($CalLog.ResponsibleUser)] $($CalLog.TriggerAction) the Meeting with $($CalLog.Client)."; - [bool] $MeetingSummaryNeeded = $False; + [array] $Output = "[$($CalLog.ResponsibleUser)] $($CalLog.TriggerAction) the Meeting with $($CalLog.Client)." + [bool] $MeetingSummaryNeeded = $False } } } Cancellation { switch ($CalLog.Client) { Transport { - [array] $Output = "Transport $($CalLog.TriggerAction)d the Meeting Cancellation from $($CalLog.SentRepresentingDisplayName)."; + [array] $Output = "Transport $($CalLog.TriggerAction)d the Meeting Cancellation from $($CalLog.SentRepresentingDisplayName)." } default { - [array] $Output = "$($CalLog.ResponsibleUser) $($CalLog.TriggerAction) the Cancellation with $($CalLog.Client)."; + [array] $Output = "$($CalLog.ResponsibleUser) $($CalLog.TriggerAction) the Cancellation with $($CalLog.Client)." } } } default { if ($CalLog.TriggerAction -eq "Create") { - $Action = "New"; + $Action = "New" } else { - $Action = "$($CalLog.TriggerAction)"; + $Action = "$($CalLog.TriggerAction)" } - [array] $Output = "$($Action) was performed on the $($CalLog.ItemClass) by $($CalLog.ResponsibleUser) with $($CalLog.Client)."; + [array] $Output = "$($Action) was performed on the $($CalLog.ItemClass) by $($CalLog.ResponsibleUser) with $($CalLog.Client)." } } @@ -1216,85 +1246,205 @@ function BuildTimeline { if ($Output) { if ($MeetingSummaryNeeded) { - MeetingSummary -Time $Time -MeetingChanges $Output; - MeetingSummary -Time " " -ShortVersion -Entry $CalLog; + MeetingSummary -Time $Time -MeetingChanges $Output + MeetingSummary -Time " " -ShortVersion -Entry $CalLog } else { - MeetingSummary -Time $Time -MeetingChanges $Output; + MeetingSummary -Time $Time -MeetingChanges $Output if ($AddChangedProperties) { - ChangedProperties; + ChangedProperties } } } # Setup Previous log (if current logs is an IPM.Appointment) if ($CalendarItemTypes.($CalLog.ItemClass) -eq "IpmAppointment" -or $CalendarItemTypes.($CalLog.ItemClass) -eq "ExceptionMsgClass") { - $PreviousCalLog = $CalLog; + $PreviousCalLog = $CalLog } } - $Results = @(); + $Results = @() } -# =================================================================================================== -# Main -# =================================================================================================== +<# +.SYNOPSIS + Function to write a line of text surrounded by a dash line box. -if (Get-Command -Name Get-Mailbox -ErrorAction SilentlyContinue) { - Write-Host "Validated Get-Mailbox" -} else { - Write-Error "Get-Mailbox not found. Please validate that you are running this script from an Exchange Management Shell and try again." - Write-Host "Look at Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline." - exit; -} +.DESCRIPTION + The Write-DashLineBoxColor function is used to create a quick and easy display around a line of text. It generates a box made of dash characters ("-") and displays the provided line of text inside the box. + +.PARAMETER Line + Specifies the line of text to be displayed inside the dash line box. + +.PARAMETER Color + Specifies the color of the dash line box and the text. The default value is "White". -Write-Host "Checking for a valid mailbox..." -$script:MB = GetMailbox -Identity $Identity -if ($null -eq $script:MB) { - # -or $script:MB.GetType().FullName -ne "Microsoft.Exchange.Data.Directory.Management.Mailbox") { - Write-Host "`n`n`n============================================================================" - Write-Error "Mailbox [$Identity] not found on Exchange Online. Please validate the mailbox name and try again." - Write-Host "=======================================================================================" - #exit; +.PARAMETER DashChar + Specifies the character used to create the dash line. The default value is "-". + +.EXAMPLE + Write-DashLineBoxColor -Line "Hello, World!" -Color "Yellow" -DashChar "=" + Displays: + ============== + Hello, World! + ============== +#> +function Write-DashLineBoxColor { + [CmdletBinding()] + param( + [string[]]$Line, + [string] $Color = "White", + [char] $DashChar = "-" + ) + $highLineLength = 0 + $Line | ForEach-Object { if ($_.Length -gt $highLineLength) { $highLineLength = $_.Length } } + $dashLine = [string]::Empty + 1..$highLineLength | ForEach-Object { $dashLine += $DashChar } + Write-Host + Write-Host -ForegroundColor $Color $dashLine + $Line | ForEach-Object { Write-Host -ForegroundColor $Color $_ } + Write-Host -ForegroundColor $Color $dashLine + Write-Host } -# Get initial CalLogs (saved in $script:InitialCDOs) -Write-Host "Getting initial Calendar Logs..." -GetCalendarDiagnosticObjects; +<# +.SYNOPSIS +Checks the identities are EXO Mailboxes. +#> +function CheckIdentities { + if (Get-Command -Name Get-Mailbox -ErrorAction SilentlyContinue) { + Write-Host "Validated connection to Exchange Online." + } else { + Write-Error "Get-Mailbox cmdlet not found. Please validate that you are running this script from an Exchange Management Shell and try again." + Write-Host "Look at Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline." + exit + } -$GlobalObjectIds = @(); + # See if it is a Customer Tenant running the cmdlet. (They will not have access to Organization parameter) + $script:MSSupport = [Bool](Get-Help Get-Mailbox -Parameter Organization -ErrorAction SilentlyContinue) + Write-Verbose "MSSupport: $script:MSSupport" -# Find all the unique Global Object IDs -foreach ($ObjectId in $script:InitialCDOs.CleanGlobalObjectId) { - if (![string]::IsNullOrEmpty($ObjectId) -and - $ObjectId -ne "NotFound" -and - $ObjectId -ne "InvalidSchemaPropertyName" -and - $ObjectId.Length -ge 90) { - $GlobalObjectIds += $ObjectId; + Write-Host "Checking for at least one valid mailbox..." + $IdentityList = @() + + Write-Host "Preparing to check $($Identity.count) Mailbox(es)..." + + foreach ($Id in $Identity) { + $Account = GetMailbox -Identity $Id + if ($null -eq $Account) { + # -or $script:MB.GetType().FullName -ne "Microsoft.Exchange.Data.Directory.Management.Mailbox") { + Write-DashLineBoxColor "`n Error: Mailbox [$Id] not found on Exchange Online. Please validate the mailbox name and try again.`n" -Color Red + continue + } + if (CheckForNoPIIAccess $Account.DisplayName) { + Write-Host -ForegroundColor DarkRed "No PII access for Mailbox [$Id]. Falling back to SMTP Address." + $IdentityList += $ID + if ($null -eq $script:MB) { + $script:MB = $Account + } + } else { + Write-Host "Mailbox [$Id] found as : $($Account.DisplayName)" + $IdentityList += $Account.PrimarySmtpAddress.ToString() + if ($null -eq $script:MB) { + $script:MB = $Account + } + } } + + Write-Verbose "IdentityList: $IdentityList" + + if ($IdentityList.count -eq 0) { + Write-DashLineBoxColor "`n No valid mailboxes found. Please validate the mailbox name and try again. `n" Red + exit + } + + return $IdentityList } -$GlobalObjectIds = $GlobalObjectIds | Select-Object -Unique; +<# +.SYNOPSIS +This function retrieves calendar logs from the specified source with a subject that matches the provided criteria. +.PARAMETER Identity +The Identity of the mailbox to get calendar logs from. +.PARAMETER Subject +The subject of the calendar logs to retrieve. +#> +function GetCalLogsWithSubject { + param ( + [string] $Identity, + [string] $Subject + ) + Write-Host "Getting CalLogs based for [$Identity] with subject [$Subject]]" + + $InitialCDOs = GetCalendarDiagnosticObjects -Identity $Identity -Subject $Subject + $GlobalObjectIds = @() + + # Find all the unique Global Object IDs + foreach ($ObjectId in $InitialCDOs.CleanGlobalObjectId) { + if (![string]::IsNullOrEmpty($ObjectId) -and + $ObjectId -ne "NotFound" -and + $ObjectId -ne "InvalidSchemaPropertyName" -and + $ObjectId.Length -ge 90) { + $GlobalObjectIds += $ObjectId + } + } + + $GlobalObjectIds = $GlobalObjectIds | Select-Object -Unique + Write-Host "Found $($GlobalObjectIds.count) unique GlobalObjectIds." + Write-Host "Getting the set of CalLogs for each GlobalObjectID." + + if ($GlobalObjectIds.count -eq 1) { + $script:GCDO = $InitialCDOs; # use the CalLogs that we already have, since there is only one. + BuildCSV -Identity $Identity + BuildTimeline -Identity $Identity + }$ID + # Get the CalLogs for each MeetingID found. + if ($GlobalObjectIds.count -gt 1) { + Write-Host "Found multiple GlobalObjectIds: $($GlobalObjectIds.Count)." + foreach ($MID in $GlobalObjectIds) { + Write-DashLineBoxColor "Processing MeetingID: [$MID]" + $script:GCDO = GetCalendarDiagnosticObjects -Identity $Identity -MeetingID $MID + Write-Verbose "Found $($GCDO.count) CalLogs with MeetingID[$MID] ." + BuildCSV -Identity $Identity + BuildTimeline -Identity $Identity + } + } else { + Write-Warning "No CalLogs were found." + } +} + +# =================================================================================================== +# Main +# =================================================================================================== -# Get the CalLogs for each MeetingID found. -if ($GlobalObjectIds.count -gt 1) { - Write-Host "Found multiple GlobalObjectIds: $($GlobalObjectIds.Count)." - $GlobalObjectIds | ForEach-Object { - Write-Verbose "Processing MeetingID: $_" - $script:GCDO = Get-CalendarDiagnosticObjects -Identity $Identity -MeetingID $_ -CustomPropertyNames $CustomPropertyNameList -WarningAction Ignore -MaxResults $LogLimit -ResultSize $LogLimit -ShouldBindToItem $true; - BuildCSV; - BuildTimeline; +$ValidatedIdentities = CheckIdentities -Identity $Identity + +if (-not ([string]::IsNullOrEmpty($Subject)) ) { + if ($ValidatedIdentities.count -gt 1) { + Write-Warning "Multiple mailboxes were found, but only one is supported for Subject searches. Please specify a single mailbox." + exit + } + GetCalLogsWithSubject -Identity $ValidatedIdentities -Subject $Subject +} elseif (-not ([string]::IsNullOrEmpty($MeetingID))) { + + foreach ($ID in $ValidatedIdentities) { + #$script:GCDO = $script:InitialCDOs; # use the CalLogs that we already have, since there is only one. + # $script:InitialCDOs = @(); # clear the Initial CDOs. + Write-DashLineBoxColor "Looking for CalLogs from [$ID] with passed in MeetingID." + Write-Verbose "Running: Get-CalendarDiagnosticObjects -Identity [$ID] -MeetingID [$MeetingID] -CustomPropertyNames $CustomPropertyNameList -WarningAction Ignore -MaxResults $LogLimit -ResultSize $LogLimit -ShouldBindToItem $true;" + $script:GCDO = GetCalendarDiagnosticObjects -Identity $ID -MeetingID $MeetingID + + if ($script:GCDO.count -gt 0) { + Write-Host "Found $($script:GCDO.count) CalLogs with MeetingID [$MeetingID]." + BuildCSV -Identity $ID + BuildTimeline -Identity $ID + } else { + Write-Warning "No CalLogs were found for [$ID] with MeetingID [$MeetingID]." + } } -} elseif ($GlobalObjectIds.count -eq 1) { - $script:GCDO = $script:InitialCDOs; # use the CalLogs that we already have, since there is only one. - $script:InitialCDOs = @(); # clear the Initial CDOs. - BuildCSV; - BuildTimeline; } else { - Write-Warning "A valid meeting ID was not found, manually confirm the meetingID"; + Write-Warning "A valid MeetingID was not found, nor Subject. Please confirm the MeetingID or Subject and try again." } -Write-Host -ForegroundColor Yellow "`n`n`n============================================================================" -Write-Host -ForegroundColor Yellow "Hope this script was helpful in getting (and understanding) the Calendar Logs." -Write-Host -ForegroundColor Yellow "If you have issues or suggestion for this script," -Write-Host -ForegroundColor Yellow "`t please send them to " -Write-Host -ForegroundColor Yellow "============================================================================`n`n`n" +Write-DashLineBoxColor "Hope this script was helpful in getting and understanding the Calendar Logs.", +"If you have issues or suggestion for this script, please send them to: ", +"`t CalLogFormatterDevs@microsoft.com" -Color Yellow -DashChar = diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index 728844f9bd..48612c0f9b 100644 --- a/Calendar/Get-RBASummary.ps1 +++ b/Calendar/Get-RBASummary.ps1 @@ -38,15 +38,15 @@ function ValidateMailbox { # check we get a response if ($null -eq $script:Mailbox) { - Write-Host -ForegroundColor Red "Get-Mailbox returned null. Make sure you Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline. Exiting script."; - exit; + Write-Host -ForegroundColor Red "Get-Mailbox returned null. Make sure you Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline. Exiting script." + exit } else { if ($script:Mailbox.RecipientTypeDetails -ne "RoomMailbox" -and $script:Mailbox.RecipientTypeDetails -ne "EquipmentMailbox") { - Write-Host -ForegroundColor Red "The mailbox is not a Room Mailbox / Equipment Mailbox. RBA will only work with these. Exiting script."; - exit; + Write-Host -ForegroundColor Red "The mailbox is not a Room Mailbox / Equipment Mailbox. RBA will only work with these. Exiting script." + exit } if ($script:Mailbox.ResourceType -eq "Workspace") { - $script:Workspace = $true; + $script:Workspace = $true } Write-Host -ForegroundColor Green "The mailbox is valid for RBA will work with." } @@ -59,11 +59,11 @@ function ValidateMailbox { Write-Error "Error: Get-Place returned Null for $Identity." Write-Host -ForegroundColor Red "Make sure you are running from the correct forest. Get-Place does not cross forest boundaries." Write-Error "Exiting Script." - exit; + exit } - Write-Host -ForegroundColor Yellow "For more information see https://learn.microsoft.com/en-us/powershell/module/exchange/get-mailbox?view=exchange-ps"; - Write-Host; + Write-Host -ForegroundColor Yellow "For more information see https://learn.microsoft.com/en-us/powershell/module/exchange/get-mailbox?view=exchange-ps" + Write-Host } # Validate that there are not delegate rules that will block RBA functionality @@ -77,7 +77,7 @@ function ValidateInboxRules { Write-Host -NoNewline "Rule to look into: " Write-Host -ForegroundColor Red "$($rules.Name -like "Delegate Rule*")" Write-Host -ForegroundColor Red "Exiting script." - exit; + exit } elseif ($rules.Name -like "REDACTED-*") { Write-Host -ForegroundColor Yellow "Warning: No PII Access to MB so cannot check for Delegate Rules." Write-Host -ForegroundColor Red " --- Inbox Rules needs to be checked manually for any Delegate Rules. --" @@ -104,15 +104,15 @@ function GetCalendarProcessing { Write-Host -ForegroundColor Red "Get-CalendarProcessing returned null. Make sure you Import-Module ExchangeOnlineManagement and Connect-ExchangeOnline - Exiting script."; - exit; + Exiting script." + exit } $RbaSettings | Format-List Write-Host -ForegroundColor Yellow "For more information on Set-CalendarProcessing see - https://learn.microsoft.com/en-us/powershell/module/exchange/set-calendarprocessing?view=exchange-ps"; - Write-Host; + https://learn.microsoft.com/en-us/powershell/module/exchange/set-calendarprocessing?view=exchange-ps" + Write-Host } function EvaluateCalProcessing { @@ -123,7 +123,7 @@ function EvaluateCalProcessing { Write-Host -ForegroundColor Red "Error: AutomateProcessing is set to $($RbaSettings.AutomateProcessing)." Write-Host -ForegroundColor Yellow "Use 'Set-CalendarProcessing -Identity $Identity -AutomateProcessing AutoAccept' to set AutomateProcessing to AutoAccept." Write-Host -ForegroundColor Red "Exiting script." - exit; + exit } else { Write-Host -ForegroundColor Green "AutomateProcessing is set to AutoAccept. RBA will analyze the meeting request." } @@ -146,7 +146,7 @@ function ProcessingLogic { function RBACriteria { Write-DashLineBoxColor @("Policy Configuration") -Color Cyan -DashChar = - Write-Host " The following criteria are used to determine if a meeting request is in-policy or out-of-policy. "; + Write-Host " The following criteria are used to determine if a meeting request is in-policy or out-of-policy. " Write-Host -ForegroundColor Cyan @" `t Setting Value `t ------------------------------ ----------------------------- @@ -164,12 +164,12 @@ function RBACriteria { `t MaximumConflictPercentage: $($RbaSettings.MaximumConflictPercentage) `t EnforceSchedulingHorizon: $($RbaSettings.EnforceSchedulingHorizon) `t SchedulingHorizonInDays: $($RbaSettings.SchedulingHorizonInDays) -"@; +"@ Write-Host -NoNewline "`r`nIf all the above criteria are met, the request is " Write-Host -ForegroundColor Yellow -NoNewline "In-Policy." Write-Host -NoNewline "`r`nIf any of the above criteria are not met, the request is " - Write-Host -ForegroundColor DarkYellow -NoNewline "Out-of-Policy."; - Write-Host; + Write-Host -ForegroundColor DarkYellow -NoNewline "Out-of-Policy." + Write-Host # RBA processing settings Verbose Output $RBACriteriaExtra = "" @@ -227,7 +227,7 @@ function RBACriteria { $RBACriteriaExtra += "RBA will reject all External meeting requests.`r`n" } - $RBACriteriaExtra += "Meetings will only be accepted if within $($RbaSettings.BookingWindowInDays) days.`r`n"; + $RBACriteriaExtra += "Meetings will only be accepted if within $($RbaSettings.BookingWindowInDays) days.`r`n" Write-Verbose $RBACriteriaExtra } @@ -252,7 +252,7 @@ function RBAProcessingValidation { Write-Host "`t AllBookInPolicy: "$RbaSettings.AllBookInPolicy Write-Host "`t RequestInPolicy: {$($RbaSettings.RequestInPolicy)}" Write-Host "`t AllRequestInPolicy: "$RbaSettings.AllRequestInPolicy - Write-Host -ForegroundColor Red "Exiting script."; + Write-Host -ForegroundColor Red "Exiting script." exit } } @@ -471,13 +471,13 @@ function VerbosePostProcessing { #Add information about RBA logs. function RBAPostScript { - Write-Host; + Write-Host Write-Host "If more information is needed about this resource mailbox, please look at the RBA logs to - see how the system proceed the meeting request."; - Write-Host -ForegroundColor Yellow "`tExport-MailboxDiagnosticLogs $Identity -ComponentName RBA"; - Write-Host; + see how the system proceed the meeting request." + Write-Host -ForegroundColor Yellow "`tExport-MailboxDiagnosticLogs $Identity -ComponentName RBA" + Write-Host Write-Host "`n`rIf you found an error with this script or a misconfigured RBA case that this should cover, - send mail to Shanefe@microsoft.com"; + send mail to Shanefe@microsoft.com" } function RBALogSummary { @@ -492,7 +492,7 @@ function RBALogSummary { if ($starts.count -gt 1) { $LastDate = ($Starts[0] -Split ",")[0].Trim() - $FirstDate = ($starts[$($Starts.count) -1 ] -Split ",")[0].Trim(); + $FirstDate = ($starts[$($Starts.count) -1 ] -Split ",")[0].Trim() Write-Host "The RBA Log for $Identity shows the following:" Write-Host "`t $($starts.count) Processed events times between $FirstDate and $LastDate" } @@ -572,7 +572,7 @@ function ValidateRoomListSettings { Write-Host -ForegroundColor White "`tTags can be used to list features of this room (i.e. Projector, etc.) so that users can narrow down their search for conference rooms." Write-Host -ForegroundColor White "`tLearn more at " -NoNewline - Write-Host -ForegroundColor Yellow "https://learn.microsoft.com/en-us/outlook/troubleshoot/calendaring/configure-room-finder-rooms-workspaces`n"; + Write-Host -ForegroundColor Yellow "https://learn.microsoft.com/en-us/outlook/troubleshoot/calendaring/configure-room-finder-rooms-workspaces`n" if ([string]::IsNullOrEmpty($Place.Localities)) { ## validate Localities @@ -580,7 +580,7 @@ function ValidateRoomListSettings { Write-Host -ForegroundColor Yellow "`tWarning: Adding this resource to a Room Lists can take 24 hours to be fully propagated." } - $requiredProperties = @("City", "Floor", "Capacity"); + $requiredProperties = @("City", "Floor", "Capacity") foreach ($prop in $requiredProperties) { if ([string]::IsNullOrEmpty($script:Place.$prop)) { @@ -598,8 +598,8 @@ function ValidateRoomListSettings { Write-Host -ForegroundColor White "to set the required properties on the resource." } - Write-Host -ForegroundColor White "`r`n`t New Room List commonly populated information:"; - Write-Host -ForegroundColor White "`t ----------------------------------------- "; + Write-Host -ForegroundColor White "`r`n`t New Room List commonly populated information:" + Write-Host -ForegroundColor White "`t ----------------------------------------- " Write-Host -ForegroundColor White @" `t Address Info `t Street: $($script:Place.Street) @@ -618,7 +618,7 @@ function ValidateRoomListSettings { `t To update any of the above information, run 'Set-Place $Identity - '. `t For more information on this command, see "@ - Write-Host -ForegroundColor Yellow "`t https://learn.microsoft.com/en-us/powershell/module/exchange/set-place?view=exchange-ps"; + Write-Host -ForegroundColor Yellow "`t https://learn.microsoft.com/en-us/powershell/module/exchange/set-place?view=exchange-ps" Write-Host } diff --git a/Databases/Analyze-SpaceDump.ps1 b/Databases/Analyze-SpaceDump.ps1 index caca2184fe..75ba7ff880 100644 --- a/Databases/Analyze-SpaceDump.ps1 +++ b/Databases/Analyze-SpaceDump.ps1 @@ -96,7 +96,7 @@ $piTablesPerMailbox = New-Object 'System.Collections.Generic.Dictionary[string, while ($null -ne ($buffer = $fileReader.ReadLine())) { if (!($buffer.StartsWith(" ")) -and $buffer -ne "") { if ($buffer.StartsWith("-----")) { - break; + break } if ($isCSV) { diff --git a/Databases/README.md b/Databases/README.md deleted file mode 100644 index 20af1e2ff6..0000000000 --- a/Databases/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# [VSSTester.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/VSSTester.ps1) - -Download the latest release here: [https://github.com/microsoft/CSS-Exchange/releases/latest/download/VSSTester.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/VSSTester.ps1) - -The script is self-explanatory. You can test run DiskShadow on a single Exchange database to ensure backups are working properly (i.e. all the Microsoft components). If the issue only happens with a 3rd-party backup solution, you can utilize operation mode 2 to enable just the logging while you execute a backup with the 3rd-party solution. - -![Start Screen](/Backups/Images/start_screen.PNG "Start Screen") - -## More information -* https://techcommunity.microsoft.com/t5/exchange-team-blog/troubleshoot-your-exchange-2010-database-backup-functionality/ba-p/594367 -* https://techcommunity.microsoft.com/t5/exchange-team-blog/vsstester-script-updated-8211-troubleshoot-exchange-2013-and/ba-p/610976 diff --git a/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 b/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 index 6ca93b7963..57a67598e1 100644 --- a/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 +++ b/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 @@ -3,22 +3,42 @@ function Invoke-CreateDiskShadowFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWMICmdlet', '', Justification = 'Required to get drives on old systems')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('CustomRules\AvoidUsingReadHost', '', Justification = 'Do not want to change logic of script as of now')] - param() + [OutputType([string[]])] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath, + + [Parameter(Mandatory = $true)] + [string] + $ServerName, + + [Parameter(Mandatory = $true)] + [object[]] + $Databases, + + [Parameter(Mandatory = $true)] + [object] + $DatabaseToBackup, + + [Parameter(Mandatory = $true)] + [string] + $DatabaseDriveLetter, + + [Parameter(Mandatory = $true)] + [string] + $LogDriveLetter + ) function Out-DHSFile { param ([string]$FileLine) - $FileLine | Out-File -FilePath "$path\DiskShadow.dsh" -Encoding ASCII -Append + $FileLine | Out-File -FilePath "$OutputPath\DiskShadow.dsh" -Encoding ASCII -Append } # creates the DiskShadow.dsh file that will be written to below # ------------------------------------------------------------- - $nl - Get-Date - Write-Host "Creating DiskShadow config file..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - $nl - New-Item -Path $path\DiskShadow.dsh -type file -Force | Out-Null + Write-Host "$(Get-Date) Creating DiskShadow config file..." + New-Item -Path $OutputPath\DiskShadow.dsh -type file -Force | Out-Null # beginning lines of file # ----------------------- @@ -56,33 +76,13 @@ function Invoke-CreateDiskShadowFile { # add databases to exclude # ------------------------ - foreach ($db in $databases) { - $dbg = ($db.guid) - - if (($db).guid -ne $dbGuid) { - if (($db.IsMailboxDatabase) -eq "True") { - $mountedOnServer = (Get-MailboxDatabase $db).server.name + foreach ($db in $Databases) { + if ($db.Identity -ne $DatabaseToBackup.Identity) { + if ($db.Server.Name -eq $ServerName) { + Out-DHSFile "writer exclude `"Microsoft Exchange Writer:\Microsoft Exchange Server\Microsoft Information Store\$serverName\$($db.Guid)`"" } else { - $mountedOnServer = (Get-PublicFolderDatabase $db).server.name - } - if ($mountedOnServer -eq $serverName) { - $script:activeNode = $true - - Out-DHSFile "writer exclude `"Microsoft Exchange Writer:\Microsoft Exchange Server\Microsoft Information Store\$serverName\$dbg`"" - } - #if passive copy, add it with replica in the string - else { - $script:activeNode = $false - Out-DHSFile "writer exclude `"Microsoft Exchange Replica Writer:\Microsoft Exchange Server\Microsoft Information Store\Replica\$serverName\$dbg`"" - } - } - # add database to include - # ----------------------- - else { - if (($db.IsMailboxDatabase) -eq "True") { - $mountedOnServer = (Get-MailboxDatabase $db).server.name - } else { - $mountedOnServer = (Get-PublicFolderDatabase $db).server.name + #if passive copy, add it with replica in the string + Out-DHSFile "writer exclude `"Microsoft Exchange Replica Writer:\Microsoft Exchange Server\Microsoft Information Store\Replica\$serverName\$($db.Guid)`"" } } } @@ -95,102 +95,50 @@ function Invoke-CreateDiskShadowFile { $mpVolumes = Get-WmiObject -Query "select name, DeviceId from win32_volume where DriveType=3 AND DriveLetter=NULL" $deviceIDs = @() - #if selected database is a mailbox database, get mailbox paths - if ((($databases[$dbToBackup]).IsMailboxDatabase) -eq "True") { - $getDB = (Get-MailboxDatabase $selDB) - - $dbMP = $false - $logMP = $false - - #if no MountPoints ($mpVolumes) causes null-valued error, need to handle - if ($null -ne $mpVolumes) { - foreach ($mp in $mpVolumes) { - $mpName = (($mp.name).substring(0, $mp.name.length - 1)) - #if following mount point path exists in database path use deviceID in DiskShadow config file - if ($getDB.EdbFilePath.pathname.ToString().ToLower().StartsWith($mpName.ToString().ToLower())) { - Write-Host " " - Write-Host "Mount point: $($mp.name) in use for database path: " - #Write-host "Yes. I am a database in MountPoint" - "The selected database path is: " + $getDB.EdbFilePath.pathname - Write-Host "adding deviceID to file: " - $dbEdbVol = $mp.DeviceId - Write-Host $dbEdbVol - - #add device ID to array - $deviceID1 = $mp.DeviceID - $dbMP = $true - } - - #if following mount point path exists in log path use deviceID in DiskShadow config file - if ($getDB.LogFolderPath.pathname.ToString().ToLower().contains($mpName.ToString().ToLower())) { - Write-Host " " - Write-Host "Mount point: $($mp.name) in use for log path: " - #Write-host "Yes. My logs are in a MountPoint" - "The log folder path of selected database is: " + $getDB.LogFolderPath.pathname - Write-Host "adding deviceID to file: " - $dbLogVol = $mp.DeviceId - Write-Host $dbLogVol - $deviceID2 = $mp.DeviceID - $logMP = $true - } + $dbMP = $false + $logMP = $false + + #if no MountPoints ($mpVolumes) causes null-valued error, need to handle + if ($null -ne $mpVolumes) { + foreach ($mp in $mpVolumes) { + $mpName = (($mp.name).substring(0, $mp.name.length - 1)) + #if following mount point path exists in database path use deviceID in DiskShadow config file + if ($DatabaseToBackup.EdbFilePath.PathName.StartsWith($mpName, [System.StringComparison]::OrdinalIgnoreCase)) { + Write-Host " Mount point: $($mp.name) in use for database path: " + #Write-host "Yes. I am a database in MountPoint" + Write-Host " The selected database path is: $($DatabaseToBackup.EdbFilePath.PathName)" + $dbEdbVol = $mp.DeviceId + Write-Host " adding deviceID to file: $dbEdbVol" + + #add device ID to array + $deviceID1 = $mp.DeviceID + $dbMP = $true } - $deviceIDs = $deviceID1, $deviceID2 - } - } - - #if not a mailbox database, assume its a public folder database, get public folder paths - if ((($databases[$dbToBackup]).IsPublicFolderDatabase) -eq "True") { - $getDB = (Get-PublicFolderDatabase $selDB) - $dbMP = $false - $logMP = $false - - if ($null -ne $mpVolumes) { - foreach ($mp in $mpVolumes) { - $mpName = (($mp.name).substring(0, $mp.name.length - 1)) - #if following mount point path exists in database path use deviceID in DiskShadow config file - - if ($getDB.EdbFilePath.pathname.ToString().ToLower().StartsWith($mpName.ToString().ToLower())) { - Write-Host " " - Write-Host "Mount point: $($mp.name) in use for database path: " - "The current database path is: " + $getDB.EdbFilePath.pathname - Write-Host "adding deviceID to file: " - $dbEdbVol = $mp.deviceId - Write-Host $dbVol - - #add device ID to array - $deviceID1 = $mp.DeviceID - $dbMP = $true - } - - #if following mount point path exists in log path use deviceID in DiskShadow config file - if ($getDB.LogFolderPath.pathname.ToString().ToLower().contains($mpName.ToString().ToLower())) { - Write-Host " " - Write-Host "Mount point: $($vol.name) in use for log path: " - "The log folder path of selected database is: " + $getDB.LogFolderPath.pathname - Write-Host "adding deviceID to file " - $dbLogVol = $mp.deviceId - Write-Host $dbLogVol - - $deviceID2 = $mp.DeviceID - $logMP = $true - } + #if following mount point path exists in log path use deviceID in DiskShadow config file + if ($DatabaseToBackup.LogFolderPath.PathName.ToLower().Contains($mpName.ToLower())) { + Write-Host + Write-Host " Mount point: $($mp.name) in use for log path: " + #Write-host "Yes. My logs are in a MountPoint" + Write-Host " The log folder path of selected database is: $($DatabaseToBackup.LogFolderPath.PathName)" + $dbLogVol = $mp.DeviceId + Write-Host " adding deviceID to file: $dbLogVol" + $deviceID2 = $mp.DeviceID + $logMP = $true } - $deviceIDs = $deviceID1, $deviceID2 } + $deviceIDs = $deviceID1, $deviceID2 } if ($dbMP -eq $false) { - - $dbEdbVol = ($getDB.EdbFilePath.pathname).substring(0, 2) - "The selected database path is '" + $getDB.EdbFilePath.pathname + "' so adding volume $dbEdbVol to backup scope" + $dbEdbVol = ($DatabaseToBackup.EdbFilePath.PathName).substring(0, 2) + Write-Host " The selected database path is '$($DatabaseToBackup.EdbFilePath.PathName)' so adding volume $dbEdbVol to backup scope" $deviceID1 = $dbEdbVol } if ($logMP -eq $false) { - $dbLogVol = ($getDB.LogFolderPath.pathname).substring(0, 2) - $nl - "The selected database log folder path is '" + $getDB.LogFolderPath.pathname + "' so adding volume $dbLogVol to backup scope" + $dbLogVol = ($DatabaseToBackup.LogFolderPath.PathName).substring(0, 2) + Write-Host " The selected database log folder path is '$($DatabaseToBackup.LogFolderPath.PathName)' so adding volume $dbLogVol to backup scope" $deviceID2 = $dbLogVol } @@ -198,7 +146,7 @@ function Invoke-CreateDiskShadowFile { # We make sure that we add only one Logical volume when we detect the EDB and log files # are on the same volume - $nl + Write-Host $deviceIDs = $deviceID1, $deviceID2 $comp = [string]::Compare($deviceID1, $deviceID2, $True) if ($comp -eq 0) { @@ -218,24 +166,21 @@ function Invoke-CreateDiskShadowFile { Write-Host " " foreach ($device in $deviceIDs) { if ($device.length -gt "2") { - Write-Host "Adding the Mount Point for DSH file" + Write-Host " Adding the Mount Point for DSH file" $addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(11, 8) - Write-Host $addVol + Write-Host " $addVol" Out-DHSFile $addVol } else { - Write-Host "Adding the volume for DSH file" + Write-Host " Adding the volume for DSH file" $addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(0, 1) - Write-Host $addVol + Write-Host " $addVol" Out-DHSFile $addVol } } } Out-DHSFile "create" Out-DHSFile " " - $nl - Get-Date - Write-Host "Getting drive letters for exposing backup snapshot" -ForegroundColor Green - Write-Host "--------------------------------------------------------------------------------------------------------------" + Write-Host "$(Get-Date) Getting drive letters for exposing backup snapshot" # check to see if the drives are the same for both database and logs # if the same volume is used, only one drive letter is needed for exposure @@ -244,45 +189,15 @@ function Invoke-CreateDiskShadowFile { $matchCondition = "^[a-z]:$" Write-Debug $matchCondition - if ($dbEdbVol -eq $dbLogVol) { - $nl - "Since the same volume is used for this database's EDB and logs, we only need a single drive" - "letter to expose the backup snapshot." - $nl - - do { - Write-Host "Enter an unused drive letter with colon (e.g. X:) to expose the snapshot" -ForegroundColor Yellow -NoNewline - $script:dbSnapVol = Read-Host " " - if ($dbSnapVol -notmatch $matchCondition) { - Write-Host "Your input was not acceptable. Please use a single letter and colon, e.g. X:" -ForegroundColor red - } - } while ($dbSnapVol -notmatch $matchCondition) + $dbSnapVol = $DatabaseDriveLetter + if ($comp -eq 0) { + $logSnapVol = $dbSnapVol + Write-Host " Since the same volume is used for this database's EDB and logs, we only need a single drive" + Write-Host " letter to expose the backup snapshot." } else { - $nl - "Since different volumes are used for this database's EDB and logs, we need two drive" - "letters to expose the backup snapshot." - $nl - - do { - Write-Host "Enter an unused drive letter with colon (e.g. X:) to expose the DATABASE volume" -ForegroundColor Yellow -NoNewline - $script:dbSnapVol = Read-Host " " - if ($dbSnapVol -notmatch $matchCondition) { - Write-Host "Your input was not acceptable. Please use a single letter and colon, e.g. X:" -ForegroundColor red - } - } while ($dbSnapVol -notmatch $matchCondition) - - do { - Write-Host "Enter an unused drive letter with colon (e.g. Y:) to expose the LOG volume" -ForegroundColor Yellow -NoNewline - $script:logSnapVol = Read-Host " " - if ($logSnapVol -notmatch $matchCondition) { - Write-Host "Your input was not acceptable. Please use a single letter and colon, e.g. Y:" -ForegroundColor red - } - if ($logSnapVol -eq $dbSnapVol) { - Write-Host "You must choose a different drive letter than the one chosen to expose the DATABASE volume." -ForegroundColor red - } - } while (($logSnapVol -notmatch $matchCondition) -or ($logSnapVol -eq $dbSnapVol)) - - $nl + $logSnapVol = $LogDriveLetter + Write-Host " Since different volumes are used for this database's EDB and logs, we need two drive" + Write-Host " letters to expose the backup snapshot." } Write-Debug "dbSnapVol: $dbSnapVol | logSnapVol: $logSnapVol" @@ -291,33 +206,39 @@ function Invoke-CreateDiskShadowFile { # if volumes are the same only one entry is needed if ($dbEdbVol -eq $dbLogVol) { if ($dbEdbVol.length -gt "2") { - $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $dbSnapVol" + $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $($dbSnapVol):" Out-DHSFile $dbVolStr } else { - $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $dbSnapVol" + $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $($dbSnapVol):" Out-DHSFile $dbVolStr } } else { # volumes are different, getting both # if MountPoint use first part of string, if not use first letter if ($dbEdbVol.length -gt "2") { - $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $dbSnapVol" + $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $($dbSnapVol)" Out-DHSFile $dbVolStr } else { - $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $dbSnapVol" + $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $($dbSnapVol)" Out-DHSFile $dbVolStr } # if MountPoint use first part of string, if not use first letter if ($dbLogVol.length -gt "2") { - $logVolStr = "expose %vss_test_" + ($dbLogVol).substring(11, 8) + "% $logSnapVol" + $logVolStr = "expose %vss_test_" + ($dbLogVol).substring(11, 8) + "% $($logSnapVol):" Out-DHSFile $logVolStr } else { - $logVolStr = "expose %vss_test_" + ($dbLogVol).substring(0, 1) + "% $logSnapVol" + $logVolStr = "expose %vss_test_" + ($dbLogVol).substring(0, 1) + "% $($logSnapVol):" Out-DHSFile $logVolStr } } # ending data of file Out-DHSFile "end backup" + + if ($dbSnapVol -eq $logSnapVol) { + return @($dbSnapVol) + } else { + return @($dbSnapVol, $logSnapVol) + } } diff --git a/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 b/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 index 1e98b2aa27..32844758e8 100644 --- a/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 +++ b/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 @@ -1,21 +1,29 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +. $PSScriptRoot\..\Logging\Get-VSSWriter.ps1 + function Invoke-DiskShadow { - Write-Host " " $nl - Get-Date - Write-Host "Starting DiskShadow copy of Exchange database: $selDB" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - Write-Host "Running the following command:" $nl - Write-Host "`"C:\Windows\System32\DiskShadow.exe /s $path\DiskShadow.dsh /l $path\DiskShadow.log`"" $nl - Write-Host " " + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath, + + [Parameter(Mandatory = $true)] + [object] + $DatabaseToBackup + ) + + Write-Host "$(Get-Date) Starting DiskShadow copy of Exchange database: $Database" + Write-Host " Running the following command:" + Write-Host " `"C:\Windows\System32\DiskShadow.exe /s $OutputPath\DiskShadow.dsh /l $OutputPath\DiskShadow.log`"" #in case the $path and the script location is different we need to change location into the $path directory to get the results to work as expected. try { $here = (Get-Location).Path - Set-Location $path - DiskShadow.exe /s $path\DiskShadow.dsh /l $path\DiskShadow.log + Set-Location $OutputPath + DiskShadow.exe /s $OutputPath\DiskShadow.dsh /l $OutputPath\DiskShadow.log } finally { Set-Location $here } diff --git a/Databases/VSSTester/DiskShadow/Invoke-RemoveExposedDrives.ps1 b/Databases/VSSTester/DiskShadow/Invoke-RemoveExposedDrives.ps1 index f27659c33d..516c414782 100644 --- a/Databases/VSSTester/DiskShadow/Invoke-RemoveExposedDrives.ps1 +++ b/Databases/VSSTester/DiskShadow/Invoke-RemoveExposedDrives.ps1 @@ -2,61 +2,45 @@ # Licensed under the MIT License. function Invoke-RemoveExposedDrives { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('CustomRules\AvoidUsingReadHost', '', Justification = 'Do not want to change logic of script as of now')] - param() + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath, + + [Parameter(Mandatory = $true)] + [string[]] + $ExposedDrives + ) function Out-removeDHSFile { param ([string]$FileLine) - $FileLine | Out-File -FilePath "$path\removeSnapshot.dsh" -Encoding ASCII -Append + $FileLine | Out-File -FilePath "$OutputPath\removeSnapshot.dsh" -Encoding ASCII -Append } - " " - Get-Date - Write-Host "DiskShadow Snapshots" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - Write-Host " " - if ($null -eq $logSnapVol) { - $exposedDrives = $dbSnapVol - } else { - $exposedDrives = $dbSnapVol.ToString() + " and " + $logSnapVol.ToString() - } - "If the snapshot was successful, the snapshot should be exposed as drive(s) $exposedDrives." - "You should be able to see and navigate the snapshot with File Explorer. How would you like to proceed?" - Write-Host " " - Write-Host "NOTE: It is recommended to wait a few minutes to allow truncation to possibly occur before moving past this point." -ForegroundColor Cyan - Write-Host " This allows time for the logs that are automatically collected to include the window for the truncation to occur." -ForegroundColor Cyan + Write-Host "$(Get-Date) DiskShadow Snapshots" + Write-Host + Write-Host "If the snapshot was successful, the snapshot should be exposed as drive(s) $ExposedDrives." + Write-Host "You should be able to see and navigate the snapshot with File Explorer." + Write-Host + Write-Host "NOTE: It is recommended to wait a few minutes to allow truncation to possibly occur before moving past this point." + Write-Host " This allows time for the logs that are automatically collected to include the window for the truncation to occur." Write-Host - Write-Host "When ready, choose from the options below:" -ForegroundColor Yellow - " " - Write-Host " 1. Remove exposed snapshot now" - Write-Host " 2. Keep snapshot exposed" - Write-Host " " - Write-Warning "Selecting option 1 will permanently delete the snapshot created, i.e. your backup will be deleted." - " " - $matchCondition = "^[1-2]$" - Write-Debug "matchCondition: $matchCondition" - do { - Write-Host "Selection" -ForegroundColor Yellow -NoNewline - $removeExpose = Read-Host " " - if ($removeExpose -notmatch $matchCondition) { - Write-Host "Error! Please choose a valid option." -ForegroundColor red - } - } while ($removeExpose -notmatch $matchCondition) - - $unexposedCommand = "delete shadows exposed $dbSnapVol" - if ($null -ne $logSnapVol) { - $unexposedCommand += $nl + "delete shadows exposed $logSnapVol" + + New-Item -Path $OutputPath\removeSnapshot.dsh -type file -Force | Out-Null + + $ExposedDrives | ForEach-Object { + Out-removeDHSFile "delete shadows exposed $($_):" } - if ($removeExpose -eq "1") { - New-Item -Path $path\removeSnapshot.dsh -type file -Force - Out-removeDHSFile $unexposedCommand - Out-removeDHSFile "exit" - & 'C:\Windows\System32\DiskShadow.exe' /s $path\removeSnapshot.dsh - } elseif ($removeExpose -eq "2") { - Write-Host "You can remove the snapshots at a later time using the DiskShadow tool from a command prompt." - Write-Host "Run DiskShadow followed by these commands:" - Write-Host $unexposedCommand + Out-removeDHSFile "exit" + + if ($PSCmdlet.ShouldProcess("$ExposedDrives", "Remove exposed drives now?")) { + DiskShadow.exe /s "$OutputPath\removeSnapshot.dsh" + } else { + Write-Host "When you are ready to remove the snapshots, run the following command:" + Write-Host + Write-Host "DiskShadow.exe /s $OutputPath\removeSnapshot.dsh" + Write-Host } } diff --git a/Databases/VSSTester/ExchangeInformation/Get-CopyStatus.ps1 b/Databases/VSSTester/ExchangeInformation/Get-CopyStatus.ps1 index fac082941c..a078c278ae 100644 --- a/Databases/VSSTester/ExchangeInformation/Get-CopyStatus.ps1 +++ b/Databases/VSSTester/ExchangeInformation/Get-CopyStatus.ps1 @@ -2,24 +2,28 @@ # Licensed under the MIT License. function Get-CopyStatus { - if ((($databases[$dbToBackup]).IsMailboxDatabase) -eq "True") { - Get-Date - Write-Host "Status of '$selDB' and its replicas (if any)" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - [array]$copyStatus = (Get-MailboxDatabaseCopyStatus -identity ($databases[$dbToBackup]).name) - ($copyStatus | Format-List) | Out-File -FilePath "$path\copyStatus.txt" - for ($i = 0; $i -lt ($copyStatus).length; $i++ ) { - if (($copyStatus[$i].status -eq "Healthy") -or ($copyStatus[$i].status -eq "Mounted")) { - Write-Host "$($copyStatus[$i].name) is $($copyStatus[$i].status)" - } else { - Write-Host "$($copyStatus[$i].name) is $($copyStatus[$i].status)" - Write-Host "One of the copies of the selected database is not healthy. Please run backup after ensuring that the database copy is healthy" -ForegroundColor Yellow - exit - } - } - } else { - Write-Host "Not checking database copy status since the selected database is a Public Folder Database..." + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $ServerName, + + [Parameter(Mandatory = $true)] + [object] + $Database, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + + Write-Host "$(Get-Date) Status of '$Database' and its replicas (if any)" + [array]$copyStatus = (Get-MailboxDatabaseCopyStatus -identity ($Database).name) + ($copyStatus | Format-List) | Out-File -FilePath "$OutputPath\copyStatus.txt" + $copyStatus | Format-Table Name, Status | Out-Host + $unhealthyCopies = $copyStatus | Where-Object { $_.Status -ne "Healthy" -and $_.Status -ne "Mounted" } + if ($null -ne $unhealthyCopies) { + Write-Warning "One of the copies of the selected database is not healthy. Please run backup after ensuring that the database copy is healthy" + exit } - " " } diff --git a/Databases/VSSTester/ExchangeInformation/Get-Databases.ps1 b/Databases/VSSTester/ExchangeInformation/Get-Databases.ps1 index de1821b83a..0854f621be 100644 --- a/Databases/VSSTester/ExchangeInformation/Get-Databases.ps1 +++ b/Databases/VSSTester/ExchangeInformation/Get-Databases.ps1 @@ -2,25 +2,15 @@ # Licensed under the MIT License. function Get-Databases { - Get-Date - Write-Host "Getting databases on server: $serverName" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " + [OutputType([System.Array])] + param( + [Parameter(Mandatory = $true)] + [string] + $ServerName + ) - [array]$script:databases = Get-MailboxDatabase -server $serverName -status - if ($null -ne (Get-PublicFolderDatabase -Server $serverName)) { - $script:databases += Get-PublicFolderDatabase -server $serverName -status - } - - #write-host "Database Name:`t`t Mounted: `t`t Mounted On Server:" -ForegroundColor Yellow $nl - $script:dbID = 0 - - foreach ($script:db in $databases) { - $script:db | Add-Member NoteProperty Number $dbID - $dbID++ - } - - $script:databases | Format-Table Number, Name, Mounted, Server -AutoSize | Out-String - - Write-Host " " $nl + Write-Host "$(Get-Date) Getting databases on server: $ServerName" + [array]$databases = Get-MailboxDatabase -server $ServerName -status + $databases | Format-Table Name, Mounted, Server -AutoSize | Out-Host + return $databases } diff --git a/Databases/VSSTester/ExchangeInformation/Get-DbToBackup.ps1 b/Databases/VSSTester/ExchangeInformation/Get-DbToBackup.ps1 deleted file mode 100644 index c7823e4da9..0000000000 --- a/Databases/VSSTester/ExchangeInformation/Get-DbToBackup.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -function Get-DBtoBackup { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('CustomRules\AvoidUsingReadHost', '', Justification = 'Do not want to change logic of script as of now')] - param() - $maxDbIndexRange = $script:databases.length - 1 - $matchCondition = "^([0-9]|[1-9][0-9])$" - Write-Debug "matchCondition: $matchCondition" - do { - Write-Host "Select the number of the database to backup" -ForegroundColor Yellow -NoNewline; - $script:dbToBackup = Read-Host " " - - if ($script:dbToBackup -notmatch $matchCondition -or [int]$script:dbToBackup -gt $maxDbIndexRange) { - Write-Host "Error! Please select a valid option!" -ForegroundColor Red - } - } while ($script:dbToBackup -notmatch $matchCondition -or [int]$script:dbToBackup -gt $maxDbIndexRange) # notmatch is case-insensitive - - if ((($databases[$dbToBackup]).IsMailboxDatabase) -eq "True") { - - $script:dbGuid = (Get-MailboxDatabase ($databases[$dbToBackup])).guid - $script:selDB = (Get-MailboxDatabase ($databases[$dbToBackup])).name - " " - "The database guid for '$selDB' is: $dbGuid" - " " - $script:dbMountedOn = (Get-MailboxDatabase ($databases[$dbToBackup])).server.name - } else { - $script:dbGuid = (Get-PublicFolderDatabase ($databases[$dbToBackup])).guid - $script:selDB = (Get-PublicFolderDatabase ($databases[$dbToBackup])).name - "The database guid for '$selDB' is: $dbGuid" - " " - $script:dbMountedOn = (Get-PublicFolderDatabase ($databases[$dbToBackup])).server.name - } - Write-Host "The database is mounted on server: $dbMountedOn $nl" - - if ($dbMountedOn -eq "$serverName") { - $script:dbStatus = "active" - } else { - $script:dbStatus = "passive" - } -} diff --git a/Databases/VSSTester/ExchangeInformation/Get-ExchangeVersion.ps1 b/Databases/VSSTester/ExchangeInformation/Get-ExchangeVersion.ps1 index 009ff44e9a..c72057561f 100644 --- a/Databases/VSSTester/ExchangeInformation/Get-ExchangeVersion.ps1 +++ b/Databases/VSSTester/ExchangeInformation/Get-ExchangeVersion.ps1 @@ -2,42 +2,46 @@ # Licensed under the MIT License. function Get-ExchangeVersion { - Get-Date - Write-Host "Verifying Exchange version..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - $script:exchVer = (Get-ExchangeServer $serverName).AdminDisplayVersion + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $ServerName + ) + + Write-Host "$(Get-Date) Verifying Exchange version..." + $exchVer = (Get-ExchangeServer $ServerName).AdminDisplayVersion $exchVerMajor = $exchVer.major $exchVerMinor = $exchVer.minor switch ($exchVerMajor) { "14" { - $script:exchVer = "2010" + $exchVer = "2010" } "15" { switch ($exchVerMinor) { "0" { - $script:exchVer = "2013" + $exchVer = "2013" } "1" { - $script:exchVer = "2016" + $exchVer = "2016" } "2" { - $script:exchVer = "2019" + $exchVer = "2019" } } } default { - Write-Host "This script is only for Exchange 2013, 2016, and 2019 servers." -ForegroundColor red $nl + Write-Host " This script is only for Exchange 2013, 2016, and 2019 servers." exit } } - Write-Host "$serverName is an Exchange $exchVer server. $nl" + Write-Host " $ServerName is an Exchange $exchVer server." if ($exchVer -eq "2010") { - Write-Host "This script no longer supports Exchange 2010." + Write-Host " This script no longer supports Exchange 2010." exit } } diff --git a/Databases/VSSTester/Logging/Get-VSSWriter.ps1 b/Databases/VSSTester/Logging/Get-VSSWriter.ps1 new file mode 100644 index 0000000000..01b165a0fb --- /dev/null +++ b/Databases/VSSTester/Logging/Get-VSSWriter.ps1 @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +function Get-VSSWriter { + [CmdletBinding()] + param () + + $writersText = vssadmin list writers + if ($LASTEXITCODE) { + Write-Warning $writersText + throw "Unable to list vss writers" + } + + for ($lineNumber = 3; $lineNumber -lt $writersText.Count; $lineNumber += 6) { + [PSCustomObject]@{ + Name = $writersText[$lineNumber].Substring($writersText[$lineNumber].IndexOf("'") + 1).TrimEnd("'") + Id = $writersText[$lineNumber + 1].Substring($writersText[$lineNumber + 1].IndexOf("{") + 1).TrimEnd("}") + InstanceId = $writersText[$lineNumber + 2].Substring($writersText[$lineNumber + 2].IndexOf("{") + 1).TrimEnd("}") + State = $writersText[$lineNumber + 3].Substring($writersText[$lineNumber + 3].IndexOf(":") + 1).Trim() + LastError = $writersText[$lineNumber + 4].Substring($writersText[$lineNumber + 4].IndexOf(":") + 1).Trim() + } + } +} diff --git a/Databases/VSSTester/Logging/Get-VSSWritersAfter.ps1 b/Databases/VSSTester/Logging/Get-VSSWritersAfter.ps1 index e554c6823d..400ff3423a 100644 --- a/Databases/VSSTester/Logging/Get-VSSWritersAfter.ps1 +++ b/Databases/VSSTester/Logging/Get-VSSWritersAfter.ps1 @@ -1,21 +1,18 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +. $PSScriptRoot\Get-VSSWriter.ps1 + function Get-VSSWritersAfter { - " " - Get-Date - Write-Host "Checking VSS Writer Status: (after backup)" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - " " - $writers = (vssadmin list writers) - $writers > $path\vssWritersAfter.txt + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) - foreach ($line in $writers) { - if ($line -like "Writer name:*") { - "$line" - } elseif ($line -like " State:*") { - "$line" + $nl - } - } + Write-Host "$(Get-Date) Checking VSS Writer Status: (after backup)" + $writers = Get-VSSWriter + $writers | Export-Csv $OutputPath\vssWritersAfter.csv -NoTypeInformation + $writers | Sort-Object Name | Format-Table | Out-Host } diff --git a/Databases/VSSTester/Logging/Get-VSSWritersBefore.ps1 b/Databases/VSSTester/Logging/Get-VSSWritersBefore.ps1 index a9c784b004..53ead15137 100644 --- a/Databases/VSSTester/Logging/Get-VSSWritersBefore.ps1 +++ b/Databases/VSSTester/Logging/Get-VSSWritersBefore.ps1 @@ -1,46 +1,31 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -function Get-VSSWritersBefore { - " " - Get-Date - Write-Host "Checking VSS Writer Status: (All Writers must be in a Stable state before running this script)" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - $writers = (vssadmin list writers) - $writers > $path\vssWritersBefore.txt - $exchangeWriter = $false - - foreach ($line in $writers) { - - if ($line -like "Writer name:*") { - "$line" +. $PSScriptRoot\Get-VSSWriter.ps1 - if ($line.Contains("Microsoft Exchange Writer")) { - $exchangeWriter = $true - } - } elseif ($line -like " State:*") { - - if ($line -ne " State: [1] Stable") { - $nl - Write-Host "!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" -ForegroundColor red - $nl - Write-Host "One or more writers are NOT in a 'Stable' state, STOPPING SCRIPT." -ForegroundColor red - $nl - Write-Host "Review the vssWritersBefore.txt file in '$path' for more information." -ForegroundColor Red - Write-Host "You can also use an Exchange Management Shell or a Command Prompt to run: 'vssadmin list writers'" -ForegroundColor red - $nl - Write-Host "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" -ForegroundColor red - $nl - exit - } else { - "$line" + $nl - } - } +function Get-VSSWritersBefore { + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + + Write-Host "$(Get-Date) Checking VSS Writer Status: (All Writers must be in a Stable state before running this script)" + $writers = Get-VSSWriter + $writers | Export-Csv $OutputPath\vssWritersBefore.csv -NoTypeInformation + $exchangeWriter = $writers | Where-Object { $_.Name -eq "Microsoft Exchange Writer" } + $writersInErrorState = $writers | Where-Object { $_.State -ne "[1] Stable" } + + if ($null -ne $writersInErrorState) { + Write-Warning "WARNING: One or more writers are NOT in a 'Stable' state, STOPPING SCRIPT." + $writersInErrorState | Format-Table Name, State | Out-Host + exit } - " " + $nl - if (!$exchangeWriter) { + $writers | Sort-Object Name | Format-Table | Out-Host + + if ($null -eq $exchangeWriter) { #Check for possible COM security issue. $oleKey = "HKLM:\SOFTWARE\Microsoft\Ole" @@ -51,18 +36,16 @@ function Get-VSSWritersBefore { ((Test-Path $dcomKey) -and ($null -ne (Get-ItemProperty $dcomKey).MachineAccessRestriction)) - Write-Host "!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" -ForegroundColor red - Write-Host "Microsoft Exchange Writer not present on server. Unable to preform proper backups on the server." -ForegroundColor Red - Write-Host + Write-Warning "WARNING: Microsoft Exchange Writer not present on server. Unable to perform proper backups on the server." if ($possibleDcomPermissionIssue) { - Write-Host " - Recommend to verify local Administrators group applied to COM+ Security settings: https://aka.ms/VSSTester-COMSecurity" -ForegroundColor Cyan + Write-Host " - Recommend to verify local Administrators group applied to COM+ Security settings: https://aka.ms/VSSTester-COMSecurity" } - Write-Host " - Recommend to restart MSExchangeRepl service to see if the writer comes back. If it doesn't, review the application logs for any events to determine why." -ForegroundColor Cyan - Write-Host " --- Look for Event ID 2003 in the application logs to verify that all internal components come online. If you see this event, try to use PSExec.exe to start a cmd.exe as the SYSTEM account and run 'vssadmin list writers'" -ForegroundColor Cyan - Write-Host " --- If you find the Microsoft Exchange Writer, then we have a permissions issue on the computer that is preventing normal user accounts from finding all the writers." -ForegroundColor Cyan - Write-Host " - If still not able to determine why, need to have a Microsoft Engineer review ExTrace with Cluster.Replay tags of the MSExchangeRepl service starting up." -ForegroundColor Cyan + Write-Host " - Recommend to restart MSExchangeRepl service to see if the writer comes back. If it doesn't, review the application logs for any events to determine why." + Write-Host " --- Look for Event ID 2003 in the application logs to verify that all internal components come online. If you see this event, try to use PSExec.exe to start a cmd.exe as the SYSTEM account and run 'vssadmin list writers'" + Write-Host " --- If you find the Microsoft Exchange Writer, then we have a permissions issue on the computer that is preventing normal user accounts from finding all the writers." + Write-Host " - If still not able to determine why, need to have a Microsoft Engineer review ExTrace with Cluster.Replay tags of the MSExchangeRepl service starting up." Write-Host Write-Host "Stopping Script" exit diff --git a/Databases/VSSTester/Logging/Get-WindowsEventLogs.ps1 b/Databases/VSSTester/Logging/Get-WindowsEventLogs.ps1 index bcc5d80b8d..17461fb8e6 100644 --- a/Databases/VSSTester/Logging/Get-WindowsEventLogs.ps1 +++ b/Databases/VSSTester/Logging/Get-WindowsEventLogs.ps1 @@ -2,26 +2,33 @@ # Licensed under the MIT License. function Get-WindowsEventLogs { + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [DateTime] + $StartTime, - function Get-WindowEventsPerServer { - param( - [string]$ComputerName - ) - "Getting application log events..." - Get-WinEvent -FilterHashtable @{LogName = "Application"; StartTime = $startTime } -ComputerName $ComputerName | Export-Clixml $path\events-$ComputerName-App.xml - "Getting system log events..." - Get-WinEvent -FilterHashtable @{LogName = "System"; StartTime = $startTime } -ComputerName $ComputerName | Export-Clixml $path\events-$ComputerName-System.xml - "Getting TruncationDebug log events..." - Get-WinEvent -FilterHashtable @{LogName = "Microsoft-Exchange-HighAvailability/TruncationDebug"; StartTime = $startTime } -ComputerName $ComputerName -ErrorAction SilentlyContinue | Export-Clixml $path\events-$ComputerName-TruncationDebug.xml + [Parameter(Mandatory = $true)] + [string] + $ComputerName, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + + Write-Host "$(Get-Date) Getting events from the application and system logs since the script's start time of ($StartTime)" + + $timeString = $StartTime.ToUniversalTime().ToString("O") + Write-Host " Getting application log events..." + if (Test-Path $OutputPath\events-Application.evtx) { + Remove-Item $OutputPath\events-Application.evtx + } + wevtutil epl Application $OutputPath\events-Application.evtx /q:"Event/System/TimeCreated[@SystemTime > '$timeString']" + Write-Host " Getting system log events..." + if (Test-Path $OutputPath\events-System.evtx) { + Remove-Item $OutputPath\events-System.evtx } - " " - Get-Date - Write-Host "Getting events from the application and system logs since the script's start time of ($startInfo)" -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - "Getting application log events..." - Get-EventLog -LogName Application -After $startInfo | Export-Clixml $path\events-App.xml - "Getting system log events..." - Get-EventLog -LogName System -After $startInfo | Export-Clixml $path\events-System.xml - "Getting events complete!" + wevtutil epl System $OutputPath\events-System.evtx /q:"Event/System/TimeCreated[@SystemTime > '$timeString']" + Write-Host " Getting events complete!" } diff --git a/Databases/VSSTester/Logging/Invoke-CreateExtraTracingConfig.ps1 b/Databases/VSSTester/Logging/Invoke-CreateExtraTracingConfig.ps1 index f2f3aab031..97ee05b552 100644 --- a/Databases/VSSTester/Logging/Invoke-CreateExtraTracingConfig.ps1 +++ b/Databases/VSSTester/Logging/Invoke-CreateExtraTracingConfig.ps1 @@ -2,18 +2,16 @@ # Licensed under the MIT License. function Invoke-CreateExTRATracingConfig { + [OutputType([System.Void])] + param() function Out-ExTRAConfigFile { param ([string]$FileLine) $FileLine | Out-File -FilePath "C:\EnabledTraces.Config" -Encoding ASCII -Append } - " " - Get-Date - Write-Host "Enabling ExTRA Tracing..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - New-Item -Path "C:\EnabledTraces.Config" -type file -Force + Write-Host "$(Get-Date) Enabling ExTRA Tracing..." + New-Item -Path "C:\EnabledTraces.Config" -type file -Force | Out-Null Out-ExTRAConfigFile "TraceLevels:Debug,Warning,Error,Fatal,Info,Performance,Function,Pfd" Out-ExTRAConfigFile "ManagedStore.PhysicalAccess:JetBackup,JetRestore,JetEventlog,SnapshotOperation" @@ -21,6 +19,6 @@ function Invoke-CreateExTRATracingConfig { Out-ExTRAConfigFile "ManagedStore.HA:BlockModeSender,Eseback" Out-ExTRAConfigFile "FilteredTracing:No" Out-ExTRAConfigFile "InMemoryTracing:No" - " " + Write-Debug "ExTRA trace config file created successfully" } diff --git a/Databases/VSSTester/Logging/Invoke-DisableDiagnosticsLogging.ps1 b/Databases/VSSTester/Logging/Invoke-DisableDiagnosticsLogging.ps1 index a3726beb43..3a9e71a447 100644 --- a/Databases/VSSTester/Logging/Invoke-DisableDiagnosticsLogging.ps1 +++ b/Databases/VSSTester/Logging/Invoke-DisableDiagnosticsLogging.ps1 @@ -1,18 +1,23 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +<# +.SYNOPSIS + Disable diagnostics logging for VSSTester. +.NOTES + This function may be called within a finally block, so it MUST NOT write to the pipeline: + https://stackoverflow.com/questions/45104509/powershell-finally-block-skipped-with-ctrl-c +#> function Invoke-DisableDiagnosticsLogging { + [OutputType([System.Void])] + param() - Write-Host " " $nl - Get-Date - Write-Host "Disabling Diagnostics Logging..." -ForegroundColor green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " + Write-Host "$(Get-Date) Disabling Diagnostics Logging..." Set-EventLogLevel 'MSExchange Repl\Service' -level lowest $disGetReplSvc = Get-EventLogLevel 'MSExchange Repl\Service' - Write-Host "$($disGetReplSvc.Identity) - $($disGetReplSvc.EventLevel) $nl" + Write-Host " $($disGetReplSvc.Identity) - $($disGetReplSvc.EventLevel)" Set-EventLogLevel 'MSExchange Repl\Exchange VSS Writer' -level lowest $disGetReplVSSWriter = Get-EventLogLevel 'MSExchange Repl\Exchange VSS Writer' - Write-Host "$($disGetReplVSSWriter.Identity) - $($disGetReplVSSWriter.EventLevel) $nl" + Write-Host " $($disGetReplVSSWriter.Identity) - $($disGetReplVSSWriter.EventLevel)" } diff --git a/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 b/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 index 6139dd1e4c..05f9428764 100644 --- a/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 @@ -1,35 +1,51 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +<# +.SYNOPSIS + Disable ExTRA tracing +.NOTES + This function may be called within a finally block, so it MUST NOT write to the pipeline: + https://stackoverflow.com/questions/45104509/powershell-finally-block-skipped-with-ctrl-c +#> function Invoke-DisableExTRATracing { - " " - Get-Date - Write-Host "Disabling ExTRA Tracing..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - if ($dbMountedOn -eq "$serverName") { + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $ServerName, + + [Parameter(Mandatory = $true)] + [object] + $DatabaseToBackup, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath + ) + Write-Host "$(Get-Date) Disabling ExTRA Tracing..." + $dbMountedOn = $DatabaseToBackup.Server.Name + if ($dbMountedOn -eq "$ServerName") { #stop active copy - Write-Host " " - "Stopping Exchange Trace data collector on $serverName..." - logman stop vssTester -s $serverName - "Deleting Exchange Trace data collector on $serverName..." - logman delete vssTester -s $serverName - " " + Write-Host + Write-Host " Stopping Exchange Trace data collector on $ServerName..." + logman stop vssTester -s $ServerName + Write-Host " Deleting Exchange Trace data collector on $ServerName..." + logman delete vssTester -s $ServerName + Write-Host } else { #stop passive copy - "Stopping Exchange Trace data collector on $serverName..." - logman stop vssTester-Passive -s $serverName - "Deleting Exchange Trace data collector on $serverName..." - logman delete vssTester-Passive -s $serverName + Write-Host " Stopping Exchange Trace data collector on $ServerName..." + logman stop vssTester-Passive -s $ServerName + Write-Host " Deleting Exchange Trace data collector on $ServerName..." + logman delete vssTester-Passive -s $ServerName #stop active copy - "Stopping Exchange Trace data collector on $dbMountedOn..." + Write-Host " Stopping Exchange Trace data collector on $dbMountedOn..." logman stop vssTester-Active -s $dbMountedOn - "Deleting Exchange Trace data collector on $dbMountedOn..." + Write-Host " Deleting Exchange Trace data collector on $dbMountedOn..." logman delete vssTester-Active -s $dbMountedOn - " " - "Moving ETL file from $dbMountedOn to $serverName..." - " " - $etlPath = $path -replace ":\\", "$\" + Write-Host " Moving ETL file from $dbMountedOn to $serverName..." + $etlPath = $OutputPath -replace ":\\", "$\" Move-Item "\\$dbMountedOn\$etlPath\vsstester-active_000001.etl" "\\$ServerName\$etlPath\vsstester-active_000001.etl" -Force } } diff --git a/Databases/VSSTester/Logging/Invoke-DisableVSSTracing.ps1 b/Databases/VSSTester/Logging/Invoke-DisableVSSTracing.ps1 index 90d0a627d3..3c53299062 100644 --- a/Databases/VSSTester/Logging/Invoke-DisableVSSTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-DisableVSSTracing.ps1 @@ -1,12 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +<# +.SYNOPSIS + Disables VSS tracing. +.NOTES + This function may be called within a finally block, so it MUST NOT write to the pipeline: + https://stackoverflow.com/questions/45104509/powershell-finally-block-skipped-with-ctrl-c +#> function Invoke-DisableVSSTracing { - " " - Get-Date - Write-Host "Disabling VSS Tracing..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " + [OutputType([System.Void])] + param() + + Write-Host "$(Get-Date) Disabling VSS Tracing..." logman stop vss -ets - " " } diff --git a/Databases/VSSTester/Logging/Invoke-EnableDiagnosticsLogging.ps1 b/Databases/VSSTester/Logging/Invoke-EnableDiagnosticsLogging.ps1 index c5b0c3d825..37fa9366de 100644 --- a/Databases/VSSTester/Logging/Invoke-EnableDiagnosticsLogging.ps1 +++ b/Databases/VSSTester/Logging/Invoke-EnableDiagnosticsLogging.ps1 @@ -2,16 +2,15 @@ # Licensed under the MIT License. function Invoke-EnableDiagnosticsLogging { - " " - Get-Date - Write-Host "Enabling Diagnostics Logging..." -ForegroundColor green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " + [OutputType([System.Void])] + param() + + Write-Host "$(Get-Date) Enabling Diagnostics Logging..." Set-EventLogLevel 'MSExchange Repl\Service' -level expert $getReplSvc = Get-EventLogLevel 'MSExchange Repl\Service' - Write-Host "$($getReplSvc.Identity) - $($getReplSvc.EventLevel) $nl" + Write-Host " $($getReplSvc.Identity) - $($getReplSvc.EventLevel)" Set-EventLogLevel 'MSExchange Repl\Exchange VSS Writer' -level expert $getReplVSSWriter = Get-EventLogLevel 'MSExchange Repl\Exchange VSS Writer' - Write-Host "$($getReplVSSWriter.Identity) - $($getReplVSSWriter.EventLevel) $nl" + Write-Host " $($getReplVSSWriter.Identity) - $($getReplVSSWriter.EventLevel)" } diff --git a/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 b/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 index 8dae052142..7c51a55ddf 100644 --- a/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 @@ -2,70 +2,100 @@ # Licensed under the MIT License. function Invoke-EnableExTRATracing { + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $ServerName, + + [Parameter(Mandatory = $true)] + [object] + $DatabaseToBackup, + + [Parameter(Mandatory = $true)] + [string] + $OutputPath, + + [Parameter(Mandatory = $true)] + [bool] + $Circular + ) function Invoke-ExtraTracingCreate { param( [string]$ComputerName, - [string]$LogmanName + [string]$LogmanName, + [string]$OutputPath, + [bool]$Circular ) - logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $path\$LogmanName.etl -ow -s $ComputerName -mode globalsequence + + if ($Circular) { + logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $OutputPath\$LogmanName.etl -ow -s $ComputerName -mode globalsequence -f bincirc -max 1024 + } else { + logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $OutputPath\$LogmanName.etl -ow -s $ComputerName -mode globalsequence + } if ($LASTEXITCODE) { Write-Host "Exchange Trace data Collector set already created. Removing it and trying again" + logman stop $LogmanName -s $ComputerName logman delete $LogmanName -s $ComputerName - logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $path\$LogmanName.etl -ow -s $ComputerName -mode globalsequence + if ($Circular) { + logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $OutputPath\$LogmanName.etl -ow -s $ComputerName -mode globalsequence -f bincirc -max 1024 + } else { + logman create trace $LogmanName -p '{79bb49e6-2a2c-46e4-9167-fa122525d540}' -o $OutputPath\$LogmanName.etl -ow -s $ComputerName -mode globalsequence + } } if ($LASTEXITCODE) { - Write-Host "Failed to create the extra trace. Stopping the VSSTester Script" -ForegroundColor Red + Write-Warning "Failed to create the extra trace. Stopping the VSSTester Script" exit } } + $dbMountedOn = $DatabaseToBackup.Server.Name + #active server, only get tracing from active node - if ($dbMountedOn -eq $serverName) { - " " - "Creating Exchange Trace data collector set..." - Invoke-ExtraTracingCreate -ComputerName $serverName -LogmanName "VSSTester" - "Starting Exchange Trace data collector..." + if ($dbMountedOn -eq $ServerName) { + Write-Host "Creating Exchange Trace data collector set..." + Invoke-ExtraTracingCreate -ComputerName $ServerName -LogmanName "VSSTester" -OutputPath $OutputPath + Write-Host "Starting Exchange Trace data collector..." logman start VSSTester if ($LASTEXITCODE) { - Write-Host "Failed to start the extra trace. Stopping the VSSTester Script" -ForegroundColor Red + Write-Warning "Failed to start the extra trace. Stopping the VSSTester Script" exit } - " " + + Write-Host } else { #passive server, get tracing from both active and passive nodes - " " - "Copying the ExTRA config file 'EnabledTraces.config' file to $dbMountedOn..." + Write-Host "Copying the ExTRA config file 'EnabledTraces.config' file to $dbMountedOn..." #copy EnabledTraces.config from current passive copy to active copy server Copy-Item "c:\EnabledTraces.Config" "\\$dbMountedOn\c$\EnabledTraces.config" -Force #create trace on passive copy - "Creating Exchange Trace data collector set on $serverName..." - Invoke-ExtraTracingCreate -ComputerName $serverName -LogmanName "VSSTester-Passive" + Write-Host "Creating Exchange Trace data collector set on $ServerName..." + Invoke-ExtraTracingCreate -ComputerName $ServerName -LogmanName "VSSTester-Passive" -OutputPath $OutputPath #create trace on active copy - "Creating Exchange Trace data collector set on $dbMountedOn..." - Invoke-ExtraTracingCreate -ComputerName $dbMountedOn -LogmanName "VSSTester-Active" + Write-Host "Creating Exchange Trace data collector set on $dbMountedOn..." + Invoke-ExtraTracingCreate -ComputerName $dbMountedOn -LogmanName "VSSTester-Active" -OutputPath $OutputPath #start trace on passive copy - "Starting Exchange Trace data collector on $serverName..." - logman start VSSTester-Passive -s $serverName + Write-Host "Starting Exchange Trace data collector on $ServerName..." + logman start VSSTester-Passive -s $ServerName if ($LASTEXITCODE) { - Write-Host "Failed to start the extra trace. Stopping the VSSTester Script" -ForegroundColor Red + Write-Warning "Failed to start the extra trace. Stopping the VSSTester Script" exit } #start trace on active copy - "Starting Exchange Trace data collector on $dbMountedOn..." + Write-Host "Starting Exchange Trace data collector on $dbMountedOn..." logman start VSSTester-Active -s $dbMountedOn if ($LASTEXITCODE) { - Write-Host "Failed to start the extra trace. Stopping the VSSTester Script" -ForegroundColor Red + Write-Warning "Failed to start the extra trace. Stopping the VSSTester Script" exit } - " " } Write-Debug "ExTRA trace started successfully" diff --git a/Databases/VSSTester/Logging/Invoke-EnableVSSTracing.ps1 b/Databases/VSSTester/Logging/Invoke-EnableVSSTracing.ps1 index 03c14a7e1f..c980f09ad9 100644 --- a/Databases/VSSTester/Logging/Invoke-EnableVSSTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-EnableVSSTracing.ps1 @@ -2,10 +2,21 @@ # Licensed under the MIT License. function Invoke-EnableVSSTracing { - " " - Get-Date - Write-Host "Enabling VSS Tracing..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - logman start vss -o $path\vss.etl -ets -p "{9138500e-3648-4edb-aa4c-859e9f7b7c38}" 0xfff 255 + [OutputType([System.Void])] + param( + [Parameter(Mandatory = $true)] + [string] + $OutputPath, + + [Parameter(Mandatory = $true)] + [bool] + $Circular + ) + + Write-Host "$(Get-Date) Enabling VSS Tracing..." + if ($Circular) { + logman start vss -o $OutputPath\vss.etl -ets -p "{9138500e-3648-4edb-aa4c-859e9f7b7c38}" 0xfff 255 -f bincirc -max 1024 -mode globalsequence + } else { + logman start vss -o $OutputPath\vss.etl -ets -p "{9138500e-3648-4edb-aa4c-859e9f7b7c38}" 0xfff 255 + } } diff --git a/Databases/VSSTester/VSSTester.ps1 b/Databases/VSSTester/VSSTester.ps1 index c719fc97eb..cd746f1cd8 100644 --- a/Databases/VSSTester/VSSTester.ps1 +++ b/Databases/VSSTester/VSSTester.ps1 @@ -1,25 +1,76 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -################################################################################# -# Purpose: -# This script will allow you to test VSS functionality on Exchange server using DiskShadow. -# The script will automatically detect active and passive database copies running on the server. -# The general logic is: -# - start a PowerShell transcript -# - enable ExTRA tracing -# - enable VSS tracing -# - optionally: create the DiskShadow config file with shadow expose enabled, -# execute VSS backup using DiskShadow, -# delete the VSS snapshot post-backup -# - stop PowerShell transcript -# -################################################################################# -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Allowing empty catch blocks for now as we need to be able to handle the exceptions.')] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('CustomRules\AvoidUsingReadHost', '', Justification = 'Do not want to change logic of script as of now')] +<# +.SYNOPSIS + Test and/or trace VSS functionality on Exchange Server. +.DESCRIPTION + Test and/or trace VSS functionality on Exchange Server. +.LINK + https://microsoft.github.io/CSS-Exchange/Databases/VSSTester/ +.EXAMPLE + .\VSSTester -TraceOnly -DatabaseName "Mailbox Database 1637196748" + Enables tracing of the specified database. The user may then attempt a backup of that database + and use Ctrl-C to stop data collection after the backup attempt completes. +.EXAMPLE + .\VSSTester -DiskShadow -DatabaseName "Mailbox Database 1637196748" -DatabaseDriveLetter M -LogDriveLetter N + Enables tracing and then uses DiskShadow to snapshot the specified database. If the database and logs + are on the same drive, the snapshot is exposed as M: drive. If they are on separate drives, the snapshots are + exposed as M: and N:. The user is prompted to stop data collection and should typically wait until + log truncation has occurred before doing so, so that the truncation is traced. +.EXAMPLE + .\VSSTester -WaitForWriterFailure -DatabaseName "Mailbox Database 1637196748" + Enables circular tracing of the specified database, and then polls "vssadmin list writers" once + per minute. When the writer is no longer present, indicating a failure, tracing is stopped + automatically. +#> [CmdletBinding()] param( + # Enable tracing and wait for the user to run a third-party backup solution. + [Parameter(Mandatory = $true, ParameterSetName = "TraceOnly")] + [switch] + $TraceOnly, + + # Enable tracing and perform a database snapshot with DiskShadow. + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [switch] + $DiskShadow, + + # Enable tracing and automatically stop when the Microsoft Exchange Writer fails. + [Parameter(Mandatory = $true, ParameterSetName = "WaitForWriterFailure")] + [switch] + $WaitForWriterFailure, + + # Name of the database to focus tracing on. + [Parameter(Mandatory = $true, ParameterSetName = "TraceOnly")] + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [Parameter(Mandatory = $true, ParameterSetName = "WaitForWriterFailure")] + [string] + $DatabaseName, + + # Drive letter on which to expose the database snapshot. + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [ValidateLength(1, 1)] + [string] + $DatabaseDriveLetter, + + # Drive letter on which to expose the log snapshot. Only used when the log volume + # is different than the database volume. + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [ValidateLength(1, 1)] + [string] + $LogDriveLetter, + + # Path in which to put the collected traces. A subfolder named with the time of + # the data collection is created in this path, and all files are put in that subfolder. + # Defaults to the folder the script is in. + [Parameter(Mandatory = $false, ParameterSetName = "TraceOnly")] + [Parameter(Mandatory = $false, ParameterSetName = "DiskShadow")] + [Parameter(Mandatory = $false, ParameterSetName = "WaitForWriterFailure")] + [string] + $LoggingPath = $PSScriptRoot ) + . $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\Get-ScriptUpdateAvailable.ps1 . $PSScriptRoot\..\..\Shared\Confirm-ExchangeShell.ps1 . .\DiskShadow\Invoke-CreateDiskShadowFile.ps1 @@ -27,7 +78,6 @@ param( . .\DiskShadow\Invoke-RemoveExposedDrives.ps1 . .\ExchangeInformation\Get-CopyStatus.ps1 . .\ExchangeInformation\Get-Databases.ps1 -. .\ExchangeInformation\Get-DbToBackup.ps1 . .\ExchangeInformation\Get-ExchangeVersion.ps1 . .\Logging\Get-WindowsEventLogs.ps1 . .\Logging\Get-VSSWritersAfter.ps1 @@ -40,167 +90,127 @@ param( . .\Logging\Invoke-EnableExtraTracing.ps1 . .\Logging\Invoke-EnableVSSTracing.ps1 -function Main { - $updateInfo = Get-ScriptUpdateAvailable - if ($updateInfo.UpdateFound) { - Write-Warning "An update is available for this script. Current: $($updateInfo.CurrentVersion) Latest: $($updateInfo.LatestVersion)" - Write-Warning "Please download the latest: https://microsoft.github.io/CSS-Exchange/Databases/VSSTester/" - } +$updateInfo = Get-ScriptUpdateAvailable +if ($updateInfo.UpdateFound) { + Write-Warning "An update is available for this script. Current: $($updateInfo.CurrentVersion) Latest: $($updateInfo.LatestVersion)" + Write-Warning "Please download the latest: https://microsoft.github.io/CSS-Exchange/Databases/VSSTester/" +} - # if a transcript is running, we need to stop it as this script will start its own - try { - Stop-Transcript | Out-Null - } catch [System.InvalidOperationException] { } +$Script:LocalExchangeShell = Confirm-ExchangeShell + +if (!$Script:LocalExchangeShell.ShellLoaded) { + Write-Host "Failed to load Exchange Shell. Stopping the script." + exit +} - Write-Host "****************************************************************************************" - Write-Host "****************************************************************************************" - Write-Host "** **" -BackgroundColor DarkMagenta - Write-Host "** VSSTESTER SCRIPT (for Exchange 2013, 2016, 2019) **" -ForegroundColor Cyan -BackgroundColor DarkMagenta - Write-Host "** **" -BackgroundColor DarkMagenta - Write-Host "****************************************************************************************" - Write-Host "****************************************************************************************" +if ($Script:LocalExchangeShell.RemoteShell -or + $Script:LocalExchangeShell.ToolsOnly) { + Write-Host "Can't run this script from a non Exchange Server." + exit +} - $Script:LocalExchangeShell = Confirm-ExchangeShell +$startTime = Get-Date +$startTimeFolderName = $startTime.ToString("yyMMdd-HHmmss") +$LoggingPath = Join-Path $LoggingPath $startTimeFolderName +$serverName = $env:COMPUTERNAME - if (!$Script:LocalExchangeShell.ShellLoaded) { - Write-Host "Failed to load Exchange Shell. Stopping the script." +try { + New-Item -ItemType Directory -Force -Path $LoggingPath | Out-Null + if (-not (Test-Path $LoggingPath)) { + Write-Host "The specified LoggingPath path does not exist. Please enter a valid path." exit } - if ($Script:LocalExchangeShell.RemoteShell -or - $Script:LocalExchangeShell.ToolsOnly) { - Write-Host "Can't run this script from a non Exchange Server." + try { + Start-Transcript -Path "$LoggingPath\vssTranscript.log" + } catch { + Write-Warning "Failed to start transcript. Stopping the script." exit } - #newLine shortcut - $script:nl = "`r`n" - $nl - - $script:serverName = $env:COMPUTERNAME - - #start time - $Script:startInfo = Get-Date - Get-Date - - if ($DebugPreference -ne 'SilentlyContinue') { - $nl - Write-Host 'This script is running in DEBUG mode since $DebugPreference is not set to SilentlyContinue.' -ForegroundColor Red + Get-ExchangeVersion -ServerName $serverName + Get-VSSWritersBefore -OutputPath $LoggingPath + $databases = Get-Databases -ServerName $serverName + $dbForBackup = $databases | Where-Object { $_.Name -eq $DatabaseName } + if ($null -eq $dbForBackup) { + Write-Warning "The specified database $DatabaseName does not exist on this server. Please enter a valid database name." + exit } - $nl - Write-Host "Please select the operation you would like to perform from the following options:" -ForegroundColor Green - $nl - Write-Host " 1. " -ForegroundColor Yellow -NoNewline; Write-Host "Test backup using built-in DiskShadow" - Write-Host " 2. " -ForegroundColor Yellow -NoNewline; Write-Host "Enable logging to troubleshoot backup issues" - $nl - - $matchCondition = "^[1|2]$" - Write-Debug "matchCondition: $matchCondition" - do { - Write-Host "Selection: " -ForegroundColor Yellow -NoNewline; - $Selection = Read-Host - if ($Selection -notmatch $matchCondition) { - Write-Host "Error! Please select a valid option!" -ForegroundColor Red + Get-CopyStatus -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath + + if ($DiskShadow) { + $p = @{ + OutputPath = $LoggingPath + ServerName = $serverName + Databases = $databases + DatabaseToBackup = $dbForBackup + DatabaseDriveLetter = $DatabaseDriveLetter + LogDriveLetter = $LogDriveLetter } + Write-Host "$p" + $exposedDrives = Invoke-CreateDiskShadowFile @p } - while ($Selection -notmatch $matchCondition) - try { + Invoke-EnableDiagnosticsLogging + Invoke-EnableVSSTracing -OutputPath $LoggingPath -Circular $WaitForWriterFailure + Invoke-CreateExTRATracingConfig + Invoke-EnableExTRATracing -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath -Circular $WaitForWriterFailure - $nl - Write-Host "Please specify a directory other than root of a volume to save the configuration and output files." -ForegroundColor Green - - $pathExists = $false - - # get path, ensuring it exists - do { - Write-Host "Directory path (e.g. C:\temp): " -ForegroundColor Yellow -NoNewline - $script:path = Read-Host - Write-Debug "path: $path" - try { - $pathExists = Test-Path -Path "$path" - } catch { } - Write-Debug "pathExists: $pathExists" - if ($pathExists -ne $true) { - Write-Host "Error! The path does not exist. Please enter a valid path." -ForegroundColor red + $collectEventLogs = $false + + try { + if ($DiskShadow) { + # Always collect event logs for this scenario + $collectEventLogs = $true + + Invoke-DiskShadow -OutputPath $LoggingPath -DatabaseToBackup $dbForBackup + Invoke-RemoveExposedDrives -OutputPath $LoggingPath -ExposedDrives $exposedDrives + } elseif ($TraceOnly) { + # Always collect event logs for this scenario + $collectEventLogs = $true + + Write-Host "$(Get-Date) Data Collection" + Write-Host + Write-Host "Data collection is now enabled." + Write-Host "Please start your backup using the third party software so the script can record the diagnostic data." + Write-Host "When the backup is COMPLETE, use Ctrl-C to terminate data collection." + while ($true) { + Start-Sleep 1 + } + } elseif ($WaitForWriterFailure) { + Write-Host "Waiting for Microsoft Exchange Writer failure. Use Ctrl-C to abort." + while ($true) { + if (vssadmin list writers | Select-String "Microsoft Exchange Writer") { + Write-Host "$(Get-Date) Microsoft Exchange Writer is present." + } else { + Write-Host "$(Get-Date) Microsoft Exchange Writer is missing. Stopping data collection." + break + } + + Start-Sleep 60 } - } while ($pathExists -ne $true) - - $nl - Get-Date - Write-Host "Starting transcript..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - - Start-Transcript -Path "$($script:path)\vssTranscript.log" - $nl - - if ($Selection -eq 1) { - Get-ExchangeVersion - Get-VSSWritersBefore - Get-Databases - Get-DBtoBackup - Get-CopyStatus - Invoke-CreateDiskShadowFile #--- - Invoke-EnableDiagnosticsLogging - Invoke-EnableVSSTracing - Invoke-CreateExTRATracingConfig - Invoke-EnableExTRATracing - Invoke-DiskShadow #--- - Get-VSSWritersAfter - Invoke-RemoveExposedDrives #--- - Invoke-DisableExTRATracing - Invoke-DisableDiagnosticsLogging - Invoke-DisableVSSTracing - Get-WindowsEventLogs - } elseif ($Selection -eq 2) { - Get-ExchangeVersion - Get-VSSWritersBefore - Get-Databases - Get-DBtoBackup - Get-CopyStatus - Invoke-EnableDiagnosticsLogging - Invoke-EnableVSSTracing - Invoke-CreateExTRATracingConfig - Invoke-EnableExTRATracing - - #Here is where we wait for the end user to perform the backup using the backup software and then come back to the script to press "Enter", thereby stopping data collection - Get-Date - Write-Host "Data Collection" -ForegroundColor green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - Write-Host "Data collection is now enabled." -ForegroundColor Yellow - Write-Host "Please start your backup using the third party software so the script can record the diagnostic data." -ForegroundColor Yellow - Write-Host "When the backup is COMPLETE, use the key to terminate data collection..." -ForegroundColor Yellow -NoNewline - Read-Host - - Invoke-DisableExTRATracing - Invoke-DisableDiagnosticsLogging - Invoke-DisableVSSTracing - Get-VSSWritersAfter - Get-WindowsEventLogs + + # Only collect event logs if we exited the loop gracefully + $collectEventLogs = $true } } finally { - # always stop our transcript at end of script's execution - # we catch a failure here if we try to stop a transcript that's not running - try { - " " + $nl - Get-Date - Write-Host "Stopping transcript log..." -ForegroundColor Green $nl - Write-Host "--------------------------------------------------------------------------------------------------------------" - " " - Stop-Transcript - " " + $nl - do { - Write-Host - $continue = Read-Host "Please use the key to exit..." - } - while ($null -notmatch $continue) - exit - } catch { } + Write-Host "$(Get-Date) Stopping traces..." + Invoke-DisableExTRATracing -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath + Invoke-DisableDiagnosticsLogging + Invoke-DisableVSSTracing + Write-Host "$(Get-Date) Tracing stopped." + + if ($collectEventLogs) { + Get-VSSWritersAfter -OutputPath $LoggingPath + Get-WindowsEventLogs -StartTime $startTime -ComputerName $ServerName -OutputPath $LoggingPath + } else { + Write-Host "Skipping event log collection, because WaitForWriterFailure was stopped before a writer failure was detected." + } } +} finally { + # always stop our transcript at end of script's execution + Write-Host "$(Get-Date) Stopping transcript log..." + Stop-Transcript -ErrorAction SilentlyContinue + Write-Host "Script completed." } - -try { - Main -} catch { } finally { } diff --git a/Diagnostics/AVTester/Start-SleepWithProgress.ps1 b/Diagnostics/AVTester/Start-SleepWithProgress.ps1 index db1c146476..8790ee787f 100644 --- a/Diagnostics/AVTester/Start-SleepWithProgress.ps1 +++ b/Diagnostics/AVTester/Start-SleepWithProgress.ps1 @@ -42,7 +42,7 @@ function Start-SleepWithProgress { # Loop Number of seconds you want to sleep for ($i = 0; $i -le $SleepTime; $i++) { - $timeLeft = ($SleepTime - $i); + $timeLeft = ($SleepTime - $i) # Progress bar showing progress of the sleep Write-Progress -Activity $Message -CurrentOperation "$timeLeft More Seconds" -PercentComplete (($i / $SleepTime) * 100) -Status " " diff --git a/Diagnostics/ExchangeLogCollector/RemoteScriptBlock/IO/Save-ServerInfoData.ps1 b/Diagnostics/ExchangeLogCollector/RemoteScriptBlock/IO/Save-ServerInfoData.ps1 index a55cf05cef..391d7cf7ff 100644 --- a/Diagnostics/ExchangeLogCollector/RemoteScriptBlock/IO/Save-ServerInfoData.ps1 +++ b/Diagnostics/ExchangeLogCollector/RemoteScriptBlock/IO/Save-ServerInfoData.ps1 @@ -14,7 +14,7 @@ function Save-ServerInfoData { msInfo32.exe /nfo (Add-ServerNameToFileName -FilePath ("{0}\msInfo.nfo" -f $copyTo)) Write-Host "Waiting for msInfo32.exe process to end before moving on..." -ForegroundColor "Yellow" while ((Get-Process | Where-Object { $_.ProcessName -eq "msInfo32" }).ProcessName -eq "msInfo32") { - Start-Sleep 5; + Start-Sleep 5 } $tlsRegistrySettingsName = "TLS_RegistrySettings" diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerHybridInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerHybridInformation.ps1 index cd4c9f325a..2017e3e329 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerHybridInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerHybridInformation.ps1 @@ -298,11 +298,11 @@ function Invoke-AnalyzerHybridInformation { switch ($connector.TlsAuthLevel) { "EncryptionOnly" { - $tlsAuthLevelM365RelayWriteType = "Yellow"; + $tlsAuthLevelM365RelayWriteType = "Yellow" break } "CertificateValidation" { - $tlsAuthLevelM365RelayWriteType = "Green"; + $tlsAuthLevelM365RelayWriteType = "Green" break } "DomainValidation" { @@ -310,7 +310,7 @@ function Invoke-AnalyzerHybridInformation { $tlsAuthLevelM365RelayWriteType = "Red" } else { $tlsAuthLevelM365RelayWriteType = "Green" - }; + } break } default { $tlsAuthLevelM365RelayWriteType = "Red" } diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Tests/Get-ExchangeConnectors.Tests.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Tests/Get-ExchangeConnectors.Tests.ps1 index 06ce9db331..c7085f8c4f 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Tests/Get-ExchangeConnectors.Tests.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Tests/Get-ExchangeConnectors.Tests.ps1 @@ -244,9 +244,9 @@ Describe "Testing Get-ExchangeConnectors.ps1" { It "Send Connector Configured As Expected For Relaying Via M365" { switch ($results) { { ($_.SmartHosts -like "*.mail.protection.outlook.com") } { - $smartHostsPointToExo = $true; - $_.Name | Should -Be "My company to Office 365"; - $_.RequireTLS | Should -Be $true; + $smartHostsPointToExo = $true + $_.Name | Should -Be "My company to Office 365" + $_.RequireTLS | Should -Be $true # 1 = EncryptionOnly; 2 = CertificateValidation; 3 = DomainValidation $_.TlsAuthLevel | Should -Be 2 } diff --git a/Diagnostics/HealthChecker/HealthChecker.ps1 b/Diagnostics/HealthChecker/HealthChecker.ps1 index aa46bf459c..e6018399b5 100644 --- a/Diagnostics/HealthChecker/HealthChecker.ps1 +++ b/Diagnostics/HealthChecker/HealthChecker.ps1 @@ -205,7 +205,7 @@ begin { -not $ScriptUpdateOnly)) { Write-Warning "The script needs to be executed in elevated mode. Start the Exchange Management Shell as an Administrator." $Error.Clear() - Start-Sleep -Seconds 2; + Start-Sleep -Seconds 2 exit } @@ -232,7 +232,7 @@ begin { exit } Get-HtmlServerReport -AnalyzedHtmlServerValues $importData.HtmlServerValues -HtmlOutFilePath $htmlOutFilePath - Start-Sleep 2; + Start-Sleep 2 return } diff --git a/Diagnostics/ManagedAvailabilityTroubleshooter/ManagedAvailabilityTroubleshooter.ps1 b/Diagnostics/ManagedAvailabilityTroubleshooter/ManagedAvailabilityTroubleshooter.ps1 index ec924354d7..d03f95d08f 100644 --- a/Diagnostics/ManagedAvailabilityTroubleshooter/ManagedAvailabilityTroubleshooter.ps1 +++ b/Diagnostics/ManagedAvailabilityTroubleshooter/ManagedAvailabilityTroubleshooter.ps1 @@ -21,8 +21,8 @@ function TestFileOrCmd { param( [String] $FileOrCmd ) if ($FileOrCmd -like "File missing for this action*") { - Write-Host -ForegroundColor red $FileOrCmd; - exit; + Write-Host -ForegroundColor red $FileOrCmd + exit } } @@ -30,7 +30,7 @@ function ParseProbeResult { [CmdletBinding()] param( [String] $FilterXpath , [String] $MonitorToInvestigate , [String] $ResponderToInvestigate) - TestFileOrCmd $ProbeResultEventCmd; + TestFileOrCmd $ProbeResultEventCmd ParseProbeResult2 -ProbeResultEventCompleteCmd ($ProbeResultEventCmd + " -MaxEvents 200" ) ` -FilterXpath $FilterXpath ` -WaitString "Parsing only last 200 probe events for quicker response time" ` @@ -49,7 +49,7 @@ function ParseProbeResult2 { [CmdletBinding()] param( [String] $ProbeResultEventCompleteCmd , [String] $FilterXpath , [String] $WaitString , [String] $MonitorToInvestigate , [String] $ResponderToInvestigate) - TestFileOrCmd $ProbeResultEventCmd; + TestFileOrCmd $ProbeResultEventCmd $ProbeEventsCmd = '(' + $ProbeResultEventCompleteCmd + ' -FilterXPath ("' + $FilterXpath + '") -ErrorAction SilentlyContinue | % {[XML]$_.toXml()}).event.userData.eventXml' Write-Verbose $ProbeEventsCmd $titleProbeEvents = "Probe events" @@ -77,7 +77,7 @@ function ParseProbeResult2 { if ($ProbeEvt.ResultType -eq 4) { $Script:lastProbeError = $ProbeEvt if ($Script:KnownIssueDetectionAlreadyDone -eq $false) { KnownIssueDetection $MonitorToInvestigate $ResponderToInvestigate } - break; + break } } if ($Script:KnownIssueDetectionAlreadyDone -eq $false) { KnownIssueDetection $MonitorToInvestigate $ResponderToInvestigate } @@ -90,7 +90,7 @@ function InvestigateProbe { [CmdletBinding()] param([String]$ProbeToInvestigate , [String]$MonitorToInvestigate , [String]$ResponderToInvestigate , [String]$ResourceNameToInvestigate , [String]$ResponderTargetResource ) - TestFileOrCmd $ProbeDefinitionEventCmd; + TestFileOrCmd $ProbeDefinitionEventCmd if (-Not ($ResponderTargetResource) -and ($ProbeToInvestigate.split("/").Count -gt 1)) { $ResponderTargetResource = $ProbeToInvestigate.split("/")[1] } @@ -183,10 +183,10 @@ function InvestigateMonitor { $MaintenanceFailureMonitor = $MonitorToInvestigate.split(".")[1] Write-Host ("`nThis is triggered by MaintenanceFailureMonitor " + $MaintenanceFailureMonitor) InvestigateMaintenanceMonitor $MaintenanceFailureMonitor $ResponderToInvestigate - break; + break } - TestFileOrCmd $MonitorDefinitionCmd; + TestFileOrCmd $MonitorDefinitionCmd $MonitorDetailsCmd = '(' + $MonitorDefinitionCmd + '| % {[XML]$_.toXml()}).event.userData.eventXml| ? {$_.Name -like "' + $MonitorToInvestigate.split("/")[0] + '*" }' Write-Verbose $MonitorDetailsCmd Write-Progress "Checking Monitor definition" @@ -253,7 +253,7 @@ function InvestigateMaintenanceMonitor { [CmdletBinding()] param([String]$MaintenanceFailureMonitor , [String] $ResponderToInvestigate) - TestFileOrCmd $MaintenanceDefinitionCmd; + TestFileOrCmd $MaintenanceDefinitionCmd $MaintenanceDefinitionCmd = '(' + $MaintenanceDefinitionCmd + '| % {[XML]$_.toXml()}).event.userData.eventXml| ? {$_.ServiceName -like "' + $MaintenanceFailureMonitor + '*" }' Write-Verbose $MaintenanceDefinitionCmd Write-Progress "Checking Maintenance definition" @@ -271,7 +271,7 @@ function InvestigateMaintenanceMonitor { $MaintenanceDetails | Format-List - TestFileOrCmd $MaintenanceResultCmd; + TestFileOrCmd $MaintenanceResultCmd $MaintenanceResultCmd = '(' + $MaintenanceResultCmd + ' -FilterXPath "*/System/Level<=3" | % {[XML]$_.toXml()}).event.userData.eventXml| ? {$_.ResultName -like "' + $MaintenanceDetails.Name + '*" }' Write-Verbose $MaintenanceResultCmd Write-Progress "Checking Maintenance Result warnings and errors" @@ -312,7 +312,7 @@ function OverrideIfNeeded { $continueToCheckIfResponderIsEnabled = $true while ( $continueToCheckIfResponderIsEnabled) { if ("yes", "YES", "Y", "y" -contains (Read-Host ("Do you like to check if " + $ResponderToInvestigate + " Responder is now disabled ? Y/N"))) { - TestFileOrCmd $ResponderDefinitionCmd; + TestFileOrCmd $ResponderDefinitionCmd $ResponderDetailsCmd = '(' + $ResponderDefinitionCmd + '| % {[XML]$_.toXml()}).event.userData.eventXml| ? {$_.Name -eq "' + $ResponderToInvestigate + '" }' Write-Verbose $ResponderDetailsCmd Write-Progress "Checking Responder definition" @@ -364,7 +364,7 @@ function InvestigateResponder { Write-Host "`nIn case it is a reboot , there can be related 1074 events in system log showing that a user forced a rebooted around that time." Write-Host "looking for 1074 events ..." - TestFileOrCmd $SystemCmd; + TestFileOrCmd $SystemCmd $SystemCmd = $SystemCmd + ' -FilterXPath ("*[System[(EventID=''1074'')]]")' Write-Verbose $SystemCmd trap [System.Exception] { continue } @@ -376,7 +376,7 @@ function InvestigateResponder { $1074events | Format-List } } else { - TestFileOrCmd $ResponderDefinitionCmd; + TestFileOrCmd $ResponderDefinitionCmd $ResponderDetailsCmd = '(' + $ResponderDefinitionCmd + '| % {[XML]$_.toXml()}).event.userData.eventXml| ? {$_.Name -eq "' + $ResponderToInvestigate + '" }' Write-Verbose $ResponderDetailsCmd Write-Progress "Checking Responder definition" @@ -462,25 +462,25 @@ function CheckIfThisCanBeAKnownIssueUsingResponder { if (($ResponderToInvestigate -eq "ActiveDirectoryConnectivityConfigDCServerReboot") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 0) -and ($BuildExchangeVersion -lt 775)) { Write-Host -foreground yellow ("There is a known issue with restarts initiated by the ActiveDirectoryConnectivityConfigDCServerReboot prior to CU3 which appears to be your case" ) Write-Host -foreground yellow ("Check https://support.microsoft.com/en-us/kb/2883203" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if ($ResponderToInvestigate -eq "ImapProxyTestCafeOffline") { Write-Host -foreground yellow ("ImapProxyTestCafeOffline can set ImapProxy component as inactive when 127.0.0.1 is blocked in IMAP bindings." ) Write-Host -foreground yellow ("Check ImapSettings using Exchange Powershell command : Get-ImapSettings." ) Write-Host ("Change the settings if needed with Set-ImapSettings - https://technet.microsoft.com/en-us/library/aa998252(v=exchg.150).aspx") - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if ($ResponderToInvestigate -eq "PopProxyTestCafeOffline") { Write-Host -foreground yellow ("PopProxyTestCafeOffline can set POPProxy component as inactive when 127.0.0.1 is blocked in POP bindings." ) Write-Host -foreground yellow ("Check PopSettings using Exchange Powershell command : Get-POPSettings." ) Write-Host -foreground yellow ("Change the settings if needed with Set-POPSettings - https://technet.microsoft.com/en-us/library/aa997154(v=exchg.150).aspx") - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if (($ResponderToInvestigate -eq "OutlookMapiHttpSelfTestRestart") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 0) -and ($BuildExchangeVersion -lt 1130)) { Write-Host -foreground yellow ("There is a known issue with OutlookMapiHttpSelfTestRestart prior to CU10 ( for reference OfficeMain: 1541090)" ) Write-Host -foreground yellow ("You may plan to apply CU10 : https://support.microsoft.com/en-us/kb/3078678" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } @@ -491,21 +491,21 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { $Script:checkForKnownIssue = $true if (($MonitorToInvestigate -like "*Mapi.Submit.Monitor") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 0)) { Write-Host -foreground yellow ("There is a known issue with Mapi.Submit.Monitor. This issue is fixed in CU11 ( OfficeMain: 1956332) " ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if (($MonitorToInvestigate -like "MaintenanceFailureMonitor.Network") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 0) -and ($BuildExchangeVersion -lt 1130)) { Write-Host -foreground yellow ("There is a known issue with MaintenanceFailureMonitor.Network/IntraDagPingProbe is fixed in CU10.( for reference OfficeMain: 2080370)" ) Write-Host -foreground yellow ("You may plan to apply CU10 : https://support.microsoft.com/en-us/kb/3078678" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if (($MonitorToInvestigate -like "MaintenanceFailureMonitor.ShadowService") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 1) ) { Write-Host -foreground yellow ("There is a known issue with MaintenanceFailureMonitor.ShadowService which fix will be included in Exchange 2016 CU5 and upper.( for reference OfficeMain: 142253)" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if (($MonitorToInvestigate -like "EacBackEndLogonMonitor") -and ($MajorExchangeVersion -eq 15) -and ($MinorExchangeVersion -eq 1) ) { Write-Host -foreground yellow ("EacBackEndLogonMonitor has been seen unhealthy linked uninitialized culture on test mailboxes. You may run this command and check if this helps : get-mailbox -Monitoring -server $env:COMPUTERNAME | Set-MailboxRegionalConfiguration -Language En-US -TimeZone ""Pacific Standard Time""" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if ($MonitorToInvestigate -like "ActiveSyncCTPMonitor") { @@ -518,7 +518,7 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { Write-Host ("ActiveSyncCTPMonitor can fail with error 401 when BasicAuthEnabled setting in get-ActiveSyncVirtualDirectory has been changed and set to `$false. - KB 3125818" ) Write-Host("If this is your case , Enable Basic Authentication again if possible using the command :`nSet-ActiveSyncVirtualDirectory -basicAuthEnabled `$true." ) Write-Host("Or disable this monitor using an override :`nAdd-GlobalMonitoringOverride -Identity ActiveSync\ActiveSyncCTPMonitor -ItemType Monitor -PropertyName Enabled -PropertyValue 0" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } @@ -534,7 +534,7 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { Write-Host ("ActiveSyncDeepTestMonitor can fail with Index was out of range error when no active database are found on the server" ) Write-Host("If this is your case , disable this monitor with this command : " ) Write-Host("Add-GlobalMonitoringOverride -Identity ActiveSync\ActiveSyncDeepTestMonitor -ItemType Monitor -PropertyName Enabled -PropertyValue 0" ) - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } @@ -551,7 +551,7 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { Write-Host -foreground yellow "WMI request failing should be : SELECT LastBootUpTime FROM Win32_OperatingSystem WHERE Primary='true'" Write-Host -foreground yellow "This may be investigated at WMI level looking for this request" Write-Host -foreground yellow "This WMI request is planned to be replaced in future version higher than 15.00.1187.000 likely CU13 by direct Windows native call without going through WMI layer.( for reference OfficeMain:2908185)" - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } if ($MonitorToInvestigate -like "ServiceHealthMSExchangeReplEndpointMonitor*") { @@ -566,12 +566,12 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { Write-Host -foreground yellow "ServiceHealthMSExchangeReplEndpointMonitor is failing due to missing DNS entry." Write-Host -foreground yellow "Make sure that the 'Register this connection's addresses in DNS' property is selected on the network adapter" Write-Host -foreground yellow "https://support.microsoft.com/en-us/kb/2969070" - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } if ($MonitorToInvestigate -like "DiscoveryErrorReportMonitor*") { Write-Host -foreground yellow "DiscoveryErrorReportMonitor is Disabled by default and should not be enabled" - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } if ($Script:lastProbeError) { if ($Script:lastProbeError.Exception -like "*The underlying connection was closed*") { @@ -580,7 +580,7 @@ function CheckIfThisCanBeAKnownIssueUsingMonitor { Write-Host -foreground yellow "This has been seen when blocking some TLS version using SecureProtocols registry key or through GPO.`n" Write-Host -foreground yellow "You can check if some TLS version are disabled under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols (https://techcommunity.microsoft.com/t5/exchange-team-blog/exchange-server-tls-guidance-part-2-enabling-tls-1-2-and/ba-p/607761).`n" Write-Host -foreground yellow "You may also check if this is linked with antivirus or local firewall rules.`n" - $Script:foundIssue = $true; return; + $Script:foundIssue = $true; return } } } @@ -591,7 +591,7 @@ function InvestigateUnhealthyMonitor { Write-Progress "Checking MonitorHealth" if ($pathForLogsSpecified) { - TestFileOrCmd $ServerHealthFile; + TestFileOrCmd $ServerHealthFile if ( -not (Test-Path $ServerHealthFile)) { Write-Host -ForegroundColor red ("Path to ServerHealth file is invalid : $ServerHealthFile"); exit } @@ -599,7 +599,7 @@ function InvestigateUnhealthyMonitor { $currentHealthEntry = New-Object -TypeName PSObject $firstLine = $true foreach ($line in (Get-Content $ServerHealthFile)) { - $propName = $line.split(" ")[0]; + $propName = $line.split(" ")[0] if ( -not $propName) { continue; } if ($propName -eq "SerializationData" -or $propName -eq "Result" -or $propName -eq "PSComputerName" -or $propName -eq "PSShowComputerName") { continue; } $newMonitor = $false @@ -622,11 +622,11 @@ function InvestigateUnhealthyMonitor { } if ($propName -eq "RunSpaceId") { continue } - $propValue = ($line.split(":")[1]).split(" ")[1]; + $propValue = ($line.split(":")[1]).split(" ")[1] if ($propValue) { $currentHealthEntry | Add-Member -Name $propName -Value $propValue -MemberType NoteProperty } } } else { - TestFileOrCmd $ServerHealthCmd; + TestFileOrCmd $ServerHealthCmd $ServerHealthCmd = $ServerHealthCmd + '|?{$_.AlertValue -ne "Healthy"}' Write-Verbose $ServerHealthCmd $myHealthEntryList = Invoke-Expression $ServerHealthCmd @@ -849,17 +849,17 @@ if ($pathForLogs) { $RecoveryActionResultsLog = ($Dir | Where-Object { $_.Name -like "*RecoveryActionResults.evtx" }) if ( $RecoveryActionResultsLog.Count -ne 1) { if ($RecoveryActionResultsLog.Count -eq 0) { - $errorMsg = "Can't find RecoveryActionResults evtx file in " + $pathForLogs + " directory. Check the directory"; + $errorMsg = "Can't find RecoveryActionResults evtx file in " + $pathForLogs + " directory. Check the directory" if ($usingLocalPath) { Write-Host -ForegroundColor yellow "Exchange Powershell not loaded.`nIn case you like to analyze directly on the Exchange server , run this script in Exchange Powershell" Write-Host ("No path for logs specified , using local path " + $pathForLogs) } } else { - $errorMsg = "Too much RecoveryActionResults evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much RecoveryActionResults evtx files in " + $pathForLogs + " directory." foreach ($RecoveryActionResultsLogFile in $RecoveryActionResultsLog) { $errorMsg += "`n" + $RecoveryActionResultsLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $RecoveryActionResultsCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $RecoveryActionResultsLog.FullName) @@ -871,11 +871,11 @@ if ($pathForLogs) { if ($ResponderDefinitionLog.Count -eq 0) { $errorMsg = "Can't find ResponderDefinition evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ResponderDefinition evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ResponderDefinition evtx files in " + $pathForLogs + " directory." foreach ($ResponderDefinitionLogFile in $ResponderDefinitionLog) { $errorMsg += "`n" + $ResponderDefinitionLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $ResponderDefinitionCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $ResponderDefinitionLog.FullName) @@ -887,11 +887,11 @@ if ($pathForLogs) { if ($MaintenanceDefinitionLog.Count -eq 0) { $errorMsg = "Can't find MaintenanceDefinition evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much MaintenanceDefinition evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much MaintenanceDefinition evtx files in " + $pathForLogs + " directory." foreach ($MaintenanceDefinitionLogFile in $MaintenanceDefinitionLog) { $errorMsg += "`n" + $MaintenanceDefinitionLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $MaintenanceDefinitionCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $MaintenanceDefinitionLog.FullName) @@ -903,11 +903,11 @@ if ($pathForLogs) { if ($MaintenanceResultLog.Count -eq 0) { $errorMsg = "Can't find MaintenanceResult evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much MaintenanceResult evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much MaintenanceResult evtx files in " + $pathForLogs + " directory." foreach ($MaintenanceResultLogFile in $MaintenanceResultLog) { $errorMsg += "`n" + $MaintenanceResultLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $MaintenanceResultCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $MaintenanceResultLog.FullName) @@ -919,11 +919,11 @@ if ($pathForLogs) { if ($MonitorDefinitionLog.Count -eq 0) { $errorMsg = "Can't find MonitorDefinition evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much MonitorDefinition evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much MonitorDefinition evtx files in " + $pathForLogs + " directory." foreach ($MonitorDefinitionLogFile in $MonitorDefinitionLog) { $errorMsg += "`n" + $MonitorDefinitionLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $MonitorDefinitionCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $MonitorDefinitionLog.FullName) @@ -935,11 +935,11 @@ if ($pathForLogs) { if ($ProbeDefinitionLog.Count -eq 0) { $errorMsg = "Can't find ProbeDefinition evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ProbeDefinition evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ProbeDefinition evtx files in " + $pathForLogs + " directory." foreach ($ProbeDefinitionLogFile in $ProbeDefinitionLog) { $errorMsg += "`n" + $ProbeDefinitionLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $ProbeDefinitionEventCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $ProbeDefinitionLog.FullName) @@ -951,11 +951,11 @@ if ($pathForLogs) { if ($ProbeResultLog.Count -eq 0) { $errorMsg = "Can't find ProbeResult evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ProbeResult evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ProbeResult evtx files in " + $pathForLogs + " directory." foreach ($ProbeResultLogFile in $ProbeResultLog) { $errorMsg += "`n" + $ProbeResultLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $ProbeResultEventCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $ProbeResultLog.FullName) @@ -967,11 +967,11 @@ if ($pathForLogs) { if ($ManagedAvailabilityMonitoringLog.Count -eq 0) { $errorMsg = "Can't find ManagedAvailability Monitoring evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ManagedAvailability Monitoring evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ManagedAvailability Monitoring evtx files in " + $pathForLogs + " directory." foreach ($ManagedAvailabilityMonitoringLogFile in $ManagedAvailabilityMonitoringLog) { $errorMsg += "`n" + $ManagedAvailabilityMonitoringLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $ManagedAvailabilityMonitoringCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $ManagedAvailabilityMonitoringLog.FullName) @@ -983,42 +983,42 @@ if ($pathForLogs) { if ($SystemLog.Count -eq 0) { $errorMsg = "Can't find System evtx file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much System evtx files in " + $pathForLogs + " directory."; + $errorMsg = "Too much System evtx files in " + $pathForLogs + " directory." foreach ($SystemLogFile in $SystemLog) { $errorMsg += "`n" + $SystemLogFile.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $SystemCmd = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $SystemLog.FullName) $SystemCmd = "Get-WinEvent -path ""$SystemLog""" $foundNoLogToAnalyze = $false } - $ServerHealthFile = Get-ChildItem ($pathForLogs + "*ServerHealth_FL.TXT"); + $ServerHealthFile = Get-ChildItem ($pathForLogs + "*ServerHealth_FL.TXT") if ( $ServerHealthFile.Count -ne 1) { if ($ServerHealthFile.Count -eq 0) { $errorMsg = "Can't find ServerHealth_FL TXT file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ServerHealth_FL TXT files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ServerHealth_FL TXT files in " + $pathForLogs + " directory." foreach ($ServerHealthFileInstance in $ServerHealthFile) { $errorMsg += "`n" + $ServerHealthFileInstance.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $ServerHealthFile = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $ServerHealthFile) $foundNoLogToAnalyze = $false } - $GetExchangeServerFile = Get-ChildItem ($pathForLogs + "*_ExchangeServer_FL.TXT"); + $GetExchangeServerFile = Get-ChildItem ($pathForLogs + "*_ExchangeServer_FL.TXT") if ( $GetExchangeServerFile.Count -ne 1) { if ($GetExchangeServerFile.Count -eq 0) { $errorMsg = "Can't find ExchangeServer_FL TXT file in " + $pathForLogs + " directory. Check the directory"; } else { - $errorMsg = "Too much ExchangeServer_FL TXT files in " + $pathForLogs + " directory."; + $errorMsg = "Too much ExchangeServer_FL TXT files in " + $pathForLogs + " directory." foreach ($GetExchangeServerFileInstance in $GetExchangeServerFile) { $errorMsg += "`n" + $GetExchangeServerFileInstance.FullName } } - Write-Host -ForegroundColor red ($errorMsg) ; + Write-Host -ForegroundColor red ($errorMsg) $GetExchangeServerFile = "File missing for this action.`n" + $errorMsg } else { Write-Host ("Found file " + $GetExchangeServerFile) @@ -1026,11 +1026,11 @@ if ($pathForLogs) { { Write-Host -ForegroundColor red ("Path to ServerHealth file is invalid : $GetExchangeServerFile") } else { foreach ($line in (Get-Content $GetExchangeServerFile)) { - $propName = $line.split(" ")[0]; + $propName = $line.split(" ")[0] if ( -not $propName) { continue; } if ($propName -eq "AdminDisplayVersion") { - $exchangeVersion = $line.split(":")[1]; - break; + $exchangeVersion = $line.split(":")[1] + break } } } @@ -1146,7 +1146,7 @@ if ($InvestigationChoose -eq 0 -or $InvestigationChoose -eq 1) { if ([string]::Compare($RecoveryActionToInvestigate.MachineName, $env:COMPUTERNAME, $true) -ne 0) { Write-Host -ForegroundColor yellow ("`nThe RecoveryAction you select is regarding a different server : " + $RecoveryActionToInvestigate.MachineName + " .") Write-Host -ForegroundColor yellow ("Run this script on this server directly to analyze this RecoveryAction further." ) - exit; + exit } } InvestigateResponder $RecoveryActionToInvestigate.RequestorName $RecoveryActionToInvestigate.ResourceName diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index 798cdb841f..03cfa74c54 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -9,28 +9,28 @@ ) Rules = @{ - PSPlaceOpenBrace = @{ + PSPlaceOpenBrace = @{ Enable = $true OnSameLine = $true NewLineAfter = $true IgnoreOneLineBlock = $true } - PSPlaceCloseBrace = @{ + PSPlaceCloseBrace = @{ Enable = $true NewLineAfter = $false IgnoreOneLineBlock = $true NoEmptyLineBefore = $true } - PSUseConsistentIndentation = @{ + PSUseConsistentIndentation = @{ Enable = $true Kind = 'space' PipelineIndentation = 'IncreaseIndentationForFirstPipeline' IndentationSize = 4 } - PSUseConsistentWhitespace = @{ + PSUseConsistentWhitespace = @{ Enable = $true CheckInnerBrace = $true CheckOpenBrace = $true @@ -43,12 +43,16 @@ IgnoreAssignmentOperatorInsideHashTable = $true } - PSAlignAssignmentStatement = @{ + PSAlignAssignmentStatement = @{ Enable = $true CheckHashtable = $true } - PSUseCorrectCasing = @{ + PSUseCorrectCasing = @{ + Enable = $true + } + + PSAvoidSemicolonsAsLineTerminators = @{ Enable = $true } } diff --git a/Performance/ExPerfAnalyzer.ps1 b/Performance/ExPerfAnalyzer.ps1 index 265e6d40eb..41e452a916 100644 --- a/Performance/ExPerfAnalyzer.ps1 +++ b/Performance/ExPerfAnalyzer.ps1 @@ -761,14 +761,14 @@ function Convert-PerformanceCounterSampleObjectToServerPerformanceObjectWithQuic $counterDataObj.DisplayOptions.FormatDivider = $xmlCounter.DisplayOptions.FormatDivider $counterDataObj.DisplayOptions.FormatString = $xmlCounter.DisplayOptions.FormatString #If we find it, we shouldn't need to loop through any longer and we can break out of the XML loop - break; + break } } #Now we need to quick Analyze the data sets while we are in here. # $measured = $counterObj.RawData | Measure-Object -Property CookedValue -Maximum -Minimum -Average ## Bill Long removed this, checking with him to verify why this change was made. - $min = [Int64]::MaxValue; - $max = [Int64]::MinValue; + $min = [Int64]::MaxValue + $max = [Int64]::MinValue foreach ($sample in $counterDataObj.RawData) { if ($sample.CookedValue -lt $min) { $min = $sample.CookedValue } if ($sample.CookedValue -gt $max) { $max = $sample.CookedValue } @@ -1020,7 +1020,7 @@ function Main { break } } - break; + break } "SingleFile" { if (-not (Test-Path $PerfmonFile)) { diff --git a/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 b/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 index 907f522625..b7d2e35d93 100644 --- a/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 +++ b/Security/src/CVE-2023-23397/CVE-2023-23397.ps1 @@ -438,7 +438,7 @@ begin { [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); + return [Microsoft.Exchange.WebServices.Data.Item]::Bind($ExchangeService, $Id, $ps) } function GetAzureApplication { @@ -884,7 +884,7 @@ begin { $pageSize = 100 $findItemsResults = $null do { - $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($pageSize, $offset); + $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($pageSize, $offset) $findItemsResults = $searchFolders[0].FindItems($itemView) foreach ($item in $findItemsResults.Items) { $item @@ -1273,7 +1273,7 @@ begin { } } - $mailboxProcessed ++; + $mailboxProcessed ++ } } } else { @@ -1347,7 +1347,7 @@ begin { } } - $mailboxProcessed ++; + $mailboxProcessed ++ } } diff --git a/Security/src/EOMT.ps1 b/Security/src/EOMT.ps1 index 3abe862cce..4f1cb43c93 100644 --- a/Security/src/EOMT.ps1 +++ b/Security/src/EOMT.ps1 @@ -847,7 +847,7 @@ try { Set-LogActivity -Stage $Stage -RegMessage $RegMessage -Message $Message -Notice } - $DisableAutoUpdateIfNeeded = "If you are getting this error even with updated EOMT, re-run with -DoNotAutoUpdateEOMT parameter"; + $DisableAutoUpdateIfNeeded = "If you are getting this error even with updated EOMT, re-run with -DoNotAutoUpdateEOMT parameter" $Stage = "AutoUpdateEOMT" if ($latestEOMTVersion -and ($BuildVersion -ne $latestEOMTVersion)) { diff --git a/Security/src/EOMTv2.ps1 b/Security/src/EOMTv2.ps1 index 525bf60908..f8f37d446f 100644 --- a/Security/src/EOMTv2.ps1 +++ b/Security/src/EOMTv2.ps1 @@ -572,7 +572,7 @@ try { Set-LogActivity -Stage $Stage -RegMessage $RegMessage -Message $Message -Notice } - $DisableAutoUpdateIfNeeded = "If you are getting this error even with updated EOMTv2, re-run with -DoNotAutoUpdateEOMTv2 parameter"; + $DisableAutoUpdateIfNeeded = "If you are getting this error even with updated EOMTv2, re-run with -DoNotAutoUpdateEOMTv2 parameter" $Stage = "AutoUpdateEOMTv2" if ($latestEOMTv2Version -and ($BuildVersion -ne $latestEOMTv2Version)) { diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 index 47b9d24ebd..7272418109 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 @@ -224,7 +224,7 @@ function Invoke-ConfigureMitigation { $progressParams.PercentComplete = ($counter / $totalCount * 100) $progressParams.Status = "$baseStatus Applying rules" Write-Progress @progressParams - $counter ++; + $counter ++ Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with arguments SiteVDirLocation: {1}, IPRangeAllowListRules : {2}" -f $Server, $SiteVDirLocation, $IPRangeAllowListString) $resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock $ConfigureMitigation -ArgumentList $ScriptBlockArgs diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 index a512edfdef..ded5647cee 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 @@ -159,7 +159,7 @@ function Invoke-RollbackIPFiltering { $progressParams.PercentComplete = ($exchangeServersProcessed / $totalExchangeServers * 100) $progressParams.Status = "$baseStatus Rolling back rules" Write-Progress @progressParams - $exchangeServersProcessed++; + $exchangeServersProcessed++ Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with Arguments Site: {1}, VDir: {2}" -f $Server.Name, $Site, $VDir) Write-Verbose ("Restoring previous state for Server {0}" -f $Server.Name) diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 index 6c8cc17f86..8f8dba5712 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 @@ -180,7 +180,7 @@ function Invoke-ValidateMitigation { $progressParams.PercentComplete = ($counter / $totalCount * 100) $progressParams.Status = "$baseStatus Validating rules" Write-Progress @progressParams - $counter ++; + $counter ++ Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with arguments SiteVDirLocations: {1}, ipRangeAllowListRules: {2}" -f $Server, [string]::Join(", ", $SiteVDirLocations), $ipRangeAllowListString) $resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock $ValidateMitigationScriptBlock -ArgumentList $ScriptBlockArgs diff --git a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Invoke-ExtendedProtectionTlsPrerequisitesCheck.ps1 b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Invoke-ExtendedProtectionTlsPrerequisitesCheck.ps1 index 4ff264586f..11fd2f993d 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Invoke-ExtendedProtectionTlsPrerequisitesCheck.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Invoke-ExtendedProtectionTlsPrerequisitesCheck.ps1 @@ -62,7 +62,7 @@ function Invoke-ExtendedProtectionTlsPrerequisitesCheck { if ($null -ne $result) { Write-Verbose "Found difference in TLS for $key" $nextServer = $true - break; + break } } diff --git a/Shared/Get-ExtendedProtectionConfiguration.ps1 b/Shared/Get-ExtendedProtectionConfiguration.ps1 index 7d4657c76f..94d1fd7af7 100644 --- a/Shared/Get-ExtendedProtectionConfiguration.ps1 +++ b/Shared/Get-ExtendedProtectionConfiguration.ps1 @@ -72,7 +72,7 @@ function Get-ExtendedProtectionConfiguration { if ($SiteVDirLocation -eq "$($WebSite[$i])/$virtualDirectory") { Write-Verbose "Set Extended Protection to None because of restriction override '$($WebSite[$i])\$virtualDirectory'" $ExtendedProtection[$i] = "None" - break; + break } } } diff --git a/Transport/Compute-TopExoRecipientsFromMessageTrace.ps1 b/Transport/Compute-TopExoRecipientsFromMessageTrace.ps1 index 195f1b4126..1291ee26f4 100644 --- a/Transport/Compute-TopExoRecipientsFromMessageTrace.ps1 +++ b/Transport/Compute-TopExoRecipientsFromMessageTrace.ps1 @@ -54,7 +54,7 @@ $CreateHourlyReport = if ($hourlyEvent.RecipientAddress -eq $_.RecipientAddress -and $hourlyEvent.Hour -eq $_.Received.Hour) { $hourlyEvent.MessageCount +=1 } else { - $eventObj = New-Object PSObject -Property @{ Hour=$_.Received.Hour; Date=$_.Received.Date.ToString("dd/mm/yyyy dd:hh tt"); MessageCount=1; RecipientAddress=$_.RecipientAddress }; + $eventObj = New-Object PSObject -Property @{ Hour=$_.Received.Hour; Date=$_.Received.Date.ToString("dd/mm/yyyy dd:hh tt"); MessageCount=1; RecipientAddress=$_.RecipientAddress } [void]$hourlyReport.Add($eventObj) } } @@ -93,5 +93,5 @@ $props = [ordered]@{ 'HourlyReport' = $hourlyReport 'MessageTraceEvents' = $eventList } -$results = New-Object -TypeName PSObject -Property $props; +$results = New-Object -TypeName PSObject -Property $props return $results diff --git a/docs/Calendar/Check-SharingStatus.md b/docs/Calendar/Check-SharingStatus.md new file mode 100644 index 0000000000..314048a531 --- /dev/null +++ b/docs/Calendar/Check-SharingStatus.md @@ -0,0 +1,21 @@ +# Check-SharingStatus + +Download the latest release: [Check-SharingStatus.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Check-SharingStatus.ps1) + +This script runs a variety of PowerShell cmdlets to validate the sharing relationship between two users. + +Terminology: + Owner - this is the mailbox that owns the Calendar being shared. + Receiver - this is the mailbox 'viewing' the owner calendar. + +First item is to determine what kind of sharing relationship the users have. + Modern Sharing (New Model Sharing) - Recipient gets a replicated copy of the Owners Calendar in their MB + Old Model Sharing – Recipient is granted rights but have so connect to the Owners server to get Calendar information. + External Sharing – Can be New or Old Model sharing, but outside of the Exchange Online Tenant / Organization. + Publishing – Owner publishes a link to their calendar, which clients can pull. + +Next you need to determine if the relationship is healthy. + Look at the logs and output included in the script. + +Last you need to look at how it is working. Generally, you will get Calendar Logs from Owner and Receiver for a copied meeting and check replication times, etc. + See [CalLogSummaryScript](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Get-CalendarDiagnosticObjectsSummary.ps1) diff --git a/docs/Databases/VSSTester.md b/docs/Databases/VSSTester.md index bbff4e2af7..1ea1c2d411 100644 --- a/docs/Databases/VSSTester.md +++ b/docs/Databases/VSSTester.md @@ -2,14 +2,37 @@ Download the latest release: [VSSTester.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/VSSTester.ps1) -The script is self-explanatory. You can test run DiskShadow on a single Exchange database to ensure backups are working properly (i.e. all the Microsoft components). If the issue only happens with a 3rd-party backup solution, you can utilize operation mode 2 to enable just the logging while you execute a backup with the 3rd-party solution. +## Usage -![Start Screen](start_screen.PNG) +### Trace while using a third-party backup solution + +`.\VSSTester -TraceOnly -DatabaseName "Mailbox Database 1637196748"` + +Enables tracing of the specified database. The user may then attempt a backup of that database +and use Ctrl-C to stop data collection after the backup attempt completes. + +### Trace a snapshot using the DiskShadow tool + +`.\VSSTester -DiskShadow -DatabaseName "Mailbox Database 1637196748" -DatabaseDriveLetter M -LogDriveLetter N` + +Enables tracing and then uses DiskShadow to snapshot the specified database. If the database and logs +are on the same drive, the snapshot is exposed as M: drive. If they are on separate drives, the snapshots are +exposed as M: and N:. The user is prompted to stop data collection and should typically wait until +log truncation has occurred before doing so, so that the truncation is traced. + +### Trace in circular mode until the Microsoft Exchange Writer fails + +`.\VSSTester -WaitForWriterFailure -DatabaseName "Mailbox Database 1637196748"` + +Enables circular tracing of the specified database, and then polls "vssadmin list writers" once +per minute. When the writer is no longer present, indicating a failure, tracing is stopped +automatically. ## More information * https://techcommunity.microsoft.com/t5/exchange-team-blog/troubleshoot-your-exchange-2010-database-backup-functionality/ba-p/594367 * https://techcommunity.microsoft.com/t5/exchange-team-blog/vsstester-script-updated-8211-troubleshoot-exchange-2013-and/ba-p/610976 +Note that script syntax and output has changed. Syntax and screenshots in the above articles are out of date. ## COM+ Security diff --git a/docs/Databases/start_screen.PNG b/docs/Databases/start_screen.PNG deleted file mode 100644 index 71d6df2412..0000000000 Binary files a/docs/Databases/start_screen.PNG and /dev/null differ diff --git a/docs/Diagnostics/HealthChecker/DownloadDomainCheck.md b/docs/Diagnostics/HealthChecker/DownloadDomainCheck.md index 8cc87f0c79..197f32d37b 100644 --- a/docs/Diagnostics/HealthChecker/DownloadDomainCheck.md +++ b/docs/Diagnostics/HealthChecker/DownloadDomainCheck.md @@ -10,11 +10,13 @@ If the feature is enabled, we validate if the URL configured to download attachm The `Download Domain` feature is available on Microsoft Exchange Server 2016 and Microsoft Exchange Server 2019. +More details and configuration instructions can be found in the Microsoft Learn article, that is linked in the additional resource section. + **Included in HTML Report?** Yes **Additional resources:** -[How to configure the Download Domain feature (see FAQ section)](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-1730) +[Configure Download Domains in Exchange Server](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-download-domains) diff --git a/docs/Diagnostics/HealthChecker/SerializedDataSigningCheck.md b/docs/Diagnostics/HealthChecker/SerializedDataSigningCheck.md index 12430bf087..754c752492 100644 --- a/docs/Diagnostics/HealthChecker/SerializedDataSigningCheck.md +++ b/docs/Diagnostics/HealthChecker/SerializedDataSigningCheck.md @@ -2,54 +2,15 @@ ## Description -Certificate-based signing of PowerShell Serialization Payload is a Defense-in-Depth security feature to prevent malicious manipulation of serialized data exchanged in Exchange Management Shell (EMS) sessions. +Certificate-based signing of PowerShell Serialization Payload is a defense-in-depth security feature to prevent malicious manipulation of serialized data exchanged in Exchange Management Shell (EMS) sessions. -The Serialized Data Signing feature was introduced with the January 2023 Exchange Server Security Update (SU). It's available on Exchange Server 2013, Exchange Server 2016 and Exchange Server 2019. +The Serialized Data Signing feature was introduced with the January 2023 Exchange Server Security Update (SU). It's available on Exchange Server 2013, Exchange Server 2016 and Exchange Server 2019 and enabled by default with the November 2023 Security Update. -In the first stage of rollout, this feature needs to be manually enabled by the Exchange Server administrator. This can be done by following the steps outlined below. +The HealthChecker check validates that the feature is enabled on supported Exchange builds. -The HealthChecker check validates that the feature is enabled on supported Exchange builds. It will also check if multiple `SettingOverrides` are available that collide with each other. +!!! success "Documentation Moved" -### Important - -Ensure all the Exchange Servers (Exchange Server 2019, 2016 and 2013) in the environment are running the January 2023 (or later) SU before turning the feature on. Enabling the feature before all servers are updated might lead to failures and errors when managing your organization. - -This features uses the `Exchange Server Auth Certificate` to sign the serialized data. Therefore, it's very important that the certificate which is configured as Auth Certificate is valid (not expired) and available on all Exchange Servers (except Edge Transport role and Exchange Management Tools role) within the organization. - -### Exchange Server 2013 -The feature must be enabled on a per-server base by creating the following `registry value`: - -Key: `HKLM\SOFTWARE\Microsoft\ExchangeServer\v15\Diagnostics\` - -ValueType: `String` - -Value: `EnableSerializationDataSigning` - -Data: `1` - -You can create the required string value by running the following PowerShell command: - -`New-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Diagnostics -Name "EnableSerializationDataSigning" -Value 1 -Type String` - -### Exchange Server 2016/2019 -The feature can be enabled on an organizational level (strongly recommended) or per-server base via `SettingOverride`: - -Organizational-wide: `New-SettingOverride -Name "EnableSigningVerification" -Component Data -Section EnableSerializationDataSigning -Parameters @("Enabled=true") -Reason "Enabling Signing Verification"` - -Per-server base: `New-SettingOverride -Name "EnableSigningVerification" -Component Data -Section EnableSerializationDataSigning -Parameters @("Enabled=true") -Reason "Enabling Signing Verification" -Server ` - -Next, refresh the VariantConfiguration argument by running the following cmdlet: -`Get-ExchangeDiagnosticInfo -Process Microsoft.Exchange.Directory.TopologyService -Component VariantConfiguration -Argument Refresh` - -### Required on Exchange 2013, 2016 and 2019 after the feature was enabled (via Registry Value or VariantConfiguration) -Restart the `World Wide Web Publishing service` and the `Windows Process Activation Service (WAS)` to apply the new settings. To do this, run the following cmdlet: -`Restart-Service -Name W3SVC, WAS -Force` - -**NOTE:** - -Exchange 2016/2019: It's sufficient to restart the services on the server where the change was made. - -Exchange 2013: It's required to restart these services on all Exchange 2013 servers whenever the registry value is updated. + This documentation has been moved to Microsoft Learn. Please read [Configure certificate signing of PowerShell serialization payloads in Exchange Server](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-serialization-payload-sign) for more information. ## Included in HTML Report? @@ -59,6 +20,6 @@ Yes [Released: January 2023 Exchange Server Security Updates](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-january-2023-exchange-server-security-updates/ba-p/3711808) -[Certificate signing of PowerShell serialization payload in Exchange Server](https://support.microsoft.com/kb/5022988) +[Released: November 2023 Exchange Server Security Updates](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2023-exchange-server-security-updates/ba-p/3980209) [MonitorExchangeAuthCertificate.ps1 script](https://aka.ms/MonitorExchangeAuthCertificate) diff --git a/docs/Security/Extended-Protection.md b/docs/Security/Extended-Protection.md index c567026251..82998a2cb2 100644 --- a/docs/Security/Extended-Protection.md +++ b/docs/Security/Extended-Protection.md @@ -1,439 +1,5 @@ # Exchange Server Support for Windows Extended Protection -## Overview +!!! success "Documentation Moved" -[Windows Extended Protection](https://docs.microsoft.com/iis/configuration/system.webserver/security/authentication/windowsauthentication/extendedprotection/) enhances the existing authentication in Windows Server and mitigates authentication relay or "man in the middle" (MitM) attacks. This mitigation is accomplished by using security information that is implemented through Channel-binding information specified through a Channel Binding Token (CBT) which is primarily used for SSL connections. - -Windows Extended Protection is supported on Exchange Server 2013, 2016 and 2019 starting with the [August 2022 Exchange Server Security Update (SU) releases](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2022-exchange-server-security-updates/ba-p/3593862). - -While Extended Protection can be enabled on each virtual directory manually, we have provided a script that can help you accomplish this in bulk. Windows Extended Protection is supported on Exchange Server 2013, 2016 and 2019 starting with the [August 2022 Exchange Server Security Update (SU) releases](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2022-exchange-server-security-updates/ba-p/3593862). - -## Terminology used in this document - -**Virtual Directory, or vDir,** is used by Exchange Server to allow access to web applications such as Exchange ActiveSync, Outlook on the Web, and the Autodiscover service. Several virtual directory settings can be configured by an admin, including authentication, security, and reporting settings. Extended Protection is one such authentication setting. - -**The Extended Protection setting** controls the behavior for checking of CBTs. Possible values for this setting are listed in the following table: - -| **Extended Protection Setting** | **Description** | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| None | Specifies that IIS will not perform CBT checking. | -| Allow | Specifies that CBT checking is enabled, but not required. This setting allows secure communication with clients that support extended protection, and still supports clients that are not capable of using extended protection. | -| Require | This value specifies that CBT checking is required. This setting blocks clients that do not support extended protection. | - -**SSL Flags**: Configuration of SSL settings is required to ensure that clients connect to IIS virtual directories in a specific way with client certificates. To enable Extended Protection, the required SSL flags are SSL and SSL128. - -**SSL offloading** terminates the connection on a device between the client and the Exchange Server and then uses a non-encrypted connection to connect to the Exchange Server. - -Example: - -```mermaid -flowchart LR - A[Client] ==>|HTTPS| B - B["Device (e.g., Load Balancer) terminates the connection"] ==>|HTTP| C["Web Server"] -``` - -**SSL bridging** is a process where a device, usually located at the edge of a network, decrypts SSL traffic, and then re-encrypts it before sending it on to the Web server. - -Example: - -```mermaid -flowchart LR - A[Client] ==>|HTTPS| B - B["Device (e.g., Load Balancer) terminates the connection"] ==>|HTTPS| C["Web Server"] -``` - -**Modern Hybrid** or **Hybrid Agent** is a mode of configuring Exchange Hybrid that removes some of the configuration requirements for Classic Hybrid (like Inbound network connections through your firewall) to enable Exchange hybrid features. You can learn more about this [here](https://docs.microsoft.com/exchange/hybrid-deployment/hybrid-agent). - -**Public Folders** are designed for shared access and to help make content in a deep hierarchy easier to browse. You can learn more about Public Folders [here](https://docs.microsoft.com/exchange/collaboration/public-folders/public-folders?view=exchserver-2019). - -## Prerequisites for enabling Extended Protection on Exchange servers - -### Make sure you are on the correct versions - -Extended Protection is supported on Exchange Server 2013 CU23 and Exchange Server 2016 CU22 and Exchange Server 2019 CU11 or later with the August 2022 Security Updates installed. - -If your organization has Exchange Server 2016 or Exchange Server 2019 installed, it must be running either the [September 2021 Quarterly Exchange Updates](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-september-2021-quarterly-exchange-updates/ba-p/2779883) (CU) with the August 2022 Security Update (SU) or later installed or the [2022 H1 Cumulative Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-2022-h1-cumulative-updates-for-exchange-server/ba-p/3285026) (CU) with the August 2022 Security Update (SU) or later installed. - -If your organization has Exchange Server 2013 installed, it must be running [CU23](https://www.microsoft.com/download/details.aspx?id=58392) with the August 2022 SU (or later) installed. - -You **must** ensure **all** your Exchange servers are on the required CU and have the August 2022 SU (or later) before you proceed further. - -### Extended Protection cannot be enabled on Exchange Server 2013 servers with Public Folders in a coexistence environment - -To enable Extended Protection on Exchange Server 2013, ensure you do not have any Public Folders on Exchange Server 2013. If you have coexistence of Exchange Server 2013 with Exchange Server 2016 or Exchange Server 2019, you must migrate your Public Folders to 2016 or 2019 **before** enabling Extended Protection. After enabling Extended Protection, if there are Public Folders on Exchange 2013, they will no longer appear to end users. - -### Extended Protection cannot be enabled on Exchange Server 2016 CU22 or Exchange Server 2019 CU11 or older that hosts a Public Folder Hierarchy - -If you have an environment containing Exchange Server 2016 CU22 or Exchange Server 2019 CU11 or older and are utilizing Public Folders, before enabling extended protection **you must confirm the version of the server hosting the Public Folder hierarchy**. Ensure the server hosting the Public Folder hierarchy is upgraded to Exchange Server 2016 CU23 or Exchange Server 2019 CU12 with the latest Security Updates or move the hierarchy to one with these latest versions and updates. - -The following table should help clarify: - -| Exchange version | CU installed | SU installed | Hosts PF mailboxes | Is EP supported? | -| ----------------- | --------------- | -------------------- | ---------------------- | ---------------- | -| Exchange 2013 | CU23 | Aug 2022 (or higher) | No | Yes | -| Exchange 2016 | CU22 | Aug 2022 (or higher) | No hierarchy mailboxes | Yes | -| Exchange 2016 | CU23+ (2022 H1) | Aug 2022 (or higher) | Any | Yes | -| Exchange 2019 | CU11 | Aug 2022 (or higher) | No hierarchy mailboxes | Yes | -| Exchange 2019 | CU12+ (2022 H1) | Aug 2022 (or higher) | Any | Yes | -| Any other version | Any other CU | Any other SU | Any | No | - -### Extended Protection does not work with hybrid servers using Modern Hybrid configuration - -Extended Protection cannot be enabled on Hybrid Servers which uses Modern Hybrid configuration. In Modern Hybrid configuration, Hybrid Server are published to Exchange Online via Hybrid Agent which proxies the Exchange Online call to Exchange Server. - -Enabling Extended Protection on Hybrid servers using Modern Hybrid configuration will lead to disruption of hybrid features like mailbox migrations and Free/Busy. Hence, it is important to identify all the Hybrid Servers in the organization published via Hybrid Agent and not enable Extended Protection specifically on these servers. - -#### Identifying hybrid Exchange servers published using Hybrid Agent - -!!! warning "Note" - - This step is not required if you are using classic Hybrid configuration. - -In case you don't have a list of servers published via Hybrid Agent, you can use the following steps to identify them: - -1. Log into a machine where the Hybrid Agent is installed and running. Open the [PowerShell module](https://docs.microsoft.com/exchange/hybrid-deployment/hybrid-agent#hybrid-agent-powershell-module) of the Hybrid Agent and run _Get-HybridApplication_ to identify the _TargetUri_ used by the Hybrid Agent. -2. The _TargetUri_ parameter gives you the FQDN of the Exchange Server that is configured to use Hybrid Agent. - 1. Deduce the Exchange Server identity using the FQDN and make a note of this Exchange Server. - 2. If you are using a Load Balancer URL in _TargetUri_, you need to identify all the Exchange servers running the Client Access role behind the load balancer URL. - -Extended Protection **should not be enabled for hybrid servers that are published using Hybrid Agent**. You need to identify these hybrid servers and ensure you skip enabling Extended Protection on them using the SkipExchangeServerNames parameter of the script. - -#### Steps to safeguard hybrid servers using Modern Hybrid - -1. Inbound connections to Exchange servers in a Modern Hybrid configuration should be restricted via firewall to allow connections only from Hybrid Agent machines. -2. No mailboxes should be hosted on the hybrid server, and if any mailbox exists, they should be migrated to other mailbox servers. -3. You can enable Extended Protection on all virtual directories except Front End EWS on the hybrid Exchange server. - -!!! warning "Note" - - Specifically skipping extended protection on Front End EWS of Exchange Server is not supported via script. So, you would need to change this setting manually. - -### NTLMv1 is not supported when Extended Protection is enabled - -!!! warning "Note" - - To increase security, we recommend that you review and configure this setting regardless of whether you experience problems or not. - -NTLMv1 is weak and doesn't provide protection against man-in-the-middle (MitM) attacks. It should be [considered as vulnerable](https://support.microsoft.com/topic/security-guidance-for-ntlmv1-and-lm-network-authentication-da2168b6-4a31-0088-fb03-f081acde6e73) and so, no longer be used. Therefore NTLMv1 should not be used together with Extended Protection. Additionally, if you enforce a client to use NTLMv1 instead of NTLMv2 and you have Extended Protection enabled on your Exchange server, this will lead to password prompts on the client side without a way to authenticate successfully against Exchange. - -If you experience password prompts on your clients once Extended Protection is enabled, you should check the following registry key and value on client and Exchange server side: - -Registry key: `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa` - -Registry value: `LmCompatibilityLevel` - -It must be set to at least `3` or higher (best practice is to set it to `5` which is: _Send NTLMv2 response only. Refuse LM & NTLM_). It's also possible to delete this value to enforce the system default. If it's not set, we treat it as if it is set to `3` (on Windows Server 2008 R2 and later) which is: _Send NTLMv2 response only_. -If you want to manage the setting centrally, you can do so via Group Policy: - -Policy location: `Computer Configuration\Windows Settings\Security Settings\Local Policies\Security Options` - -More information: [Network security: LAN Manager authentication level](https://docs.microsoft.com/windows/security/threat-protection/security-policy-settings/network-security-lan-manager-authentication-level) - -### SSL Offloading scenarios are not supported - -Extended Protection is not supported in environments that use SSL offloading. SSL termination during SSL offloading causes Extended Protection to fail. To enable Extended Protection in your Exchange environment, **you must not be using SSL offloading** with your Load Balancers. - -### SSL Bridging supported scenarios - -Extended Protection is supported in environments that use SSL Bridging under certain conditions. To enable Extended Protection in your Exchange environment using SSL Bridging, **you must use the same SSL certificate on Exchange and your Load Balancers**. If not this will cause Extended Protection to fail. - -### TLS configuration must be consistent across all Exchange servers - -Before enabling Extended Protection, you must ensure that all TLS configurations are consistent across all Exchange servers. For example, if one of the servers uses TLS 1.2, you must ensure that all the servers in the organization are configured using TLS 1.2. Any variation in TLS version use across servers can cause client connections to fail. - -In addition to this, the value of _SchUseStrongCrypto_ registry value must be set to 1 across all the Exchange Servers in the organization. -If this value is not explicitly set to 1, the default value of this key may be interpreted as 0 or 1 depending on the .NET version in use by the Exchange binaries. -The same applies to the _SystemDefaultTlsVersions_ registry value which must also be explicitly set to 1. If they aren't set as expected, this can cause TLS mismatch and so, leading to client connectivity issues. - -Please refer to this [guide](https://docs.microsoft.com/Exchange/exchange-tls-configuration?view=exchserver-2019) to configure the required TLS settings on your Exchange servers. - -### Third-party software compatibility - -Please ensure to test all third-party products in your Exchange Server environment to ensure that they work properly when Extended Protection is enabled. For example we have seen AntiVirus solutions send connections through a proxy in order to protect the client machine, this would prevent communication to the Exchange Server and would need to be disabled. - -## Enabling Extended Protection - -Extended Protection can be enabled manually through IIS Manager or via a script (strongly recommended). To correctly configure Extended Protection, each virtual directory on all Exchange servers in the organization (excluding Edge Transport servers) should be set to prescribed value of Extended Protection as well as sslFlags. The following table summarizes the settings needed for each virtual directory on the supported versions of Microsoft Exchange. - -Enabling Extended Protection involves making many changes on all Exchange servers, so **we strongly recommend using the ExchangeExtendedProtectionManagement.ps1 script** that can be downloaded from . - -| IIS Website | Virtual Directory | Recommended Extended Protection | Recommended sslFlags | -| ---------------- | --------------------------- | ------------------------------- | --------------------------- | -| Default Website | API | Required | Ssl,Ssl128 | -| Default Website | AutoDiscover | Off | Ssl,Ssl128 | -| Default Website | ECP | Required | Ssl,Ssl128 | -| Default Website | EWS | Accept (UI) /Allow (Script) | Ssl,Ssl128 | -| Default Website | MAPI | Required | Ssl,Ssl128 | -| Default Website | Microsoft-Server-ActiveSync | Accept (UI) /Allow (Script) | Ssl,Ssl128 | -| Default Website | OAB | Accept (UI) /Allow (Script) | Ssl,Ssl128 | -| Default Website | OWA | Required | Ssl,Ssl128 | -| Default Website | PowerShell | Required | SslNegotiateCert | -| Default Website | RPC | Required | Ssl,Ssl128 | -| Exchange Backend | API | Required | Ssl,Ssl128 | -| Exchange Backend | AutoDiscover | Off | Ssl,Ssl128 | -| Exchange Backend | ECP | Required | Ssl,Ssl128 | -| Exchange Backend | EWS | Required | Ssl,Ssl128 | -| Exchange Backend | Microsoft-Server-ActiveSync | Required | Ssl,Ssl128 | -| Exchange Backend | OAB | Required | Ssl,Ssl128 | -| Exchange Backend | OWA | Required | Ssl,Ssl128 | -| Exchange Backend | PowerShell | Required | Ssl,SslNegotiateCert,Ssl128 | -| Exchange Backend | RPC | Required | Ssl,Ssl128 | -| Exchange Backend | PushNotifications | Required | Ssl,Ssl128 | -| Exchange Backend | RPCWithCert | Required | Ssl,Ssl128 | -| Exchange Backend | MAPI/emsmdb | Required | Ssl,Ssl128 | -| Exchange Backend | MAPI/nspi | Required | Ssl,Ssl128 | - -!!! warning "Note" - - After initial release, we have updated `Default Website/OAB` to be `Accept/Allow` instead of `Required`. This is because of Outlook for Mac clients not being able to download the OAB any longer with the `Required` setting. - -SSL offloading for Outlook Anywhere is enabled by default and must be disabled for extended protection by following the steps shown [here](https://docs.microsoft.com/powershell/module/exchange/set-outlookanywhere?view=exchange-ps#example-3). - -### Enabling Extended Protection using the script - -Before enabling Extended Protection in your Exchange environment, ensure you meet all the prerequisites listed in this document. - -To enable Extended Protection on all your Exchange Servers, you can use the [ExchangeExtendedProtectionManagement.ps1](https://aka.ms/ExchangeEPScript) script, which is hosted on the Microsoft Exchange-CSS repository on GitHub. - -It's not required to run the script directly on any specific Exchange Server in your environment. Just copy it to a machine that has the Exchange Management Shell (EMS) installed. - -!!! warning "Note" - - Over time, we will be updating the script and documentation. The script will attempt to auto-update when it is run. If the computer where the script is run is not connected to the Internet, this update check will fail. You should always check for the latest version of the script before running it. - -#### Parameters - -If the script is executed without any parameters, it will enable Extended Protection on any Exchange Server that can be reached from the machine where the script was run. You can use the following parameters to specify the scope of script operations: - -| Parameter | Usage | -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ExchangeServerNames | Used to specify which Exchange servers should be **included in the scope of script execution**. It can be either a single Exchange server hostname or a comma separated list of hostnames. Parameter values: **Exchange Server Hostname (NetBIOS or FQDN)** | -| SkipExchangeServerNames | Used to specify which Exchange servers should be **excluded from the scope of script execution**. It can be either a single Exchange Server hostname or a comma separated list of hostnames. Parameter values: **Exchange Server Hostname (NetBIOS or FQDN)** | -| RollbackType | Used to revert changes made by the Extended Protection script. Parameter Values: **"RestoreIISAppConfig, RestrictTypeEWSBackend"**" | -| ShowExtendedProtection | Used to display the current Extended Protection configuration state in your organization or on a specific computer (use the _ExchangeServerNames_ or _SkipExchangeServerNames_ parameter to show the configuration for a subset of Exchange servers). | -| RestrictType | Used to restrict incoming IP connections on specified vDir Parameter Value: **EWSBackend**: This parameter should be used to restrict incoming IP connections to a specified allow list of IP addresses or Subnets. This will also turn off EP on EWSBackend. **Note:** This parameter should no longer be used. | -| IPRangeFilePath | This is a mandatory parameter which must be used to provide an allow list of IP ranges when RestrictType parameter is used. The filepath provides should be of a .TXT file with IP addresses or subnets. | -| ValidateType | Used to cross check allow list of IP addresses on vDir against IPList file provided in IPRangeFilePath. Parameter Value: **RestrictTypeEWSBackend**: should be used to cross check allow list of IP addresses on EWS Backend vDir against IPList file provided in IPRangeFilePath. | -| FindExchangeServerIPAddresses | Used to create list of IPv4 and IPv6 addresses of all Exchange Servers in the organization. | - -#### Enabling Extended Protection on all Exchange servers - -After copying the script to a suitable machine, create a directory to store the script and move the script there. Then open the Exchange Management Shell (EMS) and go to the respective directory where the script is stored. - -Make sure that the account you are using is a member of the **Organization Management** role group. - -Execute the script as follows: - -`.\ExchangeExtendedProtectionManagement.ps1` - -In case that you have Modern Hybrid configuration, you need to skip Exchange servers published using the Hybrid Agent. This can be done by using the _SkipExchangeServerNames_ parameter: - -`.\ExchangeExtendedProtectionManagement.ps1 -SkipExchangeServerNames HybridServer1, HybridServer2` - -The script will check to ensure that all Exchange servers in scope have the minimum CU and SU required to enable Extended Protection. - -The script will also check if all Exchange servers in scope have the same TLS configuration. An inconsistent (or invalid) TLS configuration will cause client connections or connections to Exchange Online to fail. - -After the prerequisites checks have been passed, the script will enable Extended Protection and add the required SSL flags on all virtual directories of all Exchange servers in scope. - -![Text Description automatically generated](attachments/9a4e6863e860e064d2831d7d714c95ce.png) - -#### Scenario 1: Using Modern Hybrid Configuration or Hybrid Agent - -In case you have Modern Hybrid configuration, you need to skip Exchange servers published using the Hybrid Agent. This can be done by using the _SkipExchangeServerNames_ parameter: - -`.\ExchangeExtendedProtectionManagement.ps1 -SkipExchangeServerNames HybridServer1, HybridServer2` - -Or - -`.\ExchangeExtendedProtectionManagement.ps1 -RestrictType EWSBackend -IPRangeFilePath "IPList.txt" -SkipExchangeServerNames HybridServer1, HybridServer2` - -#### Troubleshooting warnings and errors during script execution - -1. Script gives a cursory warning of known issues before enabling Extended Protection: - - To prevent a scenario where existing Exchange functions are disrupted due to enabling Extended Protection, the script provides a list of scenarios that have known issues. You should **read and evaluate this list carefully** before enabling Extended Protection. - You can proceed to turn on Extended Protection by pressing Y. - - ![Text Description automatically generated](attachments/7f3e88c6e5ca34c25c0e1ca9e684cb6a.png) - -2. Script does not enable Extended Protection because of Failed Prerequisite Check: - - 1. No Exchange server runs an Extended Protection supported build: - - If no Exchange server in the organization is running a CU that supports Extended Protection, the script will not enable Extended Protection on unsupported servers thereby ensuring server-to-server communication does not fail. - To resolve this, upgrade all servers to the latest CU and SU and re-run the script to enable Extended Protection. - - 2. TLS mismatch: - - A valid and consistent TLS configuration is required on all Exchange servers in scope. If the TLS settings on all servers in scope are not the same, enabling Extended Protection will disrupt client connections to mailbox servers. - - ![Text Description automatically generated](attachments/fca12d63a89e230c7f3cfaf67b642330.png) - To resolve this, configure the TLS settings on all servers in the organization to be the same and then re-run the script. You can find an overview of the Exchange Server TLS configuration best practices [here](https://docs.microsoft.com/Exchange/exchange-tls-configuration). - -3. Some Exchange servers are not reachable: - - The script performs multiple tests against all Exchange servers in scope. If one or more of these servers aren't reachable, the script will exclude them and not configure Extended Protection on them. - - ![Text Description automatically generated](attachments/3095edc994a8aa4bb79f90fe519a0e36.png) - If the server is offline, you should enable Extended Protection on it once it is back online. If the server was unreachable for other reasons, you should run the script directly on the servers to enable Extended Protection. - -#### Rolling back Extended Protection settings - -You can also use the script to roll back the **Extended Protection settings and any IP restriction rules** added via script from one or more servers. When Extended Protection settings are modified by the script, an applicationHost.cep.\*.bak file is created on each server, which contains a backup of pre-existing settings before the script is run. Those files are going to be local to each individual server that the script modifies. Therefore, the rollback of Extended Protection settings can be rolled back from any machine where the script will run using the _earliest_ version of the .bak file to roll back the changes. - -The following command initiates a full rollback of **Extended Protection settings** and **IP restriction rules** on any Exchange server where it was enabled using the script: - -`.\ExchangeExtendedProtectionManagement.ps1 –RollbackType RestoreIISAppConfig` - -#### Rolling back IP Restriction settings - -You can use the script to **only** roll back **Allow and Deny rules** set in Backend EWS vDir's IP Address and Domain Restriction module in the following way. - -`.\ExchangeExtendedProtectionManagement.ps1 -RollbackType RestrictTypeEWSBackend` - -!!! warning "Note" - - To safeguard Backend EWS vDir against NTLM relay, executing above command will set Extended Protection setting back to Required. - -### Enabling Extended Protection manually via IIS settings - -If you want to enable Extended Protection in your environment manually without using the script, you can use the following steps. - -!!! warning "Note" - - When manually enabling Extended Protection, ensure that all virtual directories on the Exchange servers have Extended Protected configured according to the table above. - -#### Set Extended Protection to either Required or Accept for an Exchange Virtual Directory - -1. Launch IIS Manager on the Exchange server where you want to configure Extended Protection. -2. Go to Sites and select either the _Default Web Site_ or _Exchange Back End._ -3. Select the Virtual Directory for which you want to change. -4. Go to _Authentication._ -5. If Windows Authentication is enabled, then select _Windows Authentication._ - ![Graphical user interface, application Description automatically generated](attachments/001f52d47d532f8ac8aa1aa3edb97520.png) -6. Select _Advanced Settings_ (on the right side) and in Advanced Settings window, select the suitable value from the _Extended Protection Dropdown._ - ![Graphical user interface, text, application Description automatically generated](attachments/4794e9f5b4d1e129ea38c0d2c2bd89fa.png) - -#### Set Require SSL settings to either Required or Accept for an Exchange Virtual Directory - -1. Go to the Virtual Directory's home page. - ![Graphical user interface, text, application, Word Description automatically generated](attachments/0d05a67039245dde885522e84ca74bc3.png) -2. Go to _SSL Settings_. -3. Check the _Require SSL_ checkbox to make sure that Require SSL is enabled for this Virtual Directory. -4. Click _Apply_. - ![Graphical user interface, text, application, Word Description automatically generated](attachments/1663e8c2fdea930b47f01c6ab30b3aa8.png) - -## Known issues and workarounds - -**Issue:** - -Changing the permissions for Public Folders by using an Outlook client will fail with the following error, if Extended Protection is enabled: - -`The modified Permissions cannot be changed.` - -**Cause:** - -This happens if the Public Folder for which you try to change the permissions, is hosted on a secondary Public Folder mailbox while the primary Public Folder mailbox is on a different server. - -**Status:** - -!!! success "Fixed" - - The issue has been fixed with the [latest Exchange Server update](https://aka.ms/LatestExchangeServerUpdate). - You'll need to create an override to enable the fix. Please follow the instructions as outlined in [this KB](https://support.microsoft.com/topic/bd2037b5-40e0-413a-b368-746b3f5439ee). - -**Issue:** - -Customers using a _Retention Policy_ containing _Retention Tags_ which perform _Move to Archive_ can now configure Extended Protection with this update. We are actively working on a permanent solution to resolve this issue. Once we ship the solution you will be required to run this script again and rollback the changes. - -**Status:** - -!!! success "Fixed" - - The archiving issue has been fixed with the [latest Exchange Server update](https://aka.ms/LatestExchangeServerUpdate). - We recommend rolling back the mitigation by following the steps outlined in the [rollback section](#rolling-back-ip-restriction-settings). - -**Issue:** - -In Exchange Server 2013, 2016 and 2019 the following probes will show _FAILED_ status after running the script which switches on Extended Protection with required SSL flags on various vDirs as per recommended guidelines: - - 1. OutlookMapiHttpCtpProbe - 2. OutlookRpcCtpProbe - 3. OutlookRpcDeepTestProbe - 4. OutlookRpcSelfTestProbe - 5. ComplianceOutlookLogonToArchiveMapiHttpCtpProbe - 6. ComplianceOutlookLogonToArchiveRpcCtpProbe - -You will also notice that some Health Mailbox logins fail with event ID: 4625 and failure reason "_An Error occurred during Logon_" and status _0xC000035B_ which is related to the failed probes. [**Get-ServerHealth**](https://docs.microsoft.com/exchange/high-availability/managed-availability/health-sets?view=exchserver-2019#use-the-exchange-management-shell-to-view-a-list-of-monitors-and-their-current-health) command will also show RPC and Mapi monitors as Unhealthy. - -**Impact of these failures:** - -Due to this probe failure, the Mapi and Rpc App pools will get restarted once. There should be no other impact. - -You can also turn off any of the above probes temporarily (till the fix is provided) by going through steps mentioned in [Configure managed availability overrides \| Microsoft Docs](https://docs.microsoft.com/exchange/high-availability/managed-availability/configure-overrides?view=exchserver-2019). - -**Status:** - -!!! success "Fixed" - - This issue has been addressed with the [October 2022 (and later) Exchange Server Security Updates](https://aka.ms/LatestExchangeServerUpdate). - -## Troubleshooting issues after enabling Extended Protection - -### Users cannot access their mailbox through one or more clients - -There may be multiple reasons why some or all clients may start giving authentication errors to users after enabling Extended Protection. If this happens, check the following: - -1. If the TLS configuration across the Exchange organization is not the same (e.g., the TLS configuration was changed on one of the Exchange servers after Extended Protection was enabled), this may cause client connections to fail. To resolve this, refer to earlier instructions to configure the same TLS version across all Exchange servers and then use the script to configure Extended Protection again. -2. Check if SSL offload is enabled. Any SSL termination causes the Extended Protection to fail for client connections. Usually if this is the case, users will be able to access their mailbox using Outlook on the Web but Outlook for Windows, Mac or mobile will fail. - To resolve this issue, disable SSL offloading and then use the script to configure Extended Protection. -3. Users can access their emails using Outlook for Windows and Outlook on the Web, but not through non-Windows clients like Outlook for Mac, Outlook on iOS, the iOS native email app, etc. This can happen if the Extended Protection setting for EWS and/or Exchange ActiveSync is set to **Required** on one or all Front-End servers. - To resolve this issue, either run the ExchangeExtendedProtectionManagement.ps1 script with the –ExchangeServerNames parameter and pass the name of the Exchange server which has the problem. You can also run the script without any parameter and configure Extended Protection for all servers. - - `.\ExchangeExtendedProtectionManagement.ps1` - - or - - `.\ExchangeExtendedProtectionManagement.ps1 -ExchangeServerNames Server1, Server2` - - Alternatively, you can also use INetMgr.exe and change the Extended Protection setting for those virtual Directories to the "Accept" value. However, we recommend using the script as it checks for the correct values and automatically performs a reconfiguration if the values are not set as expected. - -4. If after doing the above, some clients are still not working properly, you can rollback Extended Protection temporarily and report the issue to us. If script was used to configure Extended Protection, you can use the _-RollbackType "RestoreIISAppConfig"_ parameter to revert any changes. If Extended Protection was enabled manually (through IIS Manager) you need to revert the settings manually. - -### Hybrid Free/Busy or mailbox migration is not working - -If you are using Modern Hybrid or the Hybrid Agent enabling Extended Protection will cause Hybrid features like Free/Busy and mailbox migration to stop working. To resolve this issue, identify the hybrid servers that are published using Hybrid Agent and disable Extended Protection on the Front-End EWS endpoints for these servers. - -### Public Folders are not accessible - -There are two issues that currently impact Public Folders Connectivity: - -#### Exchange 2013 - -If Public Folders exist on Exchange 2013 servers and Extended Protection is enabled, they will no longer appear and end users will be unable to access them. To resolve the issue in a coexistence environment, migrate all Public Folders to Exchange Server 2016 or Exchange Server 2019. If you have an environment containing only Exchange 2013 servers with Public Folders, you can manually remove the SSL flag from the Backend RPC virtual directory to make Public Folders accessible. - -#### Exchange Server 2016 CU22 / Exchange Server 2019 CU11 or older - -If you have an environment containing Exchange Server 2016 CU22 or Exchange Server 2019 CU11 or older and are utilizing Public Folders, before enabling extended protection you must confirm the version of the server hosting the Public Folder hierarchy. **Ensure the server hosting the Public Folder hierarchy is upgraded to Exchange Server 2016 CU23 or Exchange Server 2019 CU12 with the latest Security Updates** or move the hierarchy to one with these latest versions and updates. - -## FAQs - -**Q:** Is it required to install the August 2022 Security Update (SU) if it was already installed on the previous Cumulative Update (CU)?
-**A:** Yes, it's required to install the August 2022 SU again if you update to a newer CU build (e.g., Exchange Server 2019 CU11 --> Exchange Server 2019 CU12). -Please remember: -If you plan to do the update immediately (means CU + SU installation) Extended Protection does not need to be switched off. -If you plan to stay on the CU without installing the SU immediately, you must disable Extended Protection (find the required steps above) as the CU without the SU being installed doesn't support Extended Protection and therefore, you'll experience client connectivity issues. - -**Q:** Is it safe to enable Windows Extended Protection on an environment that uses Active Directory Federation Services (ADFS) for OWA?
-**A:** Yes, ADFS is not impacted by this change. - - -**Q:** Is it safe to enable Windows Extended Protection on an environment that uses Hybrid Modern Auth (HMA)?
-**A:** Yes, HMA is not impacted by this change. While EP does not further enhance HMA, windows auth may still be used for applications that do not support Hybrid Modern Auth. Considering this, the enablement of Extended Protection would be recommended in any environment eligible that still has Exchange on-premises services. - -**Q:** Does Extended Protection Impact Hybrid Modern Auth or Teams Integration?
-**A:** Extended Protection will not influence Teams Integration or Hybrid Modern Auth. - -**Q:** While we understand that preventing MitM attacks is important, can we have our own devices in the middle with our own certificates?
-**A:** If the device uses the same certificate as the Exchange Server, they can be used. + This documentation has been moved to Microsoft Learn. Please read [Configure Windows Extended Protection in Exchange Server](https://learn.microsoft.com/exchange/plan-and-deploy/post-installation-tasks/security-best-practices/exchange-extended-protection) for more information. diff --git a/mkdocs.yml b/mkdocs.yml index 7cfd2193ea..c4dd8d0f1e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ nav: - Test-AMSI: Admin/Test-AMSI.md - Update-Engines: Admin/Update-Engines.md - Calendar: + - Check-SharingStatus: Calendar/Check-SharingStatus.md - Get-CalendarDiagnosticObjectsSummary: Calendar/Get-CalendarDiagnosticObjectsSummary.md - Get-RBASummary: Calendar/Get-RBASummary.md - Databases: