Private/ReportHelper.ps1
|
<# .SYNOPSIS Report formatting and generation helper functions. .DESCRIPTION This file contains functions for formatting DLP policy data into various output formats (HTML, Markdown) including SIT configurations, advanced rules, location scopes, and policy statistics. .NOTES Internal module file - not exported to users. Functions are available to all module cmdlets. #> function Get-PolicyStatistics { <# .SYNOPSIS Calculates statistics from an array of DLP policies. .DESCRIPTION Analyzes DLP policy data and returns comprehensive statistics including policy counts, rule counts, mode distribution, and workload distribution. .PARAMETER Policies Array of policy objects (from backup JSON or Get-DlpCompliancePolicy). .OUTPUTS System.Collections.Hashtable Returns hashtable with statistics: TotalPolicies, EnabledPolicies, DisabledPolicies, TotalRules, TestMode, EnforceMode, Workloads. .EXAMPLE $stats = Get-PolicyStatistics -Policies $policies Write-Host "Total: $($stats.TotalPolicies), Enabled: $($stats.EnabledPolicies)" #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [array]$Policies ) $stats = @{ TotalPolicies = $Policies.Count EnabledPolicies = ($Policies | Where-Object { $_.Enabled -eq $true }).Count DisabledPolicies = ($Policies | Where-Object { $_.Enabled -eq $false }).Count TotalRules = ($Policies | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum TestMode = ($Policies | Where-Object { $_.Mode -like "*Test*" }).Count EnforceMode = ($Policies | Where-Object { $_.Mode -eq "Enforce" }).Count Workloads = @{} } # Count workload distribution foreach ($policy in $Policies) { if ($policy.Workload) { $workloads = $policy.Workload -split ", " | ForEach-Object { $_.Trim() } foreach ($workload in $workloads) { if ($stats.Workloads.ContainsKey($workload)) { $stats.Workloads[$workload]++ } else { $stats.Workloads[$workload] = 1 } } } } Write-Verbose "Calculated statistics: $($stats.TotalPolicies) policies, $($stats.TotalRules) rules" return $stats } function Format-SITConfiguration { <# .SYNOPSIS Formats Sensitive Information Type (SIT) configuration for display. .DESCRIPTION Parses and formats SIT JSON configuration into human-readable format. Supports both grouped and simple array formats. Handles confidence levels, count ranges, and classifier types. .PARAMETER SITJson The JSON string containing SIT configuration. .PARAMETER Format Output format: "Markdown" or "HTML". .OUTPUTS System.String Returns formatted SIT configuration string. .EXAMPLE $formatted = Format-SITConfiguration -SITJson $rule.ContentContainsSensitiveInformation -Format "HTML" #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $false)] [string]$SITJson, [Parameter(Mandatory = $true)] [ValidateSet('Markdown', 'HTML')] [string]$Format ) if (-not $SITJson) { return "" } try { $sitData = $SITJson | ConvertFrom-Json if ($sitData.groups) { # Complex grouped SIT configuration if ($Format -eq "Markdown") { $topOp = if ($sitData.operator) { $sitData.operator } else { "(not specified)" } $output = "`n**Operator:** $topOp`n`n" foreach ($group in $sitData.groups) { $groupName = if ($group.name) { $group.name } else { "(unnamed)" } $groupOp = if ($group.operator) { $group.operator } else { "(not specified)" } $output += "**Group: $groupName** (Operator: $groupOp)`n`n" foreach ($sit in $group.sensitivetypes) { $output += "- **$($sit.name)**`n" if ($sit.confidencelevel) { $output += " - Confidence: $($sit.confidencelevel)`n" } if ($sit.mincount) { $output += " - Count: $($sit.mincount) to $($sit.maxcount)`n" } if ($sit.classifiertype -and $sit.classifiertype -ne 'Content') { $output += " - Type: $($sit.classifiertype)`n" } } $output += "`n" } return $output } else { # HTML format $topOp = if ($sitData.operator) { $sitData.operator } else { "<em>(not specified)</em>" } $output = "<div class='sit-config'><strong>Operator:</strong> $topOp<br>" foreach ($group in $sitData.groups) { $groupName = if ($group.name) { $group.name } else { "<em>(unnamed)</em>" } $groupOp = if ($group.operator) { $group.operator } else { "<em>(not specified)</em>" } $output += "<div class='sit-group'><strong>Group: $groupName</strong> (Operator: $groupOp)<ul>" foreach ($sit in $group.sensitivetypes) { $output += "<li><strong>$($sit.name)</strong>" if ($sit.confidencelevel -or $sit.mincount -or ($sit.classifiertype -and $sit.classifiertype -ne 'Content')) { $output += "<ul>" if ($sit.confidencelevel) { $output += "<li>Confidence: $($sit.confidencelevel)</li>" } if ($sit.mincount) { $output += "<li>Count: $($sit.mincount) to $($sit.maxcount)</li>" } if ($sit.classifiertype -and $sit.classifiertype -ne 'Content') { $output += "<li>Type: $($sit.classifiertype)</li>" } $output += "</ul>" } $output += "</li>" } $output += "</ul></div>" } $output += "</div>" return $output } } elseif ($sitData -is [array]) { # Simple array format if ($Format -eq "Markdown") { $output = "`n" foreach ($sit in $sitData) { $output += "- **$($sit.name)**`n" if ($sit.confidencelevel) { $output += " - Confidence: $($sit.confidencelevel)`n" } if ($sit.mincount) { $output += " - Count: $($sit.mincount) to $($sit.maxcount)`n" } if ($sit.classifiertype) { $output += " - Type: $($sit.classifiertype)`n" } } return $output } else { $output = "<ul>" foreach ($sit in $sitData) { $output += "<li><strong>$($sit.name)</strong>" if ($sit.confidencelevel -or $sit.mincount -or $sit.classifiertype) { $output += "<ul>" if ($sit.confidencelevel) { $output += "<li>Confidence: $($sit.confidencelevel)</li>" } if ($sit.mincount) { $output += "<li>Count: $($sit.mincount) to $($sit.maxcount)</li>" } if ($sit.classifiertype) { $output += "<li>Type: $($sit.classifiertype)</li>" } $output += "</ul>" } $output += "</li>" } $output += "</ul>" return $output } } } catch { Write-Warning "Error parsing SIT configuration: $($_.Exception.Message)" return "Error parsing SIT configuration" } return "" } function Format-AdvancedRule { <# .SYNOPSIS Formats Advanced Rule configuration for display. .DESCRIPTION Parses and formats Advanced Rule JSON configuration into human-readable format. Recursively handles nested conditions and operators. .PARAMETER AdvancedRuleJson The JSON string containing Advanced Rule configuration. .PARAMETER Format Output format: "Markdown", "HTML", or "Plain" (for CSV). .OUTPUTS System.String Returns formatted Advanced Rule configuration string. .EXAMPLE $formatted = Format-AdvancedRule -AdvancedRuleJson $rule.AdvancedRule -Format "HTML" .EXAMPLE $formatted = Format-AdvancedRule -AdvancedRuleJson $rule.AdvancedRule -Format "Plain" #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $false)] [string]$AdvancedRuleJson, [Parameter(Mandatory = $true)] [ValidateSet('Markdown', 'HTML', 'Plain')] [string]$Format ) if (-not $AdvancedRuleJson) { return "" } try { $advancedRule = $AdvancedRuleJson | ConvertFrom-Json if ($Format -eq "Markdown") { $output = "`n**Advanced Rule Configuration**`n`n" $output += "**Version:** $($advancedRule.Version)`n`n" if ($advancedRule.Condition) { $output += "**Conditions:**`n`n" $output += Format-AdvancedRuleCondition -Condition $advancedRule.Condition -Format "Markdown" -Indent 0 } return $output } elseif ($Format -eq "Plain") { # Plain text format for CSV $output = "Advanced Rule (v$($advancedRule.Version)): " if ($advancedRule.Condition) { $output += Format-AdvancedRuleCondition -Condition $advancedRule.Condition -Format "Plain" -Indent 0 } return $output } else { # HTML format $output = "<div class='advanced-rule'>" $output += "<strong>Advanced Rule Configuration</strong> (Version: $($advancedRule.Version))<br>" if ($advancedRule.Condition) { $output += "<div class='advanced-rule-conditions'>" $output += Format-AdvancedRuleCondition -Condition $advancedRule.Condition -Format "HTML" -Indent 0 $output += "</div>" } $output += "</div>" return $output } } catch { Write-Warning "Error parsing Advanced Rule: $($_.Exception.Message)" return "Error parsing Advanced Rule: $($_.Exception.Message)" } } function Format-AdvancedRuleCondition { <# .SYNOPSIS Recursively formats Advanced Rule conditions. .DESCRIPTION Internal helper function for Format-AdvancedRule that recursively processes nested conditions and operators in Advanced Rule configurations. .PARAMETER Condition The condition object to format. .PARAMETER Format Output format: "Markdown", "HTML", or "Plain". .PARAMETER Indent Indentation level for nested conditions. .OUTPUTS System.String Returns formatted condition string. .EXAMPLE $formatted = Format-AdvancedRuleCondition -Condition $cond -Format "HTML" -Indent 1 #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [object]$Condition, [Parameter(Mandatory = $true)] [ValidateSet('Markdown', 'HTML', 'Plain')] [string]$Format, [Parameter(Mandatory = $false)] [int]$Indent = 0 ) $indentStr = " " * $Indent $output = "" if ($Format -eq "Markdown") { if ($Condition.Operator) { $output += "$indentStr**Operator:** $($Condition.Operator)`n" } if ($Condition.SubConditions) { foreach ($subCond in $Condition.SubConditions) { if ($subCond.ConditionName) { $output += "$indentStr- **$($subCond.ConditionName)**`n" if ($subCond.Value) { if ($subCond.ConditionName -eq "ContentContainsSensitiveInformation") { foreach ($sitGroup in $subCond.Value) { if ($sitGroup.groups) { foreach ($group in $sitGroup.groups) { $output += "$indentStr - Group: $($group.name) (Operator: $($group.Operator))`n" if ($group.sensitivetypes) { foreach ($sit in $group.sensitivetypes) { $output += "$indentStr - **$($sit.name)**" if ($sit.confidencelevel) { $output += " (Confidence: $($sit.confidencelevel))" } $output += "`n" } } } } } } else { $output += "$indentStr - Value: $($subCond.Value -join ', ')`n" } } } elseif ($subCond.Operator) { $output += Format-AdvancedRuleCondition -Condition $subCond -Format $Format -Indent ($Indent + 1) } } } } elseif ($Format -eq "Plain") { # Plain text format for CSV - compact and readable if ($Condition.Operator) { $output += "$($Condition.Operator) [" } if ($Condition.SubConditions) { $conditions = @() foreach ($subCond in $Condition.SubConditions) { if ($subCond.ConditionName) { $condText = $subCond.ConditionName if ($subCond.Value) { if ($subCond.ConditionName -eq "ContentContainsSensitiveInformation") { # Handle SITs - extract just the names $sitNames = @() foreach ($sitGroup in $subCond.Value) { if ($sitGroup.groups) { foreach ($group in $sitGroup.groups) { if ($group.sensitivetypes) { $sitNames += $group.sensitivetypes | ForEach-Object { $_.name } } } } } if ($sitNames.Count -gt 0) { $condText += ": " + ($sitNames -join ", ") } } else { $condText += ": " + ($subCond.Value -join ", ") } } $conditions += $condText } elseif ($subCond.Operator) { $conditions += Format-AdvancedRuleCondition -Condition $subCond -Format $Format -Indent ($Indent + 1) } } $output += $conditions -join "; " } if ($Condition.Operator) { $output += "]" } } else { # HTML format if ($Condition.Operator) { $output += "<div style='margin-left: ${Indent}em;'><strong>Operator:</strong> $($Condition.Operator)</div>" } if ($Condition.SubConditions) { $output += "<ul style='margin-left: ${Indent}em;'>" foreach ($subCond in $Condition.SubConditions) { if ($subCond.ConditionName) { $output += "<li><strong>$($subCond.ConditionName)</strong>" if ($subCond.Value) { if ($subCond.ConditionName -eq "ContentContainsSensitiveInformation") { # Handle SITs within advanced rules - can be grouped or flat array $output += "<ul>" # Check if first item has 'groups' property (grouped format) if ($subCond.Value[0].groups) { foreach ($sitGroup in $subCond.Value) { foreach ($group in $sitGroup.groups) { $groupName = if ($group.name) { $group.name } else { "(unnamed)" } $groupOp = if ($group.Operator) { $group.Operator } else { "(not specified)" } $output += "<li>Group: <strong>$groupName</strong> (Operator: $groupOp)<ul>" if ($group.sensitivetypes) { foreach ($sit in $group.sensitivetypes) { $output += "<li><strong>$($sit.name)</strong>" if ($sit.confidencelevel -or $sit.mincount) { $output += "<ul>" if ($sit.confidencelevel) { $output += "<li>Confidence: $($sit.confidencelevel)</li>" } if ($sit.mincount) { $output += "<li>Count: $($sit.mincount) to $($sit.maxcount)</li>" } $output += "</ul>" } $output += "</li>" } } $output += "</ul></li>" } } } else { # Flat array of SITs foreach ($sit in $subCond.Value) { $output += "<li><strong>$($sit.name)</strong>" if ($sit.confidencelevel -or $sit.mincount) { $output += "<ul>" if ($sit.confidencelevel) { $output += "<li>Confidence: $($sit.confidencelevel)</li>" } if ($sit.mincount) { $output += "<li>Count: $($sit.mincount) to $($sit.maxcount)</li>" } $output += "</ul>" } $output += "</li>" } } $output += "</ul>" } elseif ($subCond.ConditionName -eq "ContentFileTypeMatches") { # Resolve file type GUIDs $fileTypes = @{ "29b89383-a6f8-47ad-b594-3b364698b921" = "Word Document (.docx)" "abae71fd-17b1-4716-963f-e0cdbe8ddf9b" = "Excel Workbook (.xlsx)" "31ed09c0-1da5-4b7f-a23f-4f8b5bae839f" = "PowerPoint Presentation (.pptx)" "d6ea7928-706f-4c8a-9a1f-53926659dd5c" = "PDF Document (.pdf)" "6256dc0f-9add-4dd2-8798-5139664ed8dc" = "Text File (.txt)" } $resolved = $subCond.Value | ForEach-Object { $guid = $_.ToString() if ($fileTypes.ContainsKey($guid)) { $fileTypes[$guid] } else { $guid } } $output += ": $($resolved -join ', ')" } elseif ($subCond.ConditionName -eq "SharedByIRMUserRisk") { # Resolve user risk GUIDs $userRisks = @{ "FCB9FA93-6269-4ACF-A756-832E79B36A2A" = "Elevated Risk" "797C4446-5C73-484F-8E58-0CCA08D6DF6C" = "Moderate Risk" "75A4318B-94A2-4323-BA42-2CA6DB29AAFE" = "Minor Risk" } $resolved = $subCond.Value | ForEach-Object { $guid = $_.ToString() if ($userRisks.ContainsKey($guid)) { $userRisks[$guid] } else { $guid } } $output += ": $($resolved -join ', ')" } else { $output += ": $($subCond.Value -join ', ')" } } $output += "</li>" } elseif ($subCond.Operator) { $output += "<li>" $output += Format-AdvancedRuleCondition -Condition $subCond -Format $Format -Indent ($Indent + 1) $output += "</li>" } } $output += "</ul>" } } return $output } function Get-LocationScopeHTML { <# .SYNOPSIS Generates HTML representation of location scope information. .DESCRIPTION Creates formatted HTML showing workload location configurations including whether each workload is set to "All", specific targeting, or not configured. .PARAMETER Policy The policy object containing location information. .OUTPUTS System.String Returns HTML string with location scope information. .EXAMPLE $html = Get-LocationScopeHTML -Policy $policy .NOTES This function uses Get-LocationScopeInfo from LocationScope.ps1 to accurately determine location scopes. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [object]$Policy ) $html = "<div class='location-scope'><table class='location-table'>" $html += "<tr><th>Workload</th><th>Scope</th><th>Details</th></tr>" # Exchange $exchangeScope = Get-LocationScopeInfo -LocationArray $Policy.ExchangeLocation ` -LocationName "Exchange" ` -ScopingProperties @($Policy.ExchangeSender, $Policy.ExchangeSenderMemberOf) ` -AdaptiveScopeProperties @($Policy.ExchangeAdaptiveScopes) if ($exchangeScope.IsConfigured) { $scopeClass = if ($exchangeScope.IsAll) { "scope-all" } else { "scope-specific" } $scopeText = if ($exchangeScope.IsAll) { "All" } else { "Specific ($($exchangeScope.Count))" } $html += "<tr><td>Exchange</td><td class='$scopeClass'>$scopeText</td><td>$($exchangeScope.Locations)</td></tr>" } # SharePoint $sharepointScope = Get-LocationScopeInfo -LocationArray $Policy.SharePointLocation ` -LocationName "SharePoint" ` -AdaptiveScopeProperties @($Policy.SharePointAdaptiveScopes) if ($sharepointScope.IsConfigured) { $scopeClass = if ($sharepointScope.IsAll) { "scope-all" } else { "scope-specific" } $scopeText = if ($sharepointScope.IsAll) { "All" } else { "Specific ($($sharepointScope.Count))" } $html += "<tr><td>SharePoint</td><td class='$scopeClass'>$scopeText</td><td>$($sharepointScope.Locations)</td></tr>" } # OneDrive $onedriveScope = Get-LocationScopeInfo -LocationArray $Policy.OneDriveLocation ` -LocationName "OneDrive" ` -ScopingProperties @($Policy.OneDriveSharedBy, $Policy.OneDriveSharedByMemberOf) ` -AdaptiveScopeProperties @($Policy.OneDriveAdaptiveScopes) if ($onedriveScope.IsConfigured) { $scopeClass = if ($onedriveScope.IsAll) { "scope-all" } else { "scope-specific" } $scopeText = if ($onedriveScope.IsAll) { "All" } else { "Specific ($($onedriveScope.Count))" } $html += "<tr><td>OneDrive</td><td class='$scopeClass'>$scopeText</td><td>$($onedriveScope.Locations)</td></tr>" } # Teams $teamsScope = Get-LocationScopeInfo -LocationArray $Policy.TeamsLocation ` -LocationName "Teams" ` -ScopingProperties @($Policy.TeamsSender, $Policy.TeamsSenderMemberOf) ` -AdaptiveScopeProperties @($Policy.TeamsAdaptiveScopes) if ($teamsScope.IsConfigured) { $scopeClass = if ($teamsScope.IsAll) { "scope-all" } else { "scope-specific" } $scopeText = if ($teamsScope.IsAll) { "All" } else { "Specific ($($teamsScope.Count))" } $html += "<tr><td>Teams</td><td class='$scopeClass'>$scopeText</td><td>$($teamsScope.Locations)</td></tr>" } $html += "</table></div>" return $html } function Build-ReportHTML { <# .SYNOPSIS Builds a complete HTML report from DLP policy data. .DESCRIPTION Generates a comprehensive, interactive HTML report showing all DLP policies, rules, and configurations with collapsible sections and color-coded indicators. .PARAMETER Policies Array of policy objects to include in the report. .PARAMETER Statistics Hashtable containing policy statistics (from Get-PolicyStatistics). .PARAMETER ReportTitle Title to display at the top of the report. Default: "Microsoft Purview DLP Policy Report" .OUTPUTS System.String Returns complete HTML document as a string. .EXAMPLE $html = Build-ReportHTML -Policies $policies -Statistics $stats $html | Out-File -FilePath "report.html" -Encoding UTF8 #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [array]$Policies, [Parameter(Mandatory = $true)] [hashtable]$Statistics, [Parameter(Mandatory = $false)] [string]$ReportTitle = "Microsoft Purview DLP Policy Report" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" # Build HTML header with styles $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>$ReportTitle</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; color: #333; } .container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); overflow: hidden; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; } .header h1 { font-size: 32px; margin-bottom: 10px; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); } .header .timestamp { font-size: 14px; opacity: 0.9; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; padding: 30px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } .stat-card { background: white; padding: 20px; border-radius: 8px; border-left: 4px solid #667eea; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .stat-card .label { font-size: 12px; color: #6c757d; text-transform: uppercase; font-weight: 600; margin-bottom: 8px; } .stat-card .value { font-size: 28px; font-weight: bold; color: #667eea; } .content { padding: 30px; } .policy { background: white; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .policy-header { background: #f8f9fa; padding: 20px; cursor: pointer; border-bottom: 1px solid #dee2e6; transition: background 0.2s; } .policy-header:hover { background: #e9ecef; } .policy-name { font-size: 20px; font-weight: bold; color: #212529; margin-bottom: 8px; } .policy-meta { display: flex; gap: 15px; flex-wrap: wrap; font-size: 13px; color: #6c757d; } .badge { display: inline-block; padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; } .badge-enforce { background: #d4edda; color: #155724; } .badge-test { background: #fff3cd; color: #856404; } .badge-disabled { background: #f8d7da; color: #721c24; } .badge-enabled { background: #d4edda; color: #155724; } .badge-portable { background: #d1ecf1; color: #0c5460; } .badge-not-portable { background: #f8d7da; color: #721c24; } .policy.disabled .policy-header { background: #f1f1f1; border-left: 5px solid #6c757d; } .policy.disabled .policy-name { color: #6c757d; } .policy-body { padding: 20px; display: none; background: #fff; } .policy-body.expanded { display: block; } .info-section { margin-bottom: 25px; } .info-section h3 { font-size: 16px; color: #495057; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #667eea; } .info-grid { display: grid; grid-template-columns: 200px 1fr; gap: 10px; font-size: 14px; } .info-label { font-weight: 600; color: #6c757d; } .info-value { color: #212529; } .rule { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 15px; margin-bottom: 15px; } .rule.disabled { background: #fff3cd; border-left: 4px solid #856404; } .rule-name { font-size: 16px; font-weight: bold; color: #495057; margin-bottom: 10px; } .rule-details { font-size: 13px; line-height: 1.8; } .sit-section { background: #fff9e6; border: 1px solid #ffe066; border-radius: 5px; padding: 15px; margin-top: 15px; } .sit-section h4 { color: #cc8800; margin-bottom: 10px; font-size: 14px; } .sit-config { font-size: 13px; } .sit-group { margin: 10px 0; padding-left: 15px; } .advanced-rule { background: #e7f3ff; border-left: 4px solid #0078D4; padding: 15px; margin-top: 15px; font-size: 13px; border-radius: 5px; } .advanced-rule strong { color: #0078D4; } .advanced-rule-conditions ul { margin-top: 10px; margin-bottom: 10px; } .advanced-rule-conditions li { margin: 5px 0; } .code { font-family: 'Courier New', monospace; background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 12px; } .toggle-icon { float: right; font-size: 18px; transition: transform 0.2s; } .toggle-icon.expanded { transform: rotate(180deg); } .scope-all { color: #28a745; font-weight: bold; } .scope-specific { color: #ffc107; font-weight: bold; } table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 10px; } table th { background: #e9ecef; padding: 10px; text-align: left; font-weight: 600; color: #495057; } table td { padding: 10px; border-bottom: 1px solid #dee2e6; } .footer { background: #f8f9fa; padding: 20px; text-align: center; font-size: 12px; color: #6c757d; border-top: 1px solid #dee2e6; } </style> </head> <body> <div class="container"> <div class="header"> <h1>$ReportTitle</h1> <div class="timestamp">Generated: $timestamp</div> </div> <div class="summary"> <div class="stat-card"> <div class="label">Total Policies</div> <div class="value">$($Statistics.TotalPolicies)</div> </div> <div class="stat-card"> <div class="label">Enabled</div> <div class="value">$($Statistics.EnabledPolicies)</div> </div> <div class="stat-card"> <div class="label">Disabled</div> <div class="value">$($Statistics.DisabledPolicies)</div> </div> <div class="stat-card"> <div class="label">Total Rules</div> <div class="value">$($Statistics.TotalRules)</div> </div> <div class="stat-card"> <div class="label">Enforce Mode</div> <div class="value">$($Statistics.EnforceMode)</div> </div> <div class="stat-card"> <div class="label">Test Mode</div> <div class="value">$($Statistics.TestMode)</div> </div> </div> <div class="content"> <h2 style="margin-bottom: 20px; color: #495057;">Policy Details</h2> "@ # Add each policy $policyIndex = 0 foreach ($policy in $Policies) { $policyIndex++ # Determine badges $modeBadge = switch ($policy.Mode) { "Enforce" { "badge-enforce" } { $_ -like "*Test*" } { "badge-test" } default { "badge-test" } } $enabledBadge = if ($policy.Enabled) { "badge-enabled" } else { "badge-disabled" } $enabledText = if ($policy.Enabled) { "Enabled" } else { "Disabled" } $portableBadge = if ($policy.IsCrossTenantPortable) { "badge-portable" } else { "badge-not-portable" } $portableText = if ($policy.IsCrossTenantPortable) { "Portable" } else { "Not Portable" } $disabledClass = if (-not $policy.Enabled) { " disabled" } else { "" } # Determine display name for policy $policyDisplayText = if ($policy.PolicyDisplayName -and $policy.PolicyDisplayName -ne $policy.PolicyName) { "$($policy.PolicyDisplayName) <span style='color:#888;font-size:0.85em;'>(Internal: $($policy.PolicyName))</span>" } else { $policy.PolicyName } $html += @" <div class="policy$disabledClass"> <div class="policy-header" onclick="togglePolicy($policyIndex)"> <div class="policy-name"> $policyDisplayText <span class="toggle-icon" id="icon-$policyIndex">▼</span> </div> <div class="policy-meta"> <span class="badge $modeBadge">$($policy.Mode)</span> <span class="badge $enabledBadge">$enabledText</span> <span class="badge $portableBadge">$portableText</span> <span>Rules: $($policy.RuleCount)</span> <span>Priority: $($policy.Priority)</span> </div> </div> <div class="policy-body" id="policy-$policyIndex"> <div class="info-section"> <h3>Policy Information</h3> <div class="info-grid"> <div class="info-label">Policy ID:</div> <div class="info-value code">$($policy.PolicyId)</div> <div class="info-label">Internal Name:</div> <div class="info-value code">$($policy.PolicyName)</div> <div class="info-label">Display Name:</div> <div class="info-value">$(if ($policy.PolicyDisplayName) { $policy.PolicyDisplayName } else { '<i>Same as internal name</i>' })</div> <div class="info-label">Created By:</div> <div class="info-value">$($policy.CreatedBy)</div> <div class="info-label">Created:</div> <div class="info-value">$($policy.CreatedDateTime)</div> <div class="info-label">Modified By:</div> <div class="info-value">$($policy.ModifiedBy)</div> <div class="info-label">Modified:</div> <div class="info-value">$($policy.ModifiedDateTime)</div> <div class="info-label">Distribution:</div> <div class="info-value">$($policy.DistributionStatus)</div> </div> "@ if ($policy.Comment) { $html += @" <div style="margin-top: 10px;"> <strong>Comment:</strong> $($policy.Comment) </div> "@ } $html += "</div>" # Location Scopes $html += @" <div class="info-section"> <h3>Location Scopes</h3> <table> <thead> <tr> <th>Workload</th> <th>Scope</th> <th>Details</th> </tr> </thead> <tbody> "@ # Add location rows $locations = @( @{ Name = "Exchange"; Location = $policy.ExchangeLocation; IsAll = $policy.ExchangeLocationIsAll; Exception = $policy.ExchangeLocationException } @{ Name = "SharePoint"; Location = $policy.SharePointLocation; IsAll = $policy.SharePointLocationIsAll; Exception = $policy.SharePointLocationException } @{ Name = "OneDrive"; Location = $policy.OneDriveLocation; IsAll = $policy.OneDriveLocationIsAll; Exception = $policy.OneDriveLocationException } @{ Name = "Teams"; Location = $policy.TeamsLocation; IsAll = $policy.TeamsLocationIsAll; Exception = $policy.TeamsLocationException } @{ Name = "Endpoint"; Location = $policy.EndpointDlpLocation; IsAll = $policy.EndpointDlpLocationIsAll; Exception = $null } ) foreach ($loc in $locations) { if ($loc.Location) { $scopeClass = if ($loc.IsAll) { "scope-all" } else { "scope-specific" } $scopeText = if ($loc.IsAll) { "All" } else { "Specific" } $details = if ($loc.IsAll) { "All $($loc.Name)" } else { $loc.Location } $html += "<tr><td>$($loc.Name)</td><td class='$scopeClass'>$scopeText</td><td>$details</td></tr>" if ($loc.Exception) { $html += "<tr><td></td><td colspan='2' style='color: #dc3545;'>Exceptions: $($loc.Exception)</td></tr>" } } } $html += @" </tbody> </table> </div> "@ # Rules if ($policy.Rules -and $policy.Rules.Count -gt 0) { $html += @" <div class="info-section"> <h3>Rules ($($policy.Rules.Count))</h3> "@ foreach ($rule in $policy.Rules) { $ruleDisabled = if ($rule.Disabled) { " (Disabled)" } else { "" } $ruleDisabledClass = if ($rule.Disabled) { " disabled" } else { "" } # Determine display name for rule $ruleDisplayText = if ($rule.RuleDisplayName -and $rule.RuleDisplayName -ne $rule.RuleName) { "$($rule.RuleDisplayName) <span style='color:#888;font-size:0.85em;'>(Internal: $($rule.RuleName))</span>" } else { $rule.RuleName } $html += @" <div class="rule$ruleDisabledClass"> <div class="rule-name">$ruleDisplayText$ruleDisabled</div> <div class="rule-details"> "@ if ($rule.Priority) { $html += "<div><strong>Priority:</strong> $($rule.Priority)</div>" } # Show AdvancedRule if it exists (has complete data including confidence/counts) # Otherwise fall back to ContentContainsSensitiveInformation if ($rule.AdvancedRule) { $advHtml = Format-AdvancedRule -AdvancedRuleJson $rule.AdvancedRule -Format "HTML" $html += @" <div class="advanced-rule"> $advHtml </div> "@ } elseif ($rule.ContentContainsSensitiveInformation) { # Only show this if no AdvancedRule (to avoid duplication) $sitHtml = Format-SITConfiguration -SITJson $rule.ContentContainsSensitiveInformation -Format "HTML" $html += @" <div class="sit-section"> <h4>Sensitive Information Types</h4> $sitHtml </div> "@ } if ($rule.NotifyUser) { $html += "<div><strong>Notify User:</strong> $($rule.NotifyUser)</div>" } if ($rule.BlockAccess) { $html += "<div><strong>Block Access:</strong> Yes ($($rule.BlockAccessScope))</div>" } if ($rule.GenerateIncidentReport) { $html += "<div><strong>Incident Report:</strong> $($rule.GenerateIncidentReport) (Severity: $($rule.ReportSeverityLevel))</div>" } $html += @" </div> </div> "@ } $html += "</div>" } $html += @" </div> </div> "@ } # Close HTML $html += @" </div> <div class="footer"> Generated by PurviewDLP PowerShell Module | $timestamp </div> </div> <script> function togglePolicy(index) { const body = document.getElementById('policy-' + index); const icon = document.getElementById('icon-' + index); if (body.classList.contains('expanded')) { body.classList.remove('expanded'); icon.classList.remove('expanded'); } else { body.classList.add('expanded'); icon.classList.add('expanded'); } } </script> </body> </html> "@ return $html } |