From a328087fda8663c85551e54c7501908b9a5a8d30 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 15 Aug 2024 09:58:41 -0500 Subject: [PATCH 01/12] Exchange 2019 CU15 Release Build Number --- Shared/Get-ExchangeBuildVersionInformation.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Shared/Get-ExchangeBuildVersionInformation.ps1 b/Shared/Get-ExchangeBuildVersionInformation.ps1 index feb26008fa..6b8a0fe12b 100644 --- a/Shared/Get-ExchangeBuildVersionInformation.ps1 +++ b/Shared/Get-ExchangeBuildVersionInformation.ps1 @@ -119,26 +119,34 @@ function Get-ExchangeBuildVersionInformation { #Latest Version AD Settings $schemaValue = 17003 $mesoValue = 13243 - $orgValue = 16762 + $orgValue = 16763 switch ($exchangeVersion) { - { $_ -ge (GetBuildVersion $ex19 "CU14") } { + { $_ -ge (GetBuildVersion $ex19 "CU15") } { + $cuLevel = "CU15" + $cuReleaseDate = "02/10/2025" + $supportedBuildNumber = $true + $latestSUBuild = $true + } + { $_ -lt (GetBuildVersion $ex19 "CU15") } { $cuLevel = "CU14" $cuReleaseDate = "02/13/2024" $supportedBuildNumber = $true + $orgValue = 16762 } (GetBuildVersion $ex19 "CU14" -SU "Nov24SUv2") { $latestSUBuild = $true } { $_ -lt (GetBuildVersion $ex19 "CU14") } { $cuLevel = "CU13" $cuReleaseDate = "05/03/2023" - $supportedBuildNumber = $true + $supportedBuildNumber = $false $orgValue = 16761 } + # Technically the SU is still secure. Might need to change pester testing on this to make it okay. But it is complaining about the second SU both being on the latest. + # for now just going to leave as is as this might change with upcoming releases. (GetBuildVersion $ex19 "CU13" -SU "Nov24SUv2") { $latestSUBuild = $true } { $_ -lt (GetBuildVersion $ex19 "CU13") } { $cuLevel = "CU12" $cuReleaseDate = "04/20/2022" - $supportedBuildNumber = $false $orgValue = 16760 } { $_ -lt (GetBuildVersion $ex19 "CU12") } { @@ -822,6 +830,7 @@ function GetExchangeBuildDictionary { "Nov24SU" = "15.2.1544.13" "Nov24SUv2" = "15.2.1544.14" }) + "CU15" = (NewCUAndSUObject "15.2.1748.10") } } } From 6e5185223379c8749eb7c02811100551571e9c6e Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 20 Aug 2024 11:18:20 -0500 Subject: [PATCH 02/12] Add detection of Windows Server 2025 --- .../ServerInformation/Get-ServerOperatingSystemVersion.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-ServerOperatingSystemVersion.ps1 b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-ServerOperatingSystemVersion.ps1 index 156a8d9923..432fb5ee6d 100644 --- a/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-ServerOperatingSystemVersion.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ServerInformation/Get-ServerOperatingSystemVersion.ps1 @@ -44,6 +44,7 @@ function Get-ServerOperatingSystemVersion { "*Server 2016*" { $osReturnValue = "Windows2016" } "*Server 2019*" { $osReturnValue = "Windows2019" } "*Server 2022*" { $osReturnValue = "Windows2022" } + "*Server 2025*" { $osReturnValue = "Windows2025" } default { $osReturnValue = "Unknown" } } } From ec808556e62a95d830ed3d55cb6b888b89c12b3b Mon Sep 17 00:00:00 2001 From: David Paulson Date: Tue, 20 Aug 2024 11:45:51 -0500 Subject: [PATCH 03/12] Add support for TLS 1.3 --- .../Analyzer/Security/Invoke-AnalyzerSecuritySettings.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecuritySettings.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecuritySettings.ps1 index fde6653792..1e47024fce 100644 --- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecuritySettings.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecuritySettings.ps1 @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +. $PSScriptRoot\..\..\..\..\Shared\CompareExchangeBuildLevel.ps1 . $PSScriptRoot\..\Add-AnalyzedResultInformation.ps1 . $PSScriptRoot\..\Get-DisplayResultsGroupingKey.ps1 . $PSScriptRoot\Invoke-AnalyzerSecurityExchangeCertificates.ps1 @@ -50,6 +51,7 @@ function Invoke-AnalyzerSecuritySettings { $tlsVersions = @("1.0", "1.1", "1.2", "1.3") $tls13SupportedOS = @("Windows2012", "Windows2012R2", "Windows2016", "Windows2019") -notcontains $osInformation.BuildInformation.MajorVersion + $tls13SupportedExchange = Test-ExchangeBuildGreaterOrEqualThanBuild -CurrentExchangeBuild $HealthServerObject.ExchangeInformation.BuildInformation.VersionInformation -Version "Exchange2019" -CU "CU15" $currentNetVersion = $osInformation.TLSSettings.Registry.NET["NETv4"] $tlsSettings = $osInformation.TLSSettings.Registry.TLS @@ -79,12 +81,13 @@ function Invoke-AnalyzerSecuritySettings { # Any TLS version is Misconfigured or Half Disabled is Red # Only TLS 1.2 being Disabled is Red - # Currently TLS 1.3 being Enabled is Red + # TLS 1.3 being Enabled is Red on unsupported OS and Exchange version. + # TLS 1.3 started support with Exchange 2019 CU15 with Windows Server 2022 or newer versions # TLS 1.0 or 1.1 being Enabled is Yellow as we recommend to disable this weak protocol versions if (($currentTlsVersion.TLSConfiguration -eq "Misconfigured" -or $currentTlsVersion.TLSConfiguration -eq "Half Disabled") -or ($tlsKey -eq "1.2" -and $currentTlsVersion.TLSConfiguration -eq "Disabled") -or - ($tlsKey -eq "1.3" -and $currentTlsVersion.TLSConfiguration -eq "Enabled")) { + ($tlsKey -eq "1.3" -and $currentTlsVersion.TLSConfiguration -eq "Enabled" -and (-not $tls13SupportedOS -or -not $tls13SupportedExchange))) { $displayWriteType = "Red" } elseif ($currentTlsVersion.TLSConfiguration -eq "Enabled" -and ($tlsKey -eq "1.1" -or $tlsKey -eq "1.0")) { From 5430161ddb9a33320d7727e6aa319487cf26ef6e Mon Sep 17 00:00:00 2001 From: David Paulson Date: Mon, 16 Dec 2024 16:11:26 -0600 Subject: [PATCH 04/12] Address Cve-2023-36434 for Windows Server 2025+ --- .../Security/Invoke-AnalyzerSecurityCve-2023-36434.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2023-36434.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2023-36434.ps1 index 9618e2bbf6..7c3ab9d24a 100644 --- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2023-36434.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityCve-2023-36434.ps1 @@ -24,12 +24,15 @@ function Invoke-AnalyzerSecurityCve-2023-36434 { begin { Write-Verbose "Calling: $($MyInvocation.MyCommand)" + # Because we don't have the revision number here, we don't want to provide the 4th value otherwise it will not work correctly + $notWindows2025OrGreater = $SecurityObject.OsInformation.BuildInformation.BuildVersion -lt [System.Version]"10.0.26100" $tokenCacheModuleVersionInformation = $SecurityObject.ExchangeInformation.IISSettings.IISTokenCacheModuleInformation $tokenCacheFixedVersionNumber = $null $tokenCacheVersionGreaterOrEqual = $false } process { - if ($SecurityObject.IsEdgeServer -eq $false) { + if ($SecurityObject.IsEdgeServer -eq $false -and + $notWindows2025OrGreater) { Write-Verbose "Testing CVE: CVE-2023-21709 / CVE-2023-36434" if ($SecurityObject.ExchangeInformation.IISSettings.IISModulesInformation.ModuleList.Name -contains "TokenCacheModule") { @@ -68,6 +71,8 @@ function Invoke-AnalyzerSecurityCve-2023-36434 { Add-AnalyzedResultInformation @params } } + } elseif ( -not $notWindows2025OrGreater) { + Write-Verbose "Windows Server 2025 or greater is not affected by this vulnerability" } else { Write-Verbose "Edge Server Role is not affected by this vulnerability as it has no IIS installed" } From 8573fa6b23a20edc1baa436279ffe9d26a869a8d Mon Sep 17 00:00:00 2001 From: David Paulson Date: Mon, 6 Jan 2025 11:30:09 -0600 Subject: [PATCH 05/12] Address multiple errors thrown in Get-ClusterNode Need to use Invoke-ErrorCatchActionLoopFromIndex to catch multiple errors that can occur within it. --- .../ExchangeInformation/Get-ExchangeServerMaintenanceState.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeServerMaintenanceState.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeServerMaintenanceState.ps1 index b81236a293..8dbbca9124 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeServerMaintenanceState.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeServerMaintenanceState.ps1 @@ -20,10 +20,11 @@ function Get-ExchangeServerMaintenanceState { $getServerComponentState = Get-ServerComponentState -Identity $Server -ErrorAction SilentlyContinue try { + $errorCount = $Error.Count $getClusterNode = Get-ClusterNode -Name $Server -ErrorAction Stop } catch { Write-Verbose "Failed to run Get-ClusterNode" - Invoke-CatchActions + Invoke-ErrorCatchActionLoopFromIndex $errorCount } Write-Verbose "Running ServerComponentStates checks" From fbb1b7299c559044f16890d9318797f0e675e071 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 22 Aug 2024 15:57:51 -0500 Subject: [PATCH 06/12] Update to Configure FIP FS for CU15 and DocParser --- .../Invoke-TextExtractionOverride.ps1 | 194 ++++++++++++------ .../Invoke-XmlConfigurationRemoteAction.ps1 | 36 +++- 2 files changed, 165 insertions(+), 65 deletions(-) diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 index f57e98403e..41a257b1ff 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 @@ -86,9 +86,35 @@ function Invoke-TextExtractionOverride { $path = (Join-Path $fipFsDatabasePath "Configuration.xml") Write-Verbose "Using the database path of '$path' to adjust" + # Need to now detect the version of Exchange for different logic added with CU15 + try { + $owaVersionParams = @{ + MachineName = $env:COMPUTERNAME + SubKey = "SOFTWARE\Microsoft\ExchangeServer\v15\Setup" + GetValue = "OwaVersion" + } + [System.Version]$owaVersion = Get-RemoteRegistryValue @owaVersionParams + + if ($null -eq $owaVersion) { + throw "No OWA Version key found" + } + } catch { + throw "Unable to get OWA Version Key from Registry. Inner Exception: $_" + } + + $cu15OrNewer = $owaVersion -ge ([System.Version]"15.2.1748.1") + + if ($cu15OrNewer) { + Write-Verbose "We are on CU15 or newer" + $backupFileName = "TextExtractionOverrideV2" + } else { + Write-Verbose "We are on a supported version, but less than CU15" + $backupFileName = "TextExtractionOverride" + } + $xmlConfigurationRemoteAction = [PSCustomObject]@{ FilePath = $path - BackupFileName = "TextExtractionOverride" + BackupFileName = $backupFileName Actions = (New-Object System.Collections.Generic.List[object]) } @@ -104,69 +130,117 @@ function Invoke-TextExtractionOverride { # If we got a true result, we stopped the service # Now create the actions list foreach ($configureActionOverride in $ArgumentList.ConfigureOverride) { - if ($configureActionOverride -eq "OutsideInModule") { - # If configureActionOverride is OutsideInModule then we are setting that path only. - $actionOperation = [PSCustomObject]@{ - SelectNodesFilter = $outsideInOnlyModuleXPathFilter - OperationType = [string]::Empty - Operation = [PSCustomObject]@{ - AttributeName = "#text" - Value = "|NO" - ReplaceValue = [string]::Empty + + if ($cu15OrNewer) { + if ($configureActionOverride -eq "OutsideInModule") { + Write-Host "OutsideInModule is no longer required with the version of Exchange that you are on." + } elseif (@("AutoCad", "Jpeg", "Tiff") -contains $configureActionOverride) { + $baseFilter = $typeListBaseXPathFilter -f "OutsideInOnly" + + if ($ArgumentList.Action -eq "Allow") { + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "AppendChildFromClone" + Operation = [PSCustomObject]@{ + AttributeName = "Name" + Value = $configureActionOverride + SelectSingleNodeFilterForClone = (($typeListBaseXPathFilter -f "PreferDocParser") + "/*[local-name()='Type']") + } + })) + } elseif ($ArgumentList.Action -eq "Block") { + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = ($baseFilter + "/*[local-name()='Type'][@Name='$configureActionOverride']") + OperationType = "RemoveNode" + })) } - } + } else { + $baseFilter = $getTypeBaseTypeListXPathFilter -f $configureActionOverride - if ($ArgumentList.Action -eq "Allow") { - $actionOperation.OperationType = "AppendAttribute" - $xmlConfigurationRemoteAction.Actions.Add($actionOperation) - } elseif ($ArgumentList.Action -eq "Block") { - $actionOperation.OperationType = "ReplaceAttributeValue" - $xmlConfigurationRemoteAction.Actions.Add($actionOperation) + if ($ArgumentList.Action -eq "Allow") { + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "MoveNode" + Operation = [PSCustomObject]@{ + MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f $defaultTypeLocations[$configureActionOverride]) + ParentNodeAttributeNameFilterAdd = "Name" + } + })) + } elseif ($ArgumentList.Action -eq "Block") { + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "MoveNode" + Operation = [PSCustomObject]@{ + MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f "PreferDocParser") + ParentNodeAttributeNameFilterAdd = "Name" + } + })) + } } } else { - # Now everything else is attempting to do the following on the Type: - # Either set or remove the |NO flag - # Move the Type to the TypeList OutsideInOnly as that is the only location where the |NO flag is honored - $baseFilter = $getTypeBaseTypeListXPathFilter -f $configureActionOverride - - if ($ArgumentList.Action -eq "Allow") { - - $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ - SelectNodesFilter = $baseFilter - OperationType = "MoveNode" - Operation = [PSCustomObject]@{ - MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f "OutsideInOnly") - ParentNodeAttributeNameFilterAdd = "Name" - } - })) - - $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ - SelectNodesFilter = $baseFilter - OperationType = "AppendAttribute" - Operation = [PSCustomObject]@{ - AttributeName = "Name" - Value = "|NO" - } - })) - } elseif ($ArgumentList.Action -eq "Block") { - $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ - SelectNodesFilter = $baseFilter - OperationType = "ReplaceAttributeValue" - Operation = [PSCustomObject]@{ - AttributeName = "Name" - Value = "|NO" - ReplaceValue = [string]::Empty - } - })) - - $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ - SelectNodesFilter = $baseFilter - OperationType = "MoveNode" - Operation = [PSCustomObject]@{ - MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f $defaultTypeLocations[$configureActionOverride]) - ParentNodeAttributeNameFilterAdd = "Name" - } - })) + if ($configureActionOverride -eq "OutsideInModule") { + # If configureActionOverride is OutsideInModule then we are setting that path only. + $actionOperation = [PSCustomObject]@{ + SelectNodesFilter = $outsideInOnlyModuleXPathFilter + OperationType = [string]::Empty + Operation = [PSCustomObject]@{ + AttributeName = "#text" + Value = "|NO" + ReplaceValue = [string]::Empty + } + } + + if ($ArgumentList.Action -eq "Allow") { + $actionOperation.OperationType = "AppendAttribute" + $xmlConfigurationRemoteAction.Actions.Add($actionOperation) + } elseif ($ArgumentList.Action -eq "Block") { + $actionOperation.OperationType = "ReplaceAttributeValue" + $xmlConfigurationRemoteAction.Actions.Add($actionOperation) + } + } else { + # Now everything else is attempting to do the following on the Type: + # Either set or remove the |NO flag + # Move the Type to the TypeList OutsideInOnly as that is the only location where the |NO flag is honored + $baseFilter = $getTypeBaseTypeListXPathFilter -f $configureActionOverride + + if ($ArgumentList.Action -eq "Allow") { + + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "MoveNode" + Operation = [PSCustomObject]@{ + MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f "OutsideInOnly") + ParentNodeAttributeNameFilterAdd = "Name" + } + })) + + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "AppendAttribute" + Operation = [PSCustomObject]@{ + AttributeName = "Name" + Value = "|NO" + } + })) + } elseif ($ArgumentList.Action -eq "Block") { + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "ReplaceAttributeValue" + Operation = [PSCustomObject]@{ + AttributeName = "Name" + Value = "|NO" + ReplaceValue = [string]::Empty + } + })) + + $xmlConfigurationRemoteAction.Actions.Add(([PSCustomObject]@{ + SelectNodesFilter = $baseFilter + OperationType = "MoveNode" + Operation = [PSCustomObject]@{ + MoveToSelectNodesFilter = ($typeListBaseXPathFilter -f $defaultTypeLocations[$configureActionOverride]) + ParentNodeAttributeNameFilterAdd = "Name" + } + })) + } } } } diff --git a/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1 b/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1 index bf10ed817a..78607d22f3 100644 --- a/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1 +++ b/Security/src/Shared/Invoke-XmlConfigurationRemoteAction.ps1 @@ -19,7 +19,7 @@ TODO List [string]FilePath [object[]]Actions [string]SelectNodesFilter - [string]OperationType AcceptedValues: RemoveNode, SetAttribute, AppendAttribute, MoveNode, ReplaceAttributeValue + [string]OperationType AcceptedValues: RemoveNode, SetAttribute, AppendAttribute, MoveNode, ReplaceAttributeValue, AppendChildFromClone [object]Operation Type = SetAttribute [string]AttributeName @@ -36,6 +36,10 @@ TODO List # This is only required if the SelectNodesFilter doesn't contain a narrow filtered request where only 1 node is returned. [string]ParentNodeAttributeNameFilterAdd + Type = AppendChildFromClone + [string]AttributeName + [string]Value + [string]SelectSingleNodeFilterForClone # SelectNodesFilter is where we are AppendChild at, this is where we are cloning from. [string]BackupFileName [object]Restore [string]FileName @@ -114,7 +118,7 @@ function Invoke-XmlConfigurationRemoteAction { [int]Id [object]Content [object[]]Actions - [string]RestoreType AcceptedValues: AppendChild, SetAttribute, MoveNode + [string]RestoreType AcceptedValues: AppendChild, SetAttribute, MoveNode, RemoveNode [string]SelectNodesFilter This should always be the location where we want to handle actions in the main configuration file. [object]Operation Type = AppendChild @@ -143,7 +147,8 @@ function Invoke-XmlConfigurationRemoteAction { $action.OperationType -ne "SetAttribute" -and $action.OperationType -ne "AppendAttribute" -and $action.OperationType -ne "ReplaceAttributeValue" -and - $action.OperationType -ne "MoveNode")) { + $action.OperationType -ne "MoveNode" -and + $action.OperationType -ne "AppendChildFromClone")) { throw "Failed to provide valid action OperationType." } @@ -157,7 +162,11 @@ function Invoke-XmlConfigurationRemoteAction { [string]::IsNullOrEmpty($action.Operation.Value) -or $null -eq $action.Operation.ReplaceValue)) -or ($action.OperationType -eq "MoveNode" -and - ([string]::IsNullOrEmpty($action.Operation.MoveToSelectNodesFilter)))) { + ([string]::IsNullOrEmpty($action.Operation.MoveToSelectNodesFilter))) -or + ($action.OperationType -eq "AppendChildFromClone" -and + ([string]::IsNullOrEmpty($action.Operation.AttributeName) -or + [string]::IsNullOrEmpty($action.Operation.Value) -or + [string]::IsNullOrEmpty($action.Operation.SelectSingleNodeFilterForClone)))) { throw "Failed to provide correct Operation values for OperationType '$($action.OperationType)'" } } catch { @@ -289,6 +298,8 @@ function Invoke-XmlConfigurationRemoteAction { [void]$selectNode.ParentNode.RemoveChild($selectNode) [void]$moveToNodeLocation.AppendChild($selectNode) + } elseif ($action.RestoreType -eq "RemoveNode") { + [void]$selectNode.ParentNode.RemoveChild($selectNode) } } catch { $allActionsPerformed = $false @@ -330,7 +341,9 @@ function Invoke-XmlConfigurationRemoteAction { Write-Verbose "Trying to find SelectNodes based off filter: '$($action.SelectNodesFilter)'" $selectNodes = $contentXml.SelectNodes($action.SelectNodesFilter) - if ($selectNodes.Count -eq 0) { + if ($selectNodes.Count -eq 0 -and $action.OperationType -eq "AppendChildFromClone") { + throw "Unable to find node where we want to append child to with filter '$($action.SelectNodesFilter)'. This breaks the append child process and we are unable to continue." + } elseif ($selectNodes.Count -eq 0) { # This shouldn't be treated as an error. Write-Verbose "No nodes were found with the current filter. This could be the action was already taken or doesn't exist." continue @@ -475,6 +488,19 @@ function Invoke-XmlConfigurationRemoteAction { $node.($action.Operation.AttributeName) = $action.Operation.Value Write-Verbose "Successfully reset the value to '$($action.Operation.Value)'" } + } elseif ($action.OperationType -eq "AppendChildFromClone") { + Write-Verbose "Getting clone from filter path: '$($action.Operation.SelectSingleNodeFilterForClone)'" + $cloneNode = $contentXml.SelectSingleNode($action.Operation.SelectSingleNodeFilterForClone).CloneNode($true) + + if ($null -eq $cloneNode.($action.Operation.AttributeName)) { + throw "Attribute '$($action.Operation.AttributeName)' doesn't exist on this node" + } + + $cloneNode.($action.Operation.AttributeName) = $action.Operation.Value + $currentRestoreAction.RestoreType = "RemoveNode" + $currentRestoreAction.SelectNodesFilter = "$($action.SelectNodesFilter)/*[@$($action.Operation.AttributeName)='$($action.Operation.Value)']" + Write-Verbose "Setting restore SelectNodesFilter to $($currentRestoreAction.SelectNodesFilter)" + [void]$node.AppendChild($cloneNode) } # Add Current Restore to list if needed. From 7673703162ea73eae7c9a0e845143c303c8bdbc5 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 29 Aug 2024 10:33:46 -0500 Subject: [PATCH 07/12] Add IFiltersOnly types to be configured --- .../Invoke-TextExtractionOverride.ps1 | 11 +++++++++++ .../ConfigureFipFsTextExtractionOverrides.ps1 | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 index 41a257b1ff..b7ebe11671 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 @@ -57,6 +57,14 @@ function Invoke-TextExtractionOverride { "OdfSpreadsheet" = "PreferIFilters" "OdfPresentation" = "PreferIFilters" "OneNote" = "PreferIFilters" + "VsdmOfficePackage" = "IFiltersOnly" + "VsdxOfficePackage" = "IFiltersOnly" + "VssmOfficePackage" = "IFiltersOnly" + "VssxOfficePackage" = "IFiltersOnly" + "VstmOfficePackage" = "IFiltersOnly" + "VstxOfficePackage" = "IFiltersOnly" + "VisioXml" = "IFiltersOnly" + "PublisherStorage" = "IFiltersOnly" "Pdf" = "PreferOutsideIn" "Html" = "PreferOutsideIn" "AutoCad" = "OutsideInOnly" @@ -64,6 +72,7 @@ function Invoke-TextExtractionOverride { "Tiff" = "OutsideInOnly" } + $cu15OnlyTypeList = @("VsdmOfficePackage", "VsdxOfficePackage", "VssmOfficePackage", "VssxOfficePackage", "VstmOfficePackage", "VstxOfficePackage", "VisioXml", "PublisherStorage") $baseXPathFilter = "//*[local-name()='Configuration']/*[local-name()='System']/*[local-name()='TextExtractionSettings']" $outsideInOnlyModuleXPathFilter = $baseXPathFilter + "/*[local-name()='ModuleLists']/*[local-name()='ModuleList'][@TypeList='OutsideInOnly']/*[local-name()='Module'][contains(., 'OutsideInModule.dll')]" @@ -196,6 +205,8 @@ function Invoke-TextExtractionOverride { $actionOperation.OperationType = "ReplaceAttributeValue" $xmlConfigurationRemoteAction.Actions.Add($actionOperation) } + } elseif ($cu15OnlyTypeList -contains $configureActionOverride) { + Write-Host "The configuration action of '$configureActionOverride' is not supported with this version of Exchange." } else { # Now everything else is attempting to do the following on the Type: # Either set or remove the |NO flag diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 index 5a3e3c019d..215a6b3326 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 @@ -66,7 +66,8 @@ param( [Parameter(Mandatory = $true, ParameterSetName = "ConfigureOverride")] [ValidateSet("OutsideInModule", "XlsbOfficePackage", "XlsmOfficePackage", "XlsxOfficePackage", "ExcelStorage" , "DocmOfficePackage", "DocxOfficePackage", "PptmOfficePackage", "PptxOfficePackage", "WordStorage", "PowerPointStorage", "VisioStorage", "Rtf", - "Xml", "OdfTextDocument", "OdfSpreadsheet", "OdfPresentation", "OneNote", "Pdf", "Html", "AutoCad", "Jpeg", "Tiff", IgnoreCase = $false)] + "Xml", "OdfTextDocument", "OdfSpreadsheet", "OdfPresentation", "OneNote", "VsdmOfficePackage", "VsdxOfficePackage", "VssmOfficePackage", + "VssxOfficePackage", "VstmOfficePackage", "VstxOfficePackage", "VisioXml", "PublisherStorage", "Pdf", "Html", "AutoCad", "Jpeg", "Tiff", IgnoreCase = $false)] [string[]]$ConfigureOverride, [Parameter(Mandatory = $false, ParameterSetName = "ConfigureOverride")] From 3fa246693bcc18e01e1399e7776f12138f583c77 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 29 Aug 2024 10:45:42 -0500 Subject: [PATCH 08/12] Improve filter for clone to avoid issues --- .../ConfigurationAction/Invoke-TextExtractionOverride.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 index b7ebe11671..23a00092ee 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 @@ -153,7 +153,7 @@ function Invoke-TextExtractionOverride { Operation = [PSCustomObject]@{ AttributeName = "Name" Value = $configureActionOverride - SelectSingleNodeFilterForClone = (($typeListBaseXPathFilter -f "PreferDocParser") + "/*[local-name()='Type']") + SelectSingleNodeFilterForClone = ($baseXPathFilter + "/*[local-name()='TypeLists']/*[local-name()='TypeList']/*[local-name()='Type']") } })) } elseif ($ArgumentList.Action -eq "Block") { From a38505aefa47f36508b5c4496eb84037fb555d76 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 29 Aug 2024 11:14:35 -0500 Subject: [PATCH 09/12] Updated ConfigureFipFsTextExtractionOverrides doc --- docs/Security/ConfigureFipFsTextExtractionOverrides.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/Security/ConfigureFipFsTextExtractionOverrides.md b/docs/Security/ConfigureFipFsTextExtractionOverrides.md index f8fa71ed7c..6fd668584c 100644 --- a/docs/Security/ConfigureFipFsTextExtractionOverrides.md +++ b/docs/Security/ConfigureFipFsTextExtractionOverrides.md @@ -21,6 +21,10 @@ Details about the security vulnerability can be found in the [MSRC security advi Microsoft strongly recommends not overriding the default behavior that was introduced with the March 2024 security update if there are no functional issues that affect your organization's mail flow. +!!! warning "Warning" + + If you are running this script against an Exchange 2019 CU15 server or newer, make sure you are using the latest version of the script as there has been changes done to the script to account for what is supported with this version of Exchange. + ## Requirements This script **must** be run as Administrator in `Exchange Management Shell (EMS)`. The user must be a member of the `Organization Management` role group. @@ -65,8 +69,8 @@ Parameter | Description ----------|------------ ExchangeServerNames | A list of Exchange servers that you want to run the script against. SkipExchangeServerNames | A list of Exchange servers that you don't want to execute the configuration action. -ConfigureOverride | A list of file types that should be allowed to be processed by the `OutsideInModule`. The following input can be used: `XlsbOfficePackage`, `XlsmOfficePackage`, `XlsxOfficePackage`, `ExcelStorage`, `DocmOfficePackage`, `DocxOfficePackage`, `PptmOfficePackage`, `PptxOfficePackage`, `WordStorage`, `PowerPointStorage`, `VisioStorage`, `Rtf`, `Xml`, `OdfTextDocument`, `OdfSpreadsheet`, `OdfPresentation`, `OneNote`, `Pdf`, `Html`, `AutoCad`, `Jpeg`, `Tiff`.

If you want to enable the previous version of the `OutsideInModule` (`8.5.3`) to process file types, you must specify `OutsideInModule` as file type. Note that the `OutsideInModule` value cannot be used together with other file type values.

The input is case-sensitive. -Action | String parameter to define the action that should be performed. Input can be `Allow` or `Block`. The default value is: `Block` +ConfigureOverride | A list of file types that should be allowed to be processed by the `OutsideInModule`. The following input can be used: `XlsbOfficePackage`, `XlsmOfficePackage`, `XlsxOfficePackage`, `ExcelStorage`, `DocmOfficePackage`, `DocxOfficePackage`, `PptmOfficePackage`, `PptxOfficePackage`, `WordStorage`, `PowerPointStorage`, `VisioStorage`, `Rtf`, `Xml`, `OdfTextDocument`, `OdfSpreadsheet`, `OdfPresentation`, `OneNote`, `Pdf`, `Html`, `AutoCad`, `Jpeg`, `Tiff`.

If you want to enable the previous version of the `OutsideInModule` (`8.5.3`) to process file types, you must specify `OutsideInModule` as file type. Note that the `OutsideInModule` value cannot be used together with other file type values.

If on Exchange 2019 CU15, we now allow `VsdmOfficePackage`, `VsdxOfficePackage`, `VssmOfficePackage`, `VssxOfficePackage`, `VstmOfficePackage`, `VstxOfficePackage`, `VisioXml`, `PublisherStorage` to be used to be able to move back to the `IFiltersOnly` type if required. Another change with CU15 support, is that we no longer support changing the `OutsideInModule` as we are now using `DocParser` as the new method.

The input is case-sensitive. +Action | String parameter to define the action that should be performed. Input can be `Allow` or `Block`. The default value is: `Block`

With Exchange 2019 CU15, `Allow` will move the Type provided to the original location prior to CU15. The `Block` action will move the Type back to `PreferDocParser` if applicable. Rollback | Switch parameter to restore the `configuration.xml` that was backed-up during a previous run of the script. ScriptUpdateOnly | Switch parameter to only update the script without performing any other actions. SkipVersionCheck | Switch parameter to skip the automatic version check and script update. From 321d1baa364c4e546897092feb4500b9031ccc50 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Mon, 28 Oct 2024 16:33:15 -0500 Subject: [PATCH 10/12] Update display results --- .../ConfigurationAction/Invoke-TextExtractionOverride.ps1 | 2 +- .../ConfigureFipFsTextExtractionOverrides.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 index 23a00092ee..8cef5909e6 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigurationAction/Invoke-TextExtractionOverride.ps1 @@ -142,7 +142,7 @@ function Invoke-TextExtractionOverride { if ($cu15OrNewer) { if ($configureActionOverride -eq "OutsideInModule") { - Write-Host "OutsideInModule is no longer required with the version of Exchange that you are on." + Write-Warning "OutsideInModule is no longer required with the version of Exchange that you are on." } elseif (@("AutoCad", "Jpeg", "Tiff") -contains $configureActionOverride) { $baseFilter = $typeListBaseXPathFilter -f "OutsideInOnly" diff --git a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 index 215a6b3326..e3f93a71b6 100644 --- a/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 +++ b/Security/src/ConfigureFipFsTextExtractionOverrides/ConfigureFipFsTextExtractionOverrides.ps1 @@ -131,7 +131,7 @@ begin { $Action -eq "Allow") { $params = @{ Message = "Display warning about OutsideInModule override operation" - Target = "This operation enables an outdate version of the OutsideInModule which is known to be vulnerable." + + Target = "This operation might enable an outdate version of the OutsideInModule which is known to be vulnerable." + "`r`n$exchangeServicesWording" + "`r`n$vulnerabilityMoreInformationWording" + "`r`nDo you want to proceed?" @@ -141,7 +141,7 @@ begin { $Action -eq "Allow") { $params = @{ Message = "Display warning about file type override operation" - Target = "This operation enables OutsideInModule usage for the following file types:" + + Target = "This operation might enable OutsideInModule usage for the following file types:" + "`r`n$([string]::Join(", ", $ConfigureOverride))" + "`r`n$exchangeServicesWording" + "`r`n$vulnerabilityMoreInformationWording" + From f8914c53c47c1f61aa1b5e91e88f5d4949d23e41 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 30 Oct 2024 15:26:57 -0500 Subject: [PATCH 11/12] Include CU15 Feature Flighting --- .../Invoke-AnalyzerExchangeInformation.ps1 | 79 +++++++++++++++++++ .../Get-ExchangeInformation.ps1 | 42 +++++++--- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index b5cd8348de..384eb97a6d 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -4,6 +4,7 @@ . $PSScriptRoot\Add-AnalyzedResultInformation.ps1 . $PSScriptRoot\Get-DisplayResultsGroupingKey.ps1 . $PSScriptRoot\Invoke-AnalyzerKnownBuildIssues.ps1 +. $PSScriptRoot\..\..\..\Shared\CompareExchangeBuildLevel.ps1 function Invoke-AnalyzerExchangeInformation { [CmdletBinding()] param( @@ -581,6 +582,84 @@ function Invoke-AnalyzerExchangeInformation { } } + if ((Test-ExchangeBuildGreaterOrEqualThanBuild -CurrentExchangeBuild $exchangeInformation.BuildInformation.VersionInformation -Version "Exchange2019" -CU "CU15") -and + $exchangeInformation.GetExchangeServer.IsEdgeServer -eq $false) { + # This feature only needs to be displayed if we are on Exchange 2019 CU15+ + if ($null -eq $exchangeInformation.GetExchangeServer.RingLevel) { + $params = $baseParams + @{ + Name = "Feature Flighting" + Details = "Unknown - No data on Get-ExchangeServer related to this feature. Likely due to connecting to an Exchange Server for shell not on supported build." + DisplayWriteType = "Yellow" + } + Add-AnalyzedResultInformation @params + } else { + Add-AnalyzedResultInformation @baseParams -Name "Feature Flighting" + + $getExchangeServer = $exchangeInformation.GetExchangeServer + $flightingBaseParams = $baseParams + @{ DisplayCustomTabNumber = 2 } + $params = $flightingBaseParams + @{ + Name = "Ring Level" + Details = $getExchangeServer.RingLevel + } + Add-AnalyzedResultInformation @params + + $endpointDisplayWriteType = "Grey" + $endpointDetails = "200 - Reachable" + if ($exchangeInformation.ExchangeFeatureFlightingServiceResult.StatusCode -ne 200) { + $endpointDisplayWriteType = "Yellow" + $endpointDetails = "Unreachable - More Information: https://aka.ms/HC-ExchangeServerFeatureFlighting" + } + $params = $flightingBaseParams + @{ + Name = "Endpoint Service Status" + Details = $endpointDetails + DisplayWriteType = $endpointDisplayWriteType + } + Add-AnalyzedResultInformation @params + + $params = $flightingBaseParams + @{ + Name = "Last Service Run Time" + Details = $getExchangeServer.LastFlightingServiceRunTime + } + Add-AnalyzedResultInformation @params + + $params = $flightingBaseParams + @{ + Name = "Features Enabled" + Details = ([string]::Join(", ", $getExchangeServer.FeaturesEnabled)) + } + Add-AnalyzedResultInformation @params + + # The rest of the settings, only display if we have something there. + if ($getExchangeServer.FeaturesApproved.Count -gt 0) { + $params = $flightingBaseParams + @{ + Name = "Features Approved" + Details = ([string]::Join(", ", $getExchangeServer.FeaturesApproved)) + } + Add-AnalyzedResultInformation @params + } + if ($getExchangeServer.FeaturesAwaitingAdminApproval.Count -gt 0) { + $params = $flightingBaseParams + @{ + Name = "Features Awaiting Admin Approval" + Details = ([string]::Join(", ", $getExchangeServer.FeaturesAwaitingAdminApproval)) + } + Add-AnalyzedResultInformation @params + } + if ($getExchangeServer.FeaturesBlocked.Count -gt 0) { + $params = $flightingBaseParams + @{ + Name = "Features Blocked" + Details = ([string]::Join(", ", $getExchangeServer.FeaturesBlocked)) + } + Add-AnalyzedResultInformation @params + } + if ($getExchangeServer.FeaturesDisabled.Count -gt 0) { + $params = $flightingBaseParams + @{ + Name = "Features Disabled" + Details = ([string]::Join(", ", $getExchangeServer.FeaturesDisabled)) + } + Add-AnalyzedResultInformation @params + } + } + } + if ($null -ne $exchangeInformation.SettingOverrides) { $overridesDetected = $null -ne $exchangeInformation.SettingOverrides.SettingOverrides diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 index 848015de9b..084ee79438 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 @@ -180,23 +180,38 @@ function Get-ExchangeInformation { $FIPFSUpdateIssue = Get-FIPFSScanEngineVersionState @fipFsParams - $eemsEndpointParams = @{ + $endpointScriptBlock = { + param($url, $proxy) + + if ($null -eq $url) { + throw "NULL URL provided for endpoint script block" + } + Write-Verbose "Going to try to get the endpoint information for: $url" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + if ($null -ne $proxy) { + Write-Verbose "Proxy Server detected. Going to use: $proxy" + [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($proxy) + [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials + [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $true + } elseif ($null -ne [System.Net.WebRequest]::DefaultWebProxy.Address) { + Write-Verbose "No Exchange proxy provided, but one is set on the PowerShell session. Going to remove it." + [System.Net.WebRequest]::DefaultWebProxy = $null + } + Invoke-WebRequest -Method Get -Uri $url -UseBasicParsing + } + + $scriptBlockEndpointParams = @{ ComputerName = $Server ScriptBlockDescription = "Test EEMS pattern service connectivity" CatchActionFunction = ${Function:Invoke-CatchActions} - ArgumentList = $getExchangeServer.InternetWebProxy - ScriptBlock = { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - if ($null -ne $args[0]) { - Write-Verbose "Proxy Server detected. Going to use: $($args[0])" - [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($args[0]) - [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials - [System.Net.WebRequest]::DefaultWebProxy.BypassProxyOnLocal = $true - } - Invoke-WebRequest -Method Get -Uri "https://officeclient.microsoft.com/GetExchangeMitigations" -UseBasicParsing - } + ArgumentList = @("https://officeclient.microsoft.com/GetExchangeMitigations", $getExchangeServer.InternetWebProxy) + ScriptBlock = $endpointScriptBlock } - $eemsEndpointResults = Invoke-ScriptBlockHandler @eemsEndpointParams + $eemsEndpointResults = Invoke-ScriptBlockHandler @scriptBlockEndpointParams + + $scriptBlockEndpointParams.ScriptBlockDescription = "Test Feature Flighting service connectivity" + $scriptBlockEndpointParams.ArgumentList[0] = "https://officeclient.microsoft.com/GetExchangeConfig" + $featureFlightingEndpointResults = Invoke-ScriptBlockHandler @scriptBlockEndpointParams Write-Verbose "Checking AES256-CBC information protection readiness and configuration" $aes256CbcParams = @{ @@ -321,6 +336,7 @@ function Get-ExchangeInformation { ServerMaintenance = $serverMaintenance ExchangeCertificates = [array]$exchangeCertificates ExchangeEmergencyMitigationServiceResult = $eemsEndpointResults + ExchangeFeatureFlightingServiceResult = $featureFlightingEndpointResults EdgeTransportResourceThrottling = $edgeTransportResourceThrottling # If we want to checkout other diagnosticInfo, we should create a new object here. ApplicationConfigFileStatus = $applicationConfigFileStatus DependentServices = $dependentServices From 217b8643993e08efe61d05396e6a1156598ad7d5 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Mon, 23 Dec 2024 11:59:10 -0600 Subject: [PATCH 12/12] Fixed a bug found by customer --- .../Analyzer/Invoke-AnalyzerExchangeInformation.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index 384eb97a6d..6c75a1ab0d 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -622,9 +622,14 @@ function Invoke-AnalyzerExchangeInformation { } Add-AnalyzedResultInformation @params + if ($getExchangeServer.FeaturesEnabled.Count -gt 0) { + $details = ([string]::Join(", ", $getExchangeServer.FeaturesEnabled)) + } else { + $details = "None Enabled" + } $params = $flightingBaseParams + @{ Name = "Features Enabled" - Details = ([string]::Join(", ", $getExchangeServer.FeaturesEnabled)) + Details = $details } Add-AnalyzedResultInformation @params