<# .SYNOPSIS Encuentra listas de distribución y grupos de seguridad con correo que no tienen dueño o no tienen miembros. .DESCRIPTION Un grupo sin dueño nadie lo mantiene, y uno sin miembros se traga el correo. El reporte da el número de miembros y si el grupo acepta correo de fuera de la organización. Cuenta solo miembros directos, sin expandir grupos anidados. Hace una consulta por grupo. 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-grupos-distribucion-sin-dueno.ps1 -RutaCsv .\grupos-sin-dueno.csv .NOTES Permisos: ninguno de Graph; usa ExchangeOnlineManagement. Rol mínimo: Lector global (o View-Only Recipients en Exchange Online). Licencia: Exchange Online. Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> #Requires -Version 7.0 [CmdletBinding()] param( [string]$RutaCsv ) $ErrorActionPreference = 'Stop' if (-not (Get-Command Connect-ExchangeOnline -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser' } # El CSV se vacía desde el inicio: si algo truena o no hay resultados, no queda uno viejo que parezca actual. if ($RutaCsv) { Set-Content -LiteralPath $RutaCsv -Value $null -Encoding utf8BOM } Connect-ExchangeOnline -ShowBanner:$false try { # RoomList queda fuera: son listas de salas, no grupos de personas. $grupos = @(Get-DistributionGroup -ResultSize Unlimited -IncludeManagedByWithDisplayNames -RecipientTypeDetails MailUniversalDistributionGroup, MailUniversalSecurityGroup, MailNonUniversalGroup -ErrorAction Stop) $n = 0 $filas = @(foreach ($g in $grupos) { $n++ $correo = "$($g.PrimarySmtpAddress)" Write-Progress -Activity 'Revisando grupos' -Status $correo -PercentComplete ($n / $grupos.Count * 100) $duenos = @(@($g.ManagedByWithDisplayNames) | Where-Object { $_ } | ForEach-Object { "$_" }) if (-not $duenos) { $duenos = @(@($g.ManagedBy) | Where-Object { $_ } | ForEach-Object { "$_" }) } $miembros = 'No se pudo consultar' $nota = '' # Sin identidad no se llama: el cmdlet interpretaría un valor vacío como todos los grupos. if ($correo) { try { $miembros = @(Get-DistributionGroupMember -Identity $correo -ResultSize Unlimited -ErrorAction Stop).Count } catch { $nota = "$_" } } else { $nota = 'El grupo no trae correo' } $problemas = @() if (-not $duenos) { $problemas += 'Sin dueño' } if ($miembros -is [int] -and $miembros -eq 0) { $problemas += 'Sin miembros' } if ($miembros -isnot [int]) { $problemas += 'No se pudo consultar' } if (-not $problemas) { continue } [pscustomobject]@{ Nombre = "$($g.DisplayName)" Correo = $correo Tipo = switch ("$($g.RecipientTypeDetails)") { 'MailUniversalSecurityGroup' { 'Seguridad con correo' } 'MailUniversalDistributionGroup' { 'Distribución' } 'MailNonUniversalGroup' { 'No universal' } default { "$_" } } Problema = $problemas -join '. ' Duenos = $duenos -join '; ' Miembros = $miembros AceptaCorreoExterno = if ($g.RequireSenderAuthenticationEnabled -eq $false) { 'Sí' } elseif ($g.RequireSenderAuthenticationEnabled -eq $true) { 'No' } else { 'No determinado' } Nota = $nota } }) Write-Progress -Activity 'Revisando grupos' -Completed } finally { Disconnect-ExchangeOnline -Confirm:$false } $fallas = @($filas | Where-Object { $_.Miembros -eq 'No se pudo consultar' }) Write-Host "Grupos revisados: $($grupos.Count). Sin dueño o sin miembros: $($filas.Count - $fallas.Count). Con falla: $($fallas.Count)" if ($RutaCsv) { # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres. foreach ($f in $filas) { foreach ($p in $f.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'" + $p.Value } } } # 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 } if ($fallas) { throw "No se pudieron consultar los miembros de $($fallas.Count) grupos: $($fallas.Nombre -join ', ')" }