diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index 66273ff5e7..e73c8d2a36 100644 --- a/Calendar/Check-SharingStatus.ps1 +++ b/Calendar/Check-SharingStatus.ps1 @@ -53,9 +53,6 @@ function ProcessCalendarSharingInviteLogs { $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 @@ -70,7 +67,7 @@ function ProcessCalendarSharingInviteLogs { # check if the output is empty if ($null -eq $logOutput.MailboxLog) { - Write-Output "No data found for [$Identity]." + Write-Host "No data found for [$Identity]." return } @@ -106,8 +103,8 @@ function ProcessCalendarSharingInviteLogs { $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 + Write-Host "User [$Identity] has shared their calendar with the following recipients:" + $mostRecentRecipients | Format-Table -a Timestamp, Recipient, SharingType, DetailLevel } <# @@ -129,9 +126,6 @@ function ProcessCalendarSharingAcceptLogs { $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 @@ -146,7 +140,7 @@ function ProcessCalendarSharingAcceptLogs { # check if the output is empty if ($null -eq $logOutput.MailboxLog) { - Write-Output "No AcceptCalendarSharingInvite Logs found for [$Identity]." + Write-Host "No AcceptCalendarSharingInvite Logs found for [$Identity]." return } @@ -180,11 +174,81 @@ function ProcessCalendarSharingAcceptLogs { # $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 "Receiver [$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 + Formats the InternetCalendar 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 ProcessInternetCalendarLogs { + param ( + [string]$Identity + ) + + # Define the header row + $header = "Timestamp", "Mailbox", "SyncDetails", "PublishingUrl", "RemoteFolderName", "LocalFolderId", "Folder" + + $csvString = @() + $csvString = $header -join "," + $csvString += "`n" + + 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 InternetCalendar + } 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-Host "No InternetCalendar Logs found for [$Identity]." + Write-Host -ForegroundColor Yellow "User [$Identity] is not receiving any Published Calendars." + 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 "*Entry Sync Details for InternetCalendar subscription DataType=calendar*") { + $csvString += $line + "`n" + } + } + + # Clean up output + $csvString = $csvString.Replace("Mailbox: ", "") + $csvString = $csvString.Replace("Entry Sync Details for InternetCalendar subscription DataType=calendar", "InternetCalendar") + $csvString = $csvString.Replace("PublishingUrl=", "") + $csvString = $csvString.Replace("RemoteFolderName=", "") + $csvString = $csvString.Replace("LocalFolderId=", "") + $csvString = $csvString.Replace("folder ", "") + + # Convert the CSV string to an object + $csvObject = $csvString | ConvertFrom-Csv + + # Clean up the Folder column + foreach ($row in $csvObject) { + $row.Folder = $row.Folder.Split("with")[0] + } + + Write-Host -ForegroundColor Cyan "Receiver [$Identity] is/was receiving the following Published Calendars:" + $csvObject | Sort-Object -Unique RemoteFolderName | Format-Table -a RemoteFolderName, Folder, PublishingUrl +} + <# .SYNOPSIS Display Calendar Owner information. @@ -200,6 +264,7 @@ function GetOwnerInformation { #Standard Owner information Write-Host -ForegroundColor DarkYellow "------------------------------------------------" Write-Host -ForegroundColor DarkYellow "Key Owner Mailbox Information:" + Write-Host -ForegroundColor DarkYellow "`t Running 'Get-Mailbox $Owner'" $script:OwnerMB = Get-Mailbox $Owner # Write-Host "`t DisplayName:" $script:OwnerMB.DisplayName # Write-Host "`t Database:" $script:OwnerMB.Database @@ -209,7 +274,14 @@ function GetOwnerInformation { # 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 (-not $script:OwnerMB) { + Write-Host -ForegroundColor Yellow "Could not find Owner Mailbox [$Owner]." + Write-Host -ForegroundColor DarkYellow "Defaulting to External Sharing or Publishing." + return + } + + $script:OwnerMB | Format-List DisplayName, Database, ServerName, LitigationHoldEnabled, CalendarVersionStoreDisabled, CalendarRepairDisabled, RecipientType* if ($null -eq $script:OwnerMB) { Write-Host -ForegroundColor Red "Could not find Owner Mailbox [$Owner]." @@ -228,23 +300,43 @@ function GetOwnerInformation { $script:PIIAccess = $false } - Write-Host -ForegroundColor DarkYellow "Owner Calendar Folder Statistics:" + Write-Host -ForegroundColor DarkYellow "Owner Calendar Folder Statistics:" + Write-Host -ForegroundColor DarkYellow "`t Running 'Get-MailboxFolderStatistics -Identity $Owner -FolderScope Calendar'" $OwnerCalendar = Get-MailboxFolderStatistics -Identity $Owner -FolderScope Calendar - $OwnerCalendarName = ($OwnerCalendar | Where-Object FolderPath -EQ "/Calendar").Name + $OwnerCalendarName = ($OwnerCalendar | Where-Object FolderType -EQ "Calendar").Name - Get-MailboxFolderStatistics -Identity $Owner -FolderScope Calendar | Format-Table -a FolderPath, ItemsInFolder, FolderAndSubfolderSize + $OwnerCalendar | 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 Calendar Permissions:" + Write-Host -ForegroundColor DarkYellow "`t Running 'Get-mailboxFolderPermission "${Owner}:\$OwnerCalendarName" | Format-Table -a User, AccessRights, SharingPermissionFlags'" + Get-mailboxFolderPermission "${Owner}:\$OwnerCalendarName" | Format-Table -a User, AccessRights, SharingPermissionFlags - Write-Host -ForegroundColor DarkYellow "Owner Root MB Permissions:" + Write-Host -ForegroundColor DarkYellow "Owner Root MB Permissions:" + Write-Host -ForegroundColor DarkYellow "`t Running 'Get-mailboxPermission $Owner | Format-Table -a User, AccessRights, SharingPermissionFlags'" 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" + Write-Host -ForegroundColor DarkYellow "Owner Modern Sharing Sent Invites" ProcessCalendarSharingInviteLogs -Identity $Owner + + Write-Host -ForegroundColor DarkYellow "Owner Calendar Folder Information:" + Write-Host -ForegroundColor DarkYellow "`t Running 'Get-MailboxCalendarFolder "${Owner}:\$OwnerCalendarName"'" + + $OwnerCalendarFolder = Get-MailboxCalendarFolder "${Owner}:\$OwnerCalendarName" + if ($OwnerCalendarFolder.PublishEnabled) { + Write-Host -ForegroundColor Green "Owner Calendar is Published." + $script:OwnerPublished = $true + } else { + Write-Host -ForegroundColor Yellow "Owner Calendar is not Published." + $script:OwnerPublished = $false + } + + if ($OwnerCalendarFolder.ExtendedFolderFlags.Contains("SharedOut")) { + Write-Host -ForegroundColor Green "Owner Calendar is Shared Out using Modern Sharing." + $script:OwnerModernSharing = $true + } else { + Write-Host -ForegroundColor Yellow "Owner Calendar is not Shared Out." + $script:OwnerModernSharing = $false + } } <# @@ -260,23 +352,48 @@ function GetReceiverInformation { [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 "`r`r`r------------------------------------------------" + Write-Host -ForegroundColor Cyan "Key Receiver MB Information: [$Receiver]" + Write-Host -ForegroundColor Cyan "Running: 'Get-Mailbox $Receiver'" + $script:ReceiverMB = Get-Mailbox $Receiver + + if (-not $script:ReceiverMB) { + Write-Host -ForegroundColor Yellow "Could not find Receiver Mailbox [$Receiver]." + Write-Host -ForegroundColor Yellow "Defaulting to External Sharing or Publishing." + return + } - Write-Host -ForegroundColor Cyan "Receiver Calendar Folders (look for a copy of [$Owner] Calendar):" + $script:ReceiverMB | Format-List DisplayName, Database, LitigationHoldEnabled, CalendarVersionStoreDisabled, CalendarRepairDisabled, RecipientType* + + if ($script:OwnerMB.OrganizationalUnitRoot -eq $script:ReceiverMB.OrganizationalUnitRoot) { + Write-Host -ForegroundColor Yellow "Owner and Receiver are in the same OU." + Write-Host -ForegroundColor Yellow "Owner and Receiver will be using Internal Sharing." + $script:SharingType = "InternalSharing" + } else { + Write-Host -ForegroundColor Yellow "Owner and Receiver are in different OUs." + Write-Host -ForegroundColor Yellow "Owner and Receiver will be using External Sharing or Publishing." + $script:SharingType = "ExternalSharing" + } + + Write-Host -ForegroundColor Cyan "Receiver Calendar Folders (look for a copy of [$($OwnerMB.DisplayName)] Calendar):" + Write-Host -ForegroundColor Cyan "Running: 'Get-MailboxFolderStatistics -Identity $Receiver -FolderScope 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." + # Note $Owner has a * at the end in case we have had multiple setup for the same user, they will be appended with a " 1", etc. + if (($CalStats | Where-Object Name -Like $owner*) -or ($CalStats | Where-Object Name -Like "$($ownerMB.DisplayName)*" )) { + Write-Host -ForegroundColor Green "Looks like we might have found a copy of the Owner Calendar in the Receiver Calendar." + Write-Host -ForegroundColor Green "This is a good indication the there is a Modern Sharing Relationship between these users." + Write-Host -ForegroundColor Green "If the clients use the Modern Sharing or not is a up to the client." + $script:ModernSharing = $true + $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." + 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." + Write-Host -ForegroundColor Yellow "Warning: Could not Identify the Owner's [$Owner] Calendar in the Receiver Calendar collection." } if ($ReceiverCalendarName -like "REDACTED-*" ) { @@ -284,43 +401,64 @@ function GetReceiverInformation { $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 + ProcessInternetCalendarLogs -Identity $Receiver + + if (($script:SharingType -like "InternalSharing") -or + ($script:SharingType -like "ExternalSharing")) { + # Validate Modern Sharing Status + 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)" + + Write-Host -ForegroundColor Cyan "`r`r`r------------------------------------------------" + Write-Host "New Model Calendar Sharing Entries:" + $ReceiverCalEntries | Where-Object SharingModelType -Like New | Format-Table CalendarGroupName, CalendarName, OwnerEmailAddress, SharingModelType, IsOrphanedEntry + + Write-Host -ForegroundColor Cyan "`r`r`r------------------------------------------------" + Write-Host "Old Model Calendar Sharing Entries:" + Write-Host "Consider upgrading these to the new model." + $ReceiverCalEntries | Where-Object SharingModelType -Like Old | Format-Table CalendarGroupName, CalendarName, OwnerEmailAddress, SharingModelType, 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." + #Output key Modern Sharing information + if (($script:PIIAccess) -and (-not ([string]::IsNullOrEmpty($script:OwnerMB)))) { + 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 Owner, so can not check the Receivers Copy of the Owner Calendar." + Write-Host "Get PII Access for both mailboxes and try again." } - } else { - Write-Host "Do Not have PII information for the Receiver." - Write-Host "Get PII Access for $($script:OwnerMB.Database)." } } # Main +$script:ModernSharing +$script:SharingType GetOwnerInformation -Owner $Owner GetReceiverInformation -Receiver $Receiver + +Write-Host -ForegroundColor Blue "`r`r`r------------------------------------------------" +Write-Host -ForegroundColor Blue "Summary:" +Write-Host -ForegroundColor Blue "Mailbox Owner [$Owner] and Receiver [$Receiver] are using [$script:SharingType] for Calendar Sharing." +Write-Host -ForegroundColor Blue "It appears like the backend [$(if ($script:ModernSharing) {"IS"} else {"is NOT"})] using Modern Calendar Sharing." diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index f41d90668f..4d5892d96e 100644 --- a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 +++ b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 @@ -90,6 +90,7 @@ $script:CalendarItemTypes = @{ 'IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}' = "ExceptionMsgClass" 'IPM.Schedule.Meeting.Notification.Forward' = "ForwardNotification" 'IPM.Appointment' = "IpmAppointment" + 'IPM.Appointment.MP' = "IpmAppointment" 'IPM.Schedule.Meeting.Request' = "MeetingRequest" 'IPM.CalendarSharing.EventUpdate' = "SharingCFM" 'IPM.CalendarSharing.EventDelete' = "SharingDelete" @@ -550,12 +551,21 @@ function CreateShortClientName { } elseif ($ClientInfoString -like "*Microsoft Outlook 16*") { $ShortClientName = "Outlook-ModernCalendarSharing" } else { - $ShortClientName = "Rest" + $ShortClientName = "[Unknown Rest Client]" } + # Client=WebServices;Mozilla/5.0 (ZoomPresence.Android 8.1.0 x86); } else { $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor } + if ($ShortClientName -eq "" -And $ClientInfoString -like "Client=WebServices*") { + if ($ClientInfoString -like "*ZoomPresence*") { + $ShortClientName = "ZoomPresence" + } else { + $ShortClientName = "Unknown EWS App" + } + } + if ($ClientInfoString -like "*InternalCalendarSharing*" -and $ClientInfoString -like "*OWA*") { $ShortClientName = "Owa-ModernCalendarSharing" } @@ -569,6 +579,10 @@ function CreateShortClientName { $ShortClientName = "Outlook-ModernCalendarSharing" } + if ($ShortClientName -eq "") { + $ShortClientName = "[NoShortNameFound]" + } + return $ShortClientName } @@ -1058,19 +1072,23 @@ function BuildTimeline { [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.ResponsibleUser) 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.ResponsibleUser) updated on the $($CalLog.MeetingRequestType) Meeting Request with $($CalLog.Client)." } MoveToDeletedItems { - [array] $Output = "$($CalLog.ResponsibleUserName) deleted the Meeting Request with $($CalLog.Client)." + if ($CalLog.ResponsibleUser -eq "Calendar Assistant") { + [array] $Output = "$($CalLog.Client) Deleted the Meeting Request." + } else { + [array] $Output = "$($CalLog.ResponsibleUser) 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.ResponsibleUser) with $($CalLog.Client)." } } } @@ -1104,14 +1122,14 @@ function BuildTimeline { [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." + ResourceBookingAssistant { + [array] $Output = "ResourceBookingAssistant $($Action) a $($MeetingRespType) Meeting Response message." } Transport { [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.ResponsibleUser) with $($CalLog.Client)." } } } @@ -1145,7 +1163,7 @@ function BuildTimeline { Transport { [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar." } - RBA { + ResourceBookingAssistant { [array] $Output = "$($CalLog.Client) added a new Tentative Meeting from $($CalLog.SentRepresentingDisplayName) to the Calendar." } default { @@ -1162,24 +1180,28 @@ function BuildTimeline { LocationProcessor { [array] $Output = "" } - RBA { - [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting." + ResourceBookingAssistant { + [array] $Output = "ResourceBookingAssistant $($CalLog.TriggerAction)d the Meeting." } default { if ($CalLog.ResponsibleUser -eq "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)." + [array] $Output = "$($CalLog.TriggerAction) to the Meeting by [$($CalLog.ResponsibleUser)] 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)." + if ($CalLog.ResponsibleUserName -eq "Calendar Assistant") { + [array] $Output = "$($CalLog.Client) Accepted the meeting." + } else { + [array] $Output = "$($CalLog.ResponsibleUser) 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)." + [array] $Output = "[$($CalLog.ResponsibleUser)] Declined the Meeting with $($CalLog.Client)." $AddChangedProperties = $False } } @@ -1191,14 +1213,14 @@ function BuildTimeline { LocationProcessor { [array] $Output = "" } - RBA { - [array] $Output = "RBA $($CalLog.TriggerAction) the Meeting." + ResourceBookingAssistant { + [array] $Output = "ResourceBookingAssistant $($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." } else { - [array] $Output = "The Meeting was $($CalLog.TriggerAction) by [$($CalLog.ResponsibleUserName)] with $($CalLog.Client)." + [array] $Output = "The Meeting was $($CalLog.TriggerAction) by [$($CalLog.ResponsibleUser)] with $($CalLog.Client)." $AddChangedProperties = $True } } @@ -1213,7 +1235,7 @@ function BuildTimeline { } } MoveToDeletedItems { - [array] $Output = "[$($CalLog.ResponsibleUser)] moved the Meeting to the Deleted Items with $($CalLog.Client)." + [array] $Output = "[$($CalLog.ResponsibleUser)] Deleted the Meeting with $($CalLog.Client) (Moved the Meeting to the Deleted Items)." } default { [array] $Output = "[$($CalLog.ResponsibleUser)] $($CalLog.TriggerAction) the Meeting with $($CalLog.Client)." diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index 48612c0f9b..e804582c8a 100644 --- a/Calendar/Get-RBASummary.ps1 +++ b/Calendar/Get-RBASummary.ps1 @@ -58,6 +58,7 @@ function ValidateMailbox { if ($null -eq $script:Place) { 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-Host "Hint Forest is likely something like: [$($script:Mailbox.Database.split("DG")[0])]." Write-Error "Exiting Script." exit } @@ -303,10 +304,10 @@ function OutOfPolicyProcessing { Write-Host "`t AllRequestOutOfPolicy: "$RbaSettings.AllRequestOutOfPolicy if ($RbaSettings.AllRequestOutOfPolicy -eq $true ) { - Write-Host "- All users are allowed to submit out-of-policy requests to the resource mailbox. Out-of-policy requests require approval by a resource mailbox delegate." + Write-Host -ForegroundColor Yellow "Information: - All users are allowed to submit out-of-policy requests to the resource mailbox. Out-of-policy requests require approval by a resource mailbox delegate." if ($RbaSettings.RequestOutOfPolicy.count -gt 0) { - Write-Host -ForegroundColor Red "Warning: The users that are listed in BookInPolicy are overridden by the AllRequestOutOfPolicy as everyone can submit out of policy requests." + Write-Host -ForegroundColor Red "Warning: The users that are listed in RequestOutOfPolicy are overridden by the AllRequestOutOfPolicy as everyone can submit out of policy requests." } } else { if ($RbaSettings.RequestOutOfPolicy.count -eq 0) { @@ -342,26 +343,33 @@ function RBADelegateSettings { if ($RbaSettings.ForwardRequestsToDelegates -eq $true ) { if ($RbaSettings.AllBookInPolicy -eq $true) { - Write-Host -ForegroundColor Yellow "Warning: Delegate will not receive any In Policy requests as they will be AutoApproved." + Write-Host -ForegroundColor White "Information: Delegate(s) will not receive any In Policy requests as they will be AutoApproved." } elseif ($RbaSettings.BookInPolicy.Count -gt 0 ) { - Write-Host -ForegroundColor Yellow "Warning: Delegate will not receive from users in the BookInPolicy." + Write-Host -ForegroundColor White "Information: Delegate(s) will not receive requests from users in the BookInPolicy as they will be AutoApproved." foreach ($BIPUser in $RbaSettings.BookInPolicy) { Write-Host -ForegroundColor Yellow " `t `t $BIPUser " } } if ($RbaSettings.AllRequestOutOfPolicy -eq $false) { if ($RbaSettings.RequestOutOfPolicy.Count -eq 0 ) { - Write-Host -ForegroundColor Yellow "Warning: Delegate will not receive any Out of Policy requests as they will all be AutoDenied." + Write-Host -ForegroundColor Yellow "Warning: Delegate(s) will not receive any Out of Policy requests as they will all be AutoDenied." } else { - Write-Host -ForegroundColor Yellow "Warning: Delegate will only receive any Out of Policy requests from the below list of users." + Write-Host -ForegroundColor Yellow "Warning: Delegate(s) will only receive any Out of Policy requests from the below list of users." foreach ($OutOfPolicyUser in $RbaSettings.RequestOutOfPolicy) { Write-Host "`t `t $OutOfPolicyUser" } } } else { - Write-Host -ForegroundColor Yellow "Note: All users can send Out of Policy requests to be approved by the Resource Delegates." + Write-Host -ForegroundColor Yellow "Warning: All users can send Out of Policy requests to be approved by the Resource Delegates." } } - } elseif ($RbaSettings.ForwardRequestsToDelegates -eq $true ` - -and $RbaSettings.AllBookInPolicy -ne $true ) { - Write-Host -ForegroundColor Yellow "Information: ForwardRequestsToDelegates is true but there are no Delegates." + } else { + Write-Host -ForegroundColor Yellow "Warning: No Delegates are configured." + if ($RbaSettings.ForwardRequestsToDelegates -eq $true -and + $RbaSettings.AllBookInPolicy -ne $true ) { + Write-Host -ForegroundColor Yellow "Warning: ForwardRequestsToDelegates is true but there are no Delegates." + } if ($RbaSettings.RequestOutOfPolicy.Count -gt 0) { + Write-Host -ForegroundColor Red "Error: Users are listed in RequestOutOfPolicy but there are no Delegates. - All Out of policy requests by these users will be Tentatively accepted." + } if ($RbaSettings.AllRequestOutOfPolicy -eq $true) { + Write-Host -ForegroundColor Red "Error: AllRequestOutOfPolicy is set but there are no Delegates. - All Out of policy requests will be Tentatively accepted." + } } } @@ -472,10 +480,15 @@ function VerbosePostProcessing { #Add information about RBA logs. function RBAPostScript { Write-Host - Write-Host "If more information is needed about this resource mailbox, please look at the RBA logs to + Write-Host "If more information is needed about this resource mailbox, please look at the RBA logs saved in this directory to see how the system proceed the meeting request." + Write-Host "To get new RBA Logs, run the following command:" Write-Host -ForegroundColor Yellow "`tExport-MailboxDiagnosticLogs $Identity -ComponentName RBA" Write-Host + Write-Host "To continue troubleshooting further, suggestion is to create a Test Meeting in this room (in the future, RBA does not process meeting in the past)." + Write-Host "and then pull the RBA Logs as well as the Calendar Diagnostic Objects to see how the system processed the meeting request." + Write-Host "For Calendar Diagnostic Objects, try [CalLogSummaryScript](https://github.com/microsoft/CSS-Exchange/releases/latest/download/Get-CalendarDiagnosticObjectsSummary.ps1)" + 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" } @@ -489,17 +502,24 @@ function RBALogSummary { if ($RBALog.count -gt 1) { $Starts = $RBALog | Select-String -Pattern "START -" + $FirstDate = "[Unknown]" + $LastDate = "[Unknown]" if ($starts.count -gt 1) { $LastDate = ($Starts[0] -Split ",")[0].Trim() $FirstDate = ($starts[$($Starts.count) -1 ] -Split ",")[0].Trim() - Write-Host "The RBA Log for $Identity shows the following:" + Write-Host "`tThe RBA Log for [$Identity] shows the following:" Write-Host "`t $($starts.count) Processed events times between $FirstDate and $LastDate" } $AcceptLogs = $RBALog | Select-String -Pattern "Action:Accept" $DeclineLogs = $RBALog | Select-String -Pattern "Action:Decline" $TentativeLogs = $RBALog | Select-String -Pattern "Action:Tentative" + $UpdatedLogs = $RBALog | Select-String -Pattern "Begin ProcessUpdateRequest" + $SkippedExternal = $RBALog | Select-String -Pattern "Skipping processing because user settings for processing external items is false." + $DelegateReferrals = $RBALog | Select-String -Pattern "Forwarding Request To Delegates" + $NonMeetingRequests = $RBALog | Select-String -Pattern "Item is not a meeting request" + $Cancellations = $RBALog | Select-String -Pattern "It's a meeting cancellation." if ($AcceptLogs.count -ne 0) { $LastAccept = ($AcceptLogs[0] -Split ",")[0].Trim() @@ -522,6 +542,52 @@ function RBALogSummary { if ($AcceptLogs.count -eq 0 -and $TentativeLogs.count -eq 0 -and $DeclineLogs.count -eq 0) { Write-Host -ForegroundColor Red "`t No meetings were processed in the RBA Log." } + + if ($UpdatedLogs.count -ne 0) { + $LastUpdated = ($UpdatedLogs[0] -Split ",")[0].Trim() + Write-Host "`t $($UpdatedLogs.count) Updates to meetings between $FirstDate and $LastDate" + Write-Host "`t`t with the last meeting updated on $LastUpdated" + } else { + Write-Host -ForegroundColor Red "`t No meetings were updated in the RBA Log." + } + + if ($Cancellations.count -ne 0) { + Write-Host "`t $($Cancellations.count) Cancellations were processed." + } else { + Write-Host "`t No meetings were canceled in the RBA Log." + } + + if ($DelegateReferrals.count -ne 0) { + $LastDelegateReferral = ($DelegateReferrals[0] -Split ",")[0].Trim() + Write-Host "`t $($DelegateReferrals.count) Delegate Referrals were sent between $FirstDate and $LastDate" + Write-Host "`t`t with the last Delegate Referral sent on $LastDelegateReferral" + } else { + Write-Host "`t No Delegate Referrals were sent in the RBA Log." + } + + if ($NonMeetingRequests.count -ne 0) { + $LastNonMeetingRequest = ($NonMeetingRequests[0] -Split ",")[0].Trim() + Write-Host "`t $($NonMeetingRequests.count) Non Meeting Requests were skipped between $FirstDate and $LastDate" + Write-Host "`t`t with the last Non Meeting Request skipped on $LastNonMeetingRequest" + } else { + Write-Host "`t No Non Meeting Requests were skipped in the RBA Log." + } + + if ($SkippedExternal.count -ne 0) { + if ($SkippedExternal.Count -lt 3) { + Write-Host "`t Warning: $($SkippedExternal.count) External meetings were skipped as processing external items is false." + } else { + Write-Host -ForegroundColor Red "`t Warning: $($SkippedExternal.count) External meetings were skipped as processing external items is false." + Write-Host -ForegroundColor Red "`t`t Many skipped external meetings may indicate a configuration issue in Transport." + Write-Host -ForegroundColor Red "`t`t Validate that Internal Meetings are not getting marked as External." + } + } + + $Filename = "RBA-Logs_$($Identity.Split('@')[0])_$((Get-Date).ToString('yyyy-MM-dd_HH-mm-ss')).txt" + Write-Host "`r`n`t RBA Logs saved as [" -NoNewline + Write-Host -ForegroundColor Cyan $Filename -NoNewline + Write-Host "] in the current directory." + $RBALog | Out-File $Filename } else { Write-Warning "No RBA Logs found. Send a test meeting invite to the room and try again if this is a newly created room mailbox." } @@ -622,19 +688,6 @@ function ValidateRoomListSettings { Write-Host } -function Get-DashLine { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory = $true)] - [int]$Length, - [char] $DashChar = "-" - ) - $dashLine = [string]::Empty - 1..$Length | ForEach-Object { $dashLine += $DashChar } - return $dashLine -} - function Write-DashLineBoxColor { [CmdletBinding()] param( @@ -652,7 +705,8 @@ function Write-DashLineBoxColor { #> $highLineLength = 0 $Line | ForEach-Object { if ($_.Length -gt $highLineLength) { $highLineLength = $_.Length } } - $dashLine = Get-DashLine $highLineLength -DashChar $DashChar + $dashLine = [string]::Empty + 1..$highLineLength | ForEach-Object { $dashLine += $DashChar } Write-Host Write-Host -ForegroundColor $Color $dashLine $Line | ForEach-Object { Write-Host -ForegroundColor $Color $_ } diff --git a/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 b/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 index 57a67598e1..5f1ccb5b4c 100644 --- a/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 +++ b/Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1 @@ -2,32 +2,30 @@ # Licensed under the MIT License. function Invoke-CreateDiskShadowFile { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWMICmdlet', '', Justification = 'Required to get drives on old systems')] [OutputType([string[]])] param( [Parameter(Mandatory = $true)] [string] $OutputPath, - [Parameter(Mandatory = $true)] [string] $ServerName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = "BackupByDatabase")] [object[]] $Databases, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = "BackupByDatabase")] [object] $DatabaseToBackup, - [Parameter(Mandatory = $true)] - [string] - $DatabaseDriveLetter, + [Parameter(Mandatory = $true, ParameterSetName = "BackupByVolume")] + [object[]] + $VolumesToBackup, [Parameter(Mandatory = $true)] - [string] - $LogDriveLetter + [string[]] + $DriveLetters ) function Out-DHSFile { @@ -74,171 +72,124 @@ function Invoke-CreateDiskShadowFile { Out-DHSFile "writer exclude {a65faa63-5ea8-4ebc-9dbd-a0c4db26912a}" Out-DHSFile " " - # add databases to exclude - # ------------------------ - 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 { - #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)`"" + if ($DatabaseToBackup) { + # add databases to exclude + # ------------------------ + 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 { + #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)`"" + } } } } + Out-DHSFile " " Out-DHSFile "Begin backup" - # add the volumes for the included database - # ----------------------------------------- - #gets a list of mount points on local server - $mpVolumes = Get-WmiObject -Query "select name, DeviceId from win32_volume where DriveType=3 AND DriveLetter=NULL" - $deviceIDs = @() - - $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 + if ($DatabaseToBackup) { + # add the volumes for the included database + # ----------------------------------------- + #gets a list of mount points on local server + $mpVolumes = Get-CimInstance -Query "select name, DeviceId from win32_volume where DriveType=3 AND DriveLetter=NULL" + $deviceIDs = @() + + $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 " 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 + } + + #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 " 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 + } } + } - #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 - } + if ($dbMP -eq $false) { + $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 } - $deviceIDs = $deviceID1, $deviceID2 - } - if ($dbMP -eq $false) { - $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 = ($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 + } - if ($logMP -eq $false) { - $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 + $deviceIDs = @($deviceID1) + if ($deviceID2 -ne $deviceID1) { + $deviceIDs += $deviceID2 + } + } else { + $validVolumes = Get-CimInstance -Query "select name, DeviceId from win32_volume where DriveType=3" | + Where-Object { $_.Name -match "^\w:" } | Select-Object Name, DeviceID + $deviceIDs = @() + foreach ($v in $VolumesToBackup) { + $volToBackup = $validVolumes | Where-Object { $_.Name -eq $v } + if ($null -eq $volToBackup) { + Write-Warning "Failed to find volume by name: $v. Available volumes:`n$([string]::Join("`n", $validVolumes))" + exit + } + + $deviceIDs += $volToBackup.DeviceID + } } # Here is where we start adding the appropriate volumes or MountPoints to the DiskShadow config file # We make sure that we add only one Logical volume when we detect the EDB and log files # are on the same volume - Write-Host - $deviceIDs = $deviceID1, $deviceID2 - $comp = [string]::Compare($deviceID1, $deviceID2, $True) - if ($comp -eq 0) { - $dID = $deviceIDs[0] - Write-Debug -Message ('$dID = ' + $dID.ToString()) - Write-Debug "When the database and log files are on the same volume, we add the volume only once" - if ($dID.length -gt "2") { - $addVol = "add volume $dID alias vss_test_" + ($dID).ToString().substring(11, 8) - Write-Host $addVol - Out-DHSFile $addVol - } else { - $addVol = "add volume $dID alias vss_test_" + ($dID).ToString().substring(0, 1) - Write-Host $addVol - Out-DHSFile $addVol - } - } else { - Write-Host " " - foreach ($device in $deviceIDs) { - if ($device.length -gt "2") { - Write-Host " Adding the Mount Point for DSH file" - $addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(11, 8) - Write-Host " $addVol" - Out-DHSFile $addVol - } else { - Write-Host " Adding the volume for DSH file" - $addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(0, 1) - Write-Host " $addVol" - Out-DHSFile $addVol - } - } + for ($i = 0; $i -lt $deviceIDs.Count; $i++) { + $id = $deviceIDs[$i] + Write-Debug -Message ('$id = ' + $id.ToString()) + $addVol = "add volume $id alias vss_test_$i" + Write-Host $addVol + Out-DHSFile $addVol } + Out-DHSFile "create" Out-DHSFile " " 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 - # if two volumes are used, two drive letters are needed - - $matchCondition = "^[a-z]:$" - Write-Debug $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 { - $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" - # expose the drives - # 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):" - Out-DHSFile $dbVolStr - } else { - $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)" - Out-DHSFile $dbVolStr - } else { - $dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $($dbSnapVol)" - Out-DHSFile $dbVolStr - } + if ($deviceIDs.Count -lt $DriveLetters.Count) { + Write-Warning "Determined that we need $($deviceIDs.Count) drive letters to expose the snapshots, but only $($DriveLetters.Count) were provided. Exiting." + exit + } - # 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):" - Out-DHSFile $logVolStr - } else { - $logVolStr = "expose %vss_test_" + ($dbLogVol).substring(0, 1) + "% $($logSnapVol):" - Out-DHSFile $logVolStr - } + for ($i = 0; $i -lt $deviceIDs.Count; $i++) { + $dbVolStr = "expose %vss_test_$($i)% $($DriveLetters[$i]):" + Out-DHSFile $dbVolStr } # ending data of file Out-DHSFile "end backup" - if ($dbSnapVol -eq $logSnapVol) { - return @($dbSnapVol) - } else { - return @($dbSnapVol, $logSnapVol) - } + # return the drive letters we used + return $DriveLetters | Select-Object -First ($deviceIDs.Count) } diff --git a/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 b/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 index 32844758e8..a121d9fb4e 100644 --- a/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 +++ b/Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1 @@ -8,14 +8,10 @@ function Invoke-DiskShadow { param( [Parameter(Mandatory = $true)] [string] - $OutputPath, - - [Parameter(Mandatory = $true)] - [object] - $DatabaseToBackup + $OutputPath ) - Write-Host "$(Get-Date) Starting DiskShadow copy of Exchange database: $Database" + Write-Host "$(Get-Date) Starting DiskShadow copy." Write-Host " Running the following command:" Write-Host " `"C:\Windows\System32\DiskShadow.exe /s $OutputPath\DiskShadow.dsh /l $OutputPath\DiskShadow.log`"" diff --git a/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 b/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 index 05f9428764..a183c1ef1b 100644 --- a/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1 @@ -15,7 +15,7 @@ function Invoke-DisableExTRATracing { [string] $ServerName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [object] $DatabaseToBackup, @@ -24,9 +24,8 @@ function Invoke-DisableExTRATracing { $OutputPath ) Write-Host "$(Get-Date) Disabling ExTRA Tracing..." - $dbMountedOn = $DatabaseToBackup.Server.Name - if ($dbMountedOn -eq "$ServerName") { - #stop active copy + $traceLocalServerOnly = $null -eq $DatabaseToBackup -or $DatabaseToBackup.Server.Name -eq $ServerName + if ($traceLocalServerOnly) { Write-Host Write-Host " Stopping Exchange Trace data collector on $ServerName..." logman stop vssTester -s $ServerName @@ -35,6 +34,7 @@ function Invoke-DisableExTRATracing { Write-Host } else { #stop passive copy + $dbMountedOn = $DatabaseToBackup.Server.Name Write-Host " Stopping Exchange Trace data collector on $ServerName..." logman stop vssTester-Passive -s $ServerName Write-Host " Deleting Exchange Trace data collector on $ServerName..." diff --git a/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 b/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 index 7c51a55ddf..3c3b366b3f 100644 --- a/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 +++ b/Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1 @@ -8,7 +8,7 @@ function Invoke-EnableExTRATracing { [string] $ServerName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [object] $DatabaseToBackup, @@ -53,10 +53,9 @@ function Invoke-EnableExTRATracing { } } - $dbMountedOn = $DatabaseToBackup.Server.Name + $traceLocalServerOnly = $null -eq $DatabaseToBackup -or $DatabaseToBackup.Server.Name -eq $ServerName - #active server, only get tracing from active node - if ($dbMountedOn -eq $ServerName) { + if ($traceLocalServerOnly) { Write-Host "Creating Exchange Trace data collector set..." Invoke-ExtraTracingCreate -ComputerName $ServerName -LogmanName "VSSTester" -OutputPath $OutputPath Write-Host "Starting Exchange Trace data collector..." @@ -70,6 +69,7 @@ function Invoke-EnableExTRATracing { Write-Host } else { #passive server, get tracing from both active and passive nodes + $dbMountedOn = $DatabaseToBackup.Server.Name 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 diff --git a/Databases/VSSTester/VSSTester.ps1 b/Databases/VSSTester/VSSTester.ps1 index cd746f1cd8..ded23979ba 100644 --- a/Databases/VSSTester/VSSTester.ps1 +++ b/Databases/VSSTester/VSSTester.ps1 @@ -13,7 +13,7 @@ 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 + .\VSSTester -DiskShadow -DatabaseName "Mailbox Database 1637196748" -ExposeSnapshotsOnDriveLetters M, 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 @@ -32,7 +32,8 @@ param( $TraceOnly, # Enable tracing and perform a database snapshot with DiskShadow. - [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByDatabase")] + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByVolume")] [switch] $DiskShadow, @@ -41,36 +42,52 @@ param( [switch] $WaitForWriterFailure, - # Name of the database to focus tracing on. + # Name of the database to focus tracing on and/or snapshot. [Parameter(Mandatory = $true, ParameterSetName = "TraceOnly")] - [Parameter(Mandatory = $true, ParameterSetName = "DiskShadow")] + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByDatabase")] [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, + # Names of the volumes to snapshot. + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByVolume")] + [ValidateCount(1, 2)] + [ValidateScript({ + $validVolumeNames = @((Get-CimInstance -Query "select name, DeviceId from win32_volume where DriveType=3" | + Where-Object { $_.Name -match "^\w:" }).Name) + if ($validVolumeNames -contains $_) { + $true + } else { + throw "Invalid volume specified. Please specify one of the following values:`n$([string]::Join("`n", $validVolumeNames))" + } + })] + [string[]] + $VolumesToBackup, - # 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")] + # Drive letters on which to expose the snapshots. + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByDatabase")] + [Parameter(Mandatory = $true, ParameterSetName = "DiskShadowByVolume")] [ValidateLength(1, 1)] - [string] - $LogDriveLetter, + [ValidateCount(1, 2)] + [string[]] + $ExposeSnapshotsOnDriveLetters, # 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 = "DiskShadowByDatabase")] + [Parameter(Mandatory = $false, ParameterSetName = "DiskShadowByVolume")] [Parameter(Mandatory = $false, ParameterSetName = "WaitForWriterFailure")] [string] $LoggingPath = $PSScriptRoot ) +if ($VolumesToBackup -and ($VolumesToBackup.Count -ne $ExposeSnapshotsOnDriveLetters.Count)) { + Write-Host "The count of VolumesToBackup must match the count of ExposeSnapshotsOnDriveLetters." + exit +} + . $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\Get-ScriptUpdateAvailable.ps1 . $PSScriptRoot\..\..\Shared\Confirm-ExchangeShell.ps1 . .\DiskShadow\Invoke-CreateDiskShadowFile.ps1 @@ -130,32 +147,43 @@ try { 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 - } - Get-CopyStatus -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath + if ($DatabaseName) { + $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 + } + + Get-CopyStatus -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath + } if ($DiskShadow) { - $p = @{ - OutputPath = $LoggingPath - ServerName = $serverName - Databases = $databases - DatabaseToBackup = $dbForBackup - DatabaseDriveLetter = $DatabaseDriveLetter - LogDriveLetter = $LogDriveLetter + if ($DatabaseName) { + $p = @{ + OutputPath = $LoggingPath + ServerName = $serverName + Databases = $databases + DatabaseToBackup = $dbForBackup + DriveLetters = $ExposeSnapshotsOnDriveLetters + } + } else { + $p = @{ + OutputPath = $LoggingPath + ServerName = $serverName + VolumesToBackup = $VolumesToBackup + DriveLetters = $ExposeSnapshotsOnDriveLetters + } } - Write-Host "$p" + $p | Out-Host $exposedDrives = Invoke-CreateDiskShadowFile @p } Invoke-EnableDiagnosticsLogging Invoke-EnableVSSTracing -OutputPath $LoggingPath -Circular $WaitForWriterFailure Invoke-CreateExTRATracingConfig - Invoke-EnableExTRATracing -ServerName $serverName -Database $dbForBackup -OutputPath $LoggingPath -Circular $WaitForWriterFailure + Invoke-EnableExTRATracing -ServerName $serverName -DatabaseToBackup $dbForBackup -OutputPath $LoggingPath -Circular $WaitForWriterFailure $collectEventLogs = $false @@ -164,7 +192,7 @@ try { # Always collect event logs for this scenario $collectEventLogs = $true - Invoke-DiskShadow -OutputPath $LoggingPath -DatabaseToBackup $dbForBackup + Invoke-DiskShadow -OutputPath $LoggingPath Invoke-RemoveExposedDrives -OutputPath $LoggingPath -ExposedDrives $exposedDrives } elseif ($TraceOnly) { # Always collect event logs for this scenario diff --git a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 index 5d603aa82b..062dab1a86 100644 --- a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 +++ b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 @@ -34,6 +34,12 @@ AV Modules loaded into Exchange Processes may indicate that AV Process Exclusion Will test not just the root folders but all SubFolders. Generally should not be needed unless all folders pass without -Recuse but AV is still suspected. +.PARAMETER WaitingTimeForAVAnalysisInMinutes +Set the waiting time for AV to analyze the EICAR files. Default is 5 minutes. + +.PARAMETER OpenLog +Open the log file after the script finishes. + .PARAMETER SkipVersionCheck Skip script version verification. @@ -42,13 +48,10 @@ Just update script version to latest one. .OUTPUTS Log file: -$env:LOCALAPPDATA\ExchAvExclusions.log +$PSScriptRoot\ExchAvExclusions.log List of Scanned Folders: -$env:LOCALAPPDATA\BadExclusions.txt - -List of Non-Default Processes -$env:LOCALAPPDATA NonDefaultModules.txt +$PSScriptRoot\BadExclusions.txt .EXAMPLE .\Test-ExchAVExclusions.ps1 @@ -64,6 +67,9 @@ Puts and Remove an EICAR file in all test paths + all SubFolders. [CmdletBinding(DefaultParameterSetName = 'Test')] param ( + [Parameter(ParameterSetName = "Test")] + [int]$WaitingTimeForAVAnalysisInMinutes = 5, + [Parameter(ParameterSetName = "Test")] [switch]$Recurse, @@ -82,7 +88,6 @@ param ( . $PSScriptRoot\..\..\Shared\Get-ExchAVExclusions.ps1 . $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 . $PSScriptRoot\Write-SimpleLogFile.ps1 -. $PSScriptRoot\Start-SleepWithProgress.ps1 $BuildVersion = "" @@ -104,10 +109,10 @@ if ((-not($SkipVersionCheck)) -and } # Log file name -$LogFile = "ExchAvExclusions.log" +$LogFileName = Join-Path $PSScriptRoot ExchAvExclusions.log # Open log file if switched -if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -Name $LogFile } +if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -LogFile $LogFileName } # Confirm that we are an administrator if (-not (Confirm-Administrator)) { @@ -145,10 +150,10 @@ if (-not($exchangeShell.ShellLoaded)) { exit } -Write-SimpleLogFile -String ("###########################################################################################") -name $LogFile -Write-SimpleLogFile -String ("Starting AV Exclusions analysis at $((Get-Date).ToString())") -name $LogFile -Write-SimpleLogFile -String ("###########################################################################################") -name $LogFile -Write-SimpleLogFile -String ("You can find a detailed log on: $($Env:LocalAppData)\$LogFile") -name $LogFile -OutHost +Write-SimpleLogFile -String ("###########################################################################################") -LogFile $LogFileName +Write-SimpleLogFile -String ("Starting AV Exclusions analysis at $((Get-Date).ToString())") -LogFile $LogFileName +Write-SimpleLogFile -String ("###########################################################################################") -LogFile $LogFileName +Write-SimpleLogFile -String ("You can find a detailed log on: $LogFileName") -LogFile $LogFileName -OutHost # Create the Array List $BaseFolders = Get-ExchAVExclusionsPaths -ExchangePath $ExchangePath -MsiProductMinor ([byte]$serverExchangeInstallDirectory.MsiProductMinor) @@ -171,13 +176,13 @@ foreach ($path in $BaseFolders) { $FolderList.Add($path.ToLower()) $nonExistentFolder.Add($path.ToLower()) New-Item -Path (Split-Path $path) -Name $path.split('\')[-1] -ItemType Directory -Force | Out-Null - Write-SimpleLogFile -string ("Created folder: " + $path) -Name $LogFile + Write-SimpleLogFile -string ("Created folder: " + $path) -LogFile $LogFileName } # Resolve path only returns a bool so we have to manually throw to catch if (!(Resolve-Path -Path $path -ErrorAction SilentlyContinue)) { $nonExistentFolder.Add($path.ToLower()) New-Item -Path (Split-Path $path) -Name $path.split('\')[-1] -ItemType Directory -Force | Out-Null - Write-SimpleLogFile -string ("Created folder: " + $path) -Name $LogFile + Write-SimpleLogFile -string ("Created folder: " + $path) -LogFile $LogFileName } # If -recurse then we need to find all SubFolders and Add them to the list to be tested @@ -191,13 +196,13 @@ foreach ($path in $BaseFolders) { } # Just Add the root folder $FolderList.Add($path.ToLower()) - } catch { Write-SimpleLogFile -string ("[ERROR] - Failed to resolve folder " + $path) -Name $LogFile } + } catch { Write-SimpleLogFile -string ("[ERROR] - Failed to resolve folder " + $path) -LogFile $LogFileName } } # Remove any Duplicates $FolderList = $FolderList | Select-Object -Unique -Write-SimpleLogFile -String "Creating EICAR Files" -name $LogFile -OutHost +Write-SimpleLogFile -String "Creating EICAR Files" -LogFile $LogFileName -OutHost # Create the EICAR file in each path $eicarFileName = "eicar" @@ -210,7 +215,7 @@ $eicarFullFileName = "$eicarFileName.$eicarFileExt" foreach ($Folder in $FolderList) { [string] $FilePath = (Join-Path $Folder $eicarFullFileName) - Write-SimpleLogFile -String ("Creating $eicarFullFileName file " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Creating $eicarFullFileName file " + $FilePath) -LogFile $LogFileName if (!(Test-Path -Path $FilePath)) { @@ -225,7 +230,7 @@ foreach ($Folder in $FolderList) { Write-Warning "$Folder $eicarFullFileName file couldn't be created. Either permissions or AV prevented file creation." } } else { - Write-SimpleLogFile -string ("[WARNING] - $eicarFullFileName already exists!: " + $FilePath) -name $LogFile -OutHost + Write-SimpleLogFile -string ("[WARNING] - $eicarFullFileName already exists!: " + $FilePath) -LogFile $LogFileName -OutHost } } @@ -238,7 +243,7 @@ $extensionsList = Get-ExchAVExclusionsExtensions -MsiProductMinor ([byte]$server if ($randomFolder) { foreach ($extension in $extensionsList) { $filepath = Join-Path $randomFolder "$eicarFileName.$extension" - Write-SimpleLogFile -String ("Creating $eicarFileName.$extension file " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Creating $eicarFileName.$extension file " + $FilePath) -LogFile $LogFileName if (!(Test-Path -Path $FilePath)) { @@ -251,22 +256,22 @@ if ($randomFolder) { Write-Warning "$randomFolder $eicarFileName.$extension file couldn't be created. Either permissions or AV prevented file creation." } } else { - Write-SimpleLogFile -string ("[WARNING] - $randomFolder $eicarFileName.$extension already exists!: ") -name $LogFile -OutHost + Write-SimpleLogFile -string ("[WARNING] - $randomFolder $eicarFileName.$extension already exists!: ") -LogFile $LogFileName -OutHost } } } else { Write-Warning "We cannot create a folder in root path to test extension exclusions." } -Write-SimpleLogFile -String "EICAR Files Created" -name $LogFile -OutHost +Write-SimpleLogFile -String "EICAR Files Created" -LogFile $LogFileName -OutHost -Write-SimpleLogFile -String "Accessing EICAR Files" -name $LogFile -OutHost +Write-SimpleLogFile -String "Accessing EICAR Files" -LogFile $LogFileName -OutHost # Try to open each EICAR file to force detection in paths $i = 0 foreach ($Folder in $FolderList) { $FilePath = (Join-Path $Folder $eicarFullFileName) if (Test-Path $FilePath -PathType Leaf) { - Write-SimpleLogFile -String ("Opening $eicarFullFileName file " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Opening $eicarFullFileName file " + $FilePath) -LogFile $LogFileName Start-Process -FilePath more -ArgumentList """$FilePath""" -ErrorAction SilentlyContinue -WindowStyle Hidden | Out-Null } $i++ @@ -277,21 +282,115 @@ $i = 0 foreach ($extension in $extensionsList) { $FilePath = Join-Path $randomFolder "$eicarFileName.$extension" if (Test-Path $FilePath -PathType Leaf) { - Write-SimpleLogFile -String ("Opening $eicarFileName.$extension file " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Opening $eicarFileName.$extension file " + $FilePath) -LogFile $LogFileName Start-Process -FilePath more -ArgumentList """$FilePath""" -ErrorAction SilentlyContinue -WindowStyle Hidden | Out-Null } $i++ } -Write-SimpleLogFile -String "Access EICAR Files Finished" -name $LogFile -OutHost +Write-SimpleLogFile -String "Access EICAR Files Finished" -LogFile $LogFileName -OutHost + +$StartDate = Get-Date +[int]$initialDiff = (New-TimeSpan -End $StartDate.AddMinutes($WaitingTimeForAVAnalysisInMinutes) -Start $StartDate).TotalSeconds +$currentDiff = $initialDiff +$firstExecution = $true +$SuspiciousProcessList = New-Object Collections.Generic.List[string] +$SuspiciousW3wpProcessList = New-Object Collections.Generic.List[string] + +Write-SimpleLogFile -String "Analyzing Exchange Processes" -LogFile $LogFileName -OutHost +while ($currentDiff -gt 0) { + if ($firstExecution) { + # Test Exchange Processes for unexpected modules + $ProcessList = Get-ExchAVExclusionsProcess -ExchangePath $ExchangePath -MsiProductMinor ([byte]$serverExchangeInstallDirectory.MsiProductMinor) + + # Include w3wp process in the analysis + $ProcessList += (Join-Path $env:SystemRoot '\System32\inetSrv\W3wp.exe') + + # Gather all processes on the computer + $ServerProcess = Get-Process | Sort-Object -Property ProcessName + + # Module allow list + $ModuleAllowList = New-Object Collections.Generic.List[string] + + # cSpell:disable + $ModuleAllowList.add("Google.Protobuf.dll") + $ModuleAllowList.add("Microsoft.RightsManagementServices.Core.dll") + $ModuleAllowList.add("Newtonsoft.Json.dll") + $ModuleAllowList.add("Microsoft.Cloud.InstrumentationFramework.Events.dll") + $ModuleAllowList.add("HealthServicePerformance.dll") + $ModuleAllowList.add("InterceptCounters.dll") + $ModuleAllowList.add("MOMConnectorPerformance.dll") + $ModuleAllowList.add("ExDbFailureItemApi.dll") + $ModuleAllowList.add("Microsoft.Cloud.InstrumentationFramework.Metrics.dll") + $ModuleAllowList.add("IfxMetrics.dll") + $ModuleAllowList.add("ManagedBlingSigned.dll") + $ModuleAllowList.add("l3codecp.acm") + $ModuleAllowList.add("System.IdentityModel.Tokens.jwt.dll") + # Oracle modules associated with 'Outside In® Technology' + $ModuleAllowList.add("wvcore.dll") + $ModuleAllowList.add("sccut.dll") + $ModuleAllowList.add("sccfut.dll") + $ModuleAllowList.add("sccfa.dll") + $ModuleAllowList.add("sccfi.dll") + $ModuleAllowList.add("sccch.dll") + $ModuleAllowList.add("sccda.dll") + $ModuleAllowList.add("sccfmt.dll") + $ModuleAllowList.add("sccind.dll") + $ModuleAllowList.add("sccca.dll") + $ModuleAllowList.add("scclo.dll") + $ModuleAllowList.add("SCCOLE2.dll") + $ModuleAllowList.add("SCCSD.dll") + $ModuleAllowList.add("SCCXT.dll") + # cSpell:enable + + Write-SimpleLogFile -string ("Allow List Module Count: " + $ModuleAllowList.count) -LogFile $LogFileName + + # Gather each process and work thru their module list to remove any known modules. + foreach ($process in $ServerProcess) { + + Write-Progress -Activity "Checking Exchange Processes" -CurrentOperation "$currentDiff More Seconds" -PercentComplete ((($initialDiff - $currentDiff) / $initialDiff) * 100) -Status " " + [int]$currentDiff = (New-TimeSpan -End $StartDate.AddMinutes($WaitingTimeForAVAnalysisInMinutes) -Start (Get-Date)).TotalSeconds + + # Determine if it is a known exchange process + if ($ProcessList -contains $process.path ) { + + # Gather all modules + [array]$ProcessModules = $process.modules + + # Remove Microsoft modules + $ProcessModules = $ProcessModules | Where-Object { $_.FileVersionInfo.CompanyName -ne "Microsoft Corporation." -and $_.FileVersionInfo.CompanyName -ne "Microsoft" -and $_.FileVersionInfo.CompanyName -ne "Microsoft Corporation" } + + # Clear out modules from the allow list + foreach ($module in $ModuleAllowList) { + $ProcessModules = $ProcessModules | Where-Object { $_.ModuleName -ne $module -and $_.ModuleName -ne $($module.Replace(".dll", ".ni.dll")) } + } -# Sleeping 5 minutes for AV to "find" the files -Start-SleepWithProgress -SleepTime 300 -message "Allowing time for AV to Scan" + if ($ProcessModules.count -gt 0) { + foreach ($module in $ProcessModules) { + $OutString = ("PROCESS: $($process.ProcessName) PID($($process.Id)) UNEXPECTED MODULE: $($module.ModuleName) COMPANY: $($module.Company)`n`tPATH: $($module.FileName)") + Write-SimpleLogFile -string "[FAIL] - $OutString" -LogFile $LogFileName -OutHost + if ($process.MainModule.ModuleName -eq "W3wp.exe") { + $SuspiciousW3wpProcessList += $OutString + } else { + $SuspiciousProcessList += $OutString + } + } + } + } + } + $firstExecution = $false + } else { + Start-Sleep -Seconds 1 + Write-Progress -Activity "Waiting for AV" -CurrentOperation "$currentDiff More Seconds" -PercentComplete ((($initialDiff - $currentDiff) / $initialDiff) * 100) -Status " " + [int]$currentDiff = (New-TimeSpan -End $StartDate.AddMinutes($WaitingTimeForAVAnalysisInMinutes) -Start (Get-Date)).TotalSeconds + } +} +Write-SimpleLogFile -String "Analyzed Exchange Processes" -LogFile $LogFileName -OutHost # Create a list of folders that are probably being scanned by AV $BadFolderList = New-Object Collections.Generic.List[string] -Write-SimpleLogFile -string "Testing for EICAR files" -name $LogFile -OutHost +Write-SimpleLogFile -string "Testing for EICAR files" -LogFile $LogFileName -OutHost # Test each location for the EICAR file foreach ($Folder in $FolderList) { @@ -303,22 +402,22 @@ foreach ($Folder in $FolderList) { #Get content to confirm that the file is not blocked by AV $output = Get-Content $FilePath -ErrorAction SilentlyContinue if ($output -eq $eicar) { - Write-SimpleLogFile -String ("Removing " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Removing " + $FilePath) -LogFile $LogFileName Remove-Item $FilePath -Confirm:$false -Force } else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Path: " + $Folder) -name $LogFile -OutHost + Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Path: " + $Folder) -LogFile $LogFileName -OutHost $BadFolderList.Add($Folder) } } # If the file doesn't exist Add that to the bad folder list -- means the folder is being scanned else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Path: " + $Folder) -name $LogFile -OutHost + Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Path: " + $Folder) -LogFile $LogFileName -OutHost $BadFolderList.Add($Folder) } if ($nonExistentFolder -contains $Folder) { Remove-Item $Folder -Confirm:$false -Force -Recurse - Write-SimpleLogFile -string ("Removed folder: " + $Folder) -Name $LogFile + Write-SimpleLogFile -string ("Removed folder: " + $Folder) -LogFile $LogFileName } } @@ -333,16 +432,16 @@ foreach ($extension in $extensionsList) { #Get content to confirm that the file is not blocked by AV $output = Get-Content $FilePath -ErrorAction SilentlyContinue if ($output -eq $eicar) { - Write-SimpleLogFile -String ("Removing " + $FilePath) -name $LogFile + Write-SimpleLogFile -String ("Removing " + $FilePath) -LogFile $LogFileName Remove-Item $FilePath -Confirm:$false -Force } else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -name $LogFile -OutHost + Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -LogFile $LogFileName -OutHost $BadExtensionList.Add($extension) } } # If the file doesn't exist Add that to the bad extension list -- means the extension is being scanned else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -name $LogFile -OutHost + Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -LogFile $LogFileName -OutHost $BadExtensionList.Add($extension) } } @@ -350,126 +449,44 @@ foreach ($extension in $extensionsList) { #Delete Random Folder Remove-Item $randomFolder +$OutputPath = Join-Path $PSScriptRoot BadExclusions.txt +"###########################################################################################" | Out-File $OutputPath +"Exclusions analysis at $((Get-Date).ToString())" | Out-File $OutputPath -Append +"###########################################################################################" | Out-File $OutputPath -Append + # Report what we found -if ($BadFolderList.count -gt 0 -or $BadExtensionList.Count -gt 0 ) { - $OutputPath = Join-Path $env:LOCALAPPDATA BadExclusions.txt - $BadFolderList | Out-File $OutputPath - $BadExtensionList | Out-File $OutputPath -Append +if ($BadFolderList.count -gt 0 -or $BadExtensionList.Count -gt 0 -or $SuspiciousProcessList.count -gt 0 -or $SuspiciousW3wpProcessList.count -gt 0) { - Write-SimpleLogFile -String "Possible AV Scanning found" -name $LogFile + Write-SimpleLogFile -String "Possible AV Scanning found" -LogFile $LogFileName if ($BadFolderList.count -gt 0 ) { + "`n[Missing Folder Exclusions]" | Out-File $OutputPath -Append + $BadFolderList | Out-File $OutputPath -Append Write-Warning ("Found $($BadFolderList.count) of $($FolderList.Count) folders that are possibly being scanned! ") } if ($BadExtensionList.count -gt 0 ) { + "`n[Missing Extension Exclusions]" | Out-File $OutputPath -Append + $BadExtensionList | Out-File $OutputPath -Append Write-Warning ("Found $($BadExtensionList.count) of $($extensionsList.Count) extensions that are possibly being scanned! ") } + if ($SuspiciousProcessList.count -gt 0 ) { + "`n[Non-Default Modules Loaded]" | Out-File $OutputPath -Append + $SuspiciousProcessList | Out-File $OutputPath -Append + Write-Warning ("Found $($SuspiciousProcessList.count) UnExpected modules loaded into Exchange Processes ") + } + if ($SuspiciousW3wpProcessList.count -gt 0 ) { + $SuspiciousW3wpProcessListString = "`n[WARNING] - W3wp.exe is not present in the recommended Exclusion list but we found 3rd Party modules on it and could affect Exchange performance or functionality." + $SuspiciousW3wpProcessListString | Out-File $OutputPath -Append + Write-Warning $SuspiciousW3wpProcessListString + Write-SimpleLogFile -string $SuspiciousW3wpProcessListString -LogFile $LogFileName + "`n[Non-Default Modules Loaded on W3wp.exe]" | Out-File $OutputPath -Append + $SuspiciousW3wpProcessList | Out-File $OutputPath -Append + Write-Warning ("Found $($SuspiciousW3wpProcessList.count) UnExpected modules loaded into W3wp.exe ") + } Write-Warning ("Review " + $OutputPath + " For the full list.") } else { - Write-SimpleLogFile -String "All EICAR files found; File Exclusions appear to be set properly" -Name $LogFile -OutHost -} - -Write-SimpleLogFile -string "Testing for AV loaded in processes" -name $LogFile -OutHost - -# Test Exchange Processes for unexpected modules -$ProcessList = Get-ExchAVExclusionsProcess -ExchangePath $ExchangePath -MsiProductMinor ([byte]$serverExchangeInstallDirectory.MsiProductMinor) - -# Include w3wp process in the analysis -$ProcessList += (Join-Path $env:SystemRoot '\System32\inetSrv\W3wp.exe') - -# Gather all processes on the computer -$ServerProcess = Get-Process | Sort-Object -Property ProcessName - -# Module allow list -$ModuleAllowList = New-Object Collections.Generic.List[string] - -# cSpell:disable -$ModuleAllowList.add("Google.Protobuf.dll") -$ModuleAllowList.add("Microsoft.RightsManagementServices.Core.dll") -$ModuleAllowList.add("Newtonsoft.Json.dll") -$ModuleAllowList.add("Microsoft.Cloud.InstrumentationFramework.Events.dll") -$ModuleAllowList.add("HealthServicePerformance.dll") -$ModuleAllowList.add("InterceptCounters.dll") -$ModuleAllowList.add("MOMConnectorPerformance.dll") -$ModuleAllowList.add("ExDbFailureItemApi.dll") -$ModuleAllowList.add("Microsoft.Cloud.InstrumentationFramework.Metrics.dll") -$ModuleAllowList.add("IfxMetrics.dll") -$ModuleAllowList.add("ManagedBlingSigned.dll") -$ModuleAllowList.add("l3codecp.acm") -$ModuleAllowList.add("System.IdentityModel.Tokens.jwt.dll") -# Oracle modules associated with 'Outside In® Technology' -$ModuleAllowList.add("wvcore.dll") -$ModuleAllowList.add("sccut.dll") -$ModuleAllowList.add("sccfut.dll") -$ModuleAllowList.add("sccfa.dll") -$ModuleAllowList.add("sccfi.dll") -$ModuleAllowList.add("sccch.dll") -$ModuleAllowList.add("sccda.dll") -$ModuleAllowList.add("sccfmt.dll") -$ModuleAllowList.add("sccind.dll") -$ModuleAllowList.add("sccca.dll") -$ModuleAllowList.add("scclo.dll") -$ModuleAllowList.add("SCCOLE2.dll") -$ModuleAllowList.add("SCCSD.dll") -$ModuleAllowList.add("SCCXT.dll") -# cSpell:enable - -Write-SimpleLogFile -string ("Allow List Module Count: " + $ModuleAllowList.count) -Name $LogFile - -$UnexpectedModuleFound = 0 -$showWarning = $false - -# Gather each process and work thru their module list to remove any known modules. -foreach ($process in $ServerProcess) { - - # Determine if it is a known exchange process - if ($ProcessList -contains $process.path ) { - - # Gather all modules - [array]$ProcessModules = $process.modules - - # Remove Microsoft modules - $ProcessModules = $ProcessModules | Where-Object { $_.FileVersionInfo.CompanyName -ne "Microsoft Corporation." -and $_.FileVersionInfo.CompanyName -ne "Microsoft" -and $_.FileVersionInfo.CompanyName -ne "Microsoft Corporation" } - - # Generate and output path for an Non-Default modules file: - $OutputProcessPath = Join-Path $env:LOCALAPPDATA NonDefaultModules.txt - - # Clear out modules from the allow list - foreach ($module in $ModuleAllowList) { - $ProcessModules = $ProcessModules | Where-Object { $_.ModuleName -ne $module -and $_.ModuleName -ne $($module.Replace(".dll", ".ni.dll")) } - } - - if ($ProcessModules.count -gt 0) { - if ($UnexpectedModuleFound -eq 0) { - "`n####################################################################################################" | Out-File $OutputProcessPath -Append - "$((Get-Date).ToString())" | Out-File $OutputProcessPath -Append - "####################################################################################################" | Out-File $OutputProcessPath -Append - } - Write-Warning ("Possible AV Modules found in process $($process.ProcessName)") - $UnexpectedModuleFound++ - foreach ($module in $ProcessModules) { - if ( $process.MainModule.ModuleName -eq "W3wp.exe" -and $showWarning -eq $false) { - Write-Warning "W3wp.exe is not present in the recommended Exclusion list but we found 3rd Party modules on it and could affect Exchange performance or functionality." - Write-SimpleLogFile -string "W3wp.exe is not present in the recommended Exclusion list but we found 3rd Party modules on it and could affect Exchange performance or functionality." -name $LogFile - $showWarning = $true - } - $OutString = ("[FAIL] - PROCESS: $($process.ProcessName) PID($($process.Id)) MODULE: $($module.ModuleName) COMPANY: $($module.Company)`n`t $($module.FileName)") - Write-SimpleLogFile -string $OutString -Name $LogFile -OutHost - $OutString | Out-File $OutputProcessPath -Append - } - } - } + $CorrectExclusionsString = "`nAll EICAR files found; File Exclusions, Extensions Exclusions and Processes Exclusions (Did not find Non-Default modules loaded) appear to be set properly" + $CorrectExclusionsString | Out-File $OutputPath -Append + Write-SimpleLogFile -String $CorrectExclusionsString -LogFile $LogFileName -OutHost } -if ($UnexpectedModuleFound -gt 0) { - "`n####################################################################################################" | Out-File $OutputProcessPath -Append -} - -# Final output for process detection -if ($UnexpectedModuleFound -gt 0) { - Write-SimpleLogFile -string ("Found $($UnexpectedModuleFound) processes with unexpected modules loaded") -Name $LogFile -OutHost - Write-SimpleLogFile ("AV Modules loaded in Exchange processes may indicate that exclusions are not properly configured.") -Name $LogFile -OutHost - Write-SimpleLogFile ("Non AV Modules loaded into Exchange processes may be expected depending on applications installed.") -Name $LogFile -OutHost - Write-Warning ("Review " + $OutputProcessPath + " For more information.") -} else { - Write-SimpleLogFile -string ("Did not find any Non-Default modules loaded.") -Name $LogFile -OutHost -} +Write-SimpleLogFile -string "Testing for AV loaded in processes" -LogFile $LogFileName -OutHost diff --git a/Diagnostics/AVTester/Write-SimpleLogFile.ps1 b/Diagnostics/AVTester/Write-SimpleLogFile.ps1 index 0996d4d0ee..a5f546ff6b 100644 --- a/Diagnostics/AVTester/Write-SimpleLogFile.ps1 +++ b/Diagnostics/AVTester/Write-SimpleLogFile.ps1 @@ -13,14 +13,17 @@ Supports writing a basic log file to LocalAppData .DESCRIPTION Supports basic log file generation for other scripts. -Places the log file into the $env:LocalAppData Folder. +Places the log file into the requested Folder. Supports out putting to the host as well as the log files. .PARAMETER String String to be written into the log file. -.PARAMETER Name +.PARAMETER Path +String with the Path where the log file will be written. + +.PARAMETER FileName Name of the log file. .PARAMETER OutHost @@ -30,18 +33,18 @@ Switch that will write the output to the host as well as the log file. Opens the log file in notepad. .OUTPUTS -Log file specified in the -Name parameter. -Writes the file in to the $Env:LocalAppData +Log file specified in the FileName parameter. +Writes the file in to the Path specified in the Path parameter. .EXAMPLE -Write-SimpleLogFile -String "Start ProcessA" -Name MyLogFile.log +Write-SimpleLogFile -String "Start ProcessA" -FileName MyLogFile.log -Path "C:\temp" -Writes "[Date] - Start ProcessA" to $env:LocalAppData\MyLogFile.log +Writes "[Date] - Start ProcessA" to C:\temp\MyLogFile.log .EXAMPLE -Write-SimpleLogFile -String "Start ProcessB" -Name MyLogFile.log -OutHost +Write-SimpleLogFile -String "Start ProcessB" -FileName MyLogFile.log -OutHost -Path "C:\temp" -Writes "[Date] - Start ProcessB" to $env:LocalAppData\MyLogFile and to the Host +Writes "[Date] - Start ProcessB" to C:\temp\MyLogFile and to the Host #> function Write-SimpleLogFile { @@ -51,8 +54,14 @@ function Write-SimpleLogFile { [Parameter(Mandatory = $true)] [string]$String, - [Parameter(Mandatory = $true)] - [string]$Name, + [parameter(Mandatory)] + [ValidateScript({ + $filePath = Split-Path -Path $_ -Parent + if ($filePath -eq "") { $filePath = "." } + if ((Test-Path -Path $filePath -PathType Container) -and ((Test-Path -Path $_ -PathType Leaf) -or -not ((Test-Path -Path $_ -PathType Container)))) { $true } + else { throw "Path $_ is not valid" } + })] + [string]$LogFile, [switch]$OutHost, @@ -61,9 +70,6 @@ function Write-SimpleLogFile { ) begin { - # Get our log file path - $LogFile = Join-Path $env:LOCALAPPDATA $Name - if ($OpenLog) { Notepad.exe $LogFile exit diff --git a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 index f6943f9900..8aa77457a2 100644 --- a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 @@ -18,7 +18,10 @@ function Get-PowerPlanSetting { if ($null -ne $win32_PowerPlan) { - if ($win32_PowerPlan.InstanceID -eq "Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c}") { + # Guid 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c is 'High Performance' power plan that comes with the OS + # Guid db310065-829b-4671-9647-2261c00e86ef is 'High Performance (ConfigMgr)' power plan when configured via Configuration Manager / SCCM + if (($win32_PowerPlan.InstanceID -eq "Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c}") -or + ($win32_PowerPlan.InstanceID -eq "Microsoft:PowerPlan\{db310065-829b-4671-9647-2261c00e86ef}")) { Write-Verbose "High Performance Power Plan is set to true" $highPerformanceSet = $true } else { Write-Verbose "High Performance Power Plan is NOT set to true" } diff --git a/Setup/SetExchAVExclusions/Set-ExchAVExclusions.ps1 b/Setup/SetExchAVExclusions/Set-ExchAVExclusions.ps1 index 5f68057b16..00b797d4ce 100644 --- a/Setup/SetExchAVExclusions/Set-ExchAVExclusions.ps1 +++ b/Setup/SetExchAVExclusions/Set-ExchAVExclusions.ps1 @@ -124,7 +124,7 @@ if ((-not($SkipVersionCheck)) -and } # Log file name -$LogFile = "SetExchAvExclusions.log" +$LogFile = "$PSScriptRoot\SetExchAvExclusions.log" # Confirm that we are an administrator if (-not (Confirm-Administrator)) { @@ -211,7 +211,7 @@ foreach ($folder in $BaseFolders) { if ($ListRecommendedExclusions) { Write-Host ("$folder") } else { - Write-SimpleLogFile -String ("Adding $folder") -name $LogFile -OutHost + Write-SimpleLogFile -String ("Adding $folder") -LogFile $LogFile -OutHost Add-MpPreference -ExclusionPath $folder } if ($FileName) { @@ -229,7 +229,7 @@ foreach ($extension in $extensionsList) { if ($ListRecommendedExclusions) { Write-Host ("$extension") } else { - Write-SimpleLogFile -String ("Adding $extension") -name $LogFile -OutHost + Write-SimpleLogFile -String ("Adding $extension") -LogFile $LogFile -OutHost Add-MpPreference -ExclusionExtension $extension } if ($FileName) { @@ -247,7 +247,7 @@ foreach ($process in $processesList) { if ($ListRecommendedExclusions) { Write-Host ("$process") } else { - Write-SimpleLogFile -String ("Adding $process") -name $LogFile -OutHost + Write-SimpleLogFile -String ("Adding $process") -LogFile $LogFile -OutHost Add-MpPreference -ExclusionPath $process Add-MpPreference -ExclusionProcess $process } @@ -260,4 +260,4 @@ if ($ListRecommendedExclusions) { Write-Host ('') } -Write-SimpleLogFile -String ("Exclusions Completed") -name $LogFile -OutHost +Write-SimpleLogFile -String ("Exclusions Completed") -LogFile $LogFile -OutHost diff --git a/docs/Databases/VSSTester.md b/docs/Databases/VSSTester.md index 1ea1c2d411..777a61041d 100644 --- a/docs/Databases/VSSTester.md +++ b/docs/Databases/VSSTester.md @@ -13,13 +13,21 @@ 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` +`.\VSSTester -DiskShadow -DatabaseName "Mailbox Database 1637196748" -ExposeSnapshotsOnDriveLetters M, 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 a snapshot using the DiskShadow tool by volume instead of by Database + +`.\VSSTester -DiskShadow -VolumesToBackup D:\, E:\ -ExposeSnapshotsOnDriveLetters M, N` + +Enables tracing and then uses DiskShadow to snapshot the specified volumes. To see a list of available +volumes, including mount points, pass an invalid volume name, such as `-VolumesToBackup foo`. The error +will show the available volumes. Volume names must be typed exactly as shown in that output. + ### Trace in circular mode until the Microsoft Exchange Writer fails `.\VSSTester -WaitForWriterFailure -DatabaseName "Mailbox Database 1637196748"` @@ -34,6 +42,11 @@ automatically. Note that script syntax and output has changed. Syntax and screenshots in the above articles are out of date. +## Missing Microsoft Exchange Writer +We have seen a few cases where the Microsoft Exchange Writer will disappear after an unspecified amount of time and restarting the Microsoft Exchange Replication service. Steps on how to resolve this are linked here: + +* https://learn.microsoft.com/en-US/troubleshoot/windows-server/backup-and-storage/event-id-513-vss-windows-server + ## COM+ Security Here are the steps to verify that the local Administrators group is allowed to the COM+ Security on the computer. The script will detect if this is a possibility if we can not see the Exchange Writers and we have the registry settings set that determine this is a possibility. diff --git a/docs/Diagnostics/Test-ExchAVExclusions.md b/docs/Diagnostics/Test-ExchAVExclusions.md index 063f7d0b17..b588705f2e 100644 --- a/docs/Diagnostics/Test-ExchAVExclusions.md +++ b/docs/Diagnostics/Test-ExchAVExclusions.md @@ -52,6 +52,7 @@ If the Module is from an AV or Security software vendor it is a strong indicatio Parameter | Description | ----------|-------------| +WaitingTimeForAVAnalysisInMinutes | Set the waiting time for AV to analyze the EICAR files. Default is 5 minutes. Recurse | Places an EICAR file in all SubFolders as well as the root. OpenLog | Opens the script log file. SkipVersionCheck | Skip script version verification. @@ -61,10 +62,7 @@ ScriptUpdateOnly | Just update script version to latest one. ## Outputs Log file: -$env:LOCALAPPDATA\ExchAvExclusions.log +$PSScriptRoot\ExchAvExclusions.log -List of Folders and extensions Scanned by AV: -$env:LOCALAPPDATA\BadExclusions.txt - -List of Non-Default Processes: -$env:LOCALAPPDATA\NonDefaultModules.txt +List of Folders, extensions Scanned by AV and List of Non-Default Processes: +$PSScriptRoot\BadExclusions.txt diff --git a/docs/Emerging-Issues.md b/docs/Emerging-Issues.md index 504520f812..9a2885cf5d 100644 --- a/docs/Emerging-Issues.md +++ b/docs/Emerging-Issues.md @@ -9,6 +9,7 @@ This page lists emerging issues for Exchange On-Premises deployments, possible r |**Updated on** | **Update causing the issue**| **Issue**| **Workaround/Solution** |-|-|-|-| +11/23/2023 | [November 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2023-exchange-server-security-updates/ba-p/3980209) for Exchange 2016, Exchange 2019 | Some customers may find queue viewer crashing with error

"Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints" | The error can occur if the Exchange server auth certificate has expired. Solution is to renew the [Exchange server auth certificate manually](https://learn.microsoft.com/exchange/troubleshoot/administration/cannot-access-owa-or-ecp-if-oauth-expired) or by using [this script](https://microsoft.github.io/CSS-Exchange/Admin/MonitorExchangeAuthCertificate/) 10/12/2023|[All versions of August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | Users in account forest can't change expired password in OWA in multi-forest Exchange deployments after installing any version of [August 2023 Security Update for Exchange servers](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811)

**Note**
The account forest user will be able to change the password after they sign in to Outlook on the web if their password is not yet expired. The issue affects only account forest users who have passwords that are already expired. This change does not affect users in organizations that don't use multiple forests.|** Update on 10/12/2023 **

Follow steps on [this article](https://support.microsoft.com/topic/users-in-account-forest-can-t-change-expired-password-in-owa-in-multi-forest-exchange-deployments-after-installing-august-2023-su-b17c3579-0233-4d84-9245-755dd1092edb) 8/15/2023|[Non-English August 2023 Security Update](https://techcommunity.microsoft.com/t5/exchange-team-blog/released-august-2023-exchange-server-security-updates/ba-p/3892811) for Exchange 2016, Exchange 2019 | When you install the Microsoft Exchange Server 2019 or 2016 August 2023 Security Update (SU) on a Windows Server-based device that is running a non-English operating system (OS) version, Setup suddenly stops and rolls back the changes. However, the Exchange Server services remain in a disabled state. |The latest SUs have been released that do not require a workaround to install. If you used a workaround to install KB5029388, it is highly recommend to uninstall the KB5029388 to avoid issues down the line. For more information please check out [this KB](https://support.microsoft.com/topic/exchange-server-2019-and-2016-august-2023-security-update-installation-fails-on-non-english-operating-systems-ef38d805-f645-4511-8cc5-cf967e5d5c75). 6/15/2023|[January 2023 Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=104914) for Exchange 2016, Exchange 2019 | When you try to uninstall Microsoft Exchange Server 2019 or 2016 on servers, that had January 2023 Security Update for Exchange Server installed at any point, the Setup fails with following error message:

[ERROR] The operation couldn't be performed because object '' couldn't be found on ''. |Install Exchange Security Update June 2023 or higher to resolve the issue. Check [this KB](https://support.microsoft.com/help/5025312) for more details diff --git a/docs/Setup/Set-ExchAVExclusions.md b/docs/Setup/Set-ExchAVExclusions.md index 4b2043d274..6c64205969 100644 --- a/docs/Setup/Set-ExchAVExclusions.md +++ b/docs/Setup/Set-ExchAVExclusions.md @@ -71,4 +71,4 @@ Exclusions List File: `FileName` Log file: -$env:LOCALAPPDATA\SetExchAvExclusions.log +$PSScriptRoot\SetExchAvExclusions.log