Skip to content

Commit f697a54

Browse files
committed
fix(dlp): normalize advanced rule handling
Add `Resolve-CIPPDlpAdvancedRule` and apply it across template capture, policy deploy, and drift comparison so DLP rules keep either `AdvancedRule` or flat condition fields, never both. Update DLP rule field metadata to expose `RuleConditions` and include `AdvancedRule`, then normalize advanced-rule JSON during comparison to avoid false drift from formatting or key-order differences.
1 parent d141595 commit f697a54

5 files changed

Lines changed: 99 additions & 17 deletions

File tree

Modules/CIPPCore/Public/Compare-CIPPDlpCompliancePolicy.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ function ConvertTo-CIPPDlpComparable {
6868
$Rules = [ordered]@{}
6969
foreach ($Rule in @($RuleSource) | Where-Object { $_ }) {
7070
$RuleParams = Format-CIPPCompliancePolicyParams -Source $Rule -AllowedFields $Fields.Rule
71+
# Mirror deploy: keep only AdvancedRule OR the flat condition params, never both, so the same
72+
# side is compared that deploy would actually send (see Resolve-CIPPDlpAdvancedRule).
73+
$RuleParams = Resolve-CIPPDlpAdvancedRule -Source $Rule -RuleParams $RuleParams
7174
$RuleName = [string]$RuleParams['Name']
7275
$RuleParams.Remove('Policy') | Out-Null
7376
$RuleParams.Remove('Name') | Out-Null
@@ -79,6 +82,12 @@ function ConvertTo-CIPPDlpComparable {
7982
if ($RuleParams.ContainsKey('IncidentReportContent') -and $RuleParams['IncidentReportContent'] -is [string]) {
8083
$RuleParams['IncidentReportContent'] = @($RuleParams['IncidentReportContent'] -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
8184
}
85+
# Expand the AdvancedRule JSON string into an object so the canonical comparison string is
86+
# independent of whitespace and key order (the service may reformat the blob it returns).
87+
# Unparseable JSON falls back to raw string comparison.
88+
if ($RuleParams.ContainsKey('AdvancedRule') -and $RuleParams['AdvancedRule'] -is [string]) {
89+
try { $RuleParams['AdvancedRule'] = $RuleParams['AdvancedRule'] | ConvertFrom-Json -ErrorAction Stop } catch {}
90+
}
8291
if (-not [string]::IsNullOrWhiteSpace($RuleName)) { $Rules[$RuleName] = $RuleParams }
8392
}
8493

Modules/CIPPCore/Public/Get-CIPPDlpComplianceFieldList.ps1

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,32 +32,41 @@ function Get-CIPPDlpComplianceFieldList {
3232
'ModernGroupLocation', 'ModernGroupLocationException'
3333
)
3434

35-
# Note: DLP rules have no 'Mode' parameter (that is policy-level). 'Policy' is the parent reference
36-
# added at deploy time; it is not a comparable setting.
37-
$Rule = @(
38-
'Name', 'Policy', 'Comment', 'Disabled',
35+
# Simple-mode condition/exception parameters, kept as their own list because they are mutually
36+
# exclusive with AdvancedRule on New-/Set-DlpComplianceRule: a rule built with Purview's Advanced
37+
# Rule editor carries its entire condition tree in the single AdvancedRule JSON blob, and sending
38+
# any of these alongside it is rejected (see Resolve-CIPPDlpAdvancedRule).
39+
$RuleConditions = @(
3940
'ContentContainsSensitiveInformation', 'ExceptIfContentContainsSensitiveInformation',
40-
'ContentPropertyContainsWords', 'BlockAccess', 'BlockAccessScope',
41-
'NotifyUser', 'NotifyEmailCustomText', 'NotifyEmailCustomSubject',
42-
'NotifyPolicyTipCustomText', 'GenerateAlert', 'AlertProperties',
43-
'GenerateIncidentReport', 'IncidentReportContent',
44-
'AccessScope', 'From', 'FromMemberOf', 'FromAddressContainsWords',
45-
'FromAddressMatchesPatterns', 'SentTo', 'SentToMemberOf',
46-
'RecipientDomainIs', 'AnyOfRecipientAddressContainsWords',
47-
'AnyOfRecipientAddressMatchesPatterns', 'AnyOfRecipientAddressDomainIs',
41+
'ContentPropertyContainsWords', 'AccessScope',
42+
'From', 'FromMemberOf', 'FromAddressContainsWords', 'FromAddressMatchesPatterns',
43+
'SentTo', 'SentToMemberOf', 'RecipientDomainIs',
44+
'AnyOfRecipientAddressContainsWords', 'AnyOfRecipientAddressMatchesPatterns',
45+
'AnyOfRecipientAddressDomainIs',
4846
'ExceptIfFrom', 'ExceptIfFromMemberOf', 'ExceptIfFromAddressContainsWords',
4947
'ExceptIfFromAddressMatchesPatterns',
50-
'AddRecipients', 'BlockMessage', 'GenerateAlertOn', 'IncidentReportTo',
51-
'ReportSeverityLevel', 'RuleErrorAction',
5248
'ContentExtensionMatchesWords', 'DocumentNameMatchesPatterns',
5349
'DocumentNameMatchesWords', 'DocumentSizeOver',
5450
'ContentCharacterSetContainsWords', 'ContentFileTypeMatches'
5551
)
5652

53+
# Note: DLP rules have no 'Mode' parameter (that is policy-level). 'Policy' is the parent reference
54+
# added at deploy time; it is not a comparable setting.
55+
$Rule = @(
56+
'Name', 'Policy', 'Comment', 'Disabled', 'AdvancedRule',
57+
'BlockAccess', 'BlockAccessScope',
58+
'NotifyUser', 'NotifyEmailCustomText', 'NotifyEmailCustomSubject',
59+
'NotifyPolicyTipCustomText', 'GenerateAlert', 'AlertProperties',
60+
'GenerateIncidentReport', 'IncidentReportContent',
61+
'AddRecipients', 'BlockMessage', 'GenerateAlertOn', 'IncidentReportTo',
62+
'ReportSeverityLevel', 'RuleErrorAction'
63+
) + $RuleConditions
64+
5765
return [pscustomobject]@{
58-
Policy = $Policy
59-
Rule = $Rule
60-
Location = @($Policy | Where-Object { $_ -like '*Location*' })
66+
Policy = $Policy
67+
Rule = $Rule
68+
RuleConditions = $RuleConditions
69+
Location = @($Policy | Where-Object { $_ -like '*Location*' })
6170
# Valid -Mode input values for New-/Set-DlpCompliancePolicy. Transient/output-only states such as
6271
# 'PendingDeletion' are NOT accepted as input and must be dropped before deploy.
6372
ValidPolicyModes = @('Enable', 'TestWithNotifications', 'TestWithoutNotifications', 'Disable')
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
function Resolve-CIPPDlpAdvancedRule {
2+
<#
3+
.SYNOPSIS
4+
Enforce the AdvancedRule vs simple-mode condition exclusivity on a filtered DLP rule param set.
5+
.DESCRIPTION
6+
A DLP rule built with Purview's Advanced Rule editor carries its entire condition tree in the
7+
single AdvancedRule JSON blob and leaves the flat simple-mode condition properties empty; a
8+
simple-mode rule is the reverse. New-/Set-DlpComplianceRule reject AdvancedRule combined with
9+
any simple-mode condition/exception parameter, so every code path that builds rule params
10+
(template capture, deploy, drift comparison) must end up with one or the other, never both.
11+
12+
Which side is authoritative comes from the source's IsAdvancedRule flag (Get-DlpComplianceRule
13+
emits it, but the allowlist strips it from stored params - hence reading it off the unfiltered
14+
source). Without the flag - e.g. a stored template, where a captured AdvancedRule is always
15+
deliberate - a populated AdvancedRule wins. AdvancedRule is normalized to the JSON string the
16+
cmdlets expect; it is kept as a string in stored templates too, so a deep condition tree can
17+
never be truncated by the template's ConvertTo-Json -Depth limit.
18+
.PARAMETER Source
19+
The unfiltered rule object (Get-DlpComplianceRule output, stored template rule, or request
20+
body), used to read IsAdvancedRule.
21+
.PARAMETER RuleParams
22+
The allowlist-filtered rule parameter hashtable to fix up. Modified in place and returned.
23+
.FUNCTIONALITY
24+
Internal
25+
#>
26+
[CmdletBinding()]
27+
param(
28+
[Parameter(Mandatory)] $Source,
29+
[Parameter(Mandatory)] [hashtable] $RuleParams
30+
)
31+
32+
if (-not $RuleParams.ContainsKey('AdvancedRule')) { return $RuleParams }
33+
34+
# ConvertFrom-Json yields a real boolean, but a hand-edited template may carry 'False' as a string;
35+
# string-compare handles both. An absent flag means AdvancedRule was stored deliberately -> advanced.
36+
$Flag = $Source.PSObject.Properties['IsAdvancedRule']
37+
$IsAdvanced = -not ($null -ne $Flag -and $null -ne $Flag.Value -and "$($Flag.Value)" -eq 'False')
38+
39+
if (-not $IsAdvanced) {
40+
# Simple-mode rule: the flat condition parameters are authoritative. Get-DlpComplianceRule can
41+
# still emit an AdvancedRule serialization of those same conditions - it must not be captured,
42+
# deployed, or drift-compared alongside them.
43+
$RuleParams.Remove('AdvancedRule') | Out-Null
44+
return $RuleParams
45+
}
46+
47+
$Fields = Get-CIPPDlpComplianceFieldList
48+
foreach ($Condition in $Fields.RuleConditions) {
49+
if ($RuleParams.ContainsKey($Condition)) { $RuleParams.Remove($Condition) | Out-Null }
50+
}
51+
52+
# The cmdlets take AdvancedRule as a JSON string (Get-* already returns it that way); serialize a
53+
# hand-authored nested object. -Depth 100 because condition trees nest far past the default.
54+
if ($RuleParams['AdvancedRule'] -isnot [string]) {
55+
$RuleParams['AdvancedRule'] = ConvertTo-Json -InputObject $RuleParams['AdvancedRule'] -Depth 100 -Compress
56+
}
57+
return $RuleParams
58+
}

Modules/CIPPCore/Public/Set-CIPPDlpCompliancePolicy.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ function Set-CIPPDlpCompliancePolicy {
9090
foreach ($Rule in $RuleList) {
9191
$RuleIndex++
9292
$RuleHash = Format-CIPPCompliancePolicyParams -Source $Rule -AllowedFields $RuleAllowedFields
93+
# Advanced-mode rules send only the AdvancedRule JSON blob, simple-mode rules only the flat
94+
# condition params - New-/Set-DlpComplianceRule reject a mix (see Resolve-CIPPDlpAdvancedRule).
95+
$RuleHash = Resolve-CIPPDlpAdvancedRule -Source $Rule -RuleParams $RuleHash
9396
foreach ($SitField in @('ContentContainsSensitiveInformation', 'ExceptIfContentContainsSensitiveInformation')) {
9497
if ($RuleHash.ContainsKey($SitField)) {
9598
$RuleHash[$SitField] = @(ConvertTo-CIPPSensitiveInformationType -SensitiveInformation $RuleHash[$SitField])

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Security/Compliance-DLP/Invoke-AddDlpCompliancePolicyTemplate.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ Function Invoke-AddDlpCompliancePolicyTemplate {
4949
$RuleParams = foreach ($Rule in $AssociatedRules) {
5050
$RuleClean = Format-CIPPCompliancePolicyParams -Source $Rule -AllowedFields $RuleAllowedFields
5151
$RuleClean.Remove('Policy') | Out-Null # added at deploy time, not stored
52+
# Advanced-mode rules keep only the AdvancedRule JSON blob, simple-mode rules only the
53+
# flat condition params - the cmdlets reject a mix (see Resolve-CIPPDlpAdvancedRule).
54+
$RuleClean = Resolve-CIPPDlpAdvancedRule -Source $Rule -RuleParams $RuleClean
5255
foreach ($SitField in @('ContentContainsSensitiveInformation', 'ExceptIfContentContainsSensitiveInformation')) {
5356
if ($RuleClean.ContainsKey($SitField)) {
5457
$RuleClean[$SitField] = @(ConvertTo-CIPPSensitiveInformationType -SensitiveInformation $RuleClean[$SitField])

0 commit comments

Comments
 (0)