Private/PolicyHelper.ps1

<#
.SYNOPSIS
    Policy and rule helper functions for DLP policy operations.

.DESCRIPTION
    This file contains functions for working with DLP policies and rules including
    pattern matching, configuration file reading, content loading, and rule updates.

.NOTES
    Internal module file - not exported to users.
    Functions are available to all module cmdlets.
#>


function Get-MatchingPolicies {
    <#
    .SYNOPSIS
        Gets DLP policies matching a wildcard pattern.
    
    .DESCRIPTION
        Retrieves all DLP compliance policies that match the specified pattern.
        Supports wildcard patterns using * for flexible matching.
    
    .PARAMETER Pattern
        The pattern to match against policy names.
        Supports wildcards: * matches any characters.
        Examples: "GDPR*", "*PII*", "Test Policy"
    
    .OUTPUTS
        Microsoft.Office.CompliancePolicy.PolicySync.PublishedPolicy[]
        Returns array of matching DLP compliance policies.
    
    .EXAMPLE
        $policies = Get-MatchingPolicies -Pattern "GDPR*"
        # Gets all policies starting with "GDPR"
    
    .EXAMPLE
        $policies = Get-MatchingPolicies -Pattern "*"
        # Gets all policies
    
    .EXAMPLE
        $policies = Get-MatchingPolicies -Pattern "Test Policy"
        # Gets exact match for "Test Policy"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [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-MatchingRules {
    <#
    .SYNOPSIS
        Gets DLP rules within a policy matching a wildcard pattern.
    
    .DESCRIPTION
        Retrieves all DLP compliance rules for a specified policy that match
        the specified pattern. Supports wildcard patterns using * for flexible matching.
    
    .PARAMETER PolicyName
        The name of the policy containing the rules.
    
    .PARAMETER Pattern
        The pattern to match against rule names.
        Supports wildcards: * matches any characters.
        Examples: "High*", "*Volume*", "Specific Rule"
    
    .OUTPUTS
        Microsoft.Office.CompliancePolicy.PolicySync.PublishedRule[]
        Returns array of matching DLP compliance rules.
    
    .EXAMPLE
        $rules = Get-MatchingRules -PolicyName "GDPR Enhanced" -Pattern "High*"
        # Gets all rules starting with "High" in the GDPR Enhanced policy
    
    .EXAMPLE
        $rules = Get-MatchingRules -PolicyName "PII Detection" -Pattern "*"
        # Gets all rules in the policy
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$PolicyName,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [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 Read-ConfigurationFile {
    <#
    .SYNOPSIS
        Reads and validates a CSV configuration file.
    
    .DESCRIPTION
        Reads a CSV configuration file, filters out comments and empty lines,
        and validates that required columns are present. Used for bulk update
        operations on DLP policies and rules.
    
    .PARAMETER FilePath
        The path to the CSV configuration file.
    
    .OUTPUTS
        System.Object[]
        Returns array of configuration objects from the CSV file.
    
    .EXAMPLE
        $config = Read-ConfigurationFile -FilePath ".\config\update-config.csv"
        foreach ($entry in $config) {
            Write-Host "Policy: $($entry.PolicyName), Rule: $($entry.RuleName)"
        }
    
    .NOTES
        Expected CSV columns: PolicyName, RuleName, PolicyTipFile, EmailBodyFile, NotifyUser
        Lines starting with # are treated as comments and ignored.
        Empty lines are ignored.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$FilePath
    )
    
    Write-Verbose "Reading configuration from: $FilePath"
    
    if (-not (Test-Path $FilePath)) {
        throw "Configuration file not found: $FilePath"
    }
    
    try {
        # Import CSV, filtering out comment lines and empty lines
        $rawContent = Get-Content $FilePath -Encoding UTF8
        $filteredContent = $rawContent | Where-Object { 
            $_ -notmatch '^\s*#' -and $_ -notmatch '^\s*$' 
        }
        
        # Convert to CSV
        $config = $filteredContent | ConvertFrom-Csv
        
        if ($null -eq $config -or $config.Count -eq 0) {
            throw "Configuration file is empty or contains no valid entries"
        }
        
        # Validate required columns
        $requiredColumns = @('PolicyName', 'RuleName', 'PolicyTipFile', 'EmailBodyFile', 'NotifyUser')
        $firstRow = $config[0]
        $missingColumns = $requiredColumns | Where-Object { 
            -not $firstRow.PSObject.Properties.Name.Contains($_) 
        }
        
        if ($missingColumns) {
            throw "Configuration file missing required columns: $($missingColumns -join ', ')"
        }
        
        Write-Verbose "Loaded $($config.Count) configuration entries"
        return $config
    }
    catch {
        throw "Failed to read configuration file: $($_.Exception.Message)"
    }
}

function Get-ContentFromFile {
    <#
    .SYNOPSIS
        Reads content from a file in the module's Templates directory.
    
    .DESCRIPTION
        Reads the full content of a template file (HTML, text, etc.) from
        the module's Templates directory structure. Handles path resolution
        relative to the module root.
    
    .PARAMETER FilePath
        The relative path to the content file within the Templates directory.
        Example: "PolicyTips\simulation-tip.html" or "EmailBodies\standard-notification.html"
    
    .OUTPUTS
        System.String
        Returns the full content of the file as a string.
    
    .EXAMPLE
        $html = Get-ContentFromFile -FilePath "PolicyTips\simulation-tip.html"
    
    .EXAMPLE
        $email = Get-ContentFromFile -FilePath "EmailBodies\standard-notification.html"
    
    .NOTES
        This function looks for files in the module's Templates directory.
        For direct template path resolution, use Get-TemplateFile from Common.ps1.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$FilePath
    )
    
    Write-Verbose "Reading content from: $FilePath"
    
    # Get module root directory (parent of Private folder)
    $moduleRoot = Split-Path -Parent $PSScriptRoot
    
    # Build full path to template file
    $fullPath = Join-Path $moduleRoot "Templates" $FilePath
    
    if (-not (Test-Path $fullPath)) {
        throw "Content file not found: $fullPath"
    }
    
    try {
        $content = Get-Content $fullPath -Raw -Encoding UTF8
        Write-Verbose "Read $($content.Length) characters from file"
        return $content
    }
    catch {
        throw "Failed to read content file '$fullPath': $($_.Exception.Message)"
    }
}

function New-DLPRuleUpdateParams {
    <#
    .SYNOPSIS
        Creates parameter hashtable for DLP rule updates.
    
    .DESCRIPTION
        Builds a hashtable of parameters for Set-DlpComplianceRule based on
        provided notification settings. Only includes parameters that have values,
        allowing selective updates.
    
    .PARAMETER RuleIdentity
        The identity (name or GUID) of the rule to update.
    
    .PARAMETER PolicyTipContent
        Optional. The HTML content for the policy tip.
    
    .PARAMETER EmailBodyContent
        Optional. The HTML content for the email body.
    
    .PARAMETER NotifyUser
        Optional. Who to notify. Values: SiteAdmin, LastModifier, Owner, or specific email addresses.
    
    .PARAMETER RemoveNotifications
        If specified, clears all notification settings.
    
    .OUTPUTS
        System.Collections.Hashtable
        Returns hashtable suitable for splatting to Set-DlpComplianceRule.
    
    .EXAMPLE
        $params = New-DLPRuleUpdateParams -RuleIdentity "Rule1" -PolicyTipContent $tip -NotifyUser "SiteAdmin"
        Set-DlpComplianceRule @params
    
    .NOTES
        This function helps centralize parameter building logic and ensures
        consistent parameter handling across update operations.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RuleIdentity,
        
        [Parameter(Mandatory = $false)]
        [string]$PolicyTipContent,
        
        [Parameter(Mandatory = $false)]
        [string]$EmailBodyContent,
        
        [Parameter(Mandatory = $false)]
        [string]$NotifyUser,
        
        [Parameter(Mandatory = $false)]
        [switch]$RemoveNotifications
    )
    
    $params = @{
        Identity = $RuleIdentity
    }
    
    if ($RemoveNotifications) {
        # Clear notification settings
        $params['NotifyUser'] = @()
        $params['NotifyPolicyTipCustomText'] = ""
        $params['NotifyEmailCustomText'] = ""
    }
    else {
        # Add parameters only if they have values
        if (-not [string]::IsNullOrWhiteSpace($PolicyTipContent)) {
            $params['NotifyPolicyTipCustomText'] = $PolicyTipContent
        }
        
        if (-not [string]::IsNullOrWhiteSpace($EmailBodyContent)) {
            $params['NotifyEmailCustomText'] = $EmailBodyContent
        }
        
        if (-not [string]::IsNullOrWhiteSpace($NotifyUser)) {
            # Parse comma-separated values
            $notifyUserArray = $NotifyUser -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
            if ($notifyUserArray.Count -gt 0) {
                $params['NotifyUser'] = $notifyUserArray
            }
        }
    }
    
    Write-Verbose "Built update parameters with $($params.Keys.Count) keys"
    return $params
}