diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 new file mode 100644 index 0000000000..5754d7ed4d --- /dev/null +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ConfigureMitigation.ps1 @@ -0,0 +1,310 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1 +. $PSScriptRoot\..\..\..\..\Shared\Write-ErrorInformation.ps1 + +function Invoke-ConfigureMitigation { + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]]$ExchangeServers, + [Parameter(Mandatory = $true)] + [object[]]$IPRangeAllowListRules , + [Parameter(Mandatory = $true)] + [string[]]$SiteVDirLocations + ) + + begin { + $FailedServersFilter = @{} + $UnchangedFilterServers = @{} + + $progressParams = @{ + Activity = "Applying IP filtering Rules" + Status = [string]::Empty + PercentComplete = 0 + } + + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + + $ConfigureMitigation = { + param( + [Object]$Arguments + ) + + $SiteVDirLocations = $Arguments.SiteVDirLocations + $IpRangesForFiltering = $Arguments.IpRangesForFiltering + $WhatIf = $Arguments.PassedWhatIf + + $results = @{ + IsWindowsFeatureInstalled = $false + IsGetLocalIPSuccessful = $false + LocalIPs = New-Object 'System.Collections.Generic.List[string]' + ErrorContext = $null + } + + function BackupCurrentIPFilteringRules { + param( + [Parameter(Mandatory = $true)] + [string]$BackupPath, + [Parameter(Mandatory = $true)] + [string]$Filter, + [Parameter(Mandatory = $true)] + [string]$IISPath, + [Parameter(Mandatory = $true)] + [string]$SiteVDirLocation, + [Parameter(Mandatory = $false)] + [object[]]$ExistingRules + ) + + $DefaultForUnspecifiedIPs = Get-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted" + if ($null -eq $ExistingRules) { + $ExistingRules = New-Object 'System.Collections.Generic.List[object]' + } + + $BackupFilteringConfiguration = @{Rules=$ExistingRules; DefaultForUnspecifiedIPs=$DefaultForUnspecifiedIPs } + if (-not $WhatIf) { + $BackupFilteringConfiguration | ConvertTo-Json -Depth 2 | Out-File $BackupPath + } + + return $true + } + + function GetLocalIPAddresses { + $ips = New-Object 'System.Collections.Generic.List[string]' + $interfaces = Get-NetIPAddress -ErrorAction Stop + foreach ($interface in $interfaces) { + if ($interface.AddressState -eq 'Preferred') { + $ips += $interface.IPAddress + } + } + + return $ips + } + + # Create IP allow list from user provided IP subnets + function CreateIPRangeAllowList { + param ( + [Parameter(Mandatory = $true)] + [string]$SiteVDirLocation, + [Parameter(Mandatory = $true)] + [object[]]$IpFilteringRules, + [Parameter(Mandatory = $true)] + [hashtable] $state + ) + + $backupPath = "$($env:WINDIR)\System32\inetsrv\config\IpFilteringRules_" + $SiteVDirLocation.Replace('/', '-') + "_$([DateTime]::Now.ToString("yyyyMMddHHMMss")).bak" + $Filter = 'system.webServer/security/ipSecurity' + $IISPath = 'IIS:\' + $ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter -Location $SiteVDirLocation -name collection) + $state.IsBackUpSuccessful = BackupCurrentIPFilteringRules -BackupPath $backupPath -Filter $Filter -IISPath $IISPath -SiteVDirLocation $SiteVDirLocation -ExistingRules $ExistingRules + + $RulesToBeAdded = @() + + foreach ($IpFilteringRule in $IpFilteringRules) { + $ExistingIPSubnetRule = $ExistingRules | Where-Object { $_.ipAddress -eq $IpFilteringRule.IP -and + ($_.subnetMask -eq $IpFilteringRule.SubnetMask -or $IpFilteringRule.Type -eq "Single IP") + } + + if ($null -eq $ExistingIPSubnetRule) { + if ($IpFilteringRule.Type -eq "Single IP") { + $RulesToBeAdded += @{ipAddress=$IpFilteringRule.IP; allowed=$IpFilteringRule.Allowed; } + } else { + $RulesToBeAdded += @{ipAddress=$IpFilteringRule.IP; subnetMask=$IpFilteringRule.SubnetMask; allowed=$IpFilteringRule.Allowed; } + } + } else { + if ($ExistingIPSubnetRule.allowed -ne $IpFilteringRule.Allowed) { + if ($IpFilteringRule.Type -eq "Single IP") { + $IpString = $IpFilteringRule.IP + } else { + $IpString = ("{0}/{1}" -f $IpFilteringRule.IP, $IpFilteringRule.SubnetMask) + } + + $state.IPsNotAdded += $IpString + } + } + } + + if ($RulesToBeAdded.Count + $ExistingRules.Count -gt 500) { + $state.IPsNotAdded += $RulesToBeAdded + throw 'Too many IP filtering rules (Existing rules [$($ExistingRules.Count)] + New rules [$($RulesToBeAdded.Count)] > 500). Please reduce the specified entries by providing appropriate subnets.' + } + + if ($RulesToBeAdded.Count -gt 0) { + $state.AreIPRulesModified = $true + Add-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "." -Value $RulesToBeAdded -ErrorAction Stop -WhatIf:$WhatIf + } + + $state.IsCreateIPRulesSuccessful = $true + + # Setting default to deny + Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted" -Value $false -WhatIf:$WhatIf + $state.IsSetDefaultRuleSuccessful = $true + } + + try { + try { + $baseError = "Installation of IP and Domain filtering Module failed." + $InstallResult = Install-WindowsFeature Web-IP-Security -ErrorAction Stop -WhatIf:$WhatIf + if (-not $InstallResult.Success) { + throw $baseError + } + } catch { + throw "$baseError Inner exception: $_" + } + + $results.IsWindowsFeatureInstalled = $true + + $localIPs = GetLocalIPAddresses + $results.IsGetLocalIPSuccessful = $true + + foreach ($localIP in $localIPs) { + if ($null -eq ($IpRangesForFiltering | Where-Object { $_.Type -eq "Single IP" -and $_.IP -eq $localIP })) { + $IpRangesForFiltering += @{Type="Single IP"; IP=$localIP; Allowed=$true } + } + } + + $results.LocalIPs = $localIPs + foreach ($SiteVDirLocation in $SiteVDirLocations) { + $state = @{ + IsBackUpSuccessful = $false + IsCreateIPRulesSuccessful = $false + IsSetDefaultRuleSuccessful = $false + ErrorContext = $null + IPsNotAdded = New-Object 'System.Collections.Generic.List[string]' + AreIPRulesModified = $false + } + + try { + CreateIPRangeAllowList -SiteVDirLocation $SiteVDirLocation -IpFilteringRules $IpRangesForFiltering -state $state + } catch { + $state.ErrorContext = $_ + } + + $results[$SiteVDirLocation] = $state + } + } catch { + $results.ErrorContext = $_ + } + + return $results + } + } process { + $scriptblockArgs = [PSCustomObject]@{ + SiteVDirLocations = $SiteVDirLocations + IpRangesForFiltering = $IPRangeAllowListRules + PassedWhatIf = $WhatIfPreference + } + + $counter = 0 + $totalCount = $ExchangeServers.Count + + if ($null -eq $IPRangeAllowListRules ) { + $IPRangeAllowListString = "null" + } else { + $IPStrings = @() + $IPRangeAllowListRules | ForEach-Object { + if ($_.Type -eq "Single IP") { + $IPStrings += $_.IP + } else { + $IPStrings += ("{0}/{1}" -f $_.IP, $_.SubnetMask) + } + } + $IPRangeAllowListString = [string]::Join(", ", $IPStrings) + } + + $SiteVDirLocations | ForEach-Object { + $FailedServersFilter[$_] = New-Object 'System.Collections.Generic.List[string]' + $UnchangedFilterServers[$_] = New-Object 'System.Collections.Generic.List[string]' + } + + foreach ($Server in $ExchangeServers) { + $baseStatus = "Processing: $Server -" + $progressParams.PercentComplete = ($counter / $totalCount * 100) + $progressParams.Status = "$baseStatus Applying rules" + Write-Progress @progressParams + $counter ++; + + Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with arguments SiteVDirLocation: {1}, IPRangeAllowListRules : {2}" -f $Server, $SiteVDirLocation, $IPRangeAllowListString) + $resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock $ConfigureMitigation -ArgumentList $scriptblockArgs + + Write-Verbose ("Adding IP Restriction rules on Server {0}" -f $Server) + if ($resultsInvoke.IsWindowsFeatureInstalled) { + Write-Verbose ("Successfully installed windows feature - Web-IP-Security on server {0}" -f $Server) + } else { + Write-Host ("Script failed to install windows feature - Web-IP-Security on server {0} with the Inner Exception:" -f $Server) -ForegroundColor Red + Write-HostErrorInformation $resultsInvoke.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } + + if ($resultsInvoke.IsGetLocalIPSuccessful) { + Write-Verbose ("Successfully retrieved local IPs for the server") + if ($null -ne $resultsInvoke.LocalIPs -and $resultsInvoke.LocalIPs.Length -gt 0) { + Write-Verbose ("Local IPs detected for this server: {0}" -f [string]::Join(", ", [string[]]$resultsInvoke.LocalIPs)) + } 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 $resultsInvoke.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } + + foreach ($SiteVDirLocation in $SiteVDirLocations) { + $state = $resultsInvoke[$SiteVDirLocation] + + if ($state.IsBackUpSuccessful) { + 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 $state.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } + + if ($state.IsCreateIPRulesSuccessful) { + if ($state.IPsNotAdded.Length -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(", ", $state.IPsNotAdded)) + } + + if (-not $state.AreIPRulesModified) { + 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 $state.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } + + if ($state.IsSetDefaultRuleSuccessful) { + 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 $state.ErrorContext + $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])) + } + } + } +} diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 new file mode 100644 index 0000000000..85f1c0056c --- /dev/null +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-RollbackIPFiltering.ps1 @@ -0,0 +1,218 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1 +. $PSScriptRoot\..\..\..\..\Shared\Write-ErrorInformation.ps1 + +function Invoke-RollbackIPFiltering { + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object[]]$ExchangeServers, + [Parameter(Mandatory = $true)] + [string[]]$SiteVDirLocations + ) + + begin { + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + $FailedServers = @{} + + $progressParams = @{ + Activity = "Rolling back IP filtering Rules" + Status = [string]::Empty + PercentComplete = 0 + } + + $RollbackIPFiltering = { + param( + [Object]$Arguments + ) + + $SiteVDirLocations = $Arguments.SiteVDirLocations + $WhatIf = $Arguments.PassedWhatIf + $Filter = 'system.webServer/security/ipSecurity' + $FilterEP = 'system.WebServer/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)] + [System.Collections.Generic.List[object]]$ExistingRules + ) + + $DefaultForUnspecifiedIPs = Get-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted" + if ($null -eq $ExistingRules) { + $ExistingRules = New-Object 'System.Collections.Generic.List[object]' + } + + $BackupFilteringConfiguration = @{Rules=$ExistingRules; DefaultForUnspecifiedIPs=$DefaultForUnspecifiedIPs } + if (-not $WhatIf) { + $BackupFilteringConfiguration | ConvertTo-Json -Depth 2 | Out-File $BackupPath + } + + return $true + } + + function RestoreOriginalIPFilteringRules { + param( + [Parameter(Mandatory = $true)] + [string]$Filter, + [Parameter(Mandatory = $true)] + [string]$IISPath, + [Parameter(Mandatory = $true)] + [string]$SiteVDirLocation, + [Parameter(Mandatory = $false)] + [object[]]$OriginalIpFilteringRules, + [Parameter(Mandatory = $true)] + [object]$DefaultForUnspecifiedIPs + ) + + Clear-WebConfiguration -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -ErrorAction Stop -WhatIf:$WhatIf + $RulesToBeAdded = New-Object 'System.Collections.Generic.List[object]' + foreach ($IpFilteringRule in $OriginalIpFilteringRules) { + $RulesToBeAdded += @{ipAddress=$IpFilteringRule.ipAddress; subnetMask=$IpFilteringRule.subnetMask; domainName=$IpFilteringRule.domainName; allowed=$IpFilteringRule.allowed; } + } + Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted" -Value $DefaultForUnspecifiedIPs.Value -WhatIf:$WhatIf + if ($OriginalIpFilteringRules.Length -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 "extendedProtection.tokenChecking" + if ($ExtendedProtection -ne "Require") { + Set-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "extendedProtection.tokenChecking" -Value "Require" + } + } + + foreach ($SiteVDirLocation in $SiteVDirLocations) { + $state = @{ + TurnOnEPSuccessful = $false + RestoreFileExists = $false + BackUpPath = $null + BackupCurrentSuccessful = $false + RestorePath = $null + RestoreSuccessful = $false + ErrorContext = $null + } + try { + $state.RestorePath = (Get-ChildItem "$($env:WINDIR)\System32\inetsrv\config\" -Filter ("*IpFilteringRules_"+ $SiteVDirLocation.Replace('/', '-') + "*.bak") | Sort-Object CreationTime | Select-Object -First 1).FullName + if ($null -eq $state.RestorePath) { + throw "Invalid operation. No backup file exisits at path $($env:WINDIR)\System32\inetsrv\config\" + } + $state.RestoreFileExists = $true + + TurnONExtendedProtection -Filter $FilterEP -IISPath $IISPath -SiteVDirLocation $SiteVDirLocation + $state.TurnOnEPSuccessful = $true + + $state.BackUpPath = "$($env:WINDIR)\System32\inetsrv\config\IpFilteringRules_" + $SiteVDirLocation.Replace('/', '-') + "_$([DateTime]::Now.ToString("yyyyMMddHHMMss")).bak" + $ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter -Location $SiteVDirLocation -name collection) + $state.BackupCurrentSuccessful = BackupCurrentIPFilteringRules -BackupPath $state.BackUpPath -Filter $Filter -IISPath $IISPath -SiteVDirLocation $SiteVDirLocation -ExistingRules $ExistingRules + + $originalIpFilteringConfigurations = (Get-Content $state.RestorePath | Out-String | ConvertFrom-Json) + $state.RestoreSuccessful = RestoreOriginalIPFilteringRules -OriginalIpFilteringRules ($originalIpFilteringConfigurations.Rules) -DefaultForUnspecifiedIPs ($originalIpFilteringConfigurations.DefaultForUnspecifiedIPs) -Filter $Filter -IISPath $IISPath -SiteVDirLocation $SiteVDirLocation + } catch { + $state.ErrorContext = $_ + } + + $results[$SiteVDirLocation] = $state + } + + return $results + } + } process { + $scriptblockArgs = [PSCustomObject]@{ + SiteVDirLocations = $SiteVDirLocations + PassedWhatIf = $WhatIfPreference + } + + $exchangeServersProcessed = 0 + $totalExchangeServers = $ExchangeServers.Count + + $SiteVDirLocations | ForEach-Object { + $FailedServers[$_] = New-Object 'System.Collections.Generic.List[string]' + } + + foreach ($Server in $ExchangeServers) { + $baseStatus = "Processing: $($Server.Name) -" + $progressParams.PercentComplete = ($exchangeServersProcessed / $totalExchangeServers * 100) + $progressParams.Status = "$baseStatus Rolling back rules" + Write-Progress @progressParams + $exchangeServersProcessed++; + + Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with Arguments Site: {1}, VDir: {2}" -f $Server.Name, $Site, $VDir) + Write-Verbose ("Restoring previous state for Server {0}" -f $Server.Name) + $resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server.Name -ScriptBlock $RollbackIPFiltering -ArgumentList $scriptblockArgs + + if ($null -eq $resultsInvoke) { + $line = "Server Unreachable: Unable to rollback IP filtering rules on server $($Server.Name)." + Write-Verbose $line + Write-Warning $line + $SiteVDirLocations | ForEach-Object { $FailedServers[$_].Add($Server.Name) } + continue + } + + foreach ($SiteVDirLocation in $SiteVDirLocations) { + $Failed = $false + $state = $resultsInvoke[$SiteVDirLocation] + if ($state.RestoreFileExists) { + if ($state.TurnOnEPSuccessful) { + Write-Host "Turned on Extended Protection on server $($Server.Name) for VDir $SiteVDirLocation" + if ($state.BackupCurrentSuccessful) { + Write-Verbose "Successfully backed up current configuration on server $($Server.Name) at $($state.BackUpPath) for VDir $SiteVDirLocation" + if ($state.RestoreSuccessful) { + Write-Host "Successfully rolled back IP filtering rules on server $($Server.Name) from $($state.RestorePath) for VDir $SiteVDirLocation" + } else { + Write-Host "Failed to rollback IP filtering rules on server $($Server.Name). Aborting rollback on the server $($Server.Name) for VDir $SiteVDirLocation. Inner Exception:" -ForegroundColor Red + Write-HostErrorInformation $state.ErrorContext + $Failed = $true + } + } else { + Write-Host "Failed to backup the current configuration on server $($Server.Name). Aborting rollback on the server $($Server.Name) for VDir $SiteVDirLocation. Inner Exception:" -ForegroundColor Red + Write-HostErrorInformation $state.ErrorContext + $Failed = $true + } + } else { + Write-Host "Failed to turn on Extended Protection on server $($Server.Name). Aborting rollback on the server $($Server.Name) for VDir $SiteVDirLocation. Inner Exception:" -ForegroundColor Red + Write-HostErrorInformation $state.ErrorContext + $Failed = $true + } + } else { + Write-Host "No restore file exists on server $($Server.Name). Aborting rollback on the server $($Server.Name) for VDir $SiteVDirLocation." -ForegroundColor Red + $Failed = $true + } + + if ($Failed) { + $FailedServers[$SiteVDirLocation] += $Server.Name + } + } + } + } 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 + } + } + } +} diff --git a/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 new file mode 100644 index 0000000000..5d706e7ad3 --- /dev/null +++ b/Security/src/ExchangeExtendedProtectionManagement/ConfigurationAction/Invoke-ValidateMitigation.ps1 @@ -0,0 +1,284 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\Shared\Invoke-ScriptBlockHandler.ps1 +. $PSScriptRoot\..\..\..\..\Shared\Write-ErrorInformation.ps1 + +function Invoke-ValidateMitigation { + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]]$ExchangeServers, + [Parameter(Mandatory = $false)] + [object[]]$ipRangeAllowListRules, + [Parameter(Mandatory = $true)] + [string[]]$SiteVDirLocations + ) + + begin { + $FailedServersEP = @{} + $FailedServersFilter = @{} + + $UnMitigatedServersEP = @{} + $UnMitigatedServersFilter = @{} + + $progressParams = @{ + Activity = "Verifying Mitigations" + Status = [string]::Empty + PercentComplete = 0 + } + + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + + $ValidateMitigationScriptBlock = { + param( + [Object]$Arguments + ) + + $SiteVDirLocations = $Arguments.SiteVDirLocations + $IpRangesForFiltering = $Arguments.IpRangesForFiltering + + $results = @{} + + function GetLocalIPAddresses { + $ips = New-Object 'System.Collections.Generic.List[string]' + $interfaces = Get-NetIPAddress + foreach ($interface in $interfaces) { + if ($interface.AddressState -eq 'Preferred') { + $ips += $interface.IPAddress + } + } + + return $ips + } + + # Set EP to None + function GetExtendedProtectionState { + param ( + [Parameter(Mandatory = $true)] + [string]$SiteVDirLocation + ) + + $Filter = 'system.webServer/security/authentication/windowsAuthentication/extendedProtection' + + $ExtendedProtection = Get-WebConfigurationProperty -Filter $Filter -Location $SiteVDirLocation -name tokenChecking + return $ExtendedProtection + } + + # Create IP allow list from user provided IP subnets + function VerifyIPRangeAllowList { + param ( + [Parameter(Mandatory = $true)] + [string]$SiteVDirLocation, + [Parameter(Mandatory = $true)] + [object[]]$IpFilteringRules, + [Parameter(Mandatory = $true)] + [hashtable]$state + ) + + $state.IsWindowsFeatureInstalled = (Get-WindowsFeature -Name "Web-IP-Security").InstallState -eq "Installed" + $state.IsWindowsFeatureVerified = $true + + if (-not $state.IsWindowsFeatureInstalled) { + return + } + + $Filter = 'system.webServer/security/ipSecurity' + $IISPath = 'IIS:\' + + $ExistingRules = @(Get-WebConfigurationProperty -Filter $Filter -Location $SiteVDirLocation -name collection) + + foreach ($IpFilteringRule in $IpFilteringRules) { + $ExistingIPSubnetRule = $ExistingRules | Where-Object { + $_.ipAddress -eq $IpFilteringRule.IP -and + ($_.subnetMask -eq $IpFilteringRule.SubnetMask -or $IpFilteringRule.Type -eq "Single IP") -and + $_.allowed -eq $IpFilteringRule.Allowed + } + + if ($null -eq $ExistingIPSubnetRule) { + if ($IpFilteringRule.Type -eq "Single IP") { + $IpString = $IpFilteringRule.IP + } else { + $IpString = ("{0}/{1}" -f $IpFilteringRule.IP, $IpFilteringRule.SubnetMask) + } + $state.RulesNotFound += $IpString + } + } + + $state.AreIPRulesVerified = $true + + $state.IsDefaultFilterDeny = -not ((Get-WebConfigurationProperty -Filter $Filter -PSPath $IISPath -Location $SiteVDirLocation -Name "allowUnlisted").Value) + $state.IsDefaultFilterVerified = $true + } + + foreach ($SiteVDirLocation in $SiteVDirLocations) { + try { + $state = @{ + IsEPVerified = $false + IsEPOff = $false + IsWindowsFeatureInstalled = $false + IsWindowsFeatureVerified = $false + AreIPRulesVerified = $false + IsDefaultFilterVerified = $false + IsDefaultFilterDeny = $false + RulesNotFound = New-Object 'System.Collections.Generic.List[string]' + ErrorContext = $null + } + + $EPState = GetExtendedProtectionState -SiteVDirLocation $SiteVDirLocation + if ($EPState -eq "None") { + $state.IsEPOff = $true + } else { + $state.IsEPOff = $false + } + + $state.IsEPVerified = $true + + if ($null -ne $IpRangesForFiltering) { + $localIPs = GetLocalIPAddresses + + $localIPs | ForEach-Object { + $IpRangesForFiltering += @{Type="Single IP"; IP=$_; Allowed=$true } + } + + VerifyIPRangeAllowList -SiteVDirLocation $SiteVDirLocation -IpFilteringRules $IpRangesForFiltering -state $state + } + } catch { + $state.ErrorContext = $_ + } + + $results[$SiteVDirLocation] = $state + } + + return $results + } + } process { + $scriptblockArgs = [PSCustomObject]@{ + SiteVDirLocations = $SiteVDirLocations + IpRangesForFiltering = $ipRangeAllowListRules + } + + $counter = 0 + $totalCount = $ExchangeServers.Count + if ($null -eq $ipRangeAllowListRules) { + $ipRangeAllowListString = "null" + } else { + $ipRangeAllowListString = [string]::Join(", ", $ipRangeAllowListRules) + } + + $SiteVDirLocations | ForEach-Object { + $FailedServersEP[$_] = New-Object 'System.Collections.Generic.List[string]' + $FailedServersFilter[$_] = New-Object 'System.Collections.Generic.List[string]' + + $UnMitigatedServersEP[$_] = New-Object 'System.Collections.Generic.List[string]' + $UnMitigatedServersFilter[$_] = New-Object 'System.Collections.Generic.List[string]' + } + + foreach ($Server in $ExchangeServers) { + $baseStatus = "Processing: $Server -" + $progressParams.PercentComplete = ($counter / $totalCount * 100) + $progressParams.Status = "$baseStatus Validating rules" + Write-Progress @progressParams + $counter ++; + + Write-Verbose ("Calling Invoke-ScriptBlockHandler on Server {0} with arguments SiteVDirLocations: {1}, ipRangeAllowListRules: {2}" -f $Server, [string]::Join(", ", $SiteVDirLocations), $ipRangeAllowListString) + $resultsInvoke = Invoke-ScriptBlockHandler -ComputerName $Server -ScriptBlock $ValidateMitigationScriptBlock -ArgumentList $scriptblockArgs + + if ($null -eq $resultsInvoke) { + $line = "Server Unreachable: Unable to validate IP filtering rules on server $($Server)." + Write-Verbose $line + Write-Warning $line + $SiteVDirLocations | ForEach-Object { $FailedServersEP[$_].Add($Server) } + $SiteVDirLocations | ForEach-Object { $FailedServersFilter[$_].Add($Server) } + continue + } + + foreach ($SiteVDirLocation in $SiteVDirLocations) { + $state = $resultsInvoke[$SiteVDirLocation] + + if ($state.IsEPOff) { + Write-Verbose ("Expected: The state of Extended protection flag is None for Vdir $($SiteVDirLocation) on server $Server") + } elseif ($state.IsEPVerified) { + 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 $results.ErrorContext + $FailedServersEP[$SiteVDirLocation] += $Server + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } + + $IsFilterUnMitigated = $false + + if (-not $state.IsWindowsFeatureVerified) { + 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 $results.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } elseif (-not $state.IsWindowsFeatureInstalled) { + 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 $state.AreIPRulesVerified) { + Write-Host ("Unknown: Script failed to verify IP Filtering Rules for Vdir $($SiteVDirLocation) on server $Server with Inner Exception") -ForegroundColor Red + Write-HostErrorInformation $results.ErrorContext + $FailedServersFilter[$SiteVDirLocation] += $Server + continue + } elseif ($null -ne $state.RulesNotFound -and $state.RulesNotFound.Length -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[]]$state.RulesNotFound)) + $IsFilterUnMitigated = $true + } else { + Write-Verbose ("Expected: Successfully verified all the IP filtering rules for Vdir $($SiteVDirLocation) on server $Server") + } + + if ($state.IsDefaultFilterDeny) { + Write-Verbose ("Expected: The default IP Filtering rule is set to deny for Vdir $($SiteVDirLocation) on server $Server") + } elseif ($state.IsDefaultFilterVerified) { + 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 $results.ErrorContext + $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 + } + } +} diff --git a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExchangeServerIPs.ps1 b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExchangeServerIPs.ps1 new file mode 100644 index 0000000000..a31fd3ee43 --- /dev/null +++ b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExchangeServerIPs.ps1 @@ -0,0 +1,79 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\Diagnostics\HealthChecker\DataCollection\ServerInformation\Get-AllNicInformation.ps1 + +# This function is used to get a list of all the IP in use by the Exchange Servers accross the topology +function Get-ExchangeServerIPs { + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$OutputFilePath, + [Parameter(Mandatory = $false)] + [object[]]$ExchangeServers + ) + + begin { + $IPs = New-Object 'System.Collections.Generic.List[string]' + $FailedServers = New-Object 'System.Collections.Generic.List[string]' + + $progressParams = @{ + Activity = "Getting List of IPs in use by Exchange Servers" + Status = [string]::Empty + PercentComplete = 0 + } + + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + } + process { + $counter = 0 + $totalCount = $ExchangeServers.Count + + foreach ($Server in $ExchangeServers) { + $baseStatus = "Processing: $($Server.Name) -" + $progressParams.PercentComplete = ($counter / $totalCount * 100) + $progressParams.Status = "$baseStatus Getting IPs" + Write-Progress @progressParams + + $IpsFound = $false + $HostNetworkInfo = Get-AllNicInformation -ComputerName $Server.Name -ComputerFQDN $Server.FQDN + if ($null -ne $HostNetworkInfo) { + if ($null -ne $HostNetworkInfo.IPv4Addresses) { + foreach ($address in $HostNetworkInfo.IPv4Addresses) { + $IPs += $address.Address + $IpsFound = $true + } + } + if ($null -ne $HostNetworkInfo.IPv6Addresses) { + foreach ($address in $HostNetworkInfo.IPv6Addresses) { + $IPs += $address.Address + $IpsFound = $true + } + } + } + + if (-not $IpsFound) { + $FailedServers += $Server.Name + Write-Verbose "IP of $($Server.Name) cannot be found and will not be added to IP allow list." + } + + $counter++ + } + + Write-Progress @progressParams -Completed + } + end { + if ($FailedServers -gt 0) { + Write-Host ("Unable to get IPs from the following servers: {0}" -f [string]::Join(", ", $FailedServers)) -ForegroundColor Red + } + + try { + $IPs | Out-File $OutputFilePath + Write-Host ("Please find the collected IPs at {0}" -f $OutputFilePath) + } catch { + Write-Host "Unable to write to file. Please check the path provided. Inner Exception:" -ForegroundColor Red + Write-HostErrorInformation $_ + } + } +} diff --git a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionConfiguration.ps1 b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionConfiguration.ps1 index 6e9e801ac3..25e122843d 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionConfiguration.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionConfiguration.ps1 @@ -10,16 +10,25 @@ function Get-ExtendedProtectionConfiguration { param( [Parameter(Mandatory = $true)] [string]$ComputerName, + [Parameter(Mandatory = $false)] [System.Xml.XmlNode]$ApplicationHostConfig, + [Parameter(Mandatory = $false)] [System.Version]$ExSetupVersion, + [Parameter(Mandatory = $false)] [bool]$IsMailboxServer = $true, + [Parameter(Mandatory = $false)] [bool]$IsClientAccessServer = $true, + [Parameter(Mandatory = $false)] [bool]$ExcludeEWS = $false, + + [Parameter(Mandatory = $false)] + [string[]]$SiteVDirLocations, + [Parameter(Mandatory = $false)] [scriptblock]$CatchActionFunction ) @@ -56,6 +65,17 @@ function Get-ExtendedProtectionConfiguration { # Set EWS Vdir to None for known issues if ($ExcludeEWS -and $virtualDirectory -eq "EWS") { $ExtendedProtection[$i] = "None" } + if ($null -ne $SiteVDirLocations -and + $SiteVDirLocations.Count -gt 0) { + foreach ($SiteVDirLocation in $SiteVDirLocations) { + if ($SiteVDirLocation -eq "$($WebSite[$i])/$virtualDirectory") { + Write-Verbose "Set Extended Protection to None because of restriction override '$($WebSite[$i])\$virtualDirectory'" + $ExtendedProtection[$i] = "None" + break; + } + } + } + [PSCustomObject]@{ VirtualDirectory = $virtualDirectory WebSite = $WebSite[$i] diff --git a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionPrerequisitesCheck.ps1 b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionPrerequisitesCheck.ps1 index f4ec0a8b30..12ed57eaa0 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionPrerequisitesCheck.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-ExtendedProtectionPrerequisitesCheck.ps1 @@ -10,6 +10,10 @@ function Get-ExtendedProtectionPrerequisitesCheck { param( [Parameter(Mandatory = $true)] [object[]]$ExchangeServers, + + [Parameter(Mandatory = $false)] + [string[]]$SiteVDirLocations, + [Parameter(Mandatory = $false)] [bool]$SkipEWS ) @@ -39,6 +43,7 @@ function Get-ExtendedProtectionPrerequisitesCheck { IsClientAccessServer = $server.IsClientAccessServer IsMailboxServer = $server.IsMailboxServer ExcludeEWS = $SkipEWS + SiteVDirLocations = $SiteVDirLocations } $extendedProtectionConfiguration = Get-ExtendedProtectionConfiguration @params diff --git a/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-IPRangeAllowListFromFile.ps1 b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-IPRangeAllowListFromFile.ps1 new file mode 100644 index 0000000000..5053fffad2 --- /dev/null +++ b/Security/src/ExchangeExtendedProtectionManagement/DataCollection/Get-IPRangeAllowListFromFile.ps1 @@ -0,0 +1,103 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\Shared\Write-ErrorInformation.ps1 + +# This function is used to get a list of all the IP in use by the Exchange Servers accross the topology +function Get-IPRangeAllowListFromFile { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + begin { + $results = @{ + ipRangeAllowListRules = New-Object 'System.Collections.Generic.List[object]' + IsError = $true + } + + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + } + process { + try { + $SubnetStrings = (Get-Content -Path $FilePath -ErrorAction Stop) | Where-Object { $_.trim() -ne "" } + } catch { + Write-Host "Unable to read the content of file provided for IPRange. Inner Exception" -ForegroundColor Red + Write-HostErrorInformation $_ + return + } + + if ($null -eq $SubnetStrings -or $SubnetStrings.Length -eq 0) { + Write-Host "The IP range file provided is empty. Please provide a valid file." -ForegroundColor Red + return + } else { + $ipRangesString = [string]::Join(", ", $SubnetStrings) + } + + # Log all the IPs present in the txt file supplied by user + Write-Verbose ("Read the contents of the file Successfully. List of IP ranges received from user: {0}" -f $ipRangesString) + + Write-Verbose "Validating the IP ranges specified in the file" + try { + foreach ($SubnetString in $SubnetStrings) { + $SubnetString = $SubnetString.Trim() + + $IpAddressString = $SubnetString.Split("/")[0] + $SubnetMaskString = $SubnetString.Split("/")[1] + + # Check the type of IP address (IPv4/IPv6) + $IpAddress = $IpAddressString -as [IPAddress] + $baseError = "Input file provided for IPRange doesn't have correct syntax of IPs or IP subnets." + if ($null -eq $IpAddress -or $null -eq $IpAddress.AddressFamily) { + # Invalid IP address found + Write-Host ("$baseError Re-execute the command with proper input file for IPRange parameter. Invalid IP address detected: {0}." -f $IpAddressString) -ForegroundColor Red + return + } + + $IsIPv6 = $IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 + + if ($SubnetMaskString) { + # Check if the subnet value is valid (IPv4 <= 32, IPv6 <= 128 or empty) + $SubnetMask = $SubnetMaskString -as [int] + + $InvalidSubnetMaskString = "$baseError Invalid Subnet Mask found: The Subnet Mask $SubnetMaskString is not in valid range.Note: Subnet Mask must be either empty or a non-negative integer. For IPv4 the value must be <= 32 and for IPv6 the value must be <= 128. Re-execute the command with proper input file for IPRange parameter." + if ($null -eq $SubnetMask) { + Write-Host ($InvalidSubnetMaskString) -ForegroundColor Red + return + } elseif (($SubnetMask -gt 32 -and -not $IsIPv6) -or $SubnetMask -gt 128 -or $SubnetMask -lt 0) { + Write-Host ($InvalidSubnetMaskString) -ForegroundColor Red + return + } + + if ($null -eq ($results.ipRangeAllowListRules | Where-Object { $_.Type -eq "Subnet" -and $_.IP -eq $IpAddressString -and $_.SubnetMask -eq $SubnetMaskString })) { + $results.ipRangeAllowListRules.Add(@{Type = "Subnet"; IP=$IpAddressString; SubnetMask=$SubnetMaskString; Allowed=$true }) + } else { + Write-Verbose ("Not adding $IpAddressString/$SubnetMaskString to the list as it is a duplicate entry in the file provided.") + } + } else { + if ($null -eq ($results.ipRangeAllowListRules | Where-Object { $_.Type -eq "Single IP" -and $_.IP -eq $IpAddressString })) { + $results.ipRangeAllowListRules.Add(@{Type = "Single IP"; IP=$IpAddressString; Allowed=$true }) + } else { + Write-Verbose ("Not adding $IpAddressString to the list as it is a duplicate entry in the file provided.") + } + } + } + + if ($results.ipRangeAllowListRules.count -gt 500) { + Write-Host ("Too many IP filtering rules. Please reduce the specified entries by providing appropriate subnets." -f $SubnetMaskString) -ForegroundColor Red + return + } + } catch { + Write-Host ("Unable to create IP allow rules. Inner Exception") -ForegroundColor Red + Write-HostErrorInformation $_ + return + } + + $results.IsError = $false + } + end { + return $results + } +} diff --git a/Security/src/ExchangeExtendedProtectionManagement/ExchangeExtendedProtectionManagement.ps1 b/Security/src/ExchangeExtendedProtectionManagement/ExchangeExtendedProtectionManagement.ps1 index ddb32edd4a..a4d337700c 100644 --- a/Security/src/ExchangeExtendedProtectionManagement/ExchangeExtendedProtectionManagement.ps1 +++ b/Security/src/ExchangeExtendedProtectionManagement/ExchangeExtendedProtectionManagement.ps1 @@ -25,24 +25,70 @@ This will set the applicationHost.config file back to the original state prior to changes made with this script. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( - [Parameter (Mandatory = $false, ValueFromPipeline, HelpMessage = "Enter the list of server names on which the script should execute on")] + [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")] [string[]]$ExchangeServerNames = $null, - [Parameter (Mandatory = $false, HelpMessage = "Enter the list of servers on which the script should not execute on")] + + [Parameter (Mandatory = $false, ParameterSetName = 'ConfigureMitigation', HelpMessage = "Enter the list of servers on which the script should not execute on")] + [Parameter (Mandatory = $false, ParameterSetName = 'ValidateMitigation', HelpMessage = "Enter the list of servers on which the script should not execute on")] + [Parameter (Mandatory = $false, ParameterSetName = 'Rollback', HelpMessage = "Using this parameter will allow you to rollback using the type you specified.")] + [Parameter (Mandatory = $false, ParameterSetName = 'ConfigureEP', HelpMessage = "Enter the list of servers on which the script should not execute on")] [string[]]$SkipExchangeServerNames = $null, - [Parameter (Mandatory = $false, HelpMessage = "Enable to provide a result of the configuration for Extended Protection")] + + [Parameter (Mandatory = $true, ParameterSetName = 'ShowEP', HelpMessage = "Enable to provide a result of the configuration for Extended Protection")] [switch]$ShowExtendedProtection, - [Parameter (Mandatory = $false, HelpMessage = "Used for internal options")] + + [Parameter (Mandatory = $false, ParameterSetName = 'ConfigureEP', HelpMessage = "Used for internal options")] [string]$InternalOption, - [Parameter (Mandatory = $false, ParameterSetName = 'Rollback', HelpMessage = "Using this parameter will allow you to rollback using the type you specified.")] - [ValidateSet("RestoreIISAppConfig")] - [string]$RollbackType + + [Parameter (Mandatory = $true, ParameterSetName = 'GetExchangeIPs', HelpMessage = "Using this parameter will allow you to get the list of IPs used by Exchange Servers.")] + [switch]$FindExchangeServerIPAddresses, + + [Parameter (Mandatory = $false, ParameterSetName = 'GetExchangeIPs', HelpMessage = "Using this parameter will allow you to specify the path to the output file.")] + [ValidateScript({ + (Test-Path -Path $_ -IsValid) -and ([string]::IsNullOrEmpty((Split-Path -Parent $_)) -or (Test-Path -Path (Split-Path -Parent $_))) + })] + [string]$OutputFilePath = [System.IO.Path]::Combine((Get-Location).Path, "IPList.txt"), + + [Parameter (Mandatory = $true, ParameterSetName = 'ConfigureMitigation', HelpMessage = "Using this parameter will allow you to specify a txt file with IP range that will be used to apply IP filters.")] + [Parameter (Mandatory = $true, ParameterSetName = 'ValidateMitigation', HelpMessage = "Using this parameter will allow you to specify a txt file with IP range that will be used to validate IP filters.")] + [ValidateScript({ + (Test-Path -Path $_) + })] + [string]$IPRangeFilePath, + + [Parameter (Mandatory = $true, ParameterSetName = 'ConfigureMitigation', HelpMessage = "Using this parameter will allow you to specify the site and vdir on which you want to configure mitigation.")] + [ValidateSet('EWSBackend')] + [ValidateScript({ + ($null -ne $_) -and ($_.Length -gt 0) + })] + [string[]]$RestrictType, + + [Parameter (Mandatory = $true, ParameterSetName = 'ValidateMitigation', HelpMessage = "Using this switch will allow you to validate if the mitigations have been applied correctly.")] + [ValidateSet('RestrictTypeEWSBackend')] + [ValidateScript({ + ($null -ne $_) -and ($_.Length -gt 0) + })] + [string[]]$ValidateType, + + [Parameter (Mandatory = $true, ParameterSetName = 'Rollback', HelpMessage = "Using this parameter will allow you to rollback using the type you specified.")] + [ValidateSet('RestrictTypeEWSBackend', 'RestoreIISAppConfig')] + [string[]]$RollbackType ) begin { . $PSScriptRoot\WriteFunctions.ps1 + . $PSScriptRoot\ConfigurationAction\Invoke-ConfigureMitigation.ps1 + . $PSScriptRoot\ConfigurationAction\Invoke-ValidateMitigation.ps1 + . $PSScriptRoot\ConfigurationAction\Invoke-RollbackIPFiltering.ps1 . $PSScriptRoot\ConfigurationAction\Invoke-ConfigureExtendedProtection.ps1 . $PSScriptRoot\ConfigurationAction\Invoke-RollbackExtendedProtection.ps1 + . $PSScriptRoot\DataCollection\Get-ExchangeServerIPs.ps1 + . $PSScriptRoot\DataCollection\Get-IPRangeAllowListFromFile.ps1 . $PSScriptRoot\DataCollection\Get-ExtendedProtectionPrerequisitesCheck.ps1 . $PSScriptRoot\DataCollection\Invoke-ExtendedProtectionTlsPrerequisitesCheck.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Host.ps1 @@ -55,11 +101,95 @@ begin { . $PSScriptRoot\..\..\..\Shared\LoggerFunctions.ps1 . $PSScriptRoot\..\..\..\Shared\Out-Columns.ps1 . $PSScriptRoot\..\..\..\Shared\Show-Disclaimer.ps1 + . $PSScriptRoot\..\..\..\Shared\Get-ExchangeBuildVersionInformation.ps1 + + # TODO: Move this so it isn't duplicated + # matching restrictions + $restrictionToSite = @{ + "APIFrontend" = "Default Web Site/API" + "AutodiscoverFrontend" = "Default Web Site/Autodiscover" + "ECPFrontend" = "Default Web Site/ECP" + "EWSFrontend" = "Default Web Site/EWS" + "Microsoft-Server-ActiveSyncFrontend" = "Default Web Site/Microsoft-Server-ActiveSync" + "OABFrontend" = "Default Web Site/OAB" + "PowershellFrontend" = "Default Web Site/Powershell" + "OWAFrontend" = "Default Web Site/OWA" + "RPCFrontend" = "Default Web Site/RPC" + "MAPIFrontend" = "Default Web Site/MAPI" + "APIBackend" = "Exchange Back End/API" + "AutodiscoverBackend" = "Exchange Back End/Autodiscover" + "ECPBackend" = "Exchange Back End/ECP" + "EWSBackend" = "Exchange Back End/EWS" + "Microsoft-Server-ActiveSyncBackend" = "Exchange Back End/Microsoft-Server-ActiveSync" + "OABBackend" = "Exchange Back End/OAB" + "PowershellBackend" = "Exchange Back End/Powershell" + "OWABackend" = "Exchange Back End/OWA" + "RPCBackend" = "Exchange Back End/RPC" + "PushNotificationsBackend" = "Exchange Back End/PushNotifications" + "RPCWithCertBackend" = "Exchange Back End/RPCWithCert" + "MAPI-emsmdbBackend" = "Exchange Back End/MAPI/emsmdb" + "MAPI-nspiBackend" = "Exchange Back End/MAPI/nspi" + } + + $Script:Logger = Get-NewLoggerInstance -LogName "ExchangeExtendedProtectionManagement-$((Get-Date).ToString("yyyyMMddhhmmss"))-Debug" ` + -AppendDateTimeToFileName $false ` + -ErrorAction SilentlyContinue + + SetWriteHostAction ${Function:Write-HostLog} + SetWriteVerboseAction ${Function:Write-VerboseLog} + SetWriteWarningAction ${Function:Write-HostLog} + SetWriteProgressAction ${Function:Write-HostLog} + + # The ParameterSetName options + $RollbackSelected = $PsCmdlet.ParameterSetName -eq "Rollback" + $RollbackRestoreIISAppConfig = $RollbackSelected -and $RollbackType.Contains("RestoreIISAppConfig") + $RollbackRestrictType = $RollbackSelected -and (-not $RollbackRestoreIISAppConfig) + $ConfigureMitigationSelected = $PsCmdlet.ParameterSetName -eq "ConfigureMitigation" + $ConfigureEPSelected = $ConfigureMitigationSelected -or + ($PsCmdlet.ParameterSetName -eq "ConfigureEP" -and -not $ShowExtendedProtection) + $ValidateTypeSelected = $PsCmdlet.ParameterSetName -eq "ValidateMitigation" + $includeExchangeServerNames = New-Object 'System.Collections.Generic.List[string]' - if ($PsCmdlet.ParameterSetName -eq "Rollback") { - $RollbackSelected = $true - if ($RollbackType -eq "RestoreIISAppConfig") { - $RollbackRestoreIISAppConfig = $true + + if ($RollbackRestoreIISAppConfig -and $RollbackType.Length -gt 1) { + Write-Host "RestoreIISAppConfig Rollback type can only be used individually" + exit + } + + if ($RollbackRestrictType) { + $RestrictType = $RollbackType.Replace("RestrictType", "") + } + + if ($ConfigureMitigationSelected) { + $RestrictType = $RestrictType | Get-Unique + } + + if ($ValidateTypeSelected) { + $RestrictType = New-Object 'System.Collections.Generic.List[string]' + $ValidateType | Get-Unique | ForEach-Object { $RestrictType += $_.Replace("RestrictType", "") } + } + + if (($ConfigureMitigationSelected -or $ValidateTypeSelected)) { + # Get list of IPs in object form from the file specified + $ipResults = Get-IPRangeAllowListFromFile -FilePath $IPRangeFilePath + if ($ipResults.IsError) { + exit + } + + $ipRangeAllowListRules = $ipResults.ipRangeAllowListRules + } + + if ($InternalOption -eq "SkipEWS") { + Write-Verbose "SkipEWS option enabled." + $Script:SkipEWS = $true + } else { + $Script:SkipEWS = $false + } + + if ($null -ne $RestrictType -and $RestrictType.Count -gt 0) { + $SiteVDirLocations = New-Object 'System.Collections.Generic.List[string]' + foreach ($key in $RestrictType) { + $SiteVDirLocations += $restrictionToSite[$key] } } } process { @@ -73,25 +203,7 @@ begin { } try { - - $Script:Logger = Get-NewLoggerInstance -LogName "ExchangeExtendedProtectionManagement-$((Get-Date).ToString("yyyyMMddhhmmss"))-Debug" ` - -AppendDateTimeToFileName $false ` - -ErrorAction SilentlyContinue - - SetWriteHostAction ${Function:Write-HostLog} - SetWriteVerboseAction ${Function:Write-VerboseLog} - SetWriteWarningAction ${Function:Write-HostLog} - SetWriteProgressAction ${Function:Write-HostLog} - - if ($InternalOption -eq "SkipEWS") { - Write-Verbose "SkipEWS option enabled." - $Script:SkipEWS = $true - } else { - $Script:SkipEWS = $false - } - - $exchangeShell = Confirm-ExchangeShell -Identity $env:COMPUTERNAME - if (-not($exchangeShell.ShellLoaded)) { + if (-not((Confirm-ExchangeShell -Identity $env:COMPUTERNAME).ShellLoaded)) { Write-Warning "Failed to load the Exchange Management Shell. Start the script using the Exchange Management Shell." exit } elseif (-not ($exchangeShell.EMS)) { @@ -104,17 +216,22 @@ begin { if ((Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/CEP-VersionsUrl")) { Write-Warning "Script was updated. Please rerun the command." - return + exit } - if (-not($RollbackSelected) -and - -not($ShowExtendedProtection)) { + if ($ConfigureEPSelected) { + + $ArchivingKnownIssueString = "`r`n - Automated Archiving using Archive policy" + if ($ConfigureMitigationSelected) { + $ArchivingKnownIssueString = "" + } + $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 - Automated Archiving using Archive policy" + + $ArchivingKnownIssueString + "`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: https://aka.ms/ExchangeEPDoc. Do you want to proceed?" @@ -127,6 +244,16 @@ begin { Write-Verbose ("Running Get-ExchangeServer to get list of all exchange servers") Set-ADServerSettings -ViewEntireForest $true $ExchangeServers = Get-ExchangeServer | 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: https://aka.ms/ExchangeEPDoc.") + return + } + $ExchangeServersPrerequisitesCheckSettingsCheck = $ExchangeServers if ($null -ne $includeExchangeServerNames -and $includeExchangeServerNames.Count -gt 0) { @@ -141,6 +268,17 @@ begin { $ExchangeServers = $ExchangeServers | Where-Object { ($_.Name -notin $SkipExchangeServerNames) -and ($_.FQDN -notin $SkipExchangeServerNames) } } + if ($null -eq $ExchangeServers) { + Write-Host "No exchange servers to process. Please specify server filters correctly" + exit + } + + if ($ValidateTypeSelected) { + # Validate mitigation + $ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Major -eq 15 -and (Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Minor -eq 0 -and $_.IsClientAccessServer) } + Invoke-ValidateMitigation -ExchangeServers $ExchangeServers.Name -ipRangeAllowListRules $ipRangeAllowListRules -SiteVDirLocations $SiteVDirLocations + } + if ($ShowExtendedProtection) { Write-Verbose "Showing Extended Protection Information Only" $extendedProtectionConfigurations = New-Object 'System.Collections.Generic.List[object]' @@ -179,11 +317,10 @@ begin { return } - if (-not($RollbackSelected)) { - $prerequisitesCheck = Get-ExtendedProtectionPrerequisitesCheck -ExchangeServers $ExchangeServersPrerequisitesCheckSettingsCheck -SkipEWS $SkipEWS + if ($ConfigureEPSelected) { + $prerequisitesCheck = Get-ExtendedProtectionPrerequisitesCheck -ExchangeServers $ExchangeServersPrerequisitesCheckSettingsCheck -SkipEWS $SkipEWS -SiteVDirLocations $SiteVDirLocations if ($null -ne $prerequisitesCheck) { - Write-Host "" # Remove the down servers from $ExchangeServers list. $downServerName = New-Object 'System.Collections.Generic.List[string]' @@ -419,26 +556,38 @@ begin { Write-Warning "Failed to get Extended Protection Prerequisites Information to be able to continue" exit } - } else { + + # Configure Extended Protection based on given parameters + # Prior to executing, add back any unsupported versions back into the list + # for onlineSupportedServers, because the are online and we want to revert them. + $unsupportedAndConfiguredServers | ForEach-Object { $onlineSupportedServers.Add($_) } + $extendedProtectionConfigurations = ($onlineSupportedServers | + Where-Object { $_.ComputerName -in $serverNames }).ExtendedProtectionConfiguration + + if ($null -ne $extendedProtectionConfigurations) { + Invoke-ConfigureExtendedProtection -ExtendedProtectionConfigurations $extendedProtectionConfigurations + } else { + Write-Host "No servers are online or no Exchange Servers Support Extended Protection." + } + + if ($ConfigureMitigationSelected) { + # Apply rules + $ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Major -eq 15 -and (Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Minor -eq 0 -and $_.IsClientAccessServer) } + Invoke-ConfigureMitigation -ExchangeServers $ExchangeServers.Name -ipRangeAllowListRules $ipRangeAllowListRules -SiteVDirLocations $SiteVDirLocations + } + } elseif ($RollbackSelected) { Write-Host "Prerequisite check will be skipped due to Rollback" if ($RollbackRestoreIISAppConfig) { Invoke-RollbackExtendedProtection -ExchangeServers $ExchangeServers } - return - } - # Configure Extended Protection based on given parameters - # Prior to executing, add back any unsupported versions back into the list - # for onlineSupportedServers, because the are online and we want to revert them. - $unsupportedAndConfiguredServers | ForEach-Object { $onlineSupportedServers.Add($_) } - $extendedProtectionConfigurations = ($onlineSupportedServers | - Where-Object { $_.ComputerName -in $serverNames }).ExtendedProtectionConfiguration - - if ($null -ne $extendedProtectionConfigurations) { - Invoke-ConfigureExtendedProtection -ExtendedProtectionConfigurations $extendedProtectionConfigurations - } else { - Write-Host "No servers are online or no Exchange Servers Support Extended Protection." + if ($RollbackRestrictType) { + $ExchangeServers = $ExchangeServers | Where-Object { -not ((Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Major -eq 15 -and (Get-ExchangeBuildVersionInformation -AdminDisplayVersion $_.AdminDisplayVersion).Minor -eq 0 -and $_.IsClientAccessServer) } + Invoke-RollbackIPFiltering -ExchangeServers $ExchangeServers -SiteVDirLocations $SiteVDirLocations + } + + return } } finally { Write-Host "Do you have feedback regarding the script? Please email ExToolsFeedback@microsoft.com."