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