Skip to content

Commit f1dd576

Browse files
Merge pull request KelvinTegelaar#1471 from Zacgoose/safelinkspolicy
Feat: Safe Links Policy - Management, Standards, and Templates
2 parents 5ccd2b7 + 2cb4772 commit f1dd576

17 files changed

+2658
-88
lines changed

Config/standards.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,6 +1722,35 @@
17221722
"powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert",
17231723
"recommendedBy": []
17241724
},
1725+
{
1726+
"name": "standards.SafeLinksTemplatePolicy",
1727+
"label": "SafeLinks Policy Template",
1728+
"cat": "Templates",
1729+
"multiple": false,
1730+
"disabledFeatures": {
1731+
"report": false,
1732+
"warn": false,
1733+
"remediate": false
1734+
},
1735+
"impact": "Medium Impact",
1736+
"addedDate": "2025-04-29",
1737+
"helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.",
1738+
"addedComponent": [
1739+
{
1740+
"type": "autoComplete",
1741+
"multiple": true,
1742+
"creatable": false,
1743+
"name": "standards.SafeLinksTemplatePolicy.TemplateIds",
1744+
"label": "Select SafeLinks Policy Templates",
1745+
"api": {
1746+
"url": "/api/ListSafeLinksPolicyTemplates",
1747+
"labelField": "TemplateName",
1748+
"valueField": "GUID",
1749+
"queryKey": "ListSafeLinksPolicyTemplates"
1750+
}
1751+
}
1752+
]
1753+
},
17251754
{
17261755
"name": "standards.SafeLinksPolicy",
17271756
"cat": "Defender Standards",

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1

Lines changed: 0 additions & 31 deletions
This file was deleted.

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
using namespace System.Net
2+
3+
Function Invoke-AddSafeLinksPolicyFromTemplate {
4+
<#
5+
.FUNCTIONALITY
6+
Entrypoint
7+
.ROLE
8+
Exchange.SafeLinks.ReadWrite
9+
.DESCRIPTION
10+
This function deploys SafeLinks policies and rules from templates to selected tenants.
11+
#>
12+
[CmdletBinding()]
13+
param($Request, $TriggerMetadata)
14+
15+
$APIName = $Request.Params.CIPPEndpoint
16+
$Headers = $Request.Headers
17+
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
18+
19+
try {
20+
$RequestBody = $Request.Body
21+
22+
# Extract tenant IDs from selectedTenants
23+
$SelectedTenants = $RequestBody.selectedTenants | ForEach-Object { $_.value }
24+
if ('AllTenants' -in $SelectedTenants) {
25+
$SelectedTenants = (Get-Tenants).defaultDomainName
26+
}
27+
28+
# Extract templates from TemplateList
29+
$Templates = $RequestBody.TemplateList | ForEach-Object { $_.value }
30+
31+
if (-not $Templates -or $Templates.Count -eq 0) {
32+
throw "No templates provided in TemplateList"
33+
}
34+
35+
# Helper function to process array fields with cleaner logic
36+
function ConvertTo-SafeArray {
37+
param($Field)
38+
39+
if ($null -eq $Field) { return @() }
40+
41+
# Handle arrays
42+
if ($Field -is [array]) {
43+
return $Field | ForEach-Object {
44+
if ($_ -is [string]) { $_ }
45+
elseif ($_.value) { $_.value }
46+
elseif ($_.userPrincipalName) { $_.userPrincipalName }
47+
elseif ($_.id) { $_.id }
48+
else { $_.ToString() }
49+
}
50+
}
51+
52+
# Handle single objects
53+
if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) {
54+
if ($Field.value) { return @($Field.value) }
55+
if ($Field.userPrincipalName) { return @($Field.userPrincipalName) }
56+
if ($Field.id) { return @($Field.id) }
57+
}
58+
59+
# Handle strings
60+
if ($Field -is [string]) { return @($Field) }
61+
62+
return @($Field)
63+
}
64+
65+
function Test-PolicyExists {
66+
param($TenantFilter, $PolicyName)
67+
68+
$ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true
69+
return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName }
70+
}
71+
72+
function Test-RuleExists {
73+
param($TenantFilter, $RuleName)
74+
75+
$ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true
76+
return $ExistingRules | Where-Object { $_.Name -eq $RuleName }
77+
}
78+
79+
function New-SafeLinksPolicyFromTemplate {
80+
param($TenantFilter, $Template)
81+
82+
$PolicyName = $Template.PolicyName
83+
$RuleName = $Template.RuleName ?? "$($PolicyName)_Rule"
84+
85+
# Check if policy already exists
86+
if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) {
87+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning'
88+
return "Policy '$PolicyName' already exists in tenant $TenantFilter"
89+
}
90+
91+
# Check if rule already exists
92+
if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) {
93+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning'
94+
return "Rule '$RuleName' already exists in tenant $TenantFilter"
95+
}
96+
97+
# Process array fields
98+
$DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls
99+
$SentTo = ConvertTo-SafeArray -Field $Template.SentTo
100+
$SentToMemberOf = ConvertTo-SafeArray -Field $Template.SentToMemberOf
101+
$RecipientDomainIs = ConvertTo-SafeArray -Field $Template.RecipientDomainIs
102+
$ExceptIfSentTo = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo
103+
$ExceptIfSentToMemberOf = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf
104+
$ExceptIfRecipientDomainIs = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs
105+
106+
# Create policy parameters
107+
$PolicyParams = @{ Name = $PolicyName }
108+
109+
# Policy configuration mapping
110+
$PolicyMappings = @{
111+
'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail'
112+
'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams'
113+
'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice'
114+
'TrackClicks' = 'TrackClicks'
115+
'AllowClickThrough' = 'AllowClickThrough'
116+
'ScanUrls' = 'ScanUrls'
117+
'EnableForInternalSenders' = 'EnableForInternalSenders'
118+
'DeliverMessageAfterScan' = 'DeliverMessageAfterScan'
119+
'DisableUrlRewrite' = 'DisableUrlRewrite'
120+
'AdminDisplayName' = 'AdminDisplayName'
121+
'CustomNotificationText' = 'CustomNotificationText'
122+
'EnableOrganizationBranding' = 'EnableOrganizationBranding'
123+
}
124+
125+
foreach ($templateKey in $PolicyMappings.Keys) {
126+
if ($null -ne $Template.$templateKey) {
127+
$PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey
128+
}
129+
}
130+
131+
if ($DoNotRewriteUrls.Count -gt 0) {
132+
$PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls
133+
}
134+
135+
# Create SafeLinks Policy
136+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true
137+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks policy '$PolicyName'" -Sev 'Info'
138+
139+
# Create rule parameters
140+
$RuleParams = @{
141+
Name = $RuleName
142+
SafeLinksPolicy = $PolicyName
143+
}
144+
145+
# Rule configuration mapping
146+
$RuleMappings = @{
147+
'Priority' = 'Priority'
148+
'TemplateDescription' = 'Comments'
149+
}
150+
151+
foreach ($templateKey in $RuleMappings.Keys) {
152+
if ($null -ne $Template.$templateKey) {
153+
$RuleParams[$RuleMappings[$templateKey]] = $Template.$templateKey
154+
}
155+
}
156+
157+
# Add array parameters if they have values
158+
$ArrayMappings = @{
159+
'SentTo' = $SentTo
160+
'SentToMemberOf' = $SentToMemberOf
161+
'RecipientDomainIs' = $RecipientDomainIs
162+
'ExceptIfSentTo' = $ExceptIfSentTo
163+
'ExceptIfSentToMemberOf' = $ExceptIfSentToMemberOf
164+
'ExceptIfRecipientDomainIs' = $ExceptIfRecipientDomainIs
165+
}
166+
167+
foreach ($paramName in $ArrayMappings.Keys) {
168+
if ($ArrayMappings[$paramName].Count -gt 0) {
169+
$RuleParams[$paramName] = $ArrayMappings[$paramName]
170+
}
171+
}
172+
173+
# Create SafeLinks Rule
174+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true
175+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info'
176+
177+
# Handle rule state
178+
$StateMessage = ""
179+
if ($null -ne $Template.State) {
180+
$IsState = switch ($Template.State) {
181+
"Enabled" { $true }
182+
"Disabled" { $false }
183+
$true { $true }
184+
$false { $false }
185+
default { $null }
186+
}
187+
188+
if ($null -ne $IsState) {
189+
$Cmdlet = $IsState ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule'
190+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true
191+
$StateMessage = " (rule $($IsState ? 'enabled' : 'disabled'))"
192+
}
193+
}
194+
195+
return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage"
196+
}
197+
198+
# Process each tenant and template combination
199+
$Results = foreach ($TenantFilter in $SelectedTenants) {
200+
foreach ($Template in $Templates) {
201+
try {
202+
New-SafeLinksPolicyFromTemplate -TenantFilter $TenantFilter -Template $Template
203+
}
204+
catch {
205+
$ErrorMessage = Get-CippException -Exception $_
206+
$ErrorDetail = "Failed to deploy template '$($Template.TemplateName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)"
207+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error'
208+
$ErrorDetail
209+
}
210+
}
211+
}
212+
213+
$StatusCode = [HttpStatusCode]::OK
214+
}
215+
catch {
216+
$ErrorMessage = Get-CippException -Exception $_
217+
$Results = "Failed to process template deployment request. Error: $($ErrorMessage.NormalizedError)"
218+
Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error'
219+
$StatusCode = [HttpStatusCode]::InternalServerError
220+
}
221+
222+
# Return response
223+
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
224+
StatusCode = $StatusCode
225+
Body = @{ Results = $Results }
226+
})
227+
}

0 commit comments

Comments
 (0)