From 392f6656019b725affaa119c36c1e177f330148e Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Wed, 22 Nov 2023 11:43:51 -0800 Subject: [PATCH 01/20] Save RBA Log, RBA Logs summary, Better coverage of known issues --- Calendar/Get-RBASummary.ps1 | 104 +++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 26 deletions(-) diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index 48612c0f9b..e2aef6acbd 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: RequestOutOfPolicy is set 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" } @@ -493,13 +506,18 @@ function RBALogSummary { 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 +540,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 +686,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 +703,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 $_ } From 066c61bb230904f0e54788aa9e20626d15dbff8e Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Wed, 22 Nov 2023 11:49:30 -0800 Subject: [PATCH 02/20] tweak msg --- Calendar/Get-RBASummary.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index e2aef6acbd..c08e381b45 100644 --- a/Calendar/Get-RBASummary.ps1 +++ b/Calendar/Get-RBASummary.ps1 @@ -366,7 +366,7 @@ function RBADelegateSettings { $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: RequestOutOfPolicy is set but there are no Delegates.- All Out of policy requests by these users will be Tentatively accepted." + 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." } From 783fe1736004700081c945c541153ec52ae3a9a2 Mon Sep 17 00:00:00 2001 From: Bhalchandra Atre-MSFT <39634045+Batre-MSFT@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:07:38 +0530 Subject: [PATCH 03/20] Update Emerging-Issues.md Queue viewer crash --- docs/Emerging-Issues.md | 1 + 1 file changed, 1 insertion(+) 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 From a120a46a5a9c62b791765ec7036587fb3a0ed691 Mon Sep 17 00:00:00 2001 From: iserrano76 Date: Mon, 27 Nov 2023 18:24:24 +0100 Subject: [PATCH 04/20] Output changes, parameter for waiting time. --- .../AVTester/Test-ExchAVExclusions.ps1 | 242 +++++++++--------- docs/Diagnostics/Test-ExchAVExclusions.md | 1 + 2 files changed, 128 insertions(+), 115 deletions(-) diff --git a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 index 5d603aa82b..4e4ab39d21 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. @@ -47,9 +53,6 @@ $env:LOCALAPPDATA\ExchAvExclusions.log List of Scanned Folders: $env:LOCALAPPDATA\BadExclusions.txt -List of Non-Default Processes -$env:LOCALAPPDATA NonDefaultModules.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 = "" @@ -285,8 +290,97 @@ foreach ($extension in $extensionsList) { Write-SimpleLogFile -String "Access EICAR Files Finished" -name $LogFile -OutHost -# Sleeping 5 minutes for AV to "find" the files -Start-SleepWithProgress -SleepTime 300 -message "Allowing time for AV to Scan" +$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] + +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) -Name $LogFile + + # 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" } + + # 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) { + 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" -Name $LogFile -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 + } +} # Create a list of folders that are probably being scanned by AV $BadFolderList = New-Object Collections.Generic.List[string] @@ -350,126 +444,44 @@ foreach ($extension in $extensionsList) { #Delete Random Folder Remove-Item $randomFolder +$OutputPath = Join-Path $env:LOCALAPPDATA 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 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 -name $LogFile + "`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 + $CorrectExclusionsString = "All 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 + Write-SimpleLogFile -String $CorrectExclusionsString -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 - } - } - } -} - -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 -} diff --git a/docs/Diagnostics/Test-ExchAVExclusions.md b/docs/Diagnostics/Test-ExchAVExclusions.md index 063f7d0b17..8b58cb40be 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. From 5964eaa948099dd3686289b80003caf8b61456ea Mon Sep 17 00:00:00 2001 From: johage <37371976+johage@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:19:57 -0600 Subject: [PATCH 05/20] Update VSSTester.md --- docs/Databases/VSSTester.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Databases/VSSTester.md b/docs/Databases/VSSTester.md index 1ea1c2d411..5eb7f7713e 100644 --- a/docs/Databases/VSSTester.md +++ b/docs/Databases/VSSTester.md @@ -34,6 +34,10 @@ 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. From 257569b1d157e8bcf02c70e42c75c992c5c02b97 Mon Sep 17 00:00:00 2001 From: johage <37371976+johage@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:13:16 -0600 Subject: [PATCH 06/20] Update VSSTester.md Fixing formatting with URL --- docs/Databases/VSSTester.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Databases/VSSTester.md b/docs/Databases/VSSTester.md index 5eb7f7713e..f2169b6fba 100644 --- a/docs/Databases/VSSTester.md +++ b/docs/Databases/VSSTester.md @@ -36,6 +36,7 @@ Note that script syntax and output has changed. Syntax and screenshots in the ab ## 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 From 8a0244f1697f6eda3e5a45f48685fb248c66f60e Mon Sep 17 00:00:00 2001 From: lusassl-msft Date: Wed, 29 Nov 2023 14:50:25 +0100 Subject: [PATCH 07/20] Add High Performance (ConfigMgr) powerplan --- .../ServerInformation/Get-PowerPlanSetting.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 index f6943f9900..3c51d2fe0f 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' powerplan that comes with the OS + # Guid db310065-829b-4671-9647-2261c00e86ef is 'High Performance (ConfigMgr)' powerplan 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" } From 7b9e31bfb3eaa836b8eafbd2273defe6c4828baf Mon Sep 17 00:00:00 2001 From: lusassl-msft Date: Wed, 29 Nov 2023 15:01:22 +0100 Subject: [PATCH 08/20] Typo fixed --- .../DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 index 3c51d2fe0f..8aa77457a2 100644 --- a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-PowerPlanSetting.ps1 @@ -18,8 +18,8 @@ function Get-PowerPlanSetting { if ($null -ne $win32_PowerPlan) { - # Guid 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c is 'High Performance' powerplan that comes with the OS - # Guid db310065-829b-4671-9647-2261c00e86ef is 'High Performance (ConfigMgr)' powerplan when configured via Configuration Manager / SCCM + # 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" From af2c0f8b1e75e0882a0d32d24b375700dfeba27f Mon Sep 17 00:00:00 2001 From: iserrano76 Date: Fri, 1 Dec 2023 11:03:55 +0100 Subject: [PATCH 09/20] Modified the outputpath and doc --- .../AVTester/Test-ExchAVExclusions.ps1 | 76 +++++++++---------- Diagnostics/AVTester/Write-SimpleLogFile.ps1 | 30 +++++--- docs/Diagnostics/Test-ExchAVExclusions.md | 9 +-- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 index 4e4ab39d21..58e9f7c1f1 100644 --- a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 +++ b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 @@ -48,10 +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 +$PSScriptRoot\BadExclusions.txt .EXAMPLE .\Test-ExchAVExclusions.ps1 @@ -109,10 +109,10 @@ if ((-not($SkipVersionCheck)) -and } # Log file name -$LogFile = "ExchAvExclusions.log" +$LogFileName = "ExchAvExclusions.log" # Open log file if switched -if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -Name $LogFile } +if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -FileName $LogFileName -Path $PSScriptRoot } # Confirm that we are an administrator if (-not (Confirm-Administrator)) { @@ -150,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 ("###########################################################################################") -FileName $LogFileName -Path $PSScriptRoot +Write-SimpleLogFile -String ("Starting AV Exclusions analysis at $((Get-Date).ToString())") -FileName $LogFileName -Path $PSScriptRoot +Write-SimpleLogFile -String ("###########################################################################################") -FileName $LogFileName -Path $PSScriptRoot +Write-SimpleLogFile -String ("You can find a detailed log on: $LogFileName") -FileName $LogFileName -OutHost -Path $PSScriptRoot # Create the Array List $BaseFolders = Get-ExchAVExclusionsPaths -ExchangePath $ExchangePath -MsiProductMinor ([byte]$serverExchangeInstallDirectory.MsiProductMinor) @@ -176,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) -FileName $LogFileName -Path $PSScriptRoot } # 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) -FileName $LogFileName -Path $PSScriptRoot } # If -recurse then we need to find all SubFolders and Add them to the list to be tested @@ -196,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) -FileName $LogFileName -Path $PSScriptRoot } } # Remove any Duplicates $FolderList = $FolderList | Select-Object -Unique -Write-SimpleLogFile -String "Creating EICAR Files" -name $LogFile -OutHost +Write-SimpleLogFile -String "Creating EICAR Files" -FileName $LogFileName -OutHost -Path $PSScriptRoot # Create the EICAR file in each path $eicarFileName = "eicar" @@ -215,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) -FileName $LogFileName -Path $PSScriptRoot if (!(Test-Path -Path $FilePath)) { @@ -230,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) -FileName $LogFileName -OutHost -Path $PSScriptRoot } } @@ -243,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) -FileName $LogFileName -Path $PSScriptRoot if (!(Test-Path -Path $FilePath)) { @@ -256,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!: ") -FileName $LogFileName -OutHost -Path $PSScriptRoot } } } 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot -Write-SimpleLogFile -String "Accessing EICAR Files" -name $LogFile -OutHost +Write-SimpleLogFile -String "Accessing EICAR Files" -FileName $LogFileName -OutHost -Path $PSScriptRoot # 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) -FileName $LogFileName -Path $PSScriptRoot Start-Process -FilePath more -ArgumentList """$FilePath""" -ErrorAction SilentlyContinue -WindowStyle Hidden | Out-Null } $i++ @@ -282,13 +282,13 @@ $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) -FileName $LogFileName -Path $PSScriptRoot 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot $StartDate = Get-Date [int]$initialDiff = (New-TimeSpan -End $StartDate.AddMinutes($WaitingTimeForAVAnalysisInMinutes) -Start $StartDate).TotalSeconds @@ -342,7 +342,7 @@ while ($currentDiff -gt 0) { $ModuleAllowList.add("SCCXT.dll") # cSpell:enable - Write-SimpleLogFile -string ("Allow List Module Count: " + $ModuleAllowList.count) -Name $LogFile + Write-SimpleLogFile -string ("Allow List Module Count: " + $ModuleAllowList.count) -FileName $LogFileName -Path $PSScriptRoot # Gather each process and work thru their module list to remove any known modules. foreach ($process in $ServerProcess) { @@ -364,7 +364,7 @@ while ($currentDiff -gt 0) { 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" -Name $LogFile -OutHost + Write-SimpleLogFile -string "[FAIL] - $OutString" -FileName $LogFileName -OutHost -Path $PSScriptRoot if ($process.MainModule.ModuleName -eq "W3wp.exe") { $SuspiciousW3wpProcessList += $OutString } else { @@ -385,7 +385,7 @@ while ($currentDiff -gt 0) { # 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot # Test each location for the EICAR file foreach ($Folder in $FolderList) { @@ -397,22 +397,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) -FileName $LogFileName -Path $PSScriptRoot 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) -FileName $LogFileName -OutHost -Path $PSScriptRoot $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) -FileName $LogFileName -OutHost -Path $PSScriptRoot $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) -FileName $LogFileName -Path $PSScriptRoot } } @@ -427,16 +427,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) -FileName $LogFileName -Path $PSScriptRoot 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) -FileName $LogFileName -OutHost -Path $PSScriptRoot $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) -FileName $LogFileName -OutHost -Path $PSScriptRoot $BadExtensionList.Add($extension) } } @@ -444,7 +444,7 @@ foreach ($extension in $extensionsList) { #Delete Random Folder Remove-Item $randomFolder -$OutputPath = Join-Path $env:LOCALAPPDATA BadExclusions.txt +$OutputPath = Join-Path $PSScriptRoot BadExclusions.txt "###########################################################################################" | Out-File $OutputPath "Exclusions analysis at $((Get-Date).ToString())" | Out-File $OutputPath -Append "###########################################################################################" | Out-File $OutputPath -Append @@ -452,7 +452,7 @@ $OutputPath = Join-Path $env:LOCALAPPDATA BadExclusions.txt # Report what we found 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" -FileName $LogFileName -Path $PSScriptRoot if ($BadFolderList.count -gt 0 ) { "`n[Missing Folder Exclusions]" | Out-File $OutputPath -Append $BadFolderList | Out-File $OutputPath -Append @@ -472,16 +472,16 @@ if ($BadFolderList.count -gt 0 -or $BadExtensionList.Count -gt 0 -or $Suspicious $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 -name $LogFile + Write-SimpleLogFile -string $SuspiciousW3wpProcessListString -FileName $LogFileName -Path $PSScriptRoot "`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 { - $CorrectExclusionsString = "All 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 - Write-SimpleLogFile -String $CorrectExclusionsString -Name $LogFile -OutHost + $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 -FileName $LogFileName -OutHost -Path $PSScriptRoot } -Write-SimpleLogFile -string "Testing for AV loaded in processes" -name $LogFile -OutHost +Write-SimpleLogFile -string "Testing for AV loaded in processes" -FileName $LogFileName -OutHost -Path $PSScriptRoot diff --git a/Diagnostics/AVTester/Write-SimpleLogFile.ps1 b/Diagnostics/AVTester/Write-SimpleLogFile.ps1 index 0996d4d0ee..702ffee4a5 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,15 @@ function Write-SimpleLogFile { [Parameter(Mandatory = $true)] [string]$String, + [parameter(Mandatory)] + [ValidateScript({ + if (Test-Path -Path $_ -PathType Container) { $true } + else { throw "Path $_ is not valid" } + })] + [String]$Path, + [Parameter(Mandatory = $true)] - [string]$Name, + [string]$FileName, [switch]$OutHost, @@ -62,7 +72,7 @@ function Write-SimpleLogFile { begin { # Get our log file path - $LogFile = Join-Path $env:LOCALAPPDATA $Name + $LogFile = Join-Path $Path $FileName if ($OpenLog) { Notepad.exe $LogFile diff --git a/docs/Diagnostics/Test-ExchAVExclusions.md b/docs/Diagnostics/Test-ExchAVExclusions.md index 8b58cb40be..b588705f2e 100644 --- a/docs/Diagnostics/Test-ExchAVExclusions.md +++ b/docs/Diagnostics/Test-ExchAVExclusions.md @@ -62,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 From 16cadbac0e597bbfd054c9d382ad911d1a012a23 Mon Sep 17 00:00:00 2001 From: iserrano76 Date: Mon, 4 Dec 2023 17:22:07 +0100 Subject: [PATCH 10/20] Fix Set-ExchAVExclusions and use just one param --- .../AVTester/Test-ExchAVExclusions.ps1 | 71 ++++++++++--------- Diagnostics/AVTester/Write-SimpleLogFile.ps1 | 12 ++-- .../Set-ExchAVExclusions.ps1 | 10 +-- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 index 58e9f7c1f1..062dab1a86 100644 --- a/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 +++ b/Diagnostics/AVTester/Test-ExchAVExclusions.ps1 @@ -109,10 +109,10 @@ if ((-not($SkipVersionCheck)) -and } # Log file name -$LogFileName = "ExchAvExclusions.log" +$LogFileName = Join-Path $PSScriptRoot ExchAvExclusions.log # Open log file if switched -if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -FileName $LogFileName -Path $PSScriptRoot } +if ($OpenLog) { Write-SimpleLogFile -OpenLog -String " " -LogFile $LogFileName } # Confirm that we are an administrator if (-not (Confirm-Administrator)) { @@ -150,10 +150,10 @@ if (-not($exchangeShell.ShellLoaded)) { exit } -Write-SimpleLogFile -String ("###########################################################################################") -FileName $LogFileName -Path $PSScriptRoot -Write-SimpleLogFile -String ("Starting AV Exclusions analysis at $((Get-Date).ToString())") -FileName $LogFileName -Path $PSScriptRoot -Write-SimpleLogFile -String ("###########################################################################################") -FileName $LogFileName -Path $PSScriptRoot -Write-SimpleLogFile -String ("You can find a detailed log on: $LogFileName") -FileName $LogFileName -OutHost -Path $PSScriptRoot +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) @@ -176,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) -FileName $LogFileName -Path $PSScriptRoot + 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) -FileName $LogFileName -Path $PSScriptRoot + 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 @@ -196,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) -FileName $LogFileName -Path $PSScriptRoot } + } 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot +Write-SimpleLogFile -String "Creating EICAR Files" -LogFile $LogFileName -OutHost # Create the EICAR file in each path $eicarFileName = "eicar" @@ -215,7 +215,7 @@ $eicarFullFileName = "$eicarFileName.$eicarFileExt" foreach ($Folder in $FolderList) { [string] $FilePath = (Join-Path $Folder $eicarFullFileName) - Write-SimpleLogFile -String ("Creating $eicarFullFileName file " + $FilePath) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -String ("Creating $eicarFullFileName file " + $FilePath) -LogFile $LogFileName if (!(Test-Path -Path $FilePath)) { @@ -230,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) -FileName $LogFileName -OutHost -Path $PSScriptRoot + Write-SimpleLogFile -string ("[WARNING] - $eicarFullFileName already exists!: " + $FilePath) -LogFile $LogFileName -OutHost } } @@ -243,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) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -String ("Creating $eicarFileName.$extension file " + $FilePath) -LogFile $LogFileName if (!(Test-Path -Path $FilePath)) { @@ -256,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!: ") -FileName $LogFileName -OutHost -Path $PSScriptRoot + 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot +Write-SimpleLogFile -String "EICAR Files Created" -LogFile $LogFileName -OutHost -Write-SimpleLogFile -String "Accessing EICAR Files" -FileName $LogFileName -OutHost -Path $PSScriptRoot +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) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -String ("Opening $eicarFullFileName file " + $FilePath) -LogFile $LogFileName Start-Process -FilePath more -ArgumentList """$FilePath""" -ErrorAction SilentlyContinue -WindowStyle Hidden | Out-Null } $i++ @@ -282,13 +282,13 @@ $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) -FileName $LogFileName -Path $PSScriptRoot + 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot +Write-SimpleLogFile -String "Access EICAR Files Finished" -LogFile $LogFileName -OutHost $StartDate = Get-Date [int]$initialDiff = (New-TimeSpan -End $StartDate.AddMinutes($WaitingTimeForAVAnalysisInMinutes) -Start $StartDate).TotalSeconds @@ -297,6 +297,7 @@ $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 @@ -342,11 +343,14 @@ while ($currentDiff -gt 0) { $ModuleAllowList.add("SCCXT.dll") # cSpell:enable - Write-SimpleLogFile -string ("Allow List Module Count: " + $ModuleAllowList.count) -FileName $LogFileName -Path $PSScriptRoot + 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 ) { @@ -364,7 +368,7 @@ while ($currentDiff -gt 0) { 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" -FileName $LogFileName -OutHost -Path $PSScriptRoot + Write-SimpleLogFile -string "[FAIL] - $OutString" -LogFile $LogFileName -OutHost if ($process.MainModule.ModuleName -eq "W3wp.exe") { $SuspiciousW3wpProcessList += $OutString } else { @@ -381,11 +385,12 @@ while ($currentDiff -gt 0) { [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" -FileName $LogFileName -OutHost -Path $PSScriptRoot +Write-SimpleLogFile -string "Testing for EICAR files" -LogFile $LogFileName -OutHost # Test each location for the EICAR file foreach ($Folder in $FolderList) { @@ -397,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) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -String ("Removing " + $FilePath) -LogFile $LogFileName Remove-Item $FilePath -Confirm:$false -Force } else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Path: " + $Folder) -FileName $LogFileName -OutHost -Path $PSScriptRoot + 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) -FileName $LogFileName -OutHost -Path $PSScriptRoot + 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) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -string ("Removed folder: " + $Folder) -LogFile $LogFileName } } @@ -427,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) -FileName $LogFileName -Path $PSScriptRoot + Write-SimpleLogFile -String ("Removing " + $FilePath) -LogFile $LogFileName Remove-Item $FilePath -Confirm:$false -Force } else { - Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -FileName $LogFileName -OutHost -Path $PSScriptRoot + 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) -FileName $LogFileName -OutHost -Path $PSScriptRoot + Write-SimpleLogFile -String ("[FAIL] - Possible AV Scanning on Extension: " + $extension) -LogFile $LogFileName -OutHost $BadExtensionList.Add($extension) } } @@ -452,7 +457,7 @@ $OutputPath = Join-Path $PSScriptRoot BadExclusions.txt # Report what we found 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" -FileName $LogFileName -Path $PSScriptRoot + 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 @@ -472,7 +477,7 @@ if ($BadFolderList.count -gt 0 -or $BadExtensionList.Count -gt 0 -or $Suspicious $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 -FileName $LogFileName -Path $PSScriptRoot + 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 ") @@ -481,7 +486,7 @@ if ($BadFolderList.count -gt 0 -or $BadExtensionList.Count -gt 0 -or $Suspicious } else { $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 -FileName $LogFileName -OutHost -Path $PSScriptRoot + Write-SimpleLogFile -String $CorrectExclusionsString -LogFile $LogFileName -OutHost } -Write-SimpleLogFile -string "Testing for AV loaded in processes" -FileName $LogFileName -OutHost -Path $PSScriptRoot +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 702ffee4a5..a5f546ff6b 100644 --- a/Diagnostics/AVTester/Write-SimpleLogFile.ps1 +++ b/Diagnostics/AVTester/Write-SimpleLogFile.ps1 @@ -56,13 +56,12 @@ function Write-SimpleLogFile { [parameter(Mandatory)] [ValidateScript({ - if (Test-Path -Path $_ -PathType Container) { $true } + $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]$Path, - - [Parameter(Mandatory = $true)] - [string]$FileName, + [string]$LogFile, [switch]$OutHost, @@ -71,9 +70,6 @@ function Write-SimpleLogFile { ) begin { - # Get our log file path - $LogFile = Join-Path $Path $FileName - if ($OpenLog) { Notepad.exe $LogFile exit 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 From 720fbb50df3aaf6f72bf4f36b024082e39c924aa Mon Sep 17 00:00:00 2001 From: iserrano76 Date: Mon, 4 Dec 2023 17:25:56 +0100 Subject: [PATCH 11/20] Updated Set-ExchAVExclusions docs --- docs/Setup/Set-ExchAVExclusions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 56b8f5c877e23140c9730107d76505ae7779768f Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Mon, 20 Nov 2023 10:40:44 -0800 Subject: [PATCH 12/20] Adding Outputing of Sharing Type specifically Adding logic for Publishing Format tables Output cmdlet calls --- Calendar/Check-SharingStatus.ps1 | 203 +++++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 39 deletions(-) diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index 66273ff5e7..a9ae71b944 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,79 @@ 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 +262,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 +272,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 +298,42 @@ 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 'et-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 +349,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 + } + + $script:ReceiverMB | Format-List DisplayName, Database, LitigationHoldEnabled, CalendarVersionStoreDisabled, CalendarRepairDisabled, RecipientType* - Write-Host -ForegroundColor Cyan "Receiver Calendar Folders (look for a copy of [$Owner] Calendar):" + 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,18 +398,27 @@ function GetReceiverInformation { $script:PIIAccess = $false } - Write-Host -ForegroundColor Cyan "`n`nReceiver Accepted the Following Modern Calendar Sharing Accept Logs:" ProcessCalendarSharingAcceptLogs -Identity $Receiver + ProcessInternetCalendarLogs -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)" + # 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 @@ -306,7 +429,7 @@ function GetReceiverInformation { } } - if ($script:PIIAccess) { + 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)" @@ -316,11 +439,13 @@ function GetReceiverInformation { Write-Error "Failed to get the Owner Calendar from the Receiver Mailbox. This is fine if not using Modern Sharing." } } else { - Write-Host "Do Not have PII information for the Receiver." - Write-Host "Get PII Access for $($script:OwnerMB.Database)." + 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." } } # Main +$script:ModernSharing +$script:SharingType GetOwnerInformation -Owner $Owner GetReceiverInformation -Receiver $Receiver From 7e5e4adc1dd533ae7af79a6db07092dee9a2479f Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 5 Dec 2023 08:44:23 -0800 Subject: [PATCH 13/20] Skip checks if they are not using modern sharing --- Calendar/Check-SharingStatus.ps1 | 79 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index a9ae71b944..27ad69cecc 100644 --- a/Calendar/Check-SharingStatus.ps1 +++ b/Calendar/Check-SharingStatus.ps1 @@ -401,46 +401,51 @@ function GetReceiverInformation { ProcessCalendarSharingAcceptLogs -Identity $Receiver ProcessInternetCalendarLogs -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)" - - 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: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) -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." + #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 Owner, so can not check the Receivers Copy of the Owner Calendar." - Write-Host "Get PII Access for both mailboxes and try again." } } From 4ce78c91c06edbc9adfd4a4c3871db4560d138a1 Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Mon, 20 Nov 2023 10:40:44 -0800 Subject: [PATCH 14/20] Adding Outputing of Sharing Type specifically Adding logic for Publishing Format tables Output cmdlet calls Skip checks if they are not using modern sharing Output Sharing specifically at bottom --- Calendar/Check-SharingStatus.ps1 | 255 +++++++++++++++++++++++-------- 1 file changed, 195 insertions(+), 60 deletions(-) diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index 66273ff5e7..dee7f5e71c 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,79 @@ 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 +262,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 +272,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 +298,42 @@ 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 +349,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 +398,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." From de0c8d6f6c19db1120059f52a2faa0b6c426af0e Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 5 Dec 2023 10:15:59 -0800 Subject: [PATCH 15/20] Clean --- Calendar/Check-SharingStatus.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Calendar/Check-SharingStatus.ps1 b/Calendar/Check-SharingStatus.ps1 index 8e895d2c02..e73c8d2a36 100644 --- a/Calendar/Check-SharingStatus.ps1 +++ b/Calendar/Check-SharingStatus.ps1 @@ -404,7 +404,7 @@ function GetReceiverInformation { ProcessCalendarSharingAcceptLogs -Identity $Receiver ProcessInternetCalendarLogs -Identity $Receiver - if (($script:SharingType -like "InternalSharing") -or + if (($script:SharingType -like "InternalSharing") -or ($script:SharingType -like "ExternalSharing")) { # Validate Modern Sharing Status if (Get-Command -Name Get-CalendarEntries -ErrorAction SilentlyContinue) { @@ -419,12 +419,12 @@ function GetReceiverInformation { 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 + $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 + $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 From f520f99508bc753f8dc5c62d30d89055d089edec Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 5 Dec 2023 13:17:21 -0800 Subject: [PATCH 16/20] Update Summary output Code standards --- .../Get-CalendarDiagnosticObjectsSummary.ps1 | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index f41d90668f..409704ecb1 100644 --- a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 +++ b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 @@ -550,7 +550,7 @@ function CreateShortClientName { } elseif ($ClientInfoString -like "*Microsoft Outlook 16*") { $ShortClientName = "Outlook-ModernCalendarSharing" } else { - $ShortClientName = "Rest" + $ShortClientName = "[Unknown Rest Client]" } } else { $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor @@ -1058,19 +1058,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 +1108,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 +1149,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 +1166,28 @@ 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)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 +1199,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 +1221,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)." From ab5b64b58462872775e1e6be6e3b8adcecbe98e2 Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 5 Dec 2023 16:12:50 -0800 Subject: [PATCH 17/20] Update EWS short name mapping --- Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index 409704ecb1..9228058337 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" @@ -552,6 +553,13 @@ function CreateShortClientName { } else { $ShortClientName = "[Unknown Rest Client]" } + # Client=WebServices;Mozilla/5.0 (ZoomPresence.Android 8.1.0 x86); + } elseif ($ClientInfoString -like "Client=WebServices*") { + if ($ClientInfoString -like "*ZoomPresence*") { + $ShortClientName = "ZoomPresence" + } else { + $ShortClientName = "EWS App" + } } else { $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor } @@ -569,6 +577,10 @@ function CreateShortClientName { $ShortClientName = "Outlook-ModernCalendarSharing" } + if ($ShortClientName -eq "") { + $ShortClientName = "[NoShortNameFound]" + } + return $ShortClientName } @@ -1167,7 +1179,7 @@ function BuildTimeline { [array] $Output = "" } ResourceBookingAssistant { - [array] $Output = "ResourceBookingAssistant $($CalLog.TriggerAction) the Meeting." + [array] $Output = "ResourceBookingAssistant $($CalLog.TriggerAction)d the Meeting." } default { if ($CalLog.ResponsibleUser -eq "Calendar Assistant") { From 453cd87a32e5db7fa56965bfeac03645b566853d Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Tue, 5 Dec 2023 16:16:11 -0800 Subject: [PATCH 18/20] CodeFormatter --- Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 index 9228058337..4d5892d96e 100644 --- a/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 +++ b/Calendar/Get-CalendarDiagnosticObjectsSummary.ps1 @@ -554,14 +554,16 @@ function CreateShortClientName { $ShortClientName = "[Unknown Rest Client]" } # Client=WebServices;Mozilla/5.0 (ZoomPresence.Android 8.1.0 x86); - } elseif ($ClientInfoString -like "Client=WebServices*") { + } else { + $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor + } + + if ($ShortClientName -eq "" -And $ClientInfoString -like "Client=WebServices*") { if ($ClientInfoString -like "*ZoomPresence*") { $ShortClientName = "ZoomPresence" } else { - $ShortClientName = "EWS App" + $ShortClientName = "Unknown EWS App" } - } else { - $ShortClientName = findMatch -PassedHash $ShortClientNameProcessor } if ($ClientInfoString -like "*InternalCalendarSharing*" -and $ClientInfoString -like "*OWA*") { From 37dc18f78ab53e361a612edabc5b8219a38e9058 Mon Sep 17 00:00:00 2001 From: Shane Ferrell Date: Wed, 6 Dec 2023 08:04:05 -0800 Subject: [PATCH 19/20] PR Fix - Predefine First / Last --- Calendar/Get-RBASummary.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Calendar/Get-RBASummary.ps1 b/Calendar/Get-RBASummary.ps1 index c08e381b45..e804582c8a 100644 --- a/Calendar/Get-RBASummary.ps1 +++ b/Calendar/Get-RBASummary.ps1 @@ -502,6 +502,8 @@ 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() From 6529140de257cee553a410d894102b967383889d Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 30 Nov 2023 00:02:38 -0600 Subject: [PATCH 20/20] VSSTester: Allow specifying volumes to back up In previous versions of VSSTester, the user had to specify a database when testing a DiskShadow backup. This would cause VSSTester to look up the volumes that stored the database and log files, and then it would snap those volumes. This approach works for most cases, but it makes it hard to compare with backup software that only allows specifying volumes, not databases. This change allows running a DiskShadow backup of specified volumes. To get a list of acceptable volume names, the user can specify one that is invalid. For example: ``` [PS] C:\>.\VSSTester.ps1 -DiskShadow -VolumesToBackup foo C:\VSSTester.ps1 : Cannot validate argument on parameter 'VolumesToBackup'. Invalid volume specified. Please specify one of the following values: C:\ C:\Databases\DB1\ At line:1 char:70 + ... code\CSS-Exchange\dist\VSSTester.ps1 -DiskShadow -VolumesToBackup foo ``` One or two volumes can be specified. As usual, drive letters must be provided. For this syntax, the number of provided drive letters must match the number of specified volumes. Example: ``` VSSTester.ps1 -DiskShadow -VolumesToBackup C:\Databases\DB1\ -ExposeSnapshotsOnDriveLetters X ``` This PR includes several other changes: * Get-WmiObject has been replaced with Get-CimInstance. * Drive letters are now specified with -ExposeSnapshotsOnDriveLetters, which more clearly communicates the intent and allows specifying one or more drive letters with a single parameter. --- .../Invoke-CreateDiskShadowFile.ps1 | 245 +++++++----------- .../DiskShadow/Invoke-DiskShadow.ps1 | 8 +- .../Logging/Invoke-DisableExtraTracing.ps1 | 8 +- .../Logging/Invoke-EnableExtraTracing.ps1 | 8 +- Databases/VSSTester/VSSTester.ps1 | 92 ++++--- docs/Databases/VSSTester.md | 10 +- 6 files changed, 177 insertions(+), 194 deletions(-) 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/docs/Databases/VSSTester.md b/docs/Databases/VSSTester.md index f2169b6fba..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"`