<# .SYNOPSIS Lista las políticas de retención y las cargas de trabajo que realmente cubren. .DESCRIPTION Usa Get-RetentionCompliancePolicy y Get-AppRetentionCompliancePolicy. Consulta cada política con -DistributionDetail porque sin ese modificador las ubicaciones salen inexactas, y no usa la propiedad Workload porque Microsoft documenta que no refleja dónde aplica. Solo lectura: no cambia nada en el tenant. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\get-politicas-retencion.ps1 -RutaCsv .\retencion.csv .NOTES Permisos: ninguno de Graph; usa ExchangeOnlineManagement. Rol mínimo: rol View-Only Retention Management en un grupo de roles, o grupo Compliance Administrator. Licencia: Microsoft 365 E5/A5/G5/E3/A3/G3, Business Premium u Office 365 E5/A5/G5/E3/A3/G3 (hay otras opciones por carga de trabajo). Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding()] param( [string]$RutaCsv ) $ErrorActionPreference = 'Stop' if (-not (Get-Command Connect-IPPSSession -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser' } function Resumir($valor) { $lista = @($valor | Where-Object { $_ }) if (-not $lista.Count) { return $null } if ("$lista" -match '(^|\s)All($|\s)') { 'todos' } else { "$($lista.Count) específicos" } } function Cargas($p) { $mapa = [ordered]@{ 'Exchange' = $p.ExchangeLocation 'SharePoint' = $p.SharePointLocation 'OneDrive' = $p.OneDriveLocation 'Grupos de Microsoft 365' = $p.ModernGroupLocation 'Carpetas públicas' = $p.PublicFolderLocation 'Skype Empresarial' = $p.SkypeLocation 'Chats de Teams' = $p.TeamsChatLocation 'Canales de Teams' = $p.TeamsChannelLocation } $partes = foreach ($nombre in $mapa.Keys) { $alcance = Resumir $mapa[$nombre] if ($alcance) { "$nombre ($alcance)" } } if ($p.Applications) { $partes = @($partes) + "Aplicaciones ($(@($p.Applications) -join ', '))" } @($partes) -join '; ' } function Detalle($politicas, $comando) { foreach ($p in $politicas) { # Un -Identity inexistente devuelve todas las políticas; por eso se vuelve a filtrar por GUID. $d = & $comando -Identity $p.Guid -DistributionDetail | Where-Object { "$($_.Guid)" -eq "$($p.Guid)" } | Select-Object -First 1 if ($d) { $d } else { $p } } } Connect-IPPSSession -ShowBanner:$false try { $normales = @(Detalle @(Get-RetentionCompliancePolicy) 'Get-RetentionCompliancePolicy') $deApps = @(Detalle @(Get-AppRetentionCompliancePolicy) 'Get-AppRetentionCompliancePolicy') # Una política sin regla no es válida: no retiene nada aunque se vea habilitada. $sinRegla = @{} foreach ($p in $normales) { if (-not @(Get-RetentionComplianceRule -Policy "$($p.Guid)")) { $sinRegla["$($p.Guid)"] = $true } } foreach ($p in $deApps) { if (-not @(Get-AppRetentionComplianceRule -Policy "$($p.Guid)")) { $sinRegla["$($p.Guid)"] = $true } } } finally { Disconnect-ExchangeOnline -Confirm:$false } $filas = @( foreach ($p in $normales + $deApps) { $cargas = Cargas $p [pscustomobject]@{ Politica = $p.Name Habilitada = $p.Enabled Modo = $p.Mode Cargas = if ($cargas) { $cargas } else { 'Sin ubicaciones visibles' } Reglas = if ($sinRegla["$($p.Guid)"]) { 'Sin reglas' } else { 'Con reglas' } Revisar = if ($p.Enabled -eq $false -or $p.Mode -notin @('Enforce', $null) -or -not $cargas -or $sinRegla["$($p.Guid)"]) { 'Sí' } else { 'No' } } } ) Write-Host "Políticas: $($filas.Count). Por revisar: $(@($filas | Where-Object Revisar -eq 'Sí').Count)." if ($RutaCsv) { # UTF-8 con BOM para que Excel en español muestre bien los acentos. $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM Write-Host "CSV guardado en $RutaCsv" } else { $filas | Format-Table -AutoSize }