ActionPlans/Start-SPORetentionChecker.ps1
<#
.SYNOPSIS Provide a report for retention policies and their distribution status along with recommendation for each case. .DESCRIPTION Provide a report for retention policies and eDiscovery holds rules affecting SharePoint, OneDrive and Microsoft 365 Groups. Check distribution status for the associated policies. Provide recommended actions and reference given the main scenarios. .EXAMPLE Use a global admin when prompted for a account. .LINK Online documentation: https://aka.ms/O365Troubleshooters/SPORetentionPoliciesTroubleshooter #> Clear-Host Connect-O365PS "SCC" # Create the Export Folder $ts = get-date -Format yyyyMMdd_HHmmss # Create export folder try { $ExportPath = "$global:WSPath\SPOTenantHoldsReport_$ts" mkdir $ExportPath -Force | out-null Write-Log -function "Start-SPORetentionChecker" -step "Create ExportPath" -Description "Success" } catch { Write-Log -function "Start-SPORetentionChecker" -step "Create ExportPath" -Description "Couldn't create folder $global:WSPath\SPOTenantHoldsReport_$ts. Error: $($_.Exception.Message)" Write-Host "Couldn't create folder $global:WSPath\SPOTenantHoldsReport_$ts" Read-Key Start-O365TroubleshootersMenu } #region Get data # Initialization variables $workloads = @("SharePoint","OneDrive","ModernGroup") # SharePoint, OneDrive, ModernGroup (Microsoft 365 Groups) $Report = New-Object -TypeName "System.Collections.ArrayList" # Get the info for each workload specified, then parses each policy in them Foreach ($workload in $workloads){ #Loads the retention policies $Policies = New-Object -TypeName "System.Collections.ArrayList" #$Policies must be reset for each workload and it must be and array $Policies += (Get-ccRetentionCompliancePolicy -ExcludeTeamsPolicy -DistributionDetail | Where-Object {$_.$($workload + "Location") -ne $Null}) #Loads the holds from eDiscovery cases $Policies += Get-ccComplianceCase | ForEach-Object { Get-ccCaseHoldPolicy -Case $_.Identity -DistributionDetail | Where-Object {$_.$($workload + "Location") -ne $Null} } # Parses each policy info ForEach ($P in $Policies) { # Treat the policies where the scope is the whole workloads, and following check for exceptions If ($P.$($workload + "Location").Name -eq "All") { $ReportLine = [PSCustomObject]@{ PolicyName = $P.Name SiteName = "All $workload Sites" Address = "All $workload Sites" Workload = $workload Type = $P.Type Guid = $P.Guid Mode = $P.Mode DistributionStatus = $P.DistributionStatus DistributionResults = $P.DistributionResults Enabled = $P.Enabled } $Report += $ReportLine # Check if the policy have exceptions If ($P.$($workload + "LocationException").count -gt 0) { $Locations = ($P | Select-Object -ExpandProperty $($workload + "LocationException")) ForEach ($L in $Locations) { $Exception = "[EXCLUDE] " + $L.DisplayName $ReportLine = [PSCustomObject]@{ PolicyName = $P.Name SiteName = $Exception Address = $L.Name Workload = $workload Type = $P.Type Guid = $P.Guid Mode = $P.Mode DistributionStatus = $P.DistributionStatus DistributionResults = $P.DistributionResults Enabled = $P.Enabled } $Report += $ReportLine } } } # Treat the policies where the scope is restrict to specific locations in the given workload If ($P.$($workload + "Location").Name -ne "All") { $Locations = ($P | Select-Object -ExpandProperty $($workload + "Location")) ForEach ($L in $Locations) { $ReportLine = [PSCustomObject]@{ PolicyName = $P.Name SiteName = $L.DisplayName Address = $L.Name Workload = $workload Type = $P.Type Guid = $P.Guid Mode = $P.Mode DistributionStatus = $P.DistributionStatus DistributionResults = $P.DistributionResults Enabled = $P.Enabled } $Report += $ReportLine } } } } #endregion Get data #region Treat data # Shape Report Source Objects $HoldsReport = $Report | Select-Object PolicyName, SiteName, Address, Workload, Type, Guid $PoliciesReport = $Report | Sort-Object Guid -Unique | Select-Object PolicyName, Type, Enabled, Guid, Mode, DistributionStatus, DistributionResults # Removes 'DistributionResults' to avoid polluting the report $PoliciesLeanReport = $PoliciesReport | Select-Object PolicyName, Type, Enabled, Guid, Mode, DistributionStatus # Filter SharePoint Holds - SharePoint + M365 groups $SPOReport = New-Object -TypeName "System.Collections.ArrayList" $SPOReport = $HoldsReport | Where-Object {$_.Workload -in @("SharePoint","ModernGroup")} $SPOReport = $SPOReport | Sort-Object -Descending "SiteName" | Sort-Object "PolicyName" # Filter OneDrive Holds - SharePoint and OneDrive except policies expliciting only SharePoint Sites $ODBReport = New-Object -TypeName "System.Collections.ArrayList" $ODBReport = $HoldsReport | Where-Object {$_.Workload -eq "SharePoint" -and $_.Address -notmatch "sharepoint.com"} $ODBReport += $HoldsReport | Where-Object {$_.Workload -eq "SharePoint" -and $_.Address -match "-my.sharepoint.com"} $ODBReport += $HoldsReport | Where-Object {$_.Workload -eq "OneDrive"} $ODBReport = $ODBReport | Sort-Object -Descending "SiteName" | Sort-Object "PolicyName" # Filter Healthy Policies $HealthyPolicies = New-Object -TypeName "System.Collections.ArrayList" $HealthyPolicies += $PoliciesLeanReport | Where-Object { $_.DistributionStatus -eq "Success" -and $_.DistributionResult.count -eq 0 -and $_.Mode -eq "Enforce" } #$Policies[0] | FL # Filter Distribution issues (Mode not: 'Enforce'; Status not: 'Success'; Results not: empty) $DistributionIssues = New-Object -TypeName "System.Collections.ArrayList" $DistributionIssues += $PoliciesReport | Where-Object { $_.DistributionStatus -ne "Success" -or $_.DistributionResult.count -gt 0 -or $_.Mode -ne "Enforce" } #endregion Treat data #region Present data $TheObjectToConvertToHTML = New-Object -TypeName "System.Collections.ArrayList" # Adds general guidance about the report [string]$SectionTitle = "Report Guidance" [string]$Description = 'Consider these principles to interprete the information in this report: <ul style="margin:0 0 0 10px"> <li>A Site/OneDrive explicitly included is protected.</li> <li>A Site/OneDrive included by an "All [workload] Sites" policy is protected if not explicitly excluded by the same policy.</li> <li>Inclusion policies precede exclusion policies.</li> </ul>' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "String" -EffectiveDataString "" $null = $TheObjectToConvertToHTML.Add($SectionHtml) # Adds the session for healthy policies in case there is any If ($HealthyPolicies.Count -gt 0){ [string]$SectionTitle = "Healthy Policies" [string]$Description = 'These policies were checked for common distribution problems and no issue were found.<br> <b>Note:</b> Disabled policies may still enforce holds due to the grace-period. (Learn more about <a href="https://docs.microsoft.com/en-us/microsoft-365/compliance/retention?view=o365-worldwide#releasing-a-policy-for-retention" target="_blank">Grace-Period</a>)' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Green" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $HealthyPolicies -TableType Table $null = $TheObjectToConvertToHTML.Add($SectionHtml) } # Adds the session for distribution issues in case there is any If ($DistributionIssues.Count -gt 0){ # Adds the session for distribution status [string]$SectionTitle = "Policies Distribution Issues" [string]$Description = 'The policies below are in an inconsistent status, review and consider fixing the issues pointed out in <i>DISTRIBUTIONRESULTS</i> column. Then, retry the distribution.<br> Use the following command to retry distribution for a given policy:<br> <div style="margin-left:20px"> <i> Set-RetentionCompliancePolicy -Identity "Hold Everything" -RetryDistribution <br> Set-CaseHoldPolicy -Identity "20ab020d-1c7e-464e-85f7-ef720c41825d" -RetryDistribution </i> <div>' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Red" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $DistributionIssues -TableType Table $null = $TheObjectToConvertToHTML.Add($SectionHtml) } # Adds the session for policies affecting SharePoint [string]$SectionTitle = "SharePoint Holds" If ($SPOReport.Count -gt 0){ [string]$Description = "These are the holds which may be preventing SharePoint files and sites to be deleted.<br> Sites connected to a M365 group are impacted by ModernGroup holds." [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $SPOReport -TableType Table } Else { [string]$Description = 'No retention policy or eDiscovery case hold were found that could be preventing SharePoint files and sites to be deleted.<br> In case you still face issues consider checking retention labels and disabled policies in grace-period. (Learn more about <a href="https://docs.microsoft.com/en-us/microsoft-365/compliance/retention?view=o365-worldwide#retention-policies-and-retention-labels" target="_blank">Retention Labels</a>)' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "String" -EffectiveDataString "" } $null = $TheObjectToConvertToHTML.Add($SectionHtml) # Adds the session for policies affecting OneDrive [string]$SectionTitle = "OneDrive Holds" If ($ODBReport.Count -gt 0){ [string]$Description = "These are the holds which may be preventing OneDrive files and sites to be deleted." [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $ODBReport -TableType Table } Else { [string]$Description = 'No retention policy or eDiscovery case hold were found that could be preventing OneDrive files and sites to be deleted.<br> In case you still face issues consider checking retention labels and disabled policies in grace-period. (Learn more about <a href="https://docs.microsoft.com/en-us/microsoft-365/compliance/retention?view=o365-worldwide#retention-policies-and-retention-labels" target="_blank">Retention Labels</a>)' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "String" -EffectiveDataString "" } $null = $TheObjectToConvertToHTML.Add($SectionHtml) # Adds recomendations to the report [string]$SectionTitle = "Recommendations" [string]$Description = 'To delete sites or their files, you must add an exclusion in all policies affecting the site. Check how to <a href="https://docs.microsoft.com/en-us/sharepoint/troubleshoot/administration/exclude-sites-from-retention-policy" target="_blank">Exclude Sites from Retention Policy</a>. <br> <b>Note:</b> For sites connected to Microsoft 365 group you must also exclude them from the M365 group workload associated.' [PSCustomObject]$SectionHtml = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "String" -EffectiveDataString "" $null = $TheObjectToConvertToHTML.Add($SectionHtml) #Build HTML report out of the previous HTML sections [string]$FilePath = $ExportPath + "\SPOTenantHoldsReport.html" Export-ReportToHTML -FilePath $FilePath -PageTitle "SPO Tenant holds Report" -ReportTitle "SPO Tenant holds Report" -TheObjectToConvertToHTML $TheObjectToConvertToHTML #Ask end-user for opening the HTMl report $OpenHTMLfile = Read-Host "Do you wish to open HTML report file now?`nType Y(Yes) to open or N(No) to exit!" if ($OpenHTMLfile.ToLower() -like "*y*") { Write-Host "Opening report...." -ForegroundColor Cyan Start-Process $FilePath } #endregion Present data # Print location where the data was exported Write-Host "`nOutput was exported in the following location: $ExportPath" -ForegroundColor Yellow Read-Key Start-O365TroubleshootersMenu |