Saltar al contenido
MS Scripts

Scripts de Exchange

Revisión de buzones compartidos

Lista todos los buzones compartidos con su tamaño, quién tiene FullAccess y SendAs, si tienen licencia y si su inicio de sesión está bloqueado. Señala los que no tienen delegados, los que permiten iniciar sesión y los que pasan de 50 GB.

Solo lecturaPide 1 permisoProbado con respuestas simuladas. Sin ejecutar en un tenant real.

Antes de ejecutarloSin licencia, un compartido se limita a 50 GB; para llegar a 100 GB necesita Exchange Online Plan 2, y Exchange Online Plan 1 no basta. El script ve si hay licencia, pero no cuál: confírmala en el centro de administración. El archivado en línea y la retención por litigio también exigen licencia y aquí no se evalúan. SendOnBehalf no cuenta como delegado en esta revisión: usa get-permisos-delegados para verlo.

Ejemplo en México

Empresa de logística en Monterrey, 120 personas

Hay un buzón compartido por sucursal y por cliente, creados durante años y con mucha rotación de personal. El auditor del corporativo pregunta quién entra a cada uno.

.\get-buzones-compartidos.ps1 -RutaCsv .\compartidos.csv

Obtiene la lista de delegados como evidencia de la revisión, bloquea el inicio de sesión en los que lo permitían y da de baja los que ya no tienen delegados.

Cómo implementarlo

  1. Instala los módulos si no los tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser e Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
  2. Ejecuta el script con -RutaCsv e inicia sesión en Microsoft Graph y en Exchange Online con una cuenta que tenga el rol indicado.
  3. Filtra la columna Alertas en el CSV.
  4. Bloquea el inicio de sesión donde siga permitido, asigna Exchange Online Plan 2 a los que pasan de 50 GB y decide con cada área qué hacer con los que no tienen delegados.
Ver el código completo de get-buzones-compartidos.ps1
<#
.SYNOPSIS
    Responde quién usa cada buzón compartido y cuáles están mal configurados.

.DESCRIPTION
    Lista los buzones compartidos con tamaño, delegados con FullAccess y SendAs, licencia, si su inicio de sesión
    está bloqueado y si su tamaño exige licencia (más de 50 GB). Señala los que no tienen delegados y los que
    permiten inicio de sesión, porque Microsoft indica mantenerlo bloqueado.
    Si un buzón falla, queda en el reporte y el script termina con error al final, después de guardar el CSV.
    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-buzones-compartidos.ps1 -RutaCsv .\compartidos.csv

.NOTES
    Permisos: User.Read.All
    Rol mínimo: Lector global (View-Only Organization Management en Exchange Online).
    Licencia: Exchange Online Plan 1 o superior, incluido en Microsoft 365 Business Premium.
    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'
}
if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
    throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}

# Los tamaños llegan como texto: 1.2 GB (1,288,490,188 bytes). Sin paréntesis (Unlimited o vacío) no hay bytes.
function Get-Bytes($valor) {
    $texto = "$valor".Trim()
    if ($texto -match '\(([\d,. ]+)\s*bytes\)') { return [long]($Matches[1] -replace '\D') }
    if ($texto -match '^0(\.0+)?\s*B$') { return [long]0 }
    $null
}

function Limpiar-Fila($fila) {
    foreach ($p in $fila.PSObject.Properties) {
        if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'$($p.Value)" }
    }
    $fila
}

# 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 }

$filas = [System.Collections.Generic.List[object]]::new()
$fallas = [System.Collections.Generic.List[string]]::new()

Connect-MgGraph -Scopes 'User.Read.All' -NoWelcome
Connect-ExchangeOnline -ShowBanner:$false
try {
    foreach ($b in @(Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -ErrorAction Stop)) {
        try {
            # Sin id, la URI apuntaría a la lista completa de usuarios en lugar de a esta cuenta.
            if (-not $b.ExternalDirectoryObjectId) { throw 'El buzón no trae ExternalDirectoryObjectId' }
            $estadisticas = Get-EXOMailboxStatistics -Identity $b.UserPrincipalName -ErrorAction Stop
            $accesoTotal = @(Get-EXOMailboxPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop |
                Where-Object { "$($_.AccessRights)" -match 'FullAccess' -and $_.IsInherited -ne $true -and $_.Deny -ne $true -and "$($_.User)" -notlike 'NT AUTHORITY\*' } |
                ForEach-Object { "$($_.User)" })
            $enviarComo = @(Get-EXORecipientPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop |
                Where-Object { "$($_.AccessRights)" -match 'SendAs' -and $_.AccessControlType -ne 'Deny' -and $_.IsInherited -ne $true -and "$($_.Trustee)" -notlike 'NT AUTHORITY\*' } |
                ForEach-Object { "$($_.Trustee)" })
            $cuenta = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$($b.ExternalDirectoryObjectId)?`$select=accountEnabled,assignedLicenses"
        } catch {
            $fallas.Add("$($b.PrimarySmtpAddress)")
            $filas.Add([pscustomobject]@{
                Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; TamanoGB = $null; FullAccess = $null; SendAs = $null
                TieneLicencia = $null; InicioSesion = $null; TamanoExigeLicencia = $null; Alertas = "No se pudo consultar: $_"
            })
            continue
        }

        $bytes = Get-Bytes $estadisticas.TotalItemSize
        $conLicencia = @($cuenta.assignedLicenses).Where({ $_ }).Count -gt 0
        $exigeLicencia = $null -ne $bytes -and $bytes -gt 50GB

        $alertas = @(
            if (-not $accesoTotal -and -not $enviarComo) { 'Sin delegados' }
            if ($cuenta.accountEnabled -eq $true) { 'Permite inicio de sesión: bloquéalo' }
            if ($exigeLicencia -and -not $conLicencia) { 'Más de 50 GB sin licencia: asigna Exchange Online Plan 2' }
            if ($exigeLicencia -and $conLicencia) { 'Más de 50 GB: confirma que la licencia sea Exchange Online Plan 2' }
        )
        $filas.Add([pscustomobject]@{
            Buzon               = $b.DisplayName
            Correo              = $b.PrimarySmtpAddress
            TamanoGB            = if ($null -ne $bytes) { [math]::Round($bytes / 1GB, 2) }
            FullAccess          = $accesoTotal -join '; '
            SendAs              = $enviarComo -join '; '
            TieneLicencia       = if ($conLicencia) { 'Sí' } else { 'No' }
            InicioSesion        = switch ($cuenta.accountEnabled) { $true { 'Permitido' } $false { 'Bloqueado' } default { 'Sin dato' } }
            TamanoExigeLicencia = if ($null -eq $bytes) { 'Sin dato' } elseif ($exigeLicencia) { 'Sí' } else { 'No' }
            Alertas             = $alertas -join '; '
        })
    }
} finally {
    Disconnect-ExchangeOnline -Confirm:$false
}

$salida = @($filas | Sort-Object TamanoGB -Descending)
Write-Host "Buzones compartidos: $($salida.Count). Con alguna alerta: $(@($salida | Where-Object Alertas).Count)."
if ($RutaCsv) {
    if ($salida.Count) {
        # UTF-8 con BOM para que Excel en español muestre bien los acentos.
        $salida | ForEach-Object { Limpiar-Fila $_ } | Export-Csv -LiteralPath $RutaCsv -NoTypeInformation -Encoding utf8BOM
    }
    Write-Host "CSV guardado en $RutaCsv"
} else {
    $salida | Format-Table -AutoSize
}

if ($fallas.Count) { throw "No se pudieron consultar $($fallas.Count) buzones: $($fallas -join ', '). Revisa las filas marcadas en el reporte." }

Parte del playbook

Relacionados