From e63b74d082bbb68ebaabf3fc6b6bd646057c3270 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 4 Jun 2026 16:06:26 -0500 Subject: [PATCH 1/3] Health Checker: Fix empty ExScripts path in mitigation service output Replace undefined $ExScripts EMS variable with the Exchange install path from registry values, so the Get-Mitigations.ps1 script path displays correctly in the report output. Fixes #2524 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Security/Invoke-AnalyzerSecurityMitigationService.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1 index 29b323d69b..7228a475ae 100644 --- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityMitigationService.ps1 @@ -129,7 +129,7 @@ function Invoke-AnalyzerSecurityMitigationService { } $params = $baseParams + @{ - Details = "Run: 'Get-Mitigations.ps1' from: '$ExScripts' to learn more." + Details = "Run: 'Get-Mitigations.ps1' from: '$([System.IO.Path]::Combine($exchangeInformation.RegistryValues.MsiInstallPath, "Scripts"))' to learn more." DisplayCustomTabNumber = 2 } Add-AnalyzedResultInformation @params From 5f1a245ab5e8d49e53c6d6d3d880641ce64cedc5 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 4 Jun 2026 16:38:52 -0500 Subject: [PATCH 2/3] Health Checker: Improve error message for orphaned SIDs in local Administrators group When Get-LocalGroupMember fails due to orphaned SIDs in the built-in Administrators group (InvalidOperationException on Server 2019/2022), display a helpful message indicating orphaned SIDs may be the cause instead of the misleading 'results were blank' message. Server 2025 handles orphaned SIDs natively so this check is only applied on older OS versions (build < 10.0.26100). Fixes #2381 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Analyzer/Invoke-AnalyzerExchangeInformation.ps1 | 8 +++++++- .../Invoke-JobExchangeInformationLocal.ps1 | 2 ++ .../Features/Get-HealthCheckerDataObject.ps1 | 7 ++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index 066a7cb4cb..b3a6d2038f 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -335,7 +335,13 @@ function Invoke-AnalyzerExchangeInformation { } } } else { - $displayMissingGroups.Add("Unable to determine Local System Membership as the results were blank.") + if ($null -ne $exchangeInformation.ADComputerObject.LocalGroupMemberException -and + $exchangeInformation.ADComputerObject.LocalGroupMemberException.Exception -is [System.InvalidOperationException] -and + $HealthServerObject.OSInformation.BuildInformation.BuildVersion -lt [System.Version]"10.0.26100") { + $displayMissingGroups.Add("Unable to determine Local System Membership. This can occur when orphaned SIDs exist in the local Administrators group.") + } else { + $displayMissingGroups.Add("Unable to determine Local System Membership as the results were blank.") + } } if ($null -ne $exchangeInformation.ADComputerObject.ADGroupMembership -and diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Invoke-JobExchangeInformationLocal.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Invoke-JobExchangeInformationLocal.ps1 index d491a90c4d..5b8ac4eabb 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Invoke-JobExchangeInformationLocal.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Invoke-JobExchangeInformationLocal.ps1 @@ -124,6 +124,7 @@ function Invoke-JobExchangeInformationLocal { $localGroupMember = Get-LocalGroupMember -SID "S-1-5-32-544" -ErrorAction Stop } catch { Write-Verbose "Failed to run Get-LocalGroupMember. Inner Exception: $_" + $localGroupMemberException = $_ Invoke-CatchActions } } @@ -210,6 +211,7 @@ function Invoke-JobExchangeInformationLocal { IanaTimeZoneMappingsRaw = $ianaTimeZoneMappingContent FileContentInformation = $fileContentInformation LocalGroupMember = $localGroupMember + LocalGroupMemberException = $localGroupMemberException RemoteJob = $true -eq $PSSenderInfo JobHandledErrors = $jobHandledErrors AllErrors = $Error diff --git a/Diagnostics/HealthChecker/Features/Get-HealthCheckerDataObject.ps1 b/Diagnostics/HealthChecker/Features/Get-HealthCheckerDataObject.ps1 index 3c0cee610c..2fd4eddc4b 100644 --- a/Diagnostics/HealthChecker/Features/Get-HealthCheckerDataObject.ps1 +++ b/Diagnostics/HealthChecker/Features/Get-HealthCheckerDataObject.ps1 @@ -127,9 +127,10 @@ function Get-HealthCheckerDataObject { VirtualDirectories = $ExchangeCmdletResult.VirtualDirectories ExchangeCertificateInformation = $exchangeCertificateInformation ADComputerObject = [PSCustomObject]@{ - ADObject = $ExchangeCmdletResult.ADObject.ComputerObject - ADGroupMembership = $ExchangeCmdletResult.ADObject.GroupMembership - LocalGroupMember = $ExchangeLocalResult.LocalGroupMember + ADObject = $ExchangeCmdletResult.ADObject.ComputerObject + ADGroupMembership = $ExchangeCmdletResult.ADObject.GroupMembership + LocalGroupMember = $ExchangeLocalResult.LocalGroupMember + LocalGroupMemberException = $ExchangeLocalResult.LocalGroupMemberException } AES256CBCInformation = $ExchangeLocalResult.AES256CBCInformation ApplicationConfigFileStatus = $ExchangeLocalResult.ApplicationConfigFileStatus From 21c894672e0b52f2111a979312a0f1bcbba5d029 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Fri, 5 Jun 2026 14:40:24 -0500 Subject: [PATCH 3/3] Clarify Visual C++ Redistributable finding and docs (#2508) Update docs and analyzer output to make it clear that Exchange specifically requires the Visual C++ 2012 and 2013 Redistributable, and that these are not replaced by newer versions. When a version is reported as outdated, the message now specifies which year's Redistributable needs updating. Fix double 'the' typo in documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 | 6 +++--- .../HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 | 4 ++-- .../HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 | 2 +- .../HealthChecker/VisualCRedistributableVersionCheck.md | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 index 83201b4925..677aeea0a9 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerOsInformation.ps1 @@ -400,7 +400,7 @@ function Invoke-AnalyzerOsInformation { $displayWriteType2012 = "Green" $displayValue2012 = "$($installed2012.DisplayVersion) Version is current" } elseif (Test-VisualCRedistributableInstalled -Year 2012 -Installed $osInformation.VcRedistributable) { - $displayValue2012 = "Redistributable ($($installed2012.DisplayVersion)) is outdated" + $displayValue2012 = "Redistributable is outdated ($($installed2012.DisplayVersion)). Update the Visual C++ 2012 version." $displayWriteType2012 = "Yellow" } @@ -408,7 +408,7 @@ function Invoke-AnalyzerOsInformation { $displayWriteType2013 = "Green" $displayValue2013 = "$($installed2013.DisplayVersion) Version is current" } elseif (Test-VisualCRedistributableInstalled -Year 2013 -Installed $osInformation.VcRedistributable) { - $displayValue2013 = "Redistributable ($($installed2013.DisplayVersion)) is outdated" + $displayValue2013 = "Redistributable is outdated ($($installed2013.DisplayVersion)). Update the Visual C++ 2013 version." $displayWriteType2013 = "Yellow" } } @@ -435,7 +435,7 @@ function Invoke-AnalyzerOsInformation { $displayWriteType2012 -eq "Yellow") { $params = $baseParams + @{ - Details = "Note: For more information about the latest C++ Redistributable please visit: https://aka.ms/HC-LatestVC`r`n`t`tThis is not a requirement to upgrade, only a notification to bring to your attention." + Details = "Note: Exchange requires the Visual C++ 2012 and 2013 Redistributable specifically. These are not replaced by newer versions.`r`n`t`tFor more information please visit: https://aka.ms/HC-LatestVC" DisplayWriteType = "Yellow" DisplayCustomTabNumber = 2 } diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 index 5cb5b5bb18..9cd842d7fd 100644 --- a/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E16.Main.Tests.ps1 @@ -58,8 +58,8 @@ Describe "Testing Health Checker by Mock Data Imports - Exchange 2016" { TestObjectMatch "Power Plan" "Balanced --- Error"-WriteType "Red" $httpProxy = GetObject "Http Proxy Setting" $httpProxy.ProxyAddress | Should -Be "None" - TestObjectMatch "Visual C++ 2012 x64" "Redistributable (11.0.50727) is outdated" -WriteType "Yellow" - TestObjectMatch "Visual C++ 2013 x64" "Redistributable (12.0.21005) is outdated" -WriteType "Yellow" + TestObjectMatch "Visual C++ 2012 x64" "Redistributable is outdated (11.0.50727). Update the Visual C++ 2012 version." -WriteType "Yellow" + TestObjectMatch "Visual C++ 2013 x64" "Redistributable is outdated (12.0.21005). Update the Visual C++ 2013 version." -WriteType "Yellow" TestObjectMatch "Server Pending Reboot" $false $pageFile = GetObject "PageFile Size 0" diff --git a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 index 4ad67cffc8..2d13fcae33 100644 --- a/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 +++ b/Diagnostics/HealthChecker/Tests/HealthChecker.E19.Main.Tests.ps1 @@ -127,7 +127,7 @@ Describe "Testing Health Checker by Mock Data Imports" { $httpProxy = GetObject "Http Proxy Setting" $httpProxy.ProxyAddress | Should -Be "None" TestObjectMatch "Visual C++ 2012 x64" "11.0.61030 Version is current" -WriteType "Green" - TestObjectMatch "Visual C++ 2013 x64" "Redistributable (12.0.21005) is outdated" -WriteType "Yellow" + TestObjectMatch "Visual C++ 2013 x64" "Redistributable is outdated (12.0.21005). Update the Visual C++ 2013 version." -WriteType "Yellow" TestObjectMatch "Server Pending Reboot" $false $pageFile = GetObject "PageFile Size 0" diff --git a/docs/Diagnostics/HealthChecker/VisualCRedistributableVersionCheck.md b/docs/Diagnostics/HealthChecker/VisualCRedistributableVersionCheck.md index 25620d875c..a1f1ff596a 100644 --- a/docs/Diagnostics/HealthChecker/VisualCRedistributableVersionCheck.md +++ b/docs/Diagnostics/HealthChecker/VisualCRedistributableVersionCheck.md @@ -2,7 +2,9 @@ **Description:** -We check if the the latest Visual C++ Redistributable version, required for the installed Exchange server role, is installed or not. +We check if the Visual C++ Redistributable versions required for the installed Exchange server role are installed and up to date. Exchange Server specifically requires the [Visual C++ 2012 (VC++ 11.0)](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2012-vc-110-update-4-no-longer-supported) and [Visual C++ 2013 (VC++ 12.0)](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2013-vc-120-no-longer-supported) Redistributable. These older versions are **not** replaced by newer Visual C++ Redistributable versions (e.g., the latest Visual C++ 2015-2022 Redistributable) and must remain installed. + +When Health Checker reports that a version is "outdated", it means the installed version of that specific year's Redistributable (2012 or 2013) needs to be updated to the latest release within that same series. **Included in HTML Report?**