diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 index b1162a0225..aa5b84f5ec 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerExchangeInformation.ps1 @@ -118,6 +118,17 @@ function Invoke-AnalyzerExchangeInformation { } } + # If the ExSetup wasn't found, we need to report that. + if ($exchangeInformation.BuildInformation.ExchangeSetup.FailedGetExSetup -eq $true) { + $params = $baseParams + @{ + Name = "Warning" + Details = "Didn't detect ExSetup.exe on the server. This might mean that setup didn't complete correctly the last time it was run." + DisplayCustomTabNumber = 2 + DisplayWriteType = "Yellow" + } + Add-AnalyzedResultInformation @params + } + if ($null -ne $exchangeInformation.BuildInformation.KBsInstalled) { Add-AnalyzedResultInformation -Name "Exchange IU or Security Hotfix Detected" @baseParams $problemKbFound = $false diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 index 6a41fd0056..0267ca064e 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerFrequentConfigurationIssues.ps1 @@ -146,6 +146,7 @@ function Invoke-AnalyzerFrequentConfigurationIssues { foreach ($configKey in $keyList) { $configStatus = $exchangeInformation.ApplicationConfigFileStatus[$configKey] + $fileName = $configStatus.FileName $writeType = "Green" [string]$writeValue = $configStatus.Present @@ -155,12 +156,12 @@ function Invoke-AnalyzerFrequentConfigurationIssues { } $params = $baseParams + @{ - Name = "$configKey Present" + Name = "$fileName Present" Details = $writeValue DisplayWriteType = $writeType } - if ($alwaysDisplayConfigs -contains $configKey -or + if ($alwaysDisplayConfigs -contains $fileName -or -not $configStatus.Present) { Add-AnalyzedResultInformation @params } @@ -171,7 +172,7 @@ function Invoke-AnalyzerFrequentConfigurationIssues { $content = [xml]($configStatus.Content) # Additional checks of configuration files. - if ($configKey -eq "noderunner.exe.config") { + if ($fileName -eq "noderunner.exe.config") { $memoryLimitMegabytes = $content.configuration.nodeRunnerSettings.memoryLimitMegabytes $writeValue = "$memoryLimitMegabytes MB" $writeType = "Green" @@ -205,7 +206,7 @@ function Invoke-AnalyzerFrequentConfigurationIssues { } } catch { $params = $baseParams + @{ - Name = "$configKey Invalid Config Format" + Name = "$fileName Invalid Config Format" Details = "True --- Error: Not able to convert to xml which means it is in an incorrect format that will cause problems with the process." DisplayTestingValue = $true DisplayWriteType = "Red" diff --git a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 index 8dd338e0a3..92c9947bca 100644 --- a/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Invoke-AnalyzerIISInformation.ps1 @@ -105,13 +105,6 @@ function Invoke-AnalyzerIISInformation { $problemCertList.Add("'$certHash' Doesn't exist on the server and this will cause problems.") } elseif ($cert.LifetimeInDays -lt 0) { $problemCertList.Add("'$certHash' Has expired and will cause problems.") - } elseif ($_.bindingInformation -eq "*:444:") { - $namespaces = $cert.Namespaces | ForEach-Object { $_.ToString() } - - if ($namespaces -notcontains $exchangeInformation.GetExchangeServer.Fqdn -or - $namespaces -notcontains $exchangeInformation.GetExchangeServer.Name) { - $problemCertList.Add("'$certHash' Exchange Back End does not have hostname or FQDN for the namespaces. This can cause connectivity issues.") - } } } diff --git a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1 b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1 index b4f70b6d35..5b3d5bfe99 100644 --- a/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1 +++ b/Diagnostics/HealthChecker/Analyzer/Security/Invoke-AnalyzerSecurityExtendedProtectionConfigState.ps1 @@ -47,10 +47,13 @@ function Invoke-AnalyzerSecurityExtendedProtectionConfigState { # This combination means that EP is not configured and the Exchange build doesn't support it. # Recommended action: Upgrade to a supported build (Aug 2022 SU+) and enable EP afterwards. $epDetails = "Your Exchange server is at risk. Install the latest SU and enable Extended Protection" - } else { + } elseif ($extendedProtection.ExtendedProtectionConfigured) { # This means that EP is supported but not configured for at least one vDir. # Recommended action: Enable EP for each vDir on the system by using the script provided by us. - $epDetails += "Extended Protection isn't configured as expected" + $epDetails = "Extended Protection isn't configured as expected" + } else { + # No Extended Protection is configured, provide a slightly different wording to avoid confusion of possible misconfigured EP. + $epDetails = "Extended Protection is not configured" } $epCveParams = $baseParams + @{ diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1 index 58526e232d..ea82172f3f 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExSetupDetails.ps1 @@ -11,7 +11,20 @@ function Get-ExSetupDetails { Write-Verbose "Calling: $($MyInvocation.MyCommand)" $exSetupDetails = [string]::Empty function Get-ExSetupDetailsScriptBlock { - Get-Command ExSetup | ForEach-Object { $_.FileVersionInfo } + try { + Get-Command ExSetup -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + } catch { + try { + Write-Verbose "Failed to find ExSetup by environment path locations. Attempting manual lookup." + $installDirectory = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup -ErrorAction Stop).MsiInstallPath + + if ($null -ne $installDirectory) { + Get-Command ([System.IO.Path]::Combine($installDirectory, "bin\ExSetup.exe")) -ErrorAction Stop | ForEach-Object { $_.FileVersionInfo } + } + } catch { + Write-Verbose "Failed to find ExSetup, need to fallback." + } + } } $exSetupDetails = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock ${Function:Get-ExSetupDetailsScriptBlock} -ScriptBlockDescription "Getting ExSetup remotely" -CatchActionFunction ${Function:Invoke-CatchActions} diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeApplicationConfigurationFileValidation.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeApplicationConfigurationFileValidation.ps1 deleted file mode 100644 index f521d543a5..0000000000 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeApplicationConfigurationFileValidation.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1 -function Get-ExchangeApplicationConfigurationFileValidation { - param( - [Parameter(Mandatory = $true)] - [string]$ComputerName, - - [string[]]$ConfigFileLocation - ) - Write-Verbose "Calling: $($MyInvocation.MyCommand)" - $results = @{} - $ConfigFileLocation | - ForEach-Object { - - $params = @{ - ComputerName = $ComputerName - ScriptBlockDescription = "Getting Exchange Application Configuration File Validation" - ArgumentList = $_ - ScriptBlock = { - param($Location) - return [PSCustomObject]@{ - Present = ((Test-Path $Location)) - FileName = ([IO.Path]::GetFileName($Location)) - FilePath = $Location - Content = (Get-Content $Location -Raw) - } - } - } - - $obj = Invoke-ScriptBlockHandler @params - $results.Add($obj.FileName, $obj) - } - return $results -} diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 index b87ff32e25..795fc6e928 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/Get-ExchangeInformation.ps1 @@ -5,10 +5,10 @@ . $PSScriptRoot\..\..\..\..\Shared\ErrorMonitorFunctions.ps1 . $PSScriptRoot\..\..\..\..\Shared\Get-ExchangeBuildVersionInformation.ps1 . $PSScriptRoot\..\..\..\..\Shared\Get-ExchangeSettingOverride.ps1 +. $PSScriptRoot\..\..\..\..\Shared\Get-FileContentInformation.ps1 . $PSScriptRoot\IISInformation\Get-ExchangeAppPoolsInformation.ps1 . $PSScriptRoot\IISInformation\Get-ExchangeServerIISSettings.ps1 . $PSScriptRoot\Get-ExchangeAES256CBCDetails.ps1 -. $PSScriptRoot\Get-ExchangeApplicationConfigurationFileValidation.ps1 . $PSScriptRoot\Get-ExchangeConnectors.ps1 . $PSScriptRoot\Get-ExchangeDependentServices.ps1 . $PSScriptRoot\Get-ExchangeRegistryValues.ps1 @@ -36,7 +36,21 @@ function Get-ExchangeInformation { $getExchangeServer = (Get-ExchangeServer -Identity $Server -Status) $exchangeCertificates = Get-ExchangeServerCertificates -Server $Server $exSetupDetails = Get-ExSetupDetails -Server $Server - $versionInformation = (Get-ExchangeBuildVersionInformation -FileVersion ($exSetupDetails.FileVersion)) + + if ($null -eq $exSetupDetails) { + # couldn't find ExSetup.exe this should be rare so we are just going to handle this by displaying the AdminDisplayVersion from Get-ExchangeServer + $versionInformation = (Get-ExchangeBuildVersionInformation -AdminDisplayVersion $getExchangeServer.AdminDisplayVersion) + $exSetupDetails = [PSCustomObject]@{ + FileVersion = $versionInformation.BuildVersion.ToString() + FileBuildPart = $versionInformation.BuildVersion.Build + FilePrivatePart = $versionInformation.BuildVersion.Revision + FileMajorPart = $versionInformation.BuildVersion.Major + FileMinorPart = $versionInformation.BuildVersion.Minor + FailedGetExSetup = $true + } + } else { + $versionInformation = (Get-ExchangeBuildVersionInformation -FileVersion ($exSetupDetails.FileVersion)) + } $buildInformation = [PSCustomObject]@{ ServerRole = (Get-ServerRole -ExchangeServerObj $getExchangeServer) @@ -98,12 +112,12 @@ function Get-ExchangeInformation { } $configParams = @{ - ComputerName = $Server - ConfigFileLocation = @("$([System.IO.Path]::Combine($serverExchangeBinDirectory, "EdgeTransport.exe.config"))", + ComputerName = $Server + FileLocation = @("$([System.IO.Path]::Combine($serverExchangeBinDirectory, "EdgeTransport.exe.config"))", "$([System.IO.Path]::Combine($serverExchangeBinDirectory, "Search\Ceres\Runtime\1.0\noderunner.exe.config"))") } - $applicationConfigFileStatus = Get-ExchangeApplicationConfigurationFileValidation @configParams + $applicationConfigFileStatus = Get-FileContentInformation @configParams $serverMaintenance = Get-ExchangeServerMaintenanceState -Server $Server -ComponentsToSkip "ForwardSyncDaemon", "ProvisioningRps" $settingOverrides = Get-ExchangeSettingOverride -Server $Server -CatchActionFunction ${Function:Invoke-CatchActions} diff --git a/Diagnostics/HealthChecker/Features/Get-HealthCheckerData.ps1 b/Diagnostics/HealthChecker/Features/Get-HealthCheckerData.ps1 index 0209fcfb30..ad7c15cfe7 100644 --- a/Diagnostics/HealthChecker/Features/Get-HealthCheckerData.ps1 +++ b/Diagnostics/HealthChecker/Features/Get-HealthCheckerData.ps1 @@ -123,19 +123,27 @@ function Get-HealthCheckerData { Write-Progress @paramWriteProgress try { - $analyzedResults | Export-Clixml -Path $Script:OutXmlFullPath -Encoding UTF8 -Depth 2 -ErrorAction SilentlyContinue + $analyzedResults | Export-Clixml -Path $Script:OutXmlFullPath -Encoding UTF8 -Depth 2 -ErrorAction Stop + Write-Verbose "Successfully export out the data" } catch { - Write-Verbose "Failed to Export-Clixml. Converting HealthCheckerExchangeServer to json" - $jsonHealthChecker = $analyzedResults.HealthCheckerExchangeServer | ConvertTo-Json - - $testOutputXml = [PSCustomObject]@{ - HealthCheckerExchangeServer = $jsonHealthChecker | ConvertFrom-Json - HtmlServerValues = $analyzedResults.HtmlServerValues - DisplayResults = $analyzedResults.DisplayResults + try { + Write-Verbose "Failed to Export-Clixml. Inner Exception: $_" + Write-Verbose "Converting HealthCheckerExchangeServer to json." + $jsonHealthChecker = $analyzedResults.HealthCheckerExchangeServer | ConvertTo-Json -Depth 6 -ErrorAction Stop + + $testOutputXml = [PSCustomObject]@{ + HealthCheckerExchangeServer = $jsonHealthChecker | ConvertFrom-Json -ErrorAction Stop + HtmlServerValues = $analyzedResults.HtmlServerValues + DisplayResults = $analyzedResults.DisplayResults + } + + $testOutputXml | Export-Clixml -Path $Script:OutXmlFullPath -Encoding UTF8 -Depth 2 -ErrorAction Stop + Write-Verbose "Successfully export out the data after the convert" + } catch { + Write-Red "Failed to Export-Clixml. Unable to export the data." } - - $testOutputXml | Export-Clixml -Path $Script:OutXmlFullPath -Encoding UTF8 -Depth 2 -ErrorAction Stop } finally { + # This prevents the need to call Invoke-CatchActions Invoke-ErrorCatchActionLoopFromIndex $currentErrors # for now don't want to display that we output the information if ReturnDataCollectionOnly is false diff --git a/Diagnostics/HealthChecker/Features/Get-HtmlServerReport.ps1 b/Diagnostics/HealthChecker/Features/Get-HtmlServerReport.ps1 index a07b4b7052..65dc7cb523 100644 --- a/Diagnostics/HealthChecker/Features/Get-HtmlServerReport.ps1 +++ b/Diagnostics/HealthChecker/Features/Get-HtmlServerReport.ps1 @@ -100,4 +100,6 @@ function Get-HtmlServerReport { $htmlReport = $htmlHeader + $htmlOverviewTable + $htmlServerDetails + "$([System.Environment]::NewLine)" $htmlReport | Out-File $HtmlOutFilePath -Encoding UTF8 + + Write-Host "HTML Report Location: $HtmlOutFilePath" } diff --git a/Diagnostics/HealthChecker/HealthChecker.ps1 b/Diagnostics/HealthChecker/HealthChecker.ps1 index e6018399b5..bef960c863 100644 --- a/Diagnostics/HealthChecker/HealthChecker.ps1 +++ b/Diagnostics/HealthChecker/HealthChecker.ps1 @@ -124,7 +124,7 @@ param( [switch]$BuildHtmlServersReport, [Parameter(Mandatory = $false, ParameterSetName = "HTMLReport", HelpMessage = "Provide the name of the Report to be created.")] - [string]$HtmlReportFile = "ExchangeAllServersReport.html", + [string]$HtmlReportFile = "ExchangeAllServersReport-$((Get-Date).ToString("yyyyMMddHHmmss")).html", [Parameter(Mandatory = $true, ParameterSetName = "DCCoreReport", HelpMessage = "Enable the DCCoreReport feature data collection against the current server's AD Site.")] [switch]$DCCoreRatio, diff --git a/Shared/Get-FileContentInformation.ps1 b/Shared/Get-FileContentInformation.ps1 new file mode 100644 index 0000000000..26416946c0 --- /dev/null +++ b/Shared/Get-FileContentInformation.ps1 @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\Invoke-ScriptBlockHandler.ps1 +function Get-FileContentInformation { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$ComputerName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string[]]$FileLocation + ) + begin { + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + $allFiles = New-Object System.Collections.Generic.List[string] + } + process { + foreach ($file in $FileLocation) { + $allFiles.Add($file) + } + } + end { + $params = @{ + ComputerName = $ComputerName + ScriptBlockDescription = "Getting File Content Information" + ArgumentList = @(, $allFiles) + ScriptBlock = { + param($FileLocations) + $results = @{} + foreach ($fileLocation in $FileLocations) { + $obj = [PSCustomObject]@{ + Present = ((Test-Path $fileLocation)) + FileName = ([IO.Path]::GetFileName($fileLocation)) + FilePath = $fileLocation + Content = (Get-Content $fileLocation -Raw) + } + + $results.Add($fileLocation, $obj) + } + return $results + } + } + return (Invoke-ScriptBlockHandler @params) + } +} diff --git a/docs/Diagnostics/HealthChecker/index.md b/docs/Diagnostics/HealthChecker/index.md index 7c2ba2a961..6a02ee8c99 100644 --- a/docs/Diagnostics/HealthChecker/index.md +++ b/docs/Diagnostics/HealthChecker/index.md @@ -152,7 +152,7 @@ ServerList | Used in combination with the LoadBalancingReport switch for letting SiteName | Used in combination with the LoadBalancingReport switch for letting the script to know which servers to run against in the site. XMLDirectoryPath | Used in combination with BuildHtmlServersReport switch for the location of the HealthChecker XML files for servers which you want to be included in the report. Default location is the current directory. BuildHtmlServersReport | Switch to enable the script to build the HTML report for all the servers XML results in the XMLDirectoryPath location. -HtmlReportFile | Name of the HTML output file from the BuildHtmlServersReport. Default is ExchangeAllServersReport.html +HtmlReportFile | Name of the HTML output file from the BuildHtmlServersReport. Default is ExchangeAllServersReport-yyyyMMddHHmmss.html DCCoreRatio | Gathers the Exchange to DC/GC Core ratio and displays the results in the current site that the script is running in. AnalyzeDataOnly | Switch to analyze the existing HealthChecker XML files. The results are displayed on the screen and an HTML report is generated. VulnerabilityReport | Switch to collect the Vulnerability Information for all the servers in the environment and export it out to json file.