0% found this document useful (0 votes)
8 views134 pages

Exchange Extended Protection Script

This document outlines a PowerShell script for enabling Extended Protection on Exchange servers, which is a security feature that helps prevent Man-in-the-Middle (MiTM) attacks. It includes parameters for configuring, validating, and rolling back changes, as well as examples of usage. The script is provided under the MIT License, allowing free use and modification with certain conditions.

Uploaded by

Malik Muzaffar
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views134 pages

Exchange Extended Protection Script

This document outlines a PowerShell script for enabling Extended Protection on Exchange servers, which is a security feature that helps prevent Man-in-the-Middle (MiTM) attacks. It includes parameters for configuring, validating, and rolling back changes, as well as examples of usage. The script is provided under the MIT License, allowing free use and modification with certain conditions.

Uploaded by

Malik Muzaffar
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

<#

MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy


of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
#>

# Version 25.04.17.1814

<#
.SYNOPSIS
This script enables extended protection on all Exchange servers in the forest.
.DESCRIPTION
The Script does the following by default.
1. Enables Extended Protection to the recommended value for the
corresponding virtual directory and site.
Extended Protection is a windows security feature which blocks MiTM attacks.
.PARAMETER RollbackType
Use this parameter to execute a Rollback Type that should be executed.
.EXAMPLE
PS C:\> .\ExchangeExtendedProtectionManagement.ps1
This will run the default mode which does the following:
1. It will set Extended Protection to the recommended value for the
corresponding virtual directory and site on all Exchange Servers in the forest.
.EXAMPLE
PS C:\> .\ExchangeExtendedProtectionManagement.ps1 -ExchangeServerNames
<Array_of_Server_Names>
This will set the Extended Protection to the recommended value for the
corresponding virtual directory and site on all Exchange Servers provided in
ExchangeServerNames
.EXAMPLE
PS C:\> .\ExchangeExtendedProtectionManagement.ps1 -SkipExchangeServerNames
<Array_of_Server_Names>
This will set the Extended Protection to the recommended value for the
corresponding virtual directory and site on all Exchange Servers in the forest
except the Exchange Servers whose names are provided in the SkipExchangeServerNames
parameter.
.EXAMPLE
PS C:\> .\ExchangeExtendedProtectionManagement.ps1 -RollbackType
"RestoreIISAppConfig"
This will set the [Link] file back to the original state prior
to changes made with this script.
This is a legacy version of the restore process. The backup process will no
longer attempt to copy out the [Link] file.
It is recommended to use "RestoreConfiguration" moving forward.
.EXAMPLE
PS C:\> .\ExchangeExtendedProtectionManagement.ps1 -RollbackType
"RestoreConfiguration"
This will restore all the various configuration changes that did occur to the
original setting when trying to configure Extended Protection without the
mitigation with this script. A rollback can not occur if a configuration attempt
was never done.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]

param(
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'ConfigureMitigation', HelpMessage = "Enter the list of server names on which the
script should execute on")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'ValidateMitigation', HelpMessage = "Enter the list of server names on which the
script should execute on")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'Rollback', HelpMessage = "Using this parameter will allow you to rollback using
the type you specified.")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'ConfigureEP', HelpMessage = "Enter the list of server names on which the script
should execute on")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName = 'ShowEP',
HelpMessage = "Enter the list of server names on which the script should execute
on")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'DisableEP', HelpMessage = "Enter the list of server names on which the script
should execute on")]
[Parameter (Mandatory = $false, ValueFromPipeline, ParameterSetName =
'PrerequisitesCheckOnly', HelpMessage = "Enter the list of server names on which
the script should execute on")]
[string[]]$ExchangeServerNames = $null,

[Parameter (Mandatory = $false, ParameterSetName = 'ConfigureMitigation',


HelpMessage = "Enter the list of servers on which the script should not execute
on")]
[Parameter (Mandatory = $false, ParameterSetName = 'ValidateMitigation',
HelpMessage = "Enter the list of servers on which the script should not execute
on")]
[Parameter (Mandatory = $false, ParameterSetName = 'Rollback', HelpMessage =
"Using this parameter will allow you to rollback using the type you specified.")]
[Parameter (Mandatory = $false, ParameterSetName = 'ConfigureEP', HelpMessage =
"Enter the list of servers on which the script should not execute on")]
[Parameter (Mandatory = $false, ParameterSetName = 'ShowEP', HelpMessage =
"Enter the list of servers on which the script should not execute on")]
[Parameter (Mandatory = $false, ParameterSetName = 'DisableEP', HelpMessage =
"Enter the list of servers on which the script should not execute on")]
[Parameter (Mandatory = $false, ParameterSetName = 'PrerequisitesCheckOnly',
HelpMessage = "Enter the list of servers on which the script should not execute
on")]
[string[]]$SkipExchangeServerNames = $null,

[Parameter (Mandatory = $true, ParameterSetName = 'ShowEP', HelpMessage =


"Enable to provide a result of the configuration for Extended Protection")]
[switch]$ShowExtendedProtection,
[Parameter (Mandatory = $true, ParameterSetName = "PrerequisitesCheckOnly",
HelpMessage = "Enable to check if the set of servers that you have provided will
pass the prerequisites check.")]
[switch]$PrerequisitesCheckOnly,

[Parameter (Mandatory = $false, ParameterSetName = 'ConfigureEP', HelpMessage =


"Used for internal options")]
[string]$InternalOption,

[Parameter (Mandatory = $false, ParameterSetName = 'ConfigureEP', HelpMessage =


"Used to not enable Extended Protection on particular virtual directories")]
[ValidateSet("EWSFrontEnd")]
[string[]]$ExcludeVirtualDirectories,

[Parameter (Mandatory = $true, ParameterSetName = 'GetExchangeIPs', HelpMessage


= "Using this parameter will allow you to get the list of IPs used by Exchange
Servers.")]
[switch]$FindExchangeServerIPAddresses,

[Parameter (Mandatory = $false, ParameterSetName = 'GetExchangeIPs',


HelpMessage = "Using this parameter will allow you to specify the path to the
output file.")]
[ValidateScript({
(Test-Path -Path $_ -IsValid) -and ([string]::IsNullOrEmpty((Split-Path
-Parent $_)) -or (Test-Path -Path (Split-Path -Parent $_)))
})]
[string]$OutputFilePath = [[Link]]::Combine((Get-Location).Path,
"[Link]"),

[Parameter (Mandatory = $true, ParameterSetName = 'ConfigureMitigation',


HelpMessage = "Using this parameter will allow you to specify a txt file with IP
range that will be used to apply IP filters.")]
[Parameter (Mandatory = $true, ParameterSetName = 'ValidateMitigation',
HelpMessage = "Using this parameter will allow you to specify a txt file with IP
range that will be used to validate IP filters.")]
[ValidateScript({
(Test-Path -Path $_)
})]
[string]$IPRangeFilePath,

[Parameter (Mandatory = $true, ParameterSetName = 'ConfigureMitigation',


HelpMessage = "Using this parameter will allow you to specify the site and VDir on
which you want to configure mitigation.")]
[ValidateSet('EWSBackend')]
[ValidateScript({
($null -ne $_) -and ($_.Length -gt 0)
})]
[string[]]$RestrictType,

[Parameter (Mandatory = $true, ParameterSetName = 'ValidateMitigation',


HelpMessage = "Using this switch will allow you to validate if the mitigations have
been applied correctly.")]
[ValidateSet('RestrictTypeEWSBackend')]
[ValidateScript({
($null -ne $_) -and ($_.Length -gt 0)
})]
[string[]]$ValidateType,
[Parameter (Mandatory = $true, ParameterSetName = 'Rollback', HelpMessage =
"Using this parameter will allow you to rollback using the type you specified.")]
[ValidateSet('RestrictTypeEWSBackend', 'RestoreIISAppConfig',
'RestoreConfiguration')]
[string[]]$RollbackType,

[Parameter (Mandatory = $true, ParameterSetName = "DisableEP", HelpMessage =


"Using this parameter will disable extended protection only for the servers you
specified.")]
[switch]$DisableExtendedProtection,

[Parameter (Mandatory = $false, HelpMessage = "Using this switch will prevent


the script from checking for an updated version.")]
[switch]$SkipAutoUpdate
)

begin {

function Write-VerboseLog ($Message) {


$Script:Logger = $Script:Logger | Write-LoggerInstance $Message
}

function Write-HostLog ($Message) {


$Script:Logger = $Script:Logger | Write-LoggerInstance $Message
}

function Invoke-CatchActionError {
[CmdletBinding()]
param(
[ScriptBlock]$CatchActionFunction
)

if ($null -ne $CatchActionFunction) {


& $CatchActionFunction
}
}

function Invoke-CatchActionErrorLoop {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[int]$CurrentErrors,
[Parameter(Mandatory = $false, Position = 1)]
[ScriptBlock]$CatchActionFunction
)
process {
if ($null -ne $CatchActionFunction -and
$[Link] -ne $CurrentErrors) {
$i = 0
while ($i -lt ($[Link] - $currentErrors)) {
& $CatchActionFunction $Error[$i]
$i++
}
}
}
}
# Common method used to handle Invoke-Command within a script.
# Avoids using Invoke-Command when running locally on a server.
function Invoke-ScriptBlockHandler {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$ComputerName,

[Parameter(Mandatory = $true)]
[ScriptBlock]
$ScriptBlock,

[string]
$ScriptBlockDescription,

[object]
$ArgumentList,

[bool]
$IncludeNoProxyServerOption,

[ScriptBlock]
$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($[Link])"
$returnValue = $null
$currentErrors = $null
}
process {

if (-not([string]::IsNullOrEmpty($ScriptBlockDescription))) {
Write-Verbose "Description: $ScriptBlockDescription"
}

try {

if (($ComputerName).Split(".")[0] -ne $env:COMPUTERNAME) {

$params = @{
ComputerName = $ComputerName
ScriptBlock = $ScriptBlock
ErrorAction = "Stop"
}

if ($IncludeNoProxyServerOption) {
Write-Verbose "Including SessionOption"
$[Link]("SessionOption", (New-PSSessionOption -
ProxyAccessType NoProxyServer))
}

if ($null -ne $ArgumentList) {


Write-Verbose "Running Invoke-Command with argument list"
$[Link]("ArgumentList", $ArgumentList)
} else {
Write-Verbose "Running Invoke-Command without argument list"
}
$returnValue = Invoke-Command @params
} else {
# Handle possible errors when executed locally.
$currentErrors = $[Link]

if ($null -ne $ArgumentList) {


Write-Verbose "Running Script Block Locally with argument list"

# if an object array type expect the result to be multiple


parameters
if ($[Link]().Name -eq "Object[]") {
$returnValue = & $ScriptBlock @ArgumentList
} else {
$returnValue = & $ScriptBlock $ArgumentList
}
} else {
Write-Verbose "Running Script Block Locally without argument
list"
$returnValue = & $ScriptBlock
}

Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction


}
} catch {
Write-Verbose "Failed to run $($[Link]) -
$ScriptBlockDescription"

# Possible that locally we hit multiple errors prior to bailing out.


if ($null -ne $currentErrors) {
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction
} else {
Invoke-CatchActionError $CatchActionFunction
}
}
}
end {
Write-Verbose "Exiting: $($[Link])"
return $returnValue
}
}

function WriteErrorInformationBase {
[CmdletBinding()]
param(
[object]$CurrentError = $Error[0],
[ValidateSet("Write-Host", "Write-Verbose")]
[string]$Cmdlet
)

if ($null -ne $[Link]) {


& $Cmdlet "Error Origin Info: $($[Link]())"
}

& $Cmdlet "$($[Link]) : $


($[Link]())"

if ($null -ne $[Link] -and


$null -ne $[Link]) {
& $Cmdlet "Inner Exception: $($[Link])"
} elseif ($null -ne $[Link]) {
& $Cmdlet "Inner Exception: $($[Link])"
}

if ($null -ne $[Link]) {


& $Cmdlet "Position Message: $
($[Link])"
}

if ($null -ne
$[Link]) {
& $Cmdlet "Remote Position Message: $
($[Link])"
}

if ($null -ne $[Link]) {


& $Cmdlet "Script Stack: $($[Link])"
}
}

function Write-VerboseErrorInformation {
[CmdletBinding()]
param(
[object]$CurrentError = $Error[0]
)
WriteErrorInformationBase $CurrentError "Write-Verbose"
}

function Write-HostErrorInformation {
[CmdletBinding()]
param(
[object]$CurrentError = $Error[0]
)
WriteErrorInformationBase $CurrentError "Write-Host"
}

function Invoke-ConfigureMitigation {
[OutputType([[Link]])]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$ExchangeServers,
[Parameter(Mandatory = $true)]
[object[]]$IPRangeAllowListRules ,
[Parameter(Mandatory = $true)]
[string[]]$SiteVDirLocations
)

begin {
$FailedServersFilter = @{}
$UnchangedFilterServers = @{}

$progressParams = @{
Activity = "Applying IP filtering Rules"
Status = [string]::Empty
PercentComplete = 0
}

Write-Verbose "Calling: $($[Link])"


$ConfigureMitigation = {
param(
[Object]$Arguments
)

$SiteVDirLocations = $[Link]
$IpRangesForFiltering = $[Link]
$WhatIf = $[Link]

$results = @{
IsWindowsFeatureInstalled = $false
IsGetLocalIPSuccessful = $false
LocalIPs = New-Object
'[Link][string]'
ErrorContext = $null
}

function BackupCurrentIPFilteringRules {
param(
[Parameter(Mandatory = $true)]
[string]$BackupPath,
[Parameter(Mandatory = $true)]
[string]$Filter,
[Parameter(Mandatory = $true)]
[string]$IISPath,
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation,
[Parameter(Mandatory = $false)]
[object[]]$ExistingRules
)

$DefaultForUnspecifiedIPs = Get-WebConfigurationProperty -Filter


$Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted"
if ($null -eq $ExistingRules) {
$ExistingRules = New-Object
'[Link][object]'
}

$BackupFilteringConfiguration = @{Rules=$ExistingRules;
DefaultForUnspecifiedIPs=$DefaultForUnspecifiedIPs }
if (-not $WhatIf) {
$BackupFilteringConfiguration | ConvertTo-Json -Depth 2 | Out-
File $BackupPath
}

return $true
}

function GetLocalIPAddresses {
$ips = New-Object '[Link][string]'
$interfaces = Get-NetIPAddress -ErrorAction Stop
foreach ($interface in $interfaces) {
if ($[Link] -eq 'Preferred') {
$ips += $[Link]
}
}

return $ips
}

# Create IP allow list from user provided IP subnets


function CreateIPRangeAllowList {
param (
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation,
[Parameter(Mandatory = $true)]
[object[]]$IpFilteringRules,
[Parameter(Mandatory = $true)]
[Hashtable] $state
)

$backupPath = "$($env:WINDIR)\System32\inetSrv\config\
IpFilteringRules_" + $[Link]('/', '-') + "_$
([DateTime]::[Link]("yyyyMMddHHMMss")).bak"
$Filter = '[Link]/security/ipSecurity'
$IISPath = 'IIS:\'
$ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter -
Location $SiteVDirLocation -Name collection)
$[Link] = BackupCurrentIPFilteringRules -
BackupPath $backupPath -Filter $Filter -IISPath $IISPath -SiteVDirLocation
$SiteVDirLocation -ExistingRules $ExistingRules

$RulesToBeAdded = @()

foreach ($IpFilteringRule in $IpFilteringRules) {


$ExistingIPSubnetRule = $ExistingRules | Where-Object
{ $_.ipAddress -eq $[Link] -and
($_.subnetMask -eq $[Link] -or
$[Link] -eq "Single IP")
}

if ($null -eq $ExistingIPSubnetRule) {


if ($[Link] -eq "Single IP") {
$RulesToBeAdded += @{ipAddress=$[Link];
allowed=$[Link]; }
} else {
$RulesToBeAdded += @{ipAddress=$[Link];
subnetMask=$[Link]; allowed=$[Link]; }
}
} else {
if ($[Link] -ne
$[Link]) {
if ($[Link] -eq "Single IP") {
$IpString = $[Link]
} else {
$IpString = ("{0}/{1}" -f $[Link],
$[Link])
}

$[Link] += $IpString
}
}
}

if ($[Link] + $[Link] -gt 500) {


$[Link] += $RulesToBeAdded
throw 'Too many IP filtering rules (Existing rules [$
($[Link])] + New rules [$($[Link])] > 500). Please
reduce the specified entries by providing appropriate subnets.'
}

if ($[Link] -gt 0) {
$[Link] = $true
Add-WebConfigurationProperty -Filter $Filter -PSPath $IISPath
-Location $SiteVDirLocation -Name "." -Value $RulesToBeAdded -ErrorAction Stop -
WhatIf:$WhatIf
}

$[Link] = $true

# Setting default to deny


Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -
Location $SiteVDirLocation -Name "allowUnlisted" -Value $false -WhatIf:$WhatIf
$[Link] = $true
}

try {
try {
$baseError = "Installation of IP and Domain filtering Module
failed."
$InstallResult = Install-WindowsFeature Web-IP-Security -
ErrorAction Stop -WhatIf:$WhatIf
if (-not $[Link]) {
throw $baseError
}
} catch {
throw "$baseError Inner exception: $_"
}

$[Link] = $true

$localIPs = GetLocalIPAddresses
$[Link] = $true

foreach ($localIP in $localIPs) {


if ($null -eq ($IpRangesForFiltering | Where-Object { $_.Type -
eq "Single IP" -and $_.IP -eq $localIP })) {
$IpRangesForFiltering += @{Type="Single IP"; IP=$localIP;
Allowed=$true }
}
}

$[Link] = $localIPs
foreach ($SiteVDirLocation in $SiteVDirLocations) {
$state = @{
IsBackUpSuccessful = $false
IsCreateIPRulesSuccessful = $false
IsSetDefaultRuleSuccessful = $false
ErrorContext = $null
IPsNotAdded = New-Object
'[Link][string]'
AreIPRulesModified = $false
}

try {
CreateIPRangeAllowList -SiteVDirLocation $SiteVDirLocation
-IpFilteringRules $IpRangesForFiltering -state $state
} catch {
$[Link] = $_
}

$results[$SiteVDirLocation] = $state
}
} catch {
$[Link] = $_
}

return $results
}
} process {
$ScriptBlockArgs = [PSCustomObject]@{
SiteVDirLocations = $SiteVDirLocations
IpRangesForFiltering = $IPRangeAllowListRules
PassedWhatIf = $WhatIfPreference
}

$counter = 0
$totalCount = $[Link]

if ($null -eq $IPRangeAllowListRules ) {


$IPRangeAllowListString = "null"
} else {
$IPStrings = @()
$IPRangeAllowListRules | ForEach-Object {
if ($_.Type -eq "Single IP") {
$IPStrings += $_.IP
} else {
$IPStrings += ("{0}/{1}" -f $_.IP, $_.SubnetMask)
}
}
$IPRangeAllowListString = [string]::Join(", ", $IPStrings)
}

$SiteVDirLocations | ForEach-Object {
$FailedServersFilter[$_] = New-Object
'[Link][string]'
$UnchangedFilterServers[$_] = New-Object
'[Link][string]'
}

foreach ($Server in $ExchangeServers) {


$baseStatus = "Processing: $Server -"
$[Link] = ($counter / $totalCount * 100)
$[Link] = "$baseStatus Applying rules"
Write-Progress @progressParams
$counter ++

Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with


arguments SiteVDirLocation: {1}, IPRangeAllowListRules : {2}" -f $Server,
$SiteVDirLocation, $IPRangeAllowListString)
$resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -
ScriptBlock $ConfigureMitigation -ArgumentList $ScriptBlockArgs

Write-Verbose ("Adding IP Restriction rules on Server {0}" -f $Server)


if ($[Link]) {
Write-Verbose ("Successfully installed windows feature - Web-IP-
Security on server {0}" -f $Server)
} else {
Write-Host ("Script failed to install windows feature - Web-IP-
Security on server {0} with the Inner Exception:" -f $Server) -ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}

if ($[Link]) {
Write-Verbose ("Successfully retrieved local IPs for the server")
if ($null -ne $[Link] -and
$[Link] -gt 0) {
Write-Verbose ("Local IPs detected for this server: {0}" -f
[string]::Join(", ", [string[]]$[Link]))
} else {
Write-Verbose ("No Local IPs detected for this server")
}
} else {
Write-Host ("Script failed to retrieve local IPs for server {0}.
Reapply IP filtering on server. Inner Exception:" -f $Server) -ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}

foreach ($SiteVDirLocation in $SiteVDirLocations) {


$state = $resultsInvoke[$SiteVDirLocation]

if ($[Link]) {
Write-Verbose ("Successfully backed up IP filtering allow list
for VDir $SiteVDirLocation on server $Server")
} else {
Write-Host ("Script failed to backup IP filtering allow list
for VDir $SiteVDirLocation on server $Server with the Inner Exception:") -
ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}

if ($[Link]) {
if ($[Link] -gt 0) {
$line = ("Some IPs provided in the IPRange file were
present in deny rules, hence these IPs were not added in the Allow List for VDir
$SiteVDirLocation on server $Server. If you wish to add these IPs in allow list,
remove these IPs from deny list in module name and reapply IP restrictions again.")
Write-Warning ($line + "Check logs for further details.")
Write-Verbose $line
Write-Verbose ([string]::Join(", ", $[Link]))
}

if (-not $[Link]) {
Write-Verbose ("No changes were made to IP filtering rules
for VDir $SiteVDirLocation on server $Server")
$UnchangedFilterServers[$SiteVDirLocation] += $Server
} else {
Write-Host ("Successfully updated IP filtering allow list
for VDir $SiteVDirLocation on server $Server")
}
} else {
Write-Host ("Script failed to update IP filtering allow list
for VDir $SiteVDirLocation on server $Server with the Inner Exception:") -
ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}

if ($[Link]) {
Write-Verbose ("Successfully set the default IP filtering rule
to deny for VDir $SiteVDirLocation on server $Server")
} else {
Write-Host ("Script failed to set the default IP filtering rule
to deny for VDir $SiteVDirLocation on server $Server with the Inner Exception:") -
ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}
}
}
} end {
foreach ($SiteVDirLocation in $SiteVDirLocations) {
if ($FailedServersFilter[$SiteVDirLocation].Length -gt 0) {
Write-Host ("Unable to create IP Filtering Rules for VDir
$SiteVDirLocation on the following servers: {0}" -f [string]::Join(", ",
$FailedServersFilter[$SiteVDirLocation])) -ForegroundColor Red
}

if ($UnchangedFilterServers[$SiteVDirLocation].Length -gt 0) {
Write-Host ("IP Restrictions are applied. No changes made in IP
Restriction rules for VDir $SiteVDirLocation in : {0}" -f [string]::Join(", ",
$UnchangedFilterServers[$SiteVDirLocation]))
}
}
}
}

<#
.DESCRIPTION
Use this function to pass in a hashtable object that you were going to splat to
a cmdlet.
It will return a string value of the parameters that are going to be
passed to the cmdlet as if you typed it out manually.
#>
function Get-ParameterString {
[CmdletBinding()]
param(
[hashtable]$InputObject
)
process {
$value = [string]::Empty

foreach ($key in $[Link]) {


$value += "-$key `"$($InputObject[$key])`" "
}
return $[Link]()
}
}

<#
.DESCRIPTION
Creates the configuration action object and validates the parameters that is
added to it.
#>
function New-IISConfigurationAction {

[[Link]('PSUseShouldProcessForStateChang
ingFunctions', '', Justification = 'No state change.')]
[CmdletBinding()]
param(
# A PSCustomObject that contains a property of [string]Cmdlet and
[hashtable]Parameters that is required.
# Cmdlet is the one that you are going to use and Parameters is what is
passed to the cmdlet.
# An optional property is a description of the action
[Parameter(Mandatory = $true)]
[object]$Action,

[string]$OverrideErrorAction = "Stop",

[bool]$OverrideWhatIf = $WhatIfPreference
)
begin {

if (([string]::IsNullOrEmpty($[Link])) -or
$null -eq $[Link] -or
$[Link]().Name -ne "hashtable") {
throw "Invalid Action parameter provided"
}

$[Link]["ErrorAction"] = $OverrideErrorAction
$[Link]["WhatIf"] = $OverrideWhatIf
$cmdParameters = $[Link]
Write-Verbose "Provided Action Cmdlet: '$($[Link])' Parameters: '$
(Get-ParameterString $cmdParameters)'"
$setWebConfigPropCmdlet = "Set-WebConfigurationProperty"
$getCurrentValueAction = $null
$restoreAction = $null
}
process {
#TODO: Validate the [Link] Pester Testing.
# Validate the Action to make sure it passes prior to trying to execute.
if ($[Link] -eq $setWebConfigPropCmdlet) {
# Set-WebConfigurationProperty requires Filter, Name, and Value.
# We will also be requiring PSPath for this.
# We currently are always using it and it should help clarify where we
are making the change at.
if (([string]::IsNullOrEmpty($cmdParameters["Filter"])) -or
([string]::IsNullOrEmpty($cmdParameters["Name"])) -or
([string]::IsNullOrEmpty($cmdParameters["Value"])) -or
([string]::IsNullOrEmpty($cmdParameters["PSPath"]))) {
throw "Invalid cmdlet parameters provided for
$setWebConfigPropCmdlet." +
" Expected value for Filter, Name, Value, and PSPath. Provided: '$
(Get-ParameterString $cmdParameters)'"
}
$currentValueActionParams = @{
Filter = $cmdParameters["Filter"]
Name = $cmdParameters["Name"]
PSPath = $cmdParameters["PSPath"]
ErrorAction = "Stop"
}

if (-not([string]::IsNullOrEmpty($cmdParameters["Location"]))) {
$[Link]("Location",
$cmdParameters["Location"])
}
$getCurrentValueAction = [PSCustomObject]@{
Cmdlet = "Get-WebConfigurationProperty"
Parameters = $currentValueActionParams
ParametersToString = (Get-ParameterString
$currentValueActionParams)
}
$restoreAction = [PSCustomObject]@{
Cmdlet = $setWebConfigPropCmdlet
Parameters = $currentValueActionParams # Should be the same, then
when executing on the server, add the value.
}
}

return [PSCustomObject]@{
Set = [PSCustomObject]@{
Cmdlet = $[Link]
Parameters = $cmdParameters
ParametersToString = (Get-ParameterString $cmdParameters)
}
Get = $getCurrentValueAction
Restore = $restoreAction
}
}
}

<#
.DESCRIPTION
Execute all the actions on the remote server. This is the script block that is
to be sent to the server.

InputObject
[array]Actions
Set
[string]ParametersToString
[hashtable]Parameters
[string]Cmdlet
Get
[string]ParametersToString
[hashtable]Parameters
[string]Cmdlet
Restore
[string]Cmdlet
[hashtable]Parameters
[string]BackupFileName
[object]Restore
[string]FileName
[bool]PassedWhatIf
#>
function Invoke-IISConfigurationRemoteAction {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[object]$InputObject
)
begin {
Write-Verbose "Calling: $($[Link])"

$isRestoreOption = $null -ne $[Link]


$errorContext = New-Object [Link][object]
$restoreActions = New-Object [Link][object]
$allActionsPerformed = $true
$gatheredAllRestoreActions = $true
$restoreActionsSaved = $isRestoreOption -eq $true
$progressCounter = 0
$backupRestoreFilePath = [string]::Empty
$loadingJson = $null
$rootSavePath = "$($env:WINDIR)\System32\inetSrv\config\"
$logFilePath = [[Link]]::Combine($rootSavePath,
"[Link]")
$restoreFileName = "IISManagementRestoreCmdlets-{0}.json"
$loggingDisabled = $false

if (-not (Test-Path $rootSavePath)) {


try {
New-Item -Path $rootSavePath -ItemType Directory -ErrorAction Stop
} catch {
Write-Verbose "Failed to create the directory for logging."
$loggingDisabled = $true
}
}

# To avoid duplicate code for log being written and keeping code in sync,
just going to do the restore option here.
# It is going to be a different property filled out on the InputObject
if ($isRestoreOption) {
$fileName = $restoreFileName -f $[Link]
$backupRestoreFilePath = [[Link]]::Combine($rootSavePath,
$fileName)
} else {
$backupProgressCounter = 0
$backupActionsCount = $[Link]
$totalActions = $[Link]

if (-not ([string]::IsNullOrEmpty($[Link]))) {
$fileName = $restoreFileName -f $[Link]
$backupRestoreFilePath = [[Link]]::Combine($rootSavePath,
$fileName)
}
}

$remoteActionProgressParams = @{
ParentId = 0
Id = 1
Activity = "Executing$(if($isRestoreOption){" Restore"}) Actions
on $env:ComputerName"
Status = [string]::Empty
PercentComplete = 0
}

function Write-VerboseAndLog {
param(
[string]$Message
)

Write-Verbose $Message

try {

if ($loggingDisabled) { return }

$Message | Out-File $logFilePath -Append -ErrorAction Stop


} catch {
# Logging shouldn't provided that configuration wasn't successful.
# Therefore, do no add to errorContext
Write-Verbose "Failed to log out file. Inner Exception: $_"
}
}

function GetLocationValue {
[CmdletBinding()]
param(
[hashtable]$CmdParameters
)

if ($null -ne $CmdParameters["Location"]) {


$location = $CmdParameters["Location"]
} else {
$location = $CmdParameters["PSPath"]
}
return $location
}
}
process {

try {
Write-VerboseAndLog "-------------------------------------------------"
Write-VerboseAndLog "Starting IIS Configuration$(if($isRestoreOption)
{ " Restore" }) Action: $([DateTime]::Now)"
Write-VerboseAndLog "-------------------------------------------------"

# Attempt to load the restore file if it exists.


if (-not ([string]::IsNullOrEmpty($backupRestoreFilePath))) {
if ((Test-Path $backupRestoreFilePath)) {
Write-VerboseAndLog "Backup file already exists, loading the
current file."

try {
$loadingJson = Get-Content $backupRestoreFilePath -
ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop

if ($null -ne $loadingJson) {


$loadingJson | ForEach-Object {
$hash = @{}
foreach ($p in $_.[Link]) {
$[Link]($[Link], $[Link])
}

$[Link]([PSCustomObject]@{
Cmdlet = $_.Cmdlet
Parameters = $hash
})
}
}
} catch {
Write-VerboseAndLog "Failed to load the current backup
file: '$backupRestoreFilePath'"
$[Link]($_)
# We should rethrow here to avoid continuing on a corrupt
backup file.
throw "Failed to load the current backup file. Inner
Exception: $_"
}
} else {
Write-VerboseAndLog "No backup file exists at:
'$backupRestoreFilePath'"
if ($isRestoreOption) {
Write-Error "Unable to restore due to no restore file.
'$backupRestoreFilePath'"
# Must throw since we need this in order to restore
throw "No restore file exists: $backupRestoreFilePath"
}
}
}

if ($isRestoreOption) {

$totalActions = $[Link]

foreach ($cmd in $restoreActions) {


try {
$commandParameters = $[Link]
$location = GetLocationValue $commandParameters
$progressCounter++
$[Link] = "Restoring settings $
($commandParameters["Name"]) at '$location'"
$[Link] =
($progressCounter / $totalActions * 100)
Write-Progress @remoteActionProgressParams

# force particular parameters


$commandParameters["ErrorAction"] = "Stop"
$commandParameters["WhatIf"] = [bool]
($[Link])

# Manual way to create param string


$paramsString = [string]::Empty

foreach ($key in $[Link]) {


$paramsString += "-$key `"$($commandParameters[$key])`"
"
}
Write-VerboseAndLog "Doing restore of cmdlet: $
($[Link]) $paramsString"

& $[Link] @commandParameters


} catch {
$allActionsPerformed = $false
Write-VerboseAndLog "Failed to restore a setting. Inner
Exception: $_"
$[Link]($_)
}
}

if ($allActionsPerformed) {
# Remove the restore file so you can't restore again.
try {
Move-Item -Path $backupRestoreFilePath -Destination
($[Link](".json", ".bak")) -Force -ErrorAction Stop
Write-VerboseAndLog "Successfully removed the restore
file."
} catch {
Write-VerboseAndLog "Failed to remove the current restore
file. Inner Exception: $_"
$[Link]($_)
$allActionsPerformed = $false
}
} else {
Write-VerboseAndLog "Not removing restore file because an issue
was detected with the restore."
}

return
}

if (-not ([string]::IsNullOrEmpty($backupRestoreFilePath))) {
Write-VerboseAndLog "Attempting to get the current value of the
action items to backup."
$totalActions = $totalActions * 2 # Double to get the current value
plus the setting.

foreach ($actionItem in $[Link]) {


try {
$backupProgressCounter++
$progressCounter++
$[Link] = "Gathering current
values. $backupProgressCounter of $backupActionsCount"
$[Link] =
($progressCounter / $totalActions * 100)
Write-Progress @remoteActionProgressParams
Write-VerboseAndLog "Working on '$($[Link])
$($[Link])"
$params = $[Link]
$currentValue = & $[Link] @params

#TODO: Need to determine if this is the correct course of


logic when not dealing with a true value or a Set-WebConfigProp
if ($null -ne $currentValue) {

# Some values will return a complete object. Only pull


out the value.
if ($null -ne $[Link]) {
$currentValue = $[Link]
}

Write-VerboseAndLog "Current value set on the server:


$currentValue"
# we want to be able to restore the original state,
prior to ever running a script that does a configuration.
# This way if any changes were done in between
executions, we will still revert back to the original state.
if ($null -ne $loadingJson) {
$parameterNames =
$[Link] | Where-Object { $_ -ne "ErrorAction" -and $_ -
ne "Value" }
$matchCmdlet = $loadingJson | Where-Object
{ $_.Cmdlet -eq $[Link] }

foreach ($restoreCmdlet in $matchCmdlet) {


$index = 0
$matchFound = $true

while ($index -lt $[Link]) {


$paramName = $parameterNames[$index]

if ($null -eq $[Link].


$paramName -or
$[Link].$paramName -
ne $[Link][$paramName]) {
$matchFound = $false
break
}
$index++
}
if ($matchFound) {
Write-VerboseAndLog "Found match, don't
overwrite setting."
break
}
}
}

if ($null -eq $loadingJson -or $matchFound -eq $false)


{
Write-VerboseAndLog "Adding restore action because
a match wasn't found."
$[Link]("Value",
$currentValue)
$[Link]($[Link])
} else {
Write-VerboseAndLog "Not adding restore action
because it was already in the list."
}
} else {
#TODO: need a test case here
throw "NULL Current Value Address Logic"
}
} catch {
Write-VerboseAndLog "Failed to collect restore actions."
$gatheredAllRestoreActions = $false
$[Link]($_)
# We don't want to continue so throw to break out.
throw "Failed to get the restore actions, therefore we are
unable to set the configuration. Inner Exception: $_"
}
}

# Save the restore information


try {
if ([string]::IsNullOrEmpty($[Link])) {
Write-VerboseAndLog "No Backup File Name Provided, so we
aren't going to backup what we have on the server."
} else {
$restoreActions | ConvertTo-Json -ErrorAction Stop -Depth 5
| Out-File $backupRestoreFilePath -ErrorAction Stop
$restoreActionsSaved = $true
Write-VerboseAndLog "Successfully saved out restore
actions."
}
} catch {
try {
# Still want to support legacy OS versions just in case
customers are still using that. The pretty version of ConvertTo-Json doesn't work.
# Need to include the compress parameter.
$restoreActions | ConvertTo-Json -ErrorAction Stop -Depth 5
-Compress | Out-File $backupRestoreFilePath -ErrorAction Stop
$restoreActionsSaved = $true
Write-VerboseAndLog "Successfully saved out restore
actions."
} catch {
Write-VerboseAndLog "Failed to Save Out the Restore
Cmdlets. Inner Exception: $_"
$[Link]($_)
throw "Failed to save out the Restore Cmdlets Inner
Exception: $_"
}
}
} else {
$restoreActionsSaved = $true # TODO: Improve logic here.
}

# Proceed to set the configuration


Write-VerboseAndLog "Setting the configuration actions"
foreach ($actionItem in $[Link]) {
try {
$commandParameters = $[Link]
$location = GetLocationValue $commandParameters
$progressCounter++
$[Link] = "Setting $
($commandParameters["Name"]) at '$location'"
$[Link] = ($progressCounter
/ $totalActions * 100)
Write-Progress @remoteActionProgressParams
Write-VerboseAndLog "Running the following: $
($[Link]) $($[Link])"

& $[Link] @commandParameters


} catch {
Write-VerboseAndLog "$($env:COMPUTERNAME): Failed to set '$
($commandParameters["Name"])' for '$location' with the value '$
($commandParameters["Value"])'. Inner Exception $_"
$allActionsPerformed = $false
$[Link]($_)
}
}
} catch {
# Catch all to make sure we return the object.
Write-VerboseAndLog "Failed to complete remote action execution. Inner
Exception: $_"
$[Link]($_)
return
}
}

end {
try {
Write-Progress @remoteActionProgressParams -Completed
} catch {
Write-VerboseAndLog "Failed to Write-Process with -Completed"
$[Link]($_)
}

Write-VerboseAndLog "Ending IIS Configuration$(if($isRestoreOption) { "


Restore"}) Action: $([DateTime]::Now)"
Write-VerboseAndLog "-------------------------------------------------"

return [PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
AllActionsPerformed = $allActionsPerformed
GatheredAllRestoreActions = $gatheredAllRestoreActions
RestoreActions = $restoreActions
RestoreActionsSaved = $restoreActionsSaved
SuccessfulExecution = $allActionsPerformed -and
$gatheredAllRestoreActions -and $restoreActionsSaved -and $[Link] -eq 0
ErrorContext = $errorContext
}
}
}

<#
.DESCRIPTION
Use this function to execute all the configuration actions against all the
servers that you would like for a particular configuration.
It will execute the Invoke-IISConfigurationRemoteAction function that is
designed to be executed locally on that server.
It will return an object that will provide if everything was configured, backed
up, or if any errors did occur.
If an error did occur, we will log it out here.
#>
function Invoke-IISConfigurationManagerAction {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object[]]$InputObject,

[string]$ConfigurationDescription = "Configure IIS"


)
begin {
Write-Verbose "Calling: $($[Link])"
$serverManagement = New-Object [Link][object]
$failedServers = New-Object [Link][object]
$successfulServers = New-Object [Link][object]
$managerActionProgressParams = @{
Id = 0
Activity = "Executing $ConfigurationDescription on Servers"
Status = [string]::Empty
PercentComplete = 0
}
}
process {
$InputObject | ForEach-Object { $[Link]($_) }
} end {

$managerActionProgressCounter = 0
$managerActionTotalActions = $[Link]

foreach ($server in $serverManagement) {


# Currently, this function is synchronous when executing on each
server. Which makes it slow in large environments.
# Would like to make this multi-threaded to improve performance.
$managerActionProgressCounter++
$[Link] = "Working on $
($[Link])"
$[Link] =
($managerActionProgressCounter / $managerActionTotalActions * 100)
Write-Progress @managerActionProgressParams
$result = Invoke-ScriptBlockHandler -ComputerName $[Link] -
ArgumentList $server -ScriptBlock ${Function:Invoke-IISConfigurationRemoteAction}

if ($null -eq $result -or


$[Link] -gt 0 -or
$[Link] -eq $false) {
$[Link]($[Link])
Write-Warning "Failed to execute request on '$
($[Link])'. NULL Result: $($null -eq $result)"

if ($[Link] -gt 0) {
Write-Warning "Error context written out to debug log."
$[Link] | ForEach-Object { Write-
VerboseErrorInformation -CurrentError $_ }
} else {
Write-Verbose "No Error Context provided."
}
} else {

if ($[Link] -gt 0) {
Write-Verbose "[$($[Link])] Restore Actions
Determined:"

$[Link] |
ForEach-Object {
Write-Verbose "$($_.Cmdlet) $(Get-ParameterString
$_.Parameters)"
}
}
$[Link]($[Link])
}
}

if ($[Link] -gt 0) {
Write-Warning "$ConfigurationDescription failed to complete for the
following servers: $([string]::Join(", ", $failedServers))"
}

if ($[Link] -gt 0) {
Write-Host "$ConfigurationDescription was successful on the following
servers: $([string]::Join(", ", $successfulServers))"
}
}
}

function Invoke-DisableExtendedProtection {
[CmdletBinding()]
param(
[string[]]$ExchangeServers
)
begin {
Write-Verbose "Calling: $($[Link])"
$counter = 0
$totalCount = $[Link]
$failedServers = New-Object '[Link][string]'
$updatedServers = New-Object '[Link][string]'
$iisConfigurationManagements = New-Object
[Link][object]
$progressParams = @{
Id = 1
Activity = "Disabling Extended Protection"
Status = [string]::Empty
PercentComplete = 0
}
}
process {
<#
We need to loop through each of the servers and set extended protection
to None for each virtual directory for exchange that we did set.
This list of virtual directories for exchange will be managed within
Get-ExtendedProtectionConfiguration.
To avoid a second list here of the names of vDirs, we will call Get-
ExtendedProtectionConfiguration for each server prior to setting EP to none.
This will result in a few calls to that server, but rather do that then
have a double list of vDirs that we want to manage.
#>

foreach ($server in $ExchangeServers) {


$counter++
$baseStatus = "Processing: $($server) -"
$[Link] = "$baseStatus Gathering Information"
$[Link] = ($counter / $totalCount * 100)
Write-Progress @progressParams

$serverExtendedProtection = Get-ExtendedProtectionConfiguration -
ComputerName $server

if (-not ($[Link])) {
Write-Warning "$($server): Server not online. Unable to execute
remotely."
$[Link]($server)
continue
}

if ($[Link] -eq
0) {
Write-Warning "$($server): Server wasn't able to collect Extended
Protection configuration."
$[Link]($server)
continue
}

#$iisConfigurationManagement = New-IISConfigurationManager -ServerName


$server
$actionList = New-Object [Link][object]

foreach ($virtualDirectory in
$[Link]) {
Write-Verbose "$($server): Virtual Directory Name: $
($[Link]) Current Set Extended Protection: $
($[Link])"
$[Link]((New-IISConfigurationAction -Action
([PSCustomObject]@{
Cmdlet = "Set-WebConfigurationProperty"
Parameters = @{
Filter =
"[Link]/security/authentication/windowsAuthentication"
Name = "[Link]"
Value = "None"
PSPath = "IIS:\"
Location =
$[Link]
}
})))
}
$[Link]([PSCustomObject]@{
ServerName = $server
Actions = $actionList
})
}
Invoke-IISConfigurationManagerAction $iisConfigurationManagements -
ConfigurationDescription "Disable Extended Protection"
}
end {
Write-Progress @progressParams -Completed
Write-Host

if ($[Link] -gt 0) {
Write-Warning "Failed to disable Extended Protection: $
([string]::Join(", ", $failedServers))"
}

if ($[Link] -gt 0) {
Write-Host "Successfully disabled Extended Protection: $
([string]::Join(",", $updatedServers))"
}
}
}
function Invoke-ValidateMitigation {
[OutputType([[Link]])]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$ExchangeServers,
[Parameter(Mandatory = $false)]
[object[]]$ipRangeAllowListRules,
[Parameter(Mandatory = $true)]
[string[]]$SiteVDirLocations
)

begin {
$FailedServersEP = @{}
$FailedServersFilter = @{}

$UnMitigatedServersEP = @{}
$UnMitigatedServersFilter = @{}

$progressParams = @{
Activity = "Verifying Mitigations"
Status = [string]::Empty
PercentComplete = 0
}

Write-Verbose "Calling: $($[Link])"

$ValidateMitigationScriptBlock = {
param(
[Object]$Arguments
)

$SiteVDirLocations = $[Link]
$IpRangesForFiltering = $[Link]

$results = @{}

function GetLocalIPAddresses {
$ips = New-Object '[Link][string]'
$interfaces = Get-NetIPAddress
foreach ($interface in $interfaces) {
if ($[Link] -eq 'Preferred') {
$ips += $[Link]
}
}

return $ips
}

# Set EP to None
function GetExtendedProtectionState {
param (
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation
)

$Filter =
'[Link]/security/authentication/windowsAuthentication/extendedProtection'

$ExtendedProtection = Get-WebConfigurationProperty -Filter $Filter


-Location $SiteVDirLocation -Name tokenChecking
return $ExtendedProtection
}

# Create IP allow list from user provided IP subnets


function VerifyIPRangeAllowList {
param (
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation,
[Parameter(Mandatory = $true)]
[object[]]$IpFilteringRules,
[Parameter(Mandatory = $true)]
[Hashtable]$state
)

$[Link] = (Get-WindowsFeature -Name "Web-


IP-Security").InstallState -eq "Installed"
$[Link] = $true

if (-not $[Link]) {
return
}

$Filter = '[Link]/security/ipSecurity'
$IISPath = 'IIS:\'

$ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter -


Location $SiteVDirLocation -Name collection)

foreach ($IpFilteringRule in $IpFilteringRules) {


$ExistingIPSubnetRule = $ExistingRules | Where-Object {
$_.ipAddress -eq $[Link] -and
($_.subnetMask -eq $[Link] -or
$[Link] -eq "Single IP") -and
$_.allowed -eq $[Link]
}

if ($null -eq $ExistingIPSubnetRule) {


if ($[Link] -eq "Single IP") {
$IpString = $[Link]
} else {
$IpString = ("{0}/{1}" -f $[Link],
$[Link])
}
$[Link] += $IpString
}
}

$[Link] = $true

$[Link] = -not ((Get-WebConfigurationProperty -


Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name
"allowUnlisted").Value)
$[Link] = $true
}
foreach ($SiteVDirLocation in $SiteVDirLocations) {
try {
$state = @{
IsEPVerified = $false
IsEPOff = $false
IsWindowsFeatureInstalled = $false
IsWindowsFeatureVerified = $false
AreIPRulesVerified = $false
IsDefaultFilterVerified = $false
IsDefaultFilterDeny = $false
RulesNotFound = New-Object
'[Link][string]'
ErrorContext = $null
}

$EPState = GetExtendedProtectionState -SiteVDirLocation


$SiteVDirLocation
if ($EPState -eq "None") {
$[Link] = $true
} else {
$[Link] = $false
}

$[Link] = $true

if ($null -ne $IpRangesForFiltering) {


$localIPs = GetLocalIPAddresses

$localIPs | ForEach-Object {
$IpRangesForFiltering += @{Type="Single IP"; IP=$_;
Allowed=$true }
}

VerifyIPRangeAllowList -SiteVDirLocation $SiteVDirLocation


-IpFilteringRules $IpRangesForFiltering -state $state
}
} catch {
$[Link] = $_
}

$results[$SiteVDirLocation] = $state
}

return $results
}
} process {
$ScriptBlockArgs = [PSCustomObject]@{
SiteVDirLocations = $SiteVDirLocations
IpRangesForFiltering = $ipRangeAllowListRules
}

$counter = 0
$totalCount = $[Link]
if ($null -eq $ipRangeAllowListRules) {
$ipRangeAllowListString = "null"
} else {
$ipRangeAllowListString = [string]::Join(", ", $ipRangeAllowListRules)
}
$SiteVDirLocations | ForEach-Object {
$FailedServersEP[$_] = New-Object
'[Link][string]'
$FailedServersFilter[$_] = New-Object
'[Link][string]'

$UnMitigatedServersEP[$_] = New-Object
'[Link][string]'
$UnMitigatedServersFilter[$_] = New-Object
'[Link][string]'
}

foreach ($Server in $ExchangeServers) {


$baseStatus = "Processing: $Server -"
$[Link] = ($counter / $totalCount * 100)
$[Link] = "$baseStatus Validating rules"
Write-Progress @progressParams
$counter ++

Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with


arguments SiteVDirLocations: {1}, ipRangeAllowListRules: {2}" -f $Server,
[string]::Join(", ", $SiteVDirLocations), $ipRangeAllowListString)
$resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -
ScriptBlock $ValidateMitigationScriptBlock -ArgumentList $ScriptBlockArgs

if ($null -eq $resultsInvoke) {


$line = "Server Unreachable: Unable to validate IP filtering rules
on server $($Server)."
Write-Verbose $line
Write-Warning $line
$SiteVDirLocations | ForEach-Object
{ $FailedServersEP[$_].Add($Server) }
$SiteVDirLocations | ForEach-Object
{ $FailedServersFilter[$_].Add($Server) }
continue
}

foreach ($SiteVDirLocation in $SiteVDirLocations) {


$state = $resultsInvoke[$SiteVDirLocation]

if ($[Link]) {
Write-Verbose ("Expected: The state of Extended protection flag
is None for VDir $($SiteVDirLocation) on server $Server")
} elseif ($[Link]) {
Write-Verbose ("Unexpected: The state of Extended protection
flag is not set to None for VDir $($SiteVDirLocation) on server $Server")
$UnMitigatedServersEP[$SiteVDirLocation] += $Server
} else {
Write-Host ("Unknown: Script failed to get state of Extended
protection flag for VDir $($SiteVDirLocation) with Inner Exception") -
ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersEP[$SiteVDirLocation] += $Server
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}

$IsFilterUnMitigated = $false
if (-not $[Link]) {
Write-Host ("Unknown: Script failed to verify if the Windows
feature Web-IP-Security is present for VDir $($SiteVDirLocation) on server $Server
with Inner Exception") -ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
} elseif (-not $[Link]) {
Write-Verbose ("Unexpected: Windows feature Web-IP-Security is
not present on the server for VDir $($SiteVDirLocation) on server $Server")
$IsFilterUnMitigated = $true
} else {
Write-Verbose ("Expected: Successfully verified that the
Windows feature Web-IP-Security is present on the server for VDir $
($SiteVDirLocation) on server $Server")
if (-not $[Link]) {
Write-Host ("Unknown: Script failed to verify IP Filtering
Rules for VDir $($SiteVDirLocation) on server $Server with Inner Exception") -
ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
} elseif ($null -ne $[Link] -and
$[Link] -gt 0) {
Write-Verbose ("Unexpected: Some or all the rules present
in the file specified aren't applied for VDir $($SiteVDirLocation) on server
$Server")
Write-Verbose ("Following Rules weren't found: {0}" -f
[string]::Join(", ", [string[]]$[Link]))
$IsFilterUnMitigated = $true
} else {
Write-Verbose ("Expected: Successfully verified all the IP
filtering rules for VDir $($SiteVDirLocation) on server $Server")
}

if ($[Link]) {
Write-Verbose ("Expected: The default IP Filtering rule is
set to deny for VDir $($SiteVDirLocation) on server $Server")
} elseif ($[Link]) {
Write-Verbose ("Unexpected: The default IP Filtering rule
is not set to deny for VDir $($SiteVDirLocation) on server $Server")
$IsFilterUnMitigated = $true
} else {
Write-Host ("Unknown: Script failed to get the default IP
Filtering rule for VDir $($SiteVDirLocation) on server $Server with Inner
Exception") -ForegroundColor Red
Write-HostErrorInformation $[Link]
$FailedServersFilter[$SiteVDirLocation] += $Server
continue
}
}

if ($IsFilterUnMitigated) {
$UnMitigatedServersFilter[$SiteVDirLocation] += $Server
}
}
}
} end {
$FoundFailedOrUnmitigated = $false
foreach ($SiteVDirLocation in $SiteVDirLocations) {
if ($UnMitigatedServersEP[$SiteVDirLocation].Length -gt 0) {
Write-Host ("Extended Protection on the following servers are not
set to expected values for VDir {0}: {1}" -f $SiteVDirLocation, [string]::Join(",
", $UnMitigatedServersEP[$SiteVDirLocation])) -ForegroundColor Red
$FoundFailedOrUnmitigated = $true
}

if ($UnMitigatedServersFilter[$SiteVDirLocation].Length -gt 0) {
Write-Host ("IP Filtering Rules or Default IP rule on the following
servers does not contain all the IP Ranges/addresses provided for validation in
VDir {0}: {1}" -f $SiteVDirLocation, [string]::Join(", ",
$UnMitigatedServersFilter[$SiteVDirLocation])) -ForegroundColor Red
$FoundFailedOrUnmitigated = $true
}

if ($FailedServersEP[$SiteVDirLocation].Length -gt 0) {
Write-Host ("Unable to verify Extended Protection on the following
servers for VDir {0}: {1}" -f $SiteVDirLocation, [string]::Join(", ",
$FailedServersEP[$SiteVDirLocation])) -ForegroundColor Red
$FoundFailedOrUnmitigated = $true
}

if ($FailedServersFilter[$SiteVDirLocation].Length -gt 0) {
Write-Host ("Unable to verify IP Filtering Rules on the following
servers for VDir {0}: {1}" -f $SiteVDirLocation, [string]::Join(", ",
$FailedServersFilter[$SiteVDirLocation])) -ForegroundColor Red
$FoundFailedOrUnmitigated = $true
}
}

if (-not $FoundFailedOrUnmitigated) {
Write-Host "All the servers have been validated successfully!" -
ForegroundColor Green
}
}
}

function Invoke-RollbackIPFiltering {
[OutputType([[Link]])]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[object[]]$ExchangeServers,
[Parameter(Mandatory = $true)]
[string[]]$SiteVDirLocations
)

begin {
Write-Verbose "Calling: $($[Link])"
$FailedServers = @{}

$progressParams = @{
Activity = "Rolling back IP filtering Rules"
Status = [string]::Empty
PercentComplete = 0
}
$RollbackIPFiltering = {
param(
[Object]$Arguments
)

$SiteVDirLocations = $[Link]
$WhatIf = $[Link]
$Filter = '[Link]/security/ipSecurity'
$FilterEP =
'[Link]/security/authentication/windowsAuthentication'
$IISPath = 'IIS:\'

$results = @{}

function BackupCurrentIPFilteringRules {
param(
[Parameter(Mandatory = $true)]
[string]$BackupPath,
[Parameter(Mandatory = $true)]
[string]$Filter,
[Parameter(Mandatory = $true)]
[string]$IISPath,
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation,
[Parameter(Mandatory = $false)]
[[Link][object]]$ExistingRules
)

$DefaultForUnspecifiedIPs = Get-WebConfigurationProperty -Filter


$Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted"
if ($null -eq $ExistingRules) {
$ExistingRules = New-Object
'[Link][object]'
}

$BackupFilteringConfiguration = @{Rules=$ExistingRules;
DefaultForUnspecifiedIPs=$DefaultForUnspecifiedIPs }
if (-not $WhatIf) {
$BackupFilteringConfiguration | ConvertTo-Json -Depth 2 | Out-
File $BackupPath
}

return $true
}

function RestoreOriginalIPFilteringRules {
param(
[Parameter(Mandatory = $true)]
[string]$Filter,
[Parameter(Mandatory = $true)]
[string]$IISPath,
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation,
[Parameter(Mandatory = $false)]
[object[]]$OriginalIpFilteringRules,
[Parameter(Mandatory = $true)]
[object]$DefaultForUnspecifiedIPs
)
Clear-WebConfiguration -Filter $Filter -PSPath $IISPath -Location
$SiteVDirLocation -ErrorAction Stop -WhatIf:$WhatIf
$RulesToBeAdded = New-Object
'[Link][object]'
foreach ($IpFilteringRule in $OriginalIpFilteringRules) {
$RulesToBeAdded += @{ipAddress=$[Link];
subnetMask=$[Link]; domainName=$[Link];
allowed=$[Link]; }
}
Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -
Location $SiteVDirLocation -Name "allowUnlisted" -Value
$[Link] -WhatIf:$WhatIf
if ($[Link] -gt 0) {
Add-WebConfigurationProperty -Filter $Filter -PSPath $IISPath
-Location $SiteVDirLocation -Name "." -Value $RulesToBeAdded -ErrorAction Stop -
WhatIf:$WhatIf
}

return $true
}

function TurnONExtendedProtection {
param(
[Parameter(Mandatory = $true)]
[string]$Filter,
[Parameter(Mandatory = $true)]
[string]$IISPath,
[Parameter(Mandatory = $true)]
[string]$SiteVDirLocation
)
$ExtendedProtection = Get-WebConfigurationProperty -Filter $Filter
-Location $SiteVDirLocation -Name "[Link]"
if ($ExtendedProtection -ne "Require") {
Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -
Location $SiteVDirLocation -Name "[Link]" -Value
"Require"
}
}

foreach ($SiteVDirLocation in $SiteVDirLocations) {


$state = @{
TurnOnEPSuccessful = $false
RestoreFileExists = $false
BackUpPath = $null
BackupCurrentSuccessful = $false
RestorePath = $null
RestoreSuccessful = $false
ErrorContext = $null
}
try {
$[Link] = (Get-ChildItem "$($env:WINDIR)\System32\
inetSrv\config\" -Filter ("*IpFilteringRules_"+ $[Link]('/',
'-') + "*.bak") | Sort-Object CreationTime | Select-Object -First 1).FullName
if ($null -eq $[Link]) {
throw "Invalid operation. No backup file exists at path $
($env:WINDIR)\System32\inetSrv\config\"
}
$[Link] = $true
TurnONExtendedProtection -Filter $FilterEP -IISPath $IISPath -
SiteVDirLocation $SiteVDirLocation
$[Link] = $true

$[Link] = "$($env:WINDIR)\System32\inetSrv\config\
IpFilteringRules_" + $[Link]('/', '-') + "_$
([DateTime]::[Link]("yyyyMMddHHMMss")).bak"
$ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter
-Location $SiteVDirLocation -Name collection)
$[Link] = BackupCurrentIPFilteringRules
-BackupPath $[Link] -Filter $Filter -IISPath $IISPath -SiteVDirLocation
$SiteVDirLocation -ExistingRules $ExistingRules

$originalIpFilteringConfigurations = (Get-Content
$[Link] | Out-String | ConvertFrom-Json)
$[Link] = RestoreOriginalIPFilteringRules -
OriginalIpFilteringRules ($[Link]) -
DefaultForUnspecifiedIPs
($[Link]) -Filter $Filter -
IISPath $IISPath -SiteVDirLocation $SiteVDirLocation
} catch {
$[Link] = $_
}

$results[$SiteVDirLocation] = $state
}

return $results
}
} process {
$ScriptBlockArgs = [PSCustomObject]@{
SiteVDirLocations = $SiteVDirLocations
PassedWhatIf = $WhatIfPreference
}

$exchangeServersProcessed = 0
$totalExchangeServers = $[Link]

$SiteVDirLocations | ForEach-Object {
$FailedServers[$_] = New-Object
'[Link][string]'
}

foreach ($Server in $ExchangeServers) {


$baseStatus = "Processing: $($[Link]) -"
$[Link] = ($exchangeServersProcessed /
$totalExchangeServers * 100)
$[Link] = "$baseStatus Rolling back rules"
Write-Progress @progressParams
$exchangeServersProcessed++

Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with


Arguments Site: {1}, VDir: {2}" -f $[Link], $Site, $VDir)
Write-Verbose ("Restoring previous state for Server {0}" -f
$[Link])
$resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $[Link] -
ScriptBlock $RollbackIPFiltering -ArgumentList $ScriptBlockArgs

if ($null -eq $resultsInvoke) {


$line = "Server Unreachable: Unable to rollback IP filtering rules
on server $($[Link])."
Write-Verbose $line
Write-Warning $line
$SiteVDirLocations | ForEach-Object
{ $FailedServers[$_].Add($[Link]) }
continue
}

foreach ($SiteVDirLocation in $SiteVDirLocations) {


$Failed = $false
$state = $resultsInvoke[$SiteVDirLocation]
if ($[Link]) {
if ($[Link]) {
Write-Host "Turned on Extended Protection on server $
($[Link]) for VDir $SiteVDirLocation"
if ($[Link]) {
Write-Verbose "Successfully backed up current
configuration on server $($[Link]) at $($[Link]) for VDir
$SiteVDirLocation"
if ($[Link]) {
Write-Host "Successfully rolled back IP filtering
rules on server $($[Link]) from $($[Link]) for VDir
$SiteVDirLocation"
} else {
Write-Host "Failed to rollback IP filtering rules
on server $($[Link]). Aborting rollback on the server $($[Link]) for VDir
$SiteVDirLocation. Inner Exception:" -ForegroundColor Red
Write-HostErrorInformation $[Link]
$Failed = $true
}
} else {
Write-Host "Failed to backup the current configuration
on server $($[Link]). Aborting rollback on the server $($[Link]) for VDir
$SiteVDirLocation. Inner Exception:" -ForegroundColor Red
Write-HostErrorInformation $[Link]
$Failed = $true
}
} else {
Write-Host "Failed to turn on Extended Protection on server
$($[Link]). Aborting rollback on the server $($[Link]) for VDir
$SiteVDirLocation. Inner Exception:" -ForegroundColor Red
Write-HostErrorInformation $[Link]
$Failed = $true
}
} else {
Write-Host "No restore file exists on server $($[Link]).
Aborting rollback on the server $($[Link]) for VDir $SiteVDirLocation." -
ForegroundColor Red
$Failed = $true
}

if ($Failed) {
$FailedServers[$SiteVDirLocation] += $[Link]
}
}
}
} end {
foreach ($SiteVDirLocation in $SiteVDirLocations) {
if ($FailedServers[$SiteVDirLocation].Length -gt 0) {
Write-Host ("Unable to rollback for VDir $SiteVDirLocation on the
following servers: {0}" -f [string]::Join(", ", $FailedServers[$SiteVDirLocation]))
-ForegroundColor Red
}
}
}
}

function Get-ExtendedProtectionConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ComputerName,

[Parameter(Mandatory = $false)]
[[Link]]$ApplicationHostConfig,

[Parameter(Mandatory = $false)]
[[Link]]$ExSetupVersion,

[Parameter(Mandatory = $false)]
[bool]$IsMailboxServer = $true,

[Parameter(Mandatory = $false)]
[bool]$IsClientAccessServer = $true,

[Parameter(Mandatory = $false)]
[bool]$ExcludeEWS = $false,

[Parameter(Mandatory = $false)]
[bool]$ExcludeEWSFe,

[Parameter(Mandatory = $false)]
[ValidateSet("Exchange Back End/EWS")]
[string[]]$SiteVDirLocations,

[Parameter(Mandatory = $false)]
[ScriptBlock]$CatchActionFunction
)

begin {
function NewVirtualDirMatchingEntry {
param(
[Parameter(Mandatory = $true)]
[string]$VirtualDirectory,
[Parameter(Mandatory = $true)]
[ValidateSet("Default Web Site", "Exchange Back End")]
[string[]]$WebSite,
[Parameter(Mandatory = $true)]
[ValidateSet("None", "Allow", "Require")]
[string[]]$ExtendedProtection,
# Need to define this twice once for Default Web Site and Exchange
Back End for the default values
[Parameter(Mandatory = $false)]
[string[]]$SslFlags = @("Ssl,Ssl128", "Ssl,Ssl128")
)
if ($[Link] -ne $[Link]) {
throw "Argument count mismatch on $VirtualDirectory"
}

for ($i = 0; $i -lt $[Link]; $i++) {


# special conditions for Exchange 2013
# powershell is on front and back so skip over those
if ($IsExchange2013 -and $virtualDirectory -ne "Powershell") {
# No API virtual directory
if ($virtualDirectory -eq "API") { return }
if ($IsClientAccessServer -eq $false -and $WebSite[$i] -eq
"Default Web Site") { continue }
if ($IsMailboxServer -eq $false -and $WebSite[$i] -eq "Exchange
Back End") { continue }
}
# Set EWS VDir to None for known issues
if ($ExcludeEWS -and $virtualDirectory -eq "EWS")
{ $ExtendedProtection[$i] = "None" }

# EWS FE
if ($ExcludeEWSFe -and $VirtualDirectory -eq "EWS" -and
$WebSite[$i] -eq "Default Web Site") { $ExtendedProtection[$i] = "None" }

if ($null -ne $SiteVDirLocations -and


$[Link] -gt 0) {
foreach ($SiteVDirLocation in $SiteVDirLocations) {
if ($SiteVDirLocation -eq
"$($WebSite[$i])/$virtualDirectory") {
Write-Verbose "Set Extended Protection to None because
of restriction override '$($WebSite[$i])\$virtualDirectory'"
$ExtendedProtection[$i] = "None"
break
}
}
}

[PSCustomObject]@{
VirtualDirectory = $virtualDirectory
WebSite = $WebSite[$i]
ExtendedProtection = $ExtendedProtection[$i]
SslFlags = $SslFlags[$i]
}
}
}

# Intended for inside of Invoke-Command.


function GetApplicationHostConfig {
$appHostConfig = New-Object -TypeName Xml
try {
$appHostConfigPath = "$($env:WINDIR)\System32\inetSrv\config\
[Link]"
$[Link]($appHostConfigPath)
} catch {
Write-Verbose "Failed to loaded application host config file. $_"
$appHostConfig = $null
}
return $appHostConfig
}
function GetExtendedProtectionConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[[Link]]$Xml,
[Parameter(Mandatory = $true)]
[string]$Path
)
process {
try {
$nodePath = [string]::Empty
$extendedProtection = "None"
$ipRestrictionsHashTable = @{}
$pathIndex =
[array]::IndexOf(($[Link]).ToLower(), $[Link]())
$rootIndex =
[array]::IndexOf(($[Link]).ToLower(), ($[Link]("/")
[0]).ToLower())
$parentIndex =
[array]::IndexOf(($[Link]).ToLower(), ($[Link](0,
$[Link]("/")).ToLower()))

if ($pathIndex -ne -1) {


$configNode = $[Link][$pathIndex]
$nodePath = $[Link]
$ep =
$configNode.'[Link]'.[Link]
[Link]
$ipRestrictions =
$configNode.'[Link]'.[Link]

if (-not ([string]::IsNullOrEmpty($ep))) {
Write-Verbose "Found tokenChecking: $ep"
$extendedProtection = $ep
} else {
if ($parentIndex -ne -1) {
$parentConfigNode =
$[Link][$parentIndex]
$ep =
$parentConfigNode.'[Link]'.[Link].
[Link]

if (-not ([string]::IsNullOrEmpty($ep))) {
Write-Verbose "Found tokenChecking: $ep"
$extendedProtection = $ep
} else {
Write-Verbose "Failed to find tokenChecking.
Using default value of None."
}
} else {
Write-Verbose "Failed to find tokenChecking. Using
default value of None."
}
}

[string]$sslSettings =
$configNode.'[Link]'.[Link]
if ([string]::IsNullOrEmpty($sslSettings)) {
Write-Verbose "Failed to find SSL settings for the
path. Falling back to the root."

if ($rootIndex -ne -1) {


Write-Verbose "Found root path."
$rootConfigNode =
$[Link][$rootIndex]
[string]$sslSettings =
$rootConfigNode.'[Link]'.[Link]
}
}

if (-not([string]::IsNullOrEmpty($ipRestrictions))) {
Write-Verbose "IP-filtered restrictions detected"
foreach ($restriction in $[Link]) {

$[Link]($[Link], $[Link])
}
}

Write-Verbose "SSLSettings: $sslSettings"

if ($null -ne $sslSettings) {


[array]$sslFlags =
($[Link](",").ToLower()).Trim()
} else {
$sslFlags = $null
}

# SSL flags:
[Link]
access#attributes
$requireSsl = $false
$ssl128Bit = $false
$clientCertificate = "Unknown"

if ($null -eq $sslFlags) {


Write-Verbose "Failed to find SSLFlags"
} elseif ($[Link]("none")) {
$clientCertificate = "Ignore"
} else {
if ($[Link]("ssl")) { $requireSsl = $true }
if ($[Link]("ssl128")) { $ssl128Bit =
$true }
if ($[Link]("sslNegotiateCert".ToLower())) {
$clientCertificate = "Accept"
} elseif
($[Link]("sslRequireCert".ToLower())) {
$clientCertificate = "Require"
} else {
$clientCertificate = "Ignore"
}
}
}
} catch {
Write-Verbose "Ran into some error trying to parse the
application host config for $Path."
Invoke-CatchActionError $CatchActionFunction
}
} end {
return [PSCustomObject]@{
ExtendedProtection = $extendedProtection
ValidPath = ($pathIndex -ne -1)
NodePath = $nodePath
SslSettings = [PSCustomObject]@{
RequireSsl = $requireSsl
Ssl128Bit = $ssl128Bit
ClientCertificate = $clientCertificate
Value = $sslSettings
}
MitigationSettings = [PScustomObject]@{
AllowUnlisted = $[Link]
Restrictions = $ipRestrictionsHashTable
}
}
}
}

Write-Verbose "Calling: $($[Link])"

$computerResult = Invoke-ScriptBlockHandler -ComputerName $ComputerName -


ScriptBlock { return $env:COMPUTERNAME }
$serverConnected = $null -ne $computerResult

if ($null -eq $computerResult) {


Write-Verbose "Failed to connect to server $ComputerName"
return
}

if ($null -eq $ExSetupVersion) {


[[Link]]$ExSetupVersion = Invoke-ScriptBlockHandler -
ComputerName $ComputerName -ScriptBlock {
(Get-Command [Link] |
ForEach-Object { $_.FileVersionInfo } |
Select-Object -First 1).FileVersion
}

if ($null -eq $ExSetupVersion) {


throw "Failed to determine Exchange build number"
}
} else {
# Hopefully the caller knows what they are doing, best be from the
correct server!!
Write-Verbose "Caller passed the ExSetupVersion information"
}

if ($null -eq $ApplicationHostConfig) {


Write-Verbose "Trying to load the application host config from
$ComputerName"
$params = @{
ComputerName = $ComputerName
ScriptBlock = ${Function:GetApplicationHostConfig}
CatchActionFunction = $CatchActionFunction
}

$ApplicationHostConfig = Invoke-ScriptBlockHandler @params


if ($null -eq $ApplicationHostConfig) {
throw "Failed to load application host config from $ComputerName"
}
} else {
# Hopefully the caller knows what they are doing, best be from the
correct server!!
Write-Verbose "Caller passed the application host config."
}

$default = "Default Web Site"


$backend = "Exchange Back End"
$Script:IsExchange2013 = $[Link] -eq 15 -and
$[Link] -eq 0
try {
$VirtualDirectoryMatchEntries = @(
(NewVirtualDirMatchingEntry "API" -WebSite $default, $backend -
ExtendedProtection "Require", "Require")
(NewVirtualDirMatchingEntry "Autodiscover" -WebSite $default,
$backend -ExtendedProtection "None", "None")
(NewVirtualDirMatchingEntry "ECP" -WebSite $default, $backend -
ExtendedProtection "Require", "Require")
(NewVirtualDirMatchingEntry "EWS" -WebSite $default, $backend -
ExtendedProtection "Allow", "Require")
(NewVirtualDirMatchingEntry "Microsoft-Server-ActiveSync" -WebSite
$default, $backend -ExtendedProtection "Allow", "Require")
(NewVirtualDirMatchingEntry "Microsoft-Server-ActiveSync/Proxy" -
WebSite $default, $backend -ExtendedProtection "Allow", "Require")
# This was changed due to Outlook for Mac not being able to do
download the OAB.
(NewVirtualDirMatchingEntry "OAB" -WebSite $default, $backend -
ExtendedProtection "Allow", "Require")
(NewVirtualDirMatchingEntry "Powershell" -WebSite $default,
$backend -ExtendedProtection "None", "Require" -SslFlags "SslNegotiateCert",
"Ssl,Ssl128,SslNegotiateCert")
(NewVirtualDirMatchingEntry "OWA" -WebSite $default, $backend -
ExtendedProtection "Require", "Require")
(NewVirtualDirMatchingEntry "RPC" -WebSite $default, $backend -
ExtendedProtection "Require", "Require")
(NewVirtualDirMatchingEntry "MAPI" -WebSite $default -
ExtendedProtection "Require")
(NewVirtualDirMatchingEntry "PushNotifications" -WebSite $backend -
ExtendedProtection "Require")
(NewVirtualDirMatchingEntry "RPCWithCert" -WebSite $backend -
ExtendedProtection "Require")
(NewVirtualDirMatchingEntry "MAPI/emsmdb" -WebSite $backend -
ExtendedProtection "Require")
(NewVirtualDirMatchingEntry "MAPI/nspi" -WebSite $backend -
ExtendedProtection "Require")
)
} catch {
# Don't handle with Catch Error as this is a bug in the script.
throw "Failed to create NewVirtualDirMatchingEntry. Inner Exception $_"
}

# Is Supported build of Exchange to have the configuration set.


# Edge Server is not accounted for. It is the caller's job to not try to
collect this info on Edge.
$supportedVersion = $false
$extendedProtectionList = New-Object
'[Link][object]'

if ($[Link] -eq 15) {


if ($[Link] -eq 2) {
$supportedVersion = $[Link] -gt 1118 -or
($[Link] -eq 1118 -and $[Link] -ge
11) -or
($[Link] -eq 986 -and $[Link] -ge
28)
} elseif ($[Link] -eq 1) {
$supportedVersion = $[Link] -gt 2507 -or
($[Link] -eq 2507 -and $[Link] -ge
11) -or
($[Link] -eq 2375 -and $[Link] -ge
30)
} elseif ($[Link] -eq 0) {
$supportedVersion = $[Link] -gt 1497 -or
($[Link] -eq 1497 -and $[Link] -ge
38)
}
Write-Verbose "Build $ExSetupVersion is supported: $supportedVersion"
} else {
Write-Verbose "Not on Exchange Version 15"
}

# Add all vDirs for which the IP filtering mitigation is supported


$mitigationSupportedVDirs =
$[Link]["SiteVDirLocations"].Attributes |
Where-Object { $_ -is
[[Link]] } |
ForEach-Object { return $_.ValidValues }
Write-Verbose "Supported mitigated virtual directories: $
([string]::Join(",", $mitigationSupportedVDirs))"
}
process {
try {
foreach ($matchEntry in $VirtualDirectoryMatchEntries) {
try {
Write-Verbose "Verify extended protection setting for $
($[Link]) on web site $($[Link])"

$extendedConfiguration = GetExtendedProtectionConfiguration -
Xml $applicationHostConfig -Path "$($[Link])/$
($[Link])"

# Extended Protection is a windows security feature which


blocks MiTM attacks.
# Supported server roles are: Mailbox and ClientAccess
# Possible configuration settings are:
# <None>: This value specifies that IIS will not perform
channel-binding token checking.
# <Allow>: This value specifies that channel-binding token
checking is enabled, but not required.
# <Require>: This value specifies that channel-binding token
checking is required.
#
[Link]
authentication/windowsauthentication/extendedprotection/
if ($[Link]) {
Write-Verbose "Configuration was successfully returned: $
($[Link])"
} else {
Write-Verbose "Extended protection setting was not queried
because it wasn't found on the system."
}

$sslFlagsToSet = $[Link]
$currentSetFlags = $[Link](",").Trim()
foreach ($sslFlag in $[Link](",").Trim()) {
if (-not($[Link]($sslFlag))) {
Write-Verbose "Failed to find SSL Flag $sslFlag"
# We do not want to include None in the flags as that
takes priority over the other options.
if ($sslFlagsToSet -eq "None") {
$sslFlagsToSet = "$sslFlag"
} else {
$sslFlagsToSet += ",$sslFlag"
}
Write-Verbose "Updated SSL Flags Value: $sslFlagsToSet"
} else {
Write-Verbose "SSL Flag $sslFlag set."
}
}

$expectedExtendedConfiguration = if ($supportedVersion)
{ $[Link] } else { "None" }
$virtualDirectoryName = "$($[Link])/$
($[Link])"

# Supported Configuration is when the current value of Extended


Protection is less than our expected extended protection value.
# While this isn't secure as we would like, it is still a
supported state that should work.
$supportedExtendedConfiguration =
$expectedExtendedConfiguration -eq $[Link]

if ($supportedExtendedConfiguration) {
Write-Verbose "The EP value set to the expected value."
} else {
Write-Verbose "We are expecting a value of
'$expectedExtendedConfiguration' but the current value is '$
($[Link])'"

if ($expectedExtendedConfiguration -eq "Require" -or


($expectedExtendedConfiguration -eq "Allow" -and
$[Link] -eq "None"))
{
$supportedExtendedConfiguration = $true
Write-Verbose "This is still supported because it is
lower than what we recommended."
} else {
Write-Verbose "This is not supported because you are
higher than the recommended value and will likely cause problems."
}
}

# Properly Secured Configuration is when the current Extended


Protection value is equal to or greater than the Expected Extended Protection
Configuration.
# If the Expected value is Allow, you can have the value set to
Allow or Required and it will not be a security risk. However, if set to None, that
is a security concern.
# For a mitigation scenario, like EWS BE, Required is the
Expected value. Therefore, on those directories, we need to verify that IP
filtering is set if not set to Require.
$properlySecuredConfiguration = $expectedExtendedConfiguration
-eq $[Link]

if ($properlySecuredConfiguration) {
Write-Verbose "We are 'properly' secure because we have EP
set to the expected EP configuration value: $($expectedExtendedConfiguration)"
} elseif ($expectedExtendedConfiguration -eq "Require") {
Write-Verbose "Checking to see if we have mitigations
enabled for the supported vDirs"
# Only care about virtual directories that we allow
mitigation for
$properlySecuredConfiguration = $mitigationSupportedVDirs -
contains $virtualDirectoryName -and
$[Link] -eq
"false"
} elseif ($expectedExtendedConfiguration -eq "Allow") {
Write-Verbose "Checking to see if Extended Protection is
set to 'Require' to still be considered secure"
$properlySecuredConfiguration =
$[Link] -eq "Require"
} else {
Write-Verbose "Recommended EP setting is 'None' means you
can have it higher, but you might run into other issues. But you are 'secure'."
$properlySecuredConfiguration = $true
}

Write-Verbose "Properly Secure Configuration value:


$properlySecuredConfiguration"

$[Link]([PSCustomObject]@{
VirtualDirectoryName = $virtualDirectoryName
Configuration = $extendedConfiguration
# The current Extended Protection configuration set on
the server
ExtendedProtection =
$[Link]
# The Recommended Extended Protection is to verify that
we have set the current Extended Protection
# setting value to the Expected Extended Protection
Value
RecommendedExtendedProtection =
$expectedExtendedConfiguration -eq $[Link]
# The supported/expected Extended Protection
Configuration value that we should be set to (based off the build of Exchange)
ExpectedExtendedConfiguration =
$expectedExtendedConfiguration
# Properly Secured is determined if we have a value
equal to or greater than the ExpectedExtendedConfiguration value
# However, if we have a value greater than the
expected, this could mean that we might run into a known set of issues.
ProperlySecuredConfiguration =
$properlySecuredConfiguration
# The Supported Extended Protection is a value that is
equal to or lower than the Expected Extended Protection configuration.
# While this is not the best security setting, it is
lower and shouldn't cause a connectivity issue and should still be supported.
SupportedExtendedProtection =
$supportedExtendedConfiguration
MitigationEnabled =
($[Link] -eq "false")
MitigationSupported =
$mitigationSupportedVDirs -contains $virtualDirectoryName
ExpectedSslFlags = $[Link]
SslFlagsSetCorrectly =
$[Link](",").Trim().Count -eq $[Link]
SslFlagsToSet = $sslFlagsToSet
})
} catch {
Write-Verbose "Failed to get extended protection match entry."
Invoke-CatchActionError $CatchActionFunction
}
}
} catch {
Write-Verbose "Failed to get get extended protection."
Invoke-CatchActionError $CatchActionFunction
}
}
end {
return [PSCustomObject]@{
ComputerName = $ComputerName
ServerConnected = $serverConnected
SupportedVersionForExtendedProtection = $supportedVersion
ApplicationHostConfig = $ApplicationHostConfig
ExtendedProtectionConfiguration = $extendedProtectionList
ExtendedProtectionConfigured = $null -ne
($[Link] | Where-Object { $_ -ne "None" })
}
}
}

function Invoke-ConfigureExtendedProtection {
param(
[object[]]$ExtendedProtectionConfigurations
)

begin {
$offlineServers = New-Object [Link][string]
$noChangesMadeServers = New-Object [Link][string]
$noEpConfigurationServer = New-Object
[Link][string]
$iisConfigurationManagements = New-Object
[Link][object]
$counter = 0
$totalCount = $[Link]
$progressParams = @{
Id = 1
Activity = "Configuring Extended Protection"
Status = [string]::Empty
PercentComplete = 0
}
Write-Verbose "Calling: $($[Link])"
} process {
foreach ($serverExtendedProtection in $ExtendedProtectionConfigurations) {
$counter++
# Check to make sure server is connected and valid information is
provided.
if (-not ($[Link])) {
Write-Warning "$($[Link]): Server
not online. Cannot get Extended Protection configuration settings."
$[Link]($[Link])
continue
}

if ($[Link] -eq
0) {
Write-Warning "$($[Link]): Server
wasn't able to collect Extended Protection configuration."

$[Link]($[Link])
continue
}

# set the extended protection (TokenChecking) configuration to the


expected and supported configuration if different
# only Set SSLFlags option if we are not setting extended protection to
None
$actionList = New-Object [Link][object]
$baseStatus = "Processing: $($[Link]) -"
$[Link] = ($counter / $totalCount * 100)
$[Link] = "$baseStatus Evaluating Extended Protection
Settings"
Write-Progress @progressParams

foreach ($virtualDirectory in
$[Link]) {
Write-Verbose "$($[Link]): Virtual
Directory Name: $($[Link]) Current Set Extended
Protection: $($[Link]) Expected Value $
($[Link])"
Write-Verbose "$($[Link]): Current
Set SSL Flags: $($[Link]) Expected SSL
Flags: $($[Link]) Set Correctly: $
($[Link])"
if ($[Link] -ne
$[Link]) {
$[Link]((New-IISConfigurationAction -Action
([PSCustomObject]@{
Cmdlet = "Set-WebConfigurationProperty"
Parameters = @{
Filter =
"[Link]/security/authentication/windowsAuthentication"
Name =
"[Link]"
Value =
$[Link]
PSPath = "IIS:\"
Location =
$[Link]
}
})))

if ($[Link] -ne "None"


-and
$[Link] -eq $false) {
$[Link]((New-IISConfigurationAction -Action
([PSCustomObject]@{
Cmdlet = "Set-WebConfigurationProperty"
Parameters = @{
Filter =
"[Link]/security/access"
Name = "sslFlags"
Value =
$[Link]
PSPath = "IIS:\"
Location =
$[Link]
}
})))
}
}
}

if ($[Link] -gt 0) {
$[Link]([PSCustomObject]@{
ServerName = $[Link]
Actions = $actionList
BackupFileName = "ConfigureExtendedProtection"
})
} else {
Write-Host "$($[Link]): No changes
made. Exchange build supports Extended Protection? $
($[Link])"
$[Link]($[Link])
}
}
} end {
Write-Progress @progressParams -Completed
if ($[Link] -gt 0) {
Invoke-IISConfigurationManagerAction $iisConfigurationManagements -
ConfigurationDescription "Configure Extended Protection"
}
Write-Host ""
if ($[Link] -gt 0) {
Write-Warning "Failed to enable Extended Protection on the following
servers, because they were offline: $([string]::Join(", " ,$offlineServers))"
}

if ($[Link] -gt 0) {
Write-Warning "Failed to determine what actions to take on the
following servers, because we couldn't retrieve the EP configuration: $
([string]::Join(",", $noEpConfigurationServer))"
}

if ($[Link] -gt 0) {
Write-Host "No changes were needed on the following servers: $
([string]::Join(", " ,$noChangesMadeServers))"
}
}
}

function Invoke-RollbackExtendedProtection {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string[]]$ExchangeServers
)
begin {
$failedServers = New-Object '[Link][string]'
Write-Verbose "Calling: $($[Link])"
} process {
foreach ($server in $ExchangeServers) {
Write-Host "Attempting to rollback on $server"
$results = Invoke-ScriptBlockHandler -ComputerName $server -ScriptBlock
{
param(
[bool]$PassedWhatIf
)
try {
$saveToPath = "$($env:WINDIR)\System32\inetSrv\config\
[Link]"
$backupLocation = $[Link](".config", ".[Link].$
([DateTime]::[Link]("yyyyMMddHHMMss")).bak")
$restoreFile = (Get-ChildItem "$($env:WINDIR)\System32\inetSrv\
config\" -Filter "*[Link].*.bak" | Sort-Object CreationTime | Select-
Object -First 1).FullName
$successRestore = $false
$successBackupCurrent = $false

if ($null -eq $restoreFile) {


throw "Failed to find [Link].*.bak file.
Either file was moved or script was never run. Please use -
DisableExtendedProtection to Disable Extended Protection."
}

$tooOld = (Get-ChildItem $restoreFile).CreationTime -lt


[DateTime]::[Link](-30)

if ($tooOld) {
throw "Configuration file is too old to restore from.
Please use -DisableExtendedProtection to Disable Extended Protection."
}

Copy-Item -Path $saveToPath -Destination $backupLocation -


ErrorAction Stop -WhatIf:$PassedWhatIf
$successBackupCurrent = $true
Copy-Item -Path $restoreFile -Destination $saveToPath -Force -
ErrorAction Stop -WhatIf:$PassedWhatIf
$successRestore = $true
} catch {
Write-Host "Failed to restore application host file on server
$env:COMPUTERNAME. Inner Exception $_"
}
return [PSCustomObject]@{
RestoreFile = $restoreFile
SuccessRestore = $successRestore
SuccessBackupCurrent = $successBackupCurrent
ErrorContext = $Error[0]
}
} -ArgumentList $WhatIfPreference

if ($[Link] -and $[Link]) {


Write-Host "Successful restored $($[Link]) on server
$server"
continue
} elseif ($[Link] -eq $false) {
$line = "Failed to backup the current configuration on server
$server"
Write-Verbose $line
Write-Warning $line
} elseif ($null -eq $results) {
$line = "Failed to restore application host config file on server
$server, because we weren't able to reach it."
Write-Verbose $line
Write-Warning $line
# need to add to list and continue because there is no error
context
$[Link]($server)
continue
} else {
$line = "Failed to restore $($[Link]) to be the active
application host config file on server $server"
Write-Verbose $line
Write-Warning $line
}
$[Link]($server)
Start-Sleep 1
Write-HostErrorInformation $[Link]
Write-Host ""
}
} end {
if ($[Link] -gt 0) {
$line = "These are the servers that failed to rollback: $
([string]::Join(", " ,$failedServers))"
Write-Verbose $line
Write-Warning $line
}
}
}

function Get-WmiObjectHandler {
[[Link]('PSAvoidUsingWMICmdlet', '',
Justification = 'This is what this function is for')]
[CmdletBinding()]
param(
[string]
$ComputerName = $env:COMPUTERNAME,

[Parameter(Mandatory = $true)]
[string]
$Class,

[string]
$Filter,
[string]
$Namespace,

[ScriptBlock]
$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($[Link])"
Write-Verbose "Passed - ComputerName: '$ComputerName' | Class: '$Class' |
Filter: '$Filter' | Namespace: '$Namespace'"

$execute = @{
ComputerName = $ComputerName
Class = $Class
ErrorAction = "Stop"
}

if (-not ([string]::IsNullOrEmpty($Filter))) {
$[Link]("Filter", $Filter)
}

if (-not ([string]::IsNullOrEmpty($Namespace))) {
$[Link]("Namespace", $Namespace)
}
}
process {
try {
$wmi = Get-WmiObject @execute
Write-Verbose "Return a value: $($null -ne $wmi)"
return $wmi
} catch {
Write-Verbose "Failed to run Get-WmiObject on class '$class'"
Invoke-CatchActionError $CatchActionFunction
}
}
}

function Get-RemoteRegistrySubKey {
[CmdletBinding()]
param(
[string]$RegistryHive = "LocalMachine",
[string]$MachineName,
[string]$SubKey,
[ScriptBlock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($[Link])"
Write-Verbose "Attempting to open the Base Key $RegistryHive on Machine
$MachineName"
$regKey = $null
}
process {

try {
$reg = [[Link]]::OpenRemoteBaseKey($RegistryHive,
$MachineName)
Write-Verbose "Attempting to open the Sub Key '$SubKey'"
$regKey = $[Link]($SubKey)
Write-Verbose "Opened Sub Key"
} catch {
Write-Verbose "Failed to open the registry"

if ($null -ne $CatchActionFunction) {


& $CatchActionFunction
}
}
}
end {
return $regKey
}
}

function Get-RemoteRegistryValue {
[CmdletBinding()]
param(
[string]$RegistryHive = "LocalMachine",
[string]$MachineName,
[string]$SubKey,
[string]$GetValue,
[string]$ValueType,
[ScriptBlock]$CatchActionFunction
)

<#
Valid ValueType return values (case-sensitive)
([Link]
view=net-5.0)
Binary = REG_BINARY
DWord = REG_DWORD
ExpandString = REG_EXPAND_SZ
MultiString = REG_MULTI_SZ
None = No data type
QWord = REG_QWORD
String = REG_SZ
Unknown = An unsupported registry data type
#>

begin {
Write-Verbose "Calling: $($[Link])"
$registryGetValue = $null
}
process {

try {

$regSubKey = Get-RemoteRegistrySubKey -RegistryHive $RegistryHive `


-MachineName $MachineName `
-SubKey $SubKey

if (-not ([[Link]]::IsNullOrWhiteSpace($regSubKey))) {
Write-Verbose "Attempting to get the value $GetValue"
$registryGetValue = $[Link]($GetValue)
Write-Verbose "Finished running GetValue()"

if ($null -ne $registryGetValue -and


(-not ([[Link]]::IsNullOrWhiteSpace($ValueType)))) {
Write-Verbose "Validating ValueType $ValueType"
$registryValueType = $[Link]($GetValue)
Write-Verbose "Finished running GetValueKind()"

if ($ValueType -ne $registryValueType) {


Write-Verbose "ValueType: $ValueType is different to the
returned ValueType: $registryValueType"
$registryGetValue = $null
} else {
Write-Verbose "ValueType matches: $ValueType"
}
}
}
} catch {
Write-Verbose "Failed to get the value on the registry"

if ($null -ne $CatchActionFunction) {


& $CatchActionFunction
}
}
}
end {
if ($[Link] -le 100) {
Write-Verbose "$($[Link]) Return Value:
'$registryGetValue'"
} else {
Write-Verbose "$($[Link]) Return Value is too long to
log"
}
return $registryGetValue
}
}
function Get-AllNicInformation {
[CmdletBinding()]
param(
[string]$ComputerName = $env:COMPUTERNAME,
[string]$ComputerFQDN,
[ScriptBlock]$CatchActionFunction
)
begin {

# Extract for Pester Testing - Start


function Get-NicPnpCapabilitiesSetting {
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]$NicAdapterComponentId
)
begin {
$nicAdapterBasicPath = "SYSTEM\CurrentControlSet\Control\Class\
{4D36E972-E325-11CE-BFC1-08002bE10318}"
[int]$i = 0
Write-Verbose "Probing started to detect NIC adapter registry path"
}
process {
$registrySubKey = Get-RemoteRegistrySubKey -MachineName
$ComputerName -SubKey $nicAdapterBasicPath
if ($null -ne $registrySubKey) {
$optionalKeys = $[Link]() | Where-Object
{ $_ -like "0*" }
do {
$nicAdapterPnPCapabilitiesProbingKey =
"$nicAdapterBasicPath\$($optionalKeys[$i])"
$netCfgRemoteRegistryParams = @{
MachineName = $ComputerName
SubKey =
$nicAdapterPnPCapabilitiesProbingKey
GetValue = "NetCfgInstanceId"
CatchActionFunction = $CatchActionFunction
}
$netCfgInstanceId = Get-RemoteRegistryValue
@netCfgRemoteRegistryParams

if ($netCfgInstanceId -eq $NicAdapterComponentId) {


Write-Verbose "Matching ComponentId found - now
checking for PnPCapabilitiesValue"
$pnpRemoteRegistryParams = @{
MachineName = $ComputerName
SubKey =
$nicAdapterPnPCapabilitiesProbingKey
GetValue = "PnPCapabilities"
CatchActionFunction = $CatchActionFunction
}
$nicAdapterPnPCapabilitiesValue = Get-
RemoteRegistryValue @pnpRemoteRegistryParams
break
} else {
Write-Verbose "No matching ComponentId found"
$i++
}
} while ($i -lt $[Link])
}
}
end {
return [PSCustomObject]@{
PnPCapabilities = $nicAdapterPnPCapabilitiesValue
SleepyNicDisabled = ($nicAdapterPnPCapabilitiesValue -eq 24 -or
$nicAdapterPnPCapabilitiesValue -eq 280)
}
}
}

# Extract for Pester Testing - End

function Get-NetworkConfiguration {
[CmdletBinding()]
param(
[string]$ComputerName
)
begin {
$currentErrors = $[Link]
$params = @{
ErrorAction = "Stop"
}
}
process {
try {
if (($ComputerName).Split(".")[0] -ne $env:COMPUTERNAME) {
$cimSession = New-CimSession -ComputerName $ComputerName -
ErrorAction Stop
$[Link]("CimSession", $cimSession)
}
$networkIpConfiguration = Get-NetIPConfiguration @params |
Where-Object { $_.[Link] -eq "Connected" }
Invoke-CatchActionErrorLoop -CurrentErrors $currentErrors -
CatchActionFunction $CatchActionFunction
return $networkIpConfiguration
} catch {
Write-Verbose "Failed to run Get-NetIPConfiguration. Error $
($_.Exception)"
#just rethrow as caller will handle the catch
throw
}
}
}

function Get-NicInformation {
[CmdletBinding()]
param(
[array]$NetworkConfiguration,
[bool]$WmiObject
)
begin {

function Get-IpvAddresses {
return [PSCustomObject]@{
Address = ([string]::Empty)
Subnet = ([string]::Empty)
DefaultGateway = ([string]::Empty)
}
}

if ($null -eq $NetworkConfiguration) {


Write-Verbose "NetworkConfiguration are null in New-
NicInformation. Returning a null object."
return $null
}

$nicObjects = New-Object '[Link][object]'


}
process {
if ($WmiObject) {
$networkAdapterConfigurationsParams = @{
ComputerName = $ComputerName
Class = "Win32_NetworkAdapterConfiguration"
Filter = "IPEnabled = True"
CatchActionFunction = $CatchActionFunction
}
$networkAdapterConfigurations = Get-WmiObjectHandler
@networkAdapterConfigurationsParams
}

foreach ($networkConfig in $NetworkConfiguration) {


$dnsClient = $null
$rssEnabledValue = 2
$netAdapterRss = $null
$mtuSize = 0
$driverDate = [DateTime]::MaxValue
$driverVersion = [string]::Empty
$description = [string]::Empty
$ipv4Address = @()
$ipv6Address = @()
$ipv6Enabled = $false
$isRegisteredInDns = $false
$dnsServerToBeUsed = $null

if (-not ($WmiObject)) {
Write-Verbose "Working on NIC: $
($[Link])"
$adapter = $[Link]

if ($[Link] -ne "[Link]") {


$nicPnpCapabilitiesSetting = Get-
NicPnpCapabilitiesSetting -NicAdapterComponentId $[Link]
} else {
Write-Verbose "Multiplexor adapter detected. Going to
skip PnpCapabilities check"
$nicPnpCapabilitiesSetting = [PSCustomObject]@{
PnPCapabilities = "MultiplexorNoPnP"
}
}

try {
$dnsClient = $adapter | Get-DnsClient -ErrorAction Stop
$isRegisteredInDns =
$[Link]
Write-Verbose "Got DNS Client information"
} catch {
Write-Verbose "Failed to get the DNS client
information"
Invoke-CatchActionError $CatchActionFunction
}

try {
$netAdapterRss = $adapter | Get-NetAdapterRss -
ErrorAction Stop
Write-Verbose "Got Net Adapter RSS Information"

if ($null -ne $netAdapterRss) {


[int]$rssEnabledValue = $[Link]
}
} catch {
Write-Verbose "Failed to get RSS Information"
Invoke-CatchActionError $CatchActionFunction
}

foreach ($ipAddress in
$[Link]) {
if ($[Link](":")) {
$ipv6Enabled = $true
}
}

for ($i = 0; $i -lt $[Link]; $i++)


{
$newIpvAddress = Get-IpvAddresses
if ($null -ne $networkConfig.IPv4Address -and
$i -lt $[Link]) {
$[Link] =
$networkConfig.IPv4Address[$i].IPAddress
$[Link] =
$networkConfig.IPv4Address[$i].PrefixLength
}

if ($null -ne $networkConfig.IPv4DefaultGateway -and


$i -lt $[Link]) {
$[Link] =
$networkConfig.IPv4DefaultGateway[$i].NextHop
}
$ipv4Address += $newIpvAddress
}

for ($i = 0; $i -lt $[Link]; $i++)


{
$newIpvAddress = Get-IpvAddresses

if ($null -ne $networkConfig.IPv6Address -and


$i -lt $[Link]) {
$[Link] =
$networkConfig.IPv6Address[$i].IPAddress
$[Link] =
$networkConfig.IPv6Address[$i].PrefixLength
}

if ($null -ne $networkConfig.IPv6DefaultGateway -and


$i -lt $[Link]) {
$[Link] =
$networkConfig.IPv6DefaultGateway[$i].NextHop
}
$ipv6Address += $newIpvAddress
}

$mtuSize = $[Link]
$driverDate = $[Link]
$driverVersion = $[Link]
$description = $[Link]
$dnsServerToBeUsed =
$[Link]
} else {
Write-Verbose "Working on NIC: $
($[Link])"
$adapter = $networkConfig
$description = $[Link]

if ($[Link] -ne "NdIsImPlatformMp") {


$nicPnpCapabilitiesSetting = Get-
NicPnpCapabilitiesSetting -NicAdapterComponentId $[Link]
} else {
Write-Verbose "Multiplexor adapter detected. Going to
skip PnpCapabilities check"
$nicPnpCapabilitiesSetting = [PSCustomObject]@{
PnPCapabilities = "MultiplexorNoPnP"
}
}
#set the correct $adapterConfiguration to link to the
correct $networkConfig that we are on
$adapterConfiguration = $networkAdapterConfigurations |
Where-Object { $_.SettingID -eq $[Link] -or
$_.SettingID -eq $[Link] }

if ($null -eq $adapterConfiguration) {


Write-Verbose "Failed to find correct
adapterConfiguration for this networkConfig."
Write-Verbose "GUID: $($[Link]) |
InterfaceGuid: $($[Link])"
} else {
$ipv6Enabled = ($[Link] |
Where-Object { $_.Contains(":") }).Count -ge 1

if ($null -ne $[Link]) {


$ipv4Gateway =
$[Link] | Where-Object { $_.Contains(".") }
$ipv6Gateway =
$[Link] | Where-Object { $_.Contains(":") }
} else {
$ipv4Gateway = "No default IPv4 gateway set"
$ipv6Gateway = "No default IPv6 gateway set"
}

for ($i = 0; $i -lt


$[Link]; $i++) {

if
($[Link][$i].Contains(":")) {
$newIpv6Address = Get-IpvAddresses
if ($i -lt
$[Link]) {
$[Link] =
$[Link][$i]
$[Link] =
$[Link][$i]
}

$[Link] = $ipv6Gateway
$ipv6Address += $newIpv6Address
} else {
$newIpv4Address = Get-IpvAddresses
if ($i -lt
$[Link]) {
$[Link] =
$[Link][$i]
$[Link] =
$[Link][$i]
}

$[Link] = $ipv4Gateway
$ipv4Address += $newIpv4Address
}
}

$isRegisteredInDns =
$[Link]
$dnsServerToBeUsed =
$[Link]
}
}

$[Link]([PSCustomObject]@{
WmiObject = $WmiObject
Name = $[Link]
LinkSpeed = ((($[Link]) /
1000000).ToString() + " Mbps")
DriverDate = $driverDate
NetAdapterRss = $netAdapterRss
RssEnabledValue = $rssEnabledValue
IPv6Enabled = $ipv6Enabled
Description = $description
DriverVersion = $driverVersion
MTUSize = $mtuSize
PnPCapabilities =
$[Link]
SleepyNicDisabled =
$[Link]
IPv4Addresses = $ipv4Address
IPv6Addresses = $ipv6Address
RegisteredInDns = $isRegisteredInDns
DnsServer = $dnsServerToBeUsed
DnsClient = $dnsClient
})
}
}
end {
Write-Verbose "Found $($[Link]) active adapters on the
computer."
Write-Verbose "Exiting: $($[Link])"
return $nicObjects
}
}

Write-Verbose "Calling: $($[Link])"


Write-Verbose "Passed - ComputerName: '$ComputerName' | ComputerFQDN:
'$ComputerFQDN'"
}
process {
try {
try {
$networkConfiguration = Get-NetworkConfiguration -ComputerName
$ComputerName
} catch {
Invoke-CatchActionError $CatchActionFunction

try {
if (-not ([string]::IsNullOrEmpty($ComputerFQDN))) {
$networkConfiguration = Get-NetworkConfiguration -
ComputerName $ComputerFQDN
} else {
$bypassCatchActions = $true
Write-Verbose "No FQDN was passed, going to rethrow error."
throw
}
} catch {
#Just throw again
throw
}
}

if ([String]::IsNullOrEmpty($networkConfiguration)) {
# Throw if nothing was returned by previous calls.
# Can be caused when executed on Server 2008 R2 where CIM namespace
ROOT/StandardCiMv2 is invalid.
Write-Verbose "No value was returned by 'Get-NetworkConfiguration'.
Fallback to WMI."
throw
}

return (Get-NicInformation -NetworkConfiguration $networkConfiguration)


} catch {
if (-not $bypassCatchActions) {
Invoke-CatchActionError $CatchActionFunction
}

$wmiNetworkCardsParams = @{
ComputerName = $ComputerName
Class = "Win32_NetworkAdapter"
Filter = "NetConnectionStatus ='2'"
CatchActionFunction = $CatchActionFunction
}
$wmiNetworkCards = Get-WmiObjectHandler @wmiNetworkCardsParams

return (Get-NicInformation -NetworkConfiguration $wmiNetworkCards -


WmiObject $true)
}
}
}

# This function is used to get a list of all the IP in use by the Exchange Servers
across the topology
function Get-ExchangeServerIPs {
[OutputType([[Link]])]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$OutputFilePath,
[Parameter(Mandatory = $false)]
[object[]]$ExchangeServers
)

begin {
$IPs = New-Object '[Link][string]'
$FailedServers = New-Object '[Link][string]'

$progressParams = @{
Activity = "Getting List of IPs in use by Exchange Servers"
Status = [string]::Empty
PercentComplete = 0
}

Write-Verbose "Calling: $($[Link])"


}
process {
$counter = 0
$totalCount = $[Link]

foreach ($Server in $ExchangeServers) {


$baseStatus = "Processing: $($[Link]) -"
$[Link] = ($counter / $totalCount * 100)
$[Link] = "$baseStatus Getting IPs"
Write-Progress @progressParams

$IpsFound = $false
# TODO: Refactor Get-AllNicInformation function to get rid of the
duplicate ComputerName / FQDN logic
$HostNetworkInfo = Get-AllNicInformation -ComputerName $[Link]
if ($null -ne $HostNetworkInfo) {
if ($null -ne $HostNetworkInfo.IPv4Addresses) {
foreach ($address in $HostNetworkInfo.IPv4Addresses) {
$IPs += $[Link]
$IpsFound = $true
}
}
if ($null -ne $HostNetworkInfo.IPv6Addresses) {
foreach ($address in $HostNetworkInfo.IPv6Addresses) {
$IPs += $[Link]
$IpsFound = $true
}
}
}

if (-not $IpsFound) {
$FailedServers += $[Link]
Write-Verbose "IP of $($[Link]) cannot be found and will not
be added to IP allow list."
}

$counter++
}

Write-Progress @progressParams -Completed


}
end {
if ($FailedServers -gt 0) {
Write-Host ("Unable to get IPs from the following servers: {0}" -f
[string]::Join(", ", $FailedServers)) -ForegroundColor Red
}

try {
$IPs | Out-File $OutputFilePath
Write-Host ("Please find the collected IPs at {0}" -f $OutputFilePath)
} catch {
Write-Host "Unable to write to file. Please check the path provided.
Inner Exception:" -ForegroundColor Red
Write-HostErrorInformation $_
}
}
}

# This function is used to get a list of all the IP in use by the Exchange Servers
across the topology
function Get-IPRangeAllowListFromFile {
[CmdletBinding()]
[OutputType([Hashtable])]
param(
[Parameter(Mandatory = $true)]
[string]$FilePath
)

begin {
$results = @{
ipRangeAllowListRules = New-Object
'[Link][object]'
IsError = $true
}

Write-Verbose "Calling: $($[Link])"


}
process {
try {
$SubnetStrings = (Get-Content -Path $FilePath -ErrorAction Stop) |
Where-Object { $_.trim() -ne "" }
} catch {
Write-Host "Unable to read the content of file provided for IPRange.
Inner Exception" -ForegroundColor Red
Write-HostErrorInformation $_
return
}

if ($null -eq $SubnetStrings -or $[Link] -eq 0) {


Write-Host "The IP range file provided is empty. Please provide a valid
file." -ForegroundColor Red
return
} else {
$ipRangesString = [string]::Join(", ", $SubnetStrings)
}

# Log all the IPs present in the txt file supplied by user
Write-Verbose ("Read the contents of the file Successfully. List of IP
ranges received from user: {0}" -f $ipRangesString)

Write-Verbose "Validating the IP ranges specified in the file"


try {
foreach ($SubnetString in $SubnetStrings) {
$SubnetString = $[Link]()

$IpAddressString = $[Link]("/")[0]
$SubnetMaskString = $[Link]("/")[1]

# Check the type of IP address (IPv4/IPv6)


$IpAddress = $IpAddressString -as [IPAddress]
$baseError = "Input file provided for IPRange doesn't have correct
syntax of IPs or IP subnets."
if ($null -eq $IpAddress -or $null -eq $[Link]) {
# Invalid IP address found
Write-Host ("$baseError Re-execute the command with proper
input file for IPRange parameter. Invalid IP address detected: {0}." -f
$IpAddressString) -ForegroundColor Red
return
}
$IsIPv6 = $[Link] -eq
[[Link]]::InterNetworkV6

if ($SubnetMaskString) {
# Check if the subnet value is valid (IPv4 <= 32, IPv6 <= 128
or empty)
$SubnetMask = $SubnetMaskString -as [int]

$InvalidSubnetMaskString = "$baseError Invalid Subnet Mask


found: The Subnet Mask $SubnetMaskString is not in valid [Link]: Subnet Mask
must be either empty or a non-negative integer. For IPv4 the value must be <= 32
and for IPv6 the value must be <= 128. Re-execute the command with proper input
file for IPRange parameter."
if ($null -eq $SubnetMask) {
Write-Host ($InvalidSubnetMaskString) -ForegroundColor Red
return
} elseif (($SubnetMask -gt 32 -and -not $IsIPv6) -or
$SubnetMask -gt 128 -or $SubnetMask -lt 0) {
Write-Host ($InvalidSubnetMaskString) -ForegroundColor Red
return
}

if ($null -eq ($[Link] | Where-Object


{ $_.Type -eq "Subnet" -and $_.IP -eq $IpAddressString -and $_.SubnetMask -eq
$SubnetMaskString })) {
$[Link](@{Type = "Subnet";
IP=$IpAddressString; SubnetMask=$SubnetMaskString; Allowed=$true })
} else {
Write-Verbose ("Not adding
$IpAddressString/$SubnetMaskString to the list as it is a duplicate entry in the
file provided.")
}
} else {
if ($null -eq ($[Link] | Where-Object
{ $_.Type -eq "Single IP" -and $_.IP -eq $IpAddressString })) {
$[Link](@{Type = "Single IP";
IP=$IpAddressString; Allowed=$true })
} else {
Write-Verbose ("Not adding $IpAddressString to the list as
it is a duplicate entry in the file provided.")
}
}
}

if ($[Link] -gt 500) {


Write-Host ("Too many IP filtering rules. Please reduce the
specified entries by providing appropriate subnets." -f $SubnetMaskString) -
ForegroundColor Red
return
}
} catch {
Write-Host ("Unable to create IP allow rules. Inner Exception") -
ForegroundColor Red
Write-HostErrorInformation $_
return
}

$[Link] = $false
}
end {
return $results
}
}

function Get-AllTlsSettingsFromRegistry {
[CmdletBinding()]
param(
[string]$MachineName = $env:COMPUTERNAME,
[ScriptBlock]$CatchActionFunction
)
begin {

function Get-TLSMemberValue {
param(
[Parameter(Mandatory = $true)]
[string]
$GetKeyType,

[Parameter(Mandatory = $false)]
[object]
$KeyValue,

[Parameter( Mandatory = $false)]


[bool]
$NullIsEnabled
)
Write-Verbose "KeyValue is null: '$($null -eq $KeyValue)' | KeyValue:
'$KeyValue' | GetKeyType: $GetKeyType | NullIsEnabled: $NullIsEnabled"
switch ($GetKeyType) {
"Enabled" {
return ($null -eq $KeyValue -and $NullIsEnabled) -or ($KeyValue
-ne 0 -and $null -ne $KeyValue)
}
"DisabledByDefault" {
return $null -ne $KeyValue -and $KeyValue -ne 0
}
}
}

function Get-NETDefaultTLSValue {
param(
[Parameter(Mandatory = $false)]
[object]
$KeyValue,

[Parameter(Mandatory = $true)]
[string]
$NetVersion,

[Parameter(Mandatory = $true)]
[string]
$KeyName
)
Write-Verbose "KeyValue is null: '$($null -eq $KeyValue)' | KeyValue:
'$KeyValue' | NetVersion: '$NetVersion' | KeyName: '$KeyName'"
return $null -ne $KeyValue -and $KeyValue -eq 1
}

Write-Verbose "Calling: $($[Link])"


Write-Verbose "Passed - MachineName: '$MachineName'"
$registryBase = "SYSTEM\CurrentControlSet\Control\SecurityProviders\
SCHANNEL\Protocols\TLS {0}\{1}"
$enabledKey = "Enabled"
$disabledKey = "DisabledByDefault"
$netRegistryBase = "SOFTWARE\{0}\.NETFramework\{1}"
$allTlsObjects = [PSCustomObject]@{
"TLS" = @{}
"NET" = @{}
}
}
process {
foreach ($tlsVersion in @("1.0", "1.1", "1.2", "1.3")) {
$registryServer = $registryBase -f $tlsVersion, "Server"
$registryClient = $registryBase -f $tlsVersion, "Client"
$baseParams = @{
MachineName = $MachineName
CatchActionFunction = $CatchActionFunction
}

# Get the Enabled and DisabledByDefault values


$serverEnabledValue = Get-RemoteRegistryValue @baseParams -SubKey
$registryServer -GetValue $enabledKey
$serverDisabledByDefaultValue = Get-RemoteRegistryValue @baseParams -
SubKey $registryServer -GetValue $disabledKey
$clientEnabledValue = Get-RemoteRegistryValue @baseParams -SubKey
$registryClient -GetValue $enabledKey
$clientDisabledByDefaultValue = Get-RemoteRegistryValue @baseParams -
SubKey $registryClient -GetValue $disabledKey
$serverEnabled = (Get-TLSMemberValue -GetKeyType $enabledKey -KeyValue
$serverEnabledValue -NullIsEnabled ($tlsVersion -ne "1.3"))
$serverDisabledByDefault = (Get-TLSMemberValue -GetKeyType $disabledKey
-KeyValue $serverDisabledByDefaultValue)
$clientEnabled = (Get-TLSMemberValue -GetKeyType $enabledKey -KeyValue
$clientEnabledValue -NullIsEnabled ($tlsVersion -ne "1.3"))
$clientDisabledByDefault = (Get-TLSMemberValue -GetKeyType $disabledKey
-KeyValue $clientDisabledByDefaultValue)
$disabled = $serverEnabled -eq $false -and ($serverDisabledByDefault -
or $null -eq $serverDisabledByDefaultValue) -and
$clientEnabled -eq $false -and ($clientDisabledByDefault -or $null -eq
$clientDisabledByDefaultValue)
$misconfigured = $serverEnabled -ne $clientEnabled -or
$serverDisabledByDefault -ne $clientDisabledByDefault
# only need to test server settings here, because $misconfigured will
be set and will be the official status.
# want to check for if Server is Disabled and Disabled By Default is
not set or the reverse. This would be only part disabled
# and not what we recommend on the blog post.
$halfDisabled = ($serverEnabled -eq $false -and
$serverDisabledByDefault -eq $false -and $null -ne $serverDisabledByDefaultValue) -
or
($serverEnabled -and $serverDisabledByDefault)
$configuration = "Enabled"

if ($disabled) {
Write-Verbose "TLS is Disabled"
$configuration = "Disabled"
}

if ($halfDisabled) {
Write-Verbose "TLS is only half disabled"
$configuration = "Half Disabled"
}

if ($misconfigured) {
Write-Verbose "TLS is misconfigured"
$configuration = "Misconfigured"
}

$currentTLSObject = [PSCustomObject]@{
TLSVersion = $tlsVersion
"Server$enabledKey" = $serverEnabled
"Server$enabledKey`Value" = $serverEnabledValue
"Server$disabledKey" = $serverDisabledByDefault
"Server$disabledKey`Value" = $serverDisabledByDefaultValue
"ServerRegistryPath" = $registryServer
"Client$enabledKey" = $clientEnabled
"Client$enabledKey`Value" = $clientEnabledValue
"Client$disabledKey" = $clientDisabledByDefault
"Client$disabledKey`Value" = $clientDisabledByDefaultValue
"ClientRegistryPath" = $registryClient
"TLSVersionDisabled" = $disabled
"TLSMisconfigured" = $misconfigured
"TLSHalfDisabled" = $halfDisabled
"TLSConfiguration" = $configuration
}
$[Link]($TlsVersion, $currentTLSObject)
}

foreach ($netVersion in @("v2.0.50727", "v4.0.30319")) {

$msRegistryKey = $netRegistryBase -f "Microsoft", $netVersion


$wowMsRegistryKey = $netRegistryBase -f "Wow6432Node\Microsoft",
$netVersion

$systemDefaultTlsVersionsValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $msRegistryKey `
-GetValue "SystemDefaultTlsVersions" `
-CatchActionFunction $CatchActionFunction
$schUseStrongCryptoValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $msRegistryKey `
-GetValue "SchUseStrongCrypto" `
-CatchActionFunction $CatchActionFunction
$wowSystemDefaultTlsVersionsValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $wowMsRegistryKey `
-GetValue "SystemDefaultTlsVersions" `
-CatchActionFunction $CatchActionFunction
$wowSchUseStrongCryptoValue = Get-RemoteRegistryValue `
-MachineName $MachineName `
-SubKey $wowMsRegistryKey `
-GetValue "SchUseStrongCrypto" `
-CatchActionFunction $CatchActionFunction
$systemDefaultTlsVersions = (Get-NETDefaultTLSValue -KeyValue
$SystemDefaultTlsVersionsValue -NetVersion $netVersion -KeyName
"SystemDefaultTlsVersions")
$wowSystemDefaultTlsVersions = (Get-NETDefaultTLSValue -KeyValue
$wowSystemDefaultTlsVersionsValue -NetVersion $netVersion -KeyName
"WowSystemDefaultTlsVersions")

$currentNetTlsDefaultVersionObject = [PSCustomObject]@{
NetVersion = $netVersion
SystemDefaultTlsVersions = $systemDefaultTlsVersions
SystemDefaultTlsVersionsValue = $systemDefaultTlsVersionsValue
SchUseStrongCrypto = (Get-NETDefaultTLSValue -
KeyValue $schUseStrongCryptoValue -NetVersion $netVersion -KeyName
"SchUseStrongCrypto")
SchUseStrongCryptoValue = $schUseStrongCryptoValue
MicrosoftRegistryLocation = $msRegistryKey
WowSystemDefaultTlsVersions = $wowSystemDefaultTlsVersions
WowSystemDefaultTlsVersionsValue =
$wowSystemDefaultTlsVersionsValue
WowSchUseStrongCrypto = (Get-NETDefaultTLSValue -
KeyValue $wowSchUseStrongCryptoValue -NetVersion $netVersion -KeyName
"WowSchUseStrongCrypto")
WowSchUseStrongCryptoValue = $wowSchUseStrongCryptoValue
WowRegistryLocation = $wowMsRegistryKey
SDtvConfiguredCorrectly = $systemDefaultTlsVersions -eq
$wowSystemDefaultTlsVersions
SDtvEnabled = $systemDefaultTlsVersions -and
$wowSystemDefaultTlsVersions
}

$hashKeyName = "NET{0}" -f ($[Link](".")[0])


$[Link]($hashKeyName,
$currentNetTlsDefaultVersionObject)
}
return $allTlsObjects
}
}

function Get-TlsCipherSuiteInformation {
[OutputType("[Link]")]
param(
[string]$MachineName = $env:COMPUTERNAME,
[ScriptBlock]$CatchActionFunction
)

begin {

function GetProtocolNames {
param(
[int[]]$Protocol
)
$protocolNames = New-Object [Link][string]

foreach ($p in $Protocol) {


$name = [string]::Empty

if ($p -eq 2) { $name = "SSL_2_0" }


elseif ($p -eq 768) { $name = "SSL_3_0" }
elseif ($p -eq 769) { $name = "TLS_1_0" }
elseif ($p -eq 770) { $name = "TLS_1_1" }
elseif ($p -eq 771) { $name = "TLS_1_2" }
elseif ($p -eq 772) { $name = "TLS_1_3" }
elseif ($p -eq 32528) { $name = "TLS_1_3_DRAFT_16" }
elseif ($p -eq 32530) { $name = "TLS_1_3_DRAFT_18" }
elseif ($p -eq 65279) { $name = "DTLS_1_0" }
elseif ($p -eq 65277) { $name = "DTLS_1_1" }
else {
Write-Verbose "Unable to determine protocol $p"
$name = $p
}

$[Link]($name)
}
return [string]::Join(" & ", $protocolNames)
}

Write-Verbose "Calling: $($[Link])"


$tlsCipherReturnObject = New-Object
'[Link][object]'
}
process {
# 'Get-TlsCipherSuite' takes account of the cipher suites which are
configured by the help of GPO.
# No need to query the ciphers defined via GPO if this call is successful.
Write-Verbose "Trying to query TlsCipherSuites via 'Get-TlsCipherSuite'"
$getTlsCipherSuiteParams = @{
ComputerName = $MachineName
ScriptBlock = { Get-TlsCipherSuite }
CatchActionFunction = $CatchActionFunction
}
$tlsCipherSuites = Invoke-ScriptBlockHandler @getTlsCipherSuiteParams

if ($null -eq $tlsCipherSuites) {


# If we can't get the ciphers via cmdlet, we need to query them via
registry call and need to check
# if ciphers suites are defined via GPO as well. If there are some,
these take precedence over what
# is in the default location.
Write-Verbose "Failed to query TlsCipherSuites via 'Get-TlsCipherSuite'
fallback to registry"

$policyTlsRegistryParams = @{
MachineName = $MachineName
SubKey = "SOFTWARE\Policies\Microsoft\Cryptography\
Configuration\SSL\00010002"
GetValue = "Functions"
ValueType = "String"
CatchActionFunction = $CatchActionFunction
}

Write-Verbose "Trying to query cipher suites configured via GPO from


registry"
$policyDefinedCiphers = Get-RemoteRegistryValue
@policyTlsRegistryParams

if ($null -ne $policyDefinedCiphers) {


Write-Verbose "Ciphers specified via GPO found - these take
precedence over what is in the default location"
$tlsCipherSuites = $[Link](",")
} else {
Write-Verbose "No cipher suites configured via GPO found - going to
query the local TLS cipher suites"
$tlsRegistryParams = @{
MachineName = $MachineName
SubKey = "SYSTEM\CurrentControlSet\Control\
Cryptography\Configuration\Local\SSL\00010002"
GetValue = "Functions"
ValueType = "MultiString"
CatchActionFunction = $CatchActionFunction
}

$tlsCipherSuites = Get-RemoteRegistryValue @tlsRegistryParams


}
}

if ($null -ne $tlsCipherSuites) {


foreach ($cipher in $tlsCipherSuites) {
$[Link]([PSCustomObject]@{
Name = if ($null -eq $[Link]) { $cipher } else
{ $[Link] }
CipherSuite = if ($null -eq $[Link]) { "N/A" }
else { $[Link] }
Cipher = if ($null -eq $[Link]) { "N/A" } else
{ $[Link] }
Certificate = if ($null -eq $[Link]) { "N/A" }
else { $[Link] }
Protocols = if ($null -eq $[Link]) { "N/A" }
else { (GetProtocolNames $[Link]) }
})
}
}
}
end {
return $tlsCipherReturnObject
}
}

# Gets all related TLS Settings, from registry or other factors


function Get-AllTlsSettings {
[CmdletBinding()]
param(
[string]$MachineName = $env:COMPUTERNAME,
[ScriptBlock]$CatchActionFunction
)
begin {
Write-Verbose "Calling: $($[Link])"
}
process {
return [PSCustomObject]@{
Registry = (Get-AllTlsSettingsFromRegistry -MachineName
$MachineName -CatchActionFunction $CatchActionFunction)
SecurityProtocol = (Invoke-ScriptBlockHandler -ComputerName
$MachineName -ScriptBlock
{ ([[Link]]::SecurityProtocol).ToString() } -
CatchActionFunction $CatchActionFunction)
TlsCipherSuite = (Get-TlsCipherSuiteInformation -MachineName
$MachineName -CatchActionFunction $CatchActionFunction)
}
}
}

# This function is used to collect the required information needed to determine if


a server is ready for Extended Protection
function Get-ExtendedProtectionPrerequisitesCheck {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[object[]]$ExchangeServers,

[Parameter(Mandatory = $false)]
[string[]]$SiteVDirLocations,

[Parameter(Mandatory = $false)]
[bool]$SkipEWS,

[Parameter(Mandatory = $false)]
[bool]$SkipEWSFe
)
begin {
$results = New-Object '[Link][object]'
$counter = 0
$totalCount = $[Link]
$progressParams = @{
Activity = "Prerequisites Check"
Status = [string]::Empty
PercentComplete = 0
}
Write-Verbose "Calling: $($[Link])"
} process {
foreach ($server in $ExchangeServers) {

$counter++
$baseStatus = "Processing: $server -"
$[Link] = ($counter / $totalCount * 100)
$[Link] = "$baseStatus Extended Protection
Configuration"
Write-Progress @progressParams
$tlsSettings = $null
$registryValues = @{
SuppressExtendedProtection = 0
LmCompatibilityLevel = $null
}
Write-Verbose "$($[Link])"

$params = @{
ComputerName = $[Link]
IsClientAccessServer = $[Link]
IsMailboxServer = $[Link]
ExcludeEWS = $SkipEWS
ExcludeEWSFe = $SkipEWSFe
}

if ($null -ne $SiteVDirLocations) {


$[Link]("SiteVDirLocations", $SiteVDirLocations)
}
$extendedProtectionConfiguration = Get-ExtendedProtectionConfiguration
@params

if ($[Link]) {
Write-Verbose "Server appears to be up going to get the TLS
settings as well"
$[Link] = "$baseStatus TLS Settings"
Write-Progress @progressParams
Write-Verbose "$($[Link])"
$tlsSettings = Get-AllTlsSettings -MachineName $[Link]
$params = @{
MachineName = $[Link]
SubKey = "SYSTEM\CurrentControlSet\Control\Lsa"
}

$lmValue = Get-RemoteRegistryValue @params -GetValue


"LmCompatibilityLevel" -ValueType "DWord"
[int]$epValue = Get-RemoteRegistryValue @params -GetValue
"SuppressExtendedProtection"

if ($null -eq $lmValue) { $lmValue = 3 }

Write-Verbose "Server $($[Link]) LmCompatibilityLevel set to


$lmValue"
$[Link] = $epValue
$[Link] = $lmValue
} else {
Write-Verbose "Server doesn't appear to be online. Skipped over
trying to get the TLS settings"
}

$[Link]([PSCustomObject]@{
ComputerName = $[Link]
FQDN = $[Link]
ExtendedProtectionConfiguration =
$extendedProtectionConfiguration
TlsSettings = [PSCustomObject]@{
ComputerName = $[Link]
FQDN = $[Link]
Settings = $tlsSettings
}
RegistryValue = $registryValues
ServerOnline =
$[Link]
})
}
Write-Progress @progressParams -Completed
} end {
return $results
}
}

# Used to test the TLS Configuration


function Invoke-ExtendedProtectionTlsPrerequisitesCheck {
[CmdletBinding()]
[OutputType("[Link]")]
param(
[Parameter(Mandatory = $true)]
[object[]]$TlsConfiguration
)

begin {
function NewActionObject {
param(
[string]$Name,
[array]$List,
[string]$Action
)

return [PSCustomObject]@{
Name = $Name
List = $List
Action = $Action
}
}

function GroupTlsServerSettings {
[CmdletBinding()]
param(
[[Link][object]]$TlsSettingsList
)

$groupedResults = New-Object '[Link][object]'

# loop through the least amount of times to compare the TLS settings
# if the values are different add them to the list
$tlsKeys = @("1.0", "1.1", "1.2")
$netKeys = @("NETv4") # Only think we care about v4

foreach ($serverTls in $TlsSettingsList) {


$currentServer = $[Link]
$tlsSettings = $[Link]
# Removing TLS 1.3 here to avoid it being displayed
$[Link]("1.3")
$tlsRegistry = $[Link]
$netRegistry = $[Link]
$listIndex = 0
$addNewGroupList = $true
Write-Verbose "Working on Server $currentServer"

# only need to compare against the current groupedResults List


# if this is the first time, we don't compare we just add
while ($listIndex -lt $[Link]) {
$referenceTlsSettings = $groupedResults[$listIndex].TlsSettings
$nextServer = $false
Write-Verbose "Working on TLS Setting index $listIndex"

foreach ($key in $tlsKeys) {


$props = $tlsRegistry[$key].[Link]
$result = Compare-Object -ReferenceObject
$[Link][$key] -DifferenceObject $tlsRegistry[$key] -
Property $props
if ($null -ne $result) {
Write-Verbose "Found difference in TLS for $key"
$nextServer = $true
break
}
}

if ($nextServer) { $listIndex++; continue; }

foreach ($key in $netKeys) {


$props = $netRegistry[$key].[Link]
$result = Compare-Object -ReferenceObject
$[Link][$key] -DifferenceObject $netRegistry[$key] -
Property $props
if ($null -ne $result) {
Write-Verbose "Found difference in NET for $key"
$nextServer = $true
break
}
}

if ($nextServer) { $listIndex++; continue; }


Write-Verbose "This server's Security Protocol is set to $
($[Link])"

# we must match so add to the current groupResults and break


Write-Verbose "Server appears to match current reference TLS
Object"
$groupedResults[$listIndex].[Link]($currentServer)
Write-Verbose "Now $
($groupedResults[$listIndex].[Link]) servers match this reference"
$addNewGroupList = $false
break
}

if ($addNewGroupList) {
Write-Verbose "Added new grouped result because of server
$currentServer"
$obj = [PSCustomObject]@{
TlsSettings = $tlsSettings
MatchedServer = New-Object
'[Link][string]'
}
$[Link]($currentServer)
$[Link]($obj)
}
}
return $groupedResults
}

$actionsRequiredList = New-Object '[Link][object]'


Write-Verbose "Calling: $($[Link])"
} process {

$tlsGroupedResults = @(GroupTlsServerSettings -TlsSettingsList


$TlsConfiguration)

if ($null -ne $tlsGroupedResults -and


$[Link] -gt 0) {

foreach ($tlsResults in $tlsGroupedResults) {


# Check for actions to take against
$netKeys = @("NETv4")
$netRegistry = $[Link]
foreach ($key in $netKeys) {
if ($netRegistry[$key].SchUseStrongCrypto -eq $false -or
$netRegistry[$key].WowSchUseStrongCrypto -eq $false -or
$null -eq $netRegistry[$key].SchUseStrongCryptoValue -or
$null -eq $netRegistry[$key].WowSchUseStrongCryptoValue) {
$params = @{
Name = "SchUseStrongCrypto is not configured as
expected"
List = $[Link]
Action = "Configure SchUseStrongCrypto for $key as
described here: [Link]
}
$[Link]((NewActionObject @params))
Write-Verbose "SchUseStrongCrypto doesn't match the
expected configuration"
}

if ($netRegistry[$key].SystemDefaultTlsVersions -eq $false -or


$netRegistry[$key].WowSystemDefaultTlsVersions -eq $false -
or
$null -eq $netRegistry[$key].SystemDefaultTlsVersionsValue
-or
$null -eq
$netRegistry[$key].WowSystemDefaultTlsVersionsValue) {
$params = @{
Name = "SystemDefaultTlsVersions is not configured as
expected"
List = $[Link]
Action = "Configure SystemDefaultTlsVersions for $key
as described here: [Link]
}
$[Link]((NewActionObject @params))
Write-Verbose "SystemDefaultTlsVersions doesn't match the
expected configuration"
}
}
}

if ($[Link] -gt 1) {
$params = @{
Name = "Multiple TLS differences have been detected"
Action = "Please ensure that all servers are running the same
TLS configuration"
}
$action = NewActionObject @params
$[Link]($action)
}
}
} end {
return [PSCustomObject]@{
CheckPassed = ($[Link] -eq 0)
TlsSettings = $tlsGroupedResults
ActionsRequired = $actionsRequiredList
}
}
}

function Write-Host {
[[Link]('PSAvoidOverwritingBuiltInCmdlet
s', '', Justification = 'Proper handling of write host with colors')]
[CmdletBinding()]
param(
[Parameter(Position = 1, ValueFromPipeline)]
[object]$Object,
[switch]$NoNewLine,
[string]$ForegroundColor
)
process {
$consoleHost = $[Link] -eq "ConsoleHost"

if ($null -ne $Script:WriteHostManipulateObjectAction) {


$Object = & $Script:WriteHostManipulateObjectAction $Object
}

$params = @{
Object = $Object
NoNewLine = $NoNewLine
}

if ([string]::IsNullOrEmpty($ForegroundColor)) {
if ($null -ne $[Link] -and
$consoleHost) {
$[Link]("ForegroundColor", $[Link])
}
} elseif ($ForegroundColor -eq "Yellow" -and
$consoleHost -and
$null -ne $[Link]) {
$[Link]("ForegroundColor",
$[Link])
} elseif ($ForegroundColor -eq "Red" -and
$consoleHost -and
$null -ne $[Link]) {
$[Link]("ForegroundColor", $[Link])
} else {
$[Link]("ForegroundColor", $ForegroundColor)
}

[Link]\Write-Host @params

if ($null -ne $Script:WriteHostDebugAction -and


$null -ne $Object) {
&$Script:WriteHostDebugAction $Object
}
}
}

function SetProperForegroundColor {
$Script:OriginalConsoleForegroundColor = $[Link]

if ($[Link] -eq
$[Link]) {
Write-Verbose "Foreground Color matches warning's color"

if ($[Link] -ne "Gray") {


$[Link] = "Gray"
}
}
if ($[Link] -eq $[Link])
{
Write-Verbose "Foreground Color matches error's color"

if ($[Link] -ne "Gray") {


$[Link] = "Gray"
}
}
}

function RevertProperForegroundColor {
$[Link] = $Script:OriginalConsoleForegroundColor
}

function SetWriteHostAction ($DebugAction) {


$Script:WriteHostDebugAction = $DebugAction
}

function SetWriteHostManipulateObjectAction ($ManipulateObject) {


$Script:WriteHostManipulateObjectAction = $ManipulateObject
}

function Write-Progress {

[[Link]('PSAvoidOverwritingBuiltInCmdlet
s', '', Justification = 'In order to log Write-Warning from Shared functions')]
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Activity = "",

[switch]$Completed,

[string]$CurrentOperation,

[Parameter(Position = 2)]
[int]$Id,

[int]$ParentId = -1,

[int]$PercentComplete,

[int]$SecondsRemaining = -1,

[int]$SourceId,

[Parameter(Position = 1)]
[string]$Status
)

process {
$params = @{
Activity = $Activity
Completed = $Completed
CurrentOperation = $CurrentOperation
Id = $Id
ParentId = $ParentId
PercentComplete = $PercentComplete
SecondsRemaining = $SecondsRemaining
SourceId = $SourceId
}

if (-not([string]::IsNullOrEmpty($Status))) {
$[Link]("Status", $Status)
}

[Link]\Write-Progress @params

$message = "Write-Progress Activity: '$Activity' Completed: $Completed


CurrentOperation: '$CurrentOperation' Id: $Id" +
" ParentId: $ParentId PercentComplete: $PercentComplete SecondsRemaining:
$SecondsRemaining SourceId: $SourceId Status: '$Status'"

if ($null -ne $Script:WriteProgressDebugAction) {


& $Script:WriteProgressDebugAction $message
}

if ($PSSenderInfo -and
$null -ne $Script:WriteRemoteProgressDebugAction) {
& $Script:WriteRemoteProgressDebugAction $message
}
}
}

function SetWriteProgressAction ($DebugAction) {


$Script:WriteProgressDebugAction = $DebugAction
}

function SetWriteRemoteProgressAction ($DebugAction) {


$Script:WriteRemoteProgressDebugAction = $DebugAction
}

function Write-Verbose {

[[Link]('PSAvoidOverwritingBuiltInCmdlet
s', '', Justification = 'In order to log Write-Verbose from Shared functions')]
[CmdletBinding()]
param(
[Parameter(Position = 1, ValueFromPipeline)]
[string]$Message
)

process {

if ($null -ne $Script:WriteVerboseManipulateMessageAction) {


$Message = & $Script:WriteVerboseManipulateMessageAction $Message
}

if ($PSSenderInfo -and
$null -ne $Script:WriteVerboseRemoteManipulateMessageAction) {
$Message = & $Script:WriteVerboseRemoteManipulateMessageAction $Message
}

[Link]\Write-Verbose $Message

if ($null -ne $Script:WriteVerboseDebugAction) {


& $Script:WriteVerboseDebugAction $Message
}

# $PSSenderInfo is set when in a remote context


if ($PSSenderInfo -and
$null -ne $Script:WriteRemoteVerboseDebugAction) {
& $Script:WriteRemoteVerboseDebugAction $Message
}
}
}

function SetWriteVerboseAction ($DebugAction) {


$Script:WriteVerboseDebugAction = $DebugAction
}

function SetWriteRemoteVerboseAction ($DebugAction) {


$Script:WriteRemoteVerboseDebugAction = $DebugAction
}

function SetWriteVerboseManipulateMessageAction ($DebugAction) {


$Script:WriteVerboseManipulateMessageAction = $DebugAction
}

function SetWriteVerboseRemoteManipulateMessageAction ($DebugAction) {


$Script:WriteVerboseRemoteManipulateMessageAction = $DebugAction
}

function Write-Warning {

[[Link]('PSAvoidOverwritingBuiltInCmdlet
s', '', Justification = 'In order to log Write-Warning from Shared functions')]
[CmdletBinding()]
param(
[Parameter(Position = 1, ValueFromPipeline)]
[string]$Message
)
process {

if ($null -ne $Script:WriteWarningManipulateMessageAction) {


$Message = & $Script:WriteWarningManipulateMessageAction $Message
}

[Link]\Write-Warning $Message

# Add WARNING to beginning of the message by default.


$Message = "WARNING: $Message"

if ($null -ne $Script:WriteWarningDebugAction) {


& $Script:WriteWarningDebugAction $Message
}

# $PSSenderInfo is set when in a remote context


if ($PSSenderInfo -and
$null -ne $Script:WriteRemoteWarningDebugAction) {
& $Script:WriteRemoteWarningDebugAction $Message
}
}
}

function SetWriteWarningAction ($DebugAction) {


$Script:WriteWarningDebugAction = $DebugAction
}

function SetWriteRemoteWarningAction ($DebugAction) {


$Script:WriteRemoteWarningDebugAction = $DebugAction
}

function SetWriteWarningManipulateMessageAction ($DebugAction) {


$Script:WriteWarningManipulateMessageAction = $DebugAction
}

# This function is used to determine the version of Exchange based off a build
number or
# by providing the Exchange Version and CU and/or SU. This provides one location in
the entire repository
# that is required to be updated for when a new release of Exchange is dropped.
function Get-ExchangeBuildVersionInformation {
[CmdletBinding(DefaultParameterSetName = "AdminDisplayVersion")]
param(
[Parameter(ParameterSetName = "AdminDisplayVersion", Position = 1)]
[object]$AdminDisplayVersion,

[Parameter(ParameterSetName = "ExSetup")]
[[Link]]$FileVersion,

[Parameter(ParameterSetName = "VersionCU", Mandatory = $true)]


[ValidateScript( { ValidateVersionParameter $_ } )]
[string]$Version,

[Parameter(ParameterSetName = "VersionCU", Mandatory = $true)]


[ValidateScript( { ValidateCUParameter $_ } )]
[string]$CU,

[Parameter(ParameterSetName = "VersionCU", Mandatory = $false)]


[ValidateScript( { ValidateSUParameter $_ } )]
[string]$SU,

[Parameter(ParameterSetName = "FindSUBuilds", Mandatory = $true)]


[ValidateScript( { ValidateSUParameter $_ } )]
[string]$FindBySUName,

[Parameter(Mandatory = $false)]
[ScriptBlock]$CatchActionFunction
)
begin {

function GetBuildVersion {
param(
[Parameter(Position = 1)]
[string]$ExchangeVersion,
[Parameter(Position = 2)]
[string]$CU,
[Parameter(Position = 3)]
[string]$SU
)
$cuResult = $exchangeBuildDictionary[$ExchangeVersion][$CU]
if ((-not [string]::IsNullOrEmpty($SU)) -and
$[Link]($SU)) {
return $[Link][$SU]
} else {
return $[Link]
}
}

# Dictionary of Exchange Version/CU/SU to build number


$exchangeBuildDictionary = GetExchangeBuildDictionary

Write-Verbose "Calling: $($[Link])"


$exchangeMajorVersion = [string]::Empty
$exchangeVersion = $null
$supportedBuildNumber = $false
$latestSUBuild = $false
$extendedSupportDate = [string]::Empty
$cuReleaseDate = [string]::Empty
$friendlyName = [string]::Empty
$cuLevel = [string]::Empty
$suName = [string]::Empty
$orgValue = 0
$schemaValue = 0
$mesoValue = 0
$ex19 = "Exchange2019"
$ex16 = "Exchange2016"
$ex13 = "Exchange2013"
}
process {
# Convert both input types to a [[Link]]
try {
if ($[Link] -eq "FindSUBuilds") {
foreach ($exchangeKey in $[Link]) {
foreach ($cuKey in $exchangeBuildDictionary[$exchangeKey].Keys)
{
if ($null -ne $exchangeBuildDictionary[$exchangeKey]
[$cuKey].SU -and
$exchangeBuildDictionary[$exchangeKey]
[$cuKey].[Link]($FindBySUName)) {
Get-ExchangeBuildVersionInformation -FileVersion
$exchangeBuildDictionary[$exchangeKey][$cuKey].SU[$FindBySUName]
}
}
}
return
} elseif ($[Link] -eq "VersionCU") {
[[Link]]$exchangeVersion = GetBuildVersion -ExchangeVersion
$Version -CU $CU -SU $SU
} elseif ($[Link] -eq "AdminDisplayVersion") {
$AdminDisplayVersion = $[Link]()
Write-Verbose "Passed AdminDisplayVersion: $AdminDisplayVersion"
$split1 =
$[Link](($[Link](" ")) + 1,
4).Split(".")
$buildStart = $[Link](" ") + 1
$split2 = $[Link]($buildStart,
($[Link](")") - $buildStart)).Split(".")
[[Link]]$exchangeVersion = "$($split1[0]).$($split1[1]).$
($split2[0]).$($split2[1])"
} else {
[[Link]]$exchangeVersion = $FileVersion
}
} catch {
Write-Verbose "Failed to convert to [Link]"
Invoke-CatchActionError $CatchActionFunction
}

<#
Exchange Build Numbers: [Link]
features/build-numbers-and-release-dates?view=exchserver-2019
Exchange 2016 & 2019 AD Changes:
[Link]
view=exchserver-2019
Exchange 2013 AD Changes:
[Link]
exchange-2013-help
#>
if ($[Link] -eq 15 -and $[Link] -eq 2) {
Write-Verbose "Exchange 2019 is detected"
$exchangeMajorVersion = "Exchange2019"
$extendedSupportDate = "10/14/2025"
$friendlyName = "Exchange 2019"

#Latest Version AD Settings


$schemaValue = 17003
$mesoValue = 13243
$orgValue = 16763

switch ($exchangeVersion) {
{ $_ -ge (GetBuildVersion $ex19 "CU15") } {
$cuLevel = "CU15"
$cuReleaseDate = "02/10/2025"
$supportedBuildNumber = $true
$latestSUBuild = $true
}
(GetBuildVersion $ex19 "CU15" -SU "Apr25HU") { $latestSUBuild =
$true }
{ $_ -lt (GetBuildVersion $ex19 "CU15") } {
$cuLevel = "CU14"
$cuReleaseDate = "02/13/2024"
$supportedBuildNumber = $true
$orgValue = 16762
}
(GetBuildVersion $ex19 "CU14" -SU "Apr25HU") { $latestSUBuild =
$true }
(GetBuildVersion $ex19 "CU14" -SU "Nov24SUv2") { $latestSUBuild =
$true }
{ $_ -lt (GetBuildVersion $ex19 "CU14") } {
$cuLevel = "CU13"
$cuReleaseDate = "05/03/2023"
$supportedBuildNumber = $false
$orgValue = 16761
}
# Technically the SU is still secure. Might need to change pester
testing on this to make it okay. But it is complaining about the second SU both
being on the latest.
# for now just going to leave as is as this might change with
upcoming releases.
(GetBuildVersion $ex19 "CU13" -SU "Nov24SUv2") { $latestSUBuild =
$true }
{ $_ -lt (GetBuildVersion $ex19 "CU13") } {
$cuLevel = "CU12"
$cuReleaseDate = "04/20/2022"
$orgValue = 16760
}
{ $_ -lt (GetBuildVersion $ex19 "CU12") } {
$cuLevel = "CU11"
$cuReleaseDate = "09/28/2021"
$mesoValue = 13242
$orgValue = 16759
}
(GetBuildVersion $ex19 "CU11" -SU "May22SU") { $mesoValue = 13243 }
{ $_ -lt (GetBuildVersion $ex19 "CU11") } {
$cuLevel = "CU10"
$cuReleaseDate = "06/29/2021"
$mesoValue = 13241
$orgValue = 16758
}
{ $_ -lt (GetBuildVersion $ex19 "CU10") } {
$cuLevel = "CU9"
$cuReleaseDate = "03/16/2021"
$schemaValue = 17002
$mesoValue = 13240
$orgValue = 16757
}
{ $_ -lt (GetBuildVersion $ex19 "CU9") } {
$cuLevel = "CU8"
$cuReleaseDate = "12/15/2020"
$mesoValue = 13239
$orgValue = 16756
}
{ $_ -lt (GetBuildVersion $ex19 "CU8") } {
$cuLevel = "CU7"
$cuReleaseDate = "09/15/2020"
$schemaValue = 17001
$mesoValue = 13238
$orgValue = 16755
}
{ $_ -lt (GetBuildVersion $ex19 "CU7") } {
$cuLevel = "CU6"
$cuReleaseDate = "06/16/2020"
$mesoValue = 13237
$orgValue = 16754
}
{ $_ -lt (GetBuildVersion $ex19 "CU6") } {
$cuLevel = "CU5"
$cuReleaseDate = "03/17/2020"
}
{ $_ -lt (GetBuildVersion $ex19 "CU5") } {
$cuLevel = "CU4"
$cuReleaseDate = "12/17/2019"
}
{ $_ -lt (GetBuildVersion $ex19 "CU4") } {
$cuLevel = "CU3"
$cuReleaseDate = "09/17/2019"
}
{ $_ -lt (GetBuildVersion $ex19 "CU3") } {
$cuLevel = "CU2"
$cuReleaseDate = "06/18/2019"
}
{ $_ -lt (GetBuildVersion $ex19 "CU2") } {
$cuLevel = "CU1"
$cuReleaseDate = "02/12/2019"
$schemaValue = 17000
$mesoValue = 13236
$orgValue = 16752
}
{ $_ -lt (GetBuildVersion $ex19 "CU1") } {
$cuLevel = "RTM"
$cuReleaseDate = "10/22/2018"
$orgValue = 16751
}
}
} elseif ($[Link] -eq 15 -and $[Link] -eq 1)
{
Write-Verbose "Exchange 2016 is detected"
$exchangeMajorVersion = "Exchange2016"
$extendedSupportDate = "10/14/2025"
$friendlyName = "Exchange 2016"

#Latest Version AD Settings


$schemaValue = 15334
$mesoValue = 13243
$orgValue = 16223

switch ($exchangeVersion) {
{ $_ -ge (GetBuildVersion $ex16 "CU23") } {
$cuLevel = "CU23"
$cuReleaseDate = "04/20/2022"
$supportedBuildNumber = $true
}
(GetBuildVersion $ex16 "CU23" -SU "Apr25HU") { $latestSUBuild =
$true }
(GetBuildVersion $ex16 "CU23" -SU "Nov24SUv2") { $latestSUBuild =
$true }
{ $_ -lt (GetBuildVersion $ex16 "CU23") } {
$cuLevel = "CU22"
$cuReleaseDate = "09/28/2021"
$supportedBuildNumber = $false
$mesoValue = 13242
$orgValue = 16222
}
(GetBuildVersion $ex16 "CU22" -SU "May22SU") { $mesoValue = 13243 }
{ $_ -lt (GetBuildVersion $ex16 "CU22") } {
$cuLevel = "CU21"
$cuReleaseDate = "06/29/2021"
$mesoValue = 13241
$orgValue = 16221
}
{ $_ -lt (GetBuildVersion $ex16 "CU21") } {
$cuLevel = "CU20"
$cuReleaseDate = "03/16/2021"
$schemaValue = 15333
$mesoValue = 13240
$orgValue = 16220
}
{ $_ -lt (GetBuildVersion $ex16 "CU20") } {
$cuLevel = "CU19"
$cuReleaseDate = "12/15/2020"
$mesoValue = 13239
$orgValue = 16219
}
{ $_ -lt (GetBuildVersion $ex16 "CU19") } {
$cuLevel = "CU18"
$cuReleaseDate = "09/15/2020"
$schemaValue = 15332
$mesoValue = 13238
$orgValue = 16218
}
{ $_ -lt (GetBuildVersion $ex16 "CU18") } {
$cuLevel = "CU17"
$cuReleaseDate = "06/16/2020"
$mesoValue = 13237
$orgValue = 16217
}
{ $_ -lt (GetBuildVersion $ex16 "CU17") } {
$cuLevel = "CU16"
$cuReleaseDate = "03/17/2020"
}
{ $_ -lt (GetBuildVersion $ex16 "CU16") } {
$cuLevel = "CU15"
$cuReleaseDate = "12/17/2019"
}
{ $_ -lt (GetBuildVersion $ex16 "CU15") } {
$cuLevel = "CU14"
$cuReleaseDate = "09/17/2019"
}
{ $_ -lt (GetBuildVersion $ex16 "CU14") } {
$cuLevel = "CU13"
$cuReleaseDate = "06/18/2019"
}
{ $_ -lt (GetBuildVersion $ex16 "CU13") } {
$cuLevel = "CU12"
$cuReleaseDate = "02/12/2019"
$mesoValue = 13236
$orgValue = 16215
}
{ $_ -lt (GetBuildVersion $ex16 "CU12") } {
$cuLevel = "CU11"
$cuReleaseDate = "10/16/2018"
$orgValue = 16214
}
{ $_ -lt (GetBuildVersion $ex16 "CU11") } {
$cuLevel = "CU10"
$cuReleaseDate = "06/19/2018"
$orgValue = 16213
}
{ $_ -lt (GetBuildVersion $ex16 "CU10") } {
$cuLevel = "CU9"
$cuReleaseDate = "03/20/2018"
}
{ $_ -lt (GetBuildVersion $ex16 "CU9") } {
$cuLevel = "CU8"
$cuReleaseDate = "12/19/2017"
}
{ $_ -lt (GetBuildVersion $ex16 "CU8") } {
$cuLevel = "CU7"
$cuReleaseDate = "09/16/2017"
}
{ $_ -lt (GetBuildVersion $ex16 "CU7") } {
$cuLevel = "CU6"
$cuReleaseDate = "06/24/2017"
$schemaValue = 15330
}
{ $_ -lt (GetBuildVersion $ex16 "CU6") } {
$cuLevel = "CU5"
$cuReleaseDate = "03/21/2017"
$schemaValue = 15326
}
{ $_ -lt (GetBuildVersion $ex16 "CU5") } {
$cuLevel = "CU4"
$cuReleaseDate = "12/13/2016"
}
{ $_ -lt (GetBuildVersion $ex16 "CU4") } {
$cuLevel = "CU3"
$cuReleaseDate = "09/20/2016"
$orgValue = 16212
}
{ $_ -lt (GetBuildVersion $ex16 "CU3") } {
$cuLevel = "CU2"
$cuReleaseDate = "06/21/2016"
$schemaValue = 15325
}
{ $_ -lt (GetBuildVersion $ex16 "CU2") } {
$cuLevel = "CU1"
$cuReleaseDate = "03/15/2016"
$schemaValue = 15323
$orgValue = 16211
}
}
} elseif ($[Link] -eq 15 -and $[Link] -eq 0)
{
Write-Verbose "Exchange 2013 is detected"
$exchangeMajorVersion = "Exchange2013"
$extendedSupportDate = "04/11/2023"
$friendlyName = "Exchange 2013"

#Latest Version AD Settings


$schemaValue = 15312
$mesoValue = 13237
$orgValue = 16133

switch ($exchangeVersion) {
{ $_ -ge (GetBuildVersion $ex13 "CU23") } {
$cuLevel = "CU23"
$cuReleaseDate = "06/18/2019"
$supportedBuildNumber = $true
}
(GetBuildVersion $ex13 "CU23" -SU "May22SU") { $mesoValue = 13238 }
{ $_ -lt (GetBuildVersion $ex13 "CU23") } {
$cuLevel = "CU22"
$cuReleaseDate = "02/12/2019"
$mesoValue = 13236
$orgValue = 16131
$supportedBuildNumber = $false
}
{ $_ -lt (GetBuildVersion $ex13 "CU22") } {
$cuLevel = "CU21"
$cuReleaseDate = "06/19/2018"
$orgValue = 16130
}
{ $_ -lt (GetBuildVersion $ex13 "CU21") } {
$cuLevel = "CU20"
$cuReleaseDate = "03/20/2018"
}
{ $_ -lt (GetBuildVersion $ex13 "CU20") } {
$cuLevel = "CU19"
$cuReleaseDate = "12/19/2017"
}
{ $_ -lt (GetBuildVersion $ex13 "CU19") } {
$cuLevel = "CU18"
$cuReleaseDate = "09/16/2017"
}
{ $_ -lt (GetBuildVersion $ex13 "CU18") } {
$cuLevel = "CU17"
$cuReleaseDate = "06/24/2017"
}
{ $_ -lt (GetBuildVersion $ex13 "CU17") } {
$cuLevel = "CU16"
$cuReleaseDate = "03/21/2017"
}
{ $_ -lt (GetBuildVersion $ex13 "CU16") } {
$cuLevel = "CU15"
$cuReleaseDate = "12/13/2016"
}
{ $_ -lt (GetBuildVersion $ex13 "CU15") } {
$cuLevel = "CU14"
$cuReleaseDate = "09/20/2016"
}
{ $_ -lt (GetBuildVersion $ex13 "CU14") } {
$cuLevel = "CU13"
$cuReleaseDate = "06/21/2016"
}
{ $_ -lt (GetBuildVersion $ex13 "CU13") } {
$cuLevel = "CU12"
$cuReleaseDate = "03/15/2016"
}
{ $_ -lt (GetBuildVersion $ex13 "CU12") } {
$cuLevel = "CU11"
$cuReleaseDate = "12/15/2015"
}
{ $_ -lt (GetBuildVersion $ex13 "CU11") } {
$cuLevel = "CU10"
$cuReleaseDate = "09/15/2015"
}
{ $_ -lt (GetBuildVersion $ex13 "CU10") } {
$cuLevel = "CU9"
$cuReleaseDate = "06/17/2015"
$orgValue = 15965
}
{ $_ -lt (GetBuildVersion $ex13 "CU9") } {
$cuLevel = "CU8"
$cuReleaseDate = "03/17/2015"
}
{ $_ -lt (GetBuildVersion $ex13 "CU8") } {
$cuLevel = "CU7"
$cuReleaseDate = "12/09/2014"
}
{ $_ -lt (GetBuildVersion $ex13 "CU7") } {
$cuLevel = "CU6"
$cuReleaseDate = "08/26/2014"
$schemaValue = 15303
}
{ $_ -lt (GetBuildVersion $ex13 "CU6") } {
$cuLevel = "CU5"
$cuReleaseDate = "05/27/2014"
$schemaValue = 15300
$orgValue = 15870
}
{ $_ -lt (GetBuildVersion $ex13 "CU5") } {
$cuLevel = "CU4"
$cuReleaseDate = "02/25/2014"
$schemaValue = 15292
$orgValue = 15844
}
{ $_ -lt (GetBuildVersion $ex13 "CU4") } {
$cuLevel = "CU3"
$cuReleaseDate = "11/25/2013"
$schemaValue = 15283
$orgValue = 15763
}
{ $_ -lt (GetBuildVersion $ex13 "CU3") } {
$cuLevel = "CU2"
$cuReleaseDate = "07/09/2013"
$schemaValue = 15281
$orgValue = 15688
}
{ $_ -lt (GetBuildVersion $ex13 "CU2") } {
$cuLevel = "CU1"
$cuReleaseDate = "04/02/2013"
$schemaValue = 15254
$orgValue = 15614
}
}
} else {
Write-Verbose "Unknown version of Exchange is detected."
}

# Now get the SU Name


if ([string]::IsNullOrEmpty($exchangeMajorVersion) -or
[string]::IsNullOrEmpty($cuLevel)) {
Write-Verbose "Can't lookup when keys aren't set"
return
}

$currentSUInfo = $exchangeBuildDictionary[$exchangeMajorVersion]
[$cuLevel].SU
$compareValue = $[Link]()
if ($null -ne $currentSUInfo -and
$[Link]($compareValue)) {
foreach ($key in $[Link]) {
if ($compareValue -eq $currentSUInfo[$key]) {
$suName = $key
}
}
}
}
end {

if ($[Link] -eq "FindSUBuilds") {


Write-Verbose "Return nothing here, results were already returned on
the pipeline"
return
}

$friendlyName = "$friendlyName $cuLevel $suName".Trim()


Write-Verbose "Determined Build Version $friendlyName"
return [PSCustomObject]@{
MajorVersion = $exchangeMajorVersion
FriendlyName = $friendlyName
BuildVersion = $exchangeVersion
CU = $cuLevel
ReleaseDate = if (-
not([[Link]]::IsNullOrEmpty($cuReleaseDate)))
{ ([[Link]]::ToDateTime([DateTime]$cuReleaseDate,
[[Link]]::InvariantInfo)) } else { $null }
ExtendedSupportDate = if (-
not([[Link]]::IsNullOrEmpty($extendedSupportDate)))
{ ([[Link]]::ToDateTime([DateTime]$extendedSupportDate,
[[Link]]::InvariantInfo)) } else { $null }
Supported = $supportedBuildNumber
LatestSU = $latestSUBuild
ADLevel = [PSCustomObject]@{
SchemaValue = $schemaValue
MESOValue = $mesoValue
OrgValue = $orgValue
}
}
}
}

function GetExchangeBuildDictionary {

function NewCUAndSUObject {
param(
[string]$CUBuildNumber,
[Hashtable]$SUBuildNumber
)
return @{
"CU" = $CUBuildNumber
"SU" = $SUBuildNumber
}
}

@{
"Exchange2013" = @{
"CU1" = (NewCUAndSUObject "[Link]")
"CU2" = (NewCUAndSUObject "[Link]")
"CU3" = (NewCUAndSUObject "[Link]")
"CU4" = (NewCUAndSUObject "[Link]")
"CU5" = (NewCUAndSUObject "[Link]")
"CU6" = (NewCUAndSUObject "[Link]")
"CU7" = (NewCUAndSUObject "15.0.1044.25")
"CU8" = (NewCUAndSUObject "15.0.1076.9")
"CU9" = (NewCUAndSUObject "15.0.1104.5")
"CU10" = (NewCUAndSUObject "15.0.1130.7")
"CU11" = (NewCUAndSUObject "15.0.1156.6")
"CU12" = (NewCUAndSUObject "15.0.1178.4")
"CU13" = (NewCUAndSUObject "15.0.1210.3")
"CU14" = (NewCUAndSUObject "15.0.1236.3")
"CU15" = (NewCUAndSUObject "15.0.1263.5")
"CU16" = (NewCUAndSUObject "15.0.1293.2")
"CU17" = (NewCUAndSUObject "15.0.1320.4")
"CU18" = (NewCUAndSUObject "15.0.1347.2" @{
"Mar18SU" = "15.0.1347.5"
})
"CU19" = (NewCUAndSUObject "15.0.1365.1" @{
"Mar18SU" = "15.0.1365.3"
"May18SU" = "15.0.1365.7"
})
"CU20" = (NewCUAndSUObject "15.0.1367.3" @{
"May18SU" = "15.0.1367.6"
"Aug18SU" = "15.0.1367.9"
})
"CU21" = (NewCUAndSUObject "15.0.1395.4" @{
"Aug18SU" = "15.0.1395.7"
"Oct18SU" = "15.0.1395.8"
"Jan19SU" = "15.0.1395.10"
"Mar21SU" = "15.0.1395.12"
})
"CU22" = (NewCUAndSUObject "15.0.1473.3" @{
"Feb19SU" = "15.0.1473.3"
"Apr19SU" = "15.0.1473.4"
"Jun19SU" = "15.0.1473.5"
"Mar21SU" = "15.0.1473.6"
})
"CU23" = (NewCUAndSUObject "15.0.1497.2" @{
"Jul19SU" = "15.0.1497.3"
"Nov19SU" = "15.0.1497.4"
"Feb20SU" = "15.0.1497.6"
"Oct20SU" = "15.0.1497.7"
"Nov20SU" = "15.0.1497.8"
"Dec20SU" = "15.0.1497.10"
"Mar21SU" = "15.0.1497.12"
"Apr21SU" = "15.0.1497.15"
"May21SU" = "15.0.1497.18"
"Jul21SU" = "15.0.1497.23"
"Oct21SU" = "15.0.1497.24"
"Nov21SU" = "15.0.1497.26"
"Jan22SU" = "15.0.1497.28"
"Mar22SU" = "15.0.1497.33"
"May22SU" = "15.0.1497.36"
"Aug22SU" = "15.0.1497.40"
"Oct22SU" = "15.0.1497.42"
"Nov22SU" = "15.0.1497.44"
"Jan23SU" = "15.0.1497.45"
"Feb23SU" = "15.0.1497.47"
"Mar23SU" = "15.0.1497.48"
})
}
"Exchange2016" = @{
"CU1" = (NewCUAndSUObject "[Link]")
"CU2" = (NewCUAndSUObject "[Link]")
"CU3" = (NewCUAndSUObject "[Link]")
"CU4" = (NewCUAndSUObject "[Link]")
"CU5" = (NewCUAndSUObject "[Link]")
"CU6" = (NewCUAndSUObject "15.1.1034.26")
"CU7" = (NewCUAndSUObject "15.1.1261.35" @{
"Mar18SU" = "15.1.1261.39"
})
"CU8" = (NewCUAndSUObject "15.1.1415.2" @{
"Mar18SU" = "15.1.1415.4"
"May18SU" = "15.1.1415.7"
"Mar21SU" = "15.1.1415.8"
})
"CU9" = (NewCUAndSUObject "15.1.1466.3" @{
"May18SU" = "15.1.1466.8"
"Aug18SU" = "15.1.1466.9"
"Mar21SU" = "15.1.1466.13"
})
"CU10" = (NewCUAndSUObject "15.1.1531.3" @{
"Aug18SU" = "15.1.1531.6"
"Oct18SU" = "15.1.1531.8"
"Jan19SU" = "15.1.1531.10"
"Mar21SU" = "15.1.1531.12"
})
"CU11" = (NewCUAndSUObject "15.1.1591.10" @{
"Dec18SU" = "15.1.1591.11"
"Jan19SU" = "15.1.1591.13"
"Apr19SU" = "15.1.1591.16"
"Jun19SU" = "15.1.1591.17"
"Mar21SU" = "15.1.1591.18"
})
"CU12" = (NewCUAndSUObject "15.1.1713.5" @{
"Feb19SU" = "15.1.1713.5"
"Apr19SU" = "15.1.1713.6"
"Jun19SU" = "15.1.1713.7"
"Jul19SU" = "15.1.1713.8"
"Sep19SU" = "15.1.1713.9"
"Mar21SU" = "15.1.1713.10"
})
"CU13" = (NewCUAndSUObject "15.1.1779.2" @{
"Jul19SU" = "15.1.1779.4"
"Sep19SU" = "15.1.1779.5"
"Nov19SU" = "15.1.1779.7"
"Mar21SU" = "15.1.1779.8"
})
"CU14" = (NewCUAndSUObject "15.1.1847.3" @{
"Nov19SU" = "15.1.1847.5"
"Feb20SU" = "15.1.1847.7"
"Mar20SU" = "15.1.1847.10"
"Mar21SU" = "15.1.1847.12"
})
"CU15" = (NewCUAndSUObject "15.1.1913.5" @{
"Feb20SU" = "15.1.1913.7"
"Mar20SU" = "15.1.1913.10"
"Mar21SU" = "15.1.1913.12"
})
"CU16" = (NewCUAndSUObject "15.1.1979.3" @{
"Sep20SU" = "15.1.1979.6"
"Mar21SU" = "15.1.1979.8"
})
"CU17" = (NewCUAndSUObject "15.1.2044.4" @{
"Sep20SU" = "15.1.2044.6"
"Oct20SU" = "15.1.2044.7"
"Nov20SU" = "15.1.2044.8"
"Dec20SU" = "15.1.2044.12"
"Mar21SU" = "15.1.2044.13"
})
"CU18" = (NewCUAndSUObject "15.1.2106.2" @{
"Oct20SU" = "15.1.2106.3"
"Nov20SU" = "15.1.2106.4"
"Dec20SU" = "15.1.2106.6"
"Feb21SU" = "15.1.2106.8"
"Mar21SU" = "15.1.2106.13"
})
"CU19" = (NewCUAndSUObject "15.1.2176.2" @{
"Feb21SU" = "15.1.2176.4"
"Mar21SU" = "15.1.2176.9"
"Apr21SU" = "15.1.2176.12"
"May21SU" = "15.1.2176.14"
})
"CU20" = (NewCUAndSUObject "15.1.2242.4" @{
"Apr21SU" = "15.1.2242.8"
"May21SU" = "15.1.2242.10"
"Jul21SU" = "15.1.2242.12"
})
"CU21" = (NewCUAndSUObject "15.1.2308.8" @{
"Jul21SU" = "15.1.2308.14"
"Oct21SU" = "15.1.2308.15"
"Nov21SU" = "15.1.2308.20"
"Jan22SU" = "15.1.2308.21"
"Mar22SU" = "15.1.2308.27"
})
"CU22" = (NewCUAndSUObject "15.1.2375.7" @{
"Oct21SU" = "15.1.2375.12"
"Nov21SU" = "15.1.2375.17"
"Jan22SU" = "15.1.2375.18"
"Mar22SU" = "15.1.2375.24"
"May22SU" = "15.1.2375.28"
"Aug22SU" = "15.1.2375.31"
"Oct22SU" = "15.1.2375.32"
"Nov22SU" = "15.1.2375.37"
})
"CU23" = (NewCUAndSUObject "15.1.2507.6" @{
"May22SU" = "15.1.2507.9"
"Aug22SU" = "15.1.2507.12"
"Oct22SU" = "15.1.2507.13"
"Nov22SU" = "15.1.2507.16"
"Jan23SU" = "15.1.2507.17"
"Feb23SU" = "15.1.2507.21"
"Mar23SU" = "15.1.2507.23"
"Jun23SU" = "15.1.2507.27"
"Aug23SU" = "15.1.2507.31"
"Aug23SUv2" = "15.1.2507.32"
"Oct23SU" = "15.1.2507.34"
"Nov23SU" = "15.1.2507.35"
"Mar24SU" = "15.1.2507.37"
"Apr24HU" = "15.1.2507.39"
"Nov24SU" = "15.1.2507.43"
"Nov24SUv2" = "15.1.2507.44"
"Apr25HU" = "15.1.2507.55"
})
}
"Exchange2019" = @{
"CU1" = (NewCUAndSUObject "[Link]" @{
"Feb19SU" = "[Link]"
"Apr19SU" = "[Link]"
"Jun19SU" = "[Link]"
"Jul19SU" = "[Link]"
"Sep19SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU2" = (NewCUAndSUObject "[Link]" @{
"Jul19SU" = "[Link]"
"Sep19SU" = "[Link]"
"Nov19SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU3" = (NewCUAndSUObject "[Link]" @{
"Nov19SU" = "[Link]"
"Feb20SU" = "[Link]"
"Mar20SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU4" = (NewCUAndSUObject "[Link]" @{
"Feb20SU" = "[Link]"
"Mar20SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU5" = (NewCUAndSUObject "[Link]" @{
"Sep20SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU6" = (NewCUAndSUObject "[Link]" @{
"Sep20SU" = "[Link]"
"Oct20SU" = "[Link]"
"Nov20SU" = "[Link]"
"Dec20SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU7" = (NewCUAndSUObject "[Link]" @{
"Oct20SU" = "[Link]"
"Nov20SU" = "[Link]"
"Dec20SU" = "[Link]"
"Feb21SU" = "[Link]"
"Mar21SU" = "[Link]"
})
"CU8" = (NewCUAndSUObject "[Link]" @{
"Feb21SU" = "[Link]"
"Mar21SU" = "[Link]"
"Apr21SU" = "[Link]"
"May21SU" = "[Link]"
})
"CU9" = (NewCUAndSUObject "[Link]" @{
"Apr21SU" = "[Link]"
"May21SU" = "[Link]"
"Jul21SU" = "[Link]"
})
"CU10" = (NewCUAndSUObject "[Link]" @{
"Jul21SU" = "[Link]"
"Oct21SU" = "[Link]"
"Nov21SU" = "[Link]"
"Jan22SU" = "[Link]"
"Mar22SU" = "[Link]"
})
"CU11" = (NewCUAndSUObject "[Link]" @{
"Oct21SU" = "[Link]"
"Nov21SU" = "[Link]"
"Jan22SU" = "[Link]"
"Mar22SU" = "[Link]"
"May22SU" = "[Link]"
"Aug22SU" = "[Link]"
"Oct22SU" = "[Link]"
"Nov22SU" = "[Link]"
"Jan23SU" = "[Link]"
"Feb23SU" = "[Link]"
"Mar23SU" = "[Link]"
})
"CU12" = (NewCUAndSUObject "15.2.1118.7" @{
"May22SU" = "15.2.1118.9"
"Aug22SU" = "15.2.1118.12"
"Oct22SU" = "15.2.1118.15"
"Nov22SU" = "15.2.1118.20"
"Jan23SU" = "15.2.1118.21"
"Feb23SU" = "15.2.1118.25"
"Mar23SU" = "15.2.1118.26"
"Jun23SU" = "15.2.1118.30"
"Aug23SU" = "15.2.1118.36"
"Aug23SUv2" = "15.2.1118.37"
"Oct23SU" = "15.2.1118.39"
"Nov23SU" = "15.2.1118.40"
})
"CU13" = (NewCUAndSUObject "15.2.1258.12" @{
"Jun23SU" = "15.2.1258.16"
"Aug23SU" = "15.2.1258.23"
"Aug23SUv2" = "15.2.1258.25"
"Oct23SU" = "15.2.1258.27"
"Nov23SU" = "15.2.1258.28"
"Mar24SU" = "15.2.1258.32"
"Apr24HU" = "15.2.1258.34"
"Nov24SU" = "15.2.1258.38"
"Nov24SUv2" = "15.2.1258.39"
})
"CU14" = (NewCUAndSUObject "15.2.1544.4" @{
"Mar24SU" = "15.2.1544.9"
"Apr24HU" = "15.2.1544.11"
"Nov24SU" = "15.2.1544.13"
"Nov24SUv2" = "15.2.1544.14"
"Apr25HU" = "15.2.1544.25"
})
"CU15" = (NewCUAndSUObject "15.2.1748.10" @{
"Apr25HU" = "15.2.1748.24"
})
}
}
}

# Must be outside function to use it as a validate script


function GetValidatePossibleParameters {
$exchangeBuildDictionary = GetExchangeBuildDictionary
$suNames = New-Object '[Link][string]'
$cuNames = New-Object '[Link][string]'
$versionNames = New-Object '[Link][string]'

foreach ($exchangeKey in $[Link]) {


[void]$[Link]($exchangeKey)
foreach ($cuKey in $exchangeBuildDictionary[$exchangeKey].Keys) {
[void]$[Link]($cuKey)
if ($null -eq $exchangeBuildDictionary[$exchangeKey][$cuKey].SU)
{ continue }
foreach ($suKey in $exchangeBuildDictionary[$exchangeKey]
[$cuKey].[Link]) {
[void]$[Link]($suKey)
}
}
}
return [PSCustomObject]@{
Version = $versionNames
CU = $cuNames
SU = $suNames
}
}

function ValidateSUParameter {
param($name)

$possibleParameters = GetValidatePossibleParameters
$[Link]($Name)
}

function ValidateCUParameter {
param($Name)

$possibleParameters = GetValidatePossibleParameters
$[Link]($Name)
}

function ValidateVersionParameter {
param($Name)

$possibleParameters = GetValidatePossibleParameters
$[Link]($Name)
}
function Test-ExchangeBuildGreaterOrEqualThanBuild {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[object]$CurrentExchangeBuild,
[Parameter(Mandatory = $true)]
[string]$Version,
[Parameter(Mandatory = $true)]
[string]$CU,
[Parameter(Mandatory = $false)]
[string]$SU
)
begin {
Write-Verbose "Calling: $($[Link])"
$testResult = $false
} process {
if ($[Link] -eq $Version) {
$params = @{
Version = $Version
CU = $CU
}

if (-not([string]::IsNullOrEmpty($SU))) {
$[Link] = $SU
}
$testBuild = Get-ExchangeBuildVersionInformation @params
$testResult = $[Link] -ge
$[Link]
}
} end {
Write-Verbose "Result $testResult"
return $testResult
}
}

function Test-ExchangeBuildLessThanBuild {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[object]$CurrentExchangeBuild,
[Parameter(Mandatory = $true)]
[string]$Version,
[Parameter(Mandatory = $true)]
[string]$CU,
[Parameter(Mandatory = $false)]
[string]$SU
)
begin {
Write-Verbose "Calling: $($[Link])"
$testResult = $false
} process {
if ($[Link] -eq $Version) {
$params = @{
Version = $Version
CU = $CU
}

if (-not([string]::IsNullOrEmpty($SU))) {
$[Link] = $SU
}

$testBuild = Get-ExchangeBuildVersionInformation @params


$testResult = $[Link] -lt
$[Link]
}
} end {
Write-Verbose "Result $testResult"
return $testResult
}
}

function Test-ExchangeBuildEqualBuild {
[CmdletBinding()]
[OutputType([bool])]
param(
[Parameter(Mandatory = $true)]
[object]$CurrentExchangeBuild,
[Parameter(Mandatory = $true)]
[string]$Version,
[Parameter(Mandatory = $true)]
[string]$CU,
[Parameter(Mandatory = $false)]
[string]$SU
)
begin {
Write-Verbose "Calling: $($[Link])"
$testResult = $false
} process {
if ($[Link] -eq $Version) {
$params = @{
Version = $Version
CU = $CU
}

if (-not([string]::IsNullOrEmpty($SU))) {
$[Link] = $SU
}
$testBuild = Get-ExchangeBuildVersionInformation @params
$testResult = $[Link] -eq
$[Link]
}
} end {
Write-Verbose "Result $testResult"
return $testResult
}
}

function Test-ExchangeBuildGreaterOrEqualThanSecurityPatch {
[CmdletBinding()]
[OutputType([bool])]
param(
[object]$CurrentExchangeBuild,
[string]$SUName
)
begin {
Write-Verbose "Calling: $($[Link])"
$testResult = $false
} process {
$allSecurityPatches = Get-ExchangeBuildVersionInformation -FindBySUName
$SUName |
Where-Object { $_.MajorVersion -eq $[Link] }
|
Sort-Object ReleaseDate -Descending

if ($null -eq $allSecurityPatches -or


$[Link] -eq 0) {
Write-Verbose "We didn't find a security path for this version of
Exchange."
Write-Verbose "We assume this means that this version of Exchange $
($[Link]) isn't vulnerable for this SU $SUName"
$testResult = $true
return
}

# The first item in the list should be the latest CU for this security
patch.
# If the current exchange build is greater than the latest CU + security
patch, then we are good.
# Otherwise, we need to look at the CU that we are on to make sure we are
patched.
if ($[Link] -ge
$allSecurityPatches[0].BuildVersion) {
$testResult = $true
return
}
Write-Verbose "Need to look at particular CU match"
$matchCU = $allSecurityPatches | Where-Object { $_.CU -eq
$[Link] }
Write-Verbose "Found match CU $($null -ne $matchCU)"
$testResult = $null -ne $matchCU -and $[Link] -
ge $[Link]
} end {
Write-Verbose "Result $testResult"
return $testResult
}
}

function Get-ExSetupFileVersionInfo {
param(
[Parameter(Mandatory = $true)]
[string]$Server,

[Parameter(Mandatory = $false)]
[ScriptBlock]$CatchActionFunction
)

Write-Verbose "Calling: $($[Link])"


$exSetupDetails = [string]::Empty
function Get-ExSetupDetailsScriptBlock {
try {
$getCommand = Get-Command ExSetup -ErrorAction Stop | ForEach-Object
{ $_.FileVersionInfo }
$getItem = Get-Item -ErrorAction SilentlyContinue
$getCommand[0].FileName
$getCommand | Add-Member -MemberType NoteProperty -Name InstallTime -
Value ($[Link])
$getCommand
} catch {
try {
Write-Verbose "Failed to find ExSetup by environment path
locations. Attempting manual lookup."
$installDirectory = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\
ExchangeServer\v15\Setup -ErrorAction Stop).MsiInstallPath

if ($null -ne $installDirectory) {


$getCommand = Get-Command
([[Link]]::Combine($installDirectory, "bin\[Link]")) -ErrorAction Stop
| ForEach-Object { $_.FileVersionInfo }
$getItem = Get-Item -ErrorAction SilentlyContinue
$getCommand[0].FileName
$getCommand | Add-Member -MemberType NoteProperty -Name
InstallTime -Value ($[Link])
$getCommand
}
} catch {
Write-Verbose "Failed to find ExSetup, need to fallback."
}
}
}

$exSetupDetails = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock


${Function:Get-ExSetupDetailsScriptBlock} -ScriptBlockDescription "Getting ExSetup
remotely" -CatchActionFunction $CatchActionFunction
Write-Verbose "Exiting: $($[Link])"
return $exSetupDetails
}

function Get-ProcessedServerList {
[CmdletBinding()]
param(
[string[]]$ExchangeServerNames,

[string[]]$SkipExchangeServerNames,

[bool]$CheckOnline,

[bool]$DisableGetExchangeServerFullList,

[string]$MinimumSU,

[bool]$DisplayOutdatedServers = $true
)
begin {
Write-Verbose "Calling: $($[Link])"
# The complete list of all the Exchange Servers that we ran Get-
ExchangeServer against.
$getExchangeServer = New-Object [Link][object]
# The list of possible validExchangeServers prior to completing the list.
$possibleValidExchangeServer = New-Object
[Link][object]
# The Get-ExchangeServer object for all the servers that are either in
ExchangeServerNames or not in SkipExchangeServerNames and are within the correct SU
build.
$validExchangeServer = New-Object [Link][object]
# The FQDN of the servers in the validExchangeServer list
$validExchangeServerFqdn = New-Object
[Link][string]
# Servers that are online within the validExchangeServer list.
$onlineExchangeServer = New-Object [Link][object]
# The FQDN of the servers that are in the onlineExchangeServer list
$onlineExchangeServerFqdn = New-Object
[Link][string]
# Servers that are not reachable and therefore classified as offline
$offlineExchangeServer = New-Object [Link][string]
# The FQDN of the servers that are not reachable and therefore classified
as offline
$offlineExchangeServerFqdn = New-Object
[Link][string]
# The list of servers that are outside min required SU
$outdatedBuildExchangeServerFqdn = New-Object
[Link][string]
}
process {
if ($DisableGetExchangeServerFullList) {
# If we don't want to get all the Exchange Servers, then we need to
make sure the list of Servers are Exchange Server
if ($null -eq $ExchangeServerNames -or
$[Link] -eq 0) {
throw "Must provide servers to process when
DisableGetExchangeServerFullList is set."
}

Write-Verbose "Getting the result of the Exchange Servers individually"


foreach ($server in $ExchangeServerNames) {
try {
$result = Get-ExchangeServer $server -ErrorAction Stop
$[Link]($result)
} catch {
Write-Verbose "Failed to run Get-ExchangeServer for server
'$server'. Inner Exception $_"
throw
}
}
} else {
Write-Verbose "Getting all the Exchange Servers in the organization"
$result = @(Get-ExchangeServer)
$[Link]($result)
}

if ($null -ne $ExchangeServerNames -and $[Link] -gt 0) {


$getExchangeServer |
Where-Object { ($_.Name -in $ExchangeServerNames) -or ($_.FQDN -in
$ExchangeServerNames) } |
ForEach-Object {
if ($null -ne $SkipExchangeServerNames -and
$[Link] -gt 0) {
if (($_.Name -notin $SkipExchangeServerNames) -and ($_.FQDN
-notin $SkipExchangeServerNames)) {
Write-Verbose "Adding Server $($_.Name) to the valid
server list"
$[Link]($_)
}
} else {
Write-Verbose "Adding Server $($_.Name) to the valid server
list"
$[Link]($_)
}
}
} else {
if ($null -ne $SkipExchangeServerNames -and
$[Link] -gt 0) {
$getExchangeServer |
Where-Object { ($_.Name -notin $SkipExchangeServerNames) -and
($_.FQDN -notin $SkipExchangeServerNames) } |
ForEach-Object {
Write-Verbose "Adding Server $($_.Name) to the valid server
list"
$[Link]($_)
}
} else {
Write-Verbose "Adding Server $($_.Name) to the valid server list"
$[Link]($getExchangeServer)
}
}

if ($CheckOnline -or (-not ([string]::IsNullOrEmpty($MinimumSU)))) {


Write-Verbose "Will check to see if the servers are online"
$serverCount = 0
$paramWriteProgress = @{
Activity = "Retrieving Exchange Server Build Information"
Status = "Progress:"
PercentComplete = $serverCount
}
Write-Progress @paramWriteProgress

foreach ($server in $possibleValidExchangeServer) {


$serverCount++
$[Link] = "Processing Server: $server"
$[Link] = (($serverCount /
$[Link]) * 100)
Write-Progress @paramWriteProgress

$exSetupDetails = Get-ExSetupFileVersionInfo -Server $[Link]

if ($null -ne $exSetupDetails -and


(-not ([string]::IsNullOrEmpty($exSetupDetails)))) {
# Got some results back, they are online.
$[Link]($server)
$[Link]($[Link])

if (-not ([string]::IsNullOrEmpty($MinimumSU))) {
$params = @{
CurrentExchangeBuild = (Get-
ExchangeBuildVersionInformation -FileVersion $[Link])
SU = $MinimumSU
}
if ((Test-ExchangeBuildGreaterOrEqualThanSecurityPatch
@params)) {
$[Link]($server)
} else {
Write-Verbose "Server $($[Link]) build is older
than our expected min SU build. Build Number: $($[Link])"
$[Link]($[Link])
}
} else {
$[Link]($server)
}
} else {
Write-Verbose "Server $($[Link]) not online"
$[Link]($server)
$[Link]($[Link])
}
}

Write-Progress @paramWriteProgress -Completed


} else {
$[Link]($possibleValidExchangeServer)
}

$validExchangeServer | ForEach-Object
{ $[Link]($_.FQDN) }

# If we have servers in the outdatedBuildExchangeServerFqdn list, the


default response should be to display that we are removing them from the list.
if ($[Link] -gt 0) {
if ($DisplayOutdatedServers) {
Write-Host ""
Write-Host "Excluded the following server(s) because the build is
older than what is required to make a change: $([string]::Join(", ",
$outdatedBuildExchangeServerFqdn))"
Write-Host ""
}
}
}
end {
return [PSCustomObject]@{
ValidExchangeServer = $validExchangeServer
ValidExchangeServerFqdn = $validExchangeServerFqdn
GetExchangeServer = $getExchangeServer
OnlineExchangeServer = $onlineExchangeServer
OnlineExchangeServerFqdn = $onlineExchangeServerFqdn
OfflineExchangeServer = $offlineExchangeServer
OfflineExchangeServerFqdn = $offlineExchangeServerFqdn
OutdatedBuildExchangeServerFqdn = $outdatedBuildExchangeServerFqdn
}
}
}

function Confirm-ProxyServer {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]
$TargetUri
)

Write-Verbose "Calling $($[Link])"


try {
$proxyObject =
([[Link]]::GetSystemWebProxy()).GetProxy($TargetUri)
if ($TargetUri -ne $[Link]) {
Write-Verbose "Proxy server configuration detected"
Write-Verbose $[Link]
return $true
} else {
Write-Verbose "No proxy server configuration detected"
return $false
}
} catch {
Write-Verbose "Unable to check for proxy server configuration"
return $false
}
}

function Invoke-WebRequestWithProxyDetection {
[CmdletBinding(DefaultParameterSetName = "Default")]
param (
[Parameter(Mandatory = $true, ParameterSetName = "Default")]
[string]
$Uri,

[Parameter(Mandatory = $false, ParameterSetName = "Default")]


[switch]
$UseBasicParsing,

[Parameter(Mandatory = $true, ParameterSetName = "ParametersObject")]


[hashtable]
$ParametersObject,

[Parameter(Mandatory = $false, ParameterSetName = "Default")]


[string]
$OutFile
)

Write-Verbose "Calling $($[Link])"


if ([[Link]]::IsNullOrEmpty($Uri)) {
$Uri = $[Link]
}

[[Link]]::SecurityProtocol = [[Link]]::Tls12
if (Confirm-ProxyServer -TargetUri $Uri) {
$webClient = New-Object [Link]
$[Link]("User-Agent", "PowerShell")
$[Link] =
[[Link]]::DefaultNetworkCredentials
}

if ($null -eq $ParametersObject) {


$params = @{
Uri = $Uri
OutFile = $OutFile
}

if ($UseBasicParsing) {
$[Link] = $true
}
} else {
$params = $ParametersObject
}

try {
Invoke-WebRequest @params
} catch {
Write-VerboseErrorInformation
}
}
<#
Determines if the script has an update available.
#>
function Get-ScriptUpdateAvailable {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param (
[Parameter(Mandatory = $false)]
[string]
$VersionsUrl =
"[Link]
[Link]"
)

$BuildVersion = "25.04.17.1814"

$scriptName = $script:[Link]
$scriptPath = [[Link]]::GetDirectoryName($script:[Link])
$scriptFullName = (Join-Path $scriptPath $scriptName)

$result = [PSCustomObject]@{
ScriptName = $scriptName
CurrentVersion = $BuildVersion
LatestVersion = ""
UpdateFound = $false
Error = $null
}

if ((Get-AuthenticodeSignature -FilePath $scriptFullName).Status -eq


"NotSigned") {
Write-Warning "This script appears to be an unsigned test build. Skipping
version check."
} else {
try {
$versionData = [[Link]]::[Link]((Invoke-
WebRequestWithProxyDetection -Uri $VersionsUrl -UseBasicParsing).Content) |
ConvertFrom-Csv
$latestVersion = ($versionData | Where-Object { $_.File -eq $scriptName
}).Version
$[Link] = $latestVersion
if ($null -ne $latestVersion) {
$[Link] = ($latestVersion -ne $BuildVersion)
} else {
Write-Warning ("Unable to check for a script update as no script
with the same name was found." +
"`r`nThis can happen if the script has been renamed. Please
check manually if there is a newer version of the script.")
}

Write-Verbose "Current version: $($[Link]) Latest


version: $($[Link]) Update found: $($[Link])"
} catch {
Write-Verbose "Unable to check for updates: $($_.Exception)"
$[Link] = $_
}
}

return $result
}

function Confirm-Signature {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]
$File
)

$IsValid = $false
$MicrosoftSigningRoot2010 = 'CN=Microsoft Root Certificate Authority 2010,
O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
$MicrosoftSigningRoot2011 = 'CN=Microsoft Root Certificate Authority 2011,
O=Microsoft Corporation, L=Redmond, S=Washington, C=US'

try {
$sig = Get-AuthenticodeSignature -FilePath $File

if ($[Link] -ne 'Valid') {


Write-Warning "Signature is not trusted by machine as Valid, status: $
($[Link])."
throw
}

$chain = New-Object -TypeName


[Link].X509Certificates.X509Chain
$[Link] = "IgnoreNotTimeValid"

if (-not $[Link]($[Link])) {
Write-Warning "Signer certificate doesn't chain correctly."
throw
}

if ($[Link] -le 1) {
Write-Warning "Certificate Chain shorter than expected."
throw
}

$rootCert = $[Link][$[Link] - 1]

if ($[Link] -ne $[Link]) {


Write-Warning "Top-level certificate in chain is not a root
certificate."
throw
}

if ($[Link] -ne $MicrosoftSigningRoot2010 -and


$[Link] -ne $MicrosoftSigningRoot2011) {
Write-Warning "Unexpected root cert. Expected $MicrosoftSigningRoot2010
or $MicrosoftSigningRoot2011, but found $($[Link])."
throw
}

Write-Host "File signed by $($[Link])"

$IsValid = $true
} catch {
$IsValid = $false
}

$IsValid
}

<#
.SYNOPSIS
Overwrites the current running script file with the latest version from the
repository.
.NOTES
This function always overwrites the current file with the latest file, which
might be
the same. Get-ScriptUpdateAvailable should be called first to determine if an
update is
needed.

In many situations, updates are expected to fail, because the server running
the script
does not have internet access. This function writes out failures as warnings,
because we
expect that Get-ScriptUpdateAvailable was already called and it successfully
reached out
to the internet.
#>
function Invoke-ScriptUpdate {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
[OutputType([boolean])]
param ()

$scriptName = $script:[Link]
$scriptPath = [[Link]]::GetDirectoryName($script:[Link])
$scriptFullName = (Join-Path $scriptPath $scriptName)

$oldName = [[Link]]::GetFileNameWithoutExtension($scriptName) + ".old"


$oldFullName = (Join-Path $scriptPath $oldName)
$tempFullName = (Join-Path ((Get-Item $env:TEMP).FullName) $scriptName)

if ($[Link]("$scriptName", "Update script to latest version"))


{
try {
Invoke-WebRequestWithProxyDetection -Uri
"[Link] -
OutFile $tempFullName
} catch {
Write-Warning "AutoUpdate: Failed to download update: $
($_.[Link])"
return $false
}

try {
if (Confirm-Signature -File $tempFullName) {
Write-Host "AutoUpdate: Signature validated."
if (Test-Path $oldFullName) {
Remove-Item $oldFullName -Force -Confirm:$false -ErrorAction
Stop
}
Move-Item $scriptFullName $oldFullName
Move-Item $tempFullName $scriptFullName
Remove-Item $oldFullName -Force -Confirm:$false -ErrorAction Stop
Write-Host "AutoUpdate: Succeeded."
return $true
} else {
Write-Warning "AutoUpdate: Signature could not be verified:
$tempFullName."
Write-Warning "AutoUpdate: Update was not applied."
}
} catch {
Write-Warning "AutoUpdate: Failed to apply update: $
($_.[Link])"
}
}

return $false
}

<#
Determines if the script has an update available. Use the optional
-AutoUpdate switch to make it update itself. Pass -Confirm:$false
to update without prompting the user. Pass -Verbose for additional
diagnostic output.

Returns $true if an update was downloaded, $false otherwise. The


result will always be $false if the -AutoUpdate switch is not used.
#>
function Test-ScriptVersion {
[[Link]('PSShouldProcess', '',
Justification = 'Need to pass through ShouldProcess settings to Invoke-
ScriptUpdate')]
[CmdletBinding(SupportsShouldProcess)]
[OutputType([bool])]
param (
[Parameter(Mandatory = $false)]
[switch]
$AutoUpdate,
[Parameter(Mandatory = $false)]
[string]
$VersionsUrl =
"[Link]
[Link]"
)

$updateInfo = Get-ScriptUpdateAvailable $VersionsUrl


if ($[Link]) {
if ($AutoUpdate) {
return Invoke-ScriptUpdate
} else {
Write-Warning "$($[Link]) $BuildVersion is outdated.
Please download the latest, version $($[Link])."
}
}

return $false
}

function Confirm-Administrator {
$currentPrincipal = New-Object
[Link]( [[Link]]::GetCurre
nt() )

return
$[Link]( [[Link]]::Administrator
)
}

# Confirm that either Remote Shell or EMS is loaded from an Edge Server, Exchange
Server, or a Tools box.
# It does this by also initializing the session and running Get-EventLogLevel.
(Server Management RBAC right)
# All script that require Confirm-ExchangeShell should be at least using Server
Management RBAC right for the user running the script.
function Confirm-ExchangeShell {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[bool]$LoadExchangeShell = $true,

[Parameter(Mandatory = $false)]
[ScriptBlock]$CatchActionFunction
)

begin {
Write-Verbose "Calling: $($[Link])"
Write-Verbose "Passed: LoadExchangeShell: $LoadExchangeShell"
$currentErrors = $[Link]
$edgeTransportKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\
EdgeTransportRole'
$setupKey = 'HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup'
$remoteShell = (-not(Test-Path $setupKey))
$toolsServer = (Test-Path $setupKey) -and
(-not(Test-Path $edgeTransportKey)) -and
($null -eq (Get-ItemProperty -Path $setupKey -Name "Services" -ErrorAction
SilentlyContinue))
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction

function IsExchangeManagementSession {
[OutputType("[Link]")]
param(
[ScriptBlock]$CatchActionFunction
)

$getEventLogLevelCallSuccessful = $false
$isExchangeManagementShell = $false

try {
$currentErrors = $[Link]
$attempts = 0
do {
$eventLogLevel = Get-EventLogLevel -ErrorAction Stop | Select-
Object -First 1
$attempts++
if ($attempts -ge 5) {
throw "Failed to run Get-EventLogLevel too many times."
}
} while ($null -eq $eventLogLevel)
$getEventLogLevelCallSuccessful = $true
foreach ($e in $eventLogLevel) {
Write-Verbose "Type is: $($[Link]().Name) BaseType is: $
($[Link]().BaseType)"
if (($[Link]().Name -eq "EventCategoryObject") -or
(($[Link]().Name -eq "PSObject") -and
($null -ne $[Link]))) {
$isExchangeManagementShell = $true
}
}
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction
} catch {
Write-Verbose "Failed to run Get-EventLogLevel"
Invoke-CatchActionError $CatchActionFunction
}

return [PSCustomObject]@{
CallWasSuccessful = $getEventLogLevelCallSuccessful
IsManagementShell = $isExchangeManagementShell
}
}
}
process {
$isEMS = IsExchangeManagementSession $CatchActionFunction
if ($[Link]) {
Write-Verbose "Exchange PowerShell Module already loaded."
} else {
if (-not ($LoadExchangeShell)) { return }

#Test 32 bit process, as we can't see the registry if that is the case.
if (-not ([[Link]]::Is64BitProcess)) {
Write-Warning "Open a 64 bit PowerShell process to continue"
return
}

if (Test-Path "$setupKey") {
Write-Verbose "We are on Exchange 2013 or newer"

try {
$currentErrors = $[Link]
if (Test-Path $edgeTransportKey) {
Write-Verbose "We are on Exchange Edge Transport Server"
[xml]$PSSnapIns = Get-Content -Path
"$env:ExchangeInstallPath\Bin\exShell.psc1" -ErrorAction Stop

foreach ($PSSnapIn in
$[Link]) {
Write-Verbose ("Trying to add PSSnapIn: {0}" -f
$[Link])
Add-PSSnapin -Name $[Link] -ErrorAction Stop
}

Import-Module $env:ExchangeInstallPath\bin\Exchange.ps1 -
ErrorAction Stop
} else {
Import-Module $env:ExchangeInstallPath\bin\
RemoteExchange.ps1 -ErrorAction Stop
Connect-ExchangeServer -Auto -
ClientApplication:ManagementShell
}
Invoke-CatchActionErrorLoop $currentErrors $CatchActionFunction

Write-Verbose "Imported Module. Trying Get-EventLogLevel Again"


$isEMS = IsExchangeManagementSession $CatchActionFunction
if (($[Link]) -and
($[Link])) {
Write-Verbose "Successfully loaded Exchange Management
Shell"
} else {
Write-Warning "Something went wrong while loading the
Exchange Management Shell"
}
} catch {
Write-Warning "Failed to Load Exchange PowerShell Module..."
Invoke-CatchActionError $CatchActionFunction
}
} else {
Write-Verbose "Not on an Exchange or Tools server"
}
}
}
end {

$returnObject = [PSCustomObject]@{
ShellLoaded = $[Link]
Major = ((Get-ItemProperty -Path $setupKey -Name
"MsiProductMajor" -ErrorAction SilentlyContinue).MsiProductMajor)
Minor = ((Get-ItemProperty -Path $setupKey -Name
"MsiProductMinor" -ErrorAction SilentlyContinue).MsiProductMinor)
Build = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMajor"
-ErrorAction SilentlyContinue).MsiBuildMajor)
Revision = ((Get-ItemProperty -Path $setupKey -Name "MsiBuildMinor"
-ErrorAction SilentlyContinue).MsiBuildMinor)
EdgeServer = $[Link] -and (Test-Path $setupKey) -and
(Test-Path $edgeTransportKey)
ToolsOnly = $[Link] -and $toolsServer
RemoteShell = $[Link] -and $remoteShell
EMS = $[Link]
}

return $returnObject
}
}

function Get-NewLoggerInstance {
[CmdletBinding()]
param(
[string]$LogDirectory = (Get-Location).Path,

[ValidateNotNullOrEmpty()]
[string]$LogName = "Script_Logging",

[bool]$AppendDateTime = $true,

[bool]$AppendDateTimeToFileName = $true,

[int]$MaxFileSizeMB = 10,
[int]$CheckSizeIntervalMinutes = 10,

[int]$NumberOfLogsToKeep = 10
)

$fileName = if ($AppendDateTimeToFileName) { "{0}_{1}.txt" -f $LogName, ((Get-


Date).ToString('yyyyMMddHHmmss')) } else { "$[Link]" }
$fullFilePath = [[Link]]::Combine($LogDirectory, $fileName)

if (-not (Test-Path $LogDirectory)) {


try {
New-Item -ItemType Directory -Path $LogDirectory -ErrorAction Stop |
Out-Null
} catch {
throw "Failed to create Log Directory: $LogDirectory. Inner Exception:
$_"
}
}

return [PSCustomObject]@{
FullPath = $fullFilePath
AppendDateTime = $AppendDateTime
MaxFileSizeMB = $MaxFileSizeMB
CheckSizeIntervalMinutes = $CheckSizeIntervalMinutes
NumberOfLogsToKeep = $NumberOfLogsToKeep
BaseInstanceFileName = $[Link](".txt", "")
Instance = 1
NextFileCheckTime = ((Get-
Date).AddMinutes($CheckSizeIntervalMinutes))
PreventLogCleanup = $false
LoggerDisabled = $false
} | Write-LoggerInstance -Object "Starting Logger Instance $(Get-Date)"
}

function Write-LoggerInstance {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]$LoggerInstance,

[Parameter(Mandatory = $true, Position = 1)]


[object]$Object
)
process {
if ($[Link]) { return }

if ($[Link] -and
$[Link]().Name -eq "string") {
$Object = "[$([[Link]]::Now)] : $Object"
}

# Doing WhatIf:$false to support -WhatIf in main scripts but still log the
information
$Object | Out-File $[Link] -Append -WhatIf:$false

#Upkeep of the logger information


if ($[Link] -gt [[Link]]::Now) {
return
}
#Set next update time to avoid issues so we can log things
$[Link] =
([[Link]]::Now).AddMinutes($[Link])
$item = Get-ChildItem $[Link]

if (($[Link] / 1MB) -gt $[Link]) {


$LoggerInstance | Write-LoggerInstance -Object "Max file size reached
rolling over" | Out-Null
$directory =
[[Link]]::GetDirectoryName($[Link])
$fileName = "$($[Link])-$
($[Link]).txt"
$[Link]++
$[Link] = [[Link]]::Combine($directory,
$fileName)

$items = Get-ChildItem -Path


([[Link]]::GetDirectoryName($[Link])) -Filter "*$
($[Link])*"

if ($[Link] -gt $[Link]) {


$item = $items | Sort-Object LastWriteTime | Select-Object -First 1
$LoggerInstance | Write-LoggerInstance "Removing Log File $
($[Link])" | Out-Null
$item | Remove-Item -Force
}
}
}
end {
return $LoggerInstance
}
}

function Invoke-LoggerInstanceCleanup {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[object]$LoggerInstance
)
process {
if ($[Link] -or
$[Link]) {
return
}

Get-ChildItem -Path
([[Link]]::GetDirectoryName($[Link])) -Filter "*$
($[Link])*" |
Remove-Item -Force
}
}

<#
.SYNOPSIS
Outputs a table of objects with certain values colorized.
.EXAMPLE
PS C:\> <example usage>
Explanation of what the example does
.INPUTS
Inputs (if any)
.OUTPUTS
Output (if any)
.NOTES
General notes
#>
function Out-Columns {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[object[]]
$InputObject,

[Parameter(Mandatory = $false, Position = 0)]


[string[]]
$Properties,

[Parameter(Mandatory = $false, Position = 1)]


[ScriptBlock[]]
$ColorizerFunctions = @(),

[Parameter(Mandatory = $false)]
[int]
$IndentSpaces = 0,

[Parameter(Mandatory = $false)]
[int]
$LinesBetweenObjects = 0,

[Parameter(Mandatory = $false)]
[ref]
$StringOutput
)

begin {
function WrapLine {
param([string]$line, [int]$width)
if ($[Link] -le $width -and $[Link]("`n") -lt 0) {
return $line
}

$lines = New-Object [Link]

$noLF = $[Link]("`r", "")


$lineSplit = $[Link]("`n")
foreach ($l in $lineSplit) {
if ($[Link] -le $width) {
[void]$[Link]($l)
} else {
$split = $[Link](" ")
$sb = New-Object [Link]
for ($i = 0; $i -lt $[Link]; $i++) {
if ($[Link] -eq 0 -and $[Link] + $split[$i].Length -
lt $width) {
[void]$[Link]($split[$i])
} elseif ($[Link] -gt 0 -and $[Link] +
$split[$i].Length + 1 -lt $width) {
[void]$[Link](" " + $split[$i])
} elseif ($[Link] -gt 0) {
[void]$[Link]($[Link]())
[void]$[Link]()
$i--
} else {
if ($split[$i].Length -le $width) {
[void]$[Link]($split[$i])
} else {
[void]$[Link]($split[$i].Substring(0, $width))
$split[$i] = $split[$i].Substring($width)
$i--
}
}
}

if ($[Link] -gt 0) {
[void]$[Link]($[Link]())
}
}
}

return $lines
}

function GetLineObjects {
param($obj, $props, $colWidths)
$linesNeededForThisObject = 1
$multiLineProps = @{}
for ($i = 0; $i -lt $[Link]; $i++) {
$p = $props[$i]
$val = $obj."$p"

if ($val -isnot [array] -and $val -isnot


[[Link]]) {
$val = WrapLine -line $val -width $colWidths[$i]
} elseif ($val -is [array] -or $val -is
[[Link]]) {
$val = $val | Where-Object { $null -ne $_ }
$val = $val | ForEach-Object { WrapLine -line $_ -width
$colWidths[$i] }
}

if ($val -is [array] -or $val -is [[Link]]) {


$multiLineProps[$p] = $val
if ($[Link] -gt $linesNeededForThisObject) {
$linesNeededForThisObject = $[Link]
}
}
}

if ($linesNeededForThisObject -eq 1) {
$obj
} else {
for ($i = 0; $i -lt $linesNeededForThisObject; $i++) {
$lineProps = @{}
foreach ($p in $props) {
if ($null -ne $multiLineProps[$p] -and
$multiLineProps[$p].Length -gt $i) {
$lineProps[$p] = $multiLineProps[$p][$i]
} elseif ($i -eq 0) {
$lineProps[$p] = $obj."$p"
} else {
$lineProps[$p] = $null
}
}

[PSCustomObject]$lineProps
}
}
}

function GetColumnColors {
param($obj, $props, $functions)

$consoleHost = (Get-Host).Name -eq "ConsoleHost"


$colColors = New-Object string[] $[Link]
for ($i = 0; $i -lt $[Link]; $i++) {
if ($consoleHost) {
$fgColor = (Get-Host).[Link]
} else {
$fgColor = "White"
}
foreach ($func in $functions) {
$result = $[Link]($obj, $props[$i])
if (-not [string]::IsNullOrEmpty($result)) {
$fgColor = $result
break # The first colorizer that takes action wins
}
}

$colColors[$i] = $fgColor
}

$colColors
}

function GetColumnWidths {
param($objects, $props)

$colWidths = New-Object int[] $[Link]

# Start with the widths of the property names


for ($i = 0; $i -lt $[Link]; $i++) {
$colWidths[$i] = $props[$i].Length
}

# Now check the widths of the widest values


foreach ($thing in $objects) {
for ($i = 0; $i -lt $[Link]; $i++) {
$val = $thing."$($props[$i])"
if ($null -ne $val) {
$width = 0
if ($val -isnot [array] -and $val -isnot
[[Link]]) {
$val = $[Link]().Split("`n")
}

$width = ($val | ForEach-Object {


if ($null -ne $_) { $_.ToString() } else { "" }
} | Sort-Object Length -Descending | Select-Object -
First 1).Length

if ($width -gt $colWidths[$i]) {


$colWidths[$i] = $width
}
}
}
}

# If we're within the window width, we're done


$totalColumnWidth = $[Link] * $padding + ($colWidths |
Measure-Object -Sum).Sum + $IndentSpaces
$windowWidth = (Get-Host).[Link]
if ($windowWidth -lt 1 -or $totalColumnWidth -lt $windowWidth) {
return $colWidths
}

# Take size away from one or more columns to make them fit
while ($totalColumnWidth -ge $windowWidth) {
$startingTotalWidth = $totalColumnWidth
$widest = $colWidths | Sort-Object -Descending | Select-Object -
First 1
$newWidest = [Math]::Floor($widest * 0.95)
for ($i = 0; $i -lt $[Link]; $i++) {
if ($colWidths[$i] -eq $widest) {
$colWidths[$i] = $newWidest
break
}
}

$totalColumnWidth = $[Link] * $padding + ($colWidths |


Measure-Object -Sum).Sum + $IndentSpaces
if ($totalColumnWidth -ge $startingTotalWidth) {
# Somehow we didn't reduce the size at all, so give up
break
}
}

return $colWidths
}

$objects = New-Object [Link]


$padding = 2
$stb = New-Object [Link]
}

process {
foreach ($thing in $InputObject) {
[void]$[Link]($thing)
}
}

end {
if ($[Link] -gt 0) {
$props = $null

if ($null -ne $Properties) {


$props = $Properties
} else {
$props = $objects[0].[Link]
}

$colWidths = GetColumnWidths $objects $props

Write-Host
[void]$[Link]([[Link]]::NewLine)

Write-Host (" " * $IndentSpaces) -NoNewline


[void]$[Link](" " * $IndentSpaces)

for ($i = 0; $i -lt $[Link]; $i++) {


Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f
$props[$i]) -NoNewline
[void]$[Link]("{0,$(-1 * ($colWidths[$i] + $padding))}" -f
$props[$i])
}

Write-Host
[void]$[Link]([[Link]]::NewLine)

Write-Host (" " * $IndentSpaces) -NoNewline


[void]$[Link](" " * $IndentSpaces)

for ($i = 0; $i -lt $[Link]; $i++) {


Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f ("-" *
$props[$i].Length)) -NoNewline
[void]$[Link]("{0,$(-1 * ($colWidths[$i] + $padding))}" -f ("-"
* $props[$i].Length))
}

Write-Host
[void]$[Link]([[Link]]::NewLine)

foreach ($o in $objects) {


$colColors = GetColumnColors -obj $o -props $props -functions
$ColorizerFunctions
$lineObjects = @(GetLineObjects -obj $o -props $props -colWidths
$colWidths)
foreach ($lineObj in $lineObjects) {
Write-Host (" " * $IndentSpaces) -NoNewline
[void]$[Link](" " * $IndentSpaces)
for ($i = 0; $i -lt $[Link]; $i++) {
$val = $lineObj."$($props[$i])"
if ($[Link] -eq 0) { $val = "" }
Write-Host ("{0,$(-1 * ($colWidths[$i] + $padding))}" -f
$val) -NoNewline -ForegroundColor $colColors[$i]
[void]$[Link]("{0,$(-1 * ($colWidths[$i] + $padding))}"
-f $val)
}

Write-Host
[void]$[Link]([[Link]]::NewLine)
}

for ($i = 0; $i -lt $LinesBetweenObjects; $i++) {


Write-Host
[void]$[Link]([[Link]]::NewLine)
}
}

Write-Host
[void]$[Link]([[Link]]::NewLine)

if ($null -ne $StringOutput) {


$[Link] = $[Link]()
}
}
}
}

function Show-Disclaimer {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[ValidateNotNullOrEmpty()]
[string]$Message,
[ValidateNotNullOrEmpty()]
[string]$Target,
[ValidateNotNullOrEmpty()]
[string]$Operation
)

if ($[Link]($Message, $Target, $Operation) -or


$WhatIfPreference) {
return
} else {
exit
}
}

# TODO: Move this so it isn't duplicated


# matching restrictions
$restrictionToSite = @{
"APIFrontend" = "Default Web Site/API"
"AutodiscoverFrontend" = "Default Web Site/Autodiscover"
"ECPFrontend" = "Default Web Site/ECP"
"EWSFrontend" = "Default Web Site/EWS"
"Microsoft-Server-ActiveSyncFrontend" = "Default Web Site/Microsoft-Server-
ActiveSync"
"OABFrontend" = "Default Web Site/OAB"
"PowershellFrontend" = "Default Web Site/Powershell"
"OWAFrontend" = "Default Web Site/OWA"
"RPCFrontend" = "Default Web Site/RPC"
"MAPIFrontend" = "Default Web Site/MAPI"
"APIBackend" = "Exchange Back End/API"
"AutodiscoverBackend" = "Exchange Back End/Autodiscover"
"ECPBackend" = "Exchange Back End/ECP"
"EWSBackend" = "Exchange Back End/EWS"
"Microsoft-Server-ActiveSyncBackend" = "Exchange Back End/Microsoft-
Server-ActiveSync"
"OABBackend" = "Exchange Back End/OAB"
"PowershellBackend" = "Exchange Back End/Powershell"
"OWABackend" = "Exchange Back End/OWA"
"RPCBackend" = "Exchange Back End/RPC"
"PushNotificationsBackend" = "Exchange Back
End/PushNotifications"
"RPCWithCertBackend" = "Exchange Back End/RPCWithCert"
"MAPI-emsmdbBackend" = "Exchange Back End/MAPI/emsmdb"
"MAPI-nspiBackend" = "Exchange Back End/MAPI/nspi"
}

$Script:Logger = Get-NewLoggerInstance -LogName


"ExchangeExtendedProtectionManagement-$((Get-Date).ToString("yyyyMMddhhmmss"))-
Debug" `
-AppendDateTimeToFileName $false `
-ErrorAction SilentlyContinue

SetWriteHostAction ${Function:Write-HostLog}
SetWriteVerboseAction ${Function:Write-VerboseLog}
SetWriteWarningAction ${Function:Write-HostLog}
SetWriteProgressAction ${Function:Write-HostLog}

# The ParameterSetName options


$RollbackSelected = $[Link] -eq "Rollback"
$RollbackRestoreIISAppConfig = $RollbackSelected -and $RollbackType -contains
"RestoreIISAppConfig"
$RollbackRestoreConfiguration = $RollbackSelected -and $RollbackType -contains
"RestoreConfiguration"
$RollbackRestrictType = $RollbackSelected -and (-not
$RollbackRestoreIISAppConfig) -and (-not $RollbackRestoreConfiguration)
$ConfigureMitigationSelected = $[Link] -eq
"ConfigureMitigation"
$ConfigureEPSelected = $ConfigureMitigationSelected -or
($[Link] -eq "ConfigureEP" -and -not
$ShowExtendedProtection)
$ValidateTypeSelected = $[Link] -eq "ValidateMitigation"

$includeExchangeServerNames = New-Object
'[Link][string]'

if ($[Link] -gt 1) {
if ($RollbackRestoreIISAppConfig) {
Write-Host "RestoreIISAppConfig Rollback type can only be used
individually"
}
if ($RollbackRestoreConfiguration) {
Write-Host "RestoreConfiguration Rollback type can only be used
individually"
}
exit
}

$ExcludeEWSFe = $false
if ($[Link] -gt 0) {
$ExcludeEWSFe = $null -ne ($ExcludeVirtualDirectories | Where-Object { $_ -
eq "EWSFrontEnd" })
}

if ($RollbackRestrictType) {
$RestrictType = $[Link]("RestrictType", "")
}

if ($ConfigureMitigationSelected) {
$RestrictType = $RestrictType | Get-Unique
}
if ($ValidateTypeSelected) {
$RestrictType = New-Object '[Link][string]'
$ValidateType | Get-Unique | ForEach-Object { $RestrictType +=
$_.Replace("RestrictType", "") }
}

if (($ConfigureMitigationSelected -or $ValidateTypeSelected)) {


# Get list of IPs in object form from the file specified
$ipResults = Get-IPRangeAllowListFromFile -FilePath $IPRangeFilePath
if ($[Link]) {
exit
}

$ipRangeAllowListRules = $[Link]
}

if ($InternalOption -eq "SkipEWS") {


Write-Verbose "SkipEWS option enabled."
$Script:SkipEWS = $true
} else {
$Script:SkipEWS = $false
}

if ($null -ne $RestrictType -and $[Link] -gt 0) {


$SiteVDirLocations = New-Object '[Link][string]'
foreach ($key in $RestrictType) {
$SiteVDirLocations += $restrictionToSite[$key]
}
}
} process {
foreach ($server in $ExchangeServerNames) {
$[Link]($server)
}
} end {
if (-not (Confirm-Administrator)) {
Write-Warning "The script needs to be executed in elevated mode. Start the
Exchange Management Shell as an Administrator."
exit
}

try {
$BuildVersion = "25.04.17.1814"
Write-Host "Version $BuildVersion"

$exchangeShell = Confirm-ExchangeShell
if (-not($[Link])) {
Write-Warning "Failed to load the Exchange Management Shell. Start the
script using the Exchange Management Shell."
exit
} elseif (-not($[Link])) {
Write-Warning "This script requires to be run inside of Exchange
Management Shell. Please run on an Exchange Management Server or an Exchange Server
with Exchange Management Shell."
Write-Warning "If the script was already executed via Exchange
Management Shell, check your Auth Certificate by using the following script:
[Link]
exit
}
if ($SkipAutoUpdate) {
Write-Verbose "Skipping AutoUpdate"
} elseif ((Test-ScriptVersion -AutoUpdate -VersionsUrl "[Link]
VersionsUrl")) {
Write-Warning "Script was updated. Please rerun the command."
exit
} else {
Write-Verbose "Script is up to date."
}

if ($ConfigureEPSelected) {
$params = @{
Message = "Display Warning about Extended Protection"
Target = "Extended Protection is recommended to be enabled for
security reasons. " +
"Known Issues: Following scenarios will not work when Extended
Protection is enabled." +
"`r`n - SSL offloading or SSL termination via Layer 7 load
balancing." +
"`r`n - Exchange Hybrid Features if using Modern Hybrid." +
"`r`n - Access to Public folders on Exchange 2013 Servers." +
"`r`nYou can find more information on:
[Link] Do you want to proceed?"
Operation = "Enabling Extended Protection"
}

Show-Disclaimer @params
}

$processParams = @{
ExchangeServerNames = $includeExchangeServerNames
SkipExchangeServerNames = $SkipExchangeServerNames
CheckOnline = $true
DisableGetExchangeServerFullList = $false # We want a list of all
Exchange Servers as we need to run the prerequisites check against them
}

$processedExchangeServers = Get-ProcessedServerList @processParams

Write-Verbose "Get a list of all Exchange servers to perform prerequisites


check against"
$ExchangeServersPrerequisitesCheckSettingsCheck =
$[Link] | Where-Object { $_.AdminDisplayVersion
-like "Version 15*" -and $_.ServerRole -ne "Edge" }

Write-Verbose "Get a list of all Exchange servers which are online and not
skipped"
$ExchangeServers = $[Link] | Where-
Object { $_.AdminDisplayVersion -like "Version 15*" -and $_.ServerRole -ne "Edge" }

if ($FindExchangeServerIPAddresses) {
Get-ExchangeServerIPs -OutputFilePath $OutputFilePath -ExchangeServers
$ExchangeServers
Write-Warning ("The file generated contains all the IPv4 and IPv6
addresses of all Exchange Servers in the organization." +
" This file should be used as a reference. Please change the file
to include/remove IP addresses for the IP filtering allow list." +
" If the number of Exchange Servers in your organization is high
(>100), consider using a IPRange file with IP Range Subnets [x.x.x.x/n] instead of
IP addresses which is more efficient." +
"`r`nYou can find more information on:
[Link]
return
}

if ($null -ne $includeExchangeServerNames -and


$[Link] -gt 0) {
Write-Verbose "Running only on servers: $([string]::Join(", " ,
$[Link]))"
}

if ($null -ne $SkipExchangeServerNames -and $[Link]


-gt 0) {
Write-Verbose "Skipping servers: $([string]::Join(", ",
$SkipExchangeServerNames))"
}

if ($null -eq $ExchangeServers) {


Write-Host "No exchange servers to process. Please specify server
filters correctly"
exit
}

if ($ValidateTypeSelected) {
# Validate mitigation
$ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 15 -and (Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 0 -and $_.IsClientAccessServer) }
Invoke-ValidateMitigation -ExchangeServers $[Link] -
ipRangeAllowListRules $ipRangeAllowListRules -SiteVDirLocations $SiteVDirLocations
}

if ($ShowExtendedProtection) {
Write-Verbose "Showing Extended Protection Information Only"
$extendedProtectionConfigurations = New-Object
'[Link][object]'
foreach ($server in $ExchangeServers) {
$params = @{
ComputerName = $[Link]
IsClientAccessServer = $[Link]
IsMailboxServer = $[Link]
ExcludeEWS = $SkipEWS
}
$[Link]((Get-
ExtendedProtectionConfiguration @params))
}

foreach ($configuration in $extendedProtectionConfigurations) {


Write-Verbose "Working on server $($[Link])"
$epFrontEndList = New-Object
'[Link][object]'
$epBackEndList = New-Object
'[Link][object]'
foreach ($entry in $[Link])
{
$vDirArray = $[Link]("/", 2)
$ssl = $[Link]

$listToAdd = $epFrontEndList
if ($vDirArray[0] -eq "Exchange Back End") {
$listToAdd = $epBackEndList
}

$[Link](([PSCustomObject]@{
$vDirArray[0] = $vDirArray[1]
Value = $[Link]
SupportedValue = if ($[Link]
-and $[Link]) { "None" } else
{ $[Link] }
ConfigSupported =
$[Link]
ConfigSecure =
$[Link]
RequireSSL = "$($[Link]) $
(if($ssl.Ssl128Bit) { "(128-bit)" })".Trim()
ClientCertificate = $[Link]
IPFilterEnabled = $[Link]
}))
}

Write-Host "Results for Server: $($[Link])"


$epFrontEndList | Format-Table | Out-String | Write-Host
$epBackEndList | Format-Table | Out-String | Write-Host
Write-Host ""
Write-Host ""
}

return
}

if ($ConfigureEPSelected -or $PrerequisitesCheckOnly) {


$prerequisitesCheckFailed = $false
$params = @{
ExchangeServers = $ExchangeServersPrerequisitesCheckSettingsCheck
SkipEWS = $SkipEWS
SkipEWSFe = $ExcludeEWSFe
SiteVDirLocations = $SiteVDirLocations
}
$prerequisitesCheck = Get-ExtendedProtectionPrerequisitesCheck @params

if ($null -ne $prerequisitesCheck) {


Write-Host ""
$onlineSupportedServers = New-Object
'[Link][object]'
$unsupportedServers = New-Object
'[Link][string]'
$unsupportedAndConfiguredServers = New-Object
'[Link][object]'
$prerequisitesCheck | ForEach-Object {
if
($_.[Link] -eq $true -and

$_.[Link] -eq
$false) {
$[Link]($_)
} elseif
($_.[Link] -eq
$false) {
$[Link]($_.FQDN)
} elseif ($_.ServerOnline) {
# For now, keep this as a failsafe
$[Link]($_)
}
}

# We don't care about the TLS version on servers that aren't yet
upgraded on
# Therefore, we can skip over them for this check.
# However, if there is an unsupported version of Exchange that does
have EP enabled,
# We need to prompt to the admin stating that we are going to
revert the change to get back to a supported state.
Write-Verbose ("Found the following servers configured for EP and
Unsupported: " +
"$(if ($[Link] -eq 0) { 'None' }
else {[string]::Join(", " ,$[Link])})")

Write-Verbose ("Found the following servers that not supported to


configure EP and not enabled: " +
"$(if ($[Link] -eq 0) { 'None' } else
{[string]::Join(", " ,$unsupportedServers)})")

if ($[Link] -gt 0) {
$params = @{
Message = "Display Warning about switching Extended
Protection Back to None for Unsupported Build of Exchange"
Target = "Found Servers that have Extended Protection
Enabled, but are on an unsupported build of Exchange." +
"`r`nBecause of this, we will be setting them back to None
for Extended Protection with the execution of this script to be in a supported
state." +
"`r`nYou can find more information on:
[Link] Do you want to proceed?"
Operation = "Set Unsupported Version of Exchange Back to
None for Extended Protection"
}

Show-Disclaimer @params
Write-Host ""
}

if ($[Link] -gt 0) {

$serversInList = @($ExchangeServers | Where-Object { $($_.FQDN


-in $unsupportedServers) })

if ($[Link] -gt 0) {
$line = "The following servers are not the minimum required
version to support Extended Protection. Please update them, or re-run the script
without including them in the list: $($serversInList -Join " ")"
Write-Verbose $line
Write-Warning $line
exit
}

Write-Verbose "The following servers are unsupported but not


included in the list to configure: $([string]::Join(", " ,$unsupportedServers))"
}

if (($[Link]).Count -gt 0)
{
$line = "Removing the following servers from the list to
configure because we weren't able to reach them: $([string]::Join(", " ,
$[Link]))"
Write-Verbose $line
Write-Warning $line
Write-Host ""
}

# Only need to set the server names for the ones we are trying to
configure and the ones that are up.
# Also need to add Unsupported Configured EP servers to the list.
$serverNames = New-Object '[Link][string]'
$ExchangeServers | ForEach-Object { $[Link]($_.FQDN) }

if ($[Link] -gt 0) {
$unsupportedAndConfiguredServers |
Where-Object { $_.FQDN -notin $serverNames } |
ForEach-Object { $[Link]($_.FQDN) }
}

# If there aren't any servers to check against for TLS settings,


bypass this check.
if ($null -ne $[Link]) {
$tlsPrerequisites = Invoke-
ExtendedProtectionTlsPrerequisitesCheck -TlsConfiguration
$[Link]

function NewDisplayObject {
param(
[string]$RegistryName,
[string]$Location,
[object]$Value
)
return [PSCustomObject]@{
RegistryName = $RegistryName
Location = $Location
Value = $Value
}
}

foreach ($tlsSettings in $[Link]) {


Write-Host "The following servers have the TLS
Configuration below"
Write-Host "$([string]::Join(", " ,
$[Link]))"
$displayObject = @()
$[Link] |
ForEach-Object {
$displayObject += NewDisplayObject "Enabled" -
Location $_.ServerRegistryPath -Value $_.ServerEnabledValue
$displayObject += NewDisplayObject
"DisabledByDefault" -Location $_.ServerRegistryPath -Value
$_.ServerDisabledByDefaultValue
$displayObject += NewDisplayObject "Enabled" -
Location $_.ClientRegistryPath -Value $_.ClientEnabledValue
$displayObject += NewDisplayObject
"DisabledByDefault" -Location $_.ClientRegistryPath -Value
$_.ClientDisabledByDefaultValue
}

$[Link] |
ForEach-Object {
$displayObject += NewDisplayObject
"SystemDefaultTlsVersions" -Location $_.MicrosoftRegistryLocation -Value
$_.SystemDefaultTlsVersionsValue
$displayObject += NewDisplayObject
"SchUseStrongCrypto" -Location $_.MicrosoftRegistryLocation -Value
$_.SchUseStrongCryptoValue
$displayObject += NewDisplayObject
"SystemDefaultTlsVersions" -Location $_.WowRegistryLocation -Value
$_.WowSystemDefaultTlsVersionsValue
$displayObject += NewDisplayObject
"SchUseStrongCrypto" -Location $_.WowRegistryLocation -Value
$_.WowSchUseStrongCryptoValue
}
$stringOutput = [string]::Empty
SetWriteHostAction $null
$displayObject | Sort-Object Location, RegistryName |
Out-Columns -StringOutput ([ref]$stringOutput)
Write-HostLog $stringOutput
SetWriteHostAction ${Function:Write-HostLog}
}

# If TLS Prerequisites Check passed, then we are good to go.


# If it doesn't, now we need to verify the servers we are
trying to enable EP on
# will pass the TLS Prerequisites and all other servers that
have EP enabled on.
if ($[Link]) {
Write-Host "TLS prerequisites check successfully passed!" -
ForegroundColor Green
Write-Host ""
} else {
# before displaying an issue, make sure that the online
supported servers & EP enabled server have the correct settings.
$epEnabledServerList = New-Object
'[Link][string]'
$epEnabledServers = $onlineSupportedServers | Where-Object
{ $_.[Link] -eq $true }
$wantedCheckAgainst = $onlineSupportedServers | Where-
Object { $_.FQDN -in $serverNames }
$checkAgainst = $onlineSupportedServers |
Where-Object {

$_.[Link] -eq $true -or


$_.FQDN -in $serverNames
}

$wantedResults = Invoke-
ExtendedProtectionTlsPrerequisitesCheck -TlsConfiguration
$[Link]
$checkResults = Invoke-
ExtendedProtectionTlsPrerequisitesCheck -TlsConfiguration $[Link]

if ($[Link] -eq $false -or


$[Link] -eq $false) {

foreach ($entry in $[Link]) {


Write-Host "Test Failed: $($[Link])" -
ForegroundColor Red
if ($null -ne $[Link]) {
foreach ($list in $[Link]) {
Write-Host "System affected: $list" -
ForegroundColor Red

if ($list -in $[Link]) {


$[Link]($list)
}
}
}
Write-Host "Action required: $($[Link])" -
ForegroundColor Red
Write-Host ""
}
if ($[Link] -eq $false) {
Write-Warning "Failed to pass the TLS prerequisites
for the servers you are trying to enable Extended Protection. Unable to continue."
Write-Host ""
Write-Host "Servers trying to enable: $
([string]::Join(", ", $serverNames))"
} else {
Write-Warning "Failed to pass the TLS prerequisites
due to the TLS settings on servers that already have Extended Protection enabled.
Unable to continue."
Write-Host ""
Write-Host "Extended Protection Enabled Servers: $
([string]::Join(", ", $epEnabledServerList))"
Write-Host ""
}

$prerequisitesCheckFailed = $true
} else {
Write-Host "All servers attempting to enable Extended
Protection or already enabled passed the TLS prerequisites."
Write-Host ""
}
}

# SuppressExtendedProtection Check
$suppressExtendedProtectionSet = $onlineSupportedServers |
Where-Object { $_.[Link]
-ne 0 }

if ($null -ne $suppressExtendedProtectionSet) {


Write-Verbose "Some Online Server have the Suppress
Extended Protection Set"
$requiredServers = $suppressExtendedProtectionSet | Where-
Object { $_.FQDN -in $serverNames -or
$_.[Link] -eq $true }
Write-Host "SYSTEM\CurrentControlSet\Control\Lsa\
SuppressExtendedProtection is set on the following servers: $([string]::Join(", ",
$[Link]))" -ForegroundColor Red

if ($null -ne $requiredServers) {


Write-Host "At least one server is trying to enable
Extended Protection or already has Extended Protection Enabled." -ForegroundColor
Red
Write-Host "Having this key set to anything other than
0 will break Extended Protection functionality" -ForegroundColor Red
$prerequisitesCheckFailed = $true
} else {
Write-Host "None of the servers that have this key set
has Extended Protection enabled or trying to configure it." -ForegroundColor Yellow
Write-Host "This may cause issues with Extended
Protection server to server communication, therefore it is recommended to address
as soon as possible." -ForegroundColor Yellow
# Don't believe this should be a scenario where we need
to block configuration from occurring
}
}

# now that we passed the TLS PrerequisitesCheck, now we need to


do the RPC VDir check for SSLOffloading.
$rpcFailedServers = New-Object
'[Link][string]'
$rpcNullServers = New-Object
'[Link][string]'
$canNotConfigure = "Therefore, we can not configure Extended
Protection."
$counter = 0
$totalCount =
@($ExchangeServersPrerequisitesCheckSettingsCheck).Count
$outlookAnywhereCount = 0
$outlookAnywhereServers =
@($ExchangeServersPrerequisitesCheckSettingsCheck | Where-Object
{ $_.IsClientAccessServer -eq $true })
$outlookAnywhereTotalCount = $[Link]

$progressParams = @{
Id = 1
Activity = "Prerequisites Check"
Status = "Running Get-OutlookAnywhere"
PercentComplete = 0
}

$outlookAnywhereProgressParams = @{
ParentId = 1
Activity = "Collecting Get-OutlookAnywhere Results"
PercentComplete = 0
}

Write-Progress @progressParams
Write-Progress @outlookAnywhereProgressParams
# Needs to be SilentlyContinue to handle down servers, we must
also exclude pre Exchange 2013 servers
$outlookAnywhere = $outlookAnywhereServers | Get-
OutlookAnywhere -ADPropertiesOnly -ErrorAction SilentlyContinue |
ForEach-Object {
$outlookAnywhereCount++
$[Link] =
($outlookAnywhereCount / $outlookAnywhereTotalCount * 100)
Write-Progress @outlookAnywhereProgressParams
$_
}

if ($null -eq $outlookAnywhere) {


Write-Warning "Failed to run Get-OutlookAnywhere. Failing
out the script."
exit
}

foreach ($server in
$ExchangeServersPrerequisitesCheckSettingsCheck) {
$counter++
$[Link] = "Checking RPC FE SSLOffloading - $
($[Link])"
$[Link] = ($counter / $totalCount *
100)
Write-Progress @progressParams
if (-not ($[Link])) {
Write-Verbose "Server $($[Link]) is not a CAS.
Skipping over the RPC FE Check."
continue
}
$skipServer = $null -eq ($onlineSupportedServers |
Where-Object {
$_.FQDN -eq $[Link] -and

($_.[Link] -eq $true -or


$[Link] -in $[Link] )
})
if ($skipServer) {
Write-Verbose "Server $($[Link]) is being skipped
because EP is not enabled there or not in our list of servers that we care about."
continue
}

# Get-OutlookAnywhere doesn't return the FQDN so we must


compare the ComputerName instead
$rpcSettings = $outlookAnywhere | Where-Object
{ $_.ServerName -eq $[Link] }

if ($null -eq $rpcSettings) {


$line = "Failed to find '$($[Link])\RPC (Default
Web Site)' Virtual Directory to determine SSLOffloading value. $canNotConfigure"
Write-Verbose $line
Write-Warning $line
$[Link]($[Link])
} elseif ($[Link] -eq $true) {
$line = "'$($[Link])\RPC (Default Web Site)' has
SSLOffloading set to true. $canNotConfigure"
Write-Verbose $line
Write-Warning $line
$[Link]($[Link])
} else {
Write-Verbose "Server $($[Link]) passed RPC
SSLOffloading check"
}
}
Write-Progress @progressParams -Completed
if ($[Link] -gt 0) {
Write-Warning "Please address the following server
regarding RPC (Default Web Site) and SSL Offloading: $([string]::Join(", " ,
$rpcFailedServers))"
Write-Warning "The following cmdlet should be run against
each of the servers: Set-OutlookAnywhere 'SERVERNAME\RPC (Default Web Site)' -
SSLOffloading `$false -InternalClientsRequireSsl `$true -ExternalClientsRequireSsl
`$true"
$prerequisitesCheckFailed = $true
} elseif ($[Link] -gt 0) {
Write-Warning "Failed to find the following servers RPC
(Default Web Site) for SSL Offloading: $([string]::Join(", " ,$rpcNullServers))"
Write-Warning $canNotConfigure
$prerequisitesCheckFailed = $true
} else {
Write-Host "All servers that we are trying to currently
configure for Extended Protection have RPC (Default Web Site) set to false for
SSLOffloading."
}
} else {
Write-Verbose "No online servers that are in a supported state.
Skipping over TLS Check."
}
} else {
Write-Warning "Failed to get Extended Protection Prerequisites
Information to be able to continue"
exit
}

Write-Host ""
Write-Host ""

if ($prerequisitesCheckFailed) {
Write-Warning "Unable to continue due to the required prerequisites
to enable Extended Protection in the environment. Please address the above issues."
Write-Host ""
exit
} elseif ($PrerequisitesCheckOnly) {
Write-Host "Successfully passed the Prerequisites Check for the
server: $([string]::Join(", ", $[Link] ))" -
ForegroundColor Green

if ($[Link] -ne
$[Link]) {
Write-Host ""
Write-Warning "Not all Exchange Servers were included in this
Prerequisites Check. This could be caused by servers being down, or being excluded
from the list to check against."
}
Write-Host ""
exit
}

# Configure Extended Protection based on given parameters


# Prior to executing, add back any unsupported versions back into the
list
# for onlineSupportedServers, because the are online and we want to
revert them.
$unsupportedAndConfiguredServers | ForEach-Object
{ $[Link]($_) }
$extendedProtectionConfigurations = ($onlineSupportedServers |
Where-Object { $_.FQDN -in
$serverNames }).ExtendedProtectionConfiguration

if ($null -ne $extendedProtectionConfigurations) {


Invoke-ConfigureExtendedProtection -
ExtendedProtectionConfigurations $extendedProtectionConfigurations
} else {
Write-Host "No servers are online or no Exchange Servers Support
Extended Protection."
}

if ($ConfigureMitigationSelected) {
# Apply rules
$ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 15 -and (Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 0 -and $_.IsClientAccessServer) }
Invoke-ConfigureMitigation -ExchangeServers $[Link] -
ipRangeAllowListRules $ipRangeAllowListRules -SiteVDirLocations $SiteVDirLocations
}
} elseif ($RollbackSelected) {
Write-Host "Prerequisite check will be skipped due to Rollback"

if ($RollbackRestoreIISAppConfig) {
$params = @{
Message = "Display warning about legacy option of
RestoreIISAppConfig"
Target = "RestoreIISAppConfig is the legacy restore option
of ExchangeExtendedProtectionManagement." +
"`r`nIt will not work if there are no backup files present or
if the file is older than 30 days." +
"`r`nIt is recommended to use RestoreConfiguration or if you
are trying to disable Extended Protection due to automatic configuration in Setup,
use -DisableExtendedProtection"
Operation = "Attempt to restore using legacy option."
}

Show-Disclaimer @params
Invoke-RollbackExtendedProtection -ExchangeServers
$[Link]
}

if ($RollbackRestoreConfiguration) {

$params = @{
Message = "Display warning about doing a restore of Extended
Protection configuration."
Target = "RestoreConfiguration is going to restore all the
previous changed settings to the original value at the time the script was run when
attempting to change the setting." +
"`r`nIf no errors occurred during restore, it will then proceed
to remove the restore file." +
"`r`nThe removing of the file is to prevent a restore action to
be taken again if the configuration action hasn't been taken again."
Operation = "Continue to restore configuration for Extended
Protection."
}

Show-Disclaimer @params

$inputList = New-Object [Link][object]


$ExchangeServers | ForEach-Object
{ $[Link]([PSCustomObject]@{
ServerName = $_.FQDN
Restore = ([PSCustomObject]@{
FileName = "ConfigureExtendedProtection"
PassedWhatIf = $WhatIfPreference
})
}) }

Invoke-IISConfigurationManagerAction -InputObject $inputList -


ConfigurationDescription "Rollback Extended Protection"
}

if ($RollbackRestrictType) {
$ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 15 -and (Get-
ExchangeBuildVersionInformation -AdminDisplayVersion
$_.AdminDisplayVersion).[Link] -eq 0 -and $_.IsClientAccessServer) }
Invoke-RollbackIPFiltering -ExchangeServers $ExchangeServers -
SiteVDirLocations $SiteVDirLocations
}

return
} elseif ($DisableExtendedProtection) {
# Disabling EP for all the servers provided in the list.
Invoke-DisableExtendedProtection -ExchangeServers $[Link]
}
} finally {
Write-Host "Do you have feedback regarding the script? Please email
ExToolsFeedback@[Link]."
}
}

# SIG # Begin signature block


# MIIoKAYJKoZIhvcNAQcCoIIoGTCCKBUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCApxj5fmOGNkR+z
# 5QREy9tec+d5XgsFnxPxebsOImis4aCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGggwghoEAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIE
# IEtvbJE4X2sW1DEg3OzlncZqYDFGwTgFQq+2Vnl5fvf8MEIGCisGAQQBgjcCAQwx
# NDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20wDQYJKoZIhvcNAQEBBQAEggEAfG3lLG0C/2TfbyBfZR5ZyaZY5JoRE3um
# qOPFlSk2T6HZVTIJ94abMFHeOmJvD8y6TBzuZZ2SOpRcLpjSLvfgdM4Qayn8YEXu
# 6KIrOPWaHHD7CYvm2Y7zARxot2un5k1S7LHErP+0PEhCjJT/rmdhiDz52GXYDrE3
# ctaPv3M88lnbFCLCLefTufK0Xfs2cdaWIVmxV2b7OHGnCg+JkLxRci1ABT03GsBa
# x/3zjaUtJ1Wwg7G0jWqBv36LN/kQAi7dvHV0B1+eFmhiURsi/ai2ofysTqICrm4d
# KuOlpprpJ4gS9a1ygUsSHFQO8Q27YvM0s/PkQJbNNlGi2RJ74ULze6GCF7Awghes
# BgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJ
# YIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYB
# BAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCCswlWC/YVf+TH3TI98hiuCkCR15GZu
# YnWN9dPO54XIPQIGaBLC5w8aGBMyMDI1MDUxNTEzMTUwNy44OTRaMASAAgH0oIHZ
# pIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYD
# VQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNV
# BAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWlj
# cm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB
# +R9njXWrpPGxAAEAAAH5MA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwMB4XDTI0MDcyNTE4MzEwOVoXDTI1MTAyMjE4MzEwOVowgdMx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1p
# Y3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNo
# aWVsZCBUU1MgRVNOOjJBMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQg
# VGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
# AgEAtD1MH3yAHWHNVslC+CBTj/Mpd55LDPtQrhN7WeqFhReC9xKXSjobW1ZHzHU8
# V2BOJUiYg7fDJ2AxGVGyovUtgGZg2+GauFKk3ZjjsLSsqehYIsUQrgX+r/VATaW8
# /ONWy6lOyGZwZpxfV2EX4qAh6mb2hadAuvdbRl1QK1tfBlR3fdeCBQG+ybz9JFZ4
# 5LN2ps8Nc1xr41N8Qi3KVJLYX0ibEbAkksR4bbszCzvY+vdSrjWyKAjR6YgYhaBa
# DxE2KDJ2sQRFFF/egCxKgogdF3VIJoCE/Wuy9MuEgypea1Hei7lFGvdLQZH5Jo2Q
# R5uN8hiMc8Z47RRJuIWCOeyIJ1YnRiiibpUZ72+wpv8LTov0yH6C5HR/D8+AT4vq
# tP57ITXsD9DPOob8tjtsefPcQJebUNiqyfyTL5j5/J+2d+GPCcXEYoeWZ+nrsZSf
# rd5DHM4ovCmD3lifgYnzjOry4ghQT/cvmdHwFr6yJGphW/HG8GQd+cB4w7wGpOhH
# VJby44kGVK8MzY9s32Dy1THnJg8p7y1sEGz/A1y84Zt6gIsITYaccHhBKp4cOVNr
# foRVUx2G/0Tr7Dk3fpCU8u+5olqPPwKgZs57jl+lOrRVsX1AYEmAnyCyGrqRAzpG
# Xyk1HvNIBpSNNuTBQk7FBvu+Ypi6A7S2V2Tj6lzYWVBvuGECAwEAAaOCAUkwggFF
# MB0GA1UdDgQWBBSJ7aO6nJXJI9eijzS5QkR2RlngADAfBgNVHSMEGDAWgBSfpxVd
# AF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIw
# UENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYG
# A1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
# AQsFAAOCAgEAZiAJgFbkf7jfhx/mmZlnGZrpae+HGpxWxs8I79vUb8GQou50M1ns
# 7iwG2CcdoXaq7VgpVkNf1uvIhrGYpKCBXQ+SaJ2O0BvwuJR7UsgTaKN0j/yf3fpH
# D0ktH+EkEuGXs9DBLyt71iutVkwow9iQmSk4oIK8S8ArNGpSOzeuu9TdJjBjsasm
# uJ+2q5TjmrgEKyPe3TApAio8cdw/b1cBAmjtI7tpNYV5PyRI3K1NhuDgfEj5kynG
# F/uizP1NuHSxF/V1ks/2tCEoriicM4k1PJTTA0TCjNbkpmBcsAMlxTzBnWsqnBCt
# 9d+Ud9Va3Iw9Bs4ccrkgBjLtg3vYGYar615ofYtU+dup+LuU0d2wBDEG1nhSWHaO
# +u2y6Si3AaNINt/pOMKU6l4AW0uDWUH39OHH3EqFHtTssZXaDOjtyRgbqMGmkf8K
# I3qIVBZJ2XQpnhEuRbh+AgpmRn/a410Dk7VtPg2uC422WLC8H8IVk/FeoiSS4vFo
# dhncFetJ0ZK36wxAa3FiPgBebRWyVtZ763qDDzxDb0mB6HL9HEfTbN+4oHCkZa1H
# Kl8B0s8RiFBMf/W7+O7EPZ+wMH8wdkjZ7SbsddtdRgRARqR8IFPWurQ+sn7ftEif
# aojzuCEahSAcq86yjwQeTPN9YG9b34RTurnkpD+wPGTB1WccMpsLlM0wggdxMIIF
# WaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNy
# b3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAx
# ODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy
# MDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL
# 1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5K
# Wv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTeg
# Cjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv62
# 6GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SH
# JMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss25
# 4o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/Nme
# Rd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afo
# mXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLi
# Mxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb
# 0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W2
# 9R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQF
# AgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1Ud
# DgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdM
# g30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJ
# KwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8w
# TTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVj
# dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBK
# BggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9N
# aWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1V
# ffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1
# OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce57
# 32pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihV
# J9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZ
# UnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW
# 9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k
# +SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pF
# EUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L
# +DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1
# ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6
# CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZ
# pIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYD
# VQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNV
# BAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWlj
# cm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAqs5WjWO7
# zVAKmIcdwhqgZvyp6UaggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg
# MjAxMDANBgkqhkiG9w0BAQsFAAIFAOvQXjswIhgPMjAyNTA1MTUxMjM2MTFaGA8y
# MDI1MDUxNjEyMzYxMVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA69BeOwIBADAK
# AgEAAgIG4QIB/zAHAgEAAgIR0TAKAgUA69GvuwIBADA2BgorBgEEAYRZCgQCMSgw
# JjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3
# DQEBCwUAA4IBAQCfifcRx74pIjXpORh5gywNLWVyp1N57aBU4vA40z8/lbDpCOX+
# 0m4xMqtgt/cbI/XxJZ0kMZPjX4mUOMUJnOQmsOj8JX20Wl3qmL51Rm8m5iJqArdk
# jYTtYuZg4fEx/DMXW3fe7XDgoKsKglzGpfNQxs76kckrZrhCTR+GXYkP55tX7auQ
# XdT6b1ErswAewU9tHteoejyjvFGBTSK6/jWIii54ABs1ONjpRVkFhIXMa1AvrI85
# yfjmpyqj8n4vtk1j9L1TzrTh5gkMtXIXf/x3AAIs7I4Vqox9n4UHW8ST22N6yU81
# m91DQW0yqU9xjpuLlDV7w77PxNOw+UAIgJoPMYIEDTCCBAkCAQEwgZMwfDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAH5H2eNdauk8bEAAQAAAfkwDQYJ
# YIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkq
# hkiG9w0BCQQxIgQgjIUF9N28+bvvYv8cL9pvhUSEHlg+F1E+LihOEIDkMLswgfoG
# CyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCA5I4zIHvCN+2T66RUOLCZrUEVdoKlK
# l8VeCO5SbGLYEDCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# AhMzAAAB+R9njXWrpPGxAAEAAAH5MCIEIFfiUc95UTZbbQoX+qhSe4pCRDXgf94X
# 6uM4cgaknesqMA0GCSqGSIb3DQEBCwUABIICAJilOi6cjP40YR5bt6THfmU1wq7B
# u1dUIsBSAVoq7ssHXXoA6AZAbrYuKnIXoxXOOcgJNN6K6iTytPuk0cizva5RQrIP
# bQhOL7d2PCdCuPzSdYtLNy0GR06xR07IJtdnpU9NYgKbwedFTZAENUkl1KyObI1K
# OxsnIEOGfJTULc6qcttptMTrQpHaHBcwiS4A2uz6l4/1AXehAVogtjidHoJd8SNN
# j9hBjOn5vSEReyaqArmPAWIekogb9tOgim2EPnx/FXe3jMb8AnyDrrzi4eNzmGIa
# 5a5jP49B87pIl+cLEIWZ0aynmh4cgk1yo+wh6bIciUETKWxZVm23kj+BdGKrufb+
# k8w0Pd2hGzIU6mJsha9ja0hGni2Z+CaCZrFD52UcRqPn9M0IBXKG6qM5wntbzKxH
# Tr6MRpPIU3AEADRZ27onMeADiNWigHrTDo+eNfkMlij7k4wL9+fKyM1dDf3rEpEf
# 5VWapihw1sszjqkNqkfUCQNczscrWtOJ6miE1tCh+1PYHrGY/h6otB3FMbfoYO34
# gEXnn9w+Ccpj8OgGGD3ZMVNw6z2y5Ps0fTdYRNwzAP9CD+CDbEqgBKU8T1NLJ5j8
# wdN2/y7Td1kC3LFyticqtOcKxUU2ipOM/HDnBppieVyoVxoBJIU/WnEWGPh2A6TU
# FLyNvMNDrg44mbBC
# SIG # End signature block

Common questions

Powered by AI

Not verifying mitigation steps for Extended Protection and IP filtering can result in security vulnerabilities, leaving systems exposed to unauthorized access or attacks. Failure to enforce IP filtering could allow unverified traffic, while improper Extended Protection settings might weaken authentication processes, increasing security risks .

Detecting and adapting to different Exchange Server versions is crucial because each version may require different configuration settings, updates, or support criteria. It ensures the script applies correct settings, maintains compatibility, and addresses specific version-related requirements or vulnerabilities .

Progress parameters provide real-time feedback on task execution status to the user, indicating which operation is currently being performed and its completion percentage. They are implemented using hash tables that include activity descriptions, status updates, and percentage calculations, which are then displayed via Write-Progress commands .

The key performance limitation of the Invoke-IISConfigurationManagerAction function is that it operates synchronously when executing on each server, which makes it slow in large environments. This issue could be addressed by making the function multi-threaded to improve performance .

The Invoke-DisableExtendedProtection function identifies a server as not online or unable to collect the configuration if the Get-ExtendedProtectionConfiguration returns an indication that the server is not connected or fails to retrieve the configuration. In such cases, it adds the server to the failedServers list and logs a warning message .

Setting the extended protection's token checking to 'None' is significant as it disables certain security measures related to authentication processes, allowing for changes in security protocols. This setting is applied within the web server at each virtual directory defined for Exchange, under the filter 'system.WebServer/security/authentication/windowsAuthentication' .

If the default IP filtering rule does not meet expected conditions during validation, the script outputs verbose notifications specifying whether the rule is set to allow or deny by default. It logs issues on servers where the rules are not verified or contain errors, providing clear indications of unexpected configurations .

The Invoke-RollbackIPFiltering function ensures settings are restored by backing up existing configurations and reapplying them during rollback. Safeguards include using a 'WhatIf' parameter to test actions without applying changes, ensuring the function can safely preview the rollback impact before execution .

During the rollback process, the script backs up current IP filtering rules by saving the existing rules and default settings into a JSON file, if not in a 'WhatIf' mode. Restoration involves clearing existing rules and applying the original setup backed up earlier, ensuring the system returns to its previous state .

Invoke-ValidateMitigation verifies IP Filtering rules by checking the existing IP security configuration against specified rules. It assesses whether required server features are installed and if default deny settings are verified. Issues are indicated by the presence of unmitigated servers which either lack required IP ranges/addresses or do not meet IP filtering expectations, identified through logging of failed verification or missing IP rules .

You might also like