Src/Private/Get-AbrPurviewRetentionPolicy.ps1
|
function Get-AbrPurviewRetentionPolicy { <# .SYNOPSIS Used by As Built Report to retrieve Microsoft Purview Retention Policy information. .DESCRIPTION Collects and reports on Retention Policies and Retention Labels configured in Microsoft Purview, including retention durations and actions. .NOTES Version: 0.1.0 Author: Pai Wei Sing .EXAMPLE .LINK #> [CmdletBinding()] param ( [Parameter ( Position = 0, Mandatory)] [string] $TenantId ) begin { Write-PScriboMessage -Message "Collecting Microsoft Purview Retention Policy information for tenant $TenantId." | Out-Null Show-AbrDebugExecutionTime -Start -TitleMessage 'Retention Policies' } process { # Retention Policies try { $RetentionPolicies = Get-RetentionCompliancePolicy -ErrorAction Stop if ($RetentionPolicies) { Section -Style Heading2 'Retention Policies' { #region Coverage Summary $HasPreservationLock = $RetentionPolicies | Where-Object { $_.RestrictiveRetention } $HasAdaptiveScope = $RetentionPolicies | Where-Object { $_.AdaptiveScopeLocation } $CovObj = [System.Collections.ArrayList]::new() $_pre_RetentionPoliciesCon_43 = if ($RetentionPolicies.Count -gt 0) { 'Yes' } else { 'No' } $_pre_HasPreservationLockP_44 = if (@($HasPreservationLock).Count -gt 0) { 'Yes' } else { 'No' } $_pre_UsesAdaptiveScopes_45 = if (@($HasAdaptiveScope).Count -gt 0) { 'Yes' } else { 'No' } $covInObj = [ordered] @{ 'Retention Policies Configured' = $_pre_RetentionPoliciesCon_43 'Has Preservation Lock Policy' = $_pre_HasPreservationLockP_44 'Uses Adaptive Scopes' = $_pre_UsesAdaptiveScopes_45 } $CovObj.Add([pscustomobject]$covInObj) | Out-Null if ($Healthcheck -and $script:HealthCheck.Purview.Retention) { $CovObj | Where-Object { $_.'Retention Policies Configured' -eq 'No' } | Set-Style -Style Critical | Out-Null } $CovTableParams = @{ Name = "Retention Coverage Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($script:Report.ShowTableCaptions) { $CovTableParams['Caption'] = "- $($CovTableParams.Name)" } $CovObj | Table @CovTableParams #endregion $OutObj = [System.Collections.ArrayList]::new() foreach ($Policy in $RetentionPolicies) { try { $_pre_Enabled_67 = if ($Policy.Enabled) { 'Yes' } else { 'No' } $_pre_PreservationLock_72 = if ($Policy.RestrictiveRetention) { 'Yes' } else { 'No' } $inObj = [ordered] @{ 'Name' = $Policy.Name 'Enabled' = $_pre_Enabled_67 'Retention Action' = $script:TextInfo.ToTitleCase($Policy.RetentionAction) 'Retention Duration'= $Policy.RetentionDuration 'Workload' = ($Policy.Workload -join ', ') 'Adaptive Scope' = $Policy.AdaptiveScopeLocation 'Preservation Lock' = $_pre_PreservationLock_72 'Created' = $Policy.WhenCreated.ToString('yyyy-MM-dd') } $OutObj.Add([pscustomobject]$inObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "Retention Policy '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } } if ($Healthcheck -and $script:HealthCheck.Purview.Retention) { $OutObj | Where-Object { $_.'Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null } $TableParams = @{ Name = "Retention Policies - $TenantId" List = $false ColumnWidths = 18, 8, 13, 13, 16, 10, 12, 10 } if ($script:Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams #region ACSC Inline Check — Retention Policies if ($script:InfoLevel.Retention -ge 3) { $CoversKeyWorkloads = [bool]($RetentionPolicies | Where-Object { ($_.Workload -join ',') -match 'Exchange' -and ($_.Workload -join ',') -match 'SharePoint' -and ($_.Workload -join ',') -match 'OneDrive' }) $HasPreservationLock = [bool]($RetentionPolicies | Where-Object { $_.RestrictiveRetention }) Write-AbrPurviewACSCCheck -TenantId $TenantId -SectionName 'Retention Policies' -Checks @( [pscustomobject]@{ ControlId = 'ISM-1511' E8 = 'E8 Backup ML1' Description = 'Backups of important data retained and restorable' Check = 'Retention policies covering Exchange, SharePoint, and OneDrive' Status = if ($CoversKeyWorkloads) { 'Pass' } elseif ($RetentionPolicies.Count -gt 0) { 'Partial' } else { 'Fail' } } [pscustomobject]@{ ControlId = 'ISM-1515' E8 = 'E8 Backup ML2' Description = 'Backups cannot be modified or deleted by unprivileged users' Check = 'Preservation Lock (RestrictiveRetention) applied to at least one retention policy' Status = if ($HasPreservationLock) { 'Pass' } else { 'Fail' } } ) } #endregion # Retention Rules per policy if ($script:InfoLevel.Retention -ge 2) { foreach ($Policy in $RetentionPolicies) { try { $Rules = Get-RetentionComplianceRule -Policy $Policy.Name -ErrorAction SilentlyContinue if ($Rules) { Section -ExcludeFromTOC -Style NOTOCHeading3 "Rules: $($Policy.Name)" { $RuleObj = [System.Collections.ArrayList]::new() foreach ($Rule in $Rules) { try { $ruleInObj = [ordered] @{ 'Rule Name' = $Rule.Name 'Retention Duration' = $Rule.RetentionDuration 'Retention Action' = $script:TextInfo.ToTitleCase($Rule.RetentionComplianceAction) 'Content Match Query' = $Rule.ContentMatchQuery } $RuleObj.Add([pscustomobject]$ruleInObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "Retention Rule '$($Rule.Name)': $($_.Exception.Message)" | Out-Null } } $RuleTableParams = @{ Name = "Retention Rules - $($Policy.Name)" List = $false ColumnWidths = 28, 18, 18, 36 } if ($script:Report.ShowTableCaptions) { $RuleTableParams['Caption'] = "- $($RuleTableParams.Name)" } $RuleObj | Sort-Object -Property 'Rule Name' | Table @RuleTableParams } } } catch { Write-PScriboMessage -IsWarning -Message "Retention Rules for '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } } } } } else { Write-PScriboMessage -Message "No Retention Policy information found for $TenantId. Disabling section." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "Retention Policy Section: $($_.Exception.Message)" | Out-Null } # Retention Labels try { $RetentionLabels = Get-ComplianceTag -ErrorAction Stop if ($RetentionLabels) { Section -Style Heading2 'Retention Labels' { #region Coverage Summary $HasRecordLabels = $RetentionLabels | Where-Object { $_.IsRecordLabel } $HasRegulatoryRecords = $RetentionLabels | Where-Object { $_.RegulatoryRecord } $HasDispositionReview = $RetentionLabels | Where-Object { $_.ReviewerEmail } $HasEventBasedLabels = $RetentionLabels | Where-Object { $_.EventType } $LabelCovObj = [System.Collections.ArrayList]::new() $_pre_RetentionLabelsConfi_184 = if ($RetentionLabels.Count -gt 0) { 'Yes' } else { 'No' } $_pre_RecordLabelsConfigur_185 = if (@($HasRecordLabels).Count -gt 0) { 'Yes' } else { 'No' } $_pre_RegulatoryRecordsCon_186 = if (@($HasRegulatoryRecords).Count -gt 0) { 'Yes' } else { 'No' } $_pre_DispositionReviewCon_187 = if (@($HasDispositionReview).Count -gt 0) { 'Yes' } else { 'No' } $_pre_EventBasedRetentionL_188 = if (@($HasEventBasedLabels).Count -gt 0) { 'Yes' } else { 'No' } $labelCovInObj = [ordered] @{ 'Retention Labels Configured' = $_pre_RetentionLabelsConfi_184 'Record Labels Configured' = $_pre_RecordLabelsConfigur_185 'Regulatory Records Configured' = $_pre_RegulatoryRecordsCon_186 'Disposition Review Configured' = $_pre_DispositionReviewCon_187 'Event-Based Retention Labels' = $_pre_EventBasedRetentionL_188 } $LabelCovObj.Add([pscustomobject]$labelCovInObj) | Out-Null if ($Healthcheck -and $script:HealthCheck.Purview.Retention) { $LabelCovObj | Where-Object { $_.'Retention Labels Configured' -eq 'No' } | Set-Style -Style Warning | Out-Null } $LabelCovTableParams = @{ Name = "Retention Label Coverage - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($script:Report.ShowTableCaptions) { $LabelCovTableParams['Caption'] = "- $($LabelCovTableParams.Name)" } $LabelCovObj | Table @LabelCovTableParams #endregion $OutObj = [System.Collections.ArrayList]::new() foreach ($Label in $RetentionLabels) { try { $_pre_RecordLabel_215 = if ($Label.IsRecordLabel) { 'Yes' } else { 'No' } $_pre_Regulatory_216 = if ($Label.RegulatoryRecord) { 'Yes' } else { 'No' } $_pre_DispositionReview_217 = if ($null -ne $Label.ReviewerEmail -and $Label.ReviewerEmail -ne '') { 'Yes' } else { 'No' } $_pre_EventType_218 = if ($Label.EventType) { $Label.EventType } else { '--' } $inObj = [ordered] @{ 'Name' = $Label.Name 'Retention Action' = $script:TextInfo.ToTitleCase($Label.RetentionAction) 'Retention Duration' = $Label.RetentionDuration 'Retention Type' = $script:TextInfo.ToTitleCase($Label.RetentionType) 'Record Label' = $_pre_RecordLabel_215 'Regulatory' = $_pre_Regulatory_216 'Disposition Review' = $_pre_DispositionReview_217 'Event Type' = $_pre_EventType_218 'Created' = $Label.WhenCreated.ToString('yyyy-MM-dd') } $OutObj.Add([pscustomobject]$inObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "Retention Label '$($Label.Name)': $($_.Exception.Message)" | Out-Null } } $TableParams = @{ Name = "Retention Labels - $TenantId" List = $false ColumnWidths = 18, 13, 13, 12, 9, 10, 10, 8, 7 } if ($script:Report.ShowTableCaptions) { $TableParams['Caption'] = "- $($TableParams.Name)" } $OutObj | Sort-Object -Property 'Name' | Table @TableParams } } else { Write-PScriboMessage -Message "No Retention Label information found for $TenantId. Disabling section." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "Retention Label Section: $($_.Exception.Message)" | Out-Null } #region Mailbox Archiving (MCCA check-IG101) try { # Get-EXOMailbox is faster/safer than Get-Mailbox at scale; fall back to Get-Mailbox $AllMailboxes = $null $ArchivedCount = 0 $TotalCount = 0 try { $AllMailboxes = Get-EXOMailbox -ResultSize Unlimited -PropertySets Archive -ErrorAction Stop } catch { $AllMailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue } if ($AllMailboxes) { $TotalCount = @($AllMailboxes).Count $ArchivedCount = @($AllMailboxes | Where-Object { $_.ArchiveStatus -eq 'Active' }).Count $AutoExpandCount = @($AllMailboxes | Where-Object { $_.AutoExpandingArchiveEnabled -eq $true }).Count $ArchivePct = if ($TotalCount -gt 0) { [math]::Round(($ArchivedCount / $TotalCount) * 100, 1) } else { 0 } Section -Style Heading2 'Mailbox Archiving' { Paragraph "In-Place Archive (online archive) extends mailbox storage and supports long-term retention. MCCA recommends enabling archiving across all mailboxes to support information governance policies." BlankLine $_pre_TotalMailboxes = $TotalCount $_pre_ArchiveEnabled = $ArchivedCount $_pre_ArchivePct = "$ArchivePct%" $_pre_AutoExpanding = $AutoExpandCount $_pre_ArchiveStatus = if ($ArchivePct -ge 90) { 'Good' } elseif ($ArchivePct -ge 50) { 'Partial' } else { 'Low Coverage' } $archInObj = [ordered] @{ 'Total Mailboxes' = $_pre_TotalMailboxes 'Mailboxes with Archive Enabled' = $_pre_ArchiveEnabled 'Archive Coverage' = $_pre_ArchivePct 'Auto-Expanding Archive Enabled' = $_pre_AutoExpanding 'Coverage Status' = $_pre_ArchiveStatus } $ArchObj = [System.Collections.ArrayList]::new() $ArchObj.Add([pscustomobject]$archInObj) | Out-Null if ($Healthcheck -and $script:HealthCheck.Purview.Retention) { $ArchObj | Where-Object { $ArchivePct -lt 50 } | Set-Style -Style Critical | Out-Null $ArchObj | Where-Object { $ArchivePct -lt 90 -and $ArchivePct -ge 50 } | Set-Style -Style Warning | Out-Null } $ArchTableParams = @{ Name = "Mailbox Archiving Coverage - $TenantId"; List = $true; ColumnWidths = 45, 55 } if ($script:Report.ShowTableCaptions) { $ArchTableParams['Caption'] = "- $($ArchTableParams.Name)" } $ArchObj | Table @ArchTableParams if ($script:InfoLevel.Retention -ge 3) { Write-AbrPurviewACSCCheck -TenantId $TenantId -SectionName 'Mailbox Archiving' -Checks @( [pscustomobject]@{ ControlId = 'ISM-1511' E8 = 'N/A' Description = 'Long-term email retention supported through online archiving' Check = 'In-Place Archive enabled on 90%+ of mailboxes' Status = if ($ArchivePct -ge 90) { 'Pass' } elseif ($ArchivePct -ge 50) { 'Partial' } else { 'Fail' } } ) } } } else { Write-PScriboMessage -Message "No mailbox information found for $TenantId. Disabling mailbox archive section." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "Mailbox Archiving Section: $($_.Exception.Message)" | Out-Null } #endregion #region MRM Policies — Exchange-native Messaging Records Management (MCCA check-IG102) try { $MRMPolicies = Get-RetentionPolicy -ErrorAction SilentlyContinue if ($MRMPolicies) { Section -Style Heading2 'Exchange MRM Retention Policies' { Paragraph "Exchange Messaging Records Management (MRM) retention policies are Exchange-native and separate from Microsoft Purview unified retention. Many tenants run both systems in parallel during migration." BlankLine $MRMObj = [System.Collections.ArrayList]::new() foreach ($Policy in $MRMPolicies) { try { $Tags = @() try { $Tags = Get-RetentionPolicyTag -RetentionPolicy $Policy.Name -ErrorAction SilentlyContinue } catch {} $_pre_IsDefault = if ($Policy.IsDefault) { 'Yes' } else { 'No' } $_pre_TagCount = $Tags.Count $_pre_RetainAged = if ($Tags | Where-Object { $_.RetentionAction -eq 'MoveToArchive' }) { 'Yes' } else { 'No' } $_pre_Delete = if ($Tags | Where-Object { $_.RetentionAction -eq 'DeleteAndAllowRecovery' -or $_.RetentionAction -eq 'PermanentlyDelete' }) { 'Yes' } else { 'No' } $mrmInObj = [ordered] @{ 'Policy Name' = $Policy.Name 'Is Default Policy' = $_pre_IsDefault 'Retention Tags' = $_pre_TagCount 'Has Archive Tag' = $_pre_RetainAged 'Has Delete Tag' = $_pre_Delete } $MRMObj.Add([pscustomobject]$mrmInObj) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "MRM Policy '$($Policy.Name)': $($_.Exception.Message)" | Out-Null } } if ($Healthcheck -and $script:HealthCheck.Purview.Retention) { # Flag if only the default policy exists and no custom MRM policies are configured $CustomMRM = @($MRMPolicies | Where-Object { -not $_.IsDefault }) if ($CustomMRM.Count -eq 0) { $MRMObj | Set-Style -Style Warning | Out-Null } } $MRMTableParams = @{ Name = "Exchange MRM Retention Policies - $TenantId"; List = $false; ColumnWidths = 36, 16, 14, 17, 17 } if ($script:Report.ShowTableCaptions) { $MRMTableParams['Caption'] = "- $($MRMTableParams.Name)" } $MRMObj | Sort-Object -Property 'Policy Name' | Table @MRMTableParams if ($script:InfoLevel.Retention -ge 3) { $_hasMRMCustom = (@($MRMPolicies | Where-Object { -not $_.IsDefault })).Count -gt 0 Write-AbrPurviewACSCCheck -TenantId $TenantId -SectionName 'Exchange MRM Policies' -Checks @( [pscustomobject]@{ ControlId = 'ISM-1511' E8 = 'N/A' Description = 'Email lifecycle management enforced via retention policies' Check = 'At least one custom Exchange MRM retention policy configured' Status = if ($_hasMRMCustom) { 'Pass' } else { 'Partial' } } ) } } } else { Write-PScriboMessage -Message "No Exchange MRM Retention Policy information found for $TenantId." | Out-Null } } catch { Write-PScriboMessage -IsWarning -Message "MRM Retention Policy Section: $($_.Exception.Message)" | Out-Null } #endregion } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Retention Policies' } } |