Public/Update-DLPNotification.ps1
|
function Update-DLPNotification { <# .SYNOPSIS Updates Microsoft Purview DLP policy and rule notification settings in bulk. .DESCRIPTION Reads a CSV configuration file and applies bulk updates to DLP policies and rules including: - Policy tip custom text - Email body custom text - Notification recipients (NotifyUser) Supports wildcard matching for policy and rule names, enabling efficient bulk updates across multiple policies and rules with a single configuration file. The CSV configuration file must contain these columns: - PolicyName: Policy name or wildcard pattern (e.g., "GDPR*") - RuleName: Rule name or wildcard pattern (e.g., "*High*") - PolicyTipFile: Path to policy tip HTML file (relative to Content directory) - EmailBodyFile: Path to email body HTML file (relative to Content directory) - NotifyUser: Comma-separated list of notification recipients .PARAMETER ConfigFile Required. Path to the CSV configuration file containing update operations. CSV Format Example: PolicyName,RuleName,PolicyTipFile,EmailBodyFile,NotifyUser GDPR*,*High*,PolicyTips/gdpr-high.html,EmailBodies/gdpr-high.html,SiteAdmin,Owner .PARAMETER ContentBasePath Optional. Base directory containing policy tip and email body content files. Default: [WorkspaceRoot]/Content .PARAMETER Force Skip confirmation prompts and apply changes immediately. .PARAMETER WhatIf Preview changes without applying them. Shows what would be updated. .PARAMETER Confirm Prompt for confirmation before applying each change. .OUTPUTS PSCustomObject with summary statistics: - TotalConfigs: Total configuration entries processed - MatchedPolicies: Number of policies matched - MatchedRules: Number of rules matched - UpdatedRules: Number of rules successfully updated - FailedRules: Number of rules that failed to update .EXAMPLE Update-DLPNotification -ConfigFile ".\Config\update-config.csv" -WhatIf Preview changes without applying them. .EXAMPLE Update-DLPNotification -ConfigFile ".\Config\update-config.csv" -Verbose Apply changes with detailed output showing each operation. .EXAMPLE Update-DLPNotification -ConfigFile ".\Config\update-config.csv" -Force Apply changes without confirmation prompts. .EXAMPLE Update-DLPNotification -ConfigFile ".\Config\update-config.csv" -ContentBasePath "C:\CustomContent" Use a custom directory for policy tip and email body files. .EXAMPLE $result = Update-DLPNotification -ConfigFile ".\Config\update-config.csv" Write-Host "Updated $($result.UpdatedRules) rules successfully" Capture and display summary statistics. .NOTES Requires: Active connection to Security & Compliance Center (use Connect-PurviewDLP first) Author: PurviewDLP Module Wildcard Patterns: - Use * to match any characters (e.g., "GDPR*" matches all policies starting with "GDPR") - Use exact names for precise matching (e.g., "GDPR Enhanced") NotifyUser Values: - SiteAdmin: Site administrators - Owner: Content owners - LastModifier: Last person who modified the content - Comma-separated for multiple (e.g., "SiteAdmin,Owner") Microsoft Warnings: - Deprecation warnings about minconfidence/maxconfidence are normal and can be ignored .LINK https://github.com/uniQuk/PurviewDLP #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateScript({ Test-Path $_ -PathType Leaf })] [string]$ConfigFile, [Parameter(Mandatory = $false)] [string]$ContentBasePath, [Parameter(Mandatory = $false)] [switch]$Force ) begin { Write-Verbose "Starting Update-DLPNotification" # Resolve content base path if (-not $ContentBasePath) { # Default: Current directory + Content $ContentBasePath = Join-Path (Get-Location).Path "Content" } # Check if content path exists if (-not (Test-Path $ContentBasePath)) { Write-ColorOutput "`n✗ Error: Content directory not found: $ContentBasePath" -Type Error Write-ColorOutput "`nThe Content directory is required for policy tips and email templates." -Type Warning Write-ColorOutput "Please run the initialization command first:`n" -Type Info Write-ColorOutput " Initialize-DLPWorkspace" -Type Info Write-ColorOutput "`nThis will create the Config/ and Content/ directories with template files." -Type Info Write-ColorOutput "You can then customize the templates for your organization.`n" -Type Info throw "Content directory not found. Run Initialize-DLPWorkspace to set up workspace." } Write-Verbose "Content base path: $ContentBasePath" # Initialize statistics $script:stats = @{ TotalConfigs = 0 ProcessedConfigs = 0 MatchedPolicies = 0 MatchedRules = 0 UpdatedRules = 0 FailedRules = 0 Errors = @() } } process { try { # Display banner Write-Banner -Message "DLP Notification Settings Update" -Type "Info" # Verify connection Write-ColorOutput "Checking connection to Security & Compliance Center..." -Type Info Test-DLPConnection # Throws if not connected Write-ColorOutput "✓ Connected successfully`n" -Type Success # Check WhatIf mode $whatIfMode = $WhatIfPreference -eq $true if ($whatIfMode) { Write-ColorOutput "⚠️ PREVIEW MODE: No changes will be applied`n" -Type Warning } # Load configuration Write-ColorOutput "Loading configuration from: $ConfigFile" -Type Info $config = Read-ConfigurationFile -FilePath $ConfigFile $script:stats.TotalConfigs = $config.Count Write-ColorOutput "✓ Loaded $($config.Count) configuration entries`n" -Type Success # Display informational note Write-ColorOutput "ℹ️ Note: Microsoft deprecation warnings about minconfidence/maxconfidence" -Type Info Write-ColorOutput " are normal and can be safely ignored.`n" -Type Info # Confirmation prompt (unless -Force or -WhatIf) if (-not $Force -and -not $whatIfMode) { $message = "This will update DLP policy settings based on $($config.Count) configuration entries." $question = "Do you want to continue?" $choices = @( [System.Management.Automation.Host.ChoiceDescription]::new("&Yes", "Apply updates") [System.Management.Automation.Host.ChoiceDescription]::new("&No", "Cancel operation") ) $decision = $Host.UI.PromptForChoice($message, $question, $choices, 1) if ($decision -ne 0) { Write-ColorOutput "`nOperation cancelled by user." -Type Warning return } Write-Host "" } Write-Banner -Message "Processing Configuration Entries" -Type "Info" # Process each configuration entry $configCounter = 0 foreach ($entry in $config) { $configCounter++ $script:stats.ProcessedConfigs++ Write-ColorOutput "`n[$configCounter/$($config.Count)] Processing: Policy='$($entry.PolicyName)' Rule='$($entry.RuleName)'" -Type Info try { # Load content files Write-Verbose "Loading content files..." $policyTipPath = Join-Path $ContentBasePath $entry.PolicyTipFile $emailBodyPath = Join-Path $ContentBasePath $entry.EmailBodyFile if (-not (Test-Path $policyTipPath)) { throw "Policy tip file not found: $policyTipPath" } if (-not (Test-Path $emailBodyPath)) { throw "Email body file not found: $emailBodyPath" } $policyTipContent = Get-Content $policyTipPath -Raw -Encoding UTF8 $emailBodyContent = Get-Content $emailBodyPath -Raw -Encoding UTF8 Write-Verbose " Loaded policy tip: $($policyTipContent.Length) chars" Write-Verbose " Loaded email body: $($emailBodyContent.Length) chars" # Validate content length (Microsoft DLP limits) $maxPolicyTipLength = 256 # Microsoft limit for policy tips $maxEmailBodyLength = 5120 # Microsoft limit for email bodies if ($policyTipContent.Length -gt $maxPolicyTipLength) { Write-ColorOutput " ⚠️ WARNING: Policy tip is $($policyTipContent.Length) characters (limit: $maxPolicyTipLength)" -Type Warning Write-ColorOutput " Tip may be truncated by Microsoft 365. Consider shortening the HTML." -Type Warning } if ($emailBodyContent.Length -gt $maxEmailBodyLength) { throw "Email body is $($emailBodyContent.Length) characters, exceeds Microsoft limit of $maxEmailBodyLength characters. Please shorten the HTML file: $emailBodyPath" } # Find matching policies $policies = Get-MatchingPoliciesInternal -Pattern $entry.PolicyName if ($policies.Count -eq 0) { Write-ColorOutput " ⚠️ No policies matched pattern: $($entry.PolicyName)" -Type Warning continue } $script:stats.MatchedPolicies += $policies.Count Write-ColorOutput " → Found $($policies.Count) matching policy/policies" -Type Info # Process each matching policy foreach ($policy in $policies) { Write-ColorOutput " Processing policy: $($policy.Name)" -Type Info # Find matching rules $rules = Get-MatchingRulesInternal -PolicyName $policy.Name -Pattern $entry.RuleName if ($rules.Count -eq 0) { Write-ColorOutput " ⚠️ No rules matched pattern: $($entry.RuleName)" -Type Warning continue } $script:stats.MatchedRules += $rules.Count Write-ColorOutput " → Found $($rules.Count) matching rule(s)" -Type Info # Update each matching rule foreach ($rule in $rules) { $updateParams = @{ Rule = $rule PolicyTipContent = $policyTipContent EmailBodyContent = $emailBodyContent NotifyUser = $entry.NotifyUser WhatIf = $whatIfMode } $success = Update-RuleNotificationInternal @updateParams if ($success) { $script:stats.UpdatedRules++ } else { $script:stats.FailedRules++ } } } } catch { $errorMessage = "Error processing config entry $configCounter`: $($_.Exception.Message)" Write-ColorOutput " ✗ $errorMessage" -Type Error Write-Verbose "Stack trace: $($_.ScriptStackTrace)" $script:stats.Errors += $errorMessage $script:stats.FailedRules++ } } # Display Summary Report Write-Banner -Message "Update Summary" -Type "Info" Write-ColorOutput "Configuration Processing:" -Type Info Write-Host " Total config entries: $($script:stats.TotalConfigs)" Write-Host " Processed entries: $($script:stats.ProcessedConfigs)" Write-Host "" Write-ColorOutput "Matching Results:" -Type Info Write-Host " Policies matched: $($script:stats.MatchedPolicies)" Write-Host " Rules matched: $($script:stats.MatchedRules)" Write-Host "" Write-ColorOutput "Update Results:" -Type Info if ($whatIfMode) { Write-ColorOutput " Rules that would be updated: $($script:stats.UpdatedRules)" -Type Warning } else { Write-ColorOutput " Rules successfully updated: $($script:stats.UpdatedRules)" -Type Success if ($script:stats.FailedRules -gt 0) { Write-ColorOutput " Rules failed: $($script:stats.FailedRules)" -Type Error } } # Display errors if any if ($script:stats.Errors.Count -gt 0) { Write-Host "" Write-ColorOutput "Errors Encountered:" -Type Error foreach ($errorMessage in $script:stats.Errors) { Write-Host " - $errorMessage" -ForegroundColor Red } } Write-Host "" if ($whatIfMode) { Write-ColorOutput "✓ Preview completed. Use -Verbose for more details." -Type Warning Write-ColorOutput " To apply changes, run without -WhatIf parameter." -Type Info } else { if ($script:stats.FailedRules -eq 0 -and $script:stats.UpdatedRules -gt 0) { Write-ColorOutput "✓ Update completed successfully!" -Type Success Write-Host "" Write-ColorOutput "ℹ️ Microsoft warnings about deprecated parameters are normal and can be ignored." -Type Info } elseif ($script:stats.UpdatedRules -eq 0) { Write-ColorOutput "⚠ No rules were updated. Check patterns and configuration." -Type Warning } else { Write-ColorOutput "⚠ Update completed with errors. Review messages above." -Type Warning } } Write-Host "" # Return statistics object return [PSCustomObject]$script:stats } catch { Write-Host "" Write-ColorOutput "✗ Error during update:" -Type Error Write-ColorOutput $_.Exception.Message -Type Error Write-Verbose "Stack Trace: $($_.ScriptStackTrace)" Write-Host "" Write-ColorOutput "Troubleshooting tips:" -Type Warning Write-ColorOutput " 1. Ensure you are connected: Connect-PurviewDLP" -Type Info Write-ColorOutput " 2. Verify the configuration file exists and is valid CSV" -Type Info Write-ColorOutput " 3. Check that content files exist in the Content/ directory" -Type Info Write-ColorOutput " 4. Verify you have permissions to modify DLP policies" -Type Info Write-ColorOutput " 5. Use -WhatIf to preview changes before applying" -Type Info Write-ColorOutput " 6. Use -Verbose for detailed diagnostic output" -Type Info Write-Host "" throw } } end { Write-Verbose "Update-DLPNotification completed" } } #region Internal Helper Functions function Get-MatchingPoliciesInternal { <# .SYNOPSIS Internal function to find policies matching a wildcard pattern. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Pattern ) Write-Verbose "Searching for policies matching pattern: $Pattern" try { $allPolicies = Get-DlpCompliancePolicy -ErrorAction Stop # Convert wildcard pattern to regex $regexPattern = '^' + [regex]::Escape($Pattern).Replace('\*', '.*') + '$' $matchingPolicies = $allPolicies | Where-Object { $_.Name -match $regexPattern } Write-Verbose " Found $($matchingPolicies.Count) matching policies" return $matchingPolicies } catch { throw "Failed to retrieve policies: $($_.Exception.Message)" } } function Get-MatchingRulesInternal { <# .SYNOPSIS Internal function to find rules matching a wildcard pattern within a policy. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PolicyName, [Parameter(Mandatory = $true)] [string]$Pattern ) Write-Verbose "Searching for rules in policy '$PolicyName' matching pattern: $Pattern" try { $allRules = Get-DlpComplianceRule -Policy $PolicyName -ErrorAction Stop # Convert wildcard pattern to regex $regexPattern = '^' + [regex]::Escape($Pattern).Replace('\*', '.*') + '$' $matchingRules = $allRules | Where-Object { $_.Name -match $regexPattern } Write-Verbose " Found $($matchingRules.Count) matching rules" return $matchingRules } catch { throw "Failed to retrieve rules for policy '$PolicyName': $($_.Exception.Message)" } } function Update-RuleNotificationInternal { <# .SYNOPSIS Internal function to update a single rule's notification settings. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [object]$Rule, [Parameter(Mandatory = $true)] [string]$PolicyTipContent, [Parameter(Mandatory = $true)] [string]$EmailBodyContent, [Parameter(Mandatory = $true)] [string]$NotifyUser, [Parameter(Mandatory = $false)] [bool]$WhatIf = $false ) $ruleName = $Rule.Name $ruleId = $Rule.Guid try { # Parse NotifyUser (comma-separated values) $notifyUserArray = $NotifyUser -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } if ($WhatIf) { Write-ColorOutput " [WHATIF] Would update rule: $ruleName" -Type Warning Write-Verbose " Policy Tip: $($PolicyTipContent.Substring(0, [Math]::Min(100, $PolicyTipContent.Length)))..." Write-Verbose " Email Body: $($EmailBodyContent.Substring(0, [Math]::Min(100, $EmailBodyContent.Length)))..." Write-Verbose " NotifyUser: $($notifyUserArray -join ', ')" return $true } # Build parameter hashtable for Set-DlpComplianceRule $params = @{ Identity = $ruleId NotifyPolicyTipCustomText = $PolicyTipContent NotifyEmailCustomText = $EmailBodyContent NotifyUser = $notifyUserArray Confirm = $false ErrorAction = 'Stop' } # Apply the update Set-DlpComplianceRule @params Write-ColorOutput " ✓ Updated rule: $ruleName" -Type Success Write-Verbose " NotifyUser: $($notifyUserArray -join ', ')" return $true } catch { Write-ColorOutput " ✗ Failed to update rule '$ruleName': $($_.Exception.Message)" -Type Error Write-Verbose " Stack trace: $($_.ScriptStackTrace)" return $false } } #endregion |