Private/Analyze/Invoke-PpaLabelAnalyzer.ps1
|
# Invoke-PpaLabelAnalyzer.ps1 - analyzer for section 01 (Sensitivity Labels). # Turns the Get-PpaSensitivityLabels raw shape into the five findings LABELS-01..05 with # statuses per CHECK_CATALOG.md. Pure function of its input (+ an -AsOf date for the # simulation-age remark) so it is unit-testable with no tenant. # ASCII-only source (Windows PowerShell 5.1). Depends on New-PpaFinding.ps1 / New-PpaSection.ps1. Set-StrictMode -Off function ConvertTo-PpaScopeDisplay { # The internal-name -> friendly-name mapping table for sensitivity-label scope # display (Wave 5 cleanup Part 3 formalized the policy). DISPLAY-TIME ONLY: # collectors and snapshots keep the raw canonical ContentType tokens - mapping # earlier would make old raw snapshots diff against new friendly ones, turning # a display tweak into phantom delta churn (guard pinned in Snapshot.Tests.ps1). # Entries are added ONLY for internal values confirmed to render in a real # report - no speculative rows; unconfirmed tokens pass through raw below. # 'Teamwork' -> 'Teams' confirmed on the live TEST report (Wave 5 Part 3). param([string[]]$Tokens) $map = @{ 'File' = 'Files'; 'Email' = 'Emails'; 'Site' = 'Sites'; 'UnifiedGroup' = 'Groups' 'Teamwork' = 'Teams' 'TeamworkChannel' = 'Teams channels'; 'SchematizedData' = 'Schematized data'; 'PurviewAssets' = 'Purview assets' } $out = foreach ($t in @($Tokens)) { if ($map.ContainsKey($t)) { $map[$t] } else { $t } } return (@($out) -join ', ') } function Invoke-PpaLabelAnalyzer { [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Raw, [datetime]$AsOf = (Get-Date), # Parsed Data/license-requirements.json (static annotation map, not detection). $LicenseMap ) $labels = @($Raw.labels.items) $pols = @($Raw.policies.items) $autos = @($Raw.autoLabels.items) $findings = New-Object System.Collections.Generic.List[object] # --- LABELS-01: taxonomy --- $lm01 = @( @{ label = 'Microsoft Purview portal - Information Protection'; url = 'https://purview.microsoft.com'; tag = 'portal' } @{ label = 'Overview of sensitivity labels'; url = 'https://learn.microsoft.com/en-us/purview/sensitivity-labels'; tag = 'docs' } ) if ([string]$Raw.labels.status -ne 'Ok') { # F-001: a failed Get-Label read must not report "0 labels / no taxonomy" as fact. $findings.Add((New-PpaFinding -Id 'LABELS-01' -DomId 'f-lab-1' -Title 'Sensitivity labels not readable this session' -Status 'Verify manually' ` -Whyline 'Sensitivity labels could not be enumerated this session, so the taxonomy was not evaluated - confirm in the Purview portal.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Sensitivity labels', 'Not readable this session') -Status 'Verify manually' -Remark ([string]$Raw.labels.error)))) -LearnMore $lm01)) } elseif ($labels.Count -gt 0) { $byGuid = @{} foreach ($l in $labels) { if ($l.guid) { $byGuid[$l.guid] = $l.name } } $rows01 = foreach ($l in ($labels | Sort-Object priority)) { $isSub = -not [string]::IsNullOrEmpty($l.parentId) $disp = if ($isSub -and $byGuid.ContainsKey($l.parentId)) { "$($byGuid[$l.parentId]) \ $($l.name)" } else { $l.name } New-PpaRow -Cells @($disp, [string]$l.priority, (ConvertTo-PpaScopeDisplay $l.scopes)) -Status 'Informational' -Indent:$isSub } $findings.Add((New-PpaFinding -Id 'LABELS-01' -DomId 'f-lab-1' -Title 'Taxonomy is defined' -Status 'Informational' ` -Whyline 'A clear, ordered taxonomy is the foundation auto-labeling, DLP and retention build on. The tenant defines:' ` -Table (New-PpaTable -Columns @('Label', 'Priority', 'Scope', 'Status') -Rows @($rows01)) -LearnMore $lm01)) } else { $findings.Add((New-PpaFinding -Id 'LABELS-01' -DomId 'f-lab-1' -Title 'No sensitivity label taxonomy defined' -Status 'Improvement' ` -Whyline 'Without a label taxonomy there is no foundation for auto-labeling, DLP or retention to build on.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Sensitivity labels', '0') -Status 'Improvement'))) -LearnMore $lm01)) } # --- LABELS-02: published to users --- $enabledPols = @($pols | Where-Object { $_.enabled }) $lm02 = @(@{ label = 'Create and publish sensitivity labels'; url = 'https://learn.microsoft.com/en-us/purview/create-sensitivity-labels'; tag = 'docs' }) if ([string]$Raw.policies.status -ne 'Ok') { # F-001: a failed Get-LabelPolicy read must not report "not published" as fact. $findings.Add((New-PpaFinding -Id 'LABELS-02' -DomId 'f-lab-2' -Title 'Label policies not readable this session' -Status 'Verify manually' ` -Whyline 'Label publishing could not be enumerated this session - confirm in the Purview portal.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Label policies', 'Not readable this session') -Status 'Verify manually' -Remark ([string]$Raw.policies.error)))) -LearnMore $lm02)) } elseif ($pols.Count -gt 0) { $status02 = if ($enabledPols.Count -gt 0) { 'OK' } else { 'Improvement' } $rows02 = foreach ($p in $pols) { New-PpaRow -Cells @($p.name, (@($p.labels) -join ', '), $p.scope) -Status ($(if ($p.enabled) { 'OK' } else { 'Improvement' })) } $findings.Add((New-PpaFinding -Id 'LABELS-02' -DomId 'f-lab-2' -Title 'Labels are published to users' -Status $status02 ` -Whyline 'Labels only classify content once published to the people creating it.' ` -Table (New-PpaTable -Columns @('Label Policy', 'Labels', 'Assigned To', 'Status') -Rows @($rows02)) -LearnMore $lm02)) } else { $findings.Add((New-PpaFinding -Id 'LABELS-02' -DomId 'f-lab-2' -Title 'Labels are not published to users' -Status 'Improvement' ` -Whyline 'Labels exist but no label policy publishes them, so users cannot apply them.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Label policies', '0') -Status 'Improvement'))) -LearnMore $lm02)) } # --- LABELS-03: auto-labeling --- $lm03 = @( @{ label = 'Microsoft Purview portal - Information Protection'; url = 'https://purview.microsoft.com'; tag = 'portal' } @{ label = 'Compliance Manager - improvement actions'; url = 'https://purview.microsoft.com'; tag = 'portal' } @{ label = 'Overview of sensitivity labels'; url = 'https://learn.microsoft.com/en-us/purview/sensitivity-labels'; tag = 'docs' } @{ label = 'How to apply a sensitivity label to content automatically'; url = 'https://learn.microsoft.com/en-us/purview/apply-sensitivity-label-automatically'; tag = 'docs' } ) $enforcing = @($autos | Where-Object { $_.mode -match '(?i)enforce' }) $simulating = @($autos | Where-Object { $_.mode -match '(?i)test|simul' }) if ([string]$Raw.autoLabels.status -ne 'Ok') { # F-001: a failed Get-AutoSensitivityLabelPolicy read must not report "none" as fact. $findings.Add((New-PpaFinding -Id 'LABELS-03' -DomId 'f-lab-3' -Title 'Auto-labeling not readable this session' -Status 'Verify manually' -Requires (Get-PpaRequirement $LicenseMap 'LABELS-03') ` -Whyline 'Auto-labeling policies could not be enumerated this session - confirm in the Purview portal.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Auto-labeling policies', 'Not readable this session') -Status 'Verify manually' -Remark ([string]$Raw.autoLabels.error)))) -LearnMore $lm03)) } elseif ($autos.Count -eq 0) { $findings.Add((New-PpaFinding -Id 'LABELS-03' -DomId 'f-lab-3' -Title 'No auto-labeling configured' -Status 'Recommendation' -Requires (Get-PpaRequirement $LicenseMap 'LABELS-03') ` -Whyline 'Automatic labeling reduces reliance on users to classify correctly; none is configured.' ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @((New-PpaRow -Cells @('Auto-labeling policies', '0') -Status 'Recommendation'))) -LearnMore $lm03)) } else { $status03 = if ($simulating.Count -gt 0) { 'Improvement' } elseif ($enforcing.Count -gt 0) { 'OK' } else { 'Improvement' } $title03 = if ($simulating.Count -gt 0) { 'Auto-labeling is not enforcing' } else { 'Auto-labeling is enforcing' } $rows03 = foreach ($a in $autos) { $isSim = $a.mode -match '(?i)test|simul' $modeDisp = if ($isSim) { 'Simulation' } elseif ($a.mode -match '(?i)enforce') { 'Enforce' } else { [string]$a.mode } $remarks = New-Object System.Collections.Generic.List[string] if ($isSim -and $a.simulationStartDate) { $start = [datetime]$a.simulationStartDate $days = [int]([math]::Round(($AsOf - $start).TotalDays)) $remarks.Add("running in simulation since $($start.ToString('dd-MMM-yyyy')) ($days days). Simulation shows $('{0:N0}' -f $a.simulationItemCount) items would be labeled - consider turning the policy on.") } # Wave 5 cleanup Part 2: the Conditions cell distinguishes where the # conditions came from - flat property (unchanged passthrough), grouped # AdvancedRule (flat sorted name list + distinct count), present-but- # unparsed, genuinely none, or rule-read-degraded. Items from older # captures carry no conditionsSource marker and keep the legacy joined # rendering exactly. $rowStatus = $(if ($isSim) { 'Improvement' } else { 'OK' }) $condCell = (@($a.sits) -join ', ') $src = '' if ($a.PSObject.Properties.Name -contains 'conditionsSource') { $src = [string]$a.conditionsSource } switch ($src) { 'grouped' { $condCell = $condCell + ' - ' + ([string]@($a.sits).Count) + ' distinct (grouped conditions)' $remarks.Add('Conditions parsed from the grouped-condition rule (AdvancedRule): named sensitive info types and trainable classifiers combined; AND/OR grouping not shown.') } 'unparsed' { $condCell = 'Conditions present - not parsed' $rowStatus = 'Verify manually' $remarks.Add('The policy rule stores grouped conditions (AdvancedRule) in a shape this run could not parse - review the policy conditions in the Purview portal.') } 'none' { $condCell = 'None detected' } 'unreadable' { $condCell = 'Conditions not readable this run' $rowStatus = 'Verify manually' $remarks.Add('The auto-labeling rule read did not complete, so conditions could not be read - review the policy conditions in the Purview portal.') } } $remark = $(if ($remarks.Count -gt 0) { $remarks -join ' ' } else { $null }) New-PpaRow -Cells @($a.name, $condCell, $modeDisp) -Status $rowStatus -Remark $remark } $findings.Add((New-PpaFinding -Id 'LABELS-03' -DomId 'f-lab-3' -Title $title03 -Status $status03 -Requires (Get-PpaRequirement $LicenseMap 'LABELS-03') ` -Whyline 'Automatic labeling reduces reliance on users to classify correctly; in simulation it observes but never applies.' ` -Table (New-PpaTable -Columns @('Auto-labeling Policy', 'Conditions (SITs)', 'Mode', 'Status') -Rows @($rows03)) -LearnMore $lm03)) } # --- LABELS-04: container labels --- $lm04 = @(@{ label = 'Use sensitivity labels to protect containers (groups & sites)'; url = 'https://learn.microsoft.com/en-us/purview/sensitivity-labels-teams-groups-sites'; tag = 'docs' }) $containerLabels = @($labels | Where-Object { $_.scopes -contains 'Site' -or $_.scopes -contains 'UnifiedGroup' }) $c = $Raw.containers $groupCov = if ($c -and $c.groups) { "$($c.groups.labeled) of $($c.groups.total) labeled" } else { $null } $siteCov = if ($c -and $c.sites) { "$($c.sites.labeled) of $($c.sites.total) labeled" } else { $null } if ([string]$Raw.labels.status -ne 'Ok') { # F-001: container labels are read from Get-Label; a failed read is not "none". $findings.Add((New-PpaFinding -Id 'LABELS-04' -DomId 'f-lab-4' -Title 'Container labels not readable this session' -Status 'Verify manually' ` -Whyline 'Container-scoped labels are read from the sensitivity-label set, which could not be enumerated this session - confirm in the Purview portal.' ` -Table (New-PpaTable -Columns @('Container type', 'Coverage', 'Status') -Rows @((New-PpaRow -Cells @('Container-scoped labels', 'Not readable this session') -Status 'Verify manually'))) -LearnMore $lm04)) } elseif ($containerLabels.Count -eq 0) { $rows04 = @( New-PpaRow -Cells @('Microsoft 365 Groups / Teams', ($(if ($groupCov) { $groupCov } else { 'Inventory not collected' }))) -Status ($(if ($groupCov) { 'Recommendation' } else { 'Verify manually' })) New-PpaRow -Cells @('SharePoint sites', ($(if ($siteCov) { $siteCov } else { 'Inventory not collected' }))) -Status ($(if ($siteCov) { 'Recommendation' } else { 'Verify manually' })) ) $findings.Add((New-PpaFinding -Id 'LABELS-04' -DomId 'f-lab-4' -Title 'No container labels for Teams / Sites / Groups' -Status 'Recommendation' ` -Whyline 'Container labels govern guest access, external sharing and unmanaged-device rules on collaborative workspaces.' ` -Table (New-PpaTable -Columns @('Container type', 'Coverage', 'Status') -Rows @($rows04)) -LearnMore $lm04)) } else { $findings.Add((New-PpaFinding -Id 'LABELS-04' -DomId 'f-lab-4' -Title 'Container labels are in use' -Status 'OK' ` -Whyline 'Container labels govern guest access, external sharing and unmanaged-device rules on collaborative workspaces.' ` -Table (New-PpaTable -Columns @('Container type', 'Coverage', 'Status') -Rows @((New-PpaRow -Cells @('Container-scoped labels', "$($containerLabels.Count) defined") -Status 'OK'))) -LearnMore $lm04)) } # --- LABELS-05: Azure Rights Management for Exchange Online (Wave 6 Part 2) --- # $null means "not read this session" (EXO session absent, read failed, or the # property missing) - the absence of the read is never reported as Disabled. # Older raw captures carry no irmConfig block at all: same Verify manually path. $lm05 = @(@{ label = 'Encryption in Microsoft 365'; url = 'https://learn.microsoft.com/en-us/purview/encryption'; tag = 'docs' }) $why05 = 'Azure RMS is the encryption engine behind sensitivity labels and encrypted mail; with it disabled, protection actions across Exchange silently do nothing.' $rmsKnown = ($null -ne $Raw.irmConfig) -and ($null -ne $Raw.irmConfig.azureRmsEnabled) $rmsOn = $rmsKnown -and [bool]$Raw.irmConfig.azureRmsEnabled if (-not $rmsKnown) { $findings.Add((New-PpaFinding -Id 'LABELS-05' -DomId 'f-lab-5' -Title 'Azure Rights Management not readable this session' -Status 'Verify manually' ` -Whyline $why05 ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @( New-PpaRow -Cells @('Azure Rights Management (Exchange Online)', 'Not readable this session (Exchange Online session not established?)') -Status 'Verify manually' )) -LearnMore $lm05)) } elseif ($rmsOn) { $findings.Add((New-PpaFinding -Id 'LABELS-05' -DomId 'f-lab-5' -Title 'Azure Rights Management is enabled for Exchange Online' -Status 'OK' ` -Whyline $why05 ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @( New-PpaRow -Cells @('Azure Rights Management (Exchange Online)', 'Enabled') -Status 'OK' )) -LearnMore $lm05)) } else { $findings.Add((New-PpaFinding -Id 'LABELS-05' -DomId 'f-lab-5' -Title 'Azure Rights Management is disabled for Exchange Online' -Status 'Improvement' ` -Whyline $why05 ` -Table (New-PpaTable -Columns @('Configuration', 'Setting', 'Status') -Rows @( New-PpaRow -Cells @('Azure Rights Management (Exchange Online)', 'Disabled (AzureRMSLicensingEnabled = false)') -Status 'Improvement' New-PpaRow -Cells @('Context', 'Default-on for tenants created after 2018 - a disabled state is usually a deliberate opt-out; confirm intent') -Status 'Informational' )) -LearnMore $lm05)) } # --- section glance --- $mid = Get-PpaMidDot if ([string]$Raw.labels.status -ne 'Ok') { $glance = New-PpaGlance -Name 'Sensitivity Labels' -Metric 'not readable' -Sub 'confirm in portal' } else { $autoState = if ($simulating.Count -gt 0) { 'auto-label in sim' } elseif ($enforcing.Count -gt 0) { 'auto-label on' } else { 'no auto-label' } $glance = New-PpaGlance -Name 'Sensitivity Labels' -Metric "$($labels.Count) labels" -Sub "$($pols.Count) policies $mid $autoState" } return New-PpaSection -Id 'Sensitivity_Labels' -Title 'Sensitivity Labels' -Group 'Microsoft Information Protection' ` -GroupIcon 'fas fa-shield-alt' -Glance $glance -Findings $findings.ToArray() } |