Remediation/ExchangeOnline/EXO-CONNECTORS.ps1
|
# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk. <# .SYNOPSIS Has the flow of the migrated domains decided and adjusts the connectors of the target accordingly (procedure EXO-CONNECTORS). .DESCRIPTION The mail flow of the target is in production: nothing is written without a decision. A mail gateway may exist in both tenants, with its own connectors. Data read: connectors of the source and the target, dnsDomains table (MX, SPF, inbound flow). Decisions: decisions\EXO-CONNECTORS-FLOW.csv, one row per migrated domain: Direct or Gateway. decisions\EXO-CONNECTORS.csv, one row per connector of the source: Covered : the target already has the equivalent, nothing to create (proposed when the name or the IP addresses match); Create : recreated in the target with the prefix of the source; Drop : nothing to do. Security choices: -RestrictDomainsToIPAddresses is not passed. With -SenderDomains '*', it would reject any email that does not come from these IP addresses, while the MX of the target arrives directly on Exchange Online. Locking only the migrated domains, with -AssociatedAcceptedDomains, needs them to be in the target already: it is a separate step, after the cutover. An outbound connector with no domain is created with -IsTransportRuleScoped, otherwise it would touch all outbound mail. -EFSkipLastIP is turned on only if a domain goes through the gateway. Guards: refuses an inbound connector whose IP addresses are already covered in the target, and an outbound connector to *. Limits: the cutover publishes the Microsoft records; a domain on Gateway needs them adapted by hand. #> $c = Open-TcRemediation function Get-DecisionList { param([string]$Text) return @($Text -split ';\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } function Get-NormalizedName { param([string]$Name) return ($Name -replace '\s+', '').ToLowerInvariant() } function Get-ConnectorDomain { <# smtp:*;1 in the snapshot: keeps *. #> param([string]$Text) if ($Text -match '^smtp:([^;]+);\d+$') { return @($Matches[1]) } return @(Get-DecisionList $Text) } $TargetConnectors = @(Get-TcRemediationTable 'Target' 'connectors') function Find-EquivalentConnector { <# Connector of the target in the same direction, by name or by shared IP addresses. #> param($Connector) $hosts = @(Get-DecisionList $Connector['Hosts or IPs']) foreach ($x in $TargetConnectors | Where-Object { $_['Direction'] -eq $Connector['Direction'] }) { if ((Get-NormalizedName $x['Name']) -eq (Get-NormalizedName $Connector['Name'])) { return $x } if ($Connector['Direction'] -eq 'Inbound' -and @(Get-DecisionList $x['Hosts or IPs'] | Where-Object { $_ -in $hosts }).Count) { return $x } } return $null } $flow = foreach ($t in $c.Tenants) { foreach ($d in (Get-TcMember (Get-TcRemediationTenant $t) 'Domains')) { $l = Get-TcRemediationTable $t 'dnsSummary' | Where-Object { $_['Domain'] -eq $d } | Select-Object -First 1 [ordered]@{ Tenant = $t; Domain = $d; MX = $(if ($l) { $l['MX'] }); 'Inbound mail flow' = $(if ($l) { $l['Inbound mail flow'] }); SPF = $(if ($l) { $l['SPF'] }); Proposal = $(if ($l -and $l['Inbound mail flow'] -eq 'Exchange Online direct') { 'Direct' } else { '' }) } } } $flowDecisions = @(Get-TcDecision -Name 'EXO-CONNECTORS-FLOW' -Proposal @($flow) -Key 'Tenant', 'Domain' -Choice 'Direct', 'Gateway') $proposals = foreach ($t in $c.Tenants) { Write-TcRemediationTenant $t $list = @(Get-TcRemediationTable $t 'connectors') if (-not $list.Count -and -not (Test-TcCollectionComplete $t 'connectors')) { Write-TcRemediationAlert 'Incomplete connector collection: run Invoke-TcAssessment again'; continue } Write-TcRemediationNote ("{0} connector(s) in the source, {1} in the target" -f $list.Count, $TargetConnectors.Count) foreach ($x in $list) { $e = Find-EquivalentConnector $x [ordered]@{ Tenant = $t; Connector = $x['Name']; Direction = $x['Direction']; Type = $x['Type']; Domains = (@(Get-ConnectorDomain $x['Domains']) -join ', '); 'Hosts or IPs' = $x['Hosts or IPs']; TLS = $x['TLS'] 'Target equivalent' = $(if ($e) { $e['Name'] }); Proposal = $(if ($e) { 'Covered' } else { '' }) } } } $decisions = @(Get-TcDecision -Name 'EXO-CONNECTORS' -Proposal @($proposals) -Key 'Tenant', 'Connector' -Choice 'Create', 'Covered', 'Drop') $Created = [System.Collections.Generic.List[object]]::new() foreach ($d in $decisions | Where-Object { (Get-TcMember $_ 'Tenant') -in $c.Tenants -and (Get-TcMember $_ 'Decision') -eq 'Create' }) { $t = (Get-TcMember $d 'Tenant') $x = Get-TcRemediationTable $t 'connectors' | Where-Object { $_['Name'] -eq (Get-TcMember $d 'Connector') } | Select-Object -First 1 if (-not $x) { Write-TcRemediationAlert ("{0}: connector missing from the snapshot" -f (Get-TcMember $d 'Connector')); continue } $name = '{0}-{1}' -f (Get-TcMember (Get-TcRemediationTenant $t) 'Prefix'), $x['Name'] $hosts = @(Get-DecisionList $x['Hosts or IPs']) $domains = @(Get-ConnectorDomain $x['Domains']) if ($x['Direction'] -eq 'Inbound') { $e = Find-EquivalentConnector $x $commonItems = if ($e) { @(Get-DecisionList $e['Hosts or IPs'] | Where-Object { $_ -in $hosts }) } else { @() } if ((Get-TcCount $commonItems)) { Write-TcRemediationAlert ("{0}: {1} already covers {2}; two connectors on the same IP addresses are not created here" -f $x['Name'], $e['Name'], ($commonItems -join ', ')); continue } $p = @{ Name = $name; ConnectorType = $x['Type']; SenderDomains = $domains; SenderIPAddresses = $hosts; RequireTls = (ConvertTo-TcBoolean $x['TLS']) } if (@($flowDecisions | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t -and (Get-TcMember $_ 'Decision') -eq 'Gateway' }).Count) { $p.EFSkipLastIP = $true } Add-TcCommandAction -Tenant 'Target' -Service Exo -Command 'New-InboundConnector' -Parameters $p -Object $name -Description ("Create the inbound connector {0}, restricted to {1} IP address(es)" -f $name, $hosts.Count) ` -Test { param($a) [bool](Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-InboundConnector' | Where-Object Name -eq $a.parameters.Name) } ` -Undo @{ command = 'Remove-InboundConnector'; parameters = @{ Identity = $name } } } else { if ('*' -in $domains) { Write-TcRemediationAlert ("{0}: an outbound connector to * would divert all the email of the target, creation refused" -f $x['Name']); continue } $p = @{ Name = $name; ConnectorType = $x['Type']; UseMXRecord = $false; SmartHosts = $hosts } if ($x['TLS'] -in 'EncryptionOnly', 'CertificateValidation', 'DomainValidation') { $p.TlsSettings = $x['TLS'] } if ($domains.Count) { $p.RecipientDomains = $domains } else { $p.IsTransportRuleScoped = $true } Add-TcCommandAction -Tenant 'Target' -Service Exo -Command 'New-OutboundConnector' -Parameters $p -Object $name -Description ("Create the outbound connector {0} to {1}" -f $name, ($hosts -join ', ')) ` -Test { param($a) [bool](Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-OutboundConnector' | Where-Object Name -eq $a.parameters.Name) } ` -Undo @{ command = 'Remove-OutboundConnector'; parameters = @{ Identity = $name } } } $Created.Add(@{ Name = $name; Direction = $x['Direction']; Tenant = $t }) } foreach ($t in $c.Tenants) { $choice = @($flowDecisions | Where-Object Tenant -eq $t) if (-not $choice.Count) { continue } $gateway = @($choice | Where-Object 'Decision' -eq 'Gateway' | ForEach-Object { (Get-TcMember $_ 'Domain') }) $instruction = @(("Record the decided flow in the DNS manifest entry: {0}." -f (@($choice | ForEach-Object { '{0} {1}' -f (Get-TcMember $_ 'Domain'), (Get-TcMember $_ 'Decision') }) -join ', '))) if ($gateway.Count) { $instruction += ("When the MX, SPF and Autodiscover records move, the MX published is the Microsoft one: for {0}, adapt the MX and the SPF to the gateway before the cutover." -f ($gateway -join ', ')) } else { $instruction += "Direct flow: remove the gateway from the SPF if it no longer sends for these domains." } Add-TcManualAction -Tenant 'Target' -Object "DNS sheet $t" -Description ("Carry the flow of {0} into the DNS manifest entry (when the MX, SPF and Autodiscover records move)" -f $t) -Instruction $instruction } if ($Created.Count) { Add-TcManualAction -Tenant 'Target' -Object 'Connector test' -Description ("Test the {0} connector(s) created before the MX, SPF and Autodiscover records move" -f $Created.Count) ` -Instruction @("Validate the connectors in the Exchange admin center > Mail flow > Connectors.", 'Check that the mail flow of the current domains of the target does not change: otherwise a failure is only discovered during the cutover.') } Invoke-TcRemediationPlan -Verify { param($t) $inbound = @(Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-InboundConnector') $outbound = @(Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-OutboundConnector') foreach ($x in $Created | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t }) { $vu = if ((Get-TcMember $x 'Direction') -eq 'Inbound') { $inbound | Where-Object Name -eq (Get-TcMember $x 'Name') } else { $outbound | Where-Object Name -eq (Get-TcMember $x 'Name') } if ($vu) { New-TcRemediationResult OK ("{0} present, active: {1}" -f (Get-TcMember $x 'Name'), (Get-TcMember $vu 'Enabled')) } else { New-TcRemediationResult Blocking ("{0} missing from the target" -f (Get-TcMember $x 'Name')) } } $restricted = @($inbound | Where-Object { (Get-TcMember $_ 'RestrictDomainsToIPAddresses') -and '*' -in @((Get-TcMember $_ 'SenderDomains') | ForEach-Object { ([string]$_) -replace '^smtp:([^;]+).*$', '$1' }) }) if ($restricted.Count) { New-TcRemediationResult Warning ("Connector(s) that reject any sender outside their IP addresses: {0}. To review with the direct MX of the target." -f (@((Get-TcMember $restricted 'Name')) -join ', ')) } } Close-TcRemediation |