Exchange Extended Protection Script
Exchange Extended Protection Script
MIT License
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,
begin {
function Invoke-CatchActionError {
[CmdletBinding()]
param(
[ScriptBlock]$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 {
$params = @{
ComputerName = $ComputerName
ScriptBlock = $ScriptBlock
ErrorAction = "Stop"
}
if ($IncludeNoProxyServerOption) {
Write-Verbose "Including SessionOption"
$[Link]("SessionOption", (New-PSSessionOption -
ProxyAccessType NoProxyServer))
}
function WriteErrorInformationBase {
[CmdletBinding()]
param(
[object]$CurrentError = $Error[0],
[ValidateSet("Write-Host", "Write-Verbose")]
[string]$Cmdlet
)
if ($null -ne
$[Link]) {
& $Cmdlet "Remote Position Message: $
($[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
}
$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
)
$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
}
$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 = @()
$[Link] += $IpString
}
}
}
if ($[Link] -gt 0) {
$[Link] = $true
Add-WebConfigurationProperty -Filter $Filter -PSPath $IISPath
-Location $SiteVDirLocation -Name "." -Value $RulesToBeAdded -ErrorAction Stop -
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
$[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]
$SiteVDirLocations | ForEach-Object {
$FailedServersFilter[$_] = New-Object
'[Link][string]'
$UnchangedFilterServers[$_] = New-Object
'[Link][string]'
}
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
}
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
<#
.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])"
# 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 }
function GetLocationValue {
[CmdletBinding()]
param(
[hashtable]$CmdParameters
)
try {
Write-VerboseAndLog "-------------------------------------------------"
Write-VerboseAndLog "Starting IIS Configuration$(if($isRestoreOption)
{ " Restore" }) Action: $([DateTime]::Now)"
Write-VerboseAndLog "-------------------------------------------------"
try {
$loadingJson = Get-Content $backupRestoreFilePath -
ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
$[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]
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.
end {
try {
Write-Progress @remoteActionProgressParams -Completed
} catch {
Write-VerboseAndLog "Failed to Write-Process with -Completed"
$[Link]($_)
}
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,
$managerActionProgressCounter = 0
$managerActionTotalActions = $[Link]
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.
#>
$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
}
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
}
$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'
if (-not $[Link]) {
return
}
$Filter = '[Link]/security/ipSecurity'
$IISPath = 'IIS:\'
$[Link] = $true
$[Link] = $true
$localIPs | ForEach-Object {
$IpRangesForFiltering += @{Type="Single IP"; IP=$_;
Allowed=$true }
}
$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]'
}
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
)
$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"
}
}
$[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]'
}
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"
}
# EWS FE
if ($ExcludeEWSFe -and $VirtualDirectory -eq "EWS" -and
$WebSite[$i] -eq "Default Web Site") { $ExtendedProtection[$i] = "None" }
[PSCustomObject]@{
VirtualDirectory = $virtualDirectory
WebSite = $WebSite[$i]
ExtendedProtection = $ExtendedProtection[$i]
SslFlags = $SslFlags[$i]
}
}
}
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 (-not([string]::IsNullOrEmpty($ipRestrictions))) {
Write-Verbose "IP-filtered restrictions detected"
foreach ($restriction in $[Link]) {
$[Link]($[Link], $[Link])
}
}
# SSL flags:
[Link]
access#attributes
$requireSsl = $false
$ssl128Bit = $false
$clientCertificate = "Unknown"
$extendedConfiguration = GetExtendedProtectionConfiguration -
Xml $applicationHostConfig -Path "$($[Link])/$
($[Link])"
$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])"
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 ($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
}
$[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
}
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] -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 ($tooOld) {
throw "Configuration file is too old to restore from.
Please use -DisableExtendedProtection to Disable Extended Protection."
}
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"
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 {
if (-not ([[Link]]::IsNullOrWhiteSpace($regSubKey))) {
Write-Verbose "Attempting to get the value $GetValue"
$registryGetValue = $[Link]($GetValue)
Write-Verbose "Finished running GetValue()"
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 (-not ($WmiObject)) {
Write-Verbose "Working on NIC: $
($[Link])"
$adapter = $[Link]
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"
foreach ($ipAddress in
$[Link]) {
if ($[Link](":")) {
$ipv6Enabled = $true
}
}
$mtuSize = $[Link]
$driverDate = $[Link]
$driverVersion = $[Link]
$description = $[Link]
$dnsServerToBeUsed =
$[Link]
} else {
Write-Verbose "Working on NIC: $
($[Link])"
$adapter = $networkConfig
$description = $[Link]
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
}
}
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
}
$wmiNetworkCardsParams = @{
ComputerName = $ComputerName
Class = "Win32_NetworkAdapter"
Filter = "NetConnectionStatus ='2'"
CatchActionFunction = $CatchActionFunction
}
$wmiNetworkCards = Get-WmiObjectHandler @wmiNetworkCardsParams
# 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
}
$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++
}
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
}
# 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)
$IpAddressString = $[Link]("/")[0]
$SubnetMaskString = $[Link]("/")[1]
if ($SubnetMaskString) {
# Check if the subnet value is valid (IPv4 <= 32, IPv6 <= 128
or empty)
$SubnetMask = $SubnetMaskString -as [int]
$[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,
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
}
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)
}
$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
}
function Get-TlsCipherSuiteInformation {
[OutputType("[Link]")]
param(
[string]$MachineName = $env:COMPUTERNAME,
[ScriptBlock]$CatchActionFunction
)
begin {
function GetProtocolNames {
param(
[int[]]$Protocol
)
$protocolNames = New-Object [Link][string]
$[Link]($name)
}
return [string]::Join(" & ", $protocolNames)
}
$policyTlsRegistryParams = @{
MachineName = $MachineName
SubKey = "SOFTWARE\Policies\Microsoft\Cryptography\
Configuration\SSL\00010002"
GetValue = "Functions"
ValueType = "String"
CatchActionFunction = $CatchActionFunction
}
[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 ($[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"
}
$[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
}
}
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
)
# 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
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
}
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"
$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
function SetProperForegroundColor {
$Script:OriginalConsoleForegroundColor = $[Link]
if ($[Link] -eq
$[Link]) {
Write-Verbose "Foreground Color matches warning's color"
function RevertProperForegroundColor {
$[Link] = $Script:OriginalConsoleForegroundColor
}
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
if ($PSSenderInfo -and
$null -ne $Script:WriteRemoteProgressDebugAction) {
& $Script:WriteRemoteProgressDebugAction $message
}
}
}
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 ($PSSenderInfo -and
$null -ne $Script:WriteVerboseRemoteManipulateMessageAction) {
$Message = & $Script:WriteVerboseRemoteManipulateMessageAction $Message
}
[Link]\Write-Verbose $Message
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 {
[Link]\Write-Warning $Message
# 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(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]
}
}
<#
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"
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"
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"
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."
}
$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 {
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"
})
}
}
}
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
}
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
# 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
)
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."
}
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])
}
}
$validExchangeServer | ForEach-Object
{ $[Link]($_.FQDN) }
function Confirm-ProxyServer {
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory = $true)]
[string]
$TargetUri
)
function Invoke-WebRequestWithProxyDetection {
[CmdletBinding(DefaultParameterSetName = "Default")]
param (
[Parameter(Mandatory = $true, ParameterSetName = "Default")]
[string]
$Uri,
[[Link]]::SecurityProtocol = [[Link]]::Tls12
if (Confirm-ProxyServer -TargetUri $Uri) {
$webClient = New-Object [Link]
$[Link]("User-Agent", "PowerShell")
$[Link] =
[[Link]]::DefaultNetworkCredentials
}
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
}
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 (-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]
$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)
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.
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
$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
)
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,
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
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)]
[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
}
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 ($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)
$colColors[$i] = $fgColor
}
$colColors
}
function GetColumnWidths {
param($objects, $props)
# 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
}
}
return $colWidths
}
process {
foreach ($thing in $InputObject) {
[void]$[Link]($thing)
}
}
end {
if ($[Link] -gt 0) {
$props = $null
Write-Host
[void]$[Link]([[Link]]::NewLine)
Write-Host
[void]$[Link]([[Link]]::NewLine)
Write-Host
[void]$[Link]([[Link]]::NewLine)
Write-Host
[void]$[Link]([[Link]]::NewLine)
}
Write-Host
[void]$[Link]([[Link]]::NewLine)
function Show-Disclaimer {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[ValidateNotNullOrEmpty()]
[string]$Message,
[ValidateNotNullOrEmpty()]
[string]$Target,
[ValidateNotNullOrEmpty()]
[string]$Operation
)
SetWriteHostAction ${Function:Write-HostLog}
SetWriteVerboseAction ${Function:Write-VerboseLog}
SetWriteWarningAction ${Function:Write-HostLog}
SetWriteProgressAction ${Function:Write-HostLog}
$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", "") }
}
$ipRangeAllowListRules = $[Link]
}
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
}
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 ($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))
}
$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]
}))
}
return
}
$_.[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])})")
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) {
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
}
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) }
}
function NewDisplayObject {
param(
[string]$RegistryName,
[string]$Location,
[object]$Value
)
return [PSCustomObject]@{
RegistryName = $RegistryName
Location = $Location
Value = $Value
}
}
$[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}
}
$wantedResults = Invoke-
ExtendedProtectionTlsPrerequisitesCheck -TlsConfiguration
$[Link]
$checkResults = Invoke-
ExtendedProtectionTlsPrerequisitesCheck -TlsConfiguration $[Link]
$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 }
$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
$_
}
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
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
}
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
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]."
}
}
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 .