Remediation/ExchangeOnline/EXO-RULES.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 Recreates in the target, in audit mode and limited to the migrated domains, the chosen transport rules (procedure EXO-RULES). .DESCRIPTION Data read: transport rules of the source and the target. The script first exports the rules of the source to XML to keep a reference. Import-TransportRuleCollection is never used: it would replace all the rules of the target. Decision: decisions\EXO-RULES.csv, one row per rule. Audit-Inbound : recreated in audit, limited to messages to the migrated domains (RecipientDomainIs); Audit-Outbound : recreated in audit, limited to messages from the migrated domains (SenderDomainIs); Enforce : moves a rule already created to enforcement; Covered : the target already has this rule (proposed when the name is the same); Drop : nothing to do (proposed for disabled rules and signatures, carried over by the signature tool). Two steps: Audit-* before the cutover. After reading the results, change the decision to Enforce and run again; moving to enforcement is postponed while the domains of the source are not in the target. Copying a rule: the conditions and actions are read in the source at run time. Only those of the list $Copiables are carried over. A rule that aims at a group, a connector or a recipient of the source cannot be moved: the action fails and says so, and the rule is recreated by hand. This copy has never run on a tenant. Limit: the priority order stays to be reviewed after the addition, the check lists the rules in order. #> $c = Open-TcRemediation $Witnesses = @{} $Metadata = @('Name', 'Identity', 'Priority', 'Mode', 'Enabled', 'Comments', 'ActivationDate', 'ExpiryDate', 'DlpPolicy', 'Confirm', 'WhatIf', 'UseLegacyRegex') $Kept = @('RuleErrorAction', 'SenderAddressLocation', 'StopRuleProcessing') $Conditions = @('FromScope', 'SentToScope', 'SenderDomainIs', 'RecipientDomainIs', 'FromAddressContainsWords', 'FromAddressMatchesPatterns', 'SenderIpRanges', 'RecipientAddressContainsWords', 'RecipientAddressMatchesPatterns', 'SubjectContainsWords', 'SubjectMatchesPatterns', 'SubjectOrBodyContainsWords', 'SubjectOrBodyMatchesPatterns', 'HeaderContainsMessageHeader', 'HeaderContainsWords', 'HeaderMatchesMessageHeader', 'HeaderMatchesPatterns', 'AttachmentExtensionMatchesWords', 'AttachmentNameMatchesPatterns', 'AttachmentSizeOver', 'MessageSizeOver', 'MessageTypeMatches', 'SCLOver', 'WithImportance') $Actions = @('SetSCL', 'SetHeaderName', 'SetHeaderValue', 'RemoveHeader', 'PrependSubject', 'ApplyHtmlDisclaimerText', 'ApplyHtmlDisclaimerLocation', 'ApplyHtmlDisclaimerFallbackAction', 'RejectMessageReasonText', 'RejectMessageEnhancedStatusCode', 'DeleteMessage', 'Quarantine') $Copiable = @($Kept + $Conditions + $Actions + @($Conditions | ForEach-Object { "ExceptIf$_" })) function Get-TargetRule { param([string]$Name) return (Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-TransportRule' | Where-Object Name -eq $Name | Select-Object -First 1) } function ConvertTo-RuleParameter { <# Parameters of New-TransportRule from a rule read; refuses what aims at an object specific to the source. #> param($Rule) $knownParameters = Get-TcCommandParameter -Command 'New-TransportRule' $p = @{} $refused = [System.Collections.Generic.List[string]]::new() foreach ($x in $Rule.PSObject.Properties) { if (-not $knownParameters.ContainsKey((Get-TcMember $x 'Name')) -or (Get-TcMember $x 'Name') -in $Metadata) { continue } $v = (Get-TcMember $x 'Value') if ($null -eq $v -or ($v -is [bool] -and -not $v) -or ([string]$v) -eq '' -or ($v -is [System.Collections.ICollection] -and -not (Get-TcCount $v))) { continue } if ((Get-TcMember $x 'Name') -in $Copiable) { $p[$x.Name] = $v } else { $refused.Add((Get-TcMember $x 'Name')) } } if ($refused.Count) { throw ("Rule that cannot be carried over, to recreate by hand: {0}" -f ($refused -join ', ')) } return $p } $TargetNames = @(Get-TcRemediationTable 'Target' 'transportRules' | ForEach-Object { $_['Name'] }) function Get-TargetName { param([string]$Prefix, [string]$Name) $n = '{0}-{1}' -f $Prefix, $Name if ((Get-TcMember $n 'Length') -gt 64) { return $n.Substring(0, 64) } return $n } $proposals = foreach ($t in $c.Tenants) { Write-TcRemediationTenant $t $rules = @(Get-TcRemediationTable $t 'transportRules') if (-not $rules.Count -and -not (Test-TcCollectionComplete $t 'transportRules')) { Write-TcRemediationAlert 'Incomplete rule collection: run Invoke-TcAssessment again'; continue } Write-TcRemediationNote ("{0} rule(s), of which {1} active" -f $rules.Count, @($rules | Where-Object { $_['State'] -eq 'Enabled' }).Count) Add-TcBlockAction -Tenant $t -Service Exo -Object 'Rules export' -Description ("Export the rules of {0} as XML, as a reference" -f $t) -Data @{ Tenant = $t } -Reason 'Read only: the file stays in the log folder.' ` -Block { param($a) $f = Invoke-TcServiceCommand -Tenant $a.tenant -Service Exo -Command 'Export-TransportRuleCollection' if (-not (Get-TcMember $f 'FileData')) { throw 'Export-TransportRuleCollection returned no content' } $outputFolder = Join-Path $script:TcRemediationRun['Folder'] 'exports' New-Item -ItemType Directory -Force -Path $outputFolder | Out-Null [System.IO.File]::WriteAllBytes((Join-Path $outputFolder ("rules-{0}.xml" -f $a.tenant)), [byte[]](Get-TcMember $f 'FileData')) } foreach ($r in $rules) { $text = ([string]$r['Description']) -replace '\s+', ' ' $proposal = if ($r['Name'] -in $TargetNames) { 'Covered' } elseif ($r['State'] -ne 'Enabled' -or $text -match 'signature|disclaimer') { 'Drop' } else { '' } [ordered]@{ Tenant = $t; 'Rule' = $r['Name']; 'State' = $r['State']; 'Priority' = $r['Priority']; Mode = $r['Mode']; Description = $text; Proposal = $proposal } } } $decisions = @(Get-TcDecision -Name 'EXO-RULES' -Proposal @($proposals) -Key 'Tenant', 'Rule' -Choice 'Audit-Inbound', 'Audit-Outbound', 'Enforce', 'Covered', 'Drop') foreach ($d in $decisions | Where-Object { (Get-TcMember $_ 'Tenant') -in $c.Tenants -and (Get-TcMember $_ 'Decision') -in 'Audit-Inbound', 'Audit-Outbound', 'Enforce' }) { $t = (Get-TcMember $d 'Tenant') $i = Get-TcRemediationTenant $t $name = Get-TargetName (Get-TcMember $i 'Prefix') (Get-TcMember $d 'Rule') if ((Get-TcMember $d 'Decision') -eq 'Enforce') { $Witnesses[$name] = (@((Get-TcMember $i 'Domains')) | Select-Object -First 1) Add-TcCommandAction -Tenant 'Target' -Service Exo -Command 'Set-TransportRule' -Parameters @{ Identity = $name; Mode = 'Enforce' } -Object $name -Description ("Switch {0} to enforcement" -f $name) ` -Test { param($a) $witness = $Witnesses[$a.parameters.Identity] if (-not (Test-TcTargetDomain $witness)) { return Test-TcPostponed $a ("{0} is not accepted in the target: run again after the cutover" -f $witness) } $r = Get-TargetRule $a.parameters.Identity if (-not $r) { return Test-TcPostponed $a ("rule {0} missing from the target: create it first with Audit-Inbound or Audit-Outbound" -f $a.parameters.Identity) } $a.undo = @{ command = 'Set-TransportRule'; parameters = @{ Identity = $a.parameters.Identity; Mode = (Get-TcText $r 'Mode') } } return (Get-TcText $r 'Mode') -eq 'Enforce' } continue } $scope = if ((Get-TcMember $d 'Decision') -eq 'Audit-Inbound') { 'RecipientDomainIs' } else { 'SenderDomainIs' } Add-TcBlockAction -Tenant 'Target' -Service Exo -Object $name -Data @{ Source = $t; Rule = (Get-TcMember $d 'Rule'); Name = $name; Scope = $scope; Domains = @((Get-TcMember $i 'Domains')) } ` -Description ("Recreate {0} in audit mode, limited to {1}" -f $name, (@((Get-TcMember $i 'Domains')) -join ', ')) ` -Test { param($a) [bool](Get-TargetRule $a.data.Name) } ` -Undo @{ command = 'Remove-TransportRule'; parameters = @{ Identity = $name } } ` -Block { param($a) $x = $a.data $r = Invoke-TcServiceCommand -Tenant (Get-TcMember $x 'Source') -Service Exo -Command 'Get-TransportRule' -Parameters @{ Identity = (Get-TcMember $x 'Rule') } $p = ConvertTo-RuleParameter $r $p.Name = (Get-TcMember $x 'Name') $p.Mode = 'Audit' $p[$x.Scope] = @((Get-TcMember $x 'Domains')) $p.Comments = "Carry over rule {0} from {1}" -f (Get-TcMember $x 'Rule'), (Get-TcMember $x 'Source') Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'New-TransportRule' -Parameters $p | Out-Null } } Invoke-TcRemediationPlan -Verify { param($t) $i = Get-TcRemediationTenant $t $rules = @(Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-TransportRule') foreach ($d in $decisions | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t -and (Get-TcMember $_ 'Decision') -in 'Audit-Inbound', 'Audit-Outbound', 'Enforce' }) { $name = Get-TargetName (Get-TcMember $i 'Prefix') (Get-TcMember $d 'Rule') $r = $rules | Where-Object Name -eq $name if (-not $r) { New-TcRemediationResult Blocking ("{0} missing from the target" -f $name); continue } $scope = @((Get-TcMember $r 'SenderDomainIs')) + @((Get-TcMember $r 'RecipientDomainIs')) | Where-Object { $_ } if (-not @($scope).Count) { New-TcRemediationResult Blocking ("{0} without a domain scope: it can affect all the email of the target" -f $name) } else { New-TcRemediationResult OK ("{0}: mode {1}, priority {2}" -f $name, (Get-TcMember $r 'Mode'), (Get-TcMember $r 'Priority')) } } } Close-TcRemediation |