<# .NOTES Name: HealthChecker.ps1 Original Author: Marc Nivens Author: David Paulson Contributor: Jason Shinbaum, Michael Schatte, Lukas Sassl Requires: Exchange Management Shell and administrator rights on the target Exchange server as well as the local machine. Major Release History: 11/10/2020 - Initial Public Release of version 3. 1/18/2017 - Initial Public Release of version 2. - rewritten by David Paulson. 3/30/2015 - Initial Public Release. 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. .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 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 #> [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]$SaveDebugLog ) $healthCheckerVersion = "3.1.1" $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://www.vmware.com/files/pdf/Exchange_2013_on_VMware_Best_Practices_Guide.pdf. 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: http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=2039495. For further details, please review the virtualization recommendations on Microsoft Docs at the following locations: Exchange 2013: https://docs.microsoft.com/en-us/exchange/exchange-2013-virtualization-exchange-2013-help#requirements-for-hardware-virtualization. Exchange 2016/2019: https://docs.microsoft.com/en-us/exchange/plan-and-deploy/virtualization?view=exchserver-2019. "@ #this is to set the verbose information to a different color if($PSBoundParameters["Verbose"]){ #Write verose output in cyan since we already use yellow for warnings $Script:VerboseEnabled = $true $VerboseForeground = $Host.PrivateData.VerboseForegroundColor $Host.PrivateData.VerboseForegroundColor = "Cyan" } 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. } // ExchangeInformation public class ExchangeInformation { public ExchangeBuildInformation BuildInformation = new ExchangeBuildInformation(); //Exchange build information public object GetExchangeServer; //Stores the Get-ExchangeServer Object public ExchangeNetFrameworkInformation NETFramework = new ExchangeNetFrameworkInformation(); public bool MapiHttpEnabled; //Stored from organization config public string ExchangeServicesNotRunning; //Contains the Exchange services not running by Test-ServiceHealth public Hashtable ApplicationPools; public ExchangeRegistryValues RegistryValues = new ExchangeRegistryValues(); public ExchangeServerMaintenance ServerMaintenance; public System.Array ExchangeCertificates; //stores all the Exchange certificates on the servers. } 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 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 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; } //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 } // 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 PageFileInformation PageFile; //stores the page file information public LmCompatibilityLevelInformation LmCompatibility; // stores Lm Compatibility Level Information public bool ServerPendingReboot; // determines if the server is pending a reboot. TODO: Adjust to contain the registry values that we are looking at. 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 Smb1ServerSettings Smb1ServerSettings = new 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 string 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 PageFileInformation { public object PageFile; //store the information that we got for the page file public double MaxPageSize; //holds the information of what our page file is set to } 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; } //enum for the dword values of the latest supported VC++ redistributable releases //https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads public enum VCRedistVersion { Unknown = 0, VCRedist2012 = 184610406, VCRedist2013 = 201367256 } public class SoftwareInformation { public string DisplayName; public string DisplayVersion; public string InstallDate; public int VersionIdentifier; } 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 } public class Smb1ServerSettings { public object RegistryValue; public object SmbServerConfiguration; public object WindowsFeature; public int Smb1Status; } // 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 double TotalMemory; //Stores the total memory available 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 int TabNumber; public object TestingValue; //Used for pester testing down the road. 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-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-VerboseOutput($message) { Write-Verbose $message Write-DebugLog $message if($Script:VerboseEnabled) { $message | Out-File ($OutputFullPath) -Append } } function Write-DebugLog($message) { if(![string]::IsNullOrEmpty($message)) { $Script:Logger.WriteToFileOnly($message) } } Function Write-Break { Write-Host "" } #Function Version 1.1 Function Write-HostWriter { param( [Parameter(Mandatory=$true)][string]$WriteString ) if($Script:Logger -ne $null) { $Script:Logger.WriteHost($WriteString) } elseif($HostFunctionCaller -eq $null) { Write-Host $WriteString } else { &$HostFunctionCaller $WriteString } } Function Write-VerboseWriter { param( [Parameter(Mandatory=$true)][string]$WriteString ) if($VerboseFunctionCaller -eq $null) { Write-Verbose $WriteString } else { &$VerboseFunctionCaller $WriteString } } $Script:VerboseFunctionCaller = ${Function:Write-VerboseOutput} #Function Version 1.0 Function Write-ScriptMethodHostWriter{ param( [Parameter(Mandatory=$true)][string]$WriteString ) if($this.LoggerObject -ne $null) { $this.LoggerObject.WriteHost($WriteString) } elseif($this.HostFunctionCaller -eq $null) { Write-Host $WriteString } else { $this.HostFunctionCaller($WriteString) } } #Function Version 1.0 Function Write-ScriptMethodVerboseWriter { param( [Parameter(Mandatory=$true)][string]$WriteString ) if($this.LoggerObject -ne $null) { $this.LoggerObject.WriteVerbose($WriteString) } elseif($this.VerboseFunctionCaller -eq $null -and $this.WriteVerboseData) { Write-Host $WriteString -ForegroundColor Cyan } elseif($this.WriteVerboseData) { $this.VerboseFunctionCaller($WriteString) } } Function Write-HealthCheckerVersion { $currentVersion = Test-ScriptVersion -ApiUri "api.github.com" -RepoOwner "dpaulson45" ` -RepoName "HealthChecker" ` -CurrentVersion $healthCheckerVersion ` -DaysOldLimit 90 ` -CatchActionFunction ${Function:Invoke-CatchActions} $Script:DisplayedScriptVersionAlready = $true if($currentVersion) { Write-Green("Exchange Health Checker version {0}" -f $healthCheckerVersion) } else { Write-Yellow("Exchange Health Checker version {0}. This script is probably outdated. Please verify before relying on the results." -f $healthCheckerVersion) } } Function Write-ResultsToScreen { param( [Hashtable]$ResultsToWrite ) Write-VerboseOutput("Calling: Write-ResultsToScreen") $indexOrderGroupingToKey = @{} foreach ($keyGrouping in $ResultsToWrite.Keys) { $indexOrderGroupingToKey[$keyGrouping.DisplayOrder] = $keyGrouping } $sortedIndexOrderGroupingToKey = $indexOrderGroupingToKey.Keys | Sort-Object foreach ($key in $sortedIndexOrderGroupingToKey) { Write-VerboseOutput("Working on Key: {0}" -f $key) $keyGrouping = $indexOrderGroupingToKey[$key] Write-VerboseOutput("Working on Key Group: {0}" -f $keyGrouping.Name) Write-VerboseOutput("Total lines to write: {0}" -f ($ResultsToWrite[$keyGrouping].Count)) if ($keyGrouping.DisplayGroupName) { Write-Grey($keyGrouping.Name) $dashes = [string]::empty 1..($keyGrouping.Name.Length) | %{$dashes = $dashes + "-"} Write-Grey($dashes) } foreach ($line in $ResultsToWrite[$keyGrouping]) { $tab = [string]::Empty if ($line.TabNumber -ne 0) { 1..($line.TabNumber) | %{$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)} } } Write-Grey("") } } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-AllNicInformation/Get-AllNicInformation.ps1 Function Get-AllNicInformation { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ComputerName, [Parameter(Mandatory=$false)][string]$ComputerFQDN, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.6 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-WmiObjectHandler/Get-WmiObjectHandler.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 #> Write-VerboseWriter("Calling: Get-AllNicInformation") Write-VerboseWriter("Passed [string]ComputerName: {0} | [string]ComputerFQDN: {1}" -f $ComputerName, $ComputerFQDN) Function Get-NicPnpCapabilitiesSetting { [CmdletBinding()] param( [string]$NicAdapterComponentId ) if ($NicAdapterComponentId -eq [string]::Empty) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid NicAdapterDeviceId or NicAdapterComponentId" } $nicAdapterBasicPath = "SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}" Write-VerboseWriter("Probing started to detect NIC adapter registry path") $nicAdapterRegKey = Invoke-RegistryGetValue -MachineName $ComputerName -Subkey $nicAdapterBasicPath -ReturnAfterOpenSubKey $true -CatchActionFunction $CatchActionFunction $nicAdapterDeviceIDs = $nicAdapterRegKey.GetSubKeyNames() | Where-Object {$_.StartsWith("0")} foreach($deviceID in $nicAdapterDeviceIDs) { $nicAdapterPnPCapabilitiesProbingKey = "{0}\{1}" -f $nicAdapterBasicPath, $deviceID $netCfgInstanceId = Invoke-RegistryGetValue -MachineName $ComputerName -Subkey $nicAdapterPnPCapabilitiesProbingKey -GetValue "NetCfgInstanceId" -CatchActionFunction $CatchActionFunction if ($netCfgInstanceId -eq $NicAdapterComponentId) { Write-VerboseWriter("Matching ComponentId found - now checking for PnPCapabilitiesValue") $nicAdapterPnPCapabilitiesValue = Invoke-RegistryGetValue -MachineName $ComputerName -SubKey $nicAdapterPnPCapabilitiesProbingKey -GetValue "PnPCapabilities" -CatchActionFunction $CatchActionFunction break } else { Write-VerboseWriter("No matching ComponentId found") } } $obj = New-Object PSCustomObject $sleepyNicDisabled = $false if ($nicAdapterPnPCapabilitiesValue -eq 24 -or $nicAdapterPnPCapabilitiesValue -eq 280) { $sleepyNicDisabled = $true } $obj | Add-Member -MemberType NoteProperty -Name "PnPCapabilities" -Value $nicAdapterPnPCapabilitiesValue $obj | Add-Member -MemberType NoteProperty -Name "SleepyNicDisabled" -Value $sleepyNicDisabled return $obj } Function Get-NetworkConfiguration { [CmdletBinding()] param( [string]$ComputerName ) try { $currentErrors = $Error.Count $cimSession = New-CimSession -ComputerName $ComputerName -ErrorAction Stop $networkIpConfiguration = Get-NetIPConfiguration -CimSession $CimSession -ErrorAction Stop | Where-Object {$_.NetAdapter.MediaConnectionState -eq "Connected"} if ($CatchActionFunction -ne $null) { $index = 0 while ($index -lt ($Error.Count - $currentErrors)) { & $CatchActionFunction $Error[$index] $index++ } } return $networkIpConfiguration } catch { Write-VerboseWriter("Failed to run Get-NetIPConfiguration. Error {0}." -f $Error[0].Exception) if ($CatchActionFunction -ne $null) { & $CatchActionFunction } throw } } Function New-NICInformation { param( [array]$NetworkConfigurations, [bool]$WmiObject ) if($NetworkConfigurations -eq $null) { Write-VerboseWriter("NetworkConfigurations are null in New-NICInformation. Returning a null object.") return $null } Function New-IpvAddresses { $obj = New-Object PSCustomObject $obj | Add-Member -MemberType NoteProperty -Name "Address" -Value ([string]::Empty) $obj | Add-Member -MemberType NoteProperty -Name "Subnet" -Value ([string]::Empty) $obj | Add-Member -MemberType NoteProperty -Name "DefaultGateway" -Value ([string]::Empty) return $obj } if ($WmiObject) { $networkAdapterConfigurations = Get-WmiObjectHandler -ComputerName $ComputerName -Class "Win32_NetworkAdapterConfiguration" -Filter "IPEnabled = True" -CatchActionFunction $CatchActionFunction } [array]$nicObjects = @() foreach($networkConfig in $NetworkConfigurations) { $dnsClient = $null $rssEnabledValue = 2 $netAdapterRss = $null if (!$WmiObject) { Write-VerboseWriter("Working on NIC: {0}" -f $networkConfig.InterfaceDescription) $adapter = $networkConfig.NetAdapter if($adapter.DriverFileName -ne "NdisImPlatform.sys") { $nicPnpCapabilitiesSetting = Get-NicPnpCapabilitiesSetting -NicAdapterComponentId $adapter.DeviceID } else { Write-VerboseWriter("Multiplexor adapter detected. Going to skip PnpCapabilities check") $nicPnpCapabilitiesSetting = @{ PnPCapabilities = "MultiplexorNoPnP" } } try { $dnsClient = $adapter | Get-DnsClient -ErrorAction Stop Write-VerboseWriter("Got DNS Client information") } catch { Write-VerboseWriter("Failed to get the DNS Client information") if ($CatchActionFunction -ne $null) { & $CatchActionFunction } } try { $netAdapterRss = $adapter | Get-NetAdapterRss -ErrorAction Stop Write-VerboseWriter("Got Net Adapter RSS information") if ($netAdapterRss -ne $null) { [int]$rssEnabledValue = $netAdapterRss.Enabled } } catch { Write-VerboseWriter("Failed to get RSS Information") if ($CatchActionFunction -ne $null) { & $CatchActionFunction } } } else { Write-VerboseWriter("Working on NIC: {0}" -f $networkConfig.Description) $adapter = $networkConfig if($adapter.ServiceName -ne "NdisImPlatformMp") { $nicPnpCapabilitiesSetting = Get-NicPnpCapabilitiesSetting -NicAdapterComponentId $adapter.Guid } else { Write-VerboseWriter("Multiplexor adapter detected. Going to skip PnpCapabilities check") $nicPnpCapabilitiesSetting = @{ PnPCapabilities = "MultiplexorNoPnP" } } } $nicInformationObj = New-Object PSCustomObject $nicInformationObj | Add-Member -MemberType NoteProperty -Name "WmiObject" -Value $WmiObject $nicInformationObj | Add-Member -MemberType NoteProperty -Name "Name" -Value ($adapter.Name) $nicInformationObj | Add-Member -MemberType NoteProperty -Name "LinkSpeed" -Value ((($adapter.Speed)/1000000).ToString() + " Mbps") $nicInformationObj | Add-Member -MemberType NoteProperty -Name "DriverDate" -Value [DateTime]::MaxValue $nicInformationObj | Add-Member -MemberType NoteProperty -Name "NICObject" -Value $networkConfig $nicInformationObj | Add-Member -MemberType NoteProperty -Name "NetAdapterRss" -Value $netAdapterRss $nicInformationObj | Add-Member -MemberType NoteProperty -Name "RssEnabledValue" -Value $rssEnabledValue $nicInformationObj | Add-Member -MemberType NoteProperty -Name "IPv6Enabled" -Value $false $nicInformationObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $adapter.Description $nicInformationObj | Add-Member -MemberType NoteProperty -Name "DriverVersion" -Value [string]::Empty $nicInformationObj | Add-Member -MemberType NoteProperty -Name "MTUSize" -Value 0 $nicInformationObj | Add-Member -MemberType NoteProperty -Name "PnPCapabilities" -Value ($nicPnpCapabilitiesSetting.PnPCapabilities) $nicInformationObj | Add-Member -MemberType NoteProperty -Name "SleepyNicDisabled" -Value ($nicPnpCapabilitiesSetting.SleepyNicDisabled) if (!$WmiObject) { $nicInformationObj.MTUSize = $adapter.MtuSize $nicInformationObj.DriverDate = $adapter.DriverDate $nicInformationObj.DriverVersion = $adapter.DriverVersionString $nicInformationObj.Description = $adapter.InterfaceDescription foreach ($ipAddress in $networkConfig.AllIPAddresses.IPAddress) { if ($ipAddress.Contains(":")) { $nicInformationObj.IPv6Enabled = $true } } $ipv4Address = @() for ($i = 0; $i -lt $networkConfig.IPv4Address.Count; $i++) { $obj = New-IpvAddresses if ($networkConfig.IPv4Address -ne $null -and $i -lt $networkConfig.IPv4Address.Count) { $obj.Address = $networkConfig.IPv4Address[$i].IPAddress $obj.Subnet = $networkConfig.IPv4Address[$i].PrefixLength } if ($networkConfig.IPv4DefaultGateway -ne $null -and $i -lt $networkConfig.IPv4DefaultGateway.Count) { $obj.DefaultGateway = $networkConfig.IPv4DefaultGateway[$i].NextHop } $ipv4Address += $obj } $ipv6Address = @() for ($i = 0; $i -lt $networkConfig.IPv6Address.Count; $i++) { $obj = New-IpvAddresses if ($networkConfig.IPv6Address -ne $null -and $i -lt $networkConfig.IPv6Address.Count) { $obj.Address = $networkConfig.IPv6Address[$i].IPAddress $obj.Subnet = $networkConfig.IPv6Address[$i].PrefixLength } if ($networkConfig.IPv6DefaultGateway -ne $null -and $i -lt $networkConfig.IPv6DefaultGateway.Count) { $obj.DefaultGateway = $networkConfig.IPv6DefaultGateway[$i].NextHop } $ipv6Address += $obj } $nicInformationObj | Add-Member -MemberType NoteProperty -Name "IPv4Addresses" -Value $ipv4Address $nicInformationObj | Add-Member -MemberType NoteProperty -Name "Ipv6Addresses" -Value $ipv6Address $nicInformationObj | Add-Member -MemberType NoteProperty -Name "RegisteredInDns" -Value $dnsClient.RegisterThisConnectionsAddress $nicInformationObj | Add-Member -MemberType NoteProperty -Name "DnsServer" -Value $networkConfig.DNSServer.ServerAddresses $nicInformationObj | Add-Member -MemberType NoteProperty -Name "DnsClientObject" -Value $dnsClient } else { $stopProcess = $false foreach ($adapterConfiguration in $networkAdapterConfigurations) { Write-VerboseWriter("Working on '{0}' | SettingID: {1}" -f $adapterConfiguration.Description, ($settingId = $adapterConfiguration.SettingID)) if ($settingId -eq $networkConfig.GUID -or $settingId -eq $networkConfig.InterfaceGuid) { foreach ($ipAddress in $adapterConfiguration.IPAddress) { if ($ipAddress.Contains(":")) { $nicInformationObj.IPv6Enabled = $true $stopProcess = $true break } } } if ($stopProcess) { break } } } $nicObjects += $nicInformationObj } Write-VerboseWriter("Found {0} active adapters on the computer." -f $nicObjects.Count) Write-VerboseWriter("Exiting: Get-AllNicInformation") return $nicObjects } try { try { $networkConfiguration = Get-NetworkConfiguration -ComputerName $ComputerName } catch { if ($CatchActionFunction -ne $null) { & $CatchActionFunction } if ($ComputerFQDN -ne [string]::Empty -and $ComputerName -ne $null) { $networkConfiguration = Get-NetworkConfiguration -ComputerName $ComputerFQDN } else { $bypassCatchActions = $true Write-VerboseWriter("No FQDN was passed, going to rethrow error.") throw } } return (New-NICInformation -NetworkConfigurations $networkConfiguration) } catch { if (!$bypassCatchActions -and $CatchActionFunction -ne $null) { & $CatchActionFunction } $wmiNetworkCards = Get-WmiObjectHandler -ComputerName $ComputerName -Class "Win32_NetworkAdapter" -Filter "NetConnectionStatus ='2'" -CatchActionFunction $CatchActionFunction return (New-NICInformation -NetworkConfigurations $wmiNetworkCards -WmiObject $true) } } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-AllTlsSettingsFromRegistry/Get-AllTlsSettingsFromRegistry.ps1 Function Get-AllTlsSettingsFromRegistry { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$MachineName, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.1 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 #> Write-VerboseWriter("Calling: Get-AllTlsSettingsFromRegistry") Write-VerboseWriter("Passed: [string]MachineName: {0}" -f $MachineName) $registryBase = "SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS {0}\{1}" $tlsVersions = @("1.0","1.1","1.2") $tlsResults = @{} $keyValues = ("Enabled","DisabledByDefault") Function Set-TLSMemberValue { param( [Parameter(Mandatory=$true)][string]$GetKeyType, [Parameter(Mandatory=$false)][object]$KeyValue, [Parameter(Mandatory=$true)][string]$ServerClientType, [Parameter(Mandatory=$true)][string]$TlsVersion ) switch($GetKeyType) { "Enabled" { if($KeyValue -eq $null) { Write-VerboseWriter("Failed to get TLS {0} {1} Enabled Key on Server {2}. We are assuming this means it is enabled." -f $TlsVersion, $ServerClientType, $MachineName) return $true } else { Write-VerboseWriter("{0} Enabled Value '{1}'" -f $ServerClientType, $serverValue) if($KeyValue -eq 1) { return $true } return $false } } "DisabledByDefault" { if($KeyValue -eq $null) { Write-VerboseWriter("Failed to get TLS {0} {1} Disabled By Default Key on Server {2}. Setting to false." -f $TlsVersion, $ServerClientType, $MachineName) return $false } else { Write-VerboseWriter("{0} Disabled By Default Value '{1}'" -f $ServerClientType, $serverValue) if($KeyValue -eq 1) { return $true } return $false } } } } Function Set-NETDefaultTLSValue { param( [Parameter(Mandatory=$false)][object]$KeyValue, [Parameter(Mandatory=$true)][string]$NetVersion, [Parameter(Mandatory=$true)][string]$KeyName ) if($KeyValue -eq $null) { Write-VerboseWriter("Failed to get {0} registry value for .NET {1} version. Setting to false." -f $KeyName, $NetVersion) return $false } else { Write-VerboseWriter("{0} value '{1}'" -f $KeyName, $KeyValue) if($KeyValue -eq 1) { return $true } return $false } } [hashtable]$allTlsObjects = @{} foreach($tlsVersion in $tlsVersions) { $registryServer = $registryBase -f $tlsVersion, "Server" $registryClient = $registryBase -f $tlsVersion, "Client" $currentTLSObject = New-Object PSCustomObject $currentTLSObject | Add-Member -MemberType NoteProperty -Name "TLSVersion" -Value $tlsVersion foreach($getKey in $keyValues) { $memberServerName = "Server{0}" -f $getKey $memberClientName = "Client{0}" -f $getKey $serverValue = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $MachineName -SubKey $registryServer -GetValue $getKey -CatchActionFunction $CatchActionFunction $clientValue = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $MachineName -SubKey $registryClient -GetValue $getKey -CatchActionFunction $CatchActionFunction $currentTLSObject | Add-Member -MemberType NoteProperty -Name $memberServerName -Value (Set-TLSMemberValue -GetKeyType $getKey -KeyValue $serverValue -ServerClientType "Server" -TlsVersion $tlsVersion) $currentTLSObject | Add-Member -MemberType NoteProperty -Name $memberClientName -Value (Set-TLSMemberValue -GetKeyType $getKey -KeyValue $clientValue -ServerClientType "Client" -TlsVersion $tlsVersion) } $allTlsObjects.Add($tlsVersion, $currentTLSObject) } $netVersions = @("v2.0.50727","v4.0.30319") $registryBase = "SOFTWARE\{0}\.NETFramework\{1}" foreach($netVersion in $netVersions) { $currentNetTlsDefaultVersionObject = New-Object PSCustomObject $currentNetTlsDefaultVersionObject | Add-Member -MemberType NoteProperty -Name "NetVersion" -Value $netVersion $SystemDefaultTlsVersions = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $MachineName -SubKey ($registryBase -f "Microsoft", $netVersion) -GetValue "SystemDefaultTlsVersions" -CatchActionFunction $CatchActionFunction $WowSystemDefaultTlsVersions = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $MachineName -SubKey ($registryBase -f "Wow6432Node\Microsoft", $netVersion) -GetValue "SystemDefaultTlsVersions" -CatchActionFunction $CatchActionFunction $currentNetTlsDefaultVersionObject | Add-Member -MemberType NoteProperty -Name "SystemDefaultTlsVersions" -Value (Set-NETDefaultTLSValue -KeyValue $SystemDefaultTlsVersions -NetVersion $netVersion -KeyName "SystemDefaultTlsVersions") $currentNetTlsDefaultVersionObject | Add-Member -MemberType NoteProperty -Name "WowSystemDefaultTlsVersions" -Value (Set-NETDefaultTLSValue -KeyValue $WowSystemDefaultTlsVersions -NetVersion $netVersion -KeyName "WowSystemDefaultTlsVersions") $hashKeyName = "NET{0}" -f ($netVersion.Split(".")[0]) $allTlsObjects.Add($hashKeyName, $currentNetTlsDefaultVersionObject) } return $allTlsObjects } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-DotNetDllFileVersions/Get-DotNetDllFileVersions.ps1 Function Get-DotNetDllFileVersions { [CmdletBinding()] param( [string]$ComputerName, [array]$FileNames, [scriptblock]$CatchActionFunction ) #Function Version 1.1 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-ScriptBlockHandler/Invoke-ScriptBlockHandler.ps1 #> Write-VerboseWriter("Calling: Get-DotNetDllFileVersions") Function ScriptBlock-GetItem{ 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 } $dotNetInstallPath = Invoke-RegistryGetValue -MachineName $ComputerName -SubKey "SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -GetValue "InstallPath" -CatchActionFunction $CatchActionFunction if ($dotNetInstallPath -eq [string]::Empty) { Write-VerboseWriter("Failed to determine .NET install path") return } $files = @{} foreach($filename in $FileNames) { Write-VerboseWriter("Query .NET DLL information for machine: {0}" -f $ComputerName) $getItem = Invoke-ScriptBlockHandler -ComputerName $ComputerName -ScriptBlock ${Function:ScriptBlock-GetItem} -ArgumentList ("{0}\{1}" -f $dotNetInstallPath, $filename) -CatchActionFunction $CatchActionFunction $files.Add($filename, $getItem) } return $files } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-ExchangeMajorVersion/Get-ExchangeMajorVersion.ps1 Function Get-ExchangeMajorVersion { [CmdletBinding()] param( [Parameter(Mandatory=$true)][object]$AdminDisplayVersion ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> Write-VerboseWriter("Calling: Get-ExchangeMajorVersion") Write-VerboseWriter("Passed: {0}" -f $AdminDisplayVersion.ToString()) if($AdminDisplayVersion.GetType().Name -eq "string") { $split = $AdminDisplayVersion.Substring(($AdminDisplayVersion.IndexOf(" ")) + 1, 4).split('.') $build = [int]$split[0] + ($split[1] / 10) } else { $build = $AdminDisplayVersion.Major + ($AdminDisplayVersion.Minor / 10) } Write-VerboseWriter("Determing build based off of: {0}" -f $build) $exchangeMajorVersion = [string]::Empty switch($build) { 14.3 {$exchangeMajorVersion = "Exchange2010"} 15 {$exchangeMajorVersion = "Exchange2013"} 15.1 {$exchangeMajorVersion = "Exchange2016"} 15.2 {$exchangeMajorVersion = "Exchange2019"} default {$exchangeMajorVersion = "Unknown"} } Write-VerboseWriter("Returned: {0}" -f $exchangeMajorVersion) return $exchangeMajorVersion } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-NETFrameworkVersion/Get-NETFrameworkVersion.ps1 Function Get-NETFrameworkVersion { [CmdletBinding()] param( [string]$MachineName = $env:COMPUTERNAME, [int]$NetVersionKey = -1, [scriptblock]$CatchActionFunction ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 #> Write-VerboseWriter("Calling: Get-NETFrameworkVersion") if ($NetVersionKey -eq -1) { [int]$NetVersionKey = Invoke-RegistryGetValue -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 } $netObject = New-Object PSCustomObject $netObject | Add-Member -MemberType NoteProperty -Name "FriendlyName" -Value $friendlyName $netObject | Add-Member -MemberType NoteProperty -Name "RegistryValue" -Value $NetVersionKey $netObject | Add-Member -MemberType NoteProperty -Name "MinimumValue" -Value $minValue Write-VerboseWriter("Returning FriendlyName: {0} | RegistryValue: {1}" -f $friendlyName, $NetVersionKey) return $netObject } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-ProcessorInformation/Get-ProcessorInformation.ps1 Function Get-ProcessorInformation { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$MachineName, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-WmiObjectHandler/Get-WmiObjectHandler.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-ScriptBlockHandler/Invoke-ScriptBlockHandler.ps1 #> Write-VerboseWriter("Calling: Get-ProcessorInformation") $wmiObject = Get-WmiObjectHandler -ComputerName $MachineName -Class "Win32_Processor" -CatchActionFunction $CatchActionFunction Write-VerboseWriter("Processor object type: {0}" -f ($wmiObjectType = $wmiObject.GetType().Name)) $multiProcessorsDetected = $false if($wmiObjectType -eq "ManagementObject") { $processorName = $wmiObject.Name $maxClockSpeed = $wmiObject.MaxClockSpeed $multiProcessorsDetected = $true } else { $processorName = $wmiObject[0].Name $maxClockSpeed = $wmiObject[0].MaxClockSpeed } Write-VerboseWriter("Getting the total number of cores in the processor(s)") $processorIsThrottled = $false $currentClockSpeed = 0 $previousProcessor = $null $differentProcessorsDetected = $false $differentProcessorCoreCountDetected = $false foreach($processor in $wmiObject) { $numberOfPhysicalCores += $processor.NumberOfCores $numberOfLogicalCores += $processor.NumberOfLogicalProcessors $numberOfProcessors++ if($processor.CurrentClockSpeed -lt $processor.MaxClockSpeed) { Write-VerboseWriter("Processor is being throttled") $processorIsThrottled = $true $currentClockSpeed = $processor.CurrentClockSpeed } if($previousProcessor -ne $null) { if($processor.Name -ne $previousProcessor.Name -or $processor.MaxClockSpeed -ne $previousProcessor.MaxMegacyclesPerCore) { Write-VerboseWriter("Different Processors are detected!!! This is an issue.") $differentProcessorsDetected = $true } if($processor.NumberOfLogicalProcessors -ne $previousProcessor.NumberOfLogicalProcessors) { Write-VerboseWriter("Different Processor core count per processor socket detected. This is an issue.") $differentProcessorCoreCountDetected = $true } } $previousProcessor = $processor } Write-VerboseWriter("NumberOfPhysicalCores: {0} | NumberOfLogicalCores: {1} | NumberOfProcessors: {2} | ProcessorIsThrottled: {3} | CurrentClockSpeed: {4} | DifferentProcessorsDetected: {5} | DifferentProcessorCoreCountDetected: {6}" -f $numberOfPhysicalCores, $numberOfLogicalCores, $numberOfProcessors, $processorIsThrottled, $currentClockSpeed, $differentProcessorsDetected, $differentProcessorCoreCountDetected) $presentedProcessorCoreCount = Invoke-ScriptBlockHandler -ComputerName $MachineName -ScriptBlock {[System.Environment]::ProcessorCount} -ScriptBlockDescription "Trying to get the System.Environment ProcessorCount" -CatchActionFunction $CatchActionFunction if($presentedProcessorCoreCount -eq $null) { Write-VerboseWriter("Wasn't able to get Presented Processor Core Count on the Server. Setting to -1.") $presentedProcessorCoreCount = -1 } else { Write-VerboseWriter("Presented Processor Core Count: {0}" -f $presentedProcessorCoreCount) } $processorInformationObject = New-Object PSCustomObject $processorInformationObject | Add-Member -MemberType NoteProperty -Name "Name" -Value $processorName $processorInformationObject | Add-Member -MemberType NoteProperty -Name "NumberOfPhysicalCores" -Value $numberOfPhysicalCores $processorInformationObject | Add-Member -MemberType NoteProperty -Name "NumberOfLogicalCores" -Value $numberOfLogicalCores $processorInformationObject | Add-Member -MemberType NoteProperty -Name "NumberOfProcessors" -Value $numberOfProcessors $processorInformationObject | Add-Member -MemberType NoteProperty -Name "MaxMegacyclesPerCore" -Value $maxClockSpeed $processorInformationObject | Add-Member -MemberType NoteProperty -Name "CurrentMegacyclesPerCore" -Value $currentClockSpeed $processorInformationObject | Add-Member -MemberType NoteProperty -Name "ProcessorIsThrottled" -Value $processorIsThrottled $processorInformationObject | Add-Member -MemberType NoteProperty -Name "DifferentProcessorsDetected" -Value $differentProcessorsDetected $processorInformationObject | Add-Member -MemberType NoteProperty -Name "DifferentProcessorCoreCountDetected" -Value $differentProcessorCoreCountDetected $processorInformationObject | Add-Member -MemberType NoteProperty -Name "EnvironmentProcessorCount" -Value $presentedProcessorCoreCount $processorInformationObject | Add-Member -MemberType NoteProperty -Name "ProcessorClassObject" -Value $wmiObject Write-VerboseWriter("Exiting: Get-ProcessorInformation") return $processorInformationObject } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-ServerOperatingSystemVersion/Get-ServerOperatingSystemVersion.ps1 Function Get-ServerOperatingSystemVersion { [CmdletBinding()] param( [string]$OsCaption ) #Function Version 1.5 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> if($OsCaption -eq [string]::Empty -or $OsCaption -eq $null) { Write-VerboseWriter("Getting the local machine version build number") $OsCaption = (Get-WmiObject -Class Win32_OperatingSystem).Caption Write-VerboseWriter("Got '{0}' for the caption" -f $OsCaption) } else { Write-VerboseWriter("Passed - [string]OsCaption : {0}" -f $OsCaption) } $osReturnValue = [string]::Empty 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"} } Write-VerboseWriter("Returned: {0}" -f $osReturnValue) return [string]$osReturnValue } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-ServerRebootPending/Get-ServerRebootPending.ps1 Function Get-ServerRebootPending { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ServerName, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-ScriptBlockHandler/Invoke-ScriptBlockHandler.ps1 #> Function Get-PendingFileReboot { try { if((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\" -Name PendingFileRenameOperations -ErrorAction Stop)) { return $true } else { return $false } } catch { throw } } Function Get-PendingSCCMReboot { try { $sccmReboot = Invoke-CimMethod -Namespace 'Root\ccm\clientSDK' -ClassName 'CCM_ClientUtilities' -Name 'DetermineIfRebootPending' -ErrorAction Stop if($sccmReboot -and ($sccmReboot.RebootPending -or $sccmReboot.IsHardRebootPending)) { return $true } } catch { throw } } Function Get-PathTestingReboot { param( [string]$TestingPath ) if(Test-Path $TestingPath) { return $true } else { return $false } } Write-VerboseWriter("Calling: Get-ServerRebootPending") if(Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PendingFileReboot} -ScriptBlockDescription "Get-PendingFileReboot" -CatchActionFunction $CatchActionFunction) { Write-VerboseWriter("Get-PendingFileReboot Determined Reboot is pending. Registry HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\ item properties of PendingFileRenameOperations.") return $true } if(Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PendingSCCMReboot} -ScriptBlockDescription "Get-PendingSCCMReboot" -CatchActionFunction $CatchActionFunction) { Write-VerboseWriter("Get-PendingSCCMReboot determined reboot is pending.") return $true } if(Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PathTestingReboot} -ScriptBlockDescription "Get-PendingAutoUpdateReboot" -CatchActionFunction $CatchActionFunction -ArgumentList "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") { Write-VerboseWriter("Get-PathTestingReboot for HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending determined reboot is pending") return $true } if(Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock ${Function:Get-PathTestingReboot} -ScriptBlockDescription "Get-PendingAutoUpdateReboot" -CatchActionFunction $CatchActionFunction -ArgumentList "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") { Write-VerboseWriter("Get-PathTestingReboot for HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired determined reboot is pending") return $true } Write-VerboseWriter("Passed all reboot checks.") return $false } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-ServerType/Get-ServerType.ps1 Function Get-ServerType { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ServerType ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> Write-VerboseWriter("Calling: Get-ServerType") $returnServerType = [string]::Empty if($ServerType -like "VMware*") { $returnServerType = "VMware"} elseif($ServerType -like "*Microsoft Corporation*") { $returnServerType = "HyperV" } elseif($ServerType.Length -gt 0) {$returnServerType = "Physical"} else { $returnServerType = "Unknown" } Write-VerboseWriter("Returning: {0}" -f $returnServerType) return $returnServerType } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-Smb1ServerSettings/Get-Smb1ServerSettings.ps1 Function Get-Smb1ServerSettings { [CmdletBinding()] param( [string]$ServerName = $env:COMPUTERNAME, [scriptblock]$CatchActionFunction ) #Function Version 1.2 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-ScriptBlockHandler/Invoke-ScriptBlockHandler.ps1 #> Write-VerboseWriter("Calling: Get-Smb1ServerSettings") Write-VerboseWriter("Passed ServerName: {0}" -f $ServerName) $smbServerConfiguration = Invoke-ScriptBlockHandler -ComputerName $ServerName -ScriptBlock {Get-SmbServerConfiguration} -CatchActionFunction $CatchActionFunction -ScriptBlockDescription "Get-SmbServerConfiguration" <# Unknown 0 Failed to get Install Setting 1 Install is set to true 2 Install is set to false 4 Failed to get Block Setting 8 SMB1 is not being blocked 16 SMB1 is being blocked 32 #> $smb1Status = 0 try { $windowsFeature = Get-WindowsFeature "FS-SMB1" -ComputerName $ServerName -ErrorAction Stop } catch { Write-VerboseWriter("Failed to Get-WindowsFeature for FS-SMB1") if ($CatchActionFunction -ne $null) { & $CatchActionFunction } } if ($windowsFeature -eq $null) { $smb1Status += 1 } elseif ($windowsFeature.Installed) { $smb1Status += 2 } else { $smb1Status += 4 } if ($smbServerConfiguration -eq $null) { $smb1Status += 8 } elseif ($smbServerConfiguration.EnableSMB1Protocol) { $smb1Status += 16 } else { $smb1Status += 32 } $smb1ServerSettings = New-Object PSCustomObject $smb1ServerSettings | Add-Member -MemberType NoteProperty -Name "SmbServerConfiguration" -Value $smbServerConfiguration $smb1ServerSettings | Add-Member -MemberType NoteProperty -Name "WindowsFeature" -Value $windowsFeature $smb1ServerSettings | Add-Member -MemberType NoteProperty -Name "Smb1Status" -Value $smb1Status return $smb1ServerSettings } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-TimeZoneInformationRegistrySettings/Get-TimeZoneInformationRegistrySettings.ps1 Function Get-TimeZoneInformationRegistrySettings { [CmdletBinding()] param( [string]$MachineName = $env:COMPUTERNAME, [scriptblock]$CatchActionFunction ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 #> Write-VerboseWriter("Calling: Get-TimeZoneInformationRegistrySettings") Write-VerboseWriter("Passed: [string]MachineName: {0}" -f $MachineName) $timeZoneInformationSubKey = "SYSTEM\CurrentControlSet\Control\TimeZoneInformation" $dynamicDaylightTimeDisabled = Invoke-RegistryGetValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "DynamicDaylightTimeDisabled" -CatchActionFunction $CatchActionFunction $timeZoneKeyName = Invoke-RegistryGetValue -MachineName $MachineName -Subkey $timeZoneInformationSubKey -GetValue "TimeZoneKeyName" -CatchActionFunction $CatchActionFunction $standardStart = Invoke-RegistryGetValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "StandardStart" -CatchActionFunction $CatchActionFunction $daylightStart = Invoke-RegistryGetValue -MachineName $MachineName -SubKey $timeZoneInformationSubKey -GetValue "DaylightStart" -CatchActionFunction $CatchActionFunction $timeZoneInformationObject = New-Object PSCustomObject $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "DynamicDaylightTimeDisabled" -Value $dynamicDaylightTimeDisabled $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "TimeZoneKeyName" -Value $timeZoneKeyName $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "StandardStart" -Value $standardStart $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "DaylightStart" -Value $daylightStart $actionsToTake = @() if($timeZoneKeyName -eq $null -or [string]::IsNullOrEmpty($timeZoneKeyName)) { Write-VerboseWriter("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." } foreach($value in $standardStart) { if($value -ne 0) { $standardStartNonZeroValue = $true break } } foreach($value in $daylightStart) { if($value -ne 0) { $daylightStartNonZeroValue = $true break } } if($dynamicDaylightTimeDisabled -ne 0 -and ( $standardStartNonZeroValue -or $daylightStartNonZeroValue )) { Write-VerboseWriter("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-VerboseWriter("Daylight savings auto adjustment is disabled.") $actionsToTake += "Warning: DynamicDaylightTimeDisabled is set, Windows can not properly detect any DST rule changes in your time zone." } $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "DstIssueDetected" -Value $dstIssueDetected $timeZoneInformationObject | Add-Member -MemberType NoteProperty -Name "ActionsToTake" -Value $actionsToTake return $timeZoneInformationObject } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Get-WmiObjectHandler/Get-WmiObjectHandler.ps1 Function Get-WmiObjectHandler { [CmdletBinding()] param( [Parameter(Mandatory=$false)][string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$true)][string]$Class, [Parameter(Mandatory=$false)][string]$Filter, [Parameter(Mandatory=$false)][string]$Namespace, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.0 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> Write-VerboseWriter("Calling: Get-WmiObjectHandler") Write-VerboseWriter("Passed: [string]ComputerName: {0} | [string]Class: {1} | [string]Filter: {2} | [string]Namespace: {3}" -f $ComputerName, $Class, $Filter, $Namespace) $execute = @{ ComputerName = $ComputerName Class = $Class } if(![string]::IsNullOrEmpty($Filter)) { $execute.Add("Filter", $Filter) } if(![string]::IsNullOrEmpty($Namespace)) { $execute.Add("Namespace", $Namespace) } try { $wmi = Get-WmiObject @execute -ErrorAction Stop return $wmi } catch { Write-VerboseWriter("Failed to run Get-WmiObject object on class '{0}'" -f $Class) if($CatchActionFunction -ne $null) { & $CatchActionFunction } } } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-RegistryGetValue/Invoke-RegistryGetValue.ps1 Function Invoke-RegistryGetValue { [CmdletBinding()] param( [Parameter(Mandatory=$false)][string]$RegistryHive = "LocalMachine", [Parameter(Mandatory=$true)][string]$MachineName, [Parameter(Mandatory=$true)][string]$SubKey, [Parameter(Mandatory=$false)][string]$GetValue, [Parameter(Mandatory=$false)][bool]$ReturnAfterOpenSubKey, [Parameter(Mandatory=$false)][object]$DefaultValue, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.2 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> Write-VerboseWriter("Calling: Invoke-RegistryGetValue") try { Write-VerboseWriter("Attempting to open the Base Key '{0}' on Server '{1}'" -f $RegistryHive, $MachineName) $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($RegistryHive, $MachineName) Write-VerboseWriter("Attempting to open the Sub Key '{0}'" -f $SubKey) $RegKey= $Reg.OpenSubKey($SubKey) if ($ReturnAfterOpenSubKey) { Write-VerboseWriter("Returning OpenSubKey") return $RegKey } Write-VerboseWriter("Attempting to get the value '{0}'" -f $GetValue) $returnGetValue = $RegKey.GetValue($GetValue) if ($null -eq $returnGetValue -and $null -ne $DefaultValue) { Write-VerboseWriter("No value found in the registry. Setting to default value: {0}" -f $DefaultValue) $returnGetValue = $DefaultValue } Write-VerboseWriter("Exiting: Invoke-RegistryHandler | Returning: {0}" -f $returnGetValue) return $returnGetValue } catch { if ($CatchActionFunction -ne $null) { & $CatchActionFunction } Write-VerboseWriter("Failed to open the registry") } } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Invoke-ScriptBlockHandler/Invoke-ScriptBlockHandler.ps1 Function Invoke-ScriptBlockHandler { [CmdletBinding()] param( [Parameter(Mandatory=$true)][string]$ComputerName, [Parameter(Mandatory=$true)][scriptblock]$ScriptBlock, [Parameter(Mandatory=$false)][string]$ScriptBlockDescription, [Parameter(Mandatory=$false)][object]$ArgumentList, [Parameter(Mandatory=$false)][bool]$IncludeNoProxyServerOption = $true, #Default in HealthChecker [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.1 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> Write-VerboseWriter("Calling: Invoke-ScriptBlockHandler") if(![string]::IsNullOrEmpty($ScriptBlockDescription)) { Write-VerboseWriter($ScriptBlockDescription) } try { if($ComputerName -ne $env:COMPUTERNAME) { $params = @{ ComputerName = $ComputerName ScriptBlock = $ScriptBlock ErrorAction = "Stop" } if ($IncludeNoProxyServerOption) { Write-VerboseWriter("Including SessionOption") $params.Add("SessionOption", (New-PSSessionOption -ProxyAccessType NoProxyServer)) } if($ArgumentList -ne $null) { $params.Add("ArgumentList", $ArgumentList) Write-VerboseWriter("Running Invoke-Command with argument list.") } else { Write-VerboseWriter("Running Invoke-Command without argument list.") } $invokeReturn = Invoke-Command @params return $invokeReturn } else { if($ArgumentList -ne $null) { Write-VerboseWriter("Running Script Block locally with argument list.") $localReturn = & $ScriptBlock $ArgumentList } else { Write-VerboseWriter("Running Script Block locally without argument list.") $localReturn = & $ScriptBlock } return $localReturn } } catch { Write-VerboseWriter("Failed to Invoke-ScriptBlockHandler") if($CatchActionFunction -ne $null) { & $CatchActionFunction } } } Function Get-ExchangeAppPoolsInformation { Write-VerboseOutput("Calling: Get-ExchangeAppPoolsInformation") Function Get-ExchangeAppPoolsScriptBlock { $windir = $env:windir $Script:appCmd = "{0}\system32\inetsrv\appcmd.exe" -f $windir $appPools = &$Script:appCmd list apppool $exchangeAppPools = @() foreach($appPool in $appPools) { $startIndex = $appPool.IndexOf('"') + 1 $appPoolName = $appPool.Substring($startIndex, ($appPool.Substring($startIndex).IndexOf('"'))) if($appPoolName.StartsWith("MSExchange")) { $exchangeAppPools += $appPoolName } } $exchAppPools = @{} foreach($appPool in $exchangeAppPools) { $status = &$Script:appCmd list apppool $appPool /text:state $config = &$Script:appCmd list apppool $appPool /text:CLRConfigFile if(!([System.String]::IsNullOrEmpty($config)) -and (Test-Path $config)) { $content = Get-Content $config } else { $content = $null } $statusObj = New-Object pscustomobject $statusObj | Add-Member -MemberType NoteProperty -Name "Status" -Value $status $statusObj | Add-Member -MemberType NoteProperty -Name "ConfigPath" -Value $config $statusObj | Add-Member -MemberType NoteProperty -Name "Content" -Value $content $exchAppPools.Add($appPool, $statusObj) } return $exchAppPools } $exchangeAppPoolsInfo = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock ${Function:Get-ExchangeAppPoolsScriptBlock} -ScriptBlockDescription "Getting Exchange App Pool information" -CatchActionFunction ${Function:Invoke-CatchActions} Write-VerboseOutput("Exiting: Get-ExchangeAppPoolsInformation") return $exchangeAppPoolsInfo } Function Get-ExchangeInformation { param( [HealthChecker.OSServerVersion]$OSMajorVersion ) Write-VerboseOutput("Calling: Get-ExchangeInformation") Write-VerboseOutput("Passed: OSMajorVersion: {0}" -f $OSMajorVersion) [HealthChecker.ExchangeInformation]$exchangeInformation = New-Object -TypeName HealthChecker.ExchangeInformation $exchangeInformation.GetExchangeServer = (Get-ExchangeServer -Identity $Script:Server) $exchangeInformation.ExchangeCertificates = Get-ExchangeServerCertificates $buildInformation = $exchangeInformation.BuildInformation $buildInformation.MajorVersion = ([HealthChecker.ExchangeMajorVersion](Get-ExchangeMajorVersion -AdminDisplayVersion $exchangeInformation.GetExchangeServer.AdminDisplayVersion)) $buildInformation.ServerRole = (Get-ServerRole -ExchangeServerObj $exchangeInformation.GetExchangeServer) $buildInformation.ExchangeSetup = Get-ExSetupDetails #Exchange 2013 or greater if($buildInformation.MajorVersion -ge [HealthChecker.ExchangeMajorVersion]::Exchange2013) { $netFrameworkExchange = $exchangeInformation.NETFramework $adminDisplayVersion = $exchangeInformation.GetExchangeServer.AdminDisplayVersion $revisionNumber = if($adminDisplayVersion.Revision -lt 10) {$adminDisplayVersion.Revision / 10} else {$adminDisplayVersion.Revision / 100 } $buildAndRevision = $adminDisplayVersion.Build + $revisionNumber Write-VerboseOutput("The build and revision number: {0}" -f $buildAndRevision) #Build Numbers: https://docs.microsoft.com/en-us/Exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019 $buildInformation.BuildNumber = "{0}.{1}.{2}.{3}" -f $adminDisplayVersion.Major, $adminDisplayVersion.Minor, $adminDisplayVersion.Build, $adminDisplayVersion.Revision if($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) { Write-VerboseOutput("Exchange 2019 is detected. Checking build number...") $buildInformation.FriendlyName = "Exchange 2019 " #Exchange 2019 Information if($buildAndRevision -lt 221.12) { $buildInformation.CU = [HealthChecker.ExchangeCULevel]::Preview; $buildInformation.FriendlyName += "Preview"; $buildInformation.ReleaseDate = "07/24/2018" } elseif($buildAndRevision -lt 330.6) {$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"; $buildInformation.SupportedBuild = $true} elseif($buildAndRevision -ge 792.3) {$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU8; $buildInformation.FriendlyName += "CU8"; $buildInformation.ReleaseDate = "12/15/2020"; $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-VerboseOutput("Exchange 2016 is detected. Checking build number...") $buildInformation.FriendlyName = "Exchange 2016 " #Exchange 2016 Information if($buildAndRevision -lt 225.42) {$buildInformation.CU = [HealthChecker.ExchangeCULevel]::Preview; $buildInformation.FriendlyName += "Preview"; $buildInformation.ReleaseDate = "07/22/2015"} elseif($buildAndRevision -lt 396.30) {$buildInformation.CU = [HealthChecker.ExchangeCULevel]::RTM; $buildInformation.FriendlyName += "RTM"; $buildInformation.ReleaseDate = "10/01/2015"} elseif($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"; $buildInformation.SupportedBuild = $true} elseif($buildAndRevision -ge 2176.2) {$buildInformation.CU = [HealthChecker.ExchangeCULevel]::CU19; $buildInformation.FriendlyName += "CU19"; $buildInformation.ReleaseDate = "12/15/2020"; $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]::CU11) {$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d2; $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-VerboseOutput("Exchange 2013 is detected. Checking build number...") $buildInformation.FriendlyName = "Exchange 2013 " #Exchange 2013 Information if($buildAndRevision -lt 620.29) {$buildInformation.CU = [HealthChecker.ExchangeCULevel]::RTM; $buildInformation.FriendlyName += "RTM"; $buildInformation.ReleaseDate = "12/03/2012"} elseif($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]::CU12){$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix; $netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix} elseif($buildInformation.CU -lt [HealthChecker.ExchangeCULevel]::CU15) {$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix; $netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d6d1wFix} elseif($buildInformation.CU -eq [HealthChecker.ExchangeCULevel]::CU15) {$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d5d2wFix; $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 -le [HealthChecker.ExchangeCULevel]::CU22) {$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d1; $netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2} else {$netFrameworkExchange.MinSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d7d2; $netFrameworkExchange.MaxSupportedVersion = [HealthChecker.NetMajorVersion]::Net4d8} } $exchangeInformation.MapiHttpEnabled = (Get-OrganizationConfig).MapiHttpEnabled if ($buildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) { $exchangeInformation.ApplicationPools = Get-ExchangeAppPoolsInformation } $buildInformation.KBsInstalled = Get-ExchangeUpdates -ExchangeMajorVersion $buildInformation.MajorVersion $exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage = Invoke-RegistryGetValue -MachineName $Script:Server -SubKey "SOFTWARE\Microsoft\ExchangeServer\v15\Search\SystemParameters" -GetValue "CtsProcessorAffinityPercentage" -CatchActionFunction ${Function:Invoke-CatchActions} $exchangeInformation.ServerMaintenance = Get-ExchangeServerMaintenanceState -ComponentsToSkip "ForwardSyncDaemon","ProvisioningRps" if($buildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::ClientAccess) { $exchangeInformation.ExchangeServicesNotRunning = Test-ServiceHealth -Server $Script:Server | %{$_.ServicesNotRunning} } } elseif($buildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2010) { Write-VerboseOutput("Exchange 2010 detected.") $buildInformation.FriendlyName = "Exchange 2010" $buildInformation.BuildNumber = $exchangeInformation.GetExchangeServer.AdminDisplayVersion.ToString() } Write-VerboseOutput("Exiting: Get-ExchangeInformation") return $exchangeInformation } Function Get-ExchangeServerCertificates { Write-VerboseOutput("Calling: Get-ExchangeServerCertificates") try { Write-VerboseOutput("Trying to receive certificates from Exchange server: {0}" -f $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 = ([DateTime]($cert.NotAfter) - (Get-Date)).Days $sanCertificateInfo = $false if ($null -ne $cert.DnsNameList -and ($cert.DnsNameList).Count -gt 1) { $sanCertificateInfo = $true } if($authConfigDetected) { $isAuthConfigInfo = $false if ($cert.Thumbprint -eq $authConfig.CurrentCertificateThumbprint) { $isAuthConfigInfo = $true } } else { $isAuthConfigInfo = "InvalidAuthConfig" } $certInformationObj = New-Object PSCustomObject $certInformationObj | Add-Member -MemberType NoteProperty -Name "FriendlyName" -Value $cert.FriendlyName $certInformationObj | Add-Member -MemberType NoteProperty -Name "Thumbprint" -Value $cert.Thumbprint $certInformationObj | Add-Member -MemberType NoteProperty -Name "PublicKeySize" -value $cert.PublicKeySize $certInformationObj | Add-Member -MemberType NoteProperty -Name "IsSanCertificate" -Value $sanCertificateInfo $certInformationObj | Add-Member -MemberType NoteProperty -Name "Namespaces" -Value $cert.DnsNameList $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 $cert.Status $certInformationObj | Add-Member -MemberType NoteProperty -Name "CertificateObject" -Value $cert $certObject += $certInformationObj } catch { Write-VerboseOutput("Unable to process certificate: {0}" -f $cert.Thumbprint) Invoke-CatchActions } } Write-VerboseOutput("Processed: {0} certificates" -f $certObject.Count) return $certObject } else { Write-VerboseOutput("Failed to find any Exchange certificates") return $null } } catch { Write-VerboseWriter("Failed to run Get-ExchangeCertificate. Error: {0}." -f $Error[0].Exception) Invoke-CatchActions } } Function Get-ExchangeServerMaintenanceState { param( [Parameter(Mandatory=$false)][array]$ComponentsToSkip ) Write-VerboseOutput("Calling Function: Get-ExchangeServerMaintenanceState") [HealthChecker.ExchangeServerMaintenance]$serverMaintenance = New-Object -TypeName HealthChecker.ExchangeServerMaintenance $serverMaintenance.GetServerComponentState = Get-ServerComponentState -Identity $Script:Server -ErrorAction SilentlyContinue try { $serverMaintenance.GetMailboxServer = Get-MailboxServer -Identity $Script:Server -ErrorAction SilentlyContinue } catch { Write-VerboseOutput("Failed to run Get-MailboxServer") Invoke-CatchActions } try { $serverMaintenance.GetClusterNode = Get-ClusterNode -Name $Script:Server -ErrorAction Stop } catch { Write-VerboseOutput("Failed to run Get-ClusterNode") Invoke-CatchActions } Write-VerboseOutput("Running ServerComponentStates checks") foreach ($component in $serverMaintenance.GetServerComponentState) { if (($ComponentsToSkip -ne $null -and $ComponentsToSkip.Count -ne 0) -and $ComponentsToSkip -notcontains $component.Component) { if ($component.State -ne "Active") { $latestLocalState = $null $latestRemoteState = $null if ($component.LocalStates -ne $null) { $latestLocalState = ($component.LocalStates | Sort-Object {$_.TimeStamp} -ErrorAction SilentlyContinue)[-1] } if ($component.RemoteStates -ne $null) { $latestRemoteState = ($component.RemoteStates | Sort-Object {$_.TimeStamp} -ErrorAction SilentlyContinue)[-1] } Write-VerboseOutput("Component: {0} LocalState: '{1}' RemoteState: '{2}'" -f $component.Component, $latestLocalState.State, $latestRemoteState.State) if ($latestLocalState.State -eq $latestRemoteState.State) { $serverMaintenance.InactiveComponents += "'{0}' is in Maintenance Mode" -f $component.Component } else { if (($latestLocalState -ne $null) -and ($latestLocalState.State -ne "Active")) { $serverMaintenance.InactiveComponents += "'{0}' is in Local Maintenance Mode only" -f $component.Component } if (($latestRemoteState -ne $null) -and ($latestRemoteState.State -ne "Active")) { $serverMaintenance.InactiveComponents += "'{0}' is in Remote Maintenance Mode only" -f $component.Component } } } else { Write-VerboseOutput("Component '{0}' is Active" -f $component.Component) } } else { Write-VerboseOutput("Component: {0} will be skipped" -f $component.Component) } } return $serverMaintenance } Function Get-ExchangeUpdates { param( [Parameter(Mandatory=$true)][HealthChecker.ExchangeMajorVersion]$ExchangeMajorVersion ) Write-VerboseOutput("Calling: Get-ExchangeUpdates") Write-VerboseOutput("Passed: {0}" -f $ExchangeMajorVersion.ToString()) $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 = Invoke-RegistryGetValue -MachineName $Script:Server -SubKey $RegLocation -ReturnAfterOpenSubKey $true -CatchActionFunction ${Function:Invoke-CatchActions} if($null -ne $RegKey) { $IU = $RegKey.GetSubKeyNames() if($null -ne $IU) { Write-VerboseOutput("Detected fixes installed on the server") $fixes = @() foreach($key in $IU) { $IUKey = $RegKey.OpenSubKey($key) $IUName = $IUKey.GetValue("PackageName") Write-VerboseOutput("Found: " + $IUName) $fixes += $IUName } return $fixes } else { Write-VerboseOutput("No IUs found in the registry") } } else { Write-VerboseOutput("No RegKey returned") } Write-VerboseOutput("Exiting: Get-ExchangeUpdates") return $null } Function Get-ExSetupDetails { Write-VerboseOutput("Calling: Get-ExSetupDetails") $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-VerboseOutput("Exiting: Get-ExSetupDetails") return $exSetupDetails } Function Get-HealthCheckerExchangeServer { Write-VerboseOutput("Calling: Get-HealthCheckerExchangeServer") [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 = $healthCheckerVersion Write-VerboseOutput("Finished building health Exchange Server Object for server: " + $Script:Server) return $HealthExSvrObj } Function Get-CredentialGuardEnabled { Write-VerboseOutput("Calling: Get-CredentialGuardEnabled") $registryValue = Invoke-RegistryGetValue -MachineName $Script:Server -SubKey "SYSTEM\CurrentControlSet\Control\LSA" -GetValue "LsaCfgFlags" -CatchActionFunction ${Function:Invoke-CatchActions} if ($registryValue -ne $null -and $registryValue -ne 0) { return $true } return $false } Function Get-HardwareInformation { Write-VerboseOutput("Calling: Get-HardwareInformation") [HealthChecker.HardwareInformation]$hardware_obj = New-Object HealthChecker.HardwareInformation $system = Get-WmiObjectHandler -ComputerName $Script:Server -Class "Win32_ComputerSystem" -CatchActionFunction ${Function:Invoke-CatchActions} $hardware_obj.Manufacturer = $system.Manufacturer $hardware_obj.System = $system $hardware_obj.AutoPageFile = $system.AutomaticManagedPagefile $hardware_obj.TotalMemory = $system.TotalPhysicalMemory $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-VerboseOutput("Exiting: Get-HardwareInformation") return $hardware_obj } Function Get-HttpProxySetting { $httpProxy32 = [String]::Empty $httpProxy64 = [String]::Empty Write-VerboseOutput("Calling: Get-HttpProxySetting") Function Get-WinHttpSettings { param( [Parameter(Mandatory=$true)][string]$RegistryLocation ) $connections = Get-ItemProperty -Path $RegistryLocation $Proxy = [string]::Empty if(($connections -ne $null) -and ($Connections | gm).Name -contains "WinHttpSettings") { foreach($Byte in $Connections.WinHttpSettings) { if($Byte -ge 48) { $Proxy += [CHAR]$Byte } } } return $(if($Proxy -eq [string]::Empty){""} else {$Proxy}) } $httpProxy32 = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock ${Function:Get-WinHttpSettings} -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:Get-WinHttpSettings} -ArgumentList "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" -ScriptBlockDescription "Getting 64 Http Proxy Value" -CatchActionFunction ${Function:Invoke-CatchActions} Write-VerboseOutput("Http Proxy 32: {0}" -f $httpProxy32) Write-VerboseOutput("Http Proxy 64: {0}" -f $httpProxy64) Write-VerboseOutput("Exiting: Get-HttpProxySetting") if($httpProxy32 -ne "") { return $httpProxy32 } else { return $httpProxy64 } } Function Get-LmCompatibilityLevelInformation { Write-VerboseOutput("Calling: Get-LmCompatibilityLevelInformation") [HealthChecker.LmCompatibilityLevelInformation]$ServerLmCompatObject = New-Object -TypeName HealthChecker.LmCompatibilityLevelInformation $ServerLmCompatObject.RegistryValue = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $Script:Server -SubKey "SYSTEM\CurrentControlSet\Control\Lsa" -GetValue "LmCompatibilityLevel" -CatchActionFunction ${Function:Invoke-CatchActions} -DefaultValue 3 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-VerboseOutput("Exiting: Get-LmCompatibilityLevelInformation") Return $ServerLmCompatObject } Function Get-OperatingSystemInformation { Write-VerboseOutput("Calling: Get-OperatingSystemInformation") [HealthChecker.OperatingSystemInformation]$osInformation = New-Object HealthChecker.OperatingSystemInformation $win32_OperatingSystem = Get-WmiObjectHandler -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($win32_PowerPlan -ne $null) { if($win32_PowerPlan.InstanceID -eq "Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c}") { Write-VerboseOutput("High Performance Power Plan is set to true") $osInformation.PowerPlan.HighPerformanceSet = $true } else{Write-VerboseOutput("High Performance Power Plan is NOT set to true")} $osInformation.PowerPlan.PowerPlanSetting = $win32_PowerPlan.ElementName } else { Write-VerboseOutput("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 = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $Script:Server -SubKey "SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" -GetValue "DisabledComponents" -CatchActionFunction ${Function:Invoke-CatchActions} $osInformation.NetworkInformation.TCPKeepAlive = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -MachineName $Script:Server -SubKey "SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -GetValue "KeepAliveTime" -CatchActionFunction ${Function:Invoke-CatchActions} $osInformation.NetworkInformation.RpcMinConnectionTimeout = Invoke-RegistryGetValue -RegistryHive "LocalMachine" -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($counterSamples -ne $null) { $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-VisualCRedistributableVersion $osInformation.CredentialGuardEnabled = Get-CredentialGuardEnabled $osInformation.RegistryValues.CurrentVersionUbr = Invoke-RegistryGetValue ` -MachineName $Script:Server ` -SubKey "SOFTWARE\Microsoft\Windows NT\CurrentVersion" ` -GetValue "UBR" ` -CatchActionFunction ${Function:Invoke-CatchActions} $osInformation.RegistryValues.LanManServerDisabledCompression = Invoke-RegistryGetValue ` -MachineName $Script:Server ` -SubKey "SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" ` -GetValue "DisableCompression" ` -CatchActionFunction ${Function:Invoke-CatchActions} $getSmb1ServerSettings = Get-Smb1ServerSettings -ServerName $Script:Server -CatchActionFunction ${Function:Invoke-CatchActions} $osInformation.Smb1ServerSettings.SmbServerConfiguration = $getSmb1ServerSettings.SmbServerConfiguration $osInformation.Smb1ServerSettings.WindowsFeature = $getSmb1ServerSettings.WindowsFeature $osInformation.Smb1ServerSettings.Smb1Status = $getSmb1ServerSettings.Smb1Status Write-VerboseOutput("Exiting: Get-OperatingSystemInformation") return $osInformation } Function Get-PageFileInformation { Write-VerboseOutput("Calling: Get-PageFileInformation") [HealthChecker.PageFileInformation]$page_obj = New-Object HealthChecker.PageFileInformation $pagefile = Get-WmiObjectHandler -ComputerName $Script:Server -Class "Win32_PageFileSetting" -CatchActionFunction ${Function:Invoke-CatchActions} if($pagefile -ne $null) { if($pagefile.GetType().Name -eq "ManagementObject") { $page_obj.MaxPageSize = $pagefile.MaximumSize } $page_obj.PageFile = $pagefile } else { Write-VerboseOutput("Return Null value") } Write-VerboseOutput("Exiting: Get-PageFileInformation") return $page_obj } Function Get-ServerRole { param( [Parameter(Mandatory=$true)][object]$ExchangeServerObj ) Write-VerboseOutput("Calling: Get-ServerRole") $roles = $ExchangeServerObj.ServerRole.ToString() Write-VerboseOutput("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-VisualCRedistributableVersion { Write-VerboseOutput("Calling: Get-VisualCRedistributableVersion") $installedSoftware = Invoke-ScriptBlockHandler -ComputerName $Script:Server -ScriptBlock {Get-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*} -ScriptBlockDescription "Quering for software" -CatchActionFunction ${Function:Invoke-CatchActions} $softwareInfos = @() foreach ($software in $installedSoftware) { if($software.DisplayName -like "Microsoft Visual C++ *") { Write-VerboseOutput("Microsoft Visual C++ Redistributable found: {0}" -f $software.DisplayName) [HealthChecker.SoftwareInformation]$softwareInfo = New-Object Healthchecker.SoftwareInformation $softwareInfo.DisplayName = $software.DisplayName $softwareInfo.DisplayVersion = $software.DisplayVersion $softwareInfo.InstallDate = $software.InstallDate $softwareInfo.VersionIdentifier = $software.Version $softwareInfos += $softwareInfo } } Write-VerboseOutput("Exiting: Get-VisualCRedistributableVersion") return $softwareInfos } Function Add-AnalyzedResultInformation { param( [object]$Details, [string]$Name, [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, [HealthChecker.AnalyzedInformation]$AnalyzedInformation ) Write-VerboseOutput("Calling Add-AnalyzedResultInformation: {0}" -f $name) if ($AddDisplayResultsLineInfo) { if (!($AnalyzedInformation.DisplayResults.ContainsKey($DisplayGroupingKey))) { Write-VerboseOutput("Adding Display Grouping Key: {0}" -f $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 $lineInfo.DisplayValue = $Details $lineInfo.Name = $Name if ($DisplayCustomTabNumber -ne -1) { $lineInfo.TabNumber = $DisplayCustomTabNumber } else { $lineInfo.TabNumber = $DisplayGroupingKey.DefaultTabNumber } if ($DisplayTestingValue -ne $null) { $lineInfo.TestingValue = $DisplayTestingValue } else { $lineInfo.TestingValue = $Details } $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 } return $AnalyzedInformation } Function New-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 Start-AnalyzerEngine { param( [HealthChecker.HealthCheckerExchangeServer]$HealthServerObject ) Write-VerboseOutput("Calling: Start-AnalyzerEngine") $analyzedResults = New-Object HealthChecker.AnalyzedInformation $analyzedResults.HealthCheckerExchangeServer = $HealthServerObject #Display Grouping Keys $order = 0 $keyBeginningInfo = New-DisplayResultsGroupingKey -Name "BeginningInfo" -DisplayGroupName $false -DisplayOrder ($order++) -DefaultTabNumber 0 $keyExchangeInformation = New-DisplayResultsGroupingKey -Name "Exchange Information" -DisplayOrder ($order++) $keyOSInformation = New-DisplayResultsGroupingKey -Name "Operating System Information" -DisplayOrder ($order++) $keyHardwareInformation = New-DisplayResultsGroupingKey -Name "Processor/Hardware Information" -DisplayOrder ($order++) $keyNICSettings = New-DisplayResultsGroupingKey -Name "NIC Settings Per Active Adapter" -DisplayOrder ($order++) -DefaultTabNumber 2 $keyFrequentConfigIssues = New-DisplayResultsGroupingKey -Name "Frequent Configuration Issues" -DisplayOrder ($order++) $keySecuritySettings = New-DisplayResultsGroupingKey -Name "Security Settings" -DisplayOrder ($order++) $keyWebApps = New-DisplayResultsGroupingKey -Name "Exchange Web App Pools" -DisplayOrder ($order++) #Set short cut variables $exchangeInformation = $HealthServerObject.ExchangeInformation $osInformation = $HealthServerObject.OSInformation $hardwareInformation = $HealthServerObject.HardwareInformation if (!$Script:DisplayedScriptVersionAlready) { $analyzedResults = Add-AnalyzedResultInformation -Name "Exchange Health Checker Version" -Details $Script:healthCheckerVersion ` -DisplayGroupingKey $keyBeginningInfo ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } 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 ` -AnalyzedInformation $analyzedResults } ######################### # Exchange Information ######################### Write-VerboseOutput("Working on Exchange Information") $analyzedResults = Add-AnalyzedResultInformation -Name "Name" -Details ($HealthServerObject.ServerName) ` -DisplayGroupingKey $keyExchangeInformation ` -AddHtmlOverviewValues $true ` -HtmlName "Server Name" ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Version" -Details ($exchangeInformation.BuildInformation.FriendlyName) ` -DisplayGroupingKey $keyExchangeInformation ` -AddHtmlOverviewValues $true ` -HtmlName "Exchange Version" ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Build Number" -Details ($exchangeInformation.BuildInformation.BuildNumber) ` -DisplayGroupingKey $keyExchangeInformation ` -AnalyzedInformation $analyzedResults if ($exchangeInformation.BuildInformation.SupportedBuild -eq $false) { $daysOld = ($date - ([System.Convert]::ToDateTime([DateTime]$exchangeInformation.BuildInformation.ReleaseDate))).Days $analyzedResults = 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 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } if ($exchangeInformation.BuildInformation.KBsInstalled -ne $null) { $analyzedResults = Add-AnalyzedResultInformation -Details ("Exchange IU or Security Hotfix Detected.") ` -DisplayGroupingKey $keyExchangeInformation ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults foreach ($kb in $exchangeInformation.BuildInformation.KBsInstalled) { $analyzedResults = Add-AnalyzedResultInformation -Details $kb ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } $analyzedResults = Add-AnalyzedResultInformation -Name "Server Role" -Details ($exchangeInformation.BuildInformation.ServerRole) ` -DisplayGroupingKey $keyExchangeInformation ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "MAPI/HTTP Enabled" -Details ($exchangeInformation.MapiHttpEnabled) ` -DisplayGroupingKey $keyExchangeInformation ` -AnalyzedInformation $analyzedResults if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013 -and $exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) { $content = [xml]$exchangeInformation.ApplicationPools["MSExchangeMapiFrontEndAppPool"].Content [bool]$enabled = $content.Configuration.Runtime.gcServer.Enabled -eq "true" [bool]$unknown = $content.Configuration.Runtime.gcServer.Enabled -ne "true" -and $content.Configuration.Runtime.gcServer.Enabled -ne "false" $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." } $analyzedResults = Add-AnalyzedResultInformation -Name "MAPI Front End App Pool GC Mode" -Details $displayValue ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType $displayWriteType ` -AnalyzedInformation $analyzedResults if ($warning -ne [string]::Empty) { $analyzedResults = Add-AnalyzedResultInformation -Details $warning ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } ############################## # Exchange Server Maintenance ############################## Write-VerboseOutput("Working on Exchange Server Maintenance") $serverMaintenance = $exchangeInformation.ServerMaintenance if (($serverMaintenance.InactiveComponents).Count -eq 0 -and ($serverMaintenance.GetClusterNode -eq $null -or $serverMaintenance.GetClusterNode.State -eq "Up") -and ($serverMaintenance.GetMailboxServer -eq $null -or ($serverMaintenance.GetMailboxServer.DatabaseCopyActivationDisabledAndMoveNow -eq $false -and $serverMaintenance.GetMailboxServer.DatabaseCopyAutoActivationPolicy -eq "Unrestricted"))) { $analyzedResults = Add-AnalyzedResultInformation -Name "Exchange Server Maintenance" -Details "Server is not in Maintenance Mode" ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayWriteType "Green" ` -AnalyzedInformation $analyzedResults } else { $analyzedResults = Add-AnalyzedResultInformation -Details "Exchange Server Maintenance" ` -DisplayGroupingKey $keyExchangeInformation ` -AnalyzedInformation $analyzedResults if (($serverMaintenance.InactiveComponents).Count -ne 0) { foreach ($inactiveComponent in $serverMaintenance.InactiveComponents) { $analyzedResults = Add-AnalyzedResultInformation -Name "Component" -Details $inactiveComponent ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AnalyzedInformation $analyzedResults } } if ($serverMaintenance.GetMailboxServer.DatabaseCopyActivationDisabledAndMoveNow -or $serverMaintenance.GetMailboxServer.DatabaseCopyAutoActivationPolicy -eq "Blocked") { $displayValue = "`r`n`t`tDatabaseCopyActivationDisabledAndMoveNow: {0} --- should be 'false'`r`n`t`tDatabaseCopyAutoActivationPolicy: {1} --- should be 'unrestricted'" -f ` $serverMaintenance.GetMailboxServer.DatabaseCopyActivationDisabledAndMoveNow, $serverMaintenance.GetMailboxServer.DatabaseCopyAutoActivationPolicy $analyzedResults = Add-AnalyzedResultInformation -Name "Database Copy Maintenance" -Details $displayValue ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AnalyzedInformation $analyzedResults } if ($serverMaintenance.GetClusterNode -ne $null -and $serverMaintenance.GetClusterNode.State -ne "Up") { $analyzedResults = Add-AnalyzedResultInformation -Name "Cluster Node" -Details ("'{0}' --- should be 'Up'" -f $serverMaintenance.GetClusterNode.State) ` -DisplayGroupingKey $keyExchangeInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AnalyzedInformation $analyzedResults } } ######################### # Operating System ######################### Write-VerboseOutput("Working on Operating System") $analyzedResults = Add-AnalyzedResultInformation -Name "Version" -Details ($osInformation.BuildInformation.FriendlyName) ` -DisplayGroupingKey $keyOSInformation ` -AddHtmlOverviewValues $true ` -HtmlName "OS Version" ` -AnalyzedInformation $analyzedResults $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 $analyzedResults = Add-AnalyzedResultInformation -Name "System Up Time" -Details $upTime ` -DisplayGroupingKey $keyOSInformation ` -DisplayTestingValue ($osInformation.ServerBootUp) ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Time Zone" -Details ($osInformation.TimeZone.CurrentTimeZone) ` -DisplayGroupingKey $keyOSInformation ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults $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" } $analyzedResults = Add-AnalyzedResultInformation -Name "Dynamic Daylight Time Enabled" -Details $writeValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType $writeType ` -AnalyzedInformation $analyzedResults if ($warning -ne [string]::Empty) { $analyzedResults = Add-AnalyzedResultInformation -Details $warning ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Yellow" ` -DisplayCustomTabNumber 2 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } if ([string]::IsNullOrEmpty($osInformation.TimeZone.TimeZoneKeyName)) { $analyzedResults = 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" ` -AnalyzedInformation $analyzedResults } if ($exchangeInformation.NETFramework.OnRecommendedVersion) { $analyzedResults = Add-AnalyzedResultInformation -Name ".NET Framework" -Details ($osInformation.NETFramework.FriendlyName) ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Green" ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults } 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 $analyzedResults = Add-AnalyzedResultInformation -Name ".NET Framework" -Details $displayValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Yellow" ` -DisplayTestingValue $testObject ` -HtmlDetailsCustomValue ($osInformation.NETFramework.FriendlyName) ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults } $displayValue = [string]::Empty $displayWriteType = "Yellow" Write-VerboseOutput("Total Memory: {0}" -f ($totalPhysicalMemory = $hardwareInformation.TotalMemory)) Write-VerboseOutput("Page File: {0}" -f ($maxPageSize = $osInformation.PageFile.MaxPageSize)) $testingValue = New-Object PSCustomObject $testingValue | Add-Member -MemberType NoteProperty -Name "TotalPhysicalMemory" -Value $totalPhysicalMemory $testingValue | Add-Member -MemberType NoteProperty -Name "MaxPageSize" -Value $maxPageSize $testingValue | Add-Member -MemberType NoteProperty -Name "MultiPageFile" -Value ($osInformation.PageFile.PageFile.Count -gt 1) $testingValue | Add-Member -MemberType NoteProperty -Name "RecommendedPageFile" -Value 0 if ($maxPageSize -eq 0) { $displayValue = "Error: System is set to automatically manage the pagefile size." $displayWriteType = "Red" } elseif ($osInformation.PageFile.PageFile.Count -gt 1) { $displayValue = "Multiple page files detected. `r`n`t`tError: This has been know to cause performance issues please address this." $displayWriteType = "Red" } elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) { $testingValue.RecommendedPageFile = ($recommendedPageFileSize = [Math]::Truncate(($totalPhysicalMemory / 1MB) / 4)) Write-VerboseOutput("Recommended Page File Size: {0}" -f $recommendedPageFileSize) if ($recommendedPageFileSize -ne $maxPageSize) { $displayValue = "{0}MB `r`n`t`tWarning: Page File is not set to 25% of the Total System Memory which is {1}MB. Recommended is {2}MB" -f $maxPageSize, ([Math]::Truncate($totalPhysicalMemory / 1MB)), $recommendedPageFileSize } else { $displayValue = "{0}MB" -f $recommendedPageFileSize $displayWriteType = "Grey" } } #32GB = 1024 * 1024 * 1024 * 32 = 34,359,738,368 elseif ($totalPhysicalMemory -ge 34359738368) { if ($maxPageSize -eq 32778) { $displayValue = "{0}MB" -f $maxPageSize $displayWriteType = "Grey" } else { $displayValue = "{0}MB `r`n`t`tWarning: Pagefile should be capped at 32778MB for 32GB plus 10MB - Article: https://docs.microsoft.com/en-us/exchange/exchange-2013-sizing-and-configuration-recommendations-exchange-2013-help#pagefile" -f $maxPageSize } } else { $testingValue.RecommendedPageFile = ($recommendedPageFileSize = [Math]::Round(($totalPhysicalMemory / 1MB) + 10)) if ($recommendedPageFileSize -ne $maxPageSize) { $displayValue = "{0}MB `r`n`t`tWarning: Page File is not set to Total System Memory plus 10MB which should be {1}MB" -f $maxPageSize, $recommendedPageFileSize } else { $displayValue = "{0}MB" -f $maxPageSize $displayWriteType = "Grey" } } $analyzedResults = Add-AnalyzedResultInformation -Name "Page File Size" -Details $displayValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue $testingValue ` -AnalyzedInformation $analyzedResults if ($osInformation.PowerPlan.HighPerformanceSet) { $analyzedResults = Add-AnalyzedResultInformation -Name "Power Plan" -Details ($osInformation.PowerPlan.PowerPlanSetting) ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Green" ` -AnalyzedInformation $analyzedResults } else { $displayValue = "{0} --- Error" -f $osInformation.PowerPlan.PowerPlanSetting $analyzedResults = Add-AnalyzedResultInformation -Name "Power Plan" -Details $displayValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults } if ($osInformation.NetworkInformation.HttpProxy -eq "") { $analyzedResults = Add-AnalyzedResultInformation -Name "Http Proxy Setting" -Details ($osInformation.NetworkInformation.HttpProxy) ` -DisplayGroupingKey $keyOSInformation ` -HtmlDetailsCustomValue "None" ` -AnalyzedInformation $analyzedResults } else { $displayValue = "{0} --- Warning this can cause client connectivity issues." -f $osInformation.NetworkInformation.HttpProxy $analyzedResults = Add-AnalyzedResultInformation -Name "Http Proxy Setting" -Details $displayValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType "Yellow" ` -DisplayTestingValue ($osInformation.NetworkInformation.HttpProxy) ` -AnalyzedInformation $analyzedResults } $displayWriteType2012 = "Yellow" $displayWriteType2013 = "Yellow" $displayValue2012 = "Unknown" $displayValue2013 = "Unknown" if ($osInformation.VcRedistributable -ne $null) { Write-VerboseOutput("VCRedist2012 Testing value: {0}" -f [HealthChecker.VCRedistVersion]::VCRedist2012.value__) Write-VerboseOutput("VCRedist2013 Testing value: {0}" -f [HealthChecker.VCRedistVersion]::VCRedist2013.value__) $vc2013Required = $exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge $displayValue2012 = "Redistributable is outdated" $displayValue2013 = "Redistributable is outdated" foreach ($detectedVisualRedistVersion in $osInformation.VcRedistributable) { Write-VerboseOutput("Testing {0} version id '{1}'" -f $detectedVisualRedistVersion.DisplayName, $detectedVisualRedistVersion.VersionIdentifier) if ($detectedVisualRedistVersion.VersionIdentifier -eq [HealthChecker.VCRedistVersion]::VCRedist2012) { $displayValue2012 = "{0} Version is current" -f $detectedVisualRedistVersion.DisplayVersion $displayWriteType2012 = "Green" } elseif ($vc2013Required -and $detectedVisualRedistVersion.VersionIdentifier -eq [HealthChecker.VCRedistVersion]::VCRedist2013) { $displayWriteType2013 = "Green" $displayValue2013 = "{0} Version is current" -f $detectedVisualRedistVersion.DisplayVersion } } } $analyzedResults = Add-AnalyzedResultInformation -Name "Visual C++ 2012" -Details $displayValue2012 ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType $displayWriteType2012 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Visual C++ 2013" -Details $displayValue2013 ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType $displayWriteType2013 ` -AnalyzedInformation $analyzedResults if ($osInformation.VcRedistributable -ne $null -and ($displayWriteType2012 -eq "Yellow" -or $displayWriteType2013 -eq "Yellow")) { $analyzedResults = Add-AnalyzedResultInformation -Details "Note: For more information about the latest C++ Redistributeable please visit: https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads`r`n`t`tThis is not a requirement to upgrade, only a notification to bring to your attention." ` -DisplayGroupingKey $keyOSInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AnalyzedInformation $analyzedResults } $displayValue = "False" $writeType = "Grey" if ($osInformation.ServerPendingReboot) { $displayValue = "True --- Warning a reboot is pending and can cause issues on the server." $writeType = "Yellow" } $analyzedResults = Add-AnalyzedResultInformation -Name "Server Pending Reboot" -Details $displayValue ` -DisplayGroupingKey $keyOSInformation ` -DisplayWriteType $writeType ` -DisplayTestingValue ($osInformation.ServerPendingReboot) ` -AnalyzedInformation $analyzedResults ################################ # Processor/Hardware Information ################################ Write-VerboseOutput("Working on Processor/Hardware Information") $analyzedResults = Add-AnalyzedResultInformation -Name "Type" -Details ($hardwareInformation.ServerType) ` -DisplayGroupingKey $keyHardwareInformation ` -AddHtmlOverviewValues $true ` -Htmlname "Hardware Type" ` -AnalyzedInformation $analyzedResults if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::Physical -or $hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) { $analyzedResults = Add-AnalyzedResultInformation -Name "Manufacturer" -Details ($hardwareInformation.Manufacturer) ` -DisplayGroupingKey $keyHardwareInformation ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Model" -Details ($hardwareInformation.Model) ` -DisplayGroupingKey $keyHardwareInformation ` -AnalyzedInformation $analyzedResults } $analyzedResults = Add-AnalyzedResultInformation -Name "Processor" -Details ($hardwareInformation.Processor.Name) ` -DisplayGroupingKey $keyHardwareInformation ` -AnalyzedInformation $analyzedResults $value = $hardwareInformation.Processor.NumberOfProcessors $processorName = "Number of Processors" if ($hardwareInformation.ServerType -ne [HealthChecker.ServerType]::Physical) { $analyzedResults = Add-AnalyzedResultInformation -Name $processorName -Details $value ` -DisplayGroupingKey $keyHardwareInformation ` -AnalyzedInformation $analyzedResults if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::VMWare) { $analyzedResults = 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 $analyzedResults } } elseif ($value -gt 2) { $analyzedResults = Add-AnalyzedResultInformation -Name $processorName -Details ("{0} - Error: Recommended to only have 2 Processors" -f $value) ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Red" ` -DisplayTestingValue $value ` -HtmlDetailsCustomValue $value ` -AnalyzedInformation $analyzedResults } else { $analyzedResults = Add-AnalyzedResultInformation -Name $processorName -Details $value ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Green" ` -AnalyzedInformation $analyzedResults } $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" } $analyzedResults = Add-AnalyzedResultInformation -Name "Number of Physical Cores" -Details $physicalValue ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType $displayWriteType ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Number of Logical Cores" -Details $logicalValue ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType $displayWriteType ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults if ($logicalValue -gt $physicalValue) { $analyzedResults = Add-AnalyzedResultInformation -Name "Hyper-Threading" -Details "Enabled --- Error: Having Hyper-Threading enabled goes against best practices and can cause performance issues. Please disable as soon as possible." ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Red" ` -DisplayTestingValue $true ` -AnalyzedInformation $analyzedResults if ($hardwareInformation.ServerType -eq [HealthChecker.ServerType]::AmazonEC2) { $analyzedResults = Add-AnalyzedResultInformation -Details "Error: For high-performance computing (HPC) application, like Exchange, Amazon recommends that you have Hyper-Threading Technology disabled in their service. More informaiton: https://aws.amazon.com/blogs/compute/disabling-intel-hyper-threading-technology-on-amazon-ec2-windows-instances/" ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Red" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } if ($hardwareInformation.Processor.Name.StartsWith("AMD")) { $analyzedResults = Add-AnalyzedResultInformation -Details "This script may incorrectly report that Hyper-Threading is enabled on certain AMD processors. Check with the manufacturer to see if your mondel supports SMT." ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } else { $analyzedResults = Add-AnalyzedResultInformation -Name "Hyper-Threading" -Details "Disabled" ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Green" ` -DisplayTestingValue $false ` -AnalyzedInformation $analyzedResults } #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" } } $analyzedResults = Add-AnalyzedResultInformation -Name $name -Details $displayValue ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue $testingValue ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Max Processor Speed" -Details ($hardwareInformation.Processor.MaxMegacyclesPerCore) ` -DisplayGroupingKey $keyHardwareInformation ` -AnalyzedInformation $analyzedResults if ($hardwareInformation.Processor.ProcessorIsThrottled) { $currentSpeed = $hardwareInformation.Processor.CurrentMegacyclesPerCore $analyzedResults = Add-AnalyzedResultInformation -Name "Current Processor Speed" -Details ("{0} --- Error: Processor appears to be throttled." -f $currentSpeed) ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Red" ` -DisplayTestingValue $currentSpeed ` -AnalyzedInformation $analyzedResults $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." } $analyzedResults = Add-AnalyzedResultInformation -Details $displayValue ` -DisplayGroupingKey $keyHardwareInformation ` -DisplayWriteType "Red" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } $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" } $analyzedResults = Add-AnalyzedResultInformation -Name "Physical Memory" -Details $displayDetails ` -DisplayGroupingKey $keyHardwareInformation ` -DipslayTestingValue $totalPhysicalMemory ` -DisplayWriteType $displayWriteType ` -AddHtmlOverviewValues $true ` -AnalyzedInformation $analyzedResults ################################ #NIC Settings Per Active Adapter ################################ Write-VerboseOutput("Working on NIC Settings Per Active Adapter Information") foreach ($adapter in $osInformation.NetworkInformation.NetworkAdapters) { if ($adapter.Description -eq "Remote NDIS Compatible Device") { Write-VerboseOutput("Remote NDSI Compatible Device found. Ignoring NIC.") continue } $value = "{0} [{1}]" -f $adapter.Description, $adapter.Name $analyzedResults = Add-AnalyzedResultInformation -Name "Interface Description" -Details $value ` -DisplayGroupingKey $keyNICSettings ` -DisplayCustomTabNumber 1 ` -AnalyzedInformation $analyzedResults if ($osInformation.BuildInformation.MajorVersion -ge [HealthChecker.OSServerVersion]::Windows2012R2) { Write-VerboseOutput("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 ($driverDate -eq $null -or $driverDate -eq [DateTime]::MaxValue) { $detailsValue = "Unknown" } elseif ((New-TimeSpan -Start $date -End $driverDate).Days -lt [int]-365) { $analyzedResults = Add-AnalyzedResultInformation -Details "Warning: NIC driver is over 1 year old. Verify you are at the latest version." ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType "Yellow" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } $analyzedResults = Add-AnalyzedResultInformation -Name "Driver Date" -Details $detailsValue ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Driver Version" -Details ($adapter.DriverVersion) ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "MTU Size" -Details ($adapter.MTUSize) ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults $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." } $analyzedResults = Add-AnalyzedResultInformation -Name "RSS Enabled" -Details $detailsValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType $writeType ` -DisplayTestingValue $testingValue ` -AnalyzedInformation $analyzedResults } else { Write-VerboseOutput("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 } $analyzedResults = Add-AnalyzedResultInformation -Name "Link Speed" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayTestingValue $linkSpeed ` -AnalyzedInformation $analyzedResults $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 } $analyzedResults = Add-AnalyzedResultInformation -Name "IPv6 Enabled" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue $TestingValue ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "IPv4 Address" ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults 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 } $analyzedResults = Add-AnalyzedResultInformation -Name "Address" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayCustomTabNumber 3 ` -AnalyzedInformation $analyzedResults } $analyzedResults = Add-AnalyzedResultInformation -Name "IPv6 Address" ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults 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 } $analyzedResults = Add-AnalyzedResultInformation -Name "Address" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayCustomTabNumber 3 ` -AnalyzedInformation $analyzedResults } $analyzedResults = Add-AnalyzedResultInformation -Name "DNS Server" -Details $adapter.DnsServer ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Registered In DNS" -Details $adapter.RegisteredInDns ` -DisplayGroupingKey $keyNICSettings ` -AnalyzedInformation $analyzedResults #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: http://support.microsoft.com/kb/2740020" } $analyzedResults = Add-AnalyzedResultInformation -Name "Sleepy NIC Disabled" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue $adapter.SleepyNicDisabled ` -AnalyzedInformation $analyzedResults } $adapterDescription = $adapter.Description $cookedValue = 0 $foundCounter = $false if ($osInformation.NetworkInformation.PacketsReceivedDiscarded -eq $null) { Write-VerboseOutput("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" } $analyzedResults = Add-AnalyzedResultInformation -Name "Packets Received Discarded" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayTestingValue $cookedValue ` -DisplayWriteType $displayWriteType ` -AnalyzedInformation $analyzedResults if ($knownIssue) { $analyzedResults = Add-AnalyzedResultInformation -Details "Known Issue with vmxnet3: 'Large packet loss at the guest operating system level on the VMXNET3 vNIC in ESXi (2039495)' - https://kb.vmware.com/s/article/2039495" ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType "Yellow" ` -DisplayCustomTabNumber 3 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } if ($osInformation.NetworkInformation.NetworkAdapters.Count -gt 1) { $analyzedResults = 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://docs.microsoft.com/en-us/exchange/planning-for-high-availability-and-site-resilience-exchange-2013-help#NR" ` -DisplayGroupingKey $keyNICSettings ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } if ($osInformation.NetworkInformation.IPv6DisabledOnNICs) { $displayWriteType = "Grey" $displayValue = "True" $testingValue = $true if ($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 key currently set to '{0}'. For details please refer to the following articles: `r`n`t`thttps://docs.microsoft.com/en-us/archive/blogs/rmilne/disabling-ipv6-and-exchange-going-all-the-way `r`n`t`thttps://support.microsoft.com/en-us/help/929852/guidance-for-configuring-ipv6-in-windows-for-advanced-users" -f $osInformation.NetworkInformation.DisabledComponents } $analyzedResults = Add-AnalyzedResultInformation -Name "Disable IPv6 Correctly" -Details $displayValue ` -DisplayGroupingKey $keyNICSettings ` -DisplayWriteType $displayWriteType ` -DisplayCustomTabNumber 1 ` -AnalyzedInformation $analyzedResults } ################ #TCP/IP Settings ################ Write-VerboseOutput("Working on TCP/IP Settings") $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://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Checklist-for-troubleshooting-Outlook-connectivity-in-Exchange/ba-p/604792" $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://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Checklist-for-troubleshooting-Outlook-connectivity-in-Exchange/ba-p/604792" -f $tcpKeepAlive $displayWriteType = "Yellow" } else { $displayValue = $tcpKeepAlive $displayWriteType = "Green" } $analyzedResults = Add-AnalyzedResultInformation -Name "TCP/IP Settings" -Details $displayValue ` -DisplayGroupingKey $keyFrequentConfigIssues ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue $tcpKeepAlive ` -HtmlName "TCPKeepAlive" ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "RPC Min Connection Timeout" -Details ("{0} `r`n`t`tMore Information: https://blogs.technet.microsoft.com/messaging_with_communications/2012/06/06/outlook-anywhere-network-timeout-issue/" -f $osInformation.NetworkInformation.RpcMinConnectionTimeout) ` -DisplayGroupingKey $keyFrequentConfigIssues ` -HtmlName "RPC Minimum Connection Timeout" ` -AnalyzedInformation $analyzedResults $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 } $analyzedResults = Add-AnalyzedResultInformation -Name "CTS Processor Affinity Percentage" -Details $displayValue ` -DisplayGroupingKey $keyFrequentConfigIssues ` -DisplayWriteType $displayWriteType ` -DisplayTestingValue ($exchangeInformation.RegistryValues.CtsProcessorAffinityPercentage) ` -HtmlName "CtsProcessorAffinityPercentage" ` -AnalyzedInformation $analyzedResults $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" } $analyzedResults = Add-AnalyzedResultInformation -Name "Credential Guard Enabled" -Details $displayValue ` -DisplayGroupingKey $keyFrequentConfigIssues ` -DisplayWriteType $displayWriteType ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "LmCompatibilityLevel Settings" -Details ($osInformation.LmCompatibility.RegistryValue) ` -DisplayGroupingKey $keySecuritySettings ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Description" -Details ($osInformation.LmCompatibility.Description) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults ############## # TLS Settings ############## Write-VerboseOutput("Working on TLS Settings") $tlsVersions = @("1.0","1.1","1.2") $currentNetVersion = $osInformation.TLSSettings["NETv4"] foreach ($tlsKey in $tlsVersions) { $currentTlsVersion = $osInformation.TLSSettings[$tlsKey] $analyzedResults = Add-AnalyzedResultInformation -Details ("TLS {0}" -f $tlsKey) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 1 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name ("Server Enabled") -Details ($currentTlsVersion.ServerEnabled) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name ("Server Disabled By Default") -Details ($currentTlsVersion.ServerDisabledByDefault) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name ("Client Enabled") -Details ($currentTlsVersion.ClientEnabled) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name ("Client Disabled By Default") -Details ($currentTlsVersion.ClientDisabledByDefault) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults if ($currentTlsVersion.ServerEnabled -ne $currentTlsVersion.ClientEnabled) { $detectedTlsMismatch = $true $analyzedResults = 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 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults } 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)) { $analyzedResults = Add-AnalyzedResultInformation -Details ("Error: Failed to set .NET SystemDefaultTlsVersions. Please visit on how to properly enable TLS 1.2 https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Server-TLS-guidance-Part-2-Enabling-TLS-1-2-and/ba-p/607761") ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 3 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults } } if ($detectedTlsMismatch) { $displayValues = @("Exchange Server TLS guidance Part 1: Getting Ready for TLS 1.2: https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Server-TLS-guidance-part-1-Getting-Ready-for-TLS-1-2/ba-p/607649", "Exchange Server TLS guidance Part 2: Enabling TLS 1.2 and Identifying Clients Not Using It: https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Server-TLS-guidance-Part-2-Enabling-TLS-1-2-and/ba-p/607761", "Exchange Server TLS guidance Part 3: Turning Off TLS 1.0/1.1: https://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Exchange-Server-TLS-guidance-Part-3-Turning-Off-TLS-1-0-1-1/ba-p/607898") $analyzedResults = Add-AnalyzedResultInformation -Details "For More Information on how to properly set TLS follow these blog posts:" ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Yellow" ` -AnalyzedInformation $analyzedResults foreach ($displayValue in $displayValues) { $analyzedResults = Add-AnalyzedResultInformation -Details $displayValue ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType "Yellow" ` -DisplayCustomTabNumber 3 ` -AnalyzedInformation $analyzedResults } } foreach($certificate in $exchangeInformation.ExchangeCertificates) { if($certificate.LifetimeInDays -ge 60) { $displayColor = "Green" } elseif($certificate.LifetimeInDays -ge 30) { $displayColor = "Yellow" } else { $displayColor = "Red" } $analyzedResults = Add-AnalyzedResultInformation -Name "Certificate" ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 1 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "FriendlyName" -Details $certificate.FriendlyName ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Thumbprint" -Details $certificate.Thumbprint ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Lifetime in days" -Details $certificate.LifetimeInDays ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -DisplayWriteType $displayColor ` -AnalyzedInformation $analyzedResults if($certificate.PublicKeySize -lt 2048) { $additionalDisplayValue = "It's recommended to use a key size of at least 2048 bit." $analyzedResults = Add-AnalyzedResultInformation -Name "Key size" -Details $certificate.PublicKeySize ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Details $additionalDisplayValue ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults } else { $analyzedResults = Add-AnalyzedResultInformation -Name "Key size" -Details $certificate.PublicKeySize ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults } $analyzedResults = Add-AnalyzedResultInformation -Name "Bound to services" -Details $certificate.Services ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Current Auth Certificate" -Details $certificate.IsCurrentAuthConfigCertificate ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "SAN Certificate" -Details $certificate.IsSanCertificate ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Name "Namespaces" ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -AnalyzedInformation $analyzedResults foreach($namespace in $certificate.Namespaces) { $analyzedResults = Add-AnalyzedResultInformation -Details $namespace ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 3 ` -AnalyzedInformation $analyzedResults } } if($exchangeInformation.ExchangeCertificates.IsCurrentAuthConfigCertificate.Contains($true)) { $analyzedResults = Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $true ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 1 ` -DisplayWriteType "Green" ` -AnalyzedInformation $analyzedResults } else { $analyzedResults = Add-AnalyzedResultInformation -Name "Valid Auth Certificate Found On Server" -Details $false ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 1 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults $analyzedResults = Add-AnalyzedResultInformation -Details "No valid Auth Certificate found. This may cause several problems --- Error" ` -DisplayGroupingKey $keySecuritySettings ` -DisplayCustomTabNumber 2 ` -DisplayWriteType "Red" ` -AnalyzedInformation $analyzedResults } $additionalDisplayValue = [string]::Empty $smb1Status = $osInformation.Smb1ServerSettings.Smb1Status if ($osInformation.BuildInformation.MajorVersion -gt [HealthChecker.OSServerVersion]::Windows2012) { $displayValue = "False" $writeType = "Green" if ($smb1Status -band 1) { $displayValue = "Failed to get install status" $writeType = "Yellow" } elseif ($smb1Status -band 2) { $displayValue = "True" $writeType = "Red" $additionalDisplayValue = "SMB1 should be uninstalled" } $analyzedResults = Add-AnalyzedResultInformation -Name "SMB1 Installed" -Details $displayValue ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType $writeType ` -AnalyzedInformation $analyzedResults } $writeType = "Green" $displayValue = "True" if ($smb1Status -band 8) { $displayValue = "Failed to get block status" $writeType = "Yellow" } elseif ($smb1Status -band 16) { $displayValue = "False" $writeType = "Red" $additionalDisplayValue += " SMB1 should be blocked" } $analyzedResults = Add-AnalyzedResultInformation -Name "SMB1 Blocked" -Details $displayValue ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType $writeType ` -AnalyzedInformation $analyzedResults if ($additionalDisplayValue -ne [string]::Empty) { $additionalDisplayValue += "`r`n`t`tMore Information: https://techcommunity.microsoft.com/t5/exchange-team-blog/exchange-server-and-smbv1/ba-p/1165615" $analyzedResults = Add-AnalyzedResultInformation -Details $additionalDisplayValue.Trim() ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType "Yellow" ` -DisplayCustomTabNumber 2 ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } ########################## #Exchange Web App GC Mode# ########################## if ($exchangeInformation.BuildInformation.ServerRole -ne [HealthChecker.ExchangeServerRole]::Edge) { Write-VerboseOutput("Working on Exchange Web App GC Mode") $analyzedResults = Add-AnalyzedResultInformation -Name "Web App Pool" -Details "GC Server Mode Enabled | Status" ` -DisplayGroupingKey $keyWebApps ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults foreach ($webAppKey in $exchangeInformation.ApplicationPools.Keys) { $xmlData = [xml]$exchangeInformation.ApplicationPools[$webAppKey].Content $testingValue = New-Object PSCustomObject $testingValue | Add-Member -MemberType NoteProperty -Name "GCMode" -Value ($enabled = $xmlData.Configuration.Runtime.gcServer.Enabled -eq 'true') $testingValue | Add-Member -MemberType NoteProperty -Name "Status" -Value ($status = $exchangeInformation.ApplicationPools[$webAppKey].Status) $analyzedResults = Add-AnalyzedResultInformation -Name $webAppKey -Details ("{0} | {1}" -f $enabled, $status) ` -DisplayGroupingKey $keyWebApps ` -DisplayTestingValue $testingValue ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } } ###################### # Vulnerability Checks ###################### Function Test-VulnerabilitiesByBuildNumbersForDisplay { 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) { $Script:AnalyzedInformation = 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 $keySecuritySettings ` -DisplayTestingValue $cveName ` -DisplayWriteType "Red" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $Script:AnalyzedInformation } $Script:AllVulnerabilitiesPassed = $false break } if($Script:breakpointHit) { break } } } $Script:AllVulnerabilitiesPassed = $true $Script:AnalyzedInformation = $analyzedResults [string]$buildRevision = ("{0}.{1}" -f $exchangeInformation.BuildInformation.ExchangeSetup.FileBuildPart, $exchangeInformation.BuildInformation.ExchangeSetup.FilePrivatePart) Write-VerboseOutput("Exchange Build Revision: {0}" -f $buildRevision) Write-VerboseOutput("Exchange CU: {0}" -f ($exchangeCU = $exchangeInformation.BuildInformation.CU)) if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2013) { if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU19) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1347.5","1365.3" -CVENames "CVE-2018-0924","CVE-2018-0940" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU20) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1365.7","1367.6" -CVENames "CVE-2018-8151","CVE-2018-8154","CVE-2018-8159" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU21) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1367.9","1395.7" -CVENames "CVE-2018-8302" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1395.8" -CVENames "CVE-2018-8265","CVE-2018-8448" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1395.10" -CVENames "CVE-2019-0586","CVE-2019-0588" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU22) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1473.3" -CVENames "CVE-2019-0686","CVE-2019-0724" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1473.4" -CVENames "CVE-2019-0817","CVE-2019-0858" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1473.5" -CVENames "ADV190018" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU23) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.3" -CVENames "CVE-2019-1084","CVE-2019-1136","CVE-2019-1137" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.4" -CVENames "CVE-2019-1373" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.6" -CVENames "CVE-2020-0688","CVE-2020-0692" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.7" -CVENames "CVE-2020-16969" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.8" -CVENames "CVE-2020-17083","CVE-2020-17084","CVE-2020-17085" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1497.10" -CVENames "CVE-2020-17117","CVE-2020-17132","CVE-2020-17142","CVE-2020-17143" } } elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2016) { if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU8) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1261.39","1415.4" -CVENames "CVE-2018-0924","CVE-2018-0940","CVE-2018-0941" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU9) { Test-VulnerabilitiesByBuildNumbersForDisplay -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) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1466.9","1531.6" -CVENames "CVE-2018-8374","CVE-2018-8302" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1531.8" -CVENames "CVE-2018-8265","CVE-2018-8448" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU11) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1531.8","1591.11" -CVENames "CVE-2018-8604" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1531.10","1591.13" -CVENames "CVE-2019-0586","CVE-2019-0588" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU12) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1591.16","1713.6" -CVENames "CVE-2019-0817","CVE-2018-0858" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1591.17","1713.7" -CVENames "ADV190018" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1713.5" -CVENames "CVE-2019-0686","CVE-2019-0724" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU13) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1713.8","1779.4" -CVENames "CVE-2019-1084","CVE-2019-1136","CVE-2019-1137" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1713.9","1779.5" -CVENames "CVE-2019-1233","CVE-2019-1266" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU14) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1779.7","1847.5" -CVENames "CVE-2019-1373" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU15) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1847.7","1913.7" -CVENames "CVE-2020-0688","CVE-2020-0692" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1847.10","1913.10" -CVENames "CVE-2020-0903" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU17) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "1979.6","2044.6" -CVENames "CVE-2020-16875" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU18) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "2044.7","2106.3" -CVENames "CVE-2020-16969" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "2044.8","2106.4" -CVENames "CVE-2020-17083","CVE-2020-17084","CVE-2020-17085" Test-VulnerabilitiesByBuildNumbersForDisplay -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 -ge [HealthChecker.ExchangeCULevel]::CU19) { Write-VerboseOutput("There are no known vulnerabilities in this Exchange Server Build.") } } elseif ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) { if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU1) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "221.14" -CVENames "CVE-2019-0586","CVE-2019-0588" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "221.16","330.7" -CVENames "CVE-2019-0817","CVE-2019-0858" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "221.17","330.8" -CVENames "ADV190018" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "330.6" -CVENames "CVE-2019-0686","CVE-2019-0724" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU2) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "330.9","397.5" -CVENames "CVE-2019-1084","CVE-2019-1137" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "397.6","330.10" -CVENames "CVE-2019-1233","CVE-2019-1266" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU3) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "397.9","464.7" -CVENames "CVE-2019-1373" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU4) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "464.11","529.8" -CVENames "CVE-2020-0688","CVE-2020-0692" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "464.14","529.11" -CVENames "CVE-2020-0903" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU6) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "595.6","659.6" -CVENames "CVE-2020-16875" } if ($exchangeCU -le [HealthChecker.ExchangeCULevel]::CU7) { Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "659.7","721.3" -CVENames "CVE-2020-16969" Test-VulnerabilitiesByBuildNumbersForDisplay -ExchangeBuildRevision $buildRevision -SecurityFixedBuilds "659.8","721.4" -CVENames "CVE-2020-17083","CVE-2020-17084","CVE-2020-17085" Test-VulnerabilitiesByBuildNumbersForDisplay -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 -ge [HealthChecker.ExchangeCULevel]::CU8) { Write-VerboseOutput("There are no known vulnerabilities in this Exchange Server Build.") } } else { Write-VerboseOutput("Unknown Version of Exchange") $Script:AllVulnerabilitiesPassed = $false } #Description: Check for CVE-2020-0796 SMBv3 vulnerability #Affected OS versions: Windows 10 build 1903 and 1909 #Fix: KB4551762 #Workaround: Disable SMBv3 compression if ($exchangeInformation.BuildInformation.MajorVersion -eq [HealthChecker.ExchangeMajorVersion]::Exchange2019) { Write-VerboseOutput("Testing CVE: CVE-2020-0796") $buildNumber = $osInformation.BuildInformation.VersionBuild.Split(".")[2] if (($buildNumber -eq 18362 -or $buildNumber -eq 18363) -and ($osInformation.RegistryValues.CurrentVersionUbr -lt 720)) { Write-VerboseOutput("Build vulnerable to CVE-2020-0796. Checking if workaround is in place.") $writeType = "Red" $writeValue = "System Vulnerable" if ($osInformation.RegistryValues.LanManServerDisabledCompression -eq 1) { Write-VerboseOutput("Workaround to disable affected SMBv3 compression is in place.") $writeType = "Yellow" $writeValue = "Workaround is in place" } else { Write-VerboseOutput("Workaround to disable affected SMBv3 compression is NOT in place.") $Script:AllVulnerabilitiesPassed = $false } $analyzedResults = Add-AnalyzedResultInformation -Name "CVE-2020-0796" -Details ("{0}`r`n`t`tSee: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0796 for more information." -f $writeValue) ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType $writeType ` -AddHtmlDetailRow $false ` -AnalyzedInformation $analyzedResults } else { Write-VerboseOutput("System NOT vulnerable to CVE-2020-0796. Information URL: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-0796") } } else { Write-VerboseOutput("Operating System NOT vulnerable to CVE-2020-0796.") } #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 ($osInformation.NETFramework.NetMajorVersion -eq [HealthChecker.NetMajorVersion]::Net4d8) { $dllFileBuildPartToCheckAgainst = 4190 } Write-VerboseOutput("System.Data.dll FileBuildPart: {0} | LastWriteTimeUtc: {1}" -f ($systemDataDll = $osInformation.NETFramework.FileInformation["System.Data.dll"]).VersionInfo.FileBuildPart, ` $systemDataDll.LastWriteTimeUtc) Write-VerboseOutput("System.Configuration.dll FileBuildPart: {0} | LastWriteTimeUtc: {1}" -f ($systemConfigurationDll = $osInformation.NETFramework.FileInformation["System.Configuration.dll"]).VersionInfo.FileBuildPart, ` $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-VerboseOutput("System NOT vulnerable to {0}. Information URL: https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/{0}" -f "CVE-2020-1147") } else { $Script:AllVulnerabilitiesPassed = $false $Script:AnalyzedInformation = 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 $keySecuritySettings ` -DisplayWriteType "Red" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $Script:AnalyzedInformation } if ($Script:AllVulnerabilitiesPassed) { $Script:AnalyzedInformation = Add-AnalyzedResultInformation -Details "All known security issues in this version of the script passed." ` -DisplayGroupingKey $keySecuritySettings ` -DisplayWriteType "Green" ` -AddHtmlDetailRow $false ` -AnalyzedInformation $Script:AnalyzedInformation } Write-Debug("End of Analyzer Engine") return $Script:AnalyzedInformation } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Confirm-Administrator/Confirm-Administrator.ps1 Function Confirm-Administrator { #Function Version 1.1 $currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent() ) if($currentPrincipal.IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator )) { return $true } else { return $false } } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Confirm-ExchangeShell/Confirm-ExchangeShell.ps1 Function Confirm-ExchangeShell { [CmdletBinding()] param( [Parameter(Mandatory=$false)][bool]$LoadExchangeShell = $true, [Parameter(Mandatory=$false)][bool]$LoadExchangeVariables = $true, [Parameter(Mandatory=$false)][bool]$ByPassLocalExchangeServerTest = $false, [Parameter(Mandatory=$false)][scriptblock]$CatchActionFunction ) #Function Version 1.6 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-HostWriters/Write-HostWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-VerboseWriter.ps1 #> $passed = $false Write-VerboseWriter("Calling: Confirm-ExchangeShell") Write-VerboseWriter("Passed: [bool]LoadExchangeShell: {0} | [bool]LoadExchangeVariables: {1} | [bool]ByPassLocalExchangeServerTest: {2}" -f $LoadExchangeShell, $LoadExchangeVariables, $ByPassLocalExchangeServerTest) #Test that we are on Exchange 2010 or newer if(($isLocalExchangeServer = (Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup')) -or ($isLocalExchangeServer = (Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup')) -or $ByPassLocalExchangeServerTest) { Write-VerboseWriter("We are on Exchange 2010 or newer") if((Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\EdgeTransportRole') -or (Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\EdgeTransportRole')) { Write-VerboseWriter("We are on Exchange Edge Transport Server") $IsEdgeTransport = $true } try { if(((Get-PSSession | Where-Object {($_.Availability -eq 'Available') -and ($_.ConfigurationName -eq 'Microsoft.Exchange')}).Count -eq 0) -and ((Get-Module -Name RemoteExchange).Count -eq 1)) { Write-VerboseWriter("Removing RemoteExchange module") Remove-Module -Name RemoteExchange $currentPSModules = Get-Module foreach ($PSModule in $currentPSModules) { if(($PSModule.ModuleType -eq "Script") -and ($PSModule.ModuleBase -like "*\Microsoft\Exchange\RemotePowerShell\*")) { Write-VerboseWriter("Removing module {0} for implicit remoting" -f $PSModule.Name) Remove-Module -Name $PSModule.Name } } } Get-ExchangeServer -ErrorAction Stop | Out-Null Write-VerboseWriter("Exchange PowerShell Module already loaded.") $passed = $true } catch { Write-VerboseWriter("Failed to run Get-ExchangeServer") if($CatchActionFunction -ne $null) { & $CatchActionFunction $watchErrors = $true } if($LoadExchangeShell -and $isLocalExchangeServer) { Write-HostWriter "Loading Exchange PowerShell Module..." try { if($watchErrors) { $currentErrors = $Error.Count } if($IsEdgeTransport) { [xml]$PSSnapIns = Get-Content -Path "$env:ExchangeInstallPath\Bin\exshell.psc1" -ErrorAction Stop ForEach($PSSnapIn in $PSSnapIns.PSConsoleFile.PSSnapIns.PSSnapIn) { Write-VerboseWriter("Trying to add PSSnapIn: {0}" -f $PSSnapIn.Name) Add-PSSnapin -Name $PSSnapIn.Name -ErrorAction Stop } Import-Module $env:ExchangeInstallPath\bin\Exchange.ps1 -ErrorAction Stop $passed = $true #We are just going to assume this passed. } else { Import-Module $env:ExchangeInstallPath\bin\RemoteExchange.ps1 -ErrorAction Stop Connect-ExchangeServer -Auto -ClientApplication:ManagementShell $passed = $true #We are just going to assume this passed. } if($watchErrors) { $index = 0 while($index -lt ($Error.Count - $currentErrors)) { & $CatchActionFunction $Error[$index] $index++ } } } catch { Write-HostWriter("Failed to Load Exchange PowerShell Module...") } } } finally { if($LoadExchangeVariables -and $passed -and $isLocalExchangeServer) { #Diff from master not using Get-ExchangeInstallDirectory because of required functions if($ExInstall -eq $null -or $ExBin -eq $null) { if(Test-Path 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup') { $Global:ExInstall = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup).MsiInstallPath } else { $Global:ExInstall = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup).MsiInstallPath } $Global:ExBin = $Global:ExInstall + "\Bin" Write-VerboseWriter("Set ExInstall: {0}" -f $Global:ExInstall) Write-VerboseWriter("Set ExBin: {0}" -f $Global:ExBin) } } } } else { Write-VerboseWriter("Does not appear to be an Exchange 2010 or newer server.") } Write-VerboseWriter("Returned: {0}" -f $passed) return $passed } #Master Template: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/New-LoggerObject/New-LoggerObject.ps1 Function New-LoggerObject { [CmdletBinding()] param( [Parameter(Mandatory=$false)][string]$LogDirectory = ".", [Parameter(Mandatory=$false)][string]$LogName = "Script_Logging", [Parameter(Mandatory=$false)][bool]$EnableDateTime = $true, [Parameter(Mandatory=$false)][bool]$IncludeDateTimeToFileName = $true, [Parameter(Mandatory=$false)][int]$MaxFileSizeInMB = 10, [Parameter(Mandatory=$false)][int]$CheckSizeIntervalMinutes = 10, [Parameter(Mandatory=$false)][int]$NumberOfLogsToKeep = 10, [Parameter(Mandatory=$false)][bool]$VerboseEnabled, [Parameter(Mandatory=$false)][scriptblock]$HostFunctionCaller, [Parameter(Mandatory=$false)][scriptblock]$VerboseFunctionCaller ) #Function Version 1.2 <# Required Functions: https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-HostWriters/Write-ScriptMethodHostWriter.ps1 https://raw.githubusercontent.com/dpaulson45/PublicPowerShellScripts/master/Functions/Write-VerboseWriters/Write-ScriptMethodVerboseWriter.ps1 #> ######################## # # Template Functions # ######################## Function Write-ToLog { param( [object]$WriteString, [string]$LogLocation ) $WriteString | Out-File ($LogLocation) -Append } ######################## # # End Template Functions # ######################## ########## Parameter Binding Exceptions ############## # throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid ParameterName" if($LogDirectory -eq ".") { $LogDirectory = (Get-Location).Path } if([string]::IsNullOrEmpty($LogName)) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid LogName" } if(!(Test-Path $LogDirectory)) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid LogDirectory" } $loggerObject = New-Object pscustomobject $loggerObject | Add-Member -MemberType NoteProperty -Name "FileDirectory" -Value $LogDirectory $loggerObject | Add-Member -MemberType NoteProperty -Name "FileName" -Value $LogName $loggerObject | Add-Member -MemberType NoteProperty -Name "FullPath" -Value $fullLogPath $loggerObject | Add-Member -MemberType NoteProperty -Name "InstanceBaseName" -Value ([string]::Empty) $loggerObject | Add-Member -MemberType NoteProperty -Name "EnableDateTime" -Value $EnableDateTime $loggerObject | Add-Member -MemberType NoteProperty -Name "IncludeDateTimeToFileName" -Value $IncludeDateTimeToFileName $loggerObject | Add-Member -MemberType NoteProperty -Name "MaxFileSizeInMB" -Value $MaxFileSizeInMB $loggerObject | Add-Member -MemberType NoteProperty -Name "CheckSizeIntervalMinutes" -Value $CheckSizeIntervalMinutes $loggerObject | Add-Member -MemberType NoteProperty -Name "NextFileCheckTime" -Value ((Get-Date).AddMinutes($CheckSizeIntervalMinutes)) $loggerObject | Add-Member -MemberType NoteProperty -Name "InstanceNumber" -Value 1 $loggerObject | Add-Member -MemberType NoteProperty -Name "NumberOfLogsToKeep" -Value $NumberOfLogsToKeep $loggerObject | Add-Member -MemberType NoteProperty -Name "WriteVerboseData" -Value $VerboseEnabled $loggerObject | Add-Member -MemberType NoteProperty -Name "PreventLogCleanup" -Value $false $loggerObject | Add-Member -MemberType ScriptMethod -Name "ToLog" -Value ${Function:Write-ToLog} $loggerObject | Add-Member -MemberType ScriptMethod -Name "WriteHostWriter" -Value ${Function:Write-ScriptMethodHostWriter} $loggerObject | Add-Member -MemberType ScriptMethod -Name "WriteVerboseWriter" -Value ${Function:Write-ScriptMethodVerboseWriter} if($HostFunctionCaller -ne $null) { $loggerObject | Add-Member -MemberType ScriptMethod -Name "HostFunctionCaller" -Value $HostFunctionCaller } if($VerboseFunctionCaller -ne $null) { $loggerObject | Add-Member -MemberType ScriptMethod -Name "VerboseFunctionCaller" -Value $VerboseFunctionCaller } $loggerObject | Add-Member -MemberType ScriptMethod -Name "WriteHost" -Value { param( [object]$LoggingString ) if($LoggingString -eq $null) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid LoggingString" } if($this.EnableDateTime) { $LoggingString = "[{0}] : {1}" -f [System.DateTime]::Now, $LoggingString } $this.WriteHostWriter($LoggingString) $this.ToLog($LoggingString, $this.FullPath) $this.LogUpKeep() } $loggerObject | Add-Member -MemberType ScriptMethod -Name "WriteVerbose" -Value { param( [object]$LoggingString ) if($LoggingString -eq $null) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid LoggingString" } if($this.EnableDateTime) { $LoggingString = "[{0}] : {1}" -f [System.DateTime]::Now, $LoggingString } $this.WriteVerboseWriter($LoggingString) $this.ToLog($LoggingString, $this.FullPath) $this.LogUpKeep() } $loggerObject | Add-Member -MemberType ScriptMethod -Name "WriteToFileOnly" -Value { param( [object]$LoggingString ) if($LoggingString -eq $null) { throw [System.Management.Automation.ParameterBindingException] "Failed to provide valid LoggingString" } if($this.EnableDateTime) { $LoggingString = "[{0}] : {1}" -f [System.DateTime]::Now, $LoggingString } $this.ToLog($LoggingString, $this.FullPath) $this.LogUpKeep() } $loggerObject | Add-Member -MemberType ScriptMethod -Name "UpdateFileLocation" -Value{ if($this.FullPath -eq $null) { if($this.IncludeDateTimeToFileName) { $this.InstanceBaseName = "{0}_{1}" -f $this.FileName, ((Get-Date).ToString('yyyyMMddHHmmss')) $this.FullPath = "{0}\{1}.txt" -f $this.FileDirectory, $this.InstanceBaseName } else { $this.InstanceBaseName = "{0}" -f $this.FileName $this.FullPath = "{0}\{1}.txt" -f $this.FileDirectory, $this.InstanceBaseName } } else { do{ $this.FullPath = "{0}\{1}_{2}.txt" -f $this.FileDirectory, $this.InstanceBaseName, $this.InstanceNumber $this.InstanceNumber++ }while(Test-Path $this.FullPath) $this.WriteVerbose("Updated to New Log") } } $loggerObject | Add-Member -MemberType ScriptMethod -Name "LogUpKeep" -Value { if($this.NextFileCheckTime -gt [System.DateTime]::Now) { return } $this.NextFileCheckTime = (Get-Date).AddMinutes($this.CheckSizeIntervalMinutes) $this.CheckFileSize() $this.CheckNumberOfFiles() $this.WriteVerbose("Did Log Object Up Keep") } $loggerObject | Add-Member -MemberType ScriptMethod -Name "CheckFileSize" -Value { $item = Get-ChildItem $this.FullPath if(($item.Length / 1MB) -gt $this.MaxFileSizeInMB) { $this.UpdateFileLocation() } } $loggerObject | Add-Member -MemberType ScriptMethod -Name "CheckNumberOfFiles" -Value { $filter = "{0}*" -f $this.InstanceBaseName $items = Get-ChildItem -Path $this.FileDirectory | ?{$_.Name -like $filter} if($items.Count -gt $this.NumberOfLogsToKeep) { do{ $items | Sort-Object LastWriteTime | Select -First 1 | Remove-Item -Force $items = Get-ChildItem -Path $this.FileDirectory | ?{$_.Name -like $filter} }while($items.Count -gt $this.NumberOfLogsToKeep) } } $loggerObject | Add-Member -MemberType ScriptMethod -Name "RemoveLatestLogFile" -Value { if(!$this.PreventLogCleanup) { $item = Get-ChildItem $this.FullPath Remove-Item $item -Force } } $loggerObject.UpdateFileLocation() try { "[{0}] : Creating a new logger instance" -f [System.DateTime]::Now | Out-File ($loggerObject.FullPath) -Append } catch { throw } return $loggerObject } Function Get-CounterSamples { param( [Parameter(Mandatory=$true)][array]$MachineNames, [Parameter(Mandatory=$true)][array]$Counters ) Write-VerboseOutput("Calling: Get-CounterSamples") try { $counterSamples = (Get-Counter -ComputerName $MachineNames -Counter $Counters -ErrorAction Stop).CounterSamples } catch { Invoke-CatchActions Write-VerboseOutput("Failed to get counter samples") } Write-VerboseOutput("Exiting: Get-CounterSamples") return $counterSamples } Function Get-ErrorsThatOccurred { if($Error.Count -gt $Script:ErrorStartCount) { Write-Grey(" "); Write-Grey(" ") Function Write-Errors { $index = 0; "`r`n`r`nErrors that occurred that wasn't handled" | Out-File ($Script:OutputFullPath) -Append $Script:Logger.WriteToFileOnly("`r`n`r`nErrors that occurred that wasn't handled") while($index -lt ($Error.Count - $Script:ErrorStartCount)) { #for 2008R2 can't use .Contains on an array object, need to do something else. $goodError = $false foreach($okayErrors in $Script:ErrorsExcluded) { if($okayErrors.Equals($Error[$index])) { $goodError = $true break } } if(!($goodError)) { $Script:Logger.WriteToFileOnly($Error[$index]) $Error[$index] | Out-File ($Script:OutputFullPath) -Append } $index++ } Write-Grey(" "); Write-Grey(" ") "Errors that were handled" | Out-File ($Script:OutputFullPath) -Append $Script:Logger.WriteToFileOnly("`r`n`r`nErrors that were handled") foreach($okayErrors in $Script:ErrorsExcluded) { $okayErrors | Out-File ($Script:OutputFullPath) -Append $Script:Logger.WriteToFileOnly($okayErrors) } } if(($Error.Count - $Script:ErrorStartCount) -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 and .xml file to ExToolsFeedback@microsoft.com.") $Script:Logger.PreventLogCleanup = $true Write-Errors } elseif($Script:VerboseEnabled -or $SaveDebugLog) { Write-VerboseOutput("All errors that occurred were in try catch blocks and was handled correctly.") $Script:Logger.PreventLogCleanup = $true Write-Errors } } else { Write-VerboseOutput("No errors occurred in the script.") } } Function Get-HealthCheckFilesItemsFromLocation{ $items = Get-ChildItem $XMLDirectoryPath | Where-Object{$_.Name -like "HealthCheck-*-*.xml"} if($items -eq $null) { 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 Invoke-CatchActions{ param( [object]$CopyThisError ) Write-VerboseOutput("Calling: Invoke-CatchActions") $Script:ErrorsExcludedCount++ if($CopyThisError -eq $null) { $Script:ErrorsExcluded += $Error[0] } else { $Script:ErrorsExcluded += $CopyThisError } } Function Set-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) { return } $byPassLocalExchangeServerTest = $false if ($Script:Server -ne $env:COMPUTERNAME) { $byPassLocalExchangeServerTest = $true } if(!(Confirm-ExchangeShell ` -ByPassLocalExchangeServerTest $byPassLocalExchangeServerTest ` -CatchActionFunction ${Function:Invoke-CatchActions} )) { Write-Yellow("Failed to load Exchange Shell... stopping script") exit } } Function Test-RequiresServerFqdn { Write-VerboseOutput("Calling: Test-RequiresServerFqdn") try { $Script:ServerFQDN = (Get-ExchangeServer $Script:Server).FQDN Invoke-Command -ComputerName $Script:Server -ScriptBlock {Get-Date | Out-Null} -ErrorAction Stop Write-VerboseOutput("Connected successfully using NetBIOS name.") } catch { Invoke-CatchActions Write-VerboseOutput("Failed to connect to {0} using NetBIOS name. Fallback to Fqdn: {1}" -f $Script:Server, $Script:ServerFQDN) $Script:Server = $Script:ServerFQDN } } Function Test-IsCurrentVersion { param( [Parameter(Mandatory=$true)][string]$CurrentVersion, [Parameter(Mandatory=$true)][string]$TestingVersion ) Write-VerboseOutput("Calling: Test-IsCurrentVersion") $splitCurrentVersion = $CurrentVersion.Split(".") $splitTestingVersion = $TestingVersion.Split(".") if($splitCurrentVersion.Count -eq $splitTestingVersion.Count) { for($i = 0; $i -lt $splitCurrentVersion.Count; $i++) { if($splitCurrentVersion[$i] -lt $splitTestingVersion[$i]) { return $false } } return $true } else { Write-VerboseOutput("Split count isn't the same, assuming that we are not on current version.") return $false } } Function Test-ScriptVersion { param( [Parameter(Mandatory=$true)][string]$ApiUri, [Parameter(Mandatory=$true)][string]$RepoOwner, [Parameter(Mandatory=$true)][string]$RepoName, [Parameter(Mandatory=$true)][string]$CurrentVersion, [Parameter(Mandatory=$true)][int]$DaysOldLimit, [Parameter(Mandatory=$false)][Scriptblock]$CatchActionFunction ) Write-VerboseOutput("Calling: Test-ScriptVersion") $isCurrent = $false if(Test-Connection -ComputerName $ApiUri -Count 1 -Quiet) { try { $ScriptBlock = { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ConvertFrom-Json(Invoke-WebRequest -Uri ($uri = "https://$($args[0])/repos/$($args[1])/$($args[2])/releases/latest")) } $WebRequestJob = Start-Job -ScriptBlock $ScriptBlock -Name "WebRequestJob" -ArgumentList $ApiUri,$RepoOwner,$RepoName do { $i++ if((Get-Job -Id $WebRequestJob.Id).State -eq "Completed") { Write-VerboseOutput("WebRequest after {0} attempts successfully completed. Receiving results." -f $i) try { $releaseInformation = Receive-Job -Id $WebRequestJob.Id -Keep -ErrorAction Stop } catch { if ($CatchActionFunction -ne $null) { & $CatchActionFunction } } Write-VerboseOutput("Removing background worker job") Remove-Job -Id $WebRequestJob.Id Break } else { Write-VerboseOutput("Attempt: {0} WebRequest not yet complete." -f $i) if($i -eq 30) { Write-VerboseOutput("Reached 30 attempts. Removing background worker job.") Remove-Job -Id $WebRequestJob.Id } Start-Sleep -Seconds 1 } } while($i -lt 30) } catch { Invoke-CatchActions Write-VerboseOutput("Failed to run Invoke-WebRequest") } if($releaseInformation -ne $null) { Write-VerboseOutput("We're online: {0} connected successfully." -f $uri) $latestVersion = ($releaseInformation.tag_name).Split("v")[1] if(Test-IsCurrentVersion -CurrentVersion $CurrentVersion -TestingVersion $latestVersion) { Write-VerboseOutput("Version '{0}' is the latest version." -f $latestVersion) $isCurrent = $true } else { Write-VerboseOutput("Version '{0}' is outdated. Lastest version is '{1}'" -f $CurrentVersion, $latestVersion) } } else { Write-VerboseOutput("Release information was null.") } } else { Write-VerboseOutput("We're offline: Unable to connect to '{0}" -f $ApiUri) Write-VerboseOutput("Unable to determine if this version '{0}' is current. Checking to see if the file is older than {1} days." -f $CurrentVersion, $DaysOldLimit) $writeTime = (Get-ChildItem ($MyInvocation.ScriptName)).LastWriteTime if($writeTime -gt ($testDate = ([datetime]::Now).AddDays(-$DaysOldLimit))) { Write-VerboseOutput("Determined that the script write time '{0}' is new than our our test date '{1}'." -f $writeTime, $testDate) $isCurrent = $true } else { Write-VerboseOutput("Script doesn't appear to be on the latest possible version. Script write time '{0}' vs out test date '{1}'" -f $writeTime, $testDate) } } Write-VerboseOutput("Exiting: Test-ScriptVersion | Returning: {0}" -f $isCurrent) return $isCurrent } Function Create-HtmlServerReport { param( [Parameter(Mandatory=$true)][array]$AnalyzedHtmlServerValues ) Write-VerboseOutput("Calling: Create-HtmlServerReport") $htmlHeader = "

Exchange Health Checker v$($Script:healthCheckerVersion)


Servers Overview

" [array]$htmlOverviewTable += "

" foreach ($tableHeaderName in $AnalyzedHtmlServerValues[0]["OverviewValues"].Name) { $htmlOverviewTable += "" -f $tableHeaderName } $htmlOverviewTable += "" foreach ($serverHtmlServerValues in $AnalyzedHtmlServerValues) { $htmlTableRow = @() [array]$htmlTableRow += "" foreach ($htmlTableDataRow in $serverHtmlServerValues["OverviewValues"]) { $htmlTableRow += "" -f $htmlTableDataRow.Class, ` $htmlTableDataRow.DetailValue } $htmlTableRow += "" $htmlOverviewTable += $htmlTableRow } $htmlOverviewTable += "
{0}
{1}

" [array]$htmlServerDetails += "

Server Details

" foreach ($serverHtmlServerValues in $AnalyzedHtmlServerValues) { foreach ($htmlTableDataRow in $serverHtmlServerValues["ServerDetails"]) { if ($htmlTableDataRow.Name -eq "Server Name") { $htmlServerDetails += "" -f $htmlTableDataRow.Name, ` $htmlTableDataRow.DetailValue } else { $htmlServerDetails += "" -f $htmlTableDataRow.Class, ` $htmlTableDataRow.Name, ` $htmlTableDataRow.DetailValue } } } $htmlServerDetails += "
{0}{1}
{1}{2}

" $htmlReport = $htmlHeader + $htmlOverviewTable + $htmlServerDetails + "" $htmlReport | Out-File $HtmlReportFile -Encoding UTF8 } Function Get-CASLoadBalancingReport { Write-VerboseOutput("Calling: Get-CASLoadBalancingReport") Write-Yellow("Note: CAS Load Balancing Report has known issues with attempting to get counter from servers. If you see errors regarding 'Get-Counter path not valid', please ignore for the time being. This is going to be addressed in later versions") #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($CasServerList -ne $null) { 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 ($_.Site.Name -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" try { $AllCounterResults = Get-Counter -ComputerName $CASServers -Counter $PerformanceCounters } catch { Invoke-CatchActions Write-VerboseOutput("Failed to get counter samples") } 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-VerboseOutput("Calling: Get-ComputerCoresObject") Write-VerboseOutput("Passed: {0}" -f $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 { Set-ScriptLogFileLocation -FileName "HealthCheck-ExchangeDCCoreRatio" Write-VerboseOutput("Calling: Get-ExchangeDCCoreRatio") Write-Grey("Exchange Server Health Checker Report - AD GC Core to Exchange Server Core Ratio - v{0}" -f $healthCheckerVersion) $coreRatioObj = New-Object pscustomobject try { Write-VerboseOutput("Attempting to load Active Directory Module") Import-Module ActiveDirectory Write-VerboseOutput("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 | %{ Get-ADDomainController -Filter {isGlobalCatalog -eq $true -and Site -eq $ADSite} -Server $_ } [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://techcommunity.microsoft.com/t5/Exchange-Team-Blog/Ask-the-Perf-Guy-Sizing-Exchange-2013-Deployments/ba-p/594229") Write-Yellow("`thttps://docs.microsoft.com/en-us/exchange/exchange-2013-sizing-and-configuration-recommendations-exchange-2013-help#active-directory") } 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-VerboseOutput("Calling: Get-MailboxDatabaseAndMailboxStatistics") $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-VerboseOutput("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-VerboseOutput("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-VerboseOutput("`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-VerboseOutput("`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 + ".") } } Function Main { if(-not (Confirm-Administrator) -and (-not $AnalyzeDataOnly -and -not $BuildHtmlServersReport)) { Write-Warning "The script needs to be executed in elevated mode. Start the Exchange Management Shell as an Administrator." $Script:ErrorStartCount = $Error.Count Start-Sleep -Seconds 2; exit } if ($Error.Count -gt 175) { Write-Verbose("Clearing Error to avoid script issues") $Error.Clear() } $Script:ErrorStartCount = $Error.Count #useful for debugging $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) { Set-ScriptLogFileLocation -FileName "HealthChecker-HTMLServerReport" $files = Get-HealthCheckFilesItemsFromLocation $fullPaths = Get-OnlyRecentUniqueServersXMLs $files $importData = Import-MyData -FilePaths $fullPaths Create-HtmlServerReport -AnalyzedHtmlServerValues $importData.HtmlServerValues sleep 2; return } if((Test-Path $OutputFilePath) -eq $false) { Write-Host "Invalid value specified for -OutputFilePath." -ForegroundColor Red return } if($LoadBalancingReport) { Set-ScriptLogFileLocation -FileName "LoadBalancingReport" Write-HealthCheckerVersion 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) { Set-ScriptLogFileLocation -FileName "HealthCheck-MailboxReport" -IncludeServerName $true Get-MailboxDatabaseAndMailboxStatistics Write-Grey("Output file written to {0}" -f $Script:OutputFullPath) return } if ($AnalyzeDataOnly) { Set-ScriptLogFileLocation -FileName "HealthChecker-Analyzer" $files = Get-HealthCheckFilesItemsFromLocation $fullPaths = Get-OnlyRecentUniqueServersXMLs $files $importData = Import-MyData -FilePaths $fullPaths $analyzedResults = @() foreach ($serverData in $importData) { $analyzedServerResults = Start-AnalyzerEngine -HealthServerObject $serverData.HealthCheckerExchangeServer Write-ResultsToScreen -ResultsToWrite $analyzedServerResults.DisplayResults $analyzedResults += $analyzedServerResults } Create-HtmlServerReport -AnalyzedHtmlServerValues $analyzedResults.HtmlServerValues return } Set-ScriptLogFileLocation -FileName "HealthCheck" -IncludeServerName $true Test-RequiresServerFqdn Write-HealthCheckerVersion [HealthChecker.HealthCheckerExchangeServer]$HealthObject = Get-HealthCheckerExchangeServer $analyzedResults = Start-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-VerboseOutput("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 = New-LoggerObject -LogName "HealthChecker-Debug" -LogDirectory $OutputFilePath -VerboseEnabled $true -EnableDateTime $false -ErrorAction SilentlyContinue Main } finally { Get-ErrorsThatOccurred if($Script:VerboseEnabled) { $Host.PrivateData.VerboseForegroundColor = $VerboseForeground } $Script:Logger.RemoveLatestLogFile() if($Script:Logger.PreventLogCleanup) { Write-Host("Output Debug file written to {0}" -f $Script:Logger.FullPath) } }