#
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
#>
# Version 21.11.09.1825
<#
.NOTES
Name: HealthChecker.ps1
Requires: Exchange Management Shell and administrator rights on the target Exchange
server as well as the local machine.
Major Release History:
4/20/2021 - Initial Public Release on CSS-Exchange
11/10/2020 - Initial Public Release of version 3.
1/18/2017 - Initial Public Release of version 2.
3/30/2015 - Initial Public Release.
.SYNOPSIS
Checks the target Exchange server for various configuration recommendations from the Exchange product group.
.DESCRIPTION
This script checks the Exchange server for various configuration recommendations outlined in the
"Exchange 2013 Performance Recommendations" section on Microsoft Docs, found here:
https://docs.microsoft.com/en-us/exchange/exchange-2013-sizing-and-configuration-recommendations-exchange-2013-help
Informational items are reported in Grey. Settings found to match the recommendations are
reported in Green. Warnings are reported in yellow. Settings that can cause performance
problems are reported in red. Please note that most of these recommendations only apply to Exchange
2013/2016. The script will run against Exchange 2010/2007 but the output is more limited.
.PARAMETER Server
This optional parameter allows the target Exchange server to be specified. If it is not the
local server is assumed.
.PARAMETER OutputFilePath
This optional parameter allows an output directory to be specified. If it is not the local
directory is assumed. This parameter must not end in a \. To specify the folder "logs" on
the root of the E: drive you would use "-OutputFilePath E:\logs", not "-OutputFilePath E:\logs\".
.PARAMETER MailboxReport
This optional parameter gives a report of the number of active and passive databases and
mailboxes on the server.
.PARAMETER LoadBalancingReport
This optional parameter will check the connection count of the Default Web Site for every server
running Exchange 2013/2016 with the Client Access role in the org. It then breaks down servers by percentage to
give you an idea of how well the load is being balanced.
.PARAMETER CasServerList
Used with -LoadBalancingReport. A comma separated list of CAS servers to operate against. Without
this switch the report will use all 2013/2016 Client Access servers in the organization.
.PARAMETER SiteName
Used with -LoadBalancingReport. Specifies a site to pull CAS servers from instead of querying every server
in the organization.
.PARAMETER 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.
.PARAMETER BuildHtmlServersReport
Switch to enable the script to build the HTML report for all the servers XML results in the XMLDirectoryPath location.
.PARAMETER HtmlReportFile
Name of the HTML output file from the BuildHtmlServersReport. Default is ExchangeAllServersReport.html
.PARAMETER DCCoreRatio
Gathers the Exchange to DC/GC Core ratio and displays the results in the current site that the script is running in.
.PARAMETER AnalyzeDataOnly
Switch to analyze the existing HealthChecker XML files. The results are displayed on the screen and an HTML report is generated.
.PARAMETER SkipVersionCheck
No version check is performed when this switch is used.
.PARAMETER SaveDebugLog
The debug log is kept even if the script is executed successfully.
.PARAMETER ScriptUpdateOnly
Switch to check for the latest version of the script and perform an auto update. No elevated permissions or EMS are required.
.PARAMETER Verbose
This optional parameter enables verbose logging.
.EXAMPLE
.\HealthChecker.ps1 -Server SERVERNAME
Run against a single remote Exchange server
.EXAMPLE
.\HealthChecker.ps1 -Server SERVERNAME -MailboxReport -Verbose
Run against a single remote Exchange server with verbose logging and mailbox report enabled.
.EXAMPLE
Get-ExchangeServer | ?{$_.AdminDisplayVersion -Match "^Version 15"} | %{.\HealthChecker.ps1 -Server $_.Name}
Run against all Exchange 2013/2016 servers in the Organization.
.EXAMPLE
.\HealthChecker.ps1 -LoadBalancingReport
Run a load balancing report comparing all Exchange 2013/2016 CAS servers in the Organization.
.EXAMPLE
.\HealthChecker.ps1 -LoadBalancingReport -CasServerList CAS01,CAS02,CAS03
Run a load balancing report comparing servers named CAS01, CAS02, and CAS03.
.LINK
https://docs.microsoft.com/en-us/exchange/exchange-2013-sizing-and-configuration-recommendations-exchange-2013-help
https://docs.microsoft.com/en-us/exchange/exchange-2013-virtualization-exchange-2013-help#requirements-for-hardware-virtualization
https://docs.microsoft.com/en-us/exchange/plan-and-deploy/virtualization?view=exchserver-2019#requirements-for-hardware-virtualization
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Variables are being used')]
[CmdletBinding(DefaultParameterSetName = "HealthChecker")]
param(
[Parameter(Mandatory = $false, ParameterSetName = "HealthChecker")]
[Parameter(Mandatory = $false, ParameterSetName = "MailboxReport")]
[string]$Server = ($env:COMPUTERNAME),
[Parameter(Mandatory = $false)]
[ValidateScript( { -not $_.ToString().EndsWith('\') })][string]$OutputFilePath = ".",
[Parameter(Mandatory = $false, ParameterSetName = "MailboxReport")]
[switch]$MailboxReport,
[Parameter(Mandatory = $false, ParameterSetName = "LoadBalancingReport")]
[switch]$LoadBalancingReport,
[Parameter(Mandatory = $false, ParameterSetName = "LoadBalancingReport")]
[array]$CasServerList = $null,
[Parameter(Mandatory = $false, ParameterSetName = "LoadBalancingReport")]
[string]$SiteName = ([string]::Empty),
[Parameter(Mandatory = $false, ParameterSetName = "HTMLReport")]
[Parameter(Mandatory = $false, ParameterSetName = "AnalyzeDataOnly")]
[ValidateScript( { -not $_.ToString().EndsWith('\') })][string]$XMLDirectoryPath = ".",
[Parameter(Mandatory = $false, ParameterSetName = "HTMLReport")]
[switch]$BuildHtmlServersReport,
[Parameter(Mandatory = $false, ParameterSetName = "HTMLReport")]
[string]$HtmlReportFile = "ExchangeAllServersReport.html",
[Parameter(Mandatory = $false, ParameterSetName = "DCCoreReport")]
[switch]$DCCoreRatio,
[Parameter(Mandatory = $false, ParameterSetName = "AnalyzeDataOnly")]
[switch]$AnalyzeDataOnly,
[Parameter(Mandatory = $false)][switch]$SkipVersionCheck,
[Parameter(Mandatory = $false)][switch]$SaveDebugLog,
[Parameter(Mandatory = $false, ParameterSetName = "ScriptUpdateOnly")]
[switch]$ScriptUpdateOnly
)
$BuildVersion = "21.11.09.1825"
$Script:VerboseEnabled = $false
#this is to set the verbose information to a different color
if ($PSBoundParameters["Verbose"]) {
#Write verbose output in cyan since we already use yellow for warnings
$Script:VerboseEnabled = $true
$VerboseForeground = $Host.PrivateData.VerboseForegroundColor
$Host.PrivateData.VerboseForegroundColor = "Cyan"
}
Function Add-AnalyzedResultInformation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[HealthChecker.AnalyzedInformation]$AnalyzedInformation,
[object]$Details,
[string]$Name,
[string]$TestingName,
[object]$OutColumns,
[string]$HtmlName,
[object]$DisplayGroupingKey,
[int]$DisplayCustomTabNumber = -1,
[object]$DisplayTestingValue,
[string]$DisplayWriteType = "Grey",
[bool]$AddDisplayResultsLineInfo = $true,
[bool]$AddHtmlDetailRow = $true,
[string]$HtmlDetailsCustomValue = "",
[bool]$AddHtmlOverviewValues = $false,
[bool]$AddHtmlActionRow = $false
#[string]$ActionSettingClass = "",
#[string]$ActionSettingValue,
#[string]$ActionRecommendedDetailsClass = "",
#[string]$ActionRecommendedDetailsValue,
#[string]$ActionMoreInformationClass = "",
#[string]$ActionMoreInformationValue,
)
process {
Write-Verbose "Calling $($MyInvocation.MyCommand): $name"
if ($AddDisplayResultsLineInfo) {
if (!($AnalyzedInformation.DisplayResults.ContainsKey($DisplayGroupingKey))) {
Write-Verbose "Adding Display Grouping Key: $($DisplayGroupingKey.Name)"
[System.Collections.Generic.List[HealthChecker.DisplayResultsLineInfo]]$list = New-Object System.Collections.Generic.List[HealthChecker.DisplayResultsLineInfo]
$AnalyzedInformation.DisplayResults.Add($DisplayGroupingKey, $list)
}
$lineInfo = New-Object HealthChecker.DisplayResultsLineInfo
if ($null -ne $OutColumns) {
$lineInfo.OutColumns = $OutColumns
$lineInfo.WriteType = "OutColumns"
$lineInfo.TestingValue = $OutColumns
} else {
$lineInfo.DisplayValue = $Details
$lineInfo.Name = $Name
if ($DisplayCustomTabNumber -ne -1) {
$lineInfo.TabNumber = $DisplayCustomTabNumber
} else {
$lineInfo.TabNumber = $DisplayGroupingKey.DefaultTabNumber
}
if ($null -ne $DisplayTestingValue) {
$lineInfo.TestingValue = $DisplayTestingValue
} else {
$lineInfo.TestingValue = $Details
}
if (-not ([string]::IsNullOrEmpty($TestingName))) {
$lineInfo.TestingName = $TestingName
} else {
$lineInfo.TestingName = $Name
}
$lineInfo.WriteType = $DisplayWriteType
}
$AnalyzedInformation.DisplayResults[$DisplayGroupingKey].Add($lineInfo)
}
if ($AddHtmlDetailRow) {
if (!($analyzedResults.HtmlServerValues.ContainsKey("ServerDetails"))) {
[System.Collections.Generic.List[HealthChecker.HtmlServerInformationRow]]$list = New-Object System.Collections.Generic.List[HealthChecker.HtmlServerInformationRow]
$AnalyzedInformation.HtmlServerValues.Add("ServerDetails", $list)
}
$detailRow = New-Object HealthChecker.HtmlServerInformationRow
if ($displayWriteType -ne "Grey") {
$detailRow.Class = $displayWriteType
}
if ([string]::IsNullOrEmpty($HtmlName)) {
$detailRow.Name = $Name
} else {
$detailRow.Name = $HtmlName
}
if ([string]::IsNullOrEmpty($HtmlDetailsCustomValue)) {
$detailRow.DetailValue = $Details
} else {
$detailRow.DetailValue = $HtmlDetailsCustomValue
}
$AnalyzedInformation.HtmlServerValues["ServerDetails"].Add($detailRow)
}
if ($AddHtmlOverviewValues) {
if (!($analyzedResults.HtmlServerValues.ContainsKey("OverviewValues"))) {
[System.Collections.Generic.List[HealthChecker.HtmlServerInformationRow]]$list = New-Object System.Collections.Generic.List[HealthChecker.HtmlServerInformationRow]
$AnalyzedInformation.HtmlServerValues.Add("OverviewValues", $list)
}
$overviewValue = New-Object HealthChecker.HtmlServerInformationRow
if ($displayWriteType -ne "Grey") {
$overviewValue.Class = $displayWriteType
}
if ([string]::IsNullOrEmpty($HtmlName)) {
$overviewValue.Name = $Name
} else {
$overviewValue.Name = $HtmlName
}
if ([string]::IsNullOrEmpty($HtmlDetailsCustomValue)) {
$overviewValue.DetailValue = $Details
} else {
$overviewValue.DetailValue = $HtmlDetailsCustomValue
}
$AnalyzedInformation.HtmlServerValues["OverviewValues"].Add($overviewValue)
}
if ($AddHtmlActionRow) {
#TODO
}
}
}
Function Get-DisplayResultsGroupingKey {
param(
[string]$Name,
[bool]$DisplayGroupName = $true,
[int]$DisplayOrder,
[int]$DefaultTabNumber = 1
)
$obj = New-Object HealthChecker.DisplayResultsGroupingKey
$obj.Name = $Name
$obj.DisplayGroupName = $DisplayGroupName
$obj.DisplayOrder = $DisplayOrder
$obj.DefaultTabNumber = $DefaultTabNumber
return $obj
}
Function Invoke-CatchActionError {
[CmdletBinding()]
param(
[scriptblock]$CatchActionFunction
)
if ($null -ne $CatchActionFunction) {
& $CatchActionFunction
}
}
Function Invoke-ScriptBlockHandler {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$ComputerName,
[Parameter(Mandatory = $true)]
[scriptblock]
$ScriptBlock,
[string]
$ScriptBlockDescription,
[object]
$ArgumentList,
[bool]
$IncludeNoProxyServerOption,
[scriptblock]
$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$returnValue = $null
}
process {
if (-not([string]::IsNullOrEmpty($ScriptBlockDescription))) {
Write-Verbose "Description: $ScriptBlockDescription"
}
try {
if (($ComputerName).Split(".")[0] -ne $env:COMPUTERNAME) {
$params = @{
ComputerName = $ComputerName
ScriptBlock = $ScriptBlock
ErrorAction = "Stop"
}
if ($IncludeNoProxyServerOption) {
Write-Verbose "Including SessionOption"
$params.Add("SessionOption", (New-PSSessionOption -ProxyAccessType NoProxyServer))
}
if ($null -ne $ArgumentList) {
Write-Verbose "Running Invoke-Command with argument list"
$params.Add("ArgumentList", $ArgumentList)
} else {
Write-Verbose "Running Invoke-Command without argument list"
}
$returnValue = Invoke-Command @params
} else {
if ($null -ne $ArgumentList) {
Write-Verbose "Running Script Block Locally with argument list"
$returnValue = & $ScriptBlock $ArgumentList
} else {
Write-Verbose "Running Script Block Locally without argument list"
$returnValue = & $ScriptBlock
}
}
} catch {
Write-Verbose "Failed to run $($MyInvocation.MyCommand)"
Invoke-CatchActionError $CatchActionFunction
}
}
end {
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $returnValue
}
}
Function Get-VisualCRedistributableInstalledVersion {
[CmdletBinding()]
param(
[string]$ComputerName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$softwareList = New-Object 'System.Collections.Generic.List[object]'
}
process {
$installedSoftware = Invoke-ScriptBlockHandler -ComputerName $ComputerName `
-ScriptBlock { Get-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* } `
-ScriptBlockDescription "Querying for software" `
-CatchActionFunction $CatchActionFunction
foreach ($software in $installedSoftware) {
if ($software.PSObject.Properties.Name -contains "DisplayName" -and $software.DisplayName -like "Microsoft Visual C++ *") {
Write-Verbose "Microsoft Visual C++ Found: $($software.DisplayName)"
$softwareList.Add([PSCustomObject]@{
DisplayName = $software.DisplayName
DisplayVersion = $software.DisplayVersion
InstallDate = $software.InstallDate
VersionIdentifier = $software.Version
})
}
}
}
end {
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $softwareList
}
}
Function Get-VisualCRedistributableInfo {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateSet(2012, 2013)]
[int]
$Year
)
if ($Year -eq 2012) {
return [PSCustomObject]@{
VersionNumber = 184610406
DownloadUrl = "https://www.microsoft.com/en-us/download/details.aspx?id=30679"
DisplayName = "Microsoft Visual C++ 2012*"
}
} else {
return [PSCustomObject]@{
VersionNumber = 201367256
DownloadUrl = "https://support.microsoft.com/en-us/topic/update-for-visual-c-2013-redistributable-package-d8ccd6a5-4e26-c290-517b-8da6cfdf4f10"
DisplayName = "Microsoft Visual C++ 2013*"
}
}
}
Function Test-VisualCRedistributableInstalled {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet(2012, 2013)]
[int]
$Year,
[Parameter(Mandatory = $true, Position = 1)]
[object]
$Installed
)
$desired = Get-VisualCRedistributableInfo $Year
return ($null -ne ($Installed | Where-Object { $_.DisplayName -like $desired.DisplayName }))
}
Function Test-VisualCRedistributableUpToDate {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet(2012, 2013)]
[int]
$Year,
[Parameter(Mandatory = $true, Position = 1)]
[object]
$Installed
)
$desired = Get-VisualCRedistributableInfo $Year
return ($null -ne ($Installed | Where-Object {
$_.DisplayName -like $desired.DisplayName -and $_.VersionIdentifier -eq $desired.VersionNumber
}))
}
Function Invoke-AnalyzerExchangeInformation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyExchangeInformation = Get-DisplayResultsGroupingKey -Name "Exchange Information" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
$hardwareInformation = $HealthServerObject.HardwareInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Name" -Details ($HealthServerObject.ServerName) `
-DisplayGroupingKey $keyExchangeInformation `
-AddHtmlOverviewValues $true `
-HtmlName "Server Name"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Generation Time" -Details $HealthServerObject.GenerationTime `
-DisplayGroupingKey $keyExchangeInformation `
-AddHtmlOverviewValues $true
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Version" -Details ($exchangeInformation.BuildInformation.FriendlyName) `
-DisplayGroupingKey $keyExchangeInformation `
-AddHtmlOverviewValues $true `
-HtmlName "Exchange Version"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Build Number" -Details ($exchangeInformation.BuildInformation.ExchangeSetup.FileVersion) `
-DisplayGroupingKey $keyExchangeInformation
if ($exchangeInformation.BuildInformation.SupportedBuild -eq $false) {
$daysOld = ($date - ([System.Convert]::ToDateTime([DateTime]$exchangeInformation.BuildInformation.ReleaseDate,
[System.Globalization.DateTimeFormatInfo]::InvariantInfo))).Days
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Error" `
-Details ("Out of date Cumulative Update. Please upgrade to one of the two most recently released Cumulative Updates. Currently running on a build that is {0} days old." -f $daysOld) `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayWriteType "Red" `
-DisplayCustomTabNumber 2 `
-TestingName "Out of Date" `
-DisplayTestingValue $true `
-AddHtmlDetailRow $false
}
if (-not ([string]::IsNullOrEmpty($exchangeInformation.BuildInformation.LocalBuildNumber))) {
$local = $exchangeInformation.BuildInformation.LocalBuildNumber
$remote = $exchangeInformation.BuildInformation.BuildNumber
if ($local.Substring(0, $local.LastIndexOf(".")) -ne $remote.Substring(0, $remote.LastIndexOf("."))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Warning" `
-Details ("Running commands from a different version box can cause issues. Local Tools Server Version: {0}" -f $local) `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
}
}
if ($null -ne $exchangeInformation.BuildInformation.KBsInstalled) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details ("Exchange IU or Security Hotfix Detected.") `
-DisplayGroupingKey $keyExchangeInformation `
-AddHtmlDetailRow $false
foreach ($kb in $exchangeInformation.BuildInformation.KBsInstalled) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $kb `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Server Role" -Details ($exchangeInformation.BuildInformation.ServerRole) `
-DisplayGroupingKey $keyExchangeInformation `
-AddHtmlOverviewValues $true
if ($exchangeInformation.BuildInformation.ServerRole -le [HealthChecker.ExchangeServerRole]::Mailbox) {
$dagName = [System.Convert]::ToString($exchangeInformation.GetMailboxServer.DatabaseAvailabilityGroup)
if ([System.String]::IsNullOrWhiteSpace($dagName)) {
$dagName = "Standalone Server"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "DAG Name" -Details $dagName `
-DisplayGroupingKey $keyExchangeInformation
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "AD Site" -Details ([System.Convert]::ToString(($exchangeInformation.GetExchangeServer.Site)).Split("/")[-1]) `
-DisplayGroupingKey $keyExchangeInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "MAPI/HTTP Enabled" -Details ($exchangeInformation.MapiHttpEnabled) `
-DisplayGroupingKey $keyExchangeInformation
if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013 -and
$exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge -and
$exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Mailbox) {
if ($null -ne $exchangeInformation.ApplicationPools -and
$exchangeInformation.ApplicationPools.Count -gt 0) {
$mapiFEAppPool = $exchangeInformation.ApplicationPools["MSExchangeMapiFrontEndAppPool"]
[bool]$enabled = $mapiFEAppPool.GCServerEnabled
[bool]$unknown = $mapiFEAppPool.GCUnknown
$warning = [string]::Empty
$displayWriteType = "Green"
$displayValue = "Server"
if ($hardwareInformation.TotalMemory -ge 21474836480 -and
$enabled -eq $false) {
$displayWriteType = "Red"
$displayValue = "Workstation --- Error"
$warning = "To Fix this issue go into the file MSExchangeMapiFrontEndAppPool_CLRConfig.config in the Exchange Bin directory and change the GCServer to true and recycle the MAPI Front End App Pool"
} elseif ($unknown) {
$displayValue = "Unknown --- Warning"
$displayWriteType = "Yellow"
} elseif (!($enabled)) {
$displayWriteType = "Yellow"
$displayValue = "Workstation --- Warning"
$warning = "You could be seeing some GC issues within the Mapi Front End App Pool. However, you don't have enough memory installed on the system to recommend switching the GC mode by default without consulting a support professional."
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "MAPI Front End App Pool GC Mode" -Details $displayValue `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $displayWriteType
} else {
$warning = "Unable to determine MAPI Front End App Pool GC Mode status. This may be a temporary issue. You should try to re-run the script"
}
if ($warning -ne [string]::Empty) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $warning `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow" `
-AddHtmlDetailRow $false
}
}
$internetProxy = $exchangeInformation.GetExchangeServer.InternetWebProxy
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Internet Web Proxy" `
-Details $( if ([string]::IsNullOrEmpty($internetProxy)) { "Not Set" } else { $internetProxy } )`
-DisplayGroupingKey $keyExchangeInformation
if (-not ([string]::IsNullOrWhiteSpace($exchangeInformation.GetWebServicesVirtualDirectory.InternalNLBBypassUrl))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "EWS Internal Bypass URL Set" `
-Details ("$($exchangeInformation.GetWebServicesVirtualDirectory.InternalNLBBypassUrl) - Can cause issues after KB 5001779") `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayWriteType "Red"
}
Write-Verbose "Working on results from Test-ServiceHealth"
$servicesNotRunning = $exchangeInformation.ExchangeServicesNotRunning
if ($null -ne $servicesNotRunning) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Services Not Running" `
-DisplayGroupingKey $keyExchangeInformation
foreach ($stoppedService in $servicesNotRunning) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $stoppedService `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
}
Write-Verbose "Working on Exchange Server Maintenance"
$serverMaintenance = $exchangeInformation.ServerMaintenance
$getMailboxServer = $exchangeInformation.GetMailboxServer
if (($serverMaintenance.InactiveComponents).Count -eq 0 -and
($null -eq $serverMaintenance.GetClusterNode -or
$serverMaintenance.GetClusterNode.State -eq "Up") -and
($null -eq $getMailboxServer -or
($getMailboxServer.DatabaseCopyActivationDisabledAndMoveNow -eq $false -and
$getMailboxServer.DatabaseCopyAutoActivationPolicy.ToString() -eq "Unrestricted"))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Exchange Server Maintenance" -Details "Server is not in Maintenance Mode" `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayWriteType "Green"
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Exchange Server Maintenance" `
-DisplayGroupingKey $keyExchangeInformation
if (($serverMaintenance.InactiveComponents).Count -ne 0) {
foreach ($inactiveComponent in $serverMaintenance.InactiveComponents) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Component" -Details $inactiveComponent `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Details "For more information: https://aka.ms/HC-ServerComponentState" `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
if ($getMailboxServer.DatabaseCopyActivationDisabledAndMoveNow -or
$getMailboxServer.DatabaseCopyAutoActivationPolicy -eq "Blocked") {
$displayValue = "`r`n`t`tDatabaseCopyActivationDisabledAndMoveNow: {0} --- should be 'false'`r`n`t`tDatabaseCopyAutoActivationPolicy: {1} --- should be 'unrestricted'" -f `
$getMailboxServer.DatabaseCopyActivationDisabledAndMoveNow,
$getMailboxServer.DatabaseCopyAutoActivationPolicy
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Database Copy Maintenance" -Details $displayValue `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
}
if ($null -ne $serverMaintenance.GetClusterNode -and
$serverMaintenance.GetClusterNode.State -ne "Up") {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Cluster Node" -Details ("'{0}' --- should be 'Up'" -f $serverMaintenance.GetClusterNode.State) `
-DisplayGroupingKey $keyExchangeInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
}
}
}
Function Invoke-AnalyzerHybridInformation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyHybridInformation = Get-DisplayResultsGroupingKey -Name "Hybrid Information" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
if ($exchangeInformation.BuildInformation.MajorVersion -ge [HealthChecker.ExchangeMajorVersion]::Exchange2013 -and
$null -ne $exchangeInformation.GetHybridConfiguration) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Organization Hybrid enabled" -Details "True" `
-DisplayGroupingKey $keyHybridInformation
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.OnPremisesSmartHost))) {
$onPremSmartHostDomain = ($exchangeInformation.GetHybridConfiguration.OnPremisesSmartHost).ToString()
$onPremSmartHostWriteType = "Grey"
} else {
$onPremSmartHostDomain = "No on-premises smart host domain configured for hybrid use"
$onPremSmartHostWriteType = "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "On-Premises Smart Host Domain" -Details $onPremSmartHostDomain `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType $onPremSmartHostWriteType
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.Domains))) {
$domainsConfiguredForHybrid = $exchangeInformation.GetHybridConfiguration.Domains
$domainsConfiguredForHybridWriteType = "Grey"
} else {
$domainsConfiguredForHybridWriteType = "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Domain(s) configured for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType $domainsConfiguredForHybridWriteType
if ($domainsConfiguredForHybrid.Count -ge 1) {
foreach ($domain in $domainsConfiguredForHybrid) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $domain `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType $domainsConfiguredForHybridWriteType `
-DisplayCustomTabNumber 2
}
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "No domain configured for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType $domainsConfiguredForHybridWriteType `
-DisplayCustomTabNumber 2
}
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.EdgeTransportServers))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Edge Transport Server(s)" `
-DisplayGroupingKey $keyHybridInformation
foreach ($edgeServer in $exchangeInformation.GetHybridConfiguration.EdgeTransportServers) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $edgeServer `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2
}
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.ReceivingTransportServers)) -or
(-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.SendingTransportServers)))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "When configuring the EdgeTransportServers parameter, you must configure the ReceivingTransportServers and SendingTransportServers parameter values to null" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 2
}
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Receiving Transport Server(s)" `
-DisplayGroupingKey $keyHybridInformation
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.ReceivingTransportServers))) {
foreach ($receivingTransportSrv in $exchangeInformation.GetHybridConfiguration.ReceivingTransportServers) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $receivingTransportSrv `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2
}
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "No Receiving Transport Server configured for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Sending Transport Server(s)" `
-DisplayGroupingKey $keyHybridInformation
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.SendingTransportServers))) {
foreach ($sendingTransportSrv in $exchangeInformation.GetHybridConfiguration.SendingTransportServers) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $sendingTransportSrv `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2
}
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "No Sending Transport Server configured for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
}
if ($exchangeInformation.GetHybridConfiguration.ServiceInstance -eq 1) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Service Instance" -Details "Office 365 operated by 21Vianet" `
-DisplayGroupingKey $keyHybridInformation
} elseif ($exchangeInformation.GetHybridConfiguration.ServiceInstance -ne 0) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Service Instance" -Details ($exchangeInformation.GetHybridConfiguration.ServiceInstance) `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType "Red"
$AnalyzeResults | Add-AnalyzedResultInformation -Details "You are using an invalid value. Please set this value to 0 (null) or re-run HCW" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType "Red"
}
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.TlsCertificateName))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "TLS Certificate Name" -Details ($exchangeInformation.GetHybridConfiguration.TlsCertificateName).ToString() `
-DisplayGroupingKey $keyHybridInformation
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "TLS Certificate Name" -Details "No valid certificate found" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayWriteType "Red"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Feature(s) enabled for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation
if (-not([System.String]::IsNullOrEmpty($exchangeInformation.GetHybridConfiguration.Features))) {
foreach ($feature in $exchangeInformation.GetHybridConfiguration.Features) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $feature `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2
}
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "No feature(s) enabled for Hybrid use" `
-DisplayGroupingKey $keyHybridInformation `
-DisplayCustomTabNumber 2
}
}
}
Function Invoke-AnalyzerOsInformation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyOSInformation = Get-DisplayResultsGroupingKey -Name "Operating System Information" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
$osInformation = $HealthServerObject.OSInformation
$hardwareInformation = $HealthServerObject.HardwareInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Version" -Details ($osInformation.BuildInformation.FriendlyName) `
-DisplayGroupingKey $keyOSInformation `
-AddHtmlOverviewValues $true `
-HtmlName "OS Version"
$upTime = "{0} day(s) {1} hour(s) {2} minute(s) {3} second(s)" -f $osInformation.ServerBootUp.Days,
$osInformation.ServerBootUp.Hours,
$osInformation.ServerBootUp.Minutes,
$osInformation.ServerBootUp.Seconds
$AnalyzeResults | Add-AnalyzedResultInformation -Name "System Up Time" -Details $upTime `
-DisplayGroupingKey $keyOSInformation `
-DisplayTestingValue ($osInformation.ServerBootUp) `
-AddHtmlDetailRow $false
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Time Zone" -Details ($osInformation.TimeZone.CurrentTimeZone) `
-DisplayGroupingKey $keyOSInformation `
-AddHtmlOverviewValues $true
$writeValue = $false
$warning = @("Windows can not properly detect any DST rule changes in your time zone. Set 'Adjust for daylight saving time automatically to on'")
if ($osInformation.TimeZone.DstIssueDetected) {
$writeType = "Red"
} elseif ($osInformation.TimeZone.DynamicDaylightTimeDisabled -ne 0) {
$writeType = "Yellow"
} else {
$warning = [string]::Empty
$writeValue = $true
$writeType = "Grey"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Dynamic Daylight Time Enabled" -Details $writeValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $writeType
if ($warning -ne [string]::Empty) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $warning `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
}
if ([string]::IsNullOrEmpty($osInformation.TimeZone.TimeZoneKeyName)) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Time Zone Key Name" -Details "Empty --- Warning Need to switch your current time zone to a different value, then switch it back to have this value populated again." `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Yellow"
}
if ($exchangeInformation.NETFramework.OnRecommendedVersion) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name ".NET Framework" -Details ($osInformation.NETFramework.FriendlyName) `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Green" `
-AddHtmlOverviewValues $true
} else {
$testObject = New-Object PSCustomObject
$testObject | Add-Member -MemberType NoteProperty -Name "CurrentValue" -Value ($osInformation.NETFramework.FriendlyName)
$testObject | Add-Member -MemberType NoteProperty -Name "MaxSupportedVersion" -Value ($exchangeInformation.NETFramework.MaxSupportedVersion)
$displayFriendly = Get-NETFrameworkVersion -NetVersionKey $exchangeInformation.NETFramework.MaxSupportedVersion
$displayValue = "{0} - Warning Recommended .NET Version is {1}" -f $osInformation.NETFramework.FriendlyName, $displayFriendly.FriendlyName
$AnalyzeResults | Add-AnalyzedResultInformation -Name ".NET Framework" -Details $displayValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Yellow" `
-DisplayTestingValue $testObject `
-HtmlDetailsCustomValue ($osInformation.NETFramework.FriendlyName) `
-AddHtmlOverviewValues $true
}
$displayValue = [string]::Empty
$displayWriteType = "Yellow"
$totalPhysicalMemory = [Math]::Round($hardwareInformation.TotalMemory / 1MB)
$instanceCount = 0
Write-Verbose "Evaluating Page File Information"
Write-Verbose "Total Memory: $totalPhysicalMemory"
foreach ($pageFile in $osInformation.PageFile) {
$maxPageSize = $pageFile.MaximumSize
Write-Verbose "Max Page Size: $maxPageSize"
$testingValue = [PSCustomObject]@{
TotalPhysicalMemory = $totalPhysicalMemory
MaxPageSize = $maxPageSize
MultiPageFile = $osInformation.PageFile.Count -gt 1
RecommendedPageFile = 0
}
if ($maxPageSize -eq 0) {
$displayValue = "Error: System is set to automatically manage the pagefile size."
$displayWriteType = "Red"
} elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) {
$recommendedPageFile = [Math]::Round($totalPhysicalMemory / 4)
$testingValue.RecommendedPageFile = $recommendedPageFile
Write-Verbose "Recommended Page File Size: $recommendedPageFile"
if ($recommendedPageFile -ne $maxPageSize) {
$displayValue = "$maxPageSize`MB `r`n`t`tWarning: Page File is not set to 25% of the Total System Memory which is $totalPhysicalMemory`MB. Recommended is $recommendedPageFile`MB"
} else {
$displayValue = "$recommendedPageFile`MB"
$displayWriteType = "Grey"
}
} elseif ($totalPhysicalMemory -ge 32768) {
if ($maxPageSize -eq 32778) {
$displayValue = "$maxPageSize`MB"
$displayWriteType = "Grey"
} else {
$displayValue = "$maxPageSize`MB `r`n`t`tWarning: Pagefile should be capped at 32778MB for 32GB plus 10MB - Article: https://aka.ms/HC-SystemRequirements2016#hardware-requirements-for-exchange-2016"
}
} else {
$recommendedPageFile = $totalPhysicalMemory + 10
$testingValue.RecommendedPageFile
if ($recommendedPageFile -ne $maxPageSize) {
$displayValue = "$maxPageSize`MB `r`n`t`tWarning: Page File is not set to Total System Memory plus 10MB which should be $recommendedPageFile`MB"
} else {
$displayValue = "$maxPageSize`MB"
$displayWriteType = "Grey"
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Page File Size" -Details $displayValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $displayWriteType `
-TestingName "Page File Size $instanceCount" `
-DisplayTestingValue $testingValue
$instanceCount++
}
if ($null -ne $osInformation.PageFile -and
$osInformation.PageFile.Count -gt 1) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Error: Multiple page files detected. This has been known to cause performance issues, please address this." `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Red" `
-TestingName "Multiple Page File Detected." `
-DisplayTestingValue $true `
-DisplayCustomTabNumber 2
}
if ($osInformation.PowerPlan.HighPerformanceSet) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Power Plan" -Details ($osInformation.PowerPlan.PowerPlanSetting) `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Green"
} else {
$displayValue = "{0} --- Error" -f $osInformation.PowerPlan.PowerPlanSetting
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Power Plan" -Details $displayValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Red"
}
$displayWriteType = "Grey"
$displayValue = $osInformation.NetworkInformation.HttpProxy.ProxyAddress
if ($osInformation.NetworkInformation.HttpProxy.ProxyAddress -ne "None") {
$displayValue = "$($osInformation.NetworkInformation.HttpProxy.ProxyAddress) --- Warning this can cause client connectivity issues."
$displayWriteType = "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Http Proxy Setting" `
-Details $displayValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $osInformation.NetworkInformation.HttpProxy
if ($displayWriteType -eq "Yellow") {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Http Proxy By Pass List" `
-Details "$($osInformation.NetworkInformation.HttpProxy.ByPassList)" `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Yellow"
}
if ($osInformation.NetworkInformation.HttpProxy.ProxyAddress -ne "None" -and
$osInformation.NetworkInformation.HttpProxy.ProxyAddress -ne $exchangeInformation.GetExchangeServer.InternetWebProxy) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Error: Exchange Internet Web Proxy doesn't match OS Web Proxy." `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Red" `
-TestingName "Proxy Doesn't Match" `
-DisplayCustomTabNumber 2
}
$displayWriteType2012 = "Yellow"
$displayWriteType2013 = "Yellow"
$displayValue2012 = "Unknown"
$displayValue2013 = "Unknown"
if ($null -ne $osInformation.VcRedistributable) {
if (Test-VisualCRedistributableUpToDate -Year 2012 -Installed $osInformation.VcRedistributable) {
$displayWriteType2012 = "Green"
$displayValue2012 = "$((Get-VisualCRedistributableInfo 2012).VersionNumber) Version is current"
} elseif (Test-VisualCRedistributableInstalled -Year 2012 -Installed $osInformation.VcRedistributable) {
$displayValue2012 = "Redistributable is outdated"
}
if (Test-VisualCRedistributableUpToDate -Year 2013 -Installed $osInformation.VcRedistributable) {
$displayWriteType2013 = "Green"
$displayValue2013 = "$((Get-VisualCRedistributableInfo 2013).VersionNumber) Version is current"
} elseif (Test-VisualCRedistributableInstalled -Year 2013 -Installed $osInformation.VcRedistributable) {
$displayValue2013 = "Redistributable is outdated"
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Visual C++ 2012" -Details $displayValue2012 `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $displayWriteType2012
if ($exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Visual C++ 2013" -Details $displayValue2013 `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $displayWriteType2013
}
if (($exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge -and
($displayWriteType2012 -eq "Yellow" -or
$displayWriteType2013 -eq "Yellow")) -or
$displayWriteType2012 -eq "Yellow") {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Note: For more information about the latest C++ Redistributeable 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." `
-DisplayGroupingKey $keyOSInformation `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
$displayValue = "False"
$writeType = "Grey"
if ($osInformation.ServerPendingReboot.PendingReboot) {
$displayValue = "True --- Warning a reboot is pending and can cause issues on the server."
$writeType = "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Server Pending Reboot" -Details $displayValue `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType $writeType `
-DisplayTestingValue ($osInformation.ServerPendingReboot.PendingReboot)
if ($osInformation.ServerPendingReboot.PendingReboot -and
$osInformation.ServerPendingReboot.PendingRebootLocations.Count -gt 0) {
foreach ($line in $osInformation.ServerPendingReboot.PendingRebootLocations) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $line `
-DisplayGroupingKey $keyOSInformation `
-DisplayCustomTabNumber 2 `
-TestingName $line `
-DisplayWriteType "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Details "More Information: https://aka.ms/HC-RebootPending" `
-DisplayGroupingKey $keyOSInformation `
-DisplayWriteType "Yellow" `
-TestingName "Reboot More Information" `
-DisplayTestingValue $true `
-DisplayCustomTabNumber 2
}
}
Function Invoke-AnalyzerHardwareInformation {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyHardwareInformation = Get-DisplayResultsGroupingKey -Name "Processor/Hardware Information" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
$osInformation = $HealthServerObject.OSInformation
$hardwareInformation = $HealthServerObject.HardwareInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Type" -Details ($hardwareInformation.ServerType) `
-DisplayGroupingKey $keyHardwareInformation `
-AddHtmlOverviewValues $true `
-HtmlName "Hardware Type"
if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::Physical -or
$hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Manufacturer" -Details ($hardwareInformation.Manufacturer) `
-DisplayGroupingKey $keyHardwareInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Model" -Details ($hardwareInformation.Model) `
-DisplayGroupingKey $keyHardwareInformation
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Processor" -Details ($hardwareInformation.Processor.Name) `
-DisplayGroupingKey $keyHardwareInformation
$value = $hardwareInformation.Processor.NumberOfProcessors
$processorName = "Number of Processors"
if ($hardwareInformation.ServerType -ne [HealthChecker.ServerType]::Physical) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name $processorName -Details $value `
-DisplayGroupingKey $keyHardwareInformation
<# Comment out for now. Not sure if we have a lot of value here as i believe this changed in newer vmware hosts versions.
if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::VMWare) {
$AnalyzeResults = Add-AnalyzedResultInformation -Details "Note: Please make sure you are following VMware's performance recommendation to get the most out of your guest machine. VMware blog 'Does corespersocket Affect Performance?' https://blogs.vmware.com/vsphere/2013/10/does-corespersocket-affect-performance.html" `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayCustomTabNumber 2 `
-AnalyzedInformation $AnalyzeResults
}
#>
} elseif ($value -gt 2) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name $processorName -Details ("{0} - Error: Recommended to only have 2 Processors" -f $value) `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType "Red" `
-DisplayTestingValue $value `
-HtmlDetailsCustomValue $value
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name $processorName -Details $value `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType "Green"
}
$physicalValue = $hardwareInformation.Processor.NumberOfPhysicalCores
$logicalValue = $hardwareInformation.Processor.NumberOfLogicalCores
$displayWriteType = "Green"
if (($logicalValue -gt 24 -and
$exchangeInformation.BuildInformation.MajorVersion -lt [HealthChecker.ExchangeMajorVersion]::Exchange2019) -or
$logicalValue -gt 48) {
$displayWriteType = "Yellow"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Number of Physical Cores" -Details $physicalValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType $displayWriteType
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Number of Logical Cores" -Details $logicalValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType $displayWriteType `
-AddHtmlOverviewValues $true
$displayValue = "Disabled"
$displayWriteType = "Green"
$displayTestingValue = $false
$additionalDisplayValue = [string]::Empty
$additionalWriteType = "Red"
if ($logicalValue -gt $physicalValue) {
if ($hardwareInformation.ServerType -ne [HealthChecker.ServerType]::HyperV) {
$displayValue = "Enabled --- Error: Having Hyper-Threading enabled goes against best practices and can cause performance issues. Please disable as soon as possible."
$displayTestingValue = $true
$displayWriteType = "Red"
} else {
$displayValue = "Enabled --- Not Applicable"
$displayTestingValue = $true
$displayWriteType = "Grey"
}
if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) {
$additionalDisplayValue = "Error: For high-performance computing (HPC) application, like Exchange, Amazon recommends that you have Hyper-Threading Technology disabled in their service. More information: https://aka.ms/HC-EC2HyperThreading"
}
if ($hardwareInformation.Processor.Name.StartsWith("AMD")) {
$additionalDisplayValue = "This script may incorrectly report that Hyper-Threading is enabled on certain AMD processors. Check with the manufacturer to see if your model supports SMT."
$additionalWriteType = "Yellow"
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Hyper-Threading" -Details $displayValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $displayTestingValue
if (!([string]::IsNullOrEmpty($additionalDisplayValue))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $additionalDisplayValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType $additionalWriteType `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
}
#NUMA BIOS CHECK - AKA check to see if we can properly see all of our cores on the box
$displayWriteType = "Yellow"
$testingValue = "Unknown"
$displayValue = [string]::Empty
if ($hardwareInformation.Model.Contains("ProLiant")) {
$name = "NUMA Group Size Optimization"
if ($hardwareInformation.Processor.EnvironmentProcessorCount -eq -1) {
$displayValue = "Unknown `r`n`t`tWarning: If this is set to Clustered, this can cause multiple types of issues on the server"
} elseif ($hardwareInformation.Processor.EnvironmentProcessorCount -ne $logicalValue) {
$displayValue = "Clustered `r`n`t`tError: This setting should be set to Flat. By having this set to Clustered, we will see multiple different types of issues."
$testingValue = "Clustered"
$displayWriteType = "Red"
} else {
$displayValue = "Flat"
$testingValue = "Flat"
$displayWriteType = "Green"
}
} else {
$name = "All Processor Cores Visible"
if ($hardwareInformation.Processor.EnvironmentProcessorCount -eq -1) {
$displayValue = "Unknown `r`n`t`tWarning: If we aren't able to see all processor cores from Exchange, we could see performance related issues."
} elseif ($hardwareInformation.Processor.EnvironmentProcessorCount -ne $logicalValue) {
$displayValue = "Failed `r`n`t`tError: Not all Processor Cores are visible to Exchange and this will cause a performance impact"
$displayWriteType = "Red"
$testingValue = "Failed"
} else {
$displayWriteType = "Green"
$displayValue = "Passed"
$testingValue = "Passed"
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name $name -Details $displayValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $testingValue
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Max Processor Speed" -Details ($hardwareInformation.Processor.MaxMegacyclesPerCore) `
-DisplayGroupingKey $keyHardwareInformation
if ($hardwareInformation.Processor.ProcessorIsThrottled) {
$currentSpeed = $hardwareInformation.Processor.CurrentMegacyclesPerCore
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Current Processor Speed" -Details ("{0} --- Error: Processor appears to be throttled." -f $currentSpeed) `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType "Red" `
-DisplayTestingValue $currentSpeed
$displayValue = "Error: Power Plan is NOT set to `"High Performance`". This change doesn't require a reboot and takes affect right away. Re-run script after doing so"
if ($osInformation.PowerPlan.HighPerformanceSet) {
$displayValue = "Error: Power Plan is set to `"High Performance`", so it is likely that we are throttling in the BIOS of the computer settings."
}
$AnalyzeResults | Add-AnalyzedResultInformation -Details $displayValue `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayWriteType "Red" `
-TestingName "HighPerformanceSet" `
-DisplayTestingValue $osInformation.PowerPlan.HighPerformanceSet `
-AddHtmlDetailRow $false
}
$totalPhysicalMemory = [System.Math]::Round($hardwareInformation.TotalMemory / 1024 / 1024 / 1024)
$displayWriteType = "Yellow"
$displayDetails = [string]::Empty
if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) {
if ($totalPhysicalMemory -gt 256) {
$displayDetails = "{0} GB `r`n`t`tWarning: We recommend for the best performance to be scaled at or below 256 GB of Memory" -f $totalPhysicalMemory
} elseif ($totalPhysicalMemory -lt 64 -and
$exchangeInformation.BuildInformation.ServerRole -eq [HealthChecker.ExchangeServerRole]::Edge) {
$displayDetails = "{0} GB `r`n`t`tWarning: We recommend for the best performance to have a minimum of 64GB of RAM installed on the machine." -f $totalPhysicalMemory
} elseif ($totalPhysicalMemory -lt 128) {
$displayDetails = "{0} GB `r`n`t`tWarning: We recommend for the best performance to have a minimum of 128GB of RAM installed on the machine." -f $totalPhysicalMemory
} else {
$displayDetails = "{0} GB" -f $totalPhysicalMemory
$displayWriteType = "Grey"
}
} elseif ($totalPhysicalMemory -gt 192 -and
$exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) {
$displayDetails = "{0} GB `r`n`t`tWarning: We recommend for the best performance to be scaled at or below 192 GB of Memory." -f $totalPhysicalMemory
} elseif ($totalPhysicalMemory -gt 96 -and
$exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) {
$displayDetails = "{0} GB `r`n`t`tWarning: We recommend for the best performance to be scaled at or below 96GB of Memory." -f $totalPhysicalMemory
} else {
$displayDetails = "{0} GB" -f $totalPhysicalMemory
$displayWriteType = "Grey"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Physical Memory" -Details $displayDetails `
-DisplayGroupingKey $keyHardwareInformation `
-DisplayTestingValue $totalPhysicalMemory `
-DisplayWriteType $displayWriteType `
-AddHtmlOverviewValues $true
}
Function Invoke-AnalyzerNicSettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyNICSettings = Get-DisplayResultsGroupingKey -Name "NIC Settings Per Active Adapter" -DisplayOrder $Order -DefaultTabNumber 2
$osInformation = $HealthServerObject.OSInformation
$hardwareInformation = $HealthServerObject.HardwareInformation
foreach ($adapter in $osInformation.NetworkInformation.NetworkAdapters) {
if ($adapter.Description -eq "Remote NDIS Compatible Device") {
Write-Verbose "Remote NDSI Compatible Device found. Ignoring NIC."
continue
}
$value = "{0} [{1}]" -f $adapter.Description, $adapter.Name
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Interface Description" -Details $value `
-DisplayGroupingKey $keyNICSettings `
-DisplayCustomTabNumber 1
if ($osInformation.BuildInformation.MajorVersion -ge [HealthChecker.OSServerVersion]::Windows2012R2) {
Write-Verbose "On Windows 2012 R2 or new. Can provide more details on the NICs"
$driverDate = $adapter.DriverDate
$detailsValue = $driverDate
if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::Physical -or
$hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) {
if ($null -eq $driverDate -or
$driverDate -eq [DateTime]::MaxValue) {
$detailsValue = "Unknown"
} elseif ((New-TimeSpan -Start $date -End $driverDate).Days -lt [int]-365) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Warning: NIC driver is over 1 year old. Verify you are at the latest version." `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType "Yellow" `
-AddHtmlDetailRow $false
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Driver Date" -Details $detailsValue `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Driver Version" -Details ($adapter.DriverVersion) `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "MTU Size" -Details ($adapter.MTUSize) `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Max Processors" -Details ($adapter.NetAdapterRss.MaxProcessors) `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Max Processor Number" -Details ($adapter.NetAdapterRss.MaxProcessorNumber) `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Number of Receive Queues" -Details ($adapter.NetAdapterRss.NumberOfReceiveQueues) `
-DisplayGroupingKey $keyNICSettings
$writeType = "Yellow"
$testingValue = $null
if ($adapter.RssEnabledValue -eq 0) {
$detailsValue = "False --- Warning: Enabling RSS is recommended."
$testingValue = $false
} elseif ($adapter.RssEnabledValue -eq 1) {
$detailsValue = "True"
$testingValue = $true
$writeType = "Green"
} else {
$detailsValue = "No RSS Feature Detected."
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "RSS Enabled" -Details $detailsValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType $writeType `
-DisplayTestingValue $testingValue
} else {
Write-Verbose "On Windows 2012 or older and can't get advanced NIC settings"
}
$linkSpeed = $adapter.LinkSpeed
$displayValue = "{0} --- This may not be accurate due to virtualized hardware" -f $linkSpeed
if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::Physical -or
$hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) {
$displayValue = $linkSpeed
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Link Speed" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayTestingValue $linkSpeed
$displayValue = "{0}" -f $adapter.IPv6Enabled
$displayWriteType = "Grey"
$testingValue = $adapter.IPv6Enabled
if ($osInformation.NetworkInformation.IPv6DisabledComponents -ne 255 -and
$adapter.IPv6Enabled -eq $false) {
$displayValue = "{0} --- Warning" -f $adapter.IPv6Enabled
$displayWriteType = "Yellow"
$testingValue = $false
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "IPv6 Enabled" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $TestingValue
$AnalyzeResults | Add-AnalyzedResultInformation -Name "IPv4 Address" `
-DisplayGroupingKey $keyNICSettings
foreach ($address in $adapter.IPv4Addresses) {
$displayValue = "{0}\{1}" -f $address.Address, $address.Subnet
if ($address.DefaultGateway -ne [string]::Empty) {
$displayValue += " Gateway: {0}" -f $address.DefaultGateway
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Address" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayCustomTabNumber 3
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "IPv6 Address" `
-DisplayGroupingKey $keyNICSettings
foreach ($address in $adapter.IPv6Addresses) {
$displayValue = "{0}\{1}" -f $address.Address, $address.Subnet
if ($address.DefaultGateway -ne [string]::Empty) {
$displayValue += " Gateway: {0}" -f $address.DefaultGateway
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Address" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayCustomTabNumber 3
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "DNS Server" -Details $adapter.DnsServer `
-DisplayGroupingKey $keyNICSettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Registered In DNS" -Details $adapter.RegisteredInDns `
-DisplayGroupingKey $keyNICSettings
#Assuming that all versions of Hyper-V doesn't allow sleepy NICs
if (($hardwareInformation.ServerType -ne [HealthChecker.ServerType]::HyperV) -and ($adapter.PnPCapabilities -ne "MultiplexorNoPnP")) {
$displayWriteType = "Grey"
$displayValue = $adapter.SleepyNicDisabled
if (!$adapter.SleepyNicDisabled) {
$displayWriteType = "Yellow"
$displayValue = "False --- Warning: It's recommended to disable NIC power saving options`r`n`t`t`tMore Information: https://aka.ms/HC-NICPowerManagement"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Sleepy NIC Disabled" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $adapter.SleepyNicDisabled
}
$adapterDescription = $adapter.Description
$cookedValue = 0
$foundCounter = $false
if ($null -eq $osInformation.NetworkInformation.PacketsReceivedDiscarded) {
Write-Verbose "PacketsReceivedDiscarded is null"
continue
}
foreach ($prdInstance in $osInformation.NetworkInformation.PacketsReceivedDiscarded) {
$instancePath = $prdInstance.Path
$startIndex = $instancePath.IndexOf("(") + 1
$charLength = $instancePath.Substring($startIndex, ($instancePath.IndexOf(")") - $startIndex)).Length
$instanceName = $instancePath.Substring($startIndex, $charLength)
$possibleInstanceName = $adapterDescription.Replace("#", "_")
if ($instanceName -eq $adapterDescription -or
$instanceName -eq $possibleInstanceName) {
$cookedValue = $prdInstance.CookedValue
$foundCounter = $true
break
}
}
$displayWriteType = "Yellow"
$displayValue = $cookedValue
$baseDisplayValue = "{0} --- {1}: This value should be at 0."
$knownIssue = $false
if ($foundCounter) {
if ($cookedValue -eq 0) {
$displayWriteType = "Green"
} elseif ($cookedValue -lt 1000) {
$displayValue = $baseDisplayValue -f $cookedValue, "Warning"
} else {
$displayWriteType = "Red"
$displayValue = [string]::Concat(($baseDisplayValue -f $cookedValue, "Error"), "We are also seeing this value being rather high so this can cause a performance impacted on a system.")
}
if ($adapterDescription -like "*vmxnet3*" -and
$cookedValue -gt 0) {
$knownIssue = $true
}
} else {
$displayValue = "Couldn't find value for the counter."
$cookedValue = $null
$displayWriteType = "Grey"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Packets Received Discarded" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayTestingValue $cookedValue `
-DisplayWriteType $displayWriteType
if ($knownIssue) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Known Issue with vmxnet3: 'Large packet loss at the guest operating system level on the VMXNET3 vNIC in ESXi (2039495)' - https://aka.ms/HC-VMwareLostPackets" `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 3 `
-AddHtmlDetailRow $false
}
}
if ($osInformation.NetworkInformation.NetworkAdapters.Count -gt 1) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Multiple active network adapters detected. Exchange 2013 or greater may not need separate adapters for MAPI and replication traffic. For details please refer to https://aka.ms/HC-PlanHA#network-requirements" `
-DisplayGroupingKey $keyNICSettings `
-AddHtmlDetailRow $false
}
if ($osInformation.NetworkInformation.IPv6DisabledOnNICs) {
$displayWriteType = "Grey"
$displayValue = "True"
$testingValue = $true
if ($osInformation.NetworkInformation.IPv6DisabledComponents -eq -1) {
$displayWriteType = "Red"
$testingValue = $false
$displayValue = "False `r`n`t`tError: IPv6 is disabled on some NIC level settings but not correctly disabled via DisabledComponents registry value. It is currently set to '-1'. `r`n`t`tThis setting cause a system startup delay of 5 seconds. For details please refer to: `r`n`t`thttps://aka.ms/HC-ConfigureIPv6"
} elseif ($osInformation.NetworkInformation.IPv6DisabledComponents -ne 255) {
$displayWriteType = "Red"
$testingValue = $false
$displayValue = "False `r`n`t`tError: IPv6 is disabled on some NIC level settings but not fully disabled. DisabledComponents registry value currently set to '{0}'. For details please refer to the following articles: `r`n`t`thttps://aka.ms/HC-DisableIPv6`r`n`t`thttps://aka.ms/HC-ConfigureIPv6" -f $osInformation.NetworkInformation.IPv6DisabledComponents
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Disable IPv6 Correctly" -Details $displayValue `
-DisplayGroupingKey $keyNICSettings `
-DisplayWriteType $displayWriteType `
-DisplayCustomTabNumber 1
}
}
Function Invoke-AnalyzerFrequentConfigurationIssues {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyFrequentConfigIssues = Get-DisplayResultsGroupingKey -Name "Frequent Configuration Issues" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
$osInformation = $HealthServerObject.OSInformation
$tcpKeepAlive = $osInformation.NetworkInformation.TCPKeepAlive
if ($tcpKeepAlive -eq 0) {
$displayValue = "Not Set `r`n`t`tError: Without this value the KeepAliveTime defaults to two hours, which can cause connectivity and performance issues between network devices such as firewalls and load balancers depending on their configuration. `r`n`t`tMore details: https://aka.ms/HC-TSPerformanceChecklist"
$displayWriteType = "Red"
} elseif ($tcpKeepAlive -lt 900000 -or
$tcpKeepAlive -gt 1800000) {
$displayValue = "{0} `r`n`t`tWarning: Not configured optimally, recommended value between 15 to 30 minutes (900000 and 1800000 decimal). `r`n`t`tMore details: https://aka.ms/HC-TSPerformanceChecklist" -f $tcpKeepAlive
$displayWriteType = "Yellow"
} else {
$displayValue = $tcpKeepAlive
$displayWriteType = "Green"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "TCP/IP Settings" -Details $displayValue `
-DisplayGroupingKey $keyFrequentConfigIssues `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue $tcpKeepAlive `
-HtmlName "TCPKeepAlive"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "RPC Min Connection Timeout" -Details ("{0} `r`n`t`tMore Information: https://aka.ms/HC-RPCSetting" -f $osInformation.NetworkInformation.RpcMinConnectionTimeout) `
-DisplayGroupingKey $keyFrequentConfigIssues `
-DisplayTestingValue $osInformation.NetworkInformation.RpcMinConnectionTimeout `
-HtmlName "RPC Minimum Connection Timeout"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "FIPS Algorithm Policy Enabled" -Details ($exchangeInformation.RegistryValues.FipsAlgorithmPolicyEnabled) `
-DisplayGroupingKey $keyFrequentConfigIssues `
-HtmlName "FipsAlgorithmPolicy-Enabled"
$displayValue = $exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage
$displayWriteType = "Green"
if ($exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage -ne 0) {
$displayWriteType = "Red"
$displayValue = "{0} `r`n`t`tError: This can cause an impact to the server's search performance. This should only be used a temporary fix if no other options are available vs a long term solution." -f $exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "CTS Processor Affinity Percentage" -Details $displayValue `
-DisplayGroupingKey $keyFrequentConfigIssues `
-DisplayWriteType $displayWriteType `
-DisplayTestingValue ($exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage) `
-HtmlName "CtsProcessorAffinityPercentage"
$displayValue = $osInformation.CredentialGuardEnabled
$displayWriteType = "Grey"
if ($osInformation.CredentialGuardEnabled) {
$displayValue = "{0} `r`n`t`tError: Credential Guard is not supported on an Exchange Server. This can cause a performance hit on the server." -f $osInformation.CredentialGuardEnabled
$displayWriteType = "Red"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Credential Guard Enabled" -Details $displayValue `
-DisplayGroupingKey $keyFrequentConfigIssues `
-DisplayTestingValue $osInformation.CredentialGuardEnabled `
-DisplayWriteType $displayWriteType
if ($null -ne $exchangeInformation.ApplicationConfigFileStatus -and
$exchangeInformation.ApplicationConfigFileStatus.Count -ge 1) {
foreach ($configKey in $exchangeInformation.ApplicationConfigFileStatus.Keys) {
$configStatus = $exchangeInformation.ApplicationConfigFileStatus[$configKey]
$writeType = "Green"
$writeName = "{0} Present" -f $configKey
$writeValue = $configStatus.Present
if (!$configStatus.Present) {
$writeType = "Red"
$writeValue = "{0} --- Error" -f $writeValue
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name $writeName -Details $writeValue `
-DisplayGroupingKey $keyFrequentConfigIssues `
-DisplayWriteType $writeType
}
}
}
Function Invoke-AnalyzerWebAppPools {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keyWebApps = Get-DisplayResultsGroupingKey -Name "Exchange Web App Pools" -DisplayOrder $Order
$exchangeInformation = $HealthServerObject.ExchangeInformation
if ($exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
Write-Verbose "Working on Exchange Web App GC Mode"
$outputObjectDisplayValue = New-Object System.Collections.Generic.List[object]
foreach ($webAppKey in $exchangeInformation.ApplicationPools.Keys) {
$appPool = $exchangeInformation.ApplicationPools[$webAppKey]
$appRestarts = $appPool.AppSettings.add.recycling.periodicRestart
$appRestartSet = ($appRestarts.PrivateMemory -ne "0" -or
$appRestarts.Memory -ne "0" -or
$appRestarts.Requests -ne "0" -or
$null -ne $appRestarts.Schedule -or
($appRestarts.Time -ne "00:00:00" -and
($webAppKey -ne "MSExchangeOWAAppPool" -and
$webAppKey -ne "MSExchangeECPAppPool")))
$outputObjectDisplayValue.Add(([PSCustomObject]@{
AppPoolName = $webAppKey
State = $appPool.AppSettings.state
GCServerEnabled = $appPool.GCServerEnabled
RestartConditionSet = $appRestartSet
})
)
}
$sbStarted = { param($o, $p) if ($p -eq "State") { if ($o."$p" -eq "Started") { "Green" } else { "Red" } } }
$sbRestart = { param($o, $p) if ($p -eq "RestartConditionSet") { if ($o."$p") { "Red" } else { "Green" } } }
$AnalyzeResults | Add-AnalyzedResultInformation -OutColumns ([PSCustomObject]@{
DisplayObject = $outputObjectDisplayValue
ColorizerFunctions = @($sbStarted, $sbRestart)
IndentSpaces = 8
}) `
-DisplayGroupingKey $keyWebApps `
-AddHtmlDetailRow $false
$periodicStartAppPools = $outputObjectDisplayValue | Where-Object { $_.RestartConditionSet -eq $true }
if ($null -ne $periodicStartAppPools) {
$outputObjectDisplayValue = New-Object System.Collections.Generic.List[object]
foreach ($appPool in $periodicStartAppPools) {
$periodicRestart = $exchangeInformation.ApplicationPools[$appPool.AppPoolName].AppSettings.add.recycling.periodicRestart
$schedule = $periodicRestart.Schedule
if ([string]::IsNullOrEmpty($schedule)) {
$schedule = "null"
}
$outputObjectDisplayValue.Add(([PSCustomObject]@{
AppPoolName = $appPool.AppPoolName
PrivateMemory = $periodicRestart.PrivateMemory
Memory = $periodicRestart.Memory
Requests = $periodicRestart.Requests
Schedule = $schedule
Time = $periodicRestart.Time
}))
}
$sbColorizer = {
param($o, $p)
switch ($p) {
{ $_ -in "PrivateMemory", "Memory", "Requests" } {
if ($o."$p" -eq "0") { "Green" } else { "Red" }
}
"Time" {
if ($o."$p" -eq "00:00:00") { "Green" } else { "Red" }
}
"Schedule" {
if ($o."$p" -eq "null") { "Green" } else { "Red" }
}
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -OutColumns ([PSCustomObject]@{
DisplayObject = $outputObjectDisplayValue
ColorizerFunctions = @($sbColorizer)
IndentSpaces = 8
}) `
-DisplayGroupingKey $keyWebApps `
-AddHtmlDetailRow $false
$AnalyzeResults | Add-AnalyzedResultInformation -Details "Error: The above app pools currently have the periodic restarts set. This restart will cause disruption to end users." `
-DisplayGroupingKey $keyWebApps `
-DisplayWriteType "Red" `
-AddHtmlDetailRow $false
}
}
}
Function Invoke-AnalyzerSecurityExchangeCertificates {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$exchangeInformation = $HealthServerObject.ExchangeInformation
foreach ($certificate in $exchangeInformation.ExchangeCertificates) {
if ($certificate.LifetimeInDays -ge 60) {
$displayColor = "Green"
} elseif ($certificate.LifetimeInDays -ge 30) {
$displayColor = "Yellow"
} else {
$displayColor = "Red"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Certificate" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 1
$AnalyzeResults | Add-AnalyzedResultInformation -Name "FriendlyName" -Details $certificate.FriendlyName `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Thumbprint" -Details $certificate.Thumbprint `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Lifetime in days" -Details $certificate.LifetimeInDays `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $displayColor
if ($certificate.LifetimeInDays -lt 0) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Certificate has expired" -Details $true `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Certificate has expired" -Details $false `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
$certStatusWriteType = [string]::Empty
if ($null -ne $certificate.Status) {
Switch ($certificate.Status) {
("Unknown") { $certStatusWriteType = "Yellow" }
("Valid") { $certStatusWriteType = "Grey" }
("Revoked") { $certStatusWriteType = "Red" }
("DateInvalid") { $certStatusWriteType = "Red" }
("Untrusted") { $certStatusWriteType = "Yellow" }
("Invalid") { $certStatusWriteType = "Red" }
("RevocationCheckFailure") { $certStatusWriteType = "Yellow" }
("PendingRequest") { $certStatusWriteType = "Yellow" }
default { $certStatusWriteType = "Yellow" }
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Certificate status" -Details $certificate.Status `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $certStatusWriteType
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Certificate status" -Details "Unknown" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
if ($certificate.PublicKeySize -lt 2048) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Key size" -Details $certificate.PublicKeySize `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
$AnalyzeResults | Add-AnalyzedResultInformation -Details "It's recommended to use a key size of at least 2048 bit" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Key size" -Details $certificate.PublicKeySize `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
if ($certificate.SignatureHashAlgorithmSecure -eq 1) {
$shaDisplayWriteType = "Yellow"
} else {
$shaDisplayWriteType = "Grey"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Signature Algorithm" -Details $certificate.SignatureAlgorithm `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $shaDisplayWriteType
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Signature Hash Algorithm" -Details $certificate.SignatureHashAlgorithm `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $shaDisplayWriteType
if ($shaDisplayWriteType -eq "Yellow") {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "It's recommended to use a hash algorithm from the SHA-2 family `r`n`t`tMore information: https://aka.ms/HC-SSLBP" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $shaDisplayWriteType
}
if ($null -ne $certificate.Services) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Bound to services" -Details $certificate.Services `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
if ($exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Current Auth Certificate" -Details $certificate.IsCurrentAuthConfigCertificate `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SAN Certificate" -Details $certificate.IsSanCertificate `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Namespaces" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
foreach ($namespace in $certificate.Namespaces) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $namespace `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 3
}
if ($certificate.IsCurrentAuthConfigCertificate -eq $true) {
$currentAuthCertificate = $certificate
}
}
if ($null -ne $currentAuthCertificate) {
if ($currentAuthCertificate.LifetimeInDays -gt 0) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $true `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 1 `
-DisplayWriteType "Green"
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $false `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 1 `
-DisplayWriteType "Red"
$renewExpiredAuthCert = "Auth Certificate has expired `r`n`t`tMore Information: https://aka.ms/HC-OAuthExpired"
$AnalyzeResults | Add-AnalyzedResultInformation -Details $renewExpiredAuthCert `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
}
} elseif ($exchangeInformation.BuildInformation.ServerRole -eq [HealthChecker.ExchangeServerRole]::Edge) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $false `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 1
$AnalyzeResults | Add-AnalyzedResultInformation -Details "We can't check for Auth Certificates on Edge Transport Servers" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $false `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 1 `
-DisplayWriteType "Red"
$createNewAuthCert = "No valid Auth Certificate found. This may cause several problems. `r`n`t`tMore Information: https://aka.ms/HC-FindOAuthHybrid"
$AnalyzeResults | Add-AnalyzedResultInformation -Details $createNewAuthCert `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red"
}
}
Function Invoke-AnalyzerSecurityMitigationService {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$exchangeInformation = $HealthServerObject.ExchangeInformation
$exchangeCU = $exchangeInformation.BuildInformation.CU
$mitigationService = $exchangeInformation.ExchangeEmergencyMitigationService
#Description: Check for Exchange Emergency Mitigation Service (EEMS)
#Introduced in: Exchange 2016 CU22, Exchange 2019 CU11
if (((($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) -and
($exchangeCU -ge [HealthChecker.ExchangeCULevel]::CU22)) -or
(($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) -and
($exchangeCU -ge [HealthChecker.ExchangeCULevel]::CU11))) -and
$exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
if (-not([String]::IsNullOrEmpty($mitigationService.MitigationServiceOrgState))) {
if (($mitigationService.MitigationServiceOrgState) -and
($mitigationService.MitigationServiceSrvState)) {
$eemsWriteType = "Green"
$eemsOveralState = "Enabled"
} elseif (($mitigationService.MitigationServiceOrgState -eq $false) -and
($mitigationService.MitigationServiceSrvState)) {
$eemsWriteType = "Yellow"
$eemsOveralState = "Disabled on org level"
} elseif (($mitigationService.MitigationServiceSrvState -eq $false) -and
($mitigationService.MitigationServiceOrgState)) {
$eemsWriteType = "Yellow"
$eemsOveralState = "Disabled on server level"
} else {
$eemsWriteType = "Yellow"
$eemsOveralState = "Disabled"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Exchange Emergency Mitigation Service" -Details $eemsOveralState `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType $eemsWriteType
$eemsWinSrvWriteType = "Yellow"
if (-not([String]::IsNullOrEmpty($mitigationService.MitigationWinServiceState))) {
if ($mitigationService.MitigationWinServiceState -eq "Running") {
$eemsWinSrvWriteType = "Grey"
}
$details = $mitigationService.MitigationWinServiceState
} else {
$details = "Unknown"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Windows service" -Details $details `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $eemsWinSrvWriteType
if ($mitigationService.MitigationServiceEndpoint -eq 200) {
$eemsPatternServiceWriteType = "Grey"
$eemsPatternServiceStatus = ("{0} - Reachable" -f $mitigationService.MitigationServiceEndpoint)
} else {
$eemsPatternServiceWriteType = "Yellow"
$eemsPatternServiceStatus = "Unreachable`r`n`t`tMore information: https://aka.ms/HelpConnectivityEEMS"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Pattern service" -Details $eemsPatternServiceStatus `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType $eemsPatternServiceWriteType
if (-not([String]::IsNullOrEmpty($mitigationService.MitigationsApplied))) {
foreach ($mitigationApplied in $mitigationService.MitigationsApplied) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Mitigation applied" -Details $mitigationApplied `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
$AnalyzeResults | Add-AnalyzedResultInformation -Details ("Run: 'Get-Mitigations.ps1' from: '{0}' to learn more." -f $exscripts) `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
if (-not([String]::IsNullOrEmpty($mitigationService.MitigationsBlocked))) {
foreach ($mitigationBlocked in $mitigationService.MitigationsBlocked) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Mitigation blocked" -Details $mitigationBlocked `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow"
}
}
if (-not([String]::IsNullOrEmpty($mitigationService.DataCollectionEnabled))) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Telemetry enabled" -Details $mitigationService.DataCollectionEnabled `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayCustomTabNumber 2
}
} else {
Write-Verbose "Unable to validate Exchange Emergency Mitigation Service state"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Exchange Emergency Mitigation Service" -Details "Failed to query config" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType "Red"
}
} else {
Write-Verbose "Exchange Emergency Mitigation Service feature not available because we are on: $($exchangeInformation.BuildInformation.MajorVersion) $exchangeCU or on Edge Transport Server"
}
}
Function Invoke-AnalyzerSecuritySettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keySecuritySettings = Get-DisplayResultsGroupingKey -Name "Security Settings" -DisplayOrder $Order
$osInformation = $HealthServerObject.OSInformation
$AnalyzeResults | Add-AnalyzedResultInformation -Name "LmCompatibilityLevel Settings" -Details ($osInformation.LmCompatibility.RegistryValue) `
-DisplayGroupingKey $keySecuritySettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Description" -Details ($osInformation.LmCompatibility.Description) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
##############
# TLS Settings
##############
Write-Verbose "Working on TLS Settings"
$tlsVersions = @("1.0", "1.1", "1.2")
$currentNetVersion = $osInformation.TLSSettings["NETv4"]
function TestTlsValue {
param(
[string]$Name,
[int]$Value,
[string]$Key
)
if ($Value -ne 0 -and
$Value -ne 1) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name $Name -Details "$Value --- Error: Must be a value of 1 or 0." `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Red" `
-TestingName "TLS $key - $Name" `
-DisplayTestingValue $Value
}
}
foreach ($tlsKey in $tlsVersions) {
$currentTlsVersion = $osInformation.TLSSettings[$tlsKey]
$AnalyzeResults | Add-AnalyzedResultInformation -Details ("TLS {0}" -f $tlsKey) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 1
$AnalyzeResults | Add-AnalyzedResultInformation -Name ("Server Enabled") -Details ($currentTlsVersion.ServerEnabled) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2
TestTlsValue -Name "Server Enabled Value" -Value $currentTlsVersion.ServerEnabledValue -Key $tlsKey
$AnalyzeResults | Add-AnalyzedResultInformation -Name ("Server Disabled By Default") -Details ($currentTlsVersion.ServerDisabledByDefault) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2
TestTlsValue -Name "Server Disabled By Default Value" -Value $currentTlsVersion.ServerDisabledByDefaultValue -Key $tlsKey
$AnalyzeResults | Add-AnalyzedResultInformation -Name ("Client Enabled") -Details ($currentTlsVersion.ClientEnabled) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2
TestTlsValue -Name "Client Enabled Value" -Value $currentTlsVersion.ClientEnabledValue -Key $tlsKey
$AnalyzeResults | Add-AnalyzedResultInformation -Name ("Client Disabled By Default") -Details ($currentTlsVersion.ClientDisabledByDefault) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2
TestTlsValue -Name "Client Disabled By Default Value" -Value $currentTlsVersion.ClientDisabledByDefaultValue -Key $tlsKey
if ($currentTlsVersion.ServerEnabled -ne $currentTlsVersion.ClientEnabled) {
$detectedTlsMismatch = $true
$AnalyzeResults | Add-AnalyzedResultInformation -Details ("Error: Mismatch in TLS version for client and server. Exchange can be both client and a server. This can cause issues within Exchange for communication.") `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 3 `
-TestingName "TLS $tlsKey - Mismatch" `
-DisplayTestingValue $true `
-DisplayWriteType "Red"
}
if (($tlsKey -eq "1.0" -or
$tlsKey -eq "1.1") -and (
$currentTlsVersion.ServerEnabled -eq $false -or
$currentTlsVersion.ClientEnabled -eq $false -or
$currentTlsVersion.ServerDisabledByDefault -or
$currentTlsVersion.ClientDisabledByDefault) -and
($currentNetVersion.SystemDefaultTlsVersions -eq $false -or
$currentNetVersion.WowSystemDefaultTlsVersions -eq $false)) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details ("Error: SystemDefaultTlsVersions is not set to the recommended value. Please visit on how to properly enable TLS 1.2 https://aka.ms/HC-TLSPart2") `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 3 `
-TestingName "TLS $tlsKey - SystemDefaultTlsVersions Error" `
-DisplayTestingValue $true `
-DisplayWriteType "Red"
}
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SystemDefaultTlsVersions" -Details ($currentNetVersion.SystemDefaultTlsVersions) `
-DisplayGroupingKey $keySecuritySettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SystemDefaultTlsVersions - Wow6432Node" -Details ($currentNetVersion.WowSystemDefaultTlsVersions) `
-DisplayGroupingKey $keySecuritySettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SchUseStrongCrypto" -Details ($currentNetVersion.SchUseStrongCrypto) `
-DisplayGroupingKey $keySecuritySettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SchUseStrongCrypto - Wow6432Node" -Details ($currentNetVersion.WowSchUseStrongCrypto) `
-DisplayGroupingKey $keySecuritySettings
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SecurityProtocol" -Details ($currentNetVersion.SecurityProtocol) `
-DisplayGroupingKey $keySecuritySettings
<#
[array]$securityProtocols = $currentNetVersion.SecurityProtocol.Split(",").Trim().ToUpper()
$lowerTLSVersions = @("1.0", "1.1")
foreach ($tlsKey in $lowerTLSVersions) {
$currentTlsVersion = $osInformation.TLSSettings[$tlsKey]
$securityProtocolCheck = "TLS"
if ($tlsKey -eq "1.1") {
$securityProtocolCheck = "TLS11"
}
if (($currentTlsVersion.ServerEnabled -eq $false -or
$currentTlsVersion.ClientEnabled -eq $false) -and
$securityProtocols.Contains($securityProtocolCheck)) {
$AnalyzeResults = Add-AnalyzedResultInformation -Details ("Security Protocol is able to use TLS when we have TLS {0} disabled in the registry. This can cause issues with connectivity. It is recommended to follow the proper TLS settings. In some cases, it may require to also set SchUseStrongCrypto in the registry." -f $tlsKey) `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2 `
-DisplayWriteType "Yellow" `
-AnalyzedInformation $AnalyzeResults
}
}
#>
if ($detectedTlsMismatch) {
$displayValues = @("Exchange Server TLS guidance Part 1: Getting Ready for TLS 1.2: https://aka.ms/HC-TLSPart1",
"Exchange Server TLS guidance Part 2: Enabling TLS 1.2 and Identifying Clients Not Using It: https://aka.ms/HC-TLSPart2",
"Exchange Server TLS guidance Part 3: Turning Off TLS 1.0/1.1: https://aka.ms/HC-TLSPart3")
$AnalyzeResults | Add-AnalyzedResultInformation -Details "For More Information on how to properly set TLS follow these blog posts:" `
-DisplayGroupingKey $keySecuritySettings `
-DisplayCustomTabNumber 2 `
-TestingName "Detected TLS Mismatch Display More Info" `
-DisplayTestingValue $true `
-DisplayWriteType "Yellow"
foreach ($displayValue in $displayValues) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details $displayValue `
-DisplayGroupingKey $keySecuritySettings `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 3
}
}
$additionalDisplayValue = [string]::Empty
$smb1Settings = $osInformation.Smb1ServerSettings
if ($osInformation.BuildInformation.MajorVersion -gt [HealthChecker.OSServerVersion]::Windows2012) {
$displayValue = "False"
$writeType = "Green"
if (-not ($smb1Settings.SuccessfulGetInstall)) {
$displayValue = "Failed to get install status"
$writeType = "Yellow"
} elseif ($smb1Settings.Installed) {
$displayValue = "True"
$writeType = "Red"
$additionalDisplayValue = "SMB1 should be uninstalled"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SMB1 Installed" -Details $displayValue `
-DisplayGroupingKey $keySecuritySettings `
-DisplayWriteType $writeType
}
$writeType = "Green"
$displayValue = "True"
if (-not ($smb1Settings.SuccessfulGetBlocked)) {
$displayValue = "Failed to get block status"
$writeType = "Yellow"
} elseif (-not($smb1Settings.IsBlocked)) {
$displayValue = "False"
$writeType = "Red"
$additionalDisplayValue += " SMB1 should be blocked"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "SMB1 Blocked" -Details $displayValue `
-DisplayGroupingKey $keySecuritySettings `
-DisplayWriteType $writeType
if ($additionalDisplayValue -ne [string]::Empty) {
$additionalDisplayValue += "`r`n`t`tMore Information: https://aka.ms/HC-SMB1"
$AnalyzeResults | Add-AnalyzedResultInformation -Details $additionalDisplayValue.Trim() `
-DisplayGroupingKey $keySecuritySettings `
-DisplayWriteType "Yellow" `
-DisplayCustomTabNumber 2 `
-AddHtmlDetailRow $false
}
Invoke-AnalyzerSecurityExchangeCertificates -AnalyzeResults $AnalyzeResults -HealthServerObject $HealthServerObject -DisplayGroupingKey $keySecuritySettings
Invoke-AnalyzerSecurityMitigationService -AnalyzeResults $AnalyzeResults -HealthServerObject $HealthServerObject -DisplayGroupingKey $keySecuritySettings
}
Function Invoke-AnalyzerSecurityCve-2020-0796 {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$SecurityObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Description: Check for CVE-2020-0796 SMBv3 vulnerability
#Affected OS versions: Windows 10 build 1903 and 1909
#Fix: KB4551762
#Workaround: Disable SMBv3 compression
if ($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) {
Write-Verbose "Testing CVE: CVE-2020-0796"
$buildNumber = $SecurityObject.OsInformation.BuildInformation.VersionBuild.Split(".")[2]
if (($buildNumber -eq 18362 -or
$buildNumber -eq 18363) -and
($SecurityObject.OsInformation.RegistryValues.CurrentVersionUbr -lt 720)) {
Write-Verbose "Build vulnerable to CVE-2020-0796. Checking if workaround is in place."
$writeType = "Red"
$writeValue = "System Vulnerable"
if ($SecurityObject.OsInformation.RegistryValues.LanManServerDisabledCompression -eq 1) {
Write-Verbose "Workaround to disable affected SMBv3 compression is in place."
$writeType = "Yellow"
$writeValue = "Workaround is in place"
} else {
Write-Verbose "Workaround to disable affected SMBv3 compression is NOT in place."
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "CVE-2020-0796" `
-Details "$writeValue`r`n`t`tSee: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0796 for more information." `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType $writeType `
-DisplayTestingValue "CVE-2020-0796" `
-AddHtmlDetailRow $false
} else {
Write-Verbose "System NOT vulnerable to CVE-2020-0796. Information URL: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0796"
}
} else {
Write-Verbose "Operating System NOT vulnerable to CVE-2020-0796."
}
}
Function Invoke-AnalyzerSecurityCve-2020-1147 {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$SecurityObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Description: Check for CVE-2020-1147
#Affected OS versions: Every OS supporting .NET Core 2.1 and 3.1 and .NET Framework 2.0 SP2 or above
#Fix: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1147
#Workaround: N/A
$dllFileBuildPartToCheckAgainst = 3630
if ($SecurityObject.OsInformation.NETFramework.NetMajorVersion -eq [HealthChecker.NetMajorVersion]::Net4d8) {
$dllFileBuildPartToCheckAgainst = 4190
}
$systemDataDll = $SecurityObject.OsInformation.NETFramework.FileInformation["System.Data.dll"]
$systemConfigurationDll = $SecurityObject.OsInformation.NETFramework.FileInformation["System.Configuration.dll"]
Write-Verbose "System.Data.dll FileBuildPart: $($systemDataDll.VersionInfo.FileBuildPart) | LastWriteTimeUtc: $($systemDataDll.LastWriteTimeUtc)"
Write-Verbose "System.Configuration.dll FileBuildPart: $($systemConfigurationDll.VersionInfo.FileBuildPart) | LastWriteTimeUtc: $($systemConfigurationDll.LastWriteTimeUtc)"
if ($systemDataDll.VersionInfo.FileBuildPart -ge $dllFileBuildPartToCheckAgainst -and
$systemConfigurationDll.VersionInfo.FileBuildPart -ge $dllFileBuildPartToCheckAgainst -and
$systemDataDll.LastWriteTimeUtc -ge ([System.Convert]::ToDateTime("06/05/2020", [System.Globalization.DateTimeFormatInfo]::InvariantInfo)) -and
$systemConfigurationDll.LastWriteTimeUtc -ge ([System.Convert]::ToDateTime("06/05/2020", [System.Globalization.DateTimeFormatInfo]::InvariantInfo))) {
Write-Verbose ("System NOT vulnerable to {0}. Information URL: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/{0}" -f "CVE-2020-1147")
} else {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerability" `
-Details ("{0}`r`n`t`tSee: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/{0} for more information." -f "CVE-2020-1147") `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType "Red" `
-DisplayTestingValue "CVE-2020-1147" `
-AddHtmlDetailRow $false
}
}
Function Invoke-AnalyzerSecurityCve-2021-1730 {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$SecurityObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Description: Check for CVE-2021-1730 vulnerability
#Fix available for: Exchange 2016 CU18+, Exchange 2019 CU7+
#Fix: Configure Download Domains feature
#Workaround: N/A
if (((($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) -and
($SecurityObject.CU -ge [HealthChecker.ExchangeCULevel]::CU18)) -or
(($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) -and
($SecurityObject.CU -ge [HealthChecker.ExchangeCULevel]::CU7))) -and
$SecurityObject.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
$downloadDomainsEnabled = $SecurityObject.ExchangeInformation.EnableDownloadDomains
$owaVDirObject = $SecurityObject.ExchangeInformation.GetOwaVirtualDirectory
$displayWriteType = "Green"
if (-not ($downloadDomainsEnabled)) {
$downloadDomainsOrgDisplayValue = "Download Domains are not configured. You should configure them to be protected against CVE-2021-1730.`r`n`t`tConfiguration instructions: https://aka.ms/HC-DownloadDomains"
$displayWriteType = "Red"
} else {
if (-not ([String]::IsNullOrEmpty($OwaVDirObject.ExternalDownloadHostName))) {
if (($OwaVDirObject.ExternalDownloadHostName -eq $OwaVDirObject.ExternalUrl.Host) -or
($OwaVDirObject.ExternalDownloadHostName -eq $OwaVDirObject.InternalUrl.Host)) {
$downloadExternalDisplayValue = "Set to the same as Internal Or External URL as OWA."
$displayWriteType = "Red"
} else {
$downloadExternalDisplayValue = "Set Correctly."
}
} else {
$downloadExternalDisplayValue = "Not Configured"
$displayWriteType = "Red"
}
if (-not ([string]::IsNullOrEmpty($owaVDirObject.InternalDownloadHostName))) {
if (($OwaVDirObject.InternalDownloadHostName -eq $OwaVDirObject.ExternalUrl.Host) -or
($OwaVDirObject.InternalDownloadHostName -eq $OwaVDirObject.InternalUrl.Host)) {
$downloadInternalDisplayValue = "Set to the same as Internal Or External URL as OWA."
$displayWriteType = "Red"
} else {
$downloadInternalDisplayValue = "Set Correctly."
}
} else {
$displayWriteType = "Red"
$downloadInternalDisplayValue = "Not Configured"
}
$downloadDomainsOrgDisplayValue = "Download Domains are configured.`r`n`t`tExternalDownloadHostName: $downloadExternalDisplayValue`r`n`t`tInternalDownloadHostName: $downloadInternalDisplayValue`r`n`t`tConfiguration instructions: https://aka.ms/HC-DownloadDomains"
}
#Only display security vulnerability if present
if ($displayWriteType -eq "Red") {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerability" -Details $downloadDomainsOrgDisplayValue `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType "Red" `
-TestingName "CVE-2021-1730" `
-DisplayTestingValue ([PSCustomObject]@{
DownloadDomainsEnabled = $downloadDomainsEnabled
ExternalDownloadHostName = $downloadExternalDisplayValue
InternalDownloadHostName = $downloadInternalDisplayValue
}) `
-AddHtmlDetailRow $false
}
} else {
Write-Verbose "Download Domains feature not available because we are on: $($SecurityObject.MajorVersion) $($SecurityObject.CU) or on Edge Transport Server"
}
}
Function Invoke-AnalyzerSecurityCve-2021-34470 {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$SecurityObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Description: Check for CVE-2021-34470 rights elevation vulnerability
#Affected Exchange versions: 2013, 2016, 2019
#Fix:
##Exchange 2013 CU23 + July 2021 SU + /PrepareSchema,
##Exchange 2016 CU20 + July 2021 SU + /PrepareSchema or CU21,
##Exchange 2019 CU9 + July 2021 SU + /PrepareSchema or CU10
#Workaround: N/A
if (($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) -or
(($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) -and
($SecurityObject.CU -lt [HealthChecker.ExchangeCULevel]::CU21)) -or
(($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) -and
($SecurityObject.CU -lt [HealthChecker.ExchangeCULevel]::CU10))) {
Write-Verbose "Testing CVE: CVE-2021-34470"
$displayWriteTypeColor = $null
if ($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $SecurityObject.BuildRevision -SecurityFixedBuilds "1497.23" -CVENames "CVE-2021-34470"
}
if ($null -eq $SecurityObject.ExchangeInformation.msExchStorageGroup) {
Write-Verbose "Unable to query classSchema: 'ms-Exch-Storage-Group' information"
$details = "CVE-2021-34470`r`n`t`tWarning: Unable to query classSchema: 'ms-Exch-Storage-Group' to perform testing."
$displayWriteTypeColor = "Yellow"
} elseif ($SecurityObject.ExchangeInformation.msExchStorageGroup.Properties.posssuperiors -eq "computer") {
Write-Verbose "Attribute: 'possSuperiors' with value: 'computer' detected in classSchema: 'ms-Exch-Storage-Group'"
$details = "CVE-2021-34470`r`n`t`tPrepareSchema required: https://aka.ms/HC-July21SU"
$displayWriteTypeColor = "Red"
} else {
Write-Verbose "System NOT vulnerable to CVE-2021-34470"
}
if ($null -ne $displayWriteTypeColor) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerability" -Details $details `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayWriteType $displayWriteTypeColor `
-DisplayTestingValue "CVE-2021-34470" `
-AddHtmlDetailRow $false
}
} else {
Write-Verbose "System NOT vulnerable to CVE-2021-34470"
}
}
Function Invoke-AnalyzerSecurityCve-MarchSuSpecial {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$SecurityObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Description: March 2021 Exchange vulnerabilities Security Update (SU) check for outdated version (CUs)
#Affected Exchange versions: Exchange 2013, Exchange 2016, Exchange 2016 (we only provide this special SU for these versions)
#Fix: Update to a supported CU and apply KB5000871
if (($SecurityObject.ExchangeInformation.BuildInformation.March2021SUInstalled) -and
($SecurityObject.ExchangeInformation.BuildInformation.SupportedBuild -eq $false)) {
if (($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) -and
($SecurityObject.CU -lt [HealthChecker.ExchangeCULevel]::CU23)) {
Switch ($SecurityObject.CU) {
([HealthChecker.ExchangeCULevel]::CU21) { $KBCveComb = @{KB4340731 = "CVE-2018-8302"; KB4459266 = "CVE-2018-8265", "CVE-2018-8448"; KB4471389 = "CVE-2019-0586", "CVE-2019-0588" } }
([HealthChecker.ExchangeCULevel]::CU22) { $KBCveComb = @{KB4487563 = "CVE-2019-0817", "CVE-2019-0858"; KB4503027 = "ADV190018" } }
}
} elseif (($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) -and
($SecurityObject.CU -lt [HealthChecker.ExchangeCULevel]::CU18)) {
Switch ($SecurityObject.CU) {
([HealthChecker.ExchangeCULevel]::CU8) { $KBCveComb = @{KB4073392 = "CVE-2018-0924", "CVE-2018-0940", "CVE-2018-0941"; KB4092041 = "CVE-2018-8151", "CVE-2018-8152", "CVE-2018-8153", "CVE-2018-8154", "CVE-2018-8159" } }
([HealthChecker.ExchangeCULevel]::CU9) { $KBCveComb = @{KB4092041 = "CVE-2018-8151", "CVE-2018-8152", "CVE-2018-8153", "CVE-2018-8154", "CVE-2018-8159"; KB4340731 = "CVE-2018-8374", "CVE-2018-8302" } }
([HealthChecker.ExchangeCULevel]::CU10) { $KBCveComb = @{KB4340731 = "CVE-2018-8374", "CVE-2018-8302"; KB4459266 = "CVE-2018-8265", "CVE-2018-8448"; KB4468741 = "CVE-2018-8604"; KB4471389 = "CVE-2019-0586", "CVE-2019-0588" } }
([HealthChecker.ExchangeCULevel]::CU11) { $KBCveComb = @{KB4468741 = "CVE-2018-8604"; KB4471389 = "CVE-2019-0586", "CVE-2019-0588"; KB4487563 = "CVE-2019-0817", "CVE-2018-0858"; KB4503027 = "ADV190018" } }
([HealthChecker.ExchangeCULevel]::CU12) { $KBCveComb = @{KB4487563 = "CVE-2019-0817", "CVE-2018-0858"; KB4503027 = "ADV190018"; KB4515832 = "CVE-2019-1233", "CVE-2019-1266" } }
([HealthChecker.ExchangeCULevel]::CU13) { $KBCveComb = @{KB4509409 = "CVE-2019-1084", "CVE-2019-1136", "CVE-2019-1137"; KB4515832 = "CVE-2019-1233", "CVE-2019-1266"; KB4523171 = "CVE-2019-1373" } }
([HealthChecker.ExchangeCULevel]::CU14) { $KBCveComb = @{KB4523171 = "CVE-2019-1373"; KB4536987 = "CVE-2020-0688", "CVE-2020-0692"; KB4540123 = "CVE-2020-0903" } }
([HealthChecker.ExchangeCULevel]::CU15) { $KBCveComb = @{KB4536987 = "CVE-2020-0688", "CVE-2020-0692"; KB4540123 = "CVE-2020-0903" } }
([HealthChecker.ExchangeCULevel]::CU16) { $KBCveComb = @{KB4577352 = "CVE-2020-16875" } }
([HealthChecker.ExchangeCULevel]::CU17) { $KBCveComb = @{KB4577352 = "CVE-2020-16875"; KB4581424 = "CVE-2020-16969"; KB4588741 = "CVE-2020-17083", "CVE-2020-17084", "CVE-2020-17085"; KB4593465 = "CVE-2020-17117", "CVE-2020-17132", "CVE-2020-17141", "CVE-2020-17142", "CVE-2020-17143" } }
}
} elseif (($SecurityObject.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) -and
($SecurityObject.CU -lt [HealthChecker.ExchangeCULevel]::CU7)) {
Switch ($SecurityObject.CU) {
([HealthChecker.ExchangeCULevel]::RTM) { $KBCveComb = @{KB4471389 = "CVE-2019-0586", "CVE-2019-0588"; KB4487563 = "CVE-2019-0817", "CVE-2019-0858"; KB4503027 = "ADV190018" } }
([HealthChecker.ExchangeCULevel]::CU1) { $KBCveComb = @{KB4487563 = "CVE-2019-0817", "CVE-2019-0858"; KB4503027 = "ADV190018"; KB4509409 = "CVE-2019-1084", "CVE-2019-1137"; KB4515832 = "CVE-2019-1233", "CVE-2019-1266" } }
([HealthChecker.ExchangeCULevel]::CU2) { $KBCveComb = @{KB4509409 = "CVE-2019-1084", "CVE-2019-1137"; KB4515832 = "CVE-2019-1233", "CVE-2019-1266"; KB4523171 = "CVE-2019-1373" } }
([HealthChecker.ExchangeCULevel]::CU3) { $KBCveComb = @{KB4523171 = "CVE-2019-1373"; KB4536987 = "CVE-2020-0688", "CVE-2020-0692"; KB4540123 = "CVE-2020-0903" } }
([HealthChecker.ExchangeCULevel]::CU4) { $KBCveComb = @{KB4536987 = "CVE-2020-0688", "CVE-2020-0692"; KB4540123 = "CVE-2020-0903" } }
([HealthChecker.ExchangeCULevel]::CU5) { $KBCveComb = @{KB4577352 = "CVE-2020-16875" } }
([HealthChecker.ExchangeCULevel]::CU6) { $KBCveComb = @{KB4577352 = "CVE-2020-16875"; KB4581424 = "CVE-2020-16969"; KB4588741 = "CVE-2020-17083", "CVE-2020-17084", "CVE-2020-17085"; KB4593465 = "CVE-2020-17117", "CVE-2020-17132", "CVE-2020-17141", "CVE-2020-17142", "CVE-2020-17143" } }
}
} else {
Write-Verbose "No need to call 'Show-March2021SUOutdatedCUWarning'"
}
if ($null -ne $KBCveComb) {
foreach ($kbName in $KBCveComb.Keys) {
foreach ($cveName in $KBCveComb[$kbName]) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "March 2021 Exchange Security Update for unsupported CU detected" `
-Details "`r`n`t`tPlease make sure $kbName is installed to be fully protected against: $cveName" `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayTestingValue $cveName `
-DisplayWriteType "Yellow" `
-AddHtmlDetailRow $false
}
}
}
}
}
Function Invoke-AnalyzerSecurityCveCheck {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[object]$DisplayGroupingKey
)
Function TestVulnerabilitiesByBuildNumbersForDisplay {
param(
[Parameter(Mandatory = $true)][string]$ExchangeBuildRevision,
[Parameter(Mandatory = $true)][array]$SecurityFixedBuilds,
[Parameter(Mandatory = $true)][array]$CVENames
)
[int]$fileBuildPart = ($split = $ExchangeBuildRevision.Split("."))[0]
[int]$filePrivatePart = $split[1]
$Script:breakpointHit = $false
foreach ($securityFixedBuild in $SecurityFixedBuilds) {
[int]$securityFixedBuildPart = ($split = $securityFixedBuild.Split("."))[0]
[int]$securityFixedPrivatePart = $split[1]
if ($fileBuildPart -eq $securityFixedBuildPart) {
$Script:breakpointHit = $true
}
if (($fileBuildPart -lt $securityFixedBuildPart) -or
($fileBuildPart -eq $securityFixedBuildPart -and
$filePrivatePart -lt $securityFixedPrivatePart)) {
foreach ($cveName in $CVENames) {
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerability" `
-Details ("{0}`r`n`t`tSee: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/{0} for more information." -f $cveName) `
-DisplayGroupingKey $DisplayGroupingKey `
-DisplayTestingValue $cveName `
-DisplayWriteType "Red" `
-AddHtmlDetailRow $false
}
break
}
if ($Script:breakpointHit) {
break
}
}
}
$exchangeInformation = $HealthServerObject.ExchangeInformation
$osInformation = $HealthServerObject.OSInformation
[string]$buildRevision = ("{0}.{1}" -f $exchangeInformation.BuildInformation.ExchangeSetup.FileBuildPart, `
$exchangeInformation.BuildInformation.ExchangeSetup.FilePrivatePart)
$exchangeCU = $exchangeInformation.BuildInformation.CU
Write-Verbose "Exchange Build Revision: $buildRevision"
Write-Verbose "Exchange CU: $exchangeCU"
if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) {
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU19) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1347.5", "1365.3" `
-CVENames "CVE-2018-0924", "CVE-2018-0940"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU20) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1365.7", "1367.6" `
-CVENames "CVE-2018-8151", "CVE-2018-8154", "CVE-2018-8159"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU21) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1367.9", "1395.7" `
-CVENames "CVE-2018-8302"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1395.8" `
-CVENames "CVE-2018-8265", "CVE-2018-8448"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1395.10" `
-CVENames "CVE-2019-0586", "CVE-2019-0588"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU22) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1473.3" `
-CVENames "CVE-2019-0686", "CVE-2019-0724"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1473.4" `
-CVENames "CVE-2019-0817", "CVE-2019-0858"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1473.5" `
-CVENames "ADV190018"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU23) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.3" `
-CVENames "CVE-2019-1084", "CVE-2019-1136", "CVE-2019-1137"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.4" `
-CVENames "CVE-2019-1373"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.6" `
-CVENames "CVE-2020-0688", "CVE-2020-0692"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.7" `
-CVENames "CVE-2020-16969"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.8" `
-CVENames "CVE-2020-17083", "CVE-2020-17084", "CVE-2020-17085"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.10" `
-CVENames "CVE-2020-17117", "CVE-2020-17132", "CVE-2020-17142", "CVE-2020-17143"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1395.12", "1473.6", "1497.12" `
-CVENames "CVE-2021-26855", "CVE-2021-26857", "CVE-2021-26858", "CVE-2021-27065"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.12" `
-CVENames "CVE-2021-26412", "CVE-2021-27078", "CVE-2021-26854"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.15" `
-CVENames "CVE-2021-28480", "CVE-2021-28481", "CVE-2021-28482", "CVE-2021-28483"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.18" `
-CVENames "CVE-2021-31195", "CVE-2021-31198", "CVE-2021-31207", "CVE-2021-31209"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.23" `
-CVENames "CVE-2021-31206", "CVE-2021-31196", "CVE-2021-33768"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.24" `
-CVENames "CVE-2021-26427"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1497.26" `
-CVENames "CVE-2021-42305", "CVE-2021-41349"
}
} elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) {
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU8) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1261.39", "1415.4" `
-CVENames "CVE-2018-0924", "CVE-2018-0940", "CVE-2018-0941"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU9) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1415.7", "1466.8" `
-CVENames "CVE-2018-8151", "CVE-2018-8152", "CVE-2018-8153", "CVE-2018-8154", "CVE-2018-8159"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU10) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1466.9", "1531.6" `
-CVENames "CVE-2018-8374", "CVE-2018-8302"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1531.8" `
-CVENames "CVE-2018-8265", "CVE-2018-8448"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU11) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1531.8", "1591.11" `
-CVENames "CVE-2018-8604"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1531.10", "1591.13" `
-CVENames "CVE-2019-0586", "CVE-2019-0588"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU12) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1591.16", "1713.6" `
-CVENames "CVE-2019-0817", "CVE-2018-0858"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1591.17", "1713.7" `
-CVENames "ADV190018"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1713.5" `
-CVENames "CVE-2019-0686", "CVE-2019-0724"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU13) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1713.8", "1779.4" `
-CVENames "CVE-2019-1084", "CVE-2019-1136", "CVE-2019-1137"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1713.9", "1779.5" `
-CVENames "CVE-2019-1233", "CVE-2019-1266"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU14) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1779.7", "1847.5" `
-CVENames "CVE-2019-1373"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU15) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1847.7", "1913.7" `
-CVENames "CVE-2020-0688", "CVE-2020-0692"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1847.10", "1913.10" `
-CVENames "CVE-2020-0903"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU17) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1979.6", "2044.6" `
-CVENames "CVE-2020-16875"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU18) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2106.2" `
-CVENames "CVE-2021-1730"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2044.7", "2106.3" `
-CVENames "CVE-2020-16969"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2044.8", "2106.4" `
-CVENames "CVE-2020-17083", "CVE-2020-17084", "CVE-2020-17085"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2044.12", "2106.6" `
-CVENames "CVE-2020-17117", "CVE-2020-17132", "CVE-2020-17141", "CVE-2020-17142", "CVE-2020-17143"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU19) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2106.8", "2176.4" `
-CVENames "CVE-2021-24085"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "1415.8", "1466.13", "1531.12", "1591.18", "1713.10", "1779.8", "1847.12", "1913.12", "1979.8", "2044.13", "2106.13", "2176.9" `
-CVENames "CVE-2021-26855", "CVE-2021-26857", "CVE-2021-26858", "CVE-2021-27065"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2106.13", "2176.9" `
-CVENames "CVE-2021-26412", "CVE-2021-27078", "CVE-2021-26854"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU20) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2176.12", "2242.8" `
-CVENames "CVE-2021-28480", "CVE-2021-28481", "CVE-2021-28482", "CVE-2021-28483"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2176.14", "2242.10" `
-CVENames "CVE-2021-31195", "CVE-2021-31198", "CVE-2021-31207", "CVE-2021-31209"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU21) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2242.12", "2308.14" `
-CVENames "CVE-2021-31206", "CVE-2021-31196", "CVE-2021-33768"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU22) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2308.15", "2375.7" `
-CVENames "CVE-2021-26427"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2308.15", "2375.12" `
-CVENames "CVE-2021-41350", "CVE-2021-41348", "CVE-2021-34453"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "2308.20", "2375.17" `
-CVENames "CVE-2021-42305", "CVE-2021-41349", "CVE-2021-42321"
}
} elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) {
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU1) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "221.14" `
-CVENames "CVE-2019-0586", "CVE-2019-0588"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "221.16", "330.7" `
-CVENames "CVE-2019-0817", "CVE-2019-0858"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "221.17", "330.8" `
-CVENames "ADV190018"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "330.6" `
-CVENames "CVE-2019-0686", "CVE-2019-0724"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU2) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "330.9", "397.5" `
-CVENames "CVE-2019-1084", "CVE-2019-1137"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "397.6", "330.10" `
-CVENames "CVE-2019-1233", "CVE-2019-1266"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU3) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "397.9", "464.7" `
-CVENames "CVE-2019-1373"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU4) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "464.11", "529.8" `
-CVENames "CVE-2020-0688", "CVE-2020-0692"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "464.14", "529.11" `
-CVENames "CVE-2020-0903"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU6) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "595.6", "659.6" `
-CVENames "CVE-2020-16875"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU7) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "721.2" `
-CVENames "CVE-2021-1730"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "659.7", "721.3" `
-CVENames "CVE-2020-16969"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "659.8", "721.4" `
-CVENames "CVE-2020-17083", "CVE-2020-17084", "CVE-2020-17085"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "659.11", "721.6" `
-CVENames "CVE-2020-17117", "CVE-2020-17132", "CVE-2020-17141", "CVE-2020-17142", "CVE-2020-17143"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU8) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "721.8", "792.5" `
-CVENames "CVE-2021-24085"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "221.18", "330.11", "397.11", "464.15", "529.13", "595.8", "659.12", "721.13", "792.10" `
-CVENames "CVE-2021-26855", "CVE-2021-26857", "CVE-2021-26858", "CVE-2021-27065"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "721.13", "792.10" `
-CVENames "CVE-2021-26412", "CVE-2021-27078", "CVE-2021-26854"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU9) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "792.13", "858.10" `
-CVENames "CVE-2021-28480", "CVE-2021-28481", "CVE-2021-28482", "CVE-2021-28483"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "792.15", "858.12" `
-CVENames "CVE-2021-31195", "CVE-2021-31198", "CVE-2021-31207", "CVE-2021-31209"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU10) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "858.15", "922.13" `
-CVENames "CVE-2021-31206", "CVE-2021-31196", "CVE-2021-33768"
}
if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU11) {
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "922.14", "986.5" `
-CVENames "CVE-2021-26427"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "922.14", "986.9" `
-CVENames "CVE-2021-41350", "CVE-2021-41348", "CVE-2021-34453"
TestVulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision `
-SecurityFixedBuilds "922.19", "986.14" `
-CVENames "CVE-2021-42305", "CVE-2021-41349", "CVE-2021-42321"
}
} else {
Write-Verbose "Unknown Version of Exchange"
}
$securityObject = [PSCustomObject]@{
MajorVersion = $exchangeInformation.BuildInformation.MajorVersion
ServerRole = $exchangeInformation.BuildInformation.ServerRole
CU = $exchangeCU
BuildRevision = $buildRevision
ExchangeInformation = $exchangeInformation
OsInformation = $osInformation
}
Invoke-AnalyzerSecurityCve-2020-0796 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCve-2020-1147 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCve-2021-1730 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCve-2021-34470 -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
Invoke-AnalyzerSecurityCve-MarchSuSpecial -AnalyzeResults $AnalyzeResults -SecurityObject $securityObject -DisplayGroupingKey $DisplayGroupingKey
}
Function Invoke-AnalyzerSecurityVulnerability {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ref]$AnalyzeResults,
[Parameter(Mandatory = $true)]
[object]$HealthServerObject,
[Parameter(Mandatory = $true)]
[int]$Order
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$keySecurityVulnerability = Get-DisplayResultsGroupingKey -Name "Security Vulnerability" -DisplayOrder $Order
Invoke-AnalyzerSecurityCveCheck -AnalyzeResults $AnalyzeResults -HealthServerObject $HealthServerObject -DisplayGroupingKey $keySecurityVulnerability
$securityVulnerabilities = $AnalyzeResults.Value.DisplayResults[$keySecurityVulnerability]
if ($null -eq $securityVulnerabilities) {
$AnalyzeResults | Add-AnalyzedResultInformation -Details "All known security issues in this version of the script passed." `
-DisplayGroupingKey $keySecurityVulnerability `
-DisplayWriteType "Green" `
-AddHtmlDetailRow $false
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerabilities" -Details "None" `
-AddDisplayResultsLineInfo $false `
-AddHtmlOverviewValues $true
} else {
$details = $securityVulnerabilities.DisplayValue |
ForEach-Object {
return $_ + "
"
}
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Security Vulnerabilities" -Details $details `
-AddDisplayResultsLineInfo $false `
-DisplayWriteType "Red"
$AnalyzeResults | Add-AnalyzedResultInformation -Name "Vulnerability Detected" -Details $true `
-AddDisplayResultsLineInfo $false `
-DisplayWriteType "Red" `
-AddHtmlOverviewValues $true `
-AddHtmlDetailRow $false
}
}
Function Invoke-AnalyzerEngine {
param(
[HealthChecker.HealthCheckerExchangeServer]$HealthServerObject
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$analyzedResults = New-Object HealthChecker.AnalyzedInformation
$analyzedResults.HealthCheckerExchangeServer = $HealthServerObject
#Display Grouping Keys
$order = 1
$keyBeginningInfo = Get-DisplayResultsGroupingKey -Name "BeginningInfo" -DisplayGroupName $false -DisplayOrder 0 -DefaultTabNumber 0
if (!$Script:DisplayedScriptVersionAlready) {
$analyzedResults | Add-AnalyzedResultInformation -Name "Exchange Health Checker Version" -Details $BuildVersion `
-DisplayGroupingKey $keyBeginningInfo `
-AddHtmlDetailRow $false
}
$VirtualizationWarning = @"
Virtual Machine detected. Certain settings about the host hardware cannot be detected from the virtual machine. Verify on the VM Host that:
- There is no more than a 1:1 Physical Core to Virtual CPU ratio (no oversubscribing)
- If Hyper-Threading is enabled do NOT count Hyper-Threaded cores as physical cores
- Do not oversubscribe memory or use dynamic memory allocation
Although Exchange technically supports up to a 2:1 physical core to vCPU ratio, a 1:1 ratio is strongly recommended for performance reasons. Certain third party Hyper-Visors such as VMWare have their own guidance.
VMWare recommends a 1:1 ratio. Their guidance can be found at https://aka.ms/HC-VMwareBP2019.
Related specifically to VMWare, if you notice you are experiencing packet loss on your VMXNET3 adapter, you may want to review the following article from VMWare: https://aka.ms/HC-VMwareLostPackets.
For further details, please review the virtualization recommendations on Microsoft Docs here: https://aka.ms/HC-Virtualization.
"@
if ($HealthServerObject.HardwareInformation.ServerType -eq [HealthChecker.ServerType]::VMWare -or
$HealthServerObject.HardwareInformation.ServerType -eq [HealthChecker.ServerType]::HyperV) {
$analyzedResults | Add-AnalyzedResultInformation -Details $VirtualizationWarning -DisplayWriteType "Yellow" `
-DisplayGroupingKey $keyBeginningInfo `
-AddHtmlDetailRow $false
}
Invoke-AnalyzerExchangeInformation -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerHybridInformation -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerOsInformation -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerHardwareInformation -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerNicSettings -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerFrequentConfigurationIssues -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerSecuritySettings -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerSecurityVulnerability -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Invoke-AnalyzerWebAppPools -AnalyzeResults ([ref]$analyzedResults) -HealthServerObject $HealthServerObject -Order ($order++)
Write-Debug("End of Analyzer Engine")
return $analyzedResults
}
Function Get-RemoteRegistrySubKey {
[CmdletBinding()]
param(
[string]$RegistryHive = "LocalMachine",
[string]$MachineName,
[string]$SubKey,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Attempting to open the Base Key $RegistryHive on Machine $MachineName"
$regKey = $null
}
process {
try {
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($RegistryHive, $MachineName)
Write-Verbose "Attempting to open the Sub Key '$SubKey'"
$regKey = $reg.OpenSubKey($SubKey)
Write-Verbose "Opened Sub Key"
} catch {
Write-Verbose "Failed to open the registry"
if ($null -ne $CatchActionFunction) {
& $CatchActionFunction
}
}
}
end {
return $regKey
}
}
Function Get-RemoteRegistryValue {
[CmdletBinding()]
param(
[string]$RegistryHive = "LocalMachine",
[string]$MachineName,
[string]$SubKey,
[string]$GetValue,
[string]$ValueType,
[scriptblock]$CatchActionFunction
)
<#
Valid ValueType return values (case-sensitive)
(https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.registryvaluekind?view=net-5.0)
Binary = REG_BINARY
DWord = REG_DWORD
ExpandString = REG_EXPAND_SZ
MultiString = REG_MULTI_SZ
None = No data type
QWord = REG_QWORD
String = REG_SZ
Unknown = An unsupported registry data type
#>
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$registryGetValue = $null
}
process {
try {
$regSubKey = Get-RemoteRegistrySubKey -RegistryHive $RegistryHive `
-MachineName $MachineName `
-SubKey $SubKey
if (-not ([System.String]::IsNullOrWhiteSpace($regSubKey))) {
Write-Verbose "Attempting to get the value $GetValue"
$registryGetValue = $regSubKey.GetValue($GetValue)
Write-Verbose "Finished running GetValue()"
if ($null -ne $registryGetValue -and
(-not ([System.String]::IsNullOrWhiteSpace($ValueType)))) {
Write-Verbose "Validating ValueType $ValueType"
$registryValueType = $regSubKey.GetValueKind($GetValue)
Write-Verbose "Finished running GetValueKind()"
if ($ValueType -ne $registryValueType) {
Write-Verbose "ValueType: $ValueType is different to the returned ValueType: $registryValueType"
$registryGetValue = $null
} else {
Write-Verbose "ValueType matches: $ValueType"
}
}
}
} catch {
Write-Verbose "Failed to get the value on the registry"
if ($null -ne $CatchActionFunction) {
& $CatchActionFunction
}
}
}
end {
Write-Verbose "Get-RemoteRegistryValue Return Value: '$registryGetValue'"
return $registryGetValue
}
}
Function Invoke-CatchActions {
param(
[object]$CopyThisError = $Error[0]
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$Script:ErrorsExcludedCount++
$Script:ErrorsExcluded += $CopyThisError
Write-Verbose "Error Excluded Count: $Script:ErrorsExcludedCount"
Write-Verbose "Error Count: $($Error.Count)"
Write-Verbose $CopyThisError
if ($null -ne $CopyThisError.ScriptStackTrace) {
Write-Verbose $CopyThisError.ScriptStackTrace
}
}
Function Get-ExchangeAdSchemaClass {
param(
[Parameter(Mandatory = $true)][string]$SchemaClassName
)
Write-Verbose "Calling: $($MyInvocation.MyCommand) to query $SchemaClassName schema class"
$rootDSE = [ADSI]("LDAP://RootDSE")
if ([string]::IsNullOrEmpty($rootDSE.schemaNamingContext)) {
return $null
}
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchScope = "Subtree"
$directorySearcher.SearchRoot = [ADSI]("LDAP://" + $rootDSE.schemaNamingContext.ToString())
$directorySearcher.Filter = "(Name={0})" -f $SchemaClassName
$findAll = $directorySearcher.FindAll()
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $findAll
}
Function Get-ExchangeApplicationConfigurationFileValidation {
param(
[string[]]$ConfigFileLocation
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$results = @{}
$ConfigFileLocation |
ForEach-Object {
$obj = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlockDescription "Getting Exchange Application Configuration File Validation" `
-CatchActionFunction ${Function:Invoke-CatchActions} `
-ScriptBlock {
param($Location)
return [PSCustomObject]@{
Present = ((Test-Path $Location))
FileName = ([IO.Path]::GetFileName($Location))
FilePath = $Location
}
} -ArgumentList $_
$results.Add($obj.FileName, $obj)
}
return $results
}
function Get-AppPool {
[CmdletBinding()]
param ()
begin {
function Get-IndentLevel ($line) {
if ($line.StartsWith(" ")) {
($line | Select-String "^ +").Matches[0].Length
} else {
0
}
}
function Convert-FromAppPoolText {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string[]]
$Text,
[Parameter(Mandatory = $false)]
[int]
$Line = 0,
[Parameter(Mandatory = $false)]
[int]
$MinimumIndentLevel = 2
)
if ($Line -ge $Text.Count) {
return $null
}
$startingIndentLevel = Get-IndentLevel $Text[$Line]
if ($startingIndentLevel -lt $MinimumIndentLevel) {
return $null
}
$hash = @{}
while ($Line -lt $Text.Count) {
$indentLevel = Get-IndentLevel $Text[$Line]
if ($indentLevel -gt $startingIndentLevel) {
# Skip until we get to the next thing at this level
} elseif ($indentLevel -eq $startingIndentLevel) {
# We have a property at this level. Add it to the object.
if ($Text[$Line] -match "\[(\S+)\]") {
$name = $Matches[1]
$value = Convert-FromAppPoolText -Text $Text -Line ($Line + 1) -MinimumIndentLevel $startingIndentLevel
$hash[$name] = $value
} elseif ($Text[$Line] -match "\s+(\S+):`"(.*)`"") {
$name = $Matches[1]
$value = $Matches[2].Trim("`"")
$hash[$name] = $value
}
} else {
# IndentLevel is less than what we started with, so return
[PSCustomObject]$hash
return
}
++$Line
}
[PSCustomObject]$hash
}
$appPoolCmd = "$env:windir\System32\inetsrv\appcmd.exe"
}
process {
$appPoolNames = & $appPoolCmd list apppool |
Select-String "APPPOOL `"(\S+)`" " |
ForEach-Object { $_.Matches.Groups[1].Value }
foreach ($appPoolName in $appPoolNames) {
$appPoolText = & $appPoolCmd list apppool $appPoolName /text:*
Convert-FromAppPoolText -Text $appPoolText -Line 1
}
}
}
Function Get-ExchangeAppPoolsInformation {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$appPool = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock ${Function:Get-AppPool} `
-ScriptBlockDescription "Getting App Pool information" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$exchangeAppPoolsInfo = @{}
$appPool |
Where-Object { $_.add.name -like "MSExchange*" } |
ForEach-Object {
$configContent = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock {
param(
$FilePath
)
if (Test-Path $FilePath) {
return (Get-Content $FilePath)
}
return [string]::Empty
} `
-ScriptBlockDescription "Getting Content file for $($_.add.name)" `
-ArgumentList $_.add.CLRConfigFile `
-CatchActionFunction ${Function:Invoke-CatchActions}
$gcUnknown = $true
$gcServerEnabled = $false
if (-not ([string]::IsNullOrEmpty($configContent))) {
$gcSetting = ([xml]$configContent).Configuration.Runtime.gcServer.Enabled
$gcUnknown = $gcSetting -ne "true" -and $gcSetting -ne "false"
$gcServerEnabled = $gcSetting -eq "true"
}
$exchangeAppPoolsInfo.Add($_.add.Name, [PSCustomObject]@{
ConfigContent = $configContent
AppSettings = $_
GCUnknown = $gcUnknown
GCServerEnabled = $gcServerEnabled
})
}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $exchangeAppPoolsInfo
}
Function Get-ExchangeBuildVersionInformation {
[CmdletBinding()]
param(
[object]$AdminDisplayVersion
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed $($AdminDisplayVersion.ToString())"
$AdminDisplayVersion = $AdminDisplayVersion.ToString()
$exchangeMajorVersion = [string]::Empty
[int]$major = 0
[int]$minor = 0
[int]$build = 0
[int]$revision = 0
$product = $null
[double]$buildVersion = 0.0
}
process {
$split = $AdminDisplayVersion.Substring(($AdminDisplayVersion.IndexOf(" ")) + 1, 4).Split(".")
$major = [int]$split[0]
$minor = [int]$split[1]
$product = $major + ($minor / 10)
$buildStart = $AdminDisplayVersion.LastIndexOf(" ") + 1
$split = $AdminDisplayVersion.Substring($buildStart, ($AdminDisplayVersion.LastIndexOf(")") - $buildStart)).Split(".")
$build = [int]$split[0]
$revision = [int]$split[1]
$revisionDecimal = if ($revision -lt 10) { $revision / 10 } else { $revision / 100 }
$buildVersion = $build + $revisionDecimal
Write-Verbose "Determining Major Version based off of $product"
switch ([string]$product) {
"14.3" { $exchangeMajorVersion = "Exchange2010" }
"15" { $exchangeMajorVersion = "Exchange2013" }
"15.1" { $exchangeMajorVersion = "Exchange2016" }
"15.2" { $exchangeMajorVersion = "Exchange2019" }
default { $exchangeMajorVersion = "Unknown" }
}
}
end {
Write-Verbose "Found Major Version '$exchangeMajorVersion'"
return [PSCustomObject]@{
MajorVersion = $exchangeMajorVersion
Major = $major
Minor = $minor
Build = $build
Revision = $revision
Product = $product
BuildVersion = $buildVersion
}
}
}
Function Get-ExchangeEmergencyMitigationServiceState {
[CmdletBinding()]
[OutputType("System.Object")]
param(
[Parameter(Mandatory = $true)]
[object]
$RequiredInformation,
[Parameter(Mandatory = $false)]
[scriptblock]
$CatchActionFunction
)
begin {
$computerName = $RequiredInformation.ComputerName
$emergencyMitigationServiceOrgState = $RequiredInformation.MitigationsEnabled
$exchangeServerConfiguration = $RequiredInformation.GetExchangeServer
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed - Computername: $ComputerName"
}
process {
if ($null -ne $emergencyMitigationServiceOrgState) {
Write-Verbose "Exchange Emergency Mitigation Service detected"
try {
$exchangeEmergencyMitigationWinServiceRating = $null
$emergencyMitigationWinService = Get-Service -ComputerName $ComputerName -Name MSExchangeMitigation -ErrorAction Stop
if (($emergencyMitigationWinService.Status.ToString() -eq "Running") -and
($emergencyMitigationWinService.StartType.ToString() -eq "Automatic")) {
$exchangeEmergencyMitigationWinServiceRating = "Running"
} else {
$exchangeEmergencyMitigationWinServiceRating = "Investigate"
}
} catch {
Write-Verbose "Failed to query EEMS Windows service data"
Invoke-CatchActionError $CatchActionFunction
}
$eemsEndpoint = Invoke-ScriptBlockHandler -ComputerName $ComputerName -ScriptBlockDescription "Test EEMS pattern service connectivity" `
-CatchActionFunction $CatchActionFunction `
-ScriptBlock {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; `
Invoke-WebRequest -Method Get -Uri "https://officeclient.microsoft.com/getexchangemitigations" -UseBasicParsing
}
}
}
end {
return [PSCustomObject]@{
MitigationWinServiceState = $exchangeEmergencyMitigationWinServiceRating
MitigationServiceOrgState = $emergencyMitigationServiceOrgState
MitigationServiceSrvState = $exchangeServerConfiguration.MitigationsEnabled
MitigationServiceEndpoint = $eemsEndpoint.StatusCode
MitigationsApplied = $exchangeServerConfiguration.MitigationsApplied
MitigationsBlocked = $exchangeServerConfiguration.MitigationsBlocked
DataCollectionEnabled = $exchangeServerConfiguration.DataCollectionEnabled
}
}
}
Function Get-ExchangeServerCertificates {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
try {
Write-Verbose "Trying to receive certificates from Exchange server: $($Script:Server)"
$exchangeServerCertificates = Get-ExchangeCertificate -Server $Script:Server -ErrorAction Stop
if ($null -ne $exchangeServerCertificates) {
try {
$authConfig = Get-AuthConfig -ErrorAction Stop
$authConfigDetected = $true
} catch {
$authConfigDetected = $false
Invoke-CatchActions
}
[array]$certObject = @()
foreach ($cert in $exchangeServerCertificates) {
try {
$certificateLifetime = ([System.Convert]::ToDateTime($cert.NotAfter, [System.Globalization.DateTimeFormatInfo]::InvariantInfo) - (Get-Date)).Days
$sanCertificateInfo = $false
$currentErrors = $Error.Count
if ($null -ne $cert.DnsNameList -and
($cert.DnsNameList).Count -gt 1) {
$sanCertificateInfo = $true
$certDnsNameList = $cert.DnsNameList
} elseif ($null -eq $cert.DnsNameList) {
$certDnsNameList = "None"
} else {
$certDnsNameList = $cert.DnsNameList
}
if ($currentErrors -lt $Error.Count) {
$i = 0
while ($i -lt ($Error.Count - $currentErrors)) {
Invoke-CatchActions $Error[$i]
$i++
}
}
if ($authConfigDetected) {
$isAuthConfigInfo = $false
if ($cert.Thumbprint -eq $authConfig.CurrentCertificateThumbprint) {
$isAuthConfigInfo = $true
}
} else {
$isAuthConfigInfo = "InvalidAuthConfig"
}
if ([String]::IsNullOrEmpty($cert.FriendlyName)) {
$certFriendlyName = ($certDnsNameList[0]).ToString()
} else {
$certFriendlyName = $cert.FriendlyName
}
if ([String]::IsNullOrEmpty($cert.Status)) {
$certStatus = "Unknown"
} else {
$certStatus = ($cert.Status).ToString()
}
if ([String]::IsNullOrEmpty($cert.SignatureAlgorithm.FriendlyName)) {
$certSignatureAlgorithm = "Unknown"
$certSignatureHashAlgorithm = "Unknown"
$certSignatureHashAlgorithmSecure = 0
} else {
$certSignatureAlgorithm = $cert.SignatureAlgorithm.FriendlyName
<#
OID Table
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnap/a48b02b2-2a10-4eb0-bed4-1807a6d2f5ad
SignatureHashAlgorithmSecure = Unknown 0
SignatureHashAlgorithmSecure = Insecure/Weak 1
SignatureHashAlgorithmSecure = Secure 2
#>
switch ($cert.SignatureAlgorithm.Value) {
"1.2.840.113549.1.1.5" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.113549.1.1.4" { $certSignatureHashAlgorithm = "md5"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.10040.4.3" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.29" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.15" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.3" { $certSignatureHashAlgorithm = "md5"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.113549.1.1.2" { $certSignatureHashAlgorithm = "md2"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.113549.1.1.3" { $certSignatureHashAlgorithm = "md4"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.2" { $certSignatureHashAlgorithm = "md4"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.4" { $certSignatureHashAlgorithm = "md4"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.7.2.3.1" { $certSignatureHashAlgorithm = "md2"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.13" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.3.14.3.2.27" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"2.16.840.1.101.2.1.1.19" { $certSignatureHashAlgorithm = "mosaicSignature"; $certSignatureHashAlgorithmSecure = 0 }
"1.3.14.3.2.26" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.113549.2.5" { $certSignatureHashAlgorithm = "md5"; $certSignatureHashAlgorithmSecure = 1 }
"2.16.840.1.101.3.4.2.1" { $certSignatureHashAlgorithm = "sha256"; $certSignatureHashAlgorithmSecure = 2 }
"2.16.840.1.101.3.4.2.2" { $certSignatureHashAlgorithm = "sha384"; $certSignatureHashAlgorithmSecure = 2 }
"2.16.840.1.101.3.4.2.3" { $certSignatureHashAlgorithm = "sha512"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.113549.1.1.11" { $certSignatureHashAlgorithm = "sha256"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.113549.1.1.12" { $certSignatureHashAlgorithm = "sha384"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.113549.1.1.13" { $certSignatureHashAlgorithm = "sha512"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.113549.1.1.10" { $certSignatureHashAlgorithm = "rsassa-pss"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.10045.4.1" { $certSignatureHashAlgorithm = "sha1"; $certSignatureHashAlgorithmSecure = 1 }
"1.2.840.10045.4.3.2" { $certSignatureHashAlgorithm = "sha256"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.10045.4.3.3" { $certSignatureHashAlgorithm = "sha384"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.10045.4.3.4" { $certSignatureHashAlgorithm = "sha512"; $certSignatureHashAlgorithmSecure = 2 }
"1.2.840.10045.4.3" { $certSignatureHashAlgorithm = "sha256"; $certSignatureHashAlgorithmSecure = 2 }
Default { $certSignatureHashAlgorithm = "Unknown"; $certSignatureHashAlgorithmSecure = 0 }
}
}
$certInformationObj = New-Object PSCustomObject
$certInformationObj | Add-Member -MemberType NoteProperty -Name "FriendlyName" -Value $certFriendlyName
$certInformationObj | Add-Member -MemberType NoteProperty -Name "Thumbprint" -Value $cert.Thumbprint
$certInformationObj | Add-Member -MemberType NoteProperty -Name "PublicKeySize" -Value $cert.PublicKey.Key.KeySize
$certInformationObj | Add-Member -MemberType NoteProperty -Name "SignatureAlgorithm" -Value $certSignatureAlgorithm
$certInformationObj | Add-Member -MemberType NoteProperty -Name "SignatureHashAlgorithm" -Value $certSignatureHashAlgorithm
$certInformationObj | Add-Member -MemberType NoteProperty -Name "SignatureHashAlgorithmSecure" -Value $certSignatureHashAlgorithmSecure
$certInformationObj | Add-Member -MemberType NoteProperty -Name "IsSanCertificate" -Value $sanCertificateInfo
$certInformationObj | Add-Member -MemberType NoteProperty -Name "Namespaces" -Value $certDnsNameList
$certInformationObj | Add-Member -MemberType NoteProperty -Name "Services" -Value $cert.Services
$certInformationObj | Add-Member -MemberType NoteProperty -Name "IsCurrentAuthConfigCertificate" -Value $isAuthConfigInfo
$certInformationObj | Add-Member -MemberType NoteProperty -Name "LifetimeInDays" -Value $certificateLifetime
$certInformationObj | Add-Member -MemberType NoteProperty -Name "Status" -Value $certStatus
$certInformationObj | Add-Member -MemberType NoteProperty -Name "CertificateObject" -Value $cert
$certObject += $certInformationObj
} catch {
Write-Verbose "Unable to process certificate: $($cert.Thumbprint)"
Invoke-CatchActions
}
}
Write-Verbose "Processed: $($certObject.Count) certificates"
return $certObject
} else {
Write-Verbose "Failed to find any Exchange certificates"
return $null
}
} catch {
Write-Verbose "Failed to run Get-ExchangeCertificate. Error: $($Error[0].Exception)."
Invoke-CatchActions
}
}
Function Get-ExchangeServerMaintenanceState {
param(
[Parameter(Mandatory = $false)][array]$ComponentsToSkip
)
Write-Verbose "Calling Function: $($MyInvocation.MyCommand)"
[HealthChecker.ExchangeServerMaintenance]$serverMaintenance = New-Object -TypeName HealthChecker.ExchangeServerMaintenance
$serverMaintenance.GetServerComponentState = Get-ServerComponentState -Identity $Script:Server -ErrorAction SilentlyContinue
try {
$serverMaintenance.GetClusterNode = Get-ClusterNode -Name $Script:Server -ErrorAction Stop
} catch {
Write-Verbose "Failed to run Get-ClusterNode"
Invoke-CatchActions
}
Write-Verbose "Running ServerComponentStates checks"
foreach ($component in $serverMaintenance.GetServerComponentState) {
if (($null -ne $ComponentsToSkip -and
$ComponentsToSkip.Count -ne 0) -and
$ComponentsToSkip -notcontains $component.Component) {
if ($component.State.ToString() -ne "Active") {
$latestLocalState = $null
$latestRemoteState = $null
if ($null -ne $component.LocalStates -and
$component.LocalStates.Count -gt 0) {
$latestLocalState = ($component.LocalStates | Sort-Object { $_.TimeStamp } -ErrorAction SilentlyContinue)[-1]
}
if ($null -ne $component.RemoteStates -and
$component.RemoteStates.Count -gt 0) {
$latestRemoteState = ($component.RemoteStates | Sort-Object { $_.TimeStamp } -ErrorAction SilentlyContinue)[-1]
}
Write-Verbose "Component: '$($component.Component)' LocalState: '$($latestLocalState.State)' RemoteState: '$($latestRemoteState.State)'"
if ($latestLocalState.State -eq $latestRemoteState.State) {
$serverMaintenance.InactiveComponents += "'{0}' is in Maintenance Mode" -f $component.Component
} else {
if (($null -ne $latestLocalState) -and
($latestLocalState.State -ne "Active")) {
$serverMaintenance.InactiveComponents += "'{0}' is in Local Maintenance Mode only" -f $component.Component
}
if (($null -ne $latestRemoteState) -and
($latestRemoteState.State -ne "Active")) {
$serverMaintenance.InactiveComponents += "'{0}' is in Remote Maintenance Mode only" -f $component.Component
}
}
} else {
Write-Verbose "Component '$($component.Component)' is Active"
}
} else {
Write-Verbose "Component: $($component.Component) will be skipped"
}
}
return $serverMaintenance
}
Function Get-ExchangeUpdates {
param(
[Parameter(Mandatory = $true)][HealthChecker.ExchangeMajorVersion]$ExchangeMajorVersion
)
Write-Verbose("Calling: $($MyInvocation.MyCommand) Passed: $ExchangeMajorVersion")
$RegLocation = [string]::Empty
if ([HealthChecker.ExchangeMajorVersion]::Exchange2013 -eq $ExchangeMajorVersion) {
$RegLocation = "SOFTWARE\Microsoft\Updates\Exchange 2013"
} elseif ([HealthChecker.ExchangeMajorVersion]::Exchange2016 -eq $ExchangeMajorVersion) {
$RegLocation = "SOFTWARE\Microsoft\Updates\Exchange 2016"
} else {
$RegLocation = "SOFTWARE\Microsoft\Updates\Exchange 2019"
}
$RegKey = Get-RemoteRegistrySubKey -MachineName $Script:Server `
-SubKey $RegLocation `
-CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -ne $RegKey) {
$IU = $RegKey.GetSubKeyNames()
if ($null -ne $IU) {
Write-Verbose "Detected fixes installed on the server"
$fixes = @()
foreach ($key in $IU) {
$IUKey = $RegKey.OpenSubKey($key)
$IUName = $IUKey.GetValue("PackageName")
Write-Verbose "Found: $IUName"
$fixes += $IUName
}
return $fixes
} else {
Write-Verbose "No IUs found in the registry"
}
} else {
Write-Verbose "No RegKey returned"
}
Write-Verbose "Exiting: Get-ExchangeUpdates"
return $null
}
Function Get-ExSetupDetails {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$exSetupDetails = [string]::Empty
Function Get-ExSetupDetailsScriptBlock {
Get-Command ExSetup | ForEach-Object { $_.FileVersionInfo }
}
$exSetupDetails = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock ${Function:Get-ExSetupDetailsScriptBlock} -ScriptBlockDescription "Getting ExSetup remotely" -CatchActionFunction ${Function:Invoke-CatchActions}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $exSetupDetails
}
Function Get-ServerRole {
param(
[Parameter(Mandatory = $true)][object]$ExchangeServerObj
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$roles = $ExchangeServerObj.ServerRole.ToString()
Write-Verbose "Roll: $roles"
#Need to change this to like because of Exchange 2010 with AIO with the hub role.
if ($roles -like "Mailbox, ClientAccess*") {
return [HealthChecker.ExchangeServerRole]::MultiRole
} elseif ($roles -eq "Mailbox") {
return [HealthChecker.ExchangeServerRole]::Mailbox
} elseif ($roles -eq "Edge") {
return [HealthChecker.ExchangeServerRole]::Edge
} elseif ($roles -like "*ClientAccess*") {
return [HealthChecker.ExchangeServerRole]::ClientAccess
} else {
return [HealthChecker.ExchangeServerRole]::None
}
}
Function Get-ExchangeInformation {
param(
[HealthChecker.OSServerVersion]$OSMajorVersion
)
Write-Verbose "Calling: $($MyInvocation.MyCommand) Passed: OSMajorVersion: $OSMajorVersion"
[HealthChecker.ExchangeInformation]$exchangeInformation = New-Object -TypeName HealthChecker.ExchangeInformation
$exchangeInformation.GetExchangeServer = (Get-ExchangeServer -Identity $Script:Server -Status)
$exchangeInformation.ExchangeCertificates = Get-ExchangeServerCertificates
$buildInformation = $exchangeInformation.BuildInformation
$buildVersionInfo = Get-ExchangeBuildVersionInformation -AdminDisplayVersion $exchangeInformation.GetExchangeServer.AdminDisplayVersion
$buildInformation.MajorVersion = ([HealthChecker.ExchangeMajorVersion]$buildVersionInfo.MajorVersion)
$buildInformation.BuildNumber = "{0}.{1}.{2}.{3}" -f $buildVersionInfo.Major, $buildVersionInfo.Minor, $buildVersionInfo.Build, $buildVersionInfo.Revision
$buildInformation.ServerRole = (Get-ServerRole -ExchangeServerObj $exchangeInformation.GetExchangeServer)
$buildInformation.ExchangeSetup = Get-ExSetupDetails
if ($buildInformation.ServerRole -le [HealthChecker.ExchangeServerRole]::Mailbox ) {
try {
$exchangeInformation.GetMailboxServer = (Get-MailboxServer -Identity $Script:Server -ErrorAction Stop)
} catch {
Write-Verbose "Failed to run Get-MailboxServer"
Invoke-CatchActions
}
}
if (($buildInformation.MajorVersion -ge [HealthChecker.ExchangeMajorVersion]::Exchange2016 -and
$buildInformation.ServerRole -le [HealthChecker.ExchangeServerRole]::Mailbox) -or
($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013 -and
($buildInformation.ServerRole -eq [HealthChecker.ExchangeServerRole]::ClientAccess -or
$buildInformation.ServerRole -eq [HealthChecker.ExchangeServerRole]::MultiRole))) {
$exchangeInformation.GetOwaVirtualDirectory = Get-OwaVirtualDirectory -Identity ("{0}\owa (Default Web Site)" -f $Script:Server) -ADPropertiesOnly
$exchangeInformation.GetWebServicesVirtualDirectory = Get-WebServicesVirtualDirectory -Server $Script:Server
}
if ($Script:ExchangeShellComputer.ToolsOnly) {
$buildInformation.LocalBuildNumber = "{0}.{1}.{2}.{3}" -f $Script:ExchangeShellComputer.Major, $Script:ExchangeShellComputer.Minor, `
$Script:ExchangeShellComputer.Build, `
$Script:ExchangeShellComputer.Revision
}
#Exchange 2013 or greater
if ($buildInformation.MajorVersion -ge [HealthChecker.ExchangeMajorVersion]::Exchange2013) {
$netFrameworkExchange = $exchangeInformation.NETFramework
$buildAndRevision = $buildVersionInfo.BuildVersion
Write-Verbose "The build and revision number: $buildAndRevision"
#Build Numbers: https://docs.microsoft.com/en-us/Exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
if ($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) {
Write-Verbose "Exchange 2019 is detected. Checking build number..."
$buildInformation.FriendlyName = "Exchange 2019 "
#Exchange 2019 Information
if ($buildAndRevision -lt 330.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::RTM
$buildInformation.FriendlyName += "RTM"
$buildInformation.ReleaseDate = "10/22/2018"
} elseif ($buildAndRevision -lt 397.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU1
$buildInformation.FriendlyName += "CU1"
$buildInformation.ReleaseDate = "02/12/2019"
} elseif ($buildAndRevision -lt 464.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU2
$buildInformation.FriendlyName += "CU2"
$buildInformation.ReleaseDate = "06/18/2019"
} elseif ($buildAndRevision -lt 529.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU3
$buildInformation.FriendlyName += "CU3"
$buildInformation.ReleaseDate = "09/17/2019"
} elseif ($buildAndRevision -lt 595.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU4
$buildInformation.FriendlyName += "CU4"
$buildInformation.ReleaseDate = "12/17/2019"
} elseif ($buildAndRevision -lt 659.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU5
$buildInformation.FriendlyName += "CU5"
$buildInformation.ReleaseDate = "03/17/2020"
} elseif ($buildAndRevision -lt 721.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU6
$buildInformation.FriendlyName += "CU6"
$buildInformation.ReleaseDate = "06/16/2020"
} elseif ($buildAndRevision -lt 792.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU7
$buildInformation.FriendlyName += "CU7"
$buildInformation.ReleaseDate = "09/15/2020"
} elseif ($buildAndRevision -lt 858.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU8
$buildInformation.FriendlyName += "CU8"
$buildInformation.ReleaseDate = "12/15/2020"
} elseif ($buildAndRevision -lt 922.7) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU9
$buildInformation.FriendlyName += "CU9"
$buildInformation.ReleaseDate = "03/16/2021"
} elseif ($buildAndRevision -lt 986.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU10
$buildInformation.FriendlyName += "CU10"
$buildInformation.ReleaseDate = "06/29/2021"
$buildInformation.SupportedBuild = $true
} elseif ($buildAndRevision -ge 986.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU11
$buildInformation.FriendlyName += "CU11"
$buildInformation.ReleaseDate = "09/28/2021"
$buildInformation.SupportedBuild = $true
}
#Exchange 2019 .NET Information
if ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU2) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU4) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
} else {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
}
} elseif ($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) {
Write-Verbose "Exchange 2016 is detected. Checking build number..."
$buildInformation.FriendlyName = "Exchange 2016 "
#Exchange 2016 Information
if ($buildAndRevision -lt 466.34) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU1
$buildInformation.FriendlyName += "CU1"
$buildInformation.ReleaseDate = "03/15/2016"
} elseif ($buildAndRevision -lt 544.27) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU2
$buildInformation.FriendlyName += "CU2"
$buildInformation.ReleaseDate = "06/21/2016"
} elseif ($buildAndRevision -lt 669.32) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU3
$buildInformation.FriendlyName += "CU3"
$buildInformation.ReleaseDate = "09/20/2016"
} elseif ($buildAndRevision -lt 845.34) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU4
$buildInformation.FriendlyName += "CU4"
$buildInformation.ReleaseDate = "12/13/2016"
} elseif ($buildAndRevision -lt 1034.26) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU5
$buildInformation.FriendlyName += "CU5"
$buildInformation.ReleaseDate = "03/21/2017"
} elseif ($buildAndRevision -lt 1261.35) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU6
$buildInformation.FriendlyName += "CU6"
$buildInformation.ReleaseDate = "06/24/2017"
} elseif ($buildAndRevision -lt 1415.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU7
$buildInformation.FriendlyName += "CU7"
$buildInformation.ReleaseDate = "09/16/2017"
} elseif ($buildAndRevision -lt 1466.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU8
$buildInformation.FriendlyName += "CU8"
$buildInformation.ReleaseDate = "12/19/2017"
} elseif ($buildAndRevision -lt 1531.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU9
$buildInformation.FriendlyName += "CU9"
$buildInformation.ReleaseDate = "03/20/2018"
} elseif ($buildAndRevision -lt 1591.10) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU10
$buildInformation.FriendlyName += "CU10"
$buildInformation.ReleaseDate = "06/19/2018"
} elseif ($buildAndRevision -lt 1713.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU11
$buildInformation.FriendlyName += "CU11"
$buildInformation.ReleaseDate = "10/16/2018"
} elseif ($buildAndRevision -lt 1779.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU12
$buildInformation.FriendlyName += "CU12"
$buildInformation.ReleaseDate = "02/12/2019"
} elseif ($buildAndRevision -lt 1847.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU13
$buildInformation.FriendlyName += "CU13"
$buildInformation.ReleaseDate = "06/18/2019"
} elseif ($buildAndRevision -lt 1913.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU14
$buildInformation.FriendlyName += "CU14"
$buildInformation.ReleaseDate = "09/17/2019"
} elseif ($buildAndRevision -lt 1979.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU15
$buildInformation.FriendlyName += "CU15"
$buildInformation.ReleaseDate = "12/17/2019"
} elseif ($buildAndRevision -lt 2044.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU16
$buildInformation.FriendlyName += "CU16"
$buildInformation.ReleaseDate = "03/17/2020"
} elseif ($buildAndRevision -lt 2106.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU17
$buildInformation.FriendlyName += "CU17"
$buildInformation.ReleaseDate = "06/16/2020"
} elseif ($buildAndRevision -lt 2176.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU18
$buildInformation.FriendlyName += "CU18"
$buildInformation.ReleaseDate = "09/15/2020"
} elseif ($buildAndRevision -lt 2242.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU19
$buildInformation.FriendlyName += "CU19"
$buildInformation.ReleaseDate = "12/15/2020"
} elseif ($buildAndRevision -lt 2308.8) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU20
$buildInformation.FriendlyName += "CU20"
$buildInformation.ReleaseDate = "03/16/2021"
} elseif ($buildAndRevision -lt 2375.7) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU21
$buildInformation.FriendlyName += "CU21"
$buildInformation.ReleaseDate = "06/29/2021"
$buildInformation.SupportedBuild = $true
} elseif ($buildAndRevision -ge 2375.7) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU22
$buildInformation.FriendlyName += "CU22"
$buildInformation.ReleaseDate = "09/28/2021"
$buildInformation.SupportedBuild = $true
}
#Exchange 2016 .NET Information
if ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU2) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
} elseif ($buildInformation.CU -eq [HealthChecker.ExchangeCULevel]::CU2) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d1wFix
} elseif ($buildInformation.CU -eq [HealthChecker.ExchangeCULevel]::CU3) {
if ($OSMajorVersion -eq [HealthChecker.OSServerVersion]::Windows2016) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
} else {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d1wFix
}
} elseif ($buildInformation.CU -eq [HealthChecker.ExchangeCULevel]::CU4) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU8) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU10) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU11) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU13) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU15) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
} else {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
}
} else {
Write-Verbose "Exchange 2013 is detected. Checking build number..."
$buildInformation.FriendlyName = "Exchange 2013 "
#Exchange 2013 Information
if ($buildAndRevision -lt 712.24) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU1
$buildInformation.FriendlyName += "CU1"
$buildInformation.ReleaseDate = "04/02/2013"
} elseif ($buildAndRevision -lt 775.38) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU2
$buildInformation.FriendlyName += "CU2"
$buildInformation.ReleaseDate = "07/09/2013"
} elseif ($buildAndRevision -lt 847.32) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU3
$buildInformation.FriendlyName += "CU3"
$buildInformation.ReleaseDate = "11/25/2013"
} elseif ($buildAndRevision -lt 913.22) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU4
$buildInformation.FriendlyName += "CU4"
$buildInformation.ReleaseDate = "02/25/2014"
} elseif ($buildAndRevision -lt 995.29) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU5
$buildInformation.FriendlyName += "CU5"
$buildInformation.ReleaseDate = "05/27/2014"
} elseif ($buildAndRevision -lt 1044.25) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU6
$buildInformation.FriendlyName += "CU6"
$buildInformation.ReleaseDate = "08/26/2014"
} elseif ($buildAndRevision -lt 1076.9) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU7
$buildInformation.FriendlyName += "CU7"
$buildInformation.ReleaseDate = "12/09/2014"
} elseif ($buildAndRevision -lt 1104.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU8
$buildInformation.FriendlyName += "CU8"
$buildInformation.ReleaseDate = "03/17/2015"
} elseif ($buildAndRevision -lt 1130.7) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU9
$buildInformation.FriendlyName += "CU9"
$buildInformation.ReleaseDate = "06/17/2015"
} elseif ($buildAndRevision -lt 1156.6) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU10
$buildInformation.FriendlyName += "CU10"
$buildInformation.ReleaseDate = "09/15/2015"
} elseif ($buildAndRevision -lt 1178.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU11
$buildInformation.FriendlyName += "CU11"
$buildInformation.ReleaseDate = "12/15/2015"
} elseif ($buildAndRevision -lt 1210.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU12
$buildInformation.FriendlyName += "CU12"
$buildInformation.ReleaseDate = "03/15/2016"
} elseif ($buildAndRevision -lt 1236.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU13
$buildInformation.FriendlyName += "CU13"
$buildInformation.ReleaseDate = "06/21/2016"
} elseif ($buildAndRevision -lt 1263.5) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU14
$buildInformation.FriendlyName += "CU14"
$buildInformation.ReleaseDate = "09/20/2016"
} elseif ($buildAndRevision -lt 1293.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU15
$buildInformation.FriendlyName += "CU15"
$buildInformation.ReleaseDate = "12/13/2016"
} elseif ($buildAndRevision -lt 1320.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU16
$buildInformation.FriendlyName += "CU16"
$buildInformation.ReleaseDate = "03/21/2017"
} elseif ($buildAndRevision -lt 1347.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU17
$buildInformation.FriendlyName += "CU17"
$buildInformation.ReleaseDate = "06/24/2017"
} elseif ($buildAndRevision -lt 1365.1) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU18
$buildInformation.FriendlyName += "CU18"
$buildInformation.ReleaseDate = "09/16/2017"
} elseif ($buildAndRevision -lt 1367.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU19
$buildInformation.FriendlyName += "CU19"
$buildInformation.ReleaseDate = "12/19/2017"
} elseif ($buildAndRevision -lt 1395.4) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU20
$buildInformation.FriendlyName += "CU20"
$buildInformation.ReleaseDate = "03/20/2018"
} elseif ($buildAndRevision -lt 1473.3) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU21
$buildInformation.FriendlyName += "CU21"
$buildInformation.ReleaseDate = "06/19/2018"
} elseif ($buildAndRevision -lt 1497.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU22
$buildInformation.FriendlyName += "CU22"
$buildInformation.ReleaseDate = "02/12/2019"
} elseif ($buildAndRevision -ge 1497.2) {
$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU23
$buildInformation.FriendlyName += "CU23"
$buildInformation.ReleaseDate = "06/18/2019"
$buildInformation.SupportedBuild = $true
}
#Exchange 2013 .NET Information
if ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU4) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU13) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU15) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d1wFix
} elseif ($buildInformation.CU -eq [HealthChecker.ExchangeCULevel]::CU15) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d1
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU19) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU21) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
} elseif ($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU23) {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
} else {
$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2
$netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8
}
}
try {
$organizationConfig = Get-OrganizationConfig -ErrorAction Stop
$exchangeInformation.GetOrganizationConfig = $organizationConfig
} catch {
Write-Yellow "Failed to run Get-OrganizationConfig."
Invoke-CatchActions
}
$mitigationsEnabled = $null
if ($null -ne $organizationConfig) {
$mitigationsEnabled = $organizationConfig.MitigationsEnabled
}
$exchangeInformation.ExchangeEmergencyMitigationService = Get-ExchangeEmergencyMitigationServiceState `
-RequiredInformation ([PSCustomObject]@{
ComputerName = $Script:Server
MitigationsEnabled = $mitigationsEnabled
GetExchangeServer = $exchangeInformation.GetExchangeServer
}) `
-CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -ne $organizationConfig) {
$exchangeInformation.MapiHttpEnabled = $organizationConfig.MapiHttpEnabled
if ($null -ne $organizationConfig.EnableDownloadDomains) {
$exchangeInformation.EnableDownloadDomains = $organizationConfig.EnableDownloadDomains
}
} else {
Write-Verbose "MAPI HTTP Enabled and Download Domains Enabled results not accurate"
}
if ($buildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) {
$exchangeInformation.ApplicationPools = Get-ExchangeAppPoolsInformation
try {
$exchangeInformation.GetHybridConfiguration = Get-HybridConfiguration -ErrorAction Stop
} catch {
Write-Yellow "Failed to run Get-HybridConfiguration"
Invoke-CatchActions
}
}
$serverExchangeBinDirectory = Invoke-ScriptBlockHandler -ComputerName $Script:Server `
-ScriptBlockDescription "Getting Exchange Bin Directory" `
-CatchActionFunction ${Function:Invoke-CatchActions} `
-ScriptBlock {
"{0}Bin\" -f (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup).MsiInstallPath
}
Write-Verbose "Found Exchange Bin: $serverExchangeBinDirectory"
$exchangeInformation.ApplicationConfigFileStatus = Get-ExchangeApplicationConfigurationFileValidation -ConfigFileLocation ("{0}EdgeTransport.exe.config" -f $serverExchangeBinDirectory)
$buildInformation.KBsInstalled = Get-ExchangeUpdates -ExchangeMajorVersion $buildInformation.MajorVersion
if (($null -ne $buildInformation.KBsInstalled) -and ($buildInformation.KBsInstalled -like "*KB5000871*")) {
Write-Verbose "March 2021 SU: KB5000871 was detected on the system"
$buildInformation.March2021SUInstalled = $true
} else {
Write-Verbose "March 2021 SU: KB5000871 was not detected on the system"
$buildInformation.March2021SUInstalled = $false
}
Write-Verbose "Query schema class information for CVE-2021-34470 testing"
try {
$exchangeInformation.msExchStorageGroup = Get-ExchangeAdSchemaClass -SchemaClassName "ms-Exch-Storage-Group"
} catch {
Write-Verbose "Failed to run Get-ExchangeAdSchemaClass"
Invoke-CatchActions
}
$exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "SOFTWARE\Microsoft\ExchangeServer\v15\Search\SystemParameters" `
-GetValue "CtsProcessorAffinityPercentage" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$exchangeInformation.RegistryValues.FipsAlgorithmPolicyEnabled = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy" `
-GetValue "Enabled" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$exchangeInformation.ServerMaintenance = Get-ExchangeServerMaintenanceState -ComponentsToSkip "ForwardSyncDaemon", "ProvisioningRps"
if (($buildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::ClientAccess) -and
($buildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::None)) {
try {
$testServiceHealthResults = Test-ServiceHealth -Server $Script:Server -ErrorAction Stop
foreach ($notRunningService in $testServiceHealthResults.ServicesNotRunning) {
if ($exchangeInformation.ExchangeServicesNotRunning -notcontains $notRunningService) {
$exchangeInformation.ExchangeServicesNotRunning += $notRunningService
}
}
} catch {
Write-Verbose "Failed to run Test-ServiceHealth"
Invoke-CatchActions
}
}
} elseif ($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2010) {
Write-Verbose "Exchange 2010 detected."
$buildInformation.FriendlyName = "Exchange 2010"
$buildInformation.BuildNumber = $exchangeInformation.GetExchangeServer.AdminDisplayVersion.ToString()
}
Write-Verbose "Exiting: Get-ExchangeInformation"
return $exchangeInformation
}
Function Get-WmiObjectHandler {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWMICmdlet', '', Justification = 'This is what this function is for')]
[CmdletBinding()]
param(
[string]
$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $true)]
[string]
$Class,
[string]
$Filter,
[string]
$Namespace,
[scriptblock]
$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed - ComputerName: '$ComputerName' | Class: '$Class' | Filter: '$Filter' | Namespace: '$Namespace'"
$execute = @{
ComputerName = $ComputerName
Class = $Class
}
if (-not ([string]::IsNullOrEmpty($Filter))) {
$execute.Add("Filter", $Filter)
}
if (-not ([string]::IsNullOrEmpty($Namespace))) {
$execute.Add("Namespace", $Namespace)
}
}
process {
try {
$wmi = Get-WmiObject @execute -ErrorAction Stop
return $wmi
} catch {
Write-Verbose "Failed to run Get-WmiObject on class '$class'"
if ($null -ne $CatchActionFunction) {
& $CatchActionFunction
}
}
}
}
Function Get-WmiObjectCriticalHandler {
[CmdletBinding()]
param(
[string]
$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $true)]
[string]
$Class,
[string]
$Filter,
[string]
$Namespace,
[scriptblock]
$CatchActionFunction
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$params = @{
ComputerName = $ComputerName
Class = $Class
Filter = $Filter
Namespace = $Namespace
CatchActionFunction = $CatchActionFunction
}
$wmi = Get-WmiObjectHandler @params
if ($null -eq $wmi) {
throw "Failed to get critical information. Stopping the script. InnerException: $($Error[0])"
}
return $wmi
}
Function Get-ProcessorInformation {
[CmdletBinding()]
param(
[string]$MachineName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$wmiObject = $null
$processorName = [string]::Empty
$maxClockSpeed = 0
$numberOfLogicalCores = 0
$numberOfPhysicalCores = 0
$numberOfProcessors = 0
$currentClockSpeed = 0
$processorIsThrottled = $false
$differentProcessorCoreCountDetected = $false
$differentProcessorsDetected = $false
$presentedProcessorCoreCount = 0
$previousProcessor = $null
}
process {
$wmiObject = @(Get-WmiObjectCriticalHandler -ComputerName $MachineName -Class "Win32_Processor" -CatchActionFunction $CatchActionFunction)
$processorName = $wmiObject[0].Name
$maxClockSpeed = $wmiObject[0].MaxClockSpeed
Write-Verbose "Evaluating processor results"
foreach ($processor in $wmiObject) {
$numberOfPhysicalCores += $processor.NumberOfCores
$numberOfLogicalCores += $processor.NumberOfLogicalProcessors
$numberOfProcessors++
if ($processor.CurrentClockSpeed -lt $processor.MaxClockSpeed) {
Write-Verbose "Processor is being throttled"
$processorIsThrottled = $true
$currentClockSpeed = $processor.CurrentClockSpeed
}
if ($null -ne $previousProcessor) {
if ($processor.Name -ne $previousProcessor.Name -or
$processor.MaxClockSpeed -ne $previousProcessor.MaxClockSpeed) {
Write-Verbose "Different Processors are detected!!! This is an issue."
$differentProcessorsDetected = $true
}
if ($processor.NumberOfLogicalProcessors -ne $previousProcessor.NumberOfLogicalProcessors) {
Write-Verbose "Different Processor core count per processor socket detected. This is an issue."
$differentProcessorCoreCountDetected = $true
}
}
$previousProcessor = $processor
}
$presentedProcessorCoreCount = Invoke-ScriptBlockHandler -ComputerName $MachineName `
-ScriptBlock { [System.Environment]::ProcessorCount } `
-ScriptBlockDescription "Trying to get the System.Environment ProcessorCount" `
-CatchActionFunction $CatchActionFunction
if ($null -eq $presentedProcessorCoreCount) {
Write-Verbose "Wasn't able to get Presented Processor Core Count on the Server. Setting to -1."
$presentedProcessorCoreCount = -1
}
}
end {
Write-Verbose "PresentedProcessorCoreCount: $presentedProcessorCoreCount"
Write-Verbose "NumberOfPhysicalCores: $numberOfPhysicalCores | NumberOfLogicalCores: $numberOfLogicalCores | NumberOfProcessors: $numberOfProcessors"
Write-Verbose "ProcessorIsThrottled: $processorIsThrottled | CurrentClockSpeed: $currentClockSpeed"
Write-Verbose "DifferentProcessorsDetected: $differentProcessorsDetected | DifferentProcessorCoreCountDetected: $differentProcessorCoreCountDetected"
return [PSCustomObject]@{
Name = $processorName
MaxMegacyclesPerCore = $maxClockSpeed
NumberOfPhysicalCores = $numberOfPhysicalCores
NumberOfLogicalCores = $numberOfLogicalCores
NumberOfProcessors = $numberOfProcessors
CurrentMegacyclesPerCore = $currentClockSpeed
ProcessorIsThrottled = $processorIsThrottled
DifferentProcessorsDetected = $differentProcessorsDetected
DifferentProcessorCoreCountDetected = $differentProcessorCoreCountDetected
EnvironmentProcessorCount = $presentedProcessorCoreCount
ProcessorClassObject = $wmiObject
}
}
}
Function Get-ServerType {
[CmdletBinding()]
[OutputType("System.String")]
param(
[Parameter(Mandatory = $true)]
[string]
$ServerType
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed - ServerType: $ServerType"
$returnServerType = [string]::Empty
}
process {
if ($ServerType -like "VMWare*") { $returnServerType = "VMware" }
elseif ($ServerType -like "*Amazon EC2*") { $returnServerType = "AmazonEC2" }
elseif ($ServerType -like "*Microsoft Corporation*") { $returnServerType = "HyperV" }
elseif ($ServerType.Length -gt 0) { $returnServerType = "Physical" }
else { $returnServerType = "Unknown" }
}
end {
Write-Verbose "Returning: $returnServerType"
return $returnServerType
}
}
Function Get-HardwareInformation {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
[HealthChecker.HardwareInformation]$hardware_obj = New-Object HealthChecker.HardwareInformation
$system = Get-WmiObjectCriticalHandler -ComputerName $Script:Server -Class "Win32_ComputerSystem" -CatchActionFunction ${Function:Invoke-CatchActions}
$hardware_obj.MemoryInformation = Get-WmiObjectHandler -ComputerName $Script:Server -Class "Win32_PhysicalMemory" -CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -eq $hardware_obj.MemoryInformation) {
Write-Verbose "Using memory from Win32_ComputerSystem class instead. This may cause memory calculation issues."
$hardware_obj.TotalMemory = $system.TotalPhysicalMemory
} else {
foreach ($memory in $hardware_obj.MemoryInformation) {
$hardware_obj.TotalMemory += $memory.Capacity
}
}
$hardware_obj.Manufacturer = $system.Manufacturer
$hardware_obj.System = $system
$hardware_obj.AutoPageFile = $system.AutomaticManagedPagefile
$hardware_obj.ServerType = (Get-ServerType -ServerType $system.Manufacturer)
$processorInformation = Get-ProcessorInformation -MachineName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
#Need to do it this way because of Windows 2012R2
$processor = New-Object HealthChecker.ProcessorInformation
$processor.Name = $processorInformation.Name
$processor.NumberOfPhysicalCores = $processorInformation.NumberOfPhysicalCores
$processor.NumberOfLogicalCores = $processorInformation.NumberOfLogicalCores
$processor.NumberOfProcessors = $processorInformation.NumberOfProcessors
$processor.MaxMegacyclesPerCore = $processorInformation.MaxMegacyclesPerCore
$processor.CurrentMegacyclesPerCore = $processorInformation.CurrentMegacyclesPerCore
$processor.ProcessorIsThrottled = $processorInformation.ProcessorIsThrottled
$processor.DifferentProcessorsDetected = $processorInformation.DifferentProcessorsDetected
$processor.DifferentProcessorCoreCountDetected = $processorInformation.DifferentProcessorCoreCountDetected
$processor.EnvironmentProcessorCount = $processorInformation.EnvironmentProcessorCount
$processor.ProcessorClassObject = $processorInformation.ProcessorClassObject
$hardware_obj.Processor = $processor
$hardware_obj.Model = $system.Model
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $hardware_obj
}
Function Get-ServerRebootPending {
[CmdletBinding()]
param(
[string]$ServerName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Function Get-PendingFileReboot {
try {
if ((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\" -Name PendingFileRenameOperations -ErrorAction Stop)) {
return $true
}
return $false
} catch {
throw
}
}
Function Get-PendingCCMReboot {
try {
return (Invoke-CimMethod -Namespace 'Root\ccm\clientSDK' -ClassName 'CCM_ClientUtilities' -Name 'DetermineIfRebootPending' -ErrorAction Stop)
} catch {
throw
}
}
Function Get-PathTestingReboot {
param(
[string]$TestingPath
)
return (Test-Path $TestingPath)
}
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$pendingRebootLocations = New-Object 'System.Collections.Generic.List[string]'
}
process {
$pendingFileRenameOperationValue = Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PendingFileReboot} `
-ScriptBlockDescription "Get-PendingFileReboot" `
-CatchActionFunction $CatchActionFunction
if ($null -eq $pendingFileRenameOperationValue) {
$pendingFileRenameOperationValue = $false
}
$componentBasedServicingPendingRebootValue = Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PathTestingReboot} `
-ScriptBlockDescription "Component Based Servicing Reboot Pending" `
-ArgumentList "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" `
-CatchActionFunction $CatchActionFunction
$ccmReboot = Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PendingCCMReboot} `
-ScriptBlockDescription "Get-PendingSCCMReboot" `
-CatchActionFunction $CatchActionFunction
$autoUpdatePendingRebootValue = Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PathTestingReboot} `
-ScriptBlockDescription "Auto Update Pending Reboot" `
-ArgumentList "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" `
-CatchActionFunction $CatchActionFunction
$ccmRebootPending = $ccmReboot -and ($ccmReboot.RebootPending -or $ccmReboot.IsHardRebootPending)
$pendingReboot = $ccmRebootPending -or $pendingFileRenameOperationValue -or $componentBasedServicingPendingRebootValue -or $autoUpdatePendingRebootValue
if ($ccmRebootPending) {
Write-Verbose "RebootPending in CCM_ClientUtilities"
$pendingRebootLocations.Add("CCM_ClientUtilities Showing Reboot Pending")
}
if ($pendingFileRenameOperationValue) {
Write-Verbose "RebootPending at HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations"
$pendingRebootLocations.Add("HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations")
}
if ($componentBasedServicingPendingRebootValue) {
Write-Verbose "RebootPending at HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"
$pendingRebootLocations.Add("HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending")
}
if ($autoUpdatePendingRebootValue) {
Write-Verbose "RebootPending at HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"
$pendingRebootLocations.Add("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired")
}
}
end {
return [PSCustomObject]@{
PendingFileRenameOperations = $pendingFileRenameOperationValue
ComponentBasedServicingPendingReboot = $componentBasedServicingPendingRebootValue
AutoUpdatePendingReboot = $autoUpdatePendingRebootValue
CcmRebootPending = $ccmRebootPending
PendingReboot = $pendingReboot
PendingRebootLocations = $pendingRebootLocations
}
}
}
Function Get-AllTlsSettingsFromRegistry {
[CmdletBinding()]
[OutputType("System.Collections.Hashtable")]
param(
[string]$MachineName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Function Get-TLSMemberValue {
param(
[Parameter(Mandatory = $true)]
[string]
$GetKeyType,
[Parameter(Mandatory = $false)]
[object]
$KeyValue,
[Parameter(Mandatory = $true)]
[string]
$ServerClientType,
[Parameter(Mandatory = $true)]
[string]
$TlsVersion
)
Write-Verbose "KeyValue is null: '$($null -eq $KeyValue)' | KeyValue: '$KeyValue' | ServerClientType: $ServerClientType | TLSVersion: $tlsVersion | GetKeyType: $GetKeyType"
switch ($GetKeyType) {
"Enabled" {
return $null -eq $KeyValue -or $KeyValue -eq 1
}
"DisabledByDefault" {
return $null -ne $KeyValue -and $KeyValue -eq 1
}
}
}
Function Get-NETDefaultTLSValue {
param(
[Parameter(Mandatory = $false)]
[object]
$KeyValue,
[Parameter(Mandatory = $true)]
[string]
$NetVersion,
[Parameter(Mandatory = $true)]
[string]
$KeyName
)
Write-Verbose "KeyValue is null: '$($null -eq $KeyValue)' | KeyValue: '$KeyValue' | NetVersion: '$NetVersion' | KeyName: '$KeyName'"
return $null -ne $KeyValue -and $KeyValue -eq 1
}
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed - MachineName: '$MachineName'"
$registryBase = "SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS {0}\{1}"
$tlsVersions = @("1.0", "1.1", "1.2")
$keyValues = ("Enabled", "DisabledByDefault")
$netVersions = @("v2.0.50727", "v4.0.30319")
$netRegistryBase = "SOFTWARE\{0}\.NETFramework\{1}"
[HashTable]$allTlsObjects = @{}
}
process {
foreach ($tlsVersion in $tlsVersions) {
$registryServer = $registryBase -f $tlsVersion, "Server"
$registryClient = $registryBase -f $tlsVersion, "Client"
$currentTLSObject = [PSCustomObject]@{
TLSVersion = $tlsVersion
}
foreach ($getKey in $keyValues) {
$serverValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $registryServer `
-GetValue $getKey `
-CatchActionFunction $CatchActionFunction
$clientValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $registryClient `
-GetValue $getKey `
-CatchActionFunction $CatchActionFunction
$currentTLSObject | Add-Member -MemberType NoteProperty `
-Name "Server$getKey" `
-Value (Get-TLSMemberValue -GetKeyType $getKey -KeyValue $serverValue -ServerClientType "Server" -TlsVersion $tlsVersion)
$currentTLSObject | Add-Member -MemberType NoteProperty `
-Name "Server$getKey`Value" `
-Value $serverValue
$currentTLSObject | Add-Member -MemberType NoteProperty `
-Name "Client$getKey" `
-Value (Get-TLSMemberValue -GetKeyType $getKey -KeyValue $clientValue -ServerClientType "Client" -TlsVersion $tlsVersion)
$currentTLSObject | Add-Member -MemberType NoteProperty `
-Name "Client$getKey`Value" `
-Value $clientValue
}
$allTlsObjects.Add($TlsVersion, $currentTLSObject)
}
foreach ($netVersion in $netVersions) {
$currentNetTlsDefaultVersionObject = New-Object PSCustomObject
$currentNetTlsDefaultVersionObject | Add-Member -MemberType NoteProperty -Name "NetVersion" -Value $netVersion
$SystemDefaultTlsVersions = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey ($netRegistryBase -f "Microsoft", $netVersion) `
-GetValue "SystemDefaultTlsVersions" `
-CatchActionFunction $CatchActionFunction
$SchUseStrongCrypto = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey ($netRegistryBase -f "Microsoft", $netVersion) `
-GetValue "SchUseStrongCrypto" `
-CatchActionFunction $CatchActionFunction
$WowSystemDefaultTlsVersions = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey ($netRegistryBase -f "Wow6432Node\Microsoft", $netVersion) `
-GetValue "SystemDefaultTlsVersions" `
-CatchActionFunction $CatchActionFunction
$WowSchUseStrongCrypto = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey ($netRegistryBase -f "Wow6432Node\Microsoft", $netVersion) `
-GetValue "SchUseStrongCrypto" `
-CatchActionFunction $CatchActionFunction
$currentNetTlsDefaultVersionObject = [PSCustomObject]@{
NetVersion = $netVersion
SystemDefaultTlsVersions = (Get-NETDefaultTLSValue -KeyValue $SystemDefaultTlsVersions -NetVersion $netVersion -KeyName "SystemDefaultTlsVersions")
SchUseStrongCrypto = (Get-NETDefaultTLSValue -KeyValue $SchUseStrongCrypto -NetVersion $netVersion -KeyName "SchUseStrongCrypto")
WowSystemDefaultTlsVersions = (Get-NETDefaultTLSValue -KeyValue $WowSystemDefaultTlsVersions -NetVersion $netVersion -KeyName "WowSystemDefaultTlsVersions")
WowSchUseStrongCrypto = (Get-NETDefaultTLSValue -KeyValue $WowSchUseStrongCrypto -NetVersion $netVersion -KeyName "WowSchUseStrongCrypto")
SecurityProtocol = (Invoke-ScriptBlockHandler -ComputerName $MachineName -ScriptBlock { ([System.Net.ServicePointManager]::SecurityProtocol).ToString() } -CatchActionFunction $CatchActionFunction)
}
$hashKeyName = "NET{0}" -f ($netVersion.Split(".")[0])
$allTlsObjects.Add($hashKeyName, $currentNetTlsDefaultVersionObject)
}
return $allTlsObjects
}
}
Function Invoke-CatchActionErrorLoop {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[int]$CurrentErrors,
[Parameter(Mandatory = $false, Position = 1)]
[scriptblock]$CatchActionFunction
)
process {
if ($null -ne $CatchActionFunction -and
$Error.Count -ne $CurrentErrors) {
$i = 0
while ($i -lt ($Error.Count - $currentErrors)) {
& $CatchActionFunction $Error[$i]
$i++
}
}
}
}
Function Get-AllNicInformation {
[CmdletBinding()]
param(
[string]$ComputerName = $env:COMPUTERNAME,
[string]$ComputerFQDN,
[scriptblock]$CatchActionFunction
)
begin {
# Extract for Pester Testing - Start
Function Get-NicPnpCapabilitiesSetting {
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]$NicAdapterComponentId
)
begin {
$nicAdapterBasicPath = "SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}"
[int]$i = 0
Write-Verbose "Probing started to detect NIC adapter registry path"
}
process {
$registrySubKey = Get-RemoteRegistrySubKey -MachineName $ComputerName `
-SubKey $nicAdapterBasicPath
$optionalKeys = $registrySubKey.GetSubKeyNames() | Where-Object { $_ -like "0*" }
do {
$nicAdapterPnPCapabilitiesProbingKey = "$nicAdapterBasicPath\$($optionalKeys[$i])"
$netCfgInstanceId = Get-RemoteRegistryValue -MachineName $ComputerName `
-SubKey $nicAdapterPnPCapabilitiesProbingKey `
-GetValue "NetCfgInstanceId" `
-CatchActionFunction $CatchActionFunction
if ($netCfgInstanceId -eq $NicAdapterComponentId) {
Write-Verbose "Matching ComponentId found - now checking for PnPCapabilitiesValue"
$nicAdapterPnPCapabilitiesValue = Get-RemoteRegistryValue -MachineName $ComputerName `
-SubKey $nicAdapterPnPCapabilitiesProbingKey `
-GetValue "PnPCapabilities" `
-CatchActionFunction $CatchActionFunction
break
} else {
Write-Verbose "No matching ComponentId found"
$i++
}
} while ($i -lt $optionalKeys.Count)
}
end {
return [PSCustomObject]@{
PnPCapabilities = $nicAdapterPnPCapabilitiesValue
SleepyNicDisabled = ($nicAdapterPnPCapabilitiesValue -eq 24 -or $nicAdapterPnPCapabilitiesValue -eq 280)
}
}
}
# Extract for Pester Testing - End
Function Get-NetworkConfiguration {
[CmdletBinding()]
param(
[string]$ComputerName
)
begin {
$currentErrors = $Error.Count
$params = @{
ErrorAction = "Stop"
}
}
process {
try {
if (($ComputerName).Split(".")[0] -ne $env:COMPUTERNAME) {
$cimSession = New-CimSession -ComputerName $ComputerName -ErrorAction Stop
$params.Add("CimSession", $cimSession)
}
$networkIpConfiguration = Get-NetIPConfiguration @params | Where-Object { $_.NetAdapter.MediaConnectionState -eq "Connected" }
Invoke-CatchActionErrorLoop -CurrentErrors $currentErrors -CatchActionFunction $CatchActionFunction
return $networkIpConfiguration
} catch {
Write-Verbose "Failed to run Get-NetIPConfiguration. Error $($_.Exception)"
#just rethrow as caller will handle the catch
throw
}
}
}
Function Get-NicInformation {
[CmdletBinding()]
param(
[array]$NetworkConfiguration,
[bool]$WmiObject
)
begin {
Function Get-IpvAddresses {
return [PSCustomObject]@{
Address = ([string]::Empty)
Subnet = ([string]::Empty)
DefaultGateway = ([string]::Empty)
}
}
if ($null -eq $NetworkConfiguration) {
Write-Verbose "NetworkConfiguration are null in New-NicInformation. Returning a null object."
return $null
}
$nicObjects = New-Object 'System.Collections.Generic.List[object]'
}
process {
if ($WmiObject) {
$networkAdapterConfigurations = Get-WmiObjectHandler -ComputerName $ComputerName `
-Class "Win32_NetworkAdapterConfiguration" `
-Filter "IPEnabled = True" `
-CatchActionFunction $CatchActionFunction
}
foreach ($networkConfig in $NetworkConfiguration) {
$dnsClient = $null
$rssEnabledValue = 2
$netAdapterRss = $null
$mtuSize = 0
$driverDate = [DateTime]::MaxValue
$driverVersion = [string]::Empty
$description = [string]::Empty
$ipv4Address = @()
$ipv6Address = @()
$ipv6Enabled = $false
if (-not ($WmiObject)) {
Write-Verbose "Working on NIC: $($networkConfig.InterfaceDescription)"
$adapter = $networkConfig.NetAdapter
if ($adapter.DriverFileName -ne "NdisImPlatform.sys") {
$nicPnpCapabilitiesSetting = Get-NicPnpCapabilitiesSetting -NicAdapterComponentId $adapter.DeviceID
} else {
Write-Verbose "Multiplexor adapter detected. Going to skip PnpCapabilities check"
$nicPnpCapabilitiesSetting = [PSCustomObject]@{
PnPCapabilities = "MultiplexorNoPnP"
}
}
try {
$dnsClient = $adapter | Get-DnsClient -ErrorAction Stop
$isRegisteredInDns = $dnsClient.RegisterThisConnectionsAddress
Write-Verbose "Got DNS Client information"
} catch {
Write-Verbose "Failed to get the DNS client information"
Invoke-CatchActionError $CatchActionFunction
}
try {
$netAdapterRss = $adapter | Get-NetAdapterRss -ErrorAction Stop
Write-Verbose "Got Net Adapter RSS Information"
if ($null -ne $netAdapterRss) {
[int]$rssEnabledValue = $netAdapterRss.Enabled
}
} catch {
Write-Verbose "Failed to get RSS Information"
Invoke-CatchActionError $CatchActionFunction
}
foreach ($ipAddress in $networkConfig.AllIPAddresses.IPAddress) {
if ($ipAddress.Contains(":")) {
$ipv6Enabled = $true
}
}
for ($i = 0; $i -lt $networkConfig.IPv4Address.Count; $i++) {
$newIpvAddress = Get-IpvAddresses
if ($null -ne $networkConfig.IPv4Address -and
$i -lt $networkConfig.IPv4Address.Count) {
$newIpvAddress.Address = $networkConfig.IPv4Address[$i].IPAddress
$newIpvAddress.Subnet = $networkConfig.IPv4Address[$i].PrefixLength
}
if ($null -ne $networkConfig.IPv4DefaultGateway -and
$i -lt $networkConfig.IPv4Address.Count) {
$newIpvAddress.DefaultGateway = $networkConfig.IPv4DefaultGateway[$i].NextHop
}
$ipv4Address += $newIpvAddress
}
for ($i = 0; $i -lt $networkConfig.IPv6Address.Count; $i++) {
$newIpvAddress = Get-IpvAddresses
if ($null -ne $networkConfig.IPv6Address -and
$i -lt $networkConfig.IPv6Address.Count) {
$newIpvAddress.Address = $networkConfig.IPv6Address[$i].IPAddress
$newIpvAddress.Subnet = $networkConfig.IPv6Address[$i].PrefixLength
}
if ($null -ne $networkConfig.IPv6DefaultGateway -and
$i -lt $networkConfig.IPv6DefaultGateway.Count) {
$newIpvAddress.DefaultGateway = $networkConfig.IPv6DefaultGateway[$i].NextHop
}
$ipv6Address += $newIpvAddress
}
$mtuSize = $adapter.MTUSize
$driverDate = $adapter.DriverDate
$driverVersion = $adapter.DriverVersionString
$description = $adapter.InterfaceDescription
$dnsServerToBeUsed = $networkConfig.DNSServer.ServerAddresses
} else {
Write-Verbose "Working on NIC: $($networkConfig.Description)"
$adapter = $networkConfig
$description = $adapter.Description
if ($adapter.ServiceName -ne "NdisImPlatformMp") {
$nicPnpCapabilitiesSetting = Get-NicPnpCapabilitiesSetting -NicAdapterComponentId $adapter.Guid
} else {
Write-Verbose "Multiplexor adapter detected. Going to skip PnpCapabilities check"
$nicPnpCapabilitiesSetting = [PSCustomObject]@{
PnPCapabilities = "MultiplexorNoPnP"
}
}
#set the correct $adapterConfiguration to link to the correct $networkConfig that we are on
$adapterConfiguration = $networkAdapterConfigurations |
Where-Object { $_.SettingID -eq $networkConfig.GUID -or
$_.SettingID -eq $networkConfig.InterfaceGuid }
if ($null -eq $adapterConfiguration) {
Write-Verbose "Failed to find correct adapterConfiguration for this networkConfig."
Write-Verbose "GUID: $($networkConfig.GUID) | InterfaceGuid: $($networkConfig.InterfaceGuid)"
}
$ipv6Enabled = ($adapterConfiguration.IPAddress | Where-Object { $_.Contains(":") }).Count -ge 1
if ($null -ne $adapterConfiguration.DefaultIPGateway) {
$ipv4Gateway = $adapterConfiguration.DefaultIPGateway | Where-Object { $_.Contains(".") }
$ipv6Gateway = $adapterConfiguration.DefaultIPGateway | Where-Object { $_.Contains(":") }
} else {
$ipv4Gateway = "No default IPv4 gateway set"
$ipv6Gateway = "No default IPv6 gateway set"
}
for ($i = 0; $i -lt $adapterConfiguration.IPAddress.Count; $i++) {
if ($adapterConfiguration.IPAddress[$i].Contains(":")) {
$newIpv6Address = Get-IpvAddresses
if ($i -lt $adapterConfiguration.IPAddress.Count) {
$newIpv6Address.Address = $adapterConfiguration.IPAddress[$i]
$newIpv6Address.Subnet = $adapterConfiguration.IPSubnet[$i]
}
$newIpv6Address.DefaultGateway = $ipv6Gateway
$ipv6Address += $newIpv6Address
} else {
$newIpv4Address = Get-IpvAddresses
if ($i -lt $adapterConfiguration.IPAddress.Count) {
$newIpv4Address.Address = $adapterConfiguration.IPAddress[$i]
$newIpv4Address.Subnet = $adapterConfiguration.IPSubnet[$i]
}
$newIpv4Address.DefaultGateway = $ipv4Gateway
$ipv4Address += $newIpv4Address
}
}
$isRegisteredInDns = $adapterConfiguration.FullDNSRegistrationEnabled
$dnsServerToBeUsed = $adapterConfiguration.DNSServerSearchOrder
}
$nicObjects.Add([PSCustomObject]@{
WmiObject = $WmiObject
Name = $adapter.Name
LinkSpeed = ((($adapter.Speed) / 1000000).ToString() + " Mbps")
DriverDate = $driverDate
NetAdapterRss = $netAdapterRss
RssEnabledValue = $rssEnabledValue
IPv6Enabled = $ipv6Enabled
Description = $description
DriverVersion = $driverVersion
MTUSize = $mtuSize
PnPCapabilities = $nicPnpCapabilitiesSetting.PnpCapabilities
SleepyNicDisabled = $nicPnpCapabilitiesSetting.SleepyNicDisabled
IPv4Addresses = $ipv4Address
IPv6Addresses = $ipv6Address
RegisteredInDns = $isRegisteredInDns
DnsServer = $dnsServerToBeUsed
DnsClient = $dnsClient
})
}
}
end {
Write-Verbose "Found $($nicObjects.Count) active adapters on the computer."
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $nicObjects
}
}
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed - ComputerName: '$ComputerName' | ComputerFQDN: '$ComputerFQDN'"
}
process {
try {
try {
$networkConfiguration = Get-NetworkConfiguration -ComputerName $ComputerName
} catch {
Invoke-CatchActionError $CatchActionFunction
try {
if (-not ([string]::IsNullOrEmpty($ComputerFQDN))) {
$networkConfiguration = Get-NetworkConfiguration -ComputerName $ComputerFQDN
} else {
$bypassCatchActions = $true
Write-Verbose "No FQDN was passed, going to rethrow error."
throw
}
} catch {
#Just throw again
throw
}
}
if ([String]::IsNullOrEmpty($networkConfiguration)) {
# Throw if nothing was returned by previous calls.
# Can be caused when executed on Server 2008 R2 where CIM namespace ROOT/StandardCimv2 is invalid.
Write-Verbose "No value was returned by 'Get-NetworkConfiguration'. Fallback to WMI."
throw
}
return (Get-NicInformation -NetworkConfiguration $networkConfiguration)
} catch {
if (-not $bypassCatchActions) {
Invoke-CatchActionError $CatchActionFunction
}
$wmiNetworkCards = Get-WmiObjectHandler -ComputerName $ComputerName `
-Class "Win32_NetworkAdapter" `
-Filter "NetConnectionStatus ='2'" `
-CatchActionFunction $CatchActionFunction
return (Get-NicInformation -NetworkConfiguration $wmiNetworkCards -WmiObject $true)
}
}
}
Function Get-CredentialGuardEnabled {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$registryValue = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Control\LSA" `
-GetValue "LsaCfgFlags" `
-CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -ne $registryValue -and
$registryValue -ne 0) {
return $true
}
return $false
}
Function Get-HttpProxySetting {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Function GetWinHttpSettings {
param(
[Parameter(Mandatory = $true)][string]$RegistryLocation
)
$connections = Get-ItemProperty -Path $RegistryLocation
$proxyAddress = [string]::Empty
$byPassList = [string]::Empty
if (($null -ne $connections) -and
($Connections | Get-Member).Name -contains "WinHttpSettings") {
$onProxy = $true
foreach ($Byte in $Connections.WinHttpSettings) {
if ($onProxy -and
$Byte -ge 42) {
$proxyAddress += [CHAR]$Byte
} elseif (-not $onProxy -and
$Byte -ge 42) {
$byPassList += [CHAR]$Byte
} elseif (-not ([string]::IsNullOrEmpty($proxyAddress)) -and
$onProxy -and
$Byte -eq 0) {
$onProxy = $false
}
}
}
return [PSCustomObject]@{
ProxyAddress = $(if ($proxyAddress -eq [string]::Empty) { "None" } else { $proxyAddress })
ByPassList = $byPassList
}
}
$httpProxy32 = Invoke-ScriptBlockHandler -ComputerName $Script:Server `
-ScriptBlock ${Function:GetWinHttpSettings} `
-ArgumentList "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" `
-ScriptBlockDescription "Getting 32 Http Proxy Value" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$httpProxy64 = Invoke-ScriptBlockHandler -ComputerName $Script:Server `
-ScriptBlock ${Function:GetWinHttpSettings} `
-ArgumentList "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" `
-ScriptBlockDescription "Getting 64 Http Proxy Value" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$httpProxy = [PSCustomObject]@{
ProxyAddress = $(if ($httpProxy32.ProxyAddress -ne "None") { $httpProxy32.ProxyAddress } else { $httpProxy64.ProxyAddress })
ByPassList = $(if ($httpProxy32.ByPassList -ne [string]::Empty) { $httpProxy32.ByPassList } else { $httpProxy64.ByPassList })
HttpProxyDifference = $httpProxy32.ProxyAddress -ne $httpProxy64.ProxyAddress
HttpByPassDifference = $httpProxy32.ByPassList -ne $httpProxy64.ByPassList
HttpProxy32 = $httpProxy32
HttpProxy64 = $httpProxy64
}
Write-Verbose "Http Proxy 32: $($httpProxy32.ProxyAddress)"
Write-Verbose "Http By Pass List 32: $($httpProxy32.ByPassList)"
Write-Verbose "Http Proxy 64: $($httpProxy64.ProxyAddress)"
Write-Verbose "Http By Pass List 64: $($httpProxy64.ByPassList)"
Write-Verbose "Proxy Address: $($httpProxy.ProxyAddress)"
Write-Verbose "By Pass List: $($httpProxy.ByPassList)"
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $httpProxy
}
Function Get-LmCompatibilityLevelInformation {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
[HealthChecker.LmCompatibilityLevelInformation]$ServerLmCompatObject = New-Object -TypeName HealthChecker.LmCompatibilityLevelInformation
$registryValue = Get-RemoteRegistryValue -RegistryHive "LocalMachine" `
-MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Control\Lsa" `
-GetValue "LmCompatibilityLevel" `
-ValueType "DWord" `
-CatchActionFunction ${Function:Invoke-CatchActions}
if ($null -eq $registryValue) {
$registryValue = 3
}
$ServerLmCompatObject.RegistryValue = $registryValue
Write-Verbose "LmCompatibilityLevel Registry Value: $registryValue"
Switch ($ServerLmCompatObject.RegistryValue) {
0 { $ServerLmCompatObject.Description = "Clients use LM and NTLM authentication, but they never use NTLMv2 session security. Domain controllers accept LM, NTLM, and NTLMv2 authentication." }
1 { $ServerLmCompatObject.Description = "Clients use LM and NTLM authentication, and they use NTLMv2 session security if the server supports it. Domain controllers accept LM, NTLM, and NTLMv2 authentication." }
2 { $ServerLmCompatObject.Description = "Clients use only NTLM authentication, and they use NTLMv2 session security if the server supports it. Domain controller accepts LM, NTLM, and NTLMv2 authentication." }
3 { $ServerLmCompatObject.Description = "Clients use only NTLMv2 authentication, and they use NTLMv2 session security if the server supports it. Domain controllers accept LM, NTLM, and NTLMv2 authentication." }
4 { $ServerLmCompatObject.Description = "Clients use only NTLMv2 authentication, and they use NTLMv2 session security if the server supports it. Domain controller refuses LM authentication responses, but it accepts NTLM and NTLMv2." }
5 { $ServerLmCompatObject.Description = "Clients use only NTLMv2 authentication, and they use NTLMv2 session security if the server supports it. Domain controller refuses LM and NTLM authentication responses, but it accepts NTLMv2." }
}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
Return $ServerLmCompatObject
}
Function Get-PageFileInformation {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$pageFiles = @(Get-WmiObjectHandler -ComputerName $Script:Server -Class "Win32_PageFileSetting" -CatchActionFunction ${Function:Invoke-CatchActions})
$pageFileList = New-Object 'System.Collections.Generic.List[object]'
if ($null -eq $pageFiles -or
$pageFiles.Count -eq 0) {
Write-Verbose "Found No Page File Settings"
$pageFileList.Add([PSCustomObject]@{
Name = [string]::Empty
InitialSize = 0
MaximumSize = 0
})
} else {
Write-Verbose "Found $($pageFiles.Count) different page files"
}
foreach ($pageFile in $pageFiles) {
$pageFileList.Add([PSCustomObject]@{
Name = $pageFile.Name
InitialSize = $pageFile.InitialSize
MaximumSize = $pageFile.MaximumSize
})
}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $pageFileList
}
Function Get-ServerOperatingSystemVersion {
[CmdletBinding()]
[OutputType("System.String")]
param(
[string]$OsCaption
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$osReturnValue = [string]::Empty
}
process {
if ([string]::IsNullOrEmpty($OsCaption)) {
Write-Verbose "Getting the local machine version build number"
$OsCaption = (Get-WmiObjectHandler -Class "Win32_OperatingSystem").Caption
}
Write-Verbose "OsCaption: '$OsCaption'"
switch -Wildcard ($OsCaption) {
"*Server 2008 R2*" { $osReturnValue = "Windows2008R2"; break }
"*Server 2008*" { $osReturnValue = "Windows2008" }
"*Server 2012 R2*" { $osReturnValue = "Windows2012R2"; break }
"*Server 2012*" { $osReturnValue = "Windows2012" }
"*Server 2016*" { $osReturnValue = "Windows2016" }
"*Server 2019*" { $osReturnValue = "Windows2019" }
"Microsoft Windows Server Standard" { $osReturnValue = "WindowsCore" }
"Microsoft Windows Server Datacenter" { $osReturnValue = "WindowsCore" }
default { $osReturnValue = "Unknown" }
}
}
end {
Write-Verbose "Returned: '$osReturnValue'"
return [string]$osReturnValue
}
}
Function Get-Smb1ServerSettings {
[CmdletBinding()]
param(
[string]$ServerName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$smbServerConfiguration = $null
$windowsFeature = $null
}
process {
$smbServerConfiguration = Invoke-ScriptBlockHandler -ComputerName $ServerName `
-ScriptBlock { Get-SmbServerConfiguration } `
-CatchActionFunction $CatchActionFunction `
-ScriptBlockDescription "Get-SmbServerConfiguration"
try {
$windowsFeature = Get-WindowsFeature "FS-SMB1" -ComputerName $ServerName -ErrorAction Stop
} catch {
Write-Verbose "Failed to Get-WindowsFeature for FS-SMB1"
Invoke-CatchActionError $CatchActionFunction
}
}
end {
return [PSCustomObject]@{
SmbServerConfiguration = $smbServerConfiguration
WindowsFeature = $windowsFeature
SuccessfulGetInstall = $null -ne $windowsFeature
SuccessfulGetBlocked = $null -ne $smbServerConfiguration
Installed = $windowsFeature.Installed -eq $true
IsBlocked = $smbServerConfiguration.EnableSMB1Protocol -eq $false
}
}
}
Function Get-TimeZoneInformationRegistrySettings {
[CmdletBinding()]
param(
[string]$MachineName = $env:COMPUTERNAME,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$timeZoneInformationSubKey = "SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
$actionsToTake = @()
$dstIssueDetected = $false
}
process {
$dynamicDaylightTimeDisabled = Get-RemoteRegistryValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "DynamicDaylightTimeDisabled" -CatchActionFunction $CatchActionFunction
$timeZoneKeyName = Get-RemoteRegistryValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "TimeZoneKeyName" -CatchActionFunction $CatchActionFunction
$standardStart = Get-RemoteRegistryValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "StandardStart" -CatchActionFunction $CatchActionFunction
$daylightStart = Get-RemoteRegistryValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "DaylightStart" -CatchActionFunction $CatchActionFunction
if ([string]::IsNullOrEmpty($timeZoneKeyName)) {
Write-Verbose "TimeZoneKeyName is null or empty. Action should be taken to address this."
$actionsToTake += "TimeZoneKeyName is blank. Need to switch your current time zone to a different value, then switch it back to have this value populated again."
}
$standardStartNonZeroValue = ($null -ne ($standardStart | Where-Object { $_ -ne 0 }))
$daylightStartNonZeroValue = ($null -ne ($daylightStart | Where-Object { $_ -ne 0 }))
if ($dynamicDaylightTimeDisabled -ne 0 -and
($standardStartNonZeroValue -or
$daylightStartNonZeroValue)) {
Write-Verbose "Determined that there is a chance the settings set could cause a DST issue."
$dstIssueDetected = $true
$actionsToTake += "High Warning: DynamicDaylightTimeDisabled is set, Windows can not properly detect any DST rule changes in your time zone. `
It is possible that you could be running into this issue. Set 'Adjust for daylight saving time automatically to on'"
} elseif ($dynamicDaylightTimeDisabled -ne 0) {
Write-Verbose "Daylight savings auto adjustment is disabled."
$actionsToTake += "Warning: DynamicDaylightTimeDisabled is set, Windows can not properly detect any DST rule changes in your time zone."
}
}
end {
return [PSCustomObject]@{
DynamicDaylightTimeDisabled = $dynamicDaylightTimeDisabled
TimeZoneKeyName = $timeZoneKeyName
StandardStart = $standardStart
DaylightStart = $daylightStart
DstIssueDetected = $dstIssueDetected
ActionsToTake = $actionsToTake
}
}
}
Function Get-CounterSamples {
param(
[Parameter(Mandatory = $true)][array]$MachineNames,
[Parameter(Mandatory = $true)][array]$Counters
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
try {
$counterSamples = (Get-Counter -ComputerName $MachineNames -Counter $Counters -ErrorAction Stop).CounterSamples
} catch {
Invoke-CatchActions
Write-Verbose "Failed to get counter samples"
}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $counterSamples
}
Function Get-OperatingSystemInformation {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
[HealthChecker.OperatingSystemInformation]$osInformation = New-Object HealthChecker.OperatingSystemInformation
$win32_OperatingSystem = Get-WmiObjectCriticalHandler -ComputerName $Script:Server -Class Win32_OperatingSystem -CatchActionFunction ${Function:Invoke-CatchActions}
$win32_PowerPlan = Get-WmiObjectHandler -ComputerName $Script:Server -Class Win32_PowerPlan -Namespace 'root\cimv2\power' -Filter "isActive='true'" -CatchActionFunction ${Function:Invoke-CatchActions}
$currentDateTime = Get-Date
$lastBootUpTime = [Management.ManagementDateTimeConverter]::ToDateTime($win32_OperatingSystem.lastbootuptime)
$osInformation.BuildInformation.VersionBuild = $win32_OperatingSystem.Version
$osInformation.BuildInformation.MajorVersion = (Get-ServerOperatingSystemVersion -OsCaption $win32_OperatingSystem.Caption)
$osInformation.BuildInformation.FriendlyName = $win32_OperatingSystem.Caption
$osInformation.BuildInformation.OperatingSystem = $win32_OperatingSystem
$osInformation.ServerBootUp.Days = ($currentDateTime - $lastBootUpTime).Days
$osInformation.ServerBootUp.Hours = ($currentDateTime - $lastBootUpTime).Hours
$osInformation.ServerBootUp.Minutes = ($currentDateTime - $lastBootUpTime).Minutes
$osInformation.ServerBootUp.Seconds = ($currentDateTime - $lastBootUpTime).Seconds
if ($null -ne $win32_PowerPlan) {
if ($win32_PowerPlan.InstanceID -eq "Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c}") {
Write-Verbose "High Performance Power Plan is set to true"
$osInformation.PowerPlan.HighPerformanceSet = $true
} else { Write-Verbose "High Performance Power Plan is NOT set to true" }
$osInformation.PowerPlan.PowerPlanSetting = $win32_PowerPlan.ElementName
} else {
Write-Verbose "Power Plan Information could not be read"
$osInformation.PowerPlan.PowerPlanSetting = "N/A"
}
$osInformation.PowerPlan.PowerPlan = $win32_PowerPlan
$osInformation.PageFile = Get-PageFileInformation
$osInformation.NetworkInformation.NetworkAdapters = (Get-AllNicInformation -ComputerName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions} -ComputerFQDN $Script:ServerFQDN)
foreach ($adapter in $osInformation.NetworkInformation.NetworkAdapters) {
if (!$adapter.IPv6Enabled) {
$osInformation.NetworkInformation.IPv6DisabledOnNICs = $true
break
}
}
$osInformation.NetworkInformation.IPv6DisabledComponents = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" `
-GetValue "DisabledComponents" `
-ValueType "DWord" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.NetworkInformation.TCPKeepAlive = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" `
-GetValue "KeepAliveTime" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.NetworkInformation.RpcMinConnectionTimeout = Get-RemoteRegistryValue -MachineName $Script:Server `
-SubKey "Software\Policies\Microsoft\Windows NT\RPC\" `
-GetValue "MinimumConnectionTimeout" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.NetworkInformation.HttpProxy = Get-HttpProxySetting
$osInformation.InstalledUpdates.HotFixes = (Get-HotFix -ComputerName $Script:Server -ErrorAction SilentlyContinue) #old school check still valid and faster and a failsafe
$osInformation.LmCompatibility = Get-LmCompatibilityLevelInformation
$counterSamples = (Get-CounterSamples -MachineNames $Script:Server -Counters "\Network Interface(*)\Packets Received Discarded")
if ($null -ne $counterSamples) {
$osInformation.NetworkInformation.PacketsReceivedDiscarded = $counterSamples
}
$osInformation.ServerPendingReboot = (Get-ServerRebootPending -ServerName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions})
$timeZoneInformation = Get-TimeZoneInformationRegistrySettings -MachineName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.TimeZone.DynamicDaylightTimeDisabled = $timeZoneInformation.DynamicDaylightTimeDisabled
$osInformation.TimeZone.TimeZoneKeyName = $timeZoneInformation.TimeZoneKeyName
$osInformation.TimeZone.StandardStart = $timeZoneInformation.StandardStart
$osInformation.TimeZone.DaylightStart = $timeZoneInformation.DaylightStart
$osInformation.TimeZone.DstIssueDetected = $timeZoneInformation.DstIssueDetected
$osInformation.TimeZone.ActionsToTake = $timeZoneInformation.ActionsToTake
$osInformation.TimeZone.CurrentTimeZone = Invoke-ScriptBlockHandler -ComputerName $Script:Server `
-ScriptBlock { ([System.TimeZone]::CurrentTimeZone).StandardName } `
-ScriptBlockDescription "Getting Current Time Zone" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.TLSSettings = Get-AllTlsSettingsFromRegistry -MachineName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.VcRedistributable = Get-VisualCRedistributableInstalledVersion -ComputerName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.CredentialGuardEnabled = Get-CredentialGuardEnabled
$osInformation.RegistryValues.CurrentVersionUbr = Get-RemoteRegistryValue `
-MachineName $Script:Server `
-SubKey "SOFTWARE\Microsoft\Windows NT\CurrentVersion" `
-GetValue "UBR" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.RegistryValues.LanManServerDisabledCompression = Get-RemoteRegistryValue `
-MachineName $Script:Server `
-SubKey "SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" `
-GetValue "DisableCompression" `
-CatchActionFunction ${Function:Invoke-CatchActions}
$osInformation.Smb1ServerSettings = Get-Smb1ServerSettings -ServerName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
Write-Verbose "Exiting: $($MyInvocation.MyCommand)"
return $osInformation
}
Function Get-DotNetDllFileVersions {
[CmdletBinding()]
[OutputType("System.Collections.Hashtable")]
param(
[string]$ComputerName,
[array]$FileNames,
[scriptblock]$CatchActionFunction
)
begin {
Function Invoke-ScriptBlockGetItem {
param(
[string]$FilePath
)
$getItem = Get-Item $FilePath
$returnObject = ([PSCustomObject]@{
GetItem = $getItem
LastWriteTimeUtc = $getItem.LastWriteTimeUtc
VersionInfo = ([PSCustomObject]@{
FileMajorPart = $getItem.VersionInfo.FileMajorPart
FileMinorPart = $getItem.VersionInfo.FileMinorPart
FileBuildPart = $getItem.VersionInfo.FileBuildPart
FilePrivatePart = $getItem.VersionInfo.FilePrivatePart
})
})
return $returnObject
}
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$dotNetInstallPath = [string]::Empty
$files = @{}
}
process {
$dotNetInstallPath = Get-RemoteRegistryValue -MachineName $ComputerName `
-SubKey "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" `
-GetValue "InstallPath" `
-CatchActionFunction $CatchActionFunction
if ([string]::IsNullOrEmpty($dotNetInstallPath)) {
Write-Verbose "Failed to determine .NET install path"
return
}
foreach ($fileName in $FileNames) {
Write-Verbose "Querying for .NET DLL File $fileName"
$getItem = Invoke-ScriptBlockHandler -ComputerName $ComputerName `
-ScriptBlock ${Function:Invoke-ScriptBlockGetItem} `
-ArgumentList ("{0}\{1}" -f $dotNetInstallPath, $filename) `
-CatchActionFunction $CatchActionFunction
$files.Add($fileName, $getItem)
}
}
end {
return $files
}
}
Function Get-NETFrameworkVersion {
[CmdletBinding()]
param(
[string]$MachineName = $env:COMPUTERNAME,
[int]$NetVersionKey = -1,
[scriptblock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$friendlyName = [string]::Empty
$minValue = -1
}
process {
if ($NetVersionKey -eq -1) {
[int]$NetVersionKey = Get-RemoteRegistryValue -MachineName $MachineName `
-SubKey "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" `
-GetValue "Release" `
-CatchActionFunction $CatchActionFunction
}
#Using Minimum Version as per https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed?redirectedfrom=MSDN#minimum-version
if ($NetVersionKey -lt 378389) {
$friendlyName = "Unknown"
$minValue = -1
} elseif ($NetVersionKey -lt 378675) {
$friendlyName = "4.5"
$minValue = 378389
} elseif ($NetVersionKey -lt 379893) {
$friendlyName = "4.5.1"
$minValue = 378675
} elseif ($NetVersionKey -lt 393295) {
$friendlyName = "4.5.2"
$minValue = 379893
} elseif ($NetVersionKey -lt 394254) {
$friendlyName = "4.6"
$minValue = 393295
} elseif ($NetVersionKey -lt 394802) {
$friendlyName = "4.6.1"
$minValue = 394254
} elseif ($NetVersionKey -lt 460798) {
$friendlyName = "4.6.2"
$minValue = 394802
} elseif ($NetVersionKey -lt 461308) {
$friendlyName = "4.7"
$minValue = 460798
} elseif ($NetVersionKey -lt 461808) {
$friendlyName = "4.7.1"
$minValue = 461308
} elseif ($NetVersionKey -lt 528040) {
$friendlyName = "4.7.2"
$minValue = 461808
} elseif ($NetVersionKey -ge 528040) {
$friendlyName = "4.8"
$minValue = 528040
}
}
end {
Write-Verbose "FriendlyName: $friendlyName | RegistryValue: $netVersionKey | MinimumValue: $minValue"
return [PSCustomObject]@{
FriendlyName = $friendlyName
RegistryValue = $NetVersionKey
MinimumValue = $minValue
}
}
}
Function Get-HealthCheckerExchangeServer {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
[HealthChecker.HealthCheckerExchangeServer]$HealthExSvrObj = New-Object -TypeName HealthChecker.HealthCheckerExchangeServer
$HealthExSvrObj.ServerName = $Script:Server
$HealthExSvrObj.HardwareInformation = Get-HardwareInformation
$HealthExSvrObj.OSInformation = Get-OperatingSystemInformation
$HealthExSvrObj.ExchangeInformation = Get-ExchangeInformation -OSMajorVersion $HealthExSvrObj.OSInformation.BuildInformation.MajorVersion
if ($HealthExSvrObj.ExchangeInformation.BuildInformation.MajorVersion -ge [HealthChecker.ExchangeMajorVersion]::Exchange2013) {
$netFrameworkVersion = Get-NETFrameworkVersion -MachineName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
$HealthExSvrObj.OSInformation.NETFramework.FriendlyName = $netFrameworkVersion.FriendlyName
$HealthExSvrObj.OSInformation.NETFramework.RegistryValue = $netFrameworkVersion.RegistryValue
$HealthExSvrObj.OSInformation.NETFramework.NetMajorVersion = $netFrameworkVersion.MinimumValue
$HealthExSvrObj.OSInformation.NETFramework.FileInformation = Get-DotNetDllFileVersions -ComputerName $Script:Server -FileNames @("System.Data.dll", "System.Configuration.dll") -CatchActionFunction ${Function:Invoke-CatchActions}
if ($netFrameworkVersion.MinimumValue -eq $HealthExSvrObj.ExchangeInformation.NETFramework.MaxSupportedVersion) {
$HealthExSvrObj.ExchangeInformation.NETFramework.OnRecommendedVersion = $true
}
}
$HealthExSvrObj.HealthCheckerVersion = $BuildVersion
$HealthExSvrObj.GenerationTime = [datetime]::Now
Write-Verbose "Finished building health Exchange Server Object for server: $Script:Server"
return $HealthExSvrObj
}
Function Get-ErrorsThatOccurred {
if ($Error.Count -gt 0) {
Write-Grey(" "); Write-Grey(" ")
Function Write-Errors {
Write-Verbose "`r`n`r`nErrors that occurred that wasn't handled"
$index = 0
$Error |
ForEach-Object {
$index++
$currentError = $_
$handledError = $Script:ErrorsExcluded |
Where-Object { $_.Equals($currentError) }
if ($null -eq $handledError) {
Write-Verbose "Error Index: $index"
Write-Verbose $currentError
if ($null -ne $currentError.ScriptStackTrace) {
Write-Verbose $currentError.ScriptStackTrace
}
Write-Verbose "-----------------------------------`r`n`r`n"
}
}
Write-Verbose "`r`n`r`nErrors that were handled"
$index = 0
$Error |
ForEach-Object {
$index++
$currentError = $_
$handledError = $Script:ErrorsExcluded |
Where-Object { $_.Equals($currentError) }
if ($null -ne $handledError) {
Write-Verbose "Error Index: $index"
Write-Verbose $handledError
if ($null -ne $handledError.ScriptStackTrace) {
Write-Verbose $handledError.ScriptStackTrace
}
Write-Verbose "-----------------------------------`r`n`r`n"
}
}
}
if ($Error.Count -ne $Script:ErrorsExcludedCount) {
Write-Red("There appears to have been some errors in the script. To assist with debugging of the script, please send the HealthChecker-Debug_*.txt, HealthChecker-Errors.json, and .xml file to ExToolsFeedback@microsoft.com.")
$Script:Logger.PreventLogCleanup = $true
Write-Errors
#Need to convert Error to Json because running into odd issues with trying to export $Error out in my lab. Got StackOverflowException for one of the errors i always see there.
try {
$Error |
ConvertTo-Json |
Out-File ("$OutputFilePath\HealthChecker-Errors.json")
} catch {
Write-Red("Failed to export the HealthChecker-Errors.json")
Invoke-CatchActions
}
} elseif ($Script:VerboseEnabled -or
$SaveDebugLog) {
Write-Verbose "All errors that occurred were in try catch blocks and was handled correctly."
$Script:Logger.PreventLogCleanup = $true
Write-Errors
}
} else {
Write-Verbose "No errors occurred in the script."
}
}
Function Get-HealthCheckFilesItemsFromLocation {
$items = Get-ChildItem $XMLDirectoryPath | Where-Object { $_.Name -like "HealthChecker-*-*.xml" }
if ($null -eq $items) {
Write-Host("Doesn't appear to be any Health Check XML files here....stopping the script")
exit
}
return $items
}
Function Get-OnlyRecentUniqueServersXMLs {
param(
[Parameter(Mandatory = $true)][array]$FileItems
)
$aObject = @()
foreach ($item in $FileItems) {
$obj = New-Object PSCustomObject
[string]$itemName = $item.Name
$ServerName = $itemName.Substring(($itemName.IndexOf("-") + 1), ($itemName.LastIndexOf("-") - $itemName.IndexOf("-") - 1))
$obj | Add-Member -MemberType NoteProperty -Name ServerName -Value $ServerName
$obj | Add-Member -MemberType NoteProperty -Name FileName -Value $itemName
$obj | Add-Member -MemberType NoteProperty -Name FileObject -Value $item
$aObject += $obj
}
$grouped = $aObject | Group-Object ServerName
$FilePathList = @()
foreach ($gServer in $grouped) {
if ($gServer.Count -gt 1) {
#going to only use the most current file for this server providing that they are using the newest updated version of Health Check we only need to sort by name
$groupData = $gServer.Group #because of win2008
$FilePathList += ($groupData | Sort-Object FileName -Descending | Select-Object -First 1).FileObject.VersionInfo.FileName
} else {
$FilePathList += ($gServer.Group).FileObject.VersionInfo.FileName
}
}
return $FilePathList
}
Function Import-MyData {
param(
[Parameter(Mandatory = $true)][array]$FilePaths
)
[System.Collections.Generic.List[System.Object]]$myData = New-Object -TypeName System.Collections.Generic.List[System.Object]
foreach ($filePath in $FilePaths) {
$importData = Import-Clixml -Path $filePath
$myData.Add($importData)
}
return $myData
}
Function Confirm-ExchangeShell {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Identity,
[Parameter(Mandatory = $false)]
[bool]$LoadExchangeShell = $true,
[Parameter(Mandatory = $false)]
[scriptblock]$CatchActionFunction
)
begin {
$passed = $false
$edgeTransportKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\EdgeTransportRole'
$setupKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup'
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Verbose "Passed: LoadExchangeShell: $LoadExchangeShell | Identity: $Identity"
$params = @{
Identity = $Identity
ErrorAction = "Stop"
}
}
process {
try {
$currentErrors = $Error.Count
Get-ExchangeServer @params | Out-Null
Write-Verbose "Exchange PowerShell Module already loaded."
$passed = $true
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction
} catch {
Write-Verbose "Failed to run Get-ExchangeServer"
Invoke-CatchActionError $CatchActionFunction
if (-not ($LoadExchangeShell)) {
return
}
#Test 32 bit process, as we can't see the registry if that is the case.
if (-not ([System.Environment]::Is64BitProcess)) {
Write-Warning "Open a 64 bit PowerShell process to continue"
return
}
if (Test-Path "$setupKey") {
$currentErrors = $Error.Count
Write-Verbose "We are on Exchange 2013 or newer"
try {
if (Test-Path $edgeTransportKey) {
Write-Verbose "We are on Exchange Edge Transport Server"
[xml]$PSSnapIns = Get-Content -Path "$env:ExchangeInstallPath\Bin\exshell.psc1" -ErrorAction Stop
foreach ($PSSnapIn in $PSSnapIns.PSConsoleFile.PSSnapIns.PSSnapIn) {
Write-Verbose "Trying to add PSSnapIn: {0}" -f $PSSnapIn.Name
Add-PSSnapin -Name $PSSnapIn.Name -ErrorAction Stop
}
Import-Module $env:ExchangeInstallPath\bin\Exchange.ps1 -ErrorAction Stop
} else {
Import-Module $env:ExchangeInstallPath\bin\RemoteExchange.ps1 -ErrorAction Stop
Connect-ExchangeServer -Auto -ClientApplication:ManagementShell
}
Write-Verbose "Imported Module. Trying Get-Exchange Server Again"
Get-ExchangeServer @params | Out-Null
$passed = $true
Write-Verbose "Successfully loaded Exchange Management Shell"
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction
} catch {
Write-Warning "Failed to Load Exchange PowerShell Module..."
Invoke-CatchActionError $CatchActionFunction
}
} else {
Write-Verbose "Not on an Exchange or Tools server"
}
}
}
end {
$currentErrors = $Error.Count
$returnObject = [PSCustomObject]@{
ShellLoaded = $passed
Major = ((Get-ItemProperty -Path $setupKey -Name "MsiProductMajor" -ErrorAction SilentlyContinue).MsiProductMajor)
Minor = ((Get-ItemProperty -Path $setupKey -Name "MsiProductMinor" -ErrorAction SilentlyContinue).MsiProductMinor)
Build = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMajor" -ErrorAction SilentlyContinue).MsiBuildMajor)
Revision = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMinor" -ErrorAction SilentlyContinue).MsiBuildMinor)
EdgeServer = $passed -and (Test-Path $setupKey) -and (Test-Path $edgeTransportKey)
ToolsOnly = $passed -and (Test-Path $setupKey) -and (!(Test-Path $edgeTransportKey)) -and `
($null -eq (Get-ItemProperty -Path $setupKey -Name "Services" -ErrorAction SilentlyContinue))
RemoteShell = $passed -and (!(Test-Path $setupKey))
}
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction
return $returnObject
}
}
Function Invoke-ScriptLogFileLocation {
param(
[Parameter(Mandatory = $true)][string]$FileName,
[Parameter(Mandatory = $false)][bool]$IncludeServerName = $false
)
$endName = "-{0}.txt" -f $dateTimeStringFormat
if ($IncludeServerName) {
$endName = "-{0}{1}" -f $Script:Server, $endName
}
$Script:OutputFullPath = "{0}\{1}{2}" -f $OutputFilePath, $FileName, $endName
$Script:OutXmlFullPath = $Script:OutputFullPath.Replace(".txt", ".xml")
if ($AnalyzeDataOnly -or
$BuildHtmlServersReport -or
$ScriptUpdateOnly) {
return
}
$Script:ExchangeShellComputer = Confirm-ExchangeShell -Identity $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions}
if (!($Script:ExchangeShellComputer.ShellLoaded)) {
Write-Yellow("Failed to load Exchange Shell... stopping script")
exit
}
if ($Script:ExchangeShellComputer.ToolsOnly -and
$env:COMPUTERNAME -eq $Script:Server -and
!($LoadBalancingReport)) {
Write-Yellow("Can't run Exchange Health Checker Against a Tools Server. Use the -Server Parameter and provide the server you want to run the script against.")
exit
}
Write-Verbose("Script Executing on Server $env:COMPUTERNAME")
Write-Verbose("ToolsOnly: $($Script:ExchangeShellComputer.ToolsOnly) | RemoteShell $($Script:ExchangeShellComputer.RemoteShell)")
}
Function Test-RequiresServerFqdn {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$tempServerName = ($Script:Server).Split(".")
if ($tempServerName[0] -eq $env:COMPUTERNAME) {
Write-Verbose "Executed against the local machine. No need to pass '-ComputerName' parameter."
return
} else {
try {
$Script:ServerFQDN = (Get-ExchangeServer $Script:Server -ErrorAction Stop).FQDN
} catch {
Invoke-CatchActions
Write-Verbose "Unable to query Fqdn via 'Get-ExchangeServer'"
}
}
try {
Invoke-Command -ComputerName $Script:Server -ScriptBlock { Get-Date | Out-Null } -ErrorAction Stop
Write-Verbose "Connected successfully using: $($Script:Server)."
} catch {
Invoke-CatchActions
if ($tempServerName.Count -gt 1) {
$Script:Server = $tempServerName[0]
} else {
$Script:Server = $Script:ServerFQDN
}
try {
Invoke-Command -ComputerName $Script:Server -ScriptBlock { Get-Date | Out-Null } -ErrorAction Stop
Write-Verbose "Fallback to: $($Script:Server) Connection was successfully established."
} catch {
Write-Red("Failed to run against: {0}. Please try to run the script locally on: {0} for results. " -f $Script:Server)
exit
}
}
}
try {
#Enums and custom data types
Add-Type -TypeDefinition @"
using System;
using System.Collections;
namespace HealthChecker
{
public class HealthCheckerExchangeServer
{
public string ServerName; //String of the server that we are working with
public HardwareInformation HardwareInformation; // Hardware Object Information
public OperatingSystemInformation OSInformation; // OS Version Object Information
public ExchangeInformation ExchangeInformation; //Detailed Exchange Information
public string HealthCheckerVersion; //To determine the version of the script on the object.
public DateTime GenerationTime; //Time stamp of running the script
}
// ExchangeInformation
public class ExchangeInformation
{
public ExchangeBuildInformation BuildInformation = new ExchangeBuildInformation(); //Exchange build information
public object GetExchangeServer; //Stores the Get-ExchangeServer Object
public object GetMailboxServer; //Stores the Get-MailboxServer Object
public object GetOwaVirtualDirectory; //Stores the Get-OwaVirtualDirectory Object
public object GetWebServicesVirtualDirectory; //stores the Get-WebServicesVirtualDirectory object
public object GetOrganizationConfig; //Stores the result from Get-OrganizationConfig
public object msExchStorageGroup; //Stores the properties of the 'ms-Exch-Storage-Group' Schema class
public object GetHybridConfiguration; //Stores the Get-HybridConfiguration Object
public bool EnableDownloadDomains = new bool(); //True if Download Domains are enabled on org level
public ExchangeNetFrameworkInformation NETFramework = new ExchangeNetFrameworkInformation();
public bool MapiHttpEnabled; //Stored from organization config
public System.Array ExchangeServicesNotRunning; //Contains the Exchange services not running by Test-ServiceHealth
public Hashtable ApplicationPools = new Hashtable();
public ExchangeRegistryValues RegistryValues = new ExchangeRegistryValues();
public ExchangeServerMaintenance ServerMaintenance;
public System.Array ExchangeCertificates; //stores all the Exchange certificates on the servers.
public object ExchangeEmergencyMitigationService; //stores the Exchange Emergency Mitigation Service (EEMS) object
public Hashtable ApplicationConfigFileStatus = new Hashtable();
}
public class ExchangeBuildInformation
{
public ExchangeServerRole ServerRole; //Roles that are currently set and installed.
public ExchangeMajorVersion MajorVersion; //Exchange Version (Exchange 2010/2013/2019)
public ExchangeCULevel CU; // Exchange CU Level
public string FriendlyName; //Exchange Friendly Name is provided
public string BuildNumber; //Exchange Build Number
public string LocalBuildNumber; //Local Build Number. Is only populated if from a Tools Machine
public string ReleaseDate; // Exchange release date for which the CU they are currently on
public bool SupportedBuild; //Determines if we are within the correct build of Exchange.
public object ExchangeSetup; //Stores the Get-Command ExSetup object
public System.Array KBsInstalled; //Stored object IU or Security KB fixes
public bool March2021SUInstalled; //True if March 2021 SU is installed
}
public class ExchangeNetFrameworkInformation
{
public NetMajorVersion MinSupportedVersion; //Min Supported .NET Framework version
public NetMajorVersion MaxSupportedVersion; //Max (Recommended) Supported .NET version.
public bool OnRecommendedVersion; //RecommendedNetVersion Info includes all the factors. Windows Version & CU.
public string DisplayWording; //Display if we are in Support or not
}
public class ExchangeServerMaintenance
{
public System.Array InactiveComponents;
public object GetServerComponentState;
public object GetClusterNode;
public object GetMailboxServer; //TODO: Remove this
}
//enum for CU levels of Exchange
public enum ExchangeCULevel
{
Unknown,
Preview,
RTM,
CU1,
CU2,
CU3,
CU4,
CU5,
CU6,
CU7,
CU8,
CU9,
CU10,
CU11,
CU12,
CU13,
CU14,
CU15,
CU16,
CU17,
CU18,
CU19,
CU20,
CU21,
CU22,
CU23
}
//enum for the server roles that the computer is
public enum ExchangeServerRole
{
MultiRole,
Mailbox,
ClientAccess,
Hub,
Edge,
None
}
//enum for the Exchange version
public enum ExchangeMajorVersion
{
Unknown,
Exchange2010,
Exchange2013,
Exchange2016,
Exchange2019
}
public class ExchangeRegistryValues
{
public int CtsProcessorAffinityPercentage; //Stores the CtsProcessorAffinityPercentage registry value from HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ExchangeServer\v15\Search\SystemParameters
public int FipsAlgorithmPolicyEnabled; //Stores the Enabled value from HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy
}
// End ExchangeInformation
// OperatingSystemInformation
public class OperatingSystemInformation
{
public OSBuildInformation BuildInformation = new OSBuildInformation(); // contains build information
public NetworkInformation NetworkInformation = new NetworkInformation(); //stores network information and settings
public PowerPlanInformation PowerPlan = new PowerPlanInformation(); //stores the power plan information
public object PageFile; //stores the page file information
public LmCompatibilityLevelInformation LmCompatibility; // stores Lm Compatibility Level Information
public object ServerPendingReboot; // determine if server is pending a reboot.
public TimeZoneInformation TimeZone = new TimeZoneInformation(); //stores time zone information
public Hashtable TLSSettings; // stores the TLS settings on the server.
public InstalledUpdatesInformation InstalledUpdates = new InstalledUpdatesInformation(); //store the install update
public ServerBootUpInformation ServerBootUp = new ServerBootUpInformation(); // stores the server boot up time information
public System.Array VcRedistributable; //stores the Visual C++ Redistributable
public OSNetFrameworkInformation NETFramework = new OSNetFrameworkInformation(); //stores OS Net Framework
public bool CredentialGuardEnabled;
public OSRegistryValues RegistryValues = new OSRegistryValues();
public object Smb1ServerSettings;
}
public class OSBuildInformation
{
public OSServerVersion MajorVersion; //OS Major Version
public string VersionBuild; //hold the build number
public string FriendlyName; //string holder of the Windows Server friendly name
public object OperatingSystem; // holds Win32_OperatingSystem
}
public class NetworkInformation
{
public double TCPKeepAlive; // value used for the TCP/IP keep alive value in the registry
public double RpcMinConnectionTimeout; //holds the value for the RPC minimum connection timeout.
public object HttpProxy; // holds the setting for HttpProxy if one is set.
public object PacketsReceivedDiscarded; //hold all the packets received discarded on the server.
public double IPv6DisabledComponents; //value stored in the registry HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\DisabledComponents
public bool IPv6DisabledOnNICs; //value that determines if we have IPv6 disabled on some NICs or not.
public System.Array NetworkAdapters; //stores all the NICs on the servers.
public string PnPCapabilities; //Value from PnPCapabilities registry
public bool SleepyNicDisabled; //If the NIC can be in power saver mode by the OS.
}
public class PowerPlanInformation
{
public bool HighPerformanceSet; // If the power plan is High Performance
public string PowerPlanSetting; //value for the power plan that is set
public object PowerPlan; //object to store the power plan information
}
public class OSRegistryValues
{
public int CurrentVersionUbr; // stores SOFTWARE\Microsoft\Windows NT\CurrentVersion\UBR
public int LanManServerDisabledCompression; // stores SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters\DisabledCompression
}
public class LmCompatibilityLevelInformation
{
public int RegistryValue; //The LmCompatibilityLevel for the server (INT 1 - 5)
public string Description; //description of the LmCompat that the server is set to
}
public class TimeZoneInformation
{
public string CurrentTimeZone; //stores the value for the current time zone of the server.
public int DynamicDaylightTimeDisabled; // the registry value for DynamicDaylightTimeDisabled.
public string TimeZoneKeyName; // the registry value TimeZoneKeyName.
public string StandardStart; // the registry value for StandardStart.
public string DaylightStart; // the registry value for DaylightStart.
public bool DstIssueDetected; // Determines if there is a high chance of an issue.
public System.Array ActionsToTake; //array of verbage of the issues detected.
}
public class ServerRebootInformation
{
public bool PendingFileRenameOperations; //bool "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\" item PendingFileRenameOperations.
public object SccmReboot; // object to store CimMethod for class name CCM_ClientUtilities
public bool SccmRebootPending; // SccmReboot has either PendingReboot or IsHardRebootPending is set to true.
public bool ComponentBasedServicingPendingReboot; // bool HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending
public bool AutoUpdatePendingReboot; // bool HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired
public bool PendingReboot; // bool if reboot types are set to true
}
public class InstalledUpdatesInformation
{
public System.Array HotFixes; //array to keep all the hotfixes of the server
public System.Array HotFixInfo; //object to store hotfix information
public System.Array InstalledUpdates; //store the install updates
}
public class ServerBootUpInformation
{
public string Days;
public string Hours;
public string Minutes;
public string Seconds;
}
public class OSNetFrameworkInformation
{
public NetMajorVersion NetMajorVersion; //NetMajorVersion value
public string FriendlyName; //string of the friendly name
public int RegistryValue; //store the registry value
public Hashtable FileInformation; //stores Get-Item information for .NET Framework
}
//enum for the OSServerVersion that we are
public enum OSServerVersion
{
Unknown,
Windows2008,
Windows2008R2,
Windows2012,
Windows2012R2,
Windows2016,
Windows2019,
WindowsCore
}
//enum for the dword value of the .NET frame 4 that we are on
public enum NetMajorVersion
{
Unknown = 0,
Net4d5 = 378389,
Net4d5d1 = 378675,
Net4d5d2 = 379893,
Net4d5d2wFix = 380035,
Net4d6 = 393295,
Net4d6d1 = 394254,
Net4d6d1wFix = 394294,
Net4d6d2 = 394802,
Net4d7 = 460798,
Net4d7d1 = 461308,
Net4d7d2 = 461808,
Net4d8 = 528040
}
// End OperatingSystemInformation
// HardwareInformation
public class HardwareInformation
{
public string Manufacturer; //String to display the hardware information
public ServerType ServerType; //Enum to determine if the hardware is VMware, HyperV, Physical, or Unknown
public System.Array MemoryInformation; //Detailed information about the installed memory
public UInt64 TotalMemory; //Stores the total memory cooked value
public object System; //object to store the system information that we have collected
public ProcessorInformation Processor; //Detailed processor Information
public bool AutoPageFile; //True/False if we are using a page file that is being automatically set
public string Model; //string to display Model
}
//enum for the type of computer that we are
public enum ServerType
{
VMWare,
AmazonEC2,
HyperV,
Physical,
Unknown
}
public class ProcessorInformation
{
public string Name; //String of the processor name
public int NumberOfPhysicalCores; //Number of Physical cores that we have
public int NumberOfLogicalCores; //Number of Logical cores that we have presented to the os
public int NumberOfProcessors; //Total number of processors that we have in the system
public int MaxMegacyclesPerCore; //Max speed that we can get out of the cores
public int CurrentMegacyclesPerCore; //Current speed that we are using the cores at
public bool ProcessorIsThrottled; //True/False if we are throttling our processor
public bool DifferentProcessorsDetected; //true/false to detect if we have different processor types detected
public bool DifferentProcessorCoreCountDetected; //detect if there are a different number of core counts per Processor CPU socket
public int EnvironmentProcessorCount; //[system.environment]::processorcount
public object ProcessorClassObject; // object to store the processor information
}
//HTML & display classes
public class HtmlServerValues
{
public System.Array OverviewValues;
public System.Array ActionItems; //use HtmlServerActionItemRow
public System.Array ServerDetails; // use HtmlServerInformationRow
}
public class HtmlServerActionItemRow
{
public string Setting;
public string DetailValue;
public string RecommendedDetails;
public string MoreInformation;
public string Class;
}
public class HtmlServerInformationRow
{
public string Name;
public string DetailValue;
public string Class;
}
public class DisplayResultsLineInfo
{
public string DisplayValue;
public string Name;
public string TestingName; // Used for pestering testing
public int TabNumber;
public object TestingValue; //Used for pester testing down the road.
public object OutColumns; //used for colorized format table option.
public string WriteType;
public string Line
{
get
{
if (String.IsNullOrEmpty(this.Name))
{
return this.DisplayValue;
}
return String.Concat(this.Name, ": ", this.DisplayValue);
}
}
}
public class DisplayResultsGroupingKey
{
public string Name;
public int DefaultTabNumber;
public bool DisplayGroupName;
public int DisplayOrder;
}
public class AnalyzedInformation
{
public HealthCheckerExchangeServer HealthCheckerExchangeServer;
public Hashtable HtmlServerValues = new Hashtable();
public Hashtable DisplayResults = new Hashtable();
}
}
"@ -ErrorAction Stop
} catch {
Write-Warning "There was an error trying to add custom classes to the current PowerShell session. You need to close this session and open a new one to have the script properly work."
exit
}
Function Write-ResultsToScreen {
param(
[Hashtable]$ResultsToWrite
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$indexOrderGroupingToKey = @{}
foreach ($keyGrouping in $ResultsToWrite.Keys) {
$indexOrderGroupingToKey[$keyGrouping.DisplayOrder] = $keyGrouping
}
$sortedIndexOrderGroupingToKey = $indexOrderGroupingToKey.Keys | Sort-Object
foreach ($key in $sortedIndexOrderGroupingToKey) {
Write-Verbose "Working on Key: $key"
$keyGrouping = $indexOrderGroupingToKey[$key]
Write-Verbose "Working on Key Group: $($keyGrouping.Name)"
Write-Verbose "Total lines to write: $($ResultsToWrite[$keyGrouping].Count)"
if ($keyGrouping.DisplayGroupName) {
Write-Grey($keyGrouping.Name)
$dashes = [string]::empty
1..($keyGrouping.Name.Length) | ForEach-Object { $dashes = $dashes + "-" }
Write-Grey($dashes)
}
foreach ($line in $ResultsToWrite[$keyGrouping]) {
$tab = [string]::Empty
if ($line.TabNumber -ne 0) {
1..($line.TabNumber) | ForEach-Object { $tab = $tab + "`t" }
}
$writeValue = "{0}{1}" -f $tab, $line.Line
switch ($line.WriteType) {
"Grey" { Write-Grey($writeValue) }
"Yellow" { Write-Yellow($writeValue) }
"Green" { Write-Green($writeValue) }
"Red" { Write-Red($writeValue) }
"OutColumns" { Write-OutColumns($line.OutColumns) }
}
}
Write-Grey("")
}
}
Function Write-Verbose {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'In order to log Write-Verbose from Shared functions')]
[CmdletBinding()]
param(
[Parameter(Position = 1, ValueFromPipeline)]
[string]$Message
)
process {
#write to the debug log and call Write-Verbose normally
Write-DebugLog $Message
Microsoft.PowerShell.Utility\Write-Verbose $Message
}
}
<#
.SYNOPSIS
Outputs a table of objects with certain values colorized.
.EXAMPLE
PS C:\>
Explanation of what the example does
.INPUTS
Inputs (if any)
.OUTPUTS
Output (if any)
.NOTES
General notes
#>
function Out-Columns {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[object[]]
$InputObject,
[Parameter(Mandatory = $false, Position = 0)]
[string[]]
$Properties,
[Parameter(Mandatory = $false, Position = 1)]
[scriptblock[]]
$ColorizerFunctions = @(),
[Parameter(Mandatory = $false)]
[int]
$IndentSpaces = 0,
[Parameter(Mandatory = $false)]
[int]
$LinesBetweenObjects = 0,
[Parameter(Mandatory = $false)]
[ref]
$StringOutput
)
begin {
function WrapLine {
param([string]$line, [int]$width)
if ($line.Length -le $width -and $line.IndexOf("`n") -lt 0) {
return $line
}
$lines = New-Object System.Collections.ArrayList
$noLF = $line.Replace("`r", "")
$lineSplit = $noLF.Split("`n")
foreach ($l in $lineSplit) {
if ($l.Length -le $width) {
[void]$lines.Add($l)
} else {
$split = $l.Split(" ")
$sb = New-Object System.Text.StringBuilder
for ($i = 0; $i -lt $split.Length; $i++) {
if ($sb.Length -eq 0 -and $sb.Length + $split[$i].Length -lt $width) {
[void]$sb.Append($split[$i])
} elseif ($sb.Length -gt 0 -and $sb.Length + $split[$i].Length + 1 -lt $width) {
[void]$sb.Append(" " + $split[$i])
} elseif ($sb.Length -gt 0) {
[void]$lines.Add($sb.ToString())
[void]$sb.Clear()
$i--
} else {
if ($split[$i].Length -lt $width) {
[void]$lines.Add($split[$i])
} else {
[void]$lines.Add($split[$i].Substring(0, $width))
$split[$i] = $split[$i].Substring($width + 1)
$i--
}
}
}
if ($sb.Length -gt 0) {
[void]$lines.Add($sb.ToString())
}
}
}
return $lines
}
function GetLineObjects {
param($obj, $props, $colWidths)
$linesNeededForThisObject = 1
$multiLineProps = @{}
for ($i = 0; $i -lt $props.Length; $i++) {
$p = $props[$i]
$val = $obj."$p"
if ($val -isnot [array]) {
$val = WrapLine -line $val -width $colWidths[$i]
} elseif ($val -is [array]) {
$val = $val | Where-Object { $null -ne $_ }
$val = $val | ForEach-Object { WrapLine -line $_ -width $colWidths[$i] }
}
if ($val -is [array]) {
$multiLineProps[$p] = $val
if ($val.Length -gt $linesNeededForThisObject) {
$linesNeededForThisObject = $val.Length
}
}
}
if ($linesNeededForThisObject -eq 1) {
$obj
} else {
for ($i = 0; $i -lt $linesNeededForThisObject; $i++) {
$lineProps = @{}
foreach ($p in $props) {
if ($null -ne $multiLineProps[$p] -and $multiLineProps[$p].Length -gt $i) {
$lineProps[$p] = $multiLineProps[$p][$i]
} elseif ($i -eq 0) {
$lineProps[$p] = $o."$p"
} else {
$lineProps[$p] = $null
}
}
[PSCustomObject]$lineProps
}
}
}
function GetColumnColors {
param($obj, $props, $funcs)
$consoleHost = (Get-Host).Name -eq "ConsoleHost"
$colColors = New-Object string[] $props.Count
for ($i = 0; $i -lt $props.Count; $i++) {
if ($consoleHost) {
$fgColor = (Get-Host).ui.rawui.ForegroundColor
} else {
$fgColor = "White"
}
foreach ($func in $funcs) {
$result = $func.Invoke($o, $props[$i])
if (-not [string]::IsNullOrEmpty($result)) {
$fgColor = $result
break # The first colorizer that takes action wins
}
}
$colColors[$i] = $fgColor
}
$colColors
}
function GetColumnWidths {
param($objects, $props)
$colWidths = New-Object int[] $props.Count
# Start with the widths of the property names
for ($i = 0; $i -lt $props.Count; $i++) {
$colWidths[$i] = $props[$i].Length
}
# Now check the widths of the widest values
foreach ($thing in $objects) {
for ($i = 0; $i -lt $props.Count; $i++) {
$val = $thing."$($props[$i])"
if ($null -ne $val) {
$width = 0
if ($val -isnot [array]) {
$val = $val.ToString().Split("`n")
}
$width = ($val | ForEach-Object {
if ($null -ne $_) { $_.ToString() } else { "" }
} | Sort-Object Length -Descending | Select-Object -First 1).Length
if ($width -gt $colWidths[$i]) {
$colWidths[$i] = $width
}
}
}
}
# If we're within the window width, we're done
$totalColumnWidth = $colWidths.Length * $padding + ($colWidths | Measure-Object -Sum).Sum + $IndentSpaces
$windowWidth = (Get-Host).UI.RawUI.WindowSize.Width
if ($windowWidth -lt 1 -or $totalColumnWidth -lt $windowWidth) {
return $colWidths
}
# Take size away from one or more columns to make them fit
while ($totalColumnWidth -ge $windowWidth) {
$startingTotalWidth = $totalColumnWidth
$widest = $colWidths | Sort-Object -Descending | Select-Object -First 1
$newWidest = [Math]::Floor($widest * 0.95)
for ($i = 0; $i -lt $colWidths.Length; $i++) {
if ($colWidths[$i] -eq $widest) {
$colWidths[$i] = $newWidest
break
}
}
$totalColumnWidth = $colWidths.Length * $padding + ($colWidths | Measure-Object -Sum).Sum + $IndentSpaces
if ($totalColumnWidth -ge $startingTotalWidth) {
# Somehow we didn't reduce the size at all, so give up
break
}
}
return $colWidths
}
$objects = New-Object System.Collections.ArrayList
$padding = 2
$stb = New-Object System.Text.StringBuilder
}
process {
foreach ($thing in $InputObject) {
[void]$objects.Add($thing)
}
}
end {
if ($objects.Count -gt 0) {
$props = $null
if ($null -ne $Properties) {
$props = $Properties
} else {
$props = $objects[0].PSObject.Properties.Name
}
$colWidths = GetColumnWidths $objects $props
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
Write-Host (" " * $IndentSpaces) -NoNewline
[void]$stb.Append(" " * $IndentSpaces)
for ($i = 0; $i -lt $props.Count; $i++) {
Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f $props[$i]) -NoNewline
[void]$stb.Append("{0,$(-1 * ($colWidths[$i] + $padding))}" -f $props[$i])
}
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
Write-Host (" " * $IndentSpaces) -NoNewline
[void]$stb.Append(" " * $IndentSpaces)
for ($i = 0; $i -lt $props.Count; $i++) {
Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f ("-" * $props[$i].Length)) -NoNewline
[void]$stb.Append("{0,$(-1 * ($colWidths[$i] + $padding))}" -f ("-" * $props[$i].Length))
}
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
foreach ($o in $objects) {
$colColors = GetColumnColors -obj $o -props $props -funcs $ColorizerFunctions
$lineObjects = @(GetLineObjects -obj $o -props $props -colWidths $colWidths)
foreach ($lineObj in $lineObjects) {
Write-Host (" " * $IndentSpaces) -NoNewline
[void]$stb.Append(" " * $IndentSpaces)
for ($i = 0; $i -lt $props.Count; $i++) {
$val = $o."$($props[$i])"
Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f $lineObj."$($props[$i])") -NoNewline -ForegroundColor $colColors[$i]
[void]$stb.Append("{0,$(-1 * ($colWidths[$i] + $padding))}" -f $lineObj."$($props[$i])")
}
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
}
for ($i = 0; $i -lt $LinesBetweenObjects; $i++) {
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
}
}
Write-Host
[void]$stb.Append([System.Environment]::NewLine)
if ($null -ne $StringOutput) {
$StringOutput.Value = $stb.ToString()
}
}
}
}
function Write-Red($message) {
Write-DebugLog $message
Write-Host $message -ForegroundColor Red
$message | Out-File ($OutputFullPath) -Append
}
function Write-Yellow($message) {
Write-DebugLog $message
Write-Host $message -ForegroundColor Yellow
$message | Out-File ($OutputFullPath) -Append
}
function Write-Green($message) {
Write-DebugLog $message
Write-Host $message -ForegroundColor Green
$message | Out-File ($OutputFullPath) -Append
}
function Write-Grey($message) {
Write-DebugLog $message
Write-Host $message
$message | Out-File ($OutputFullPath) -Append
}
function Write-DebugLog($message) {
if (![string]::IsNullOrEmpty($message)) {
$Script:Logger = $Script:Logger | Write-LoggerInstance $message
}
}
function Write-OutColumns($OutColumns) {
if ($null -ne $OutColumns) {
$stringOutput = $null
$OutColumns.DisplayObject |
Out-Columns -Properties $OutColumns.SelectProperties `
-ColorizerFunctions $OutColumns.ColorizerFunctions `
-IndentSpaces $OutColumns.IndentSpaces `
-StringOutput ([ref]$stringOutput)
$stringOutput | Out-File ($OutputFullPath) -Append
Write-DebugLog $stringOutput
}
}
Function Write-Break {
Write-Host ""
}
Function Get-HtmlServerReport {
param(
[Parameter(Mandatory = $true)][array]$AnalyzedHtmlServerValues
)
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$htmlHeader = "
Exchange Health Checker v$($BuildVersion)
Servers Overview
"
[array]$htmlOverviewTable += "
"
foreach ($tableHeaderName in $AnalyzedHtmlServerValues[0]["OverviewValues"].Name) {
$htmlOverviewTable += "{0} | " -f $tableHeaderName
}
$htmlOverviewTable += "
"
foreach ($serverHtmlServerValues in $AnalyzedHtmlServerValues) {
$htmlTableRow = @()
[array]$htmlTableRow += ""
foreach ($htmlTableDataRow in $serverHtmlServerValues["OverviewValues"]) {
$htmlTableRow += "{1} | " -f $htmlTableDataRow.Class, `
$htmlTableDataRow.DetailValue
}
$htmlTableRow += "
"
$htmlOverviewTable += $htmlTableRow
}
$htmlOverviewTable += "
"
[array]$htmlServerDetails += "Server Details
"
foreach ($serverHtmlServerValues in $AnalyzedHtmlServerValues) {
foreach ($htmlTableDataRow in $serverHtmlServerValues["ServerDetails"]) {
if ($htmlTableDataRow.Name -eq "Server Name") {
$htmlServerDetails += "{0} | {1} |
---|
" -f $htmlTableDataRow.Name, `
$htmlTableDataRow.DetailValue
} else {
$htmlServerDetails += "
{1} | {2} |
" -f $htmlTableDataRow.Class, `
$htmlTableDataRow.Name, `
$htmlTableDataRow.DetailValue
}
}
}
$htmlServerDetails += "
"
$htmlReport = $htmlHeader + $htmlOverviewTable + $htmlServerDetails + ""
$htmlReport | Out-File $HtmlReportFile -Encoding UTF8
}
Function Get-CASLoadBalancingReport {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
#Connection and requests per server and client type values
$CASConnectionStats = @{}
$TotalCASConnectionCount = 0
$AutoDStats = @{}
$TotalAutoDRequests = 0
$EWSStats = @{}
$TotalEWSRequests = 0
$MapiHttpStats = @{}
$TotalMapiHttpRequests = 0
$EASStats = @{}
$TotalEASRequests = 0
$OWAStats = @{}
$TotalOWARequests = 0
$RpcHttpStats = @{}
$TotalRpcHttpRequests = 0
$CASServers = @()
if ($null -ne $CasServerList) {
Write-Grey("Custom CAS server list is being used. Only servers specified after the -CasServerList parameter will be used in the report.")
foreach ($cas in $CasServerList) {
$CASServers += (Get-ExchangeServer $cas)
}
} elseif ($SiteName -ne [string]::Empty) {
Write-Grey("Site filtering ON. Only Exchange 2013/2016 CAS servers in {0} will be used in the report." -f $SiteName)
$CASServers = Get-ExchangeServer | Where-Object { `
($_.IsClientAccessServer -eq $true) -and `
($_.AdminDisplayVersion -Match "^Version 15") -and `
([System.Convert]::ToString($_.Site).Split("/")[-1] -eq $SiteName) }
} else {
Write-Grey("Site filtering OFF. All Exchange 2013/2016 CAS servers will be used in the report.")
$CASServers = Get-ExchangeServer | Where-Object { ($_.IsClientAccessServer -eq $true) -and ($_.AdminDisplayVersion -Match "^Version 15") }
}
if ($CASServers.Count -eq 0) {
Write-Red("Error: No CAS servers found using the specified search criteria.")
Exit
}
#Request stats from perfmon for all CAS
$PerformanceCounters = @()
$PerformanceCounters += "\Web Service(Default Web Site)\Current Connections"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_Autodiscover)\Requests Executing"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_EWS)\Requests Executing"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_mapi)\Requests Executing"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_Microsoft-Server-ActiveSync)\Requests Executing"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_owa)\Requests Executing"
$PerformanceCounters += "\ASP.NET Apps v4.0.30319(_LM_W3SVC_1_ROOT_Rpc)\Requests Executing"
$currentErrors = $Error.Count
$AllCounterResults = Get-Counter -ComputerName $CASServers -Counter $PerformanceCounters -ErrorAction SilentlyContinue
if ($currentErrors -ne $Error.Count) {
$i = 0
while ($i -lt ($Error.Count - $currentErrors)) {
Invoke-CatchActions -CopyThisError $Error[$i]
$i++
}
Write-Verbose("Failed to get some counters")
}
foreach ($Result in $AllCounterResults.CounterSamples) {
$CasName = ($Result.Path).Split("\\", [System.StringSplitOptions]::RemoveEmptyEntries)[0]
$ResultCookedValue = $Result.CookedValue
if ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[0]) {
#Total connections
$CASConnectionStats.Add($CasName, $ResultCookedValue)
$TotalCASConnectionCount += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[1]) {
#AutoD requests
$AutoDStats.Add($CasName, $ResultCookedValue)
$TotalAutoDRequests += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[2]) {
#EWS requests
$EWSStats.Add($CasName, $ResultCookedValue)
$TotalEWSRequests += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[3]) {
#MapiHttp requests
$MapiHttpStats.Add($CasName, $ResultCookedValue)
$TotalMapiHttpRequests += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[4]) {
#EAS requests
$EASStats.Add($CasName, $ResultCookedValue)
$TotalEASRequests += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[5]) {
#OWA requests
$OWAStats.Add($CasName, $ResultCookedValue)
$TotalOWARequests += $ResultCookedValue
} elseif ($Result.Path -like "*{0}*{1}" -f $CasName, $PerformanceCounters[6]) {
#RPCHTTP requests
$RpcHttpStats.Add($CasName, $ResultCookedValue)
$TotalRpcHttpRequests += $ResultCookedValue
}
}
#Report the results for connection count
Write-Grey("")
Write-Grey("Connection Load Distribution Per Server")
Write-Grey("Total Connections: {0}" -f $TotalCASConnectionCount)
#Calculate percentage of connection load
$CASConnectionStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Connections = " + [math]::Round((([int]$_.Value / $TotalCASConnectionCount) * 100)) + "% Distribution")
}
#Same for each client type. These are request numbers not connection numbers.
#AutoD
if ($TotalAutoDRequests -gt 0) {
Write-Grey("")
Write-Grey("Current AutoDiscover Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalAutoDRequests)
$AutoDStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalAutoDRequests) * 100)) + "% Distribution")
}
}
#EWS
if ($TotalEWSRequests -gt 0) {
Write-Grey("")
Write-Grey("Current EWS Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalEWSRequests)
$EWSStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalEWSRequests) * 100)) + "% Distribution")
}
}
#MapiHttp
if ($TotalMapiHttpRequests -gt 0) {
Write-Grey("")
Write-Grey("Current MapiHttp Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalMapiHttpRequests)
$MapiHttpStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalMapiHttpRequests) * 100)) + "% Distribution")
}
}
#EAS
if ($TotalEASRequests -gt 0) {
Write-Grey("")
Write-Grey("Current EAS Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalEASRequests)
$EASStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalEASRequests) * 100)) + "% Distribution")
}
}
#OWA
if ($TotalOWARequests -gt 0) {
Write-Grey("")
Write-Grey("Current OWA Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalOWARequests)
$OWAStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalOWARequests) * 100)) + "% Distribution")
}
}
#RpcHttp
if ($TotalRpcHttpRequests -gt 0) {
Write-Grey("")
Write-Grey("Current RpcHttp Requests Per Server")
Write-Grey("Total Requests: {0}" -f $TotalRpcHttpRequests)
$RpcHttpStats.GetEnumerator() | Sort-Object -Descending | ForEach-Object {
Write-Grey($_.Key + ": " + $_.Value + " Requests = " + [math]::Round((([int]$_.Value / $TotalRpcHttpRequests) * 100)) + "% Distribution")
}
}
Write-Grey("")
}
Function Get-ComputerCoresObject {
param(
[Parameter(Mandatory = $true)][string]$Machine_Name
)
Write-Verbose "Calling: $($MyInvocation.MyCommand) Passed: $Machine_Name"
$returnObj = New-Object PSCustomObject
$returnObj | Add-Member -MemberType NoteProperty -Name Error -Value $false
$returnObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $Machine_Name
$returnObj | Add-Member -MemberType NoteProperty -Name NumberOfCores -Value ([int]::empty)
$returnObj | Add-Member -MemberType NoteProperty -Name Exception -Value ([string]::empty)
$returnObj | Add-Member -MemberType NoteProperty -Name ExceptionType -Value ([string]::empty)
try {
$wmi_obj_processor = Get-WmiObjectHandler -ComputerName $Machine_Name -Class "Win32_Processor" -CatchActionFunction ${Function:Invoke-CatchActions}
foreach ($processor in $wmi_obj_processor) {
$returnObj.NumberOfCores += $processor.NumberOfCores
}
Write-Grey("Server {0} Cores: {1}" -f $Machine_Name, $returnObj.NumberOfCores)
} catch {
Invoke-CatchActions
$thisError = $Error[0]
if ($thisError.Exception.Gettype().FullName -eq "System.UnauthorizedAccessException") {
Write-Yellow("Unable to get processor information from server {0}. You do not have the correct permissions to get this data from that server. Exception: {1}" -f $Machine_Name, $thisError.ToString())
} else {
Write-Yellow("Unable to get processor information from server {0}. Reason: {1}" -f $Machine_Name, $thisError.ToString())
}
$returnObj.Exception = $thisError.ToString()
$returnObj.ExceptionType = $thisError.Exception.Gettype().FullName
$returnObj.Error = $true
}
return $returnObj
}
Function Get-ExchangeDCCoreRatio {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-ExchangeDCCoreRatio"
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
Write-Grey("Exchange Server Health Checker Report - AD GC Core to Exchange Server Core Ratio - v{0}" -f $BuildVersion)
$coreRatioObj = New-Object PSCustomObject
try {
Write-Verbose "Attempting to load Active Directory Module"
Import-Module ActiveDirectory
Write-Verbose "Successfully loaded"
} catch {
Write-Red("Failed to load Active Directory Module. Stopping the script")
exit
}
$ADSite = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite().Name
[array]$DomainControllers = (Get-ADForest).Domains |
ForEach-Object { Get-ADDomainController -Server $_ } |
Where-Object { $_.IsGlobalCatalog -eq $true -and $_.Site -eq $ADSite }
[System.Collections.Generic.List[System.Object]]$DCList = New-Object System.Collections.Generic.List[System.Object]
$DCCoresTotal = 0
Write-Break
Write-Grey("Collecting data for the Active Directory Environment in Site: {0}" -f $ADSite)
$iFailedDCs = 0
foreach ($DC in $DomainControllers) {
$DCCoreObj = Get-ComputerCoresObject -Machine_Name $DC.Name
$DCList.Add($DCCoreObj)
if (-not ($DCCoreObj.Error)) {
$DCCoresTotal += $DCCoreObj.NumberOfCores
} else {
$iFailedDCs++
}
}
$coreRatioObj | Add-Member -MemberType NoteProperty -Name DCList -Value $DCList
if ($iFailedDCs -eq $DomainControllers.count) {
#Core count is going to be 0, no point to continue the script
Write-Red("Failed to collect data from your DC servers in site {0}." -f $ADSite)
Write-Yellow("Because we can't determine the ratio, we are going to stop the script. Verify with the above errors as to why we failed to collect the data and address the issue, then run the script again.")
exit
}
[array]$ExchangeServers = Get-ExchangeServer | Where-Object { $_.Site -match $ADSite }
$EXCoresTotal = 0
[System.Collections.Generic.List[System.Object]]$EXList = New-Object System.Collections.Generic.List[System.Object]
Write-Break
Write-Grey("Collecting data for the Exchange Environment in Site: {0}" -f $ADSite)
foreach ($svr in $ExchangeServers) {
$EXCoreObj = Get-ComputerCoresObject -Machine_Name $svr.Name
$EXList.Add($EXCoreObj)
if (-not ($EXCoreObj.Error)) {
$EXCoresTotal += $EXCoreObj.NumberOfCores
}
}
$coreRatioObj | Add-Member -MemberType NoteProperty -Name ExList -Value $EXList
Write-Break
$CoreRatio = $EXCoresTotal / $DCCoresTotal
Write-Grey("Total DC/GC Cores: {0}" -f $DCCoresTotal)
Write-Grey("Total Exchange Cores: {0}" -f $EXCoresTotal)
Write-Grey("You have {0} Exchange Cores for every Domain Controller Global Catalog Server Core" -f $CoreRatio)
if ($CoreRatio -gt 8) {
Write-Break
Write-Red("Your Exchange to Active Directory Global Catalog server's core ratio does not meet the recommended guidelines of 8:1")
Write-Red("Recommended guidelines for Exchange 2013/2016 for every 8 Exchange cores you want at least 1 Active Directory Global Catalog Core.")
Write-Yellow("Documentation:")
Write-Yellow("`thttps://aka.ms/HC-PerfSize")
Write-Yellow("`thttps://aka.ms/HC-ADCoreCount")
} else {
Write-Break
Write-Green("Your Exchange Environment meets the recommended core ratio of 8:1 guidelines.")
}
$XMLDirectoryPath = $OutputFullPath.Replace(".txt", ".xml")
$coreRatioObj | Export-Clixml $XMLDirectoryPath
Write-Grey("Output file written to {0}" -f $OutputFullPath)
Write-Grey("Output XML Object file written to {0}" -f $XMLDirectoryPath)
}
Function Get-MailboxDatabaseAndMailboxStatistics {
Write-Verbose "Calling: $($MyInvocation.MyCommand)"
$AllDBs = Get-MailboxDatabaseCopyStatus -server $Script:Server -ErrorAction SilentlyContinue
$MountedDBs = $AllDBs | Where-Object { $_.ActiveCopy -eq $true }
if ($MountedDBs.Count -gt 0) {
Write-Grey("`tActive Database:")
foreach ($db in $MountedDBs) {
Write-Grey("`t`t" + $db.Name)
}
$MountedDBs.DatabaseName | ForEach-Object { Write-Verbose "Calculating User Mailbox Total for Active Database: $_"; $TotalActiveUserMailboxCount += (Get-Mailbox -Database $_ -ResultSize Unlimited).Count }
Write-Grey("`tTotal Active User Mailboxes on server: " + $TotalActiveUserMailboxCount)
$MountedDBs.DatabaseName | ForEach-Object { Write-Verbose "Calculating Public Mailbox Total for Active Database: $_"; $TotalActivePublicFolderMailboxCount += (Get-Mailbox -Database $_ -ResultSize Unlimited -PublicFolder).Count }
Write-Grey("`tTotal Active Public Folder Mailboxes on server: " + $TotalActivePublicFolderMailboxCount)
Write-Grey("`tTotal Active Mailboxes on server " + $Script:Server + ": " + ($TotalActiveUserMailboxCount + $TotalActivePublicFolderMailboxCount).ToString())
} else {
Write-Grey("`tNo Active Mailbox Databases found on server " + $Script:Server + ".")
}
$HealthyDbs = $AllDBs | Where-Object { $_.Status -match 'Healthy' }
if ($HealthyDbs.count -gt 0) {
Write-Grey("`r`n`tPassive Databases:")
foreach ($db in $HealthyDbs) {
Write-Grey("`t`t" + $db.Name)
}
$HealthyDbs.DatabaseName | ForEach-Object { Write-Verbose "`tCalculating User Mailbox Total for Passive Healthy Databases: $_"; $TotalPassiveUserMailboxCount += (Get-Mailbox -Database $_ -ResultSize Unlimited).Count }
Write-Grey("`tTotal Passive user Mailboxes on Server: " + $TotalPassiveUserMailboxCount)
$HealthyDbs.DatabaseName | ForEach-Object { Write-Verbose "`tCalculating Passive Mailbox Total for Passive Healthy Databases: $_"; $TotalPassivePublicFolderMailboxCount += (Get-Mailbox -Database $_ -ResultSize Unlimited -PublicFolder).Count }
Write-Grey("`tTotal Passive Public Mailboxes on server: " + $TotalPassivePublicFolderMailboxCount)
Write-Grey("`tTotal Passive Mailboxes on server: " + ($TotalPassiveUserMailboxCount + $TotalPassivePublicFolderMailboxCount).ToString())
} else {
Write-Grey("`tNo Passive Mailboxes found on server " + $Script:Server + ".")
}
}
#https://github.com/dpaulson45/PublicPowerShellFunctions/blob/master/src/Common/Confirm-Administrator/Confirm-Administrator.ps1
#v21.01.22.2212
Function Confirm-Administrator {
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() )
if ($currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator )) {
return $true
} else {
return $false
}
}
Function Get-NewLoggerInstance {
[CmdletBinding()]
param(
[ValidateScript( { Test-Path $_ })]
[string]$LogDirectory = (Get-Location).Path,
[ValidateNotNullOrEmpty()]
[string]$LogName = "Script_Logging",
[bool]$AppendDateTime = $true,
[bool]$AppendDateTimeToFileName = $true,
[int]$MaxFileSizeMB = 10,
[int]$CheckSizeIntervalMinutes = 10,
[int]$NumberOfLogsToKeep = 10
)
$fileName = if ($AppendDateTimeToFileName) { "{0}_{1}.txt" -f $LogName, ((Get-Date).ToString('yyyyMMddHHmmss')) } else { "$LogName.txt" }
$fullFilePath = [System.IO.Path]::Combine($LogDirectory, $fileName)
return [PSCustomObject]@{
FullPath = $fullFilePath
AppendDateTime = $AppendDateTime
MaxFileSizeMB = $MaxFileSizeMB
CheckSizeIntervalMinutes = $CheckSizeIntervalMinutes
NumberOfLogsToKeep = $NumberOfLogsToKeep
BaseInstanceFileName = $fileName.Replace(".txt", "")
Instance = 1
NextFileCheckTime = ((Get-Date).AddMinutes($CheckSizeIntervalMinutes))
PreventLogCleanup = $false
LoggerDisabled = $false
} | Write-LoggerInstance -Object "Starting Logger Instance $(Get-Date)"
}
Function Write-LoggerInstance {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]$LoggerInstance,
[Parameter(Mandatory = $true, Position = 1)]
[object]$Object
)
process {
if ($LoggerInstance.LoggerDisabled) { return }
if ($LoggerInstance.AppendDateTime -and
$Object.GetType().Name -eq "string") {
$Object = "[$([System.DateTime]::Now)] : $Object"
}
$Object | Out-File $LoggerInstance.FullPath -Append
#Upkeep of the logger information
if ($LoggerInstance.NextFileCheckTime -gt [System.DateTime]::Now) {
return
}
#Set next update time to avoid issues so we can log things
$LoggerInstance.NextFileCheckTime = ([System.DateTime]::Now).AddMinutes($LoggerInstance.CheckSizeIntervalMinutes)
$item = Get-ChildItem $LoggerInstance.FullPath
if (($item.Length / 1MB) -gt $LoggerInstance.MaxFileSizeMB) {
$LoggerInstance | Write-LoggerInstance -Object "Max file size reached rolling over" | Out-Null
$directory = [System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath)
$fileName = "$($LoggerInstance.BaseInstanceFileName)-$($LoggerInstance.Instance).txt"
$LoggerInstance.Instance++
$LoggerInstance.FullPath = [System.IO.Path]::Combine($directory, $fileName)
$items = Get-ChildItem -Path ([System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath)) -Filter "*$($LoggerInstance.BaseInstanceFileName)*"
if ($items.Count -gt $LoggerInstance.NumberOfLogsToKeep) {
$item = $items | Sort-Object LastWriteTime | Select-Object -First 1
$LoggerInstance | Write-LoggerInstance "Removing Log File $($item.FullName)" | Out-Null
$item | Remove-Item -Force
}
}
}
end {
return $LoggerInstance
}
}
Function Invoke-LoggerInstanceCleanup {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]$LoggerInstance
)
process {
if ($LoggerInstance.LoggerDisabled -or
$LoggerInstance.PreventLogCleanup) {
return
}
Get-ChildItem -Path ([System.IO.Path]::GetDirectoryName($LoggerInstance.FullPath)) -Filter "*$($LoggerInstance.BaseInstanceFileName)*" |
Remove-Item -Force
}
}
<#
Determines if the script has an update available. Use the optional
-AutoUpdate switch to make it update itself. Returns $true if an
update was downloaded, $false otherwise. The result will always
be $false if the -AutoUpdate switch is not used.
#>
function Test-ScriptVersion {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter()]
[switch]
$AutoUpdate
)
function Confirm-ProxyServer {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]
$TargetUri
)
try {
$proxyObject = ([System.Net.WebRequest]::GetSystemWebproxy()).GetProxy($TargetUri)
if ($TargetUri -ne $proxyObject.OriginalString) {
return $true
} else {
return $false
}
} catch {
return $false
}
}
function Confirm-Signature {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]
$File
)
$IsValid = $false
$MicrosoftSigningRoot2010 = 'CN=Microsoft Root Certificate Authority 2010, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
$MicrosoftSigningRoot2011 = 'CN=Microsoft Root Certificate Authority 2011, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
try {
$sig = Get-AuthenticodeSignature -FilePath $File
if ($sig.Status -ne 'Valid') {
Write-Warning "Signature is not trusted by machine as Valid, status: $($sig.Status)."
throw
}
$chain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain
$chain.ChainPolicy.VerificationFlags = "IgnoreNotTimeValid"
if (-not $chain.Build($sig.SignerCertificate)) {
Write-Warning "Signer certificate doesn't chain correctly."
throw
}
if ($chain.ChainElements.Count -le 1) {
Write-Warning "Certificate Chain shorter than expected."
throw
}
$rootCert = $chain.ChainElements[$chain.ChainElements.Count - 1]
if ($rootCert.Certificate.Subject -ne $rootCert.Certificate.Issuer) {
Write-Warning "Top-level certifcate in chain is not a root certificate."
throw
}
if ($rootCert.Certificate.Subject -ne $MicrosoftSigningRoot2010 -and $rootCert.Certificate.Subject -ne $MicrosoftSigningRoot2011) {
Write-Warning "Unexpected root cert. Expected $MicrosoftSigningRoot2010 or $MicrosoftSigningRoot2011, but found $($rootCert.Certificate.Subject)."
throw
}
Write-Host "File signed by $($sig.SignerCertificate.Subject)"
$IsValid = $true
} catch {
$IsValid = $false
}
$IsValid
}
$scriptName = $script:MyInvocation.MyCommand.Name
$scriptPath = [IO.Path]::GetDirectoryName($script:MyInvocation.MyCommand.Path)
$scriptFullName = (Join-Path $scriptPath $scriptName)
if ((Get-AuthenticodeSignature -FilePath $scriptFullName).Status -eq "NotSigned") {
Write-Warning "This script appears to be an unsigned test build. Skipping version check."
return $false
}
$oldName = [IO.Path]::GetFileNameWithoutExtension($scriptName) + ".old"
$oldFullName = (Join-Path $scriptPath $oldName)
$tempFullName = (Join-Path $env:TEMP $scriptName)
$BuildVersion = "21.11.09.1825"
try {
$versionsUrl = "https://github.com/microsoft/CSS-Exchange/releases/latest/download/ScriptVersions.csv"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if (Confirm-ProxyServer -TargetUri "https://github.com") {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("User-Agent", "PowerShell")
$webClient.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
}
$versionData = [Text.Encoding]::UTF8.GetString((Invoke-WebRequest $versionsUrl -UseBasicParsing).Content) | ConvertFrom-Csv
$latestVersion = ($versionData | Where-Object { $_.File -eq $scriptName }).Version
if ($null -ne $latestVersion -and $latestVersion -ne $BuildVersion) {
if ($AutoUpdate) {
if (Test-Path $tempFullName) {
Remove-Item $tempFullName -Force -Confirm:$false -ErrorAction Stop
}
Write-Host "AutoUpdate: Downloading update."
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest "https://github.com/microsoft/CSS-Exchange/releases/latest/download/$scriptName" -OutFile $tempFullName -UseBasicParsing
if (Confirm-Signature -File $tempFullName) {
Write-Host "AutoUpdate: Signature validated."
if (Test-Path $oldFullName) {
Remove-Item $oldFullName -Force -Confirm:$false -ErrorAction Stop
}
Move-Item $scriptFullName $oldFullName
Move-Item $tempFullName $scriptFullName
Write-Host "AutoUpdate: Succeeded."
return $true
} else {
Write-Warning "AutoUpdate: Signature could not be verified: $tempFullName."
Write-Warning "AutoUpdate: Update was not applied."
}
} else {
Write-Warning "$scriptName $BuildVersion is outdated. Please download the latest, version $latestVersion."
}
}
} catch {
# Work around empty catch block rule. The failure is intentionally silent.
# For example, the script might be running on a computer with no internet access.
"Version check failed" | Out-Null
}
return $false
}
Function Write-Host {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidOverwritingBuiltInCmdlets', '', Justification = 'Proper handling of write host with colors')]
[CmdletBinding()]
param(
[Parameter(Position = 1)]
[object]$Object,
[switch]$NoNewLine,
[string]$ForegroundColor
)
begin {
$consoleHost = $host.Name -eq "ConsoleHost"
$params = @{
Object = $Object
NoNewLine = $NoNewLine
}
}
process {
if ([string]::IsNullOrEmpty($ForegroundColor)) {
if ($null -ne $host.UI.RawUI.ForegroundColor -and
$consoleHost) {
$params.Add("ForegroundColor", $host.UI.RawUI.ForegroundColor)
}
} elseif ($ForegroundColor -eq "Yellow" -and
$consoleHost -and
$null -ne $host.PrivateData.WarningForegroundColor) {
$params.Add("ForegroundColor", $host.PrivateData.WarningForegroundColor)
} elseif ($ForegroundColor -eq "Red" -and
$consoleHost -and
$null -ne $host.PrivateData.ErrorForegroundColor) {
$params.Add("ForegroundColor", $host.PrivateData.ErrorForegroundColor)
} else {
$params.Add("ForegroundColor", $ForegroundColor)
}
Microsoft.PowerShell.Utility\Write-Host @params
}
}
Function SetProperForegroundColor {
$Script:OriginalConsoleForegroundColor = $host.UI.RawUI.ForegroundColor
if ($Host.UI.RawUI.ForegroundColor -eq $Host.PrivateData.WarningForegroundColor) {
Write-Verbose "Foreground Color matches warning's color"
if ($Host.UI.RawUI.ForegroundColor -ne "Gray") {
$Host.UI.RawUI.ForegroundColor = "Gray"
}
}
if ($Host.UI.RawUI.ForegroundColor -eq $Host.PrivateData.ErrorForegroundColor) {
Write-Verbose "Foreground Color matches error's color"
if ($Host.UI.RawUI.ForegroundColor -ne "Gray") {
$Host.UI.RawUI.ForegroundColor = "Gray"
}
}
}
Function RevertProperForegroundColor {
$Host.UI.RawUI.ForegroundColor = $Script:OriginalConsoleForegroundColor
}
Function Main {
if (-not (Confirm-Administrator) -and
(-not $AnalyzeDataOnly -and
-not $BuildHtmlServersReport -and
-not $ScriptUpdateOnly)) {
Write-Warning "The script needs to be executed in elevated mode. Start the Exchange Management Shell as an Administrator."
$Error.Clear()
Start-Sleep -Seconds 2;
exit
}
$Error.Clear() #Always clear out the errors
$Script:ErrorsExcludedCount = 0 #this is a way to determine if the only errors occurred were in try catch blocks. If there is a combination of errors in and out, then i will just dump it all out to avoid complex issues.
$Script:ErrorsExcluded = @()
$Script:date = (Get-Date)
$Script:dateTimeStringFormat = $date.ToString("yyyyMMddHHmmss")
if ($BuildHtmlServersReport) {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-HTMLServerReport"
$files = Get-HealthCheckFilesItemsFromLocation
$fullPaths = Get-OnlyRecentUniqueServersXMLs $files
$importData = Import-MyData -FilePaths $fullPaths
Get-HtmlServerReport -AnalyzedHtmlServerValues $importData.HtmlServerValues
Start-Sleep 2;
return
}
if ((Test-Path $OutputFilePath) -eq $false) {
Write-Host "Invalid value specified for -OutputFilePath." -ForegroundColor Red
return
}
if ($LoadBalancingReport) {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-LoadBalancingReport"
Write-Green("Client Access Load Balancing Report on " + $date)
Get-CASLoadBalancingReport
Write-Grey("Output file written to " + $OutputFullPath)
Write-Break
Write-Break
return
}
if ($DCCoreRatio) {
$oldErrorAction = $ErrorActionPreference
$ErrorActionPreference = "Stop"
try {
Get-ExchangeDCCoreRatio
return
} finally {
$ErrorActionPreference = $oldErrorAction
}
}
if ($MailboxReport) {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-MailboxReport" -IncludeServerName $true
Get-MailboxDatabaseAndMailboxStatistics
Write-Grey("Output file written to {0}" -f $Script:OutputFullPath)
return
}
if ($AnalyzeDataOnly) {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-Analyzer"
$files = Get-HealthCheckFilesItemsFromLocation
$fullPaths = Get-OnlyRecentUniqueServersXMLs $files
$importData = Import-MyData -FilePaths $fullPaths
$analyzedResults = @()
foreach ($serverData in $importData) {
$analyzedServerResults = Invoke-AnalyzerEngine -HealthServerObject $serverData.HealthCheckerExchangeServer
Write-ResultsToScreen -ResultsToWrite $analyzedServerResults.DisplayResults
$analyzedResults += $analyzedServerResults
}
Get-HtmlServerReport -AnalyzedHtmlServerValues $analyzedResults.HtmlServerValues
return
}
if ($ScriptUpdateOnly) {
Invoke-ScriptLogFileLocation -FileName "HealthChecker-ScriptUpdateOnly"
switch (Test-ScriptVersion -AutoUpdate) {
($true) { Write-Green("Script was successfully updated.") }
($false) { Write-Yellow("No update of the script performed.") }
default { Write-Red("Unable to perform ScriptUpdateOnly operation.") }
}
return
}
Invoke-ScriptLogFileLocation -FileName "HealthChecker" -IncludeServerName $true
$currentErrors = $Error.Count
if ((-not $SkipVersionCheck) -and
(Test-ScriptVersion -AutoUpdate)) {
Write-Yellow "Script was updated. Please rerun the command."
return
} else {
$Script:DisplayedScriptVersionAlready = $true
Write-Green "Exchange Health Checker version $BuildVersion"
}
if ($currentErrors -ne $Error.Count) {
$index = 0
while ($index -lt ($Error.Count - $currentErrors)) {
Invoke-CatchActions $Error[$index]
$index++
}
}
Test-RequiresServerFqdn
[HealthChecker.HealthCheckerExchangeServer]$HealthObject = Get-HealthCheckerExchangeServer
$analyzedResults = Invoke-AnalyzerEngine -HealthServerObject $HealthObject
Write-ResultsToScreen -ResultsToWrite $analyzedResults.DisplayResults
$currentErrors = $Error.Count
try {
$analyzedResults | Export-Clixml -Path $OutXmlFullPath -Encoding UTF8 -Depth 6 -ErrorAction SilentlyContinue
} catch {
Write-Verbose "Failed to Export-Clixml. Converting HealthCheckerExchangeServer to json"
$jsonHealthChecker = $analyzedResults.HealthCheckerExchangeServer | ConvertTo-Json
$testOuputxml = [PSCustomObject]@{
HealthCheckerExchangeServer = $jsonHealthChecker | ConvertFrom-Json
HtmlServerValues = $analyzedResults.HtmlServerValues
DisplayResults = $analyzedResults.DisplayResults
}
$testOuputxml | Export-Clixml -Path $OutXmlFullPath -Encoding UTF8 -Depth 6 -ErrorAction Stop
} finally {
if ($currentErrors -ne $Error.Count) {
$index = 0
while ($index -lt ($Error.Count - $currentErrors)) {
Invoke-CatchActions $Error[$index]
$index++
}
}
Write-Grey("Output file written to {0}" -f $Script:OutputFullPath)
Write-Grey("Exported Data Object Written to {0} " -f $Script:OutXmlFullPath)
}
}
try {
$Script:Logger = Get-NewLoggerInstance -LogName "HealthChecker-$($Script:Server)-Debug" `
-LogDirectory $OutputFilePath `
-AppendDateTime $false `
-ErrorAction SilentlyContinue
SetProperForegroundColor
Main
} finally {
Get-ErrorsThatOccurred
if ($Script:VerboseEnabled) {
$Host.PrivateData.VerboseForegroundColor = $VerboseForeground
}
$Script:Logger | Invoke-LoggerInstanceCleanup
if ($Script:Logger.PreventLogCleanup) {
Write-Host("Output Debug file written to {0}" -f $Script:Logger.FullPath)
}
if (((Get-Date).Ticks % 2) -eq 1) {
Write-Host("Do you like the script? Visit https://aka.ms/HC-Feedback to rate it and to provide feedback.") -ForegroundColor Green
Write-Host
}
RevertProperForegroundColor
}
# SIG # Begin signature block
# MIIjtwYJKoZIhvcNAQcCoIIjqDCCI6QCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAgZIDfmczGklZg
# X/ZdA5C43qysLFoihj2FffelbtldvKCCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVjDCCFYgCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBxjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg7Rc5YYS6
# LGDgCXxWQjBpIRZTLsALglbdlBwcjHA+fPcwWgYKKwYBBAGCNwIBDDFMMEqgGoAY
# AEMAUwBTACAARQB4AGMAaABhAG4AZwBloSyAKmh0dHBzOi8vZ2l0aHViLmNvbS9t
# aWNyb3NvZnQvQ1NTLUV4Y2hhbmdlIDANBgkqhkiG9w0BAQEFAASCAQBP4/mlTuRC
# VPxII3nEHEYl3IvBUHkiWogVl/nPgJCMgB++dHgl1GQvZYmzQl3ZZtnH8FHpDt5u
# 6wgTA4LZE9JnM5vMyYOjPtoafC4amQOfNCj6XSMT8WwRzDgJK8thc3gyahEKOt0e
# fAktoX9UopcXN24M8sfQ2HW3C52ynlVb4899qIwNFJ/FyZLeMvSkN7w0Ab0LKmuy
# jE5/HGoD5U58A9MEokQDQGS2Rl7Ji3tkxJvYiqXYtg/qXhZu7FwerWeNzULPw4n+
# 3ORftGXmKyDkFfV9Fa6iIu/ayKIkxeGIJ3djjiQKLFGYIoJlOy5miv/OoyKLyqDp
# Evgve89d75AToYIS/jCCEvoGCisGAQQBgjcDAwExghLqMIIS5gYJKoZIhvcNAQcC
# oIIS1zCCEtMCAQMxDzANBglghkgBZQMEAgEFADCCAVkGCyqGSIb3DQEJEAEEoIIB
# SASCAUQwggFAAgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIGWsVMd/
# oX/QAuw1vmq6ShhMYiLOiBdjAFEobiWtXJq3AgZhgwjEMkcYEzIwMjExMTA5MjA0
# NjA5LjM1MVowBIACAfSggdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlv
# bnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RkM0MS00QkQ0LUQy
# MjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wggg5NMIIE
# +TCCA+GgAwIBAgITMwAAAUAjGdZe3pUkMQAAAAABQDANBgkqhkiG9w0BAQsFADB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMDEwMTUxNzI4MjZaFw0y
# MjAxMTIxNzI4MjZaMIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkZDNDEtNEJENC1EMjIwMSUwIwYD
# VQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEArn1rM3Hq1S9N0z8R+YKqZu25ykk5OlT8TsuwtdBW
# yDCRFoASk9fB6siColFhXBhyej4c3yIwN0UyJWBOTAjHteOIYjfCpx539rbgBI5/
# BTHtC+qcBT7ftPknTtQn89lNOcpP70fuYVZLoQsDnLjGxxtW/eVewR5Q0I1mWQfJ
# y5xOfelk5OWjz3YV4HKtqyIRzJZd/RzcY8w6qmzoSNsYIdvliT2eeQZbyYTdJQsR
# ozIKTMLCJUBfVjow2fJMDtzDB9XEOdfhPWzvUOadYgqqh0lslAR7NV90FFmZgZWA
# RrG8j7ZnVnC5MOXOS/NI58S48ycsug0pN2NGLLk2YWjxCwIDAQABo4IBGzCCARcw
# HQYDVR0OBBYEFDVDHC4md0YgjozSqnVs+OeELQ5nMB8GA1UdIwQYMBaAFNVjOlyK
# MZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWlj
# cm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0YVBDQV8yMDEwLTA3
# LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENBXzIwMTAtMDctMDEu
# Y3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcN
# AQELBQADggEBAGMMUq2gQuCC9wr4YhISfPyobaNYV3Ov4YwWsSfIz/j1xaN9TvLA
# B2BxPi2CtRbgbBUf48n07yReZInwu2r8vwLoNG2TtYzey01DRyjjsNoiHF9UuRLF
# yKZChkKC3o9r0Oy2x0YYjUpDxVChZ5q5cAfw884wP0iUcYnKKGn8eJ0nwpr7zr/T
# lu+HOjXDT9C754aS4KUFNm8D7iwuvWWzSOVl7XMWdu82BnnTmB7s2Ocf3I4adGzd
# ixQ5Zxxa3zOAvKzrV+0HcVQIY3SQJ9PzjDRlzCviMThxA8FUIRL3FnYqvchWkEoZ
# 4w8S7FsGWNlXLWQ7fHMb3l4gjueHyO4p6tUwggZxMIIEWaADAgECAgphCYEqAAAA
# AAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBB
# dXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0yNTA3MDEyMTQ2NTVaMHwx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p
# Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjANBgkqhkiG9w0BAQEFAAOC
# AQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RUENWlCgCChfvtfGhLLF/F
# w+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBED/FgiIRUQwzXTbg4CLNC
# 3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50YWeRX4FUsc+TTJLBxKZd
# 0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd/XcfPfBXday9ikJNQFHR
# D5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaRtogINeh4HLDpmc085y9E
# uqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQABo4IB5jCCAeIwEAYJKwYB
# BAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8RhvFM2hahW1VMBkGCSsG
# AQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTAD
# AQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0w
# S6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz
# L01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYI
# KwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWlj
# Um9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSABAf8EgZUwgZIwgY8GCSsG
# AQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEFBQcCAjA0HjIgHQBMAGUA
# ZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBtAGUAbgB0AC4gHTANBgkq
# hkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Ehb7Prpsz1Mb7PBeKp/vpX
# bRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7uVOMzPRgEop2zEBAQZvc
# XBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqRUgCvOA8X9S95gWXZqbVr
# 5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9Va8v/rbljjO7Yl+a21dA
# 6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8+n99lmqQeKZt0uGc+R38
# ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+Y1klD3ouOVd2onGqBooP
# iRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh2rBQHm+98eEA3+cxB6ST
# OvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRyzR30uIUBHoD7G4kqVDmy
# W9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoouLGp25ayp0Kiyc8ZQU3g
# hvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx16HSxVXjad5XwdHeMMD9
# zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341Hgi62jbb01+P3nSISRKh
# ggLXMIICQAIBATCCAQChgdikgdUwgdIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlv
# bnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RkM0MS00QkQ0LUQy
# MjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAH
# BgUrDgMCGgMVAEKl5h7yE6Y7MpfmMpEbQzkJclFToIGDMIGApH4wfDELMAkGA1UE
# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDlNB5hMCIYDzIw
# MjExMTA5MDYwODMzWhgPMjAyMTExMTAwNjA4MzNaMHcwPQYKKwYBBAGEWQoEATEv
# MC0wCgIFAOU0HmECAQAwCgIBAAICJgcCAf8wBwIBAAICEVQwCgIFAOU1b+ECAQAw
# NgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgC
# AQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQAv5J22ouo8Ur3kyn+VcIxy1uD9Dkhg
# zCbqVELk0+4UAJwp3qU/NoR0te4cq+g8c3pQe1znd3uf3khv9ijgH1Xz18e9qGsi
# /ezIChqNIvOdOLOMrPkT3l8jwhnWfB6aicK1uKfjKOMJVB5eLzD+jX81FTtXtfyf
# LQH6AR5OkCMCHTGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwAhMzAAABQCMZ1l7elSQxAAAAAAFAMA0GCWCGSAFlAwQCAQUAoIIBSjAa
# BgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIGBtTUn9
# pH8fFPtCaaQuLGDyjy3XmHRvnZ65JilMYZ2qMIH6BgsqhkiG9w0BCRACLzGB6jCB
# 5zCB5DCBvQQgLzawterM0qRcJO/zcvJT7do/ycp8RZsRSTqqtgIIl4MwgZgwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAUAjGdZe3pUkMQAA
# AAABQDAiBCBD6FdvvycZgPWg+kOEZBsh841MCpxmJuPTnPnBvhJoXzANBgkqhkiG
# 9w0BAQsFAASCAQB+tf1kIENusQ02Ns6EeicbuyyhcETubCs8UPPRVDF2QqQjNlTu
# 3/MZQeN+R9PeQcfywkTo2YNZ5wgLnk2V0LrgX2jAC+dCGL/aNadOma5wtw+WWiY3
# 5HTdMyEJNM1h7ee7P7zD2pgl6Udrh+UFthgBxHlCT8wbNVcRyr3ZtgfBR7mT/d7U
# BXGV6d6rEEkqgbuGgtrpnXiO99d7Nt0MiNK2OnnIle/yjT0rYaWKFzRb5WethhYX
# xY45/h/iHUHHToEOIvMQ0Q2hAmDrTw9d1Ad7KxPmGGenE1/AiufIRC88MlNwmt9S
# 2d0JD9T6KTGpHz60XxRHSAXM4gx3U+WGEw4X
# SIG # End signature block