Skip to content

Commit eceae65

Browse files
authored
Merge pull request #2 from thetechgy/develop
Promote New-EXOExternalDisclaimerTransportRule.ps1
2 parents 453e11f + aab0497 commit eceae65

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#Requires -Version 5.1
2+
3+
<#
4+
.SYNOPSIS
5+
Create or update Exchange Online transport rule for external email disclaimers
6+
.DESCRIPTION
7+
Creates or updates a transport rule to prepend security banners to external emails.
8+
Based on methodology from ArchiTech Labs: https://www.architechlabs.io
9+
10+
Blog post: https://www.architechlabs.io/articles/external-email-banner/
11+
.PARAMETER OrgPrefix
12+
Your organization prefix/name for the header (2-50 characters). Spaces will be removed.
13+
Examples: 'Contoso Corp' becomes 'X-ContosoCorp-Disclaimer-External'
14+
'ACME Industries' becomes 'X-ACMEIndustries-Disclaimer-External'
15+
'CONTOSO' becomes 'X-CONTOSO-Disclaimer-External'
16+
.PARAMETER Priority
17+
Transport rule priority (0 = highest). Default: 0
18+
.PARAMETER RuleName
19+
Transport rule name. Default: "Security – Inbound External – Prepend Disclaimer"
20+
.PARAMETER Disabled
21+
Create the rule in disabled state (safer for testing)
22+
.EXAMPLE
23+
.\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "Contoso Corp"
24+
.EXAMPLE
25+
.\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "ACME" -Priority 2 -WhatIf
26+
.EXAMPLE
27+
.\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "MyOrg" -Disabled
28+
.NOTES
29+
Requires Exchange Online PowerShell connection (Connect-ExchangeOnline)
30+
31+
Author: Travis McDade
32+
Organization: ArchiTech Labs
33+
Website: https://www.architechlabs.io
34+
Version: 1.1.0
35+
36+
Based on security methodology developed by ArchiTech Labs.
37+
38+
.LINK
39+
https://www.architechlabs.io/articles/external-email-banner/
40+
41+
.LINK
42+
https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules
43+
44+
.LINK
45+
https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/conditions-and-exceptions
46+
#>
47+
48+
[CmdletBinding(SupportsShouldProcess)]
49+
param(
50+
[Parameter(Mandatory = $true, HelpMessage = "Your organization prefix/name for the header (2-50 characters, letters/numbers/spaces only)")]
51+
[ValidateLength(2, 50)]
52+
[ValidatePattern('^[A-Za-z0-9\s]+$')]
53+
[ValidateScript({
54+
$cleaned = $_ -replace '\s+', '' -replace '[^A-Za-z0-9]', ''
55+
if ($cleaned.Length -lt 2 -or $cleaned.Length -gt 50) {
56+
throw "Organization prefix results in '$cleaned' which must be 2-50 characters after cleanup."
57+
}
58+
$true
59+
})]
60+
[string]$OrgPrefix,
61+
62+
[Parameter(HelpMessage = "Transport rule priority (0 = highest priority)")]
63+
[ValidateRange(0, 100)]
64+
[int]$Priority = 0,
65+
66+
[Parameter(HelpMessage = "Transport rule name")]
67+
[ValidateNotNullOrEmpty()]
68+
[string]$RuleName = "Security – Inbound External – Prepend Disclaimer",
69+
70+
[Parameter(HelpMessage = "Create the rule in disabled state (safer for testing)")]
71+
[switch]$Disabled
72+
)
73+
74+
# Convert organization prefix to header-safe format
75+
$HeaderSafePrefix = $OrgPrefix -replace '\s+', '' -replace '[^A-Za-z0-9]', ''
76+
$HeaderName = "X-$HeaderSafePrefix-Disclaimer-External"
77+
$HeaderValue = "Applied"
78+
79+
#region Functions
80+
81+
function Install-RequiredModules {
82+
[CmdletBinding()]
83+
param([string[]]$ModuleNames)
84+
85+
foreach ($Module in $ModuleNames) {
86+
if (-not (Get-Module -ListAvailable -Name $Module)) {
87+
Write-Information "Installing required module: $Module" -InformationAction Continue
88+
try {
89+
Install-Module -Name $Module -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop
90+
Write-Information "Successfully installed $Module" -InformationAction Continue
91+
}
92+
catch {
93+
throw "Failed to install required module '$Module': $($_.Exception.Message). Please run 'Install-Module -Name $Module' manually or ensure you have appropriate permissions."
94+
}
95+
}
96+
}
97+
}
98+
99+
function Test-ExchangeOnlineConnection {
100+
[CmdletBinding()]
101+
param()
102+
103+
try {
104+
Get-OrganizationConfig -ErrorAction Stop | Out-Null
105+
Write-Verbose "Connected to Exchange Online"
106+
}
107+
catch {
108+
Write-Information "Not connected to Exchange Online. Attempting to connect..." -InformationAction Continue
109+
try {
110+
Connect-ExchangeOnline -ShowProgress:$false -ErrorAction Stop
111+
Write-Information "Successfully connected to Exchange Online" -InformationAction Continue
112+
}
113+
catch {
114+
throw "Failed to connect to Exchange Online: $($_.Exception.Message). Please ensure you have the necessary permissions and network connectivity."
115+
}
116+
}
117+
}
118+
119+
function Set-ExternalDisclaimerRule {
120+
[CmdletBinding()]
121+
param(
122+
[string]$RuleName,
123+
[int]$Priority,
124+
[string]$HeaderName,
125+
[string]$HeaderValue,
126+
[string]$BannerHtml,
127+
[switch]$Disabled
128+
)
129+
130+
$existing = Get-TransportRule -Identity $RuleName -ErrorAction SilentlyContinue
131+
132+
$ruleParams = @{
133+
Comments = "External email disclaimer per ArchiTech Labs methodology (https://www.architechlabs.io). Prevents duplicates via header stamp. Blog: https://www.architechlabs.io/articles/external-email-banner/"
134+
Priority = $Priority
135+
FromScope = 'NotInOrganization'
136+
SentToScope = 'InOrganization'
137+
ApplyHtmlDisclaimerLocation = 'Prepend'
138+
ApplyHtmlDisclaimerText = $BannerHtml
139+
ApplyHtmlDisclaimerFallbackAction = 'Wrap'
140+
SetHeaderName = $HeaderName
141+
SetHeaderValue = $HeaderValue
142+
ExceptIfHeaderMatchesMessageHeader = $HeaderName
143+
ExceptIfHeaderMatchesPatterns = $HeaderValue
144+
Enabled = -not $Disabled
145+
}
146+
147+
$action = if (-not $existing) { "Creating" } else { "Updating" }
148+
Write-Information "$action transport rule: $RuleName" -InformationAction Continue
149+
150+
if (-not $existing) {
151+
New-TransportRule -Name $RuleName @ruleParams -ErrorAction Stop
152+
}
153+
else {
154+
Set-TransportRule -Identity $RuleName @ruleParams -ErrorAction Stop
155+
}
156+
157+
$stateMsg = if ($Disabled) { " (DISABLED)" } else { " (ENABLED)" }
158+
Write-Information "Successfully $($action.ToLower()) transport rule: $RuleName$stateMsg" -InformationAction Continue
159+
}
160+
161+
#endregion Functions
162+
163+
# Install required modules and verify connection
164+
Install-RequiredModules -ModuleNames @('ExchangeOnlineManagement')
165+
Test-ExchangeOnlineConnection
166+
167+
# Display configuration
168+
Write-Information "MODE: Deploy/Update Rule" -InformationAction Continue
169+
Write-Information "Organization: $OrgPrefix" -InformationAction Continue
170+
Write-Information "Header Name: $HeaderName" -InformationAction Continue
171+
Write-Information "Rule Name: $RuleName" -InformationAction Continue
172+
Write-Information "Priority: $Priority" -InformationAction Continue
173+
Write-Information "Rule State: $(if ($Disabled) { 'Disabled' } else { 'Enabled' })" -InformationAction Continue
174+
Write-Information "Duplicate Prevention: Enabled (via $HeaderName header)" -InformationAction Continue
175+
176+
if ($Disabled) {
177+
Write-Warning "Rule will be created in DISABLED state for safe testing"
178+
}
179+
180+
# Banner HTML content (using single-quoted here-string to prevent variable expansion)
181+
$BannerHtml = @'
182+
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="mso-table-lspace:0;mso-table-rspace:0;">
183+
<tr>
184+
<td align="left" style="mso-table-lspace:0;mso-table-rspace:0;">
185+
<table role="presentation" border="0" cellspacing="0" cellpadding="0" width="760" style="width:100%;max-width:760px;mso-table-lspace:0;mso-table-rspace:0;">
186+
<tr>
187+
<td style="mso-table-lspace:0;mso-table-rspace:0;">
188+
<div dir="ltr" lang="en" role="note" aria-label="External email warning"
189+
style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;
190+
mso-line-height-rule:exactly;border:2px solid #d79c2b;
191+
padding:8px;background:transparent;color:inherit;
192+
font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.5;">
193+
<strong>⚠️ External Email – Check Before You&nbsp;Act</strong><br><br>
194+
This email is from <strong>outside our&nbsp;organization</strong>.<br>
195+
• Do not reply, click links, or open attachments unless you trust the&nbsp;sender.<br>
196+
• If it looks like it came from someone inside, confirm another way before acting.<br>
197+
• Report suspicious messages using the <strong>REPORT</strong>&nbsp;button.
198+
</div>
199+
<div style="line-height:0;font-size:0;" aria-hidden="true">
200+
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;">
201+
<tr><td style="height:8px;line-height:8px;font-size:8px;">&nbsp;</td></tr>
202+
</table>
203+
</div>
204+
</td>
205+
</tr>
206+
</table>
207+
</td>
208+
</tr>
209+
</table>
210+
'@
211+
212+
# Deploy the rule
213+
try {
214+
if ($PSCmdlet.ShouldProcess($RuleName, "Create/update transport rule")) {
215+
Set-ExternalDisclaimerRule -RuleName $RuleName -Priority $Priority -HeaderName $HeaderName -HeaderValue $HeaderValue -BannerHtml $BannerHtml -Disabled:$Disabled
216+
}
217+
218+
# Display completion message
219+
Write-Information "Configuration complete!" -InformationAction Continue
220+
if ($Disabled) {
221+
Write-Warning "The rule '$RuleName' is created but DISABLED. Enable it when ready to activate."
222+
Write-Information "To enable: Set-TransportRule -Identity '$RuleName' -Enabled `$true" -InformationAction Continue
223+
}
224+
else {
225+
Write-Information "The rule '$RuleName' is now active with NO authentication exceptions." -InformationAction Continue
226+
}
227+
}
228+
catch {
229+
Write-Error "Failed to configure transport rule: $($_.Exception.Message)"
230+
Write-Warning "Common issues: Insufficient Exchange Online permissions, network connectivity, rule name conflicts, or transport rule size limits"
231+
throw
232+
}

0 commit comments

Comments
 (0)