Src/Private/Get-AbrPurviewDLPPolicy.ps1
|
function Get-AbrPurviewDLPPolicy { <# .SYNOPSIS Used by As Built Report to retrieve Microsoft Purview Data Loss Prevention policy information. .DESCRIPTION Collects and reports on DLP Compliance Policies and their associated rules configured in Microsoft Purview, including coverage summary, locations, conditions, actions, notifications, and incident reports. .NOTES Version: 0.1.0 Author: Pai Wei Sing .EXAMPLE Get-AbrPurviewDLPPolicy -TenantId 'contoso.onmicrosoft.com' #> [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string]$TenantId ) begin { Write-PScriboMessage -Message "Collecting Microsoft Purview DLP Policy information for tenant $TenantId." | Out-Null Show-AbrDebugExecutionTime -Start -TitleMessage 'DLP Policies' } process { try { $DLPPolicies = Get-DlpCompliancePolicy -ErrorAction Stop if ($DLPPolicies) { Section -Style Heading2 'Data Loss Prevention Policies' { #region Pre-scan all rules for flags and SIT coverage $AllRules = [System.Collections.ArrayList]::new() $HasEnforcedPolicies = $false $HasEndpointDlp = $false $UsesUserNotifications= $false $UsesBlockingRules = $false $HasEphiPolicy = $false $HasPiiPolicy = $false $HasFinancialPolicy = $false $HasCopilotDlpPolicy = $false foreach ($Policy in $DLPPolicies) { if ($Policy.Mode -eq 'Enable') { $HasEnforcedPolicies = $true } if ($Policy.Workload -join ',' -match 'Endpoint') { $HasEndpointDlp = $true } if ($Policy.AdaptiveScopes -or ($Policy.Workload -join ',' -match 'Copilot')) { $HasCopilotDlpPolicy = $true } try { $Rules = Get-DlpComplianceRule -Policy $Policy.Name -ErrorAction SilentlyContinue foreach ($Rule in $Rules) { $AllRules.Add($Rule) | Out-Null if ($Rule.NotifyUser) { $UsesUserNotifications = $true } if ($Rule.BlockAccess) { $UsesBlockingRules = $true } # Check SIT names from ContentContainsSensitiveInformation foreach ($sit in $Rule.ContentContainsSensitiveInformation) { if ($sit.Name -match 'HIPAA|Health') { $HasEphiPolicy = $true } if ($sit.Name -match 'Credit Card') { $HasFinancialPolicy = $true } if ($sit.Name -match 'Social Security|SSN|Tax') { $HasPiiPolicy = $true } } } } catch { } } #endregion #region Summary Table $OutObj = [System.Collections.ArrayList]::new() foreach ($Policy in $DLPPolicies) { try { $_pre_Enabled_79 = if ($Policy.Enabled) { 'Yes' } else { 'No' } $inObj = [ordered] @{ 'Name' = $Policy.Name 'Mode' = switch ($Policy.Mode) { 'Enable' { 'On (Enforced)' } 'Disable' { 'Off (Disabled)' } 'TestWithNotifications' { 'Test with notifications' } 'TestWithoutNotifications' { 'Test without notifications' } default { $script:TextInfo.ToTitleCase($Policy.Mode) } } 'Enabled' = $_pre_Enabled_79 'Workload' = ($Policy.Workload -join ', ') 'Created' = $Policy.WhenCreated.ToString('yyyy-MM-dd') 'Last Modified' = $Policy.WhenChanged.ToString('yyyy-MM-dd') } $OutObj.Add([pscustomobject]$inObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "DLP Policy '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } } if ($Healthcheck -and $script:HealthCheck.Purview.DLP) { $OutObj | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null $OutObj | Where-Object { $_.'Mode' -notmatch 'Enforced' } | Set-Style -Style Warning | Out-Null } $TableParams = @{ Name = "DLP Policies - $TenantId"; List = $false; ColumnWidths = 25, 18, 8, 21, 14, 14 } if ($script:Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams #endregion #region Coverage Summary (HealthCheck flags) $CovObj = [System.Collections.ArrayList]::new() $_pre_HasEnforcedPoliciesM_104 = if ($HasEnforcedPolicies) { 'Yes' } else { 'No' } $_pre_HasEndpointDLPCovera_106 = if ($HasEndpointDlp) { 'Yes' } else { 'No' } $_pre_HasCopilotDLPCoverag_108 = if ($HasCopilotDlpPolicy) { 'Yes' } else { 'No' } $_pre_UsesUserNotification_110 = if ($UsesUserNotifications) { 'Yes' } else { 'No' } $_pre_UsesBlockingRules_112 = if ($UsesBlockingRules) { 'Yes' } else { 'No' } $_pre_CoversHealthePHIData_114 = if ($HasEphiPolicy) { 'Yes' } else { 'No' } $_pre_CoversPIIDataSSNTax_116 = if ($HasPiiPolicy) { 'Yes' } else { 'No' } $_pre_CoversFinancialDataC_118 = if ($HasFinancialPolicy) { 'Yes' } else { 'No' } $covInObj = [ordered] @{ 'Has Enforced Policies (Mode: On)' = $_pre_HasEnforcedPoliciesM_104 'Has Endpoint DLP Coverage' = $_pre_HasEndpointDLPCovera_106 'Has Copilot DLP Coverage' = $_pre_HasCopilotDLPCoverag_108 'Uses User Notifications' = $_pre_UsesUserNotification_110 'Uses Blocking Rules' = $_pre_UsesBlockingRules_112 'Covers Health / ePHI Data (HIPAA)' = $_pre_CoversHealthePHIData_114 'Covers PII Data (SSN / Tax)' = $_pre_CoversPIIDataSSNTax_116 'Covers Financial Data (Credit Card)' = $_pre_CoversFinancialDataC_118 } $CovObj.Add([pscustomobject]$covInObj) | Out-Null if ($Healthcheck -and $script:HealthCheck.Purview.DLP) { $CovObj | Where-Object { $_.'Has Enforced Policies (Mode: On)' -eq 'No' } | Set-Style -Style Critical | Out-Null $CovObj | Where-Object { $_.'Has Endpoint DLP Coverage' -eq 'No' } | Set-Style -Style Warning | Out-Null $CovObj | Where-Object { $_.'Has Copilot DLP Coverage' -eq 'No' } | Set-Style -Style Warning | Out-Null $CovObj | Where-Object { $_.'Uses Blocking Rules' -eq 'No' } | Set-Style -Style Warning | Out-Null } $CovTableParams = @{ Name = "DLP Coverage Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($script:Report.ShowTableCaptions) { $CovTableParams['Caption'] = "- $($CovTableParams.Name)" } $CovObj | Table @CovTableParams #endregion #region ACSC Inline Check — DLP Policies if ($script:InfoLevel.DLP -ge 3) { Write-AbrPurviewACSCCheck -TenantId $TenantId -SectionName 'DLP Policies' -Checks @( [pscustomobject]@{ ControlId = 'ISM-1550' E8 = 'N/A' Description = 'DLP controls preventing unauthorised disclosure of sensitive data' Check = 'At least one DLP policy in enforced (Enable) mode' Status = if ($HasEnforcedPolicies) { 'Pass' } elseif ($DLPPolicies.Count -gt 0) { 'Partial' } else { 'Fail' } } [pscustomobject]@{ ControlId = 'ISM-1550' E8 = 'N/A' Description = 'DLP rules actively block access to sensitive content' Check = 'At least one DLP rule with BlockAccess action configured' Status = if ($UsesBlockingRules) { 'Pass' } else { 'Fail' } } ) } #endregion #region Per-Policy Detail Sections (InfoLevel 2+) if ($script:InfoLevel.DLP -ge 2) { foreach ($Policy in ($DLPPolicies | Sort-Object Name)) { try { # Fetch rules for this policy $DLPRules = Get-DlpComplianceRule -Policy $Policy.Name -ErrorAction SilentlyContinue Section -Style Heading3 $Policy.Name { Paragraph "The $($Policy.Name) DLP policy is configured as follows." BlankLine #region Policy Details $DetObj = [System.Collections.ArrayList]::new() $_pre_Whatinfodoyouwanttop_179 = if ($Policy.Workload) { ($Policy.Workload -join ', ') } else { '--' } $_pre_Description_181 = if ($Policy.Comment) { $Policy.Comment } else { '--' } $detInObj = [ordered] @{ 'What info do you want to protect?' = $_pre_Whatinfodoyouwanttop_179 'Name' = $Policy.Name 'Description' = $_pre_Description_181 'Priority' = $Policy.Priority } $DetObj.Add([pscustomobject]$detInObj) | Out-Null $DetTableParams = @{ Name = "Policy Details - $($Policy.Name)"; List = $true; ColumnWidths = 40, 60 } if ($script:Report.ShowTableCaptions) { $DetTableParams['Caption'] = "- $($DetTableParams.Name)" } $DetObj | Table @DetTableParams #endregion #region Locations $locExchange = if ($Policy.ExchangeLocation) { "Checked ($(if ($Policy.ExchangeLocation.Name -contains 'All') { 'All groups' } else { ($Policy.ExchangeLocation.Name -join ', ') }))" } else { 'Not checked' } $locSharePoint = if ($Policy.SharePointLocation) { "Checked ($(if ($Policy.SharePointLocation.Name -contains 'All') { 'All sites' } else { ($Policy.SharePointLocation.Name -join ', ') }))" } else { 'Not checked' } $locOneDrive = if ($Policy.OneDriveLocation) { "Checked ($(if ($Policy.OneDriveLocation.Name -contains 'All') { 'All accounts' } else { ($Policy.OneDriveLocation.Name -join ', ') }))" } else { 'Not checked' } $locTeams = if ($Policy.TeamsLocation) { "Checked ($(if ($Policy.TeamsLocation.Name -contains 'All') { 'All teams' } else { ($Policy.TeamsLocation.Name -join ', ') }))" } else { 'Not checked' } $locDevices = if ($Policy.EndpointDlpLocation) { "Checked ($(if ($Policy.EndpointDlpLocation.Name -contains 'All') { 'All devices' } else { ($Policy.EndpointDlpLocation.Name -join ', ') }))" } else { 'Not checked' } $LocObj = [System.Collections.ArrayList]::new() $_pre_Onpremisesrepositori_205 = if ($Policy.OnPremisesScannerDlpLocation) { 'Checked' } else { 'Not checked' } $_pre_FabricandPowerBIwork_206 = if ($Policy.PowerBIDlpLocation) { 'Checked' } else { 'N/A' } $_pre_Microsoft365Copilota_207 = if ($Policy.AdaptiveScopes) { 'Checked' } else { 'N/A' } $_pre_Managedcloudapps_208 = if ($Policy.ThirdPartyAppDlpLocation) { 'Checked' } else { 'N/A' } $locInObj = [ordered] @{ 'Exchange email' = $locExchange 'SharePoint sites' = $locSharePoint 'OneDrive accounts' = $locOneDrive 'Teams and channel messages' = $locTeams 'Devices' = $locDevices 'On-premises repositories' = $_pre_Onpremisesrepositori_205 'Fabric and Power BI workspaces' = $_pre_FabricandPowerBIwork_206 'Microsoft 365 Copilot and Copilot Chat' = $_pre_Microsoft365Copilota_207 'Managed cloud apps' = $_pre_Managedcloudapps_208 } $LocObj.Add([pscustomobject]$locInObj) | Out-Null $LocTableParams = @{ Name = "Locations - $($Policy.Name)"; List = $true; ColumnWidths = 40, 60 } if ($script:Report.ShowTableCaptions) { $LocTableParams['Caption'] = "- $($LocTableParams.Name)" } $LocObj | Table @LocTableParams #endregion #region Policy Settings - Rules Table try { if ($DLPRules) { $RuleObj = [System.Collections.ArrayList]::new() foreach ($Rule in ($DLPRules | Where-Object { -not $_.Disabled } | Sort-Object Name)) { try { #-- Conditions -- $ConditionParts = [System.Collections.ArrayList]::new() # SIT-based conditions (direct property) foreach ($sit in $Rule.ContentContainsSensitiveInformation) { $sitDetail = "Contains SIT: $($sit.Name)" if ($sit.minCount) { $sitDetail += " (min count: $($sit.minCount))" } if ($sit.confidenceLevel) { $sitDetail += " [confidence: $($sit.confidenceLevel)]" } $ConditionParts.Add($sitDetail) | Out-Null } # Sensitivity label conditions from AdvancedRule JSON if ($Rule.AdvancedRule) { try { $AdvJson = $Rule.AdvancedRule | ConvertFrom-Json -ErrorAction SilentlyContinue $nodes = [System.Collections.Queue]::new() $nodes.Enqueue($AdvJson) while ($nodes.Count -gt 0) { $node = $nodes.Dequeue() if ($node.PSObject.Properties['Operator']) { foreach ($sub in $node.SubConditions) { $nodes.Enqueue($sub) } } if ($node.ConditionName -eq 'ContentContainsSensitivityLabel') { foreach ($lv in $node.Value) { $ConditionParts.Add("Content Contains Sensitivity Label: $lv") | Out-Null } } if ($node.ConditionName -eq 'ContentContainsSensitiveInformation') { foreach ($sitNode in $node.Value) { $sitDetail = "Contains SIT: $($sitNode.name)" if ($sitNode.minCount) { $sitDetail += " (min: $($sitNode.minCount))" } if ($sitNode.confidenceLevel) { $sitDetail += " [confidence: $($sitNode.confidenceLevel)]" } $ConditionParts.Add($sitDetail) | Out-Null } } } } catch { } } if ($Rule.SentTo) { $ConditionParts.Add("Sent to: $($Rule.SentTo -join ', ')") | Out-Null } if ($Rule.SentToMemberOf) { $ConditionParts.Add("Sent to member of: $($Rule.SentToMemberOf -join ', ')") | Out-Null } if ($Rule.ContentPropertyContainsWords) { $ConditionParts.Add("Property contains: $($Rule.ContentPropertyContainsWords -join ', ')") | Out-Null } $ConditionsDisplay = if ($ConditionParts.Count -gt 0) { $ConditionParts -join "`n" } else { '--' } #-- Actions -- $ActionParts = [System.Collections.ArrayList]::new() if ($Rule.BlockAccess) { $scope = if ($Rule.BlockAccessScope) { " ($($script:TextInfo.ToTitleCase($Rule.BlockAccessScope)))" } else { '' } $ActionParts.Add("Block Access$scope") | Out-Null } if ($Rule.SetHeader) { foreach ($h in $Rule.SetHeader) { $ActionParts.Add("Set header: $($h.Name):$($h.Value)") | Out-Null } } if ($Rule.ModifySubject) { foreach ($ms in $Rule.ModifySubject) { $ActionParts.Add("Modify subject, remove text that matches: $($ms.SearchString)`nInsert replacement text: $($ms.ReplaceString)`nPosition: $($ms.Position)") | Out-Null } } if ($Rule.NotifyUser) { $ActionParts.Add("Notify user: $($Rule.NotifyUser -join ', ')") | Out-Null } if ($Rule.GenerateIncidentReport) { $ActionParts.Add("Generate incident report to: $($Rule.IncidentReportContent -join ', ')") | Out-Null } if ($Rule.ExceptIfHeaderMatchesPatterns) { foreach ($ex in $Rule.ExceptIfHeaderMatchesPatterns) { $ActionParts.Add("Except if header matches: $($ex.Name):$($ex.Value -join '|')") | Out-Null } } $ActionsDisplay = if ($ActionParts.Count -gt 0) { $ActionParts -join "`n" } else { 'Audit Only' } $ruleInObj = [ordered] @{ 'Rule' = $Rule.Name 'Conditions' = $ConditionsDisplay 'Actions' = $ActionsDisplay } $RuleObj.Add([pscustomobject]$ruleInObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "DLP Rule '$($Rule.Name)': $($_.Exception.Message)" | Out-Null } } if ($RuleObj.Count -gt 0) { $RuleTableParams = @{ Name = "Policy Settings - $($Policy.Name)"; List = $false; ColumnWidths = 28, 36, 36 } if ($script:Report.ShowTableCaptions) { $RuleTableParams['Caption'] = "- $($RuleTableParams.Name)" } $RuleObj | Table @RuleTableParams } } } catch { Write-PScriboMessage -IsWarning -Message "DLP Rules for '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } #endregion #region User Notifications $NotifObj = [System.Collections.ArrayList]::new() $_pre_Usenotificationstoin_327 = if ($DLPRules | Where-Object { $_.NotifyUser }) { 'On' } else { 'Off' } $notifInObj = [ordered] @{ 'Use notifications to inform your users and help educate them on the proper use of sensitive info.' = $_pre_Usenotificationstoin_327 } $NotifObj.Add([pscustomobject]$notifInObj) | Out-Null $NotifTableParams = @{ Name = "User Notifications - $($Policy.Name)"; List = $true; ColumnWidths = 70, 30 } if ($script:Report.ShowTableCaptions) { $NotifTableParams['Caption'] = "- $($NotifTableParams.Name)" } $NotifObj | Table @NotifTableParams #endregion #region Incident Reports $IncObj = [System.Collections.ArrayList]::new() $FirstRule = $DLPRules | Where-Object { $_.ReportSeverityLevel } | Select-Object -First 1 $firstRuleSev = if ($FirstRule) { $script:TextInfo.ToTitleCase($FirstRule.ReportSeverityLevel) } else { 'Low' } $_pre_Sendanalerttoadminsw_342 = if ($DLPRules | Where-Object { $_.AlertProperties }) { 'On' } else { 'Off' } $_pre_Useemailincidentrepo_343 = if ($DLPRules | Where-Object { $_.GenerateIncidentReport }) { 'On' } else { 'Off' } $incInObj = [ordered] @{ 'Use this severity level in admin alerts and reports:' = $firstRuleSev 'Send an alert to admins when a rule match occurs.' = $_pre_Sendanalerttoadminsw_342 'Use email incident reports to notify you when a policy match occurs.' = $_pre_Useemailincidentrepo_343 } $IncObj.Add([pscustomobject]$incInObj) | Out-Null $IncTableParams = @{ Name = "Incident Reports - $($Policy.Name)"; List = $true; ColumnWidths = 70, 30 } if ($script:Report.ShowTableCaptions) { $IncTableParams['Caption'] = "- $($IncTableParams.Name)" } $IncObj | Table @IncTableParams #endregion #region Additional Options $AddObj = [System.Collections.ArrayList]::new() $StopProcessing = $DLPRules | Where-Object { $_.StopPolicyProcessing } | Select-Object -First 1 $_preStopProc = if ($StopProcessing) { 'Checked' } else { 'Not checked' } $addInObj = [ordered] @{ "If there's a match for this rule, stop processing additional DLP policies and rules." = $_preStopProc } $AddObj.Add([pscustomobject]$addInObj) | Out-Null $AddTableParams = @{ Name = "Additional Options - $($Policy.Name)"; List = $true; ColumnWidths = 70, 30 } if ($script:Report.ShowTableCaptions) { $AddTableParams['Caption'] = "- $($AddTableParams.Name)" } $AddObj | Table @AddTableParams #endregion #region Policy Mode $ModeObj = [System.Collections.ArrayList]::new() $modeInObj = [ordered] @{ 'Policy mode' = switch ($Policy.Mode) { 'Enable' { 'On (Enabled)' } 'Disable' { 'Off (Disabled)' } 'TestWithNotifications' { 'Test mode with notifications' } 'TestWithoutNotifications' { 'Test mode without notifications' } default { $script:TextInfo.ToTitleCase($Policy.Mode) } } } $ModeObj.Add([pscustomobject]$modeInObj) | Out-Null $ModeTableParams = @{ Name = "Policy Mode - $($Policy.Name)"; List = $true; ColumnWidths = 40, 60 } if ($script:Report.ShowTableCaptions) { $ModeTableParams['Caption'] = "- $($ModeTableParams.Name)" } $ModeObj | Table @ModeTableParams #endregion } } catch { Write-PScriboMessage -IsWarning -Message "DLP Policy Detail '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } } } #endregion #region DLP Category Gap Analysis — MCCA ImprovementActions XML (InfoLevel 3) # Uses DLPImprovementActions.xml to evaluate coverage by category (Financial, PII, ePHI etc.) # filtered to SITs actually relevant to the tenant's detected geo regions. if ($script:InfoLevel.DLP -ge 3) { try { # --- Build SIT -> workload coverage map from live policies --- $SITCoverage = @{} $WLKeys = @('Exchange','SharePoint','OneDrive','Teams','Endpoint','Copilot') $ActivePolicies = $DLPPolicies | Where-Object { $_.Mode -ne 'PendingDeletion' } foreach ($Policy in $ActivePolicies) { $WL = ($Policy.Workload -join ',').ToLower() $coveredWL = @() if ($WL -match 'exchange') { $coveredWL += 'Exchange' } if ($WL -match 'sharepoint') { $coveredWL += 'SharePoint' } if ($WL -match 'onedrive') { $coveredWL += 'OneDrive' } if ($WL -match 'teams') { $coveredWL += 'Teams' } if ($WL -match 'endpoint|device') { $coveredWL += 'Endpoint' } if ($WL -match 'copilot|thirdparty') { $coveredWL += 'Copilot' } if ($coveredWL.Count -eq 0) { if ($Policy.ExchangeLocation) { $coveredWL += 'Exchange' } if ($Policy.SharePointLocation) { $coveredWL += 'SharePoint' } if ($Policy.OneDriveLocation) { $coveredWL += 'OneDrive' } if ($Policy.TeamsLocation) { $coveredWL += 'Teams' } if ($Policy.EndpointDlpLocation) { $coveredWL += 'Endpoint' } if ($Policy.ThirdPartyAppDlpLocation) { $coveredWL += 'Copilot' } } $Rules = Get-DlpComplianceRule -Policy $Policy.Name -ErrorAction SilentlyContinue foreach ($Rule in ($Rules | Where-Object { $_.ContentContainsSensitiveInformation })) { foreach ($sit in $Rule.ContentContainsSensitiveInformation) { $sitName = if ($sit.name) { $sit.name } elseif ($sit -is [string]) { $sit } else { "$sit" } if (-not $SITCoverage.ContainsKey($sitName)) { $SITCoverage[$sitName] = @{} foreach ($wl in $WLKeys) { $SITCoverage[$sitName][$wl] = $false } } foreach ($wl in $coveredWL) { if ($WLKeys -contains $wl) { $SITCoverage[$sitName][$wl] = $true } } } } } # --- Load MCCA improvement actions XML --- $XMLPath = Join-Path $PSScriptRoot 'Data\DLPImprovementActions.xml' if (-not (Test-Path $XMLPath)) { $XMLPath = Join-Path (Split-Path $PSScriptRoot) 'Private\Data\DLPImprovementActions.xml' } if (Test-Path $XMLPath) { [xml]$ImprovementXML = Get-Content $XMLPath -Raw -Encoding UTF8 # Use tenant country-derived geos (set at startup from Get-MgOrganization) # Fall back to INTL-only if not available $TenantGeos = if ($script:TenantGeos -and $script:TenantGeos.Count -gt 0) { $script:TenantGeos } else { @('INTL') } Section -Style Heading3 'DLP Category Gap Analysis' { Paragraph "The following analysis evaluates DLP coverage against Microsoft recommended categories. Each table shows relevant Sensitive Information Types (SITs) for the detected geo regions ($($TenantGeos -join ', ')) and their workload coverage status." BlankLine foreach ($action in $ImprovementXML.ImprovementActions.ActionItem) { $category = $action.Category $importance = $action.Importance # Filter SITs to tenant-relevant geos only $relevantSITs = $action.SITs.SIT | Where-Object { $TenantGeos -contains $_.Geo } if (-not $relevantSITs) { continue } $catCovered = 0 $catTotal = 0 $CatObj = [System.Collections.ArrayList]::new() foreach ($sit in $relevantSITs) { $sitName = $sit.'#text' $catTotal++ $covered = $SITCoverage.ContainsKey($sitName) if ($covered) { $catCovered++ } $wlMap = if ($covered) { $SITCoverage[$sitName] } else { @{} } $sitInObj = [ordered] @{ 'Sensitive Info Type' = $sitName 'Geo' = $sit.Geo 'In Policy' = if ($covered) { 'Yes' } else { 'No' } 'Exchange' = if ($covered -and $wlMap['Exchange']) { 'Yes' } else { 'No' } 'SharePoint' = if ($covered -and $wlMap['SharePoint']) { 'Yes' } else { 'No' } 'OneDrive' = if ($covered -and $wlMap['OneDrive']) { 'Yes' } else { 'No' } 'Teams' = if ($covered -and $wlMap['Teams']) { 'Yes' } else { 'No' } 'Endpoint' = if ($covered -and $wlMap['Endpoint']) { 'Yes' } else { 'No' } } $CatObj.Add([pscustomobject]$sitInObj) | Out-Null } $coveragePct = if ($catTotal -gt 0) { [math]::Round(($catCovered / $catTotal) * 100) } else { 0 } Section -Style NOTOCHeading5 "$category ($catCovered / $catTotal covered — $coveragePct%)" { Paragraph $importance BlankLine if ($Healthcheck -and $script:HealthCheck.Purview.DLP) { $CatObj | Where-Object { $_.'In Policy' -eq 'No' } | Set-Style -Style Critical | Out-Null $CatObj | Where-Object { $_.'In Policy' -eq 'Yes' -and ($_.'Teams' -eq 'No' -or $_.'Endpoint' -eq 'No') } | Set-Style -Style Warning | Out-Null } $CatTableParams = @{ Name = "DLP Coverage - $category - $TenantId" List = $false ColumnWidths = 35, 6, 9, 9, 9, 9, 8, 8, 7 } if ($script:Report.ShowTableCaptions) { $CatTableParams['Caption'] = "- $($CatTableParams.Name)" } $CatObj | Table @CatTableParams } } } } else { Write-PScriboMessage -IsWarning -Message "DLPImprovementActions.xml not found at $XMLPath — skipping category analysis." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "DLP Category Gap Analysis: $($_.Exception.Message)" | Out-Null } } #endregion } } else { Write-PScriboMessage -Message "No DLP Policy information found for $TenantId. Disabling section." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "DLP Policy Section: $($_.Exception.Message)" | Out-Null } } end { Show-AbrDebugExecutionTime -End -TitleMessage 'DLP Policies' } } |