Saltar al contenido
Microsoft scripts

Scripts de Entra

Licencias sin usar

Compara licencias compradas contra asignadas por SKU y lista a los usuarios deshabilitados que siguen con licencia. Te dice cuánto puedes reasignar antes de comprar más.

Antes de correrloLa lista incluye licencias heredadas de grupos: en ese caso el ajuste se hace en el grupo. Revisa qué pasa con los datos del usuario antes de quitarle la licencia. Compradas solo cuenta unidades habilitadas, así que SinAsignar puede salir negativo en una suscripción suspendida o en advertencia.

Ejemplo en México

Cadena de tiendas en León, 12 sucursales y 260 personas

Viene la renovación anual y Compras pide justificar las 300 licencias, mientras varias sucursales tuvieron recortes de personal.

.\get-licencias-sin-usar.ps1 -RutaCsv .\licencias.csv

Encuentra 22 licencias sin asignar y 17 en usuarios deshabilitados. Reasigna lo que hace falta y renueva 39 licencias menos.

Cómo implementarlo

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lector global.
  3. Filtra el CSV por la columna Hallazgo: un tipo es el resumen de SKU y el otro son los usuarios deshabilitados.
  4. Quita las licencias que ya no se ocupen y ajusta la cantidad en la siguiente renovación.
Ver el código completo de get-licencias-sin-usar.ps1
<#
.SYNOPSIS
    ¿Cuántas licencias compraste contra cuántas asignaste, y qué usuarios deshabilitados siguen con licencia?

.DESCRIPTION
    Compara las unidades compradas y asignadas de cada SKU y lista a los usuarios deshabilitados con licencia.
    Te ayuda a reasignar lo que ya pagas antes de comprar más o a ajustar la renovación.
    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-licencias-sin-usar.ps1 -RutaCsv .\licencias.csv

.NOTES
    Permisos: LicenseAssignment.Read.All, User.Read.All
    Rol mínimo: Lector global (Global Reader)
    Licencia: Microsoft Entra ID Free
    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'
$permisos = @('LicenseAssignment.Read.All', 'User.Read.All')

if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
    throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
Connect-MgGraph -Scopes $permisos -NoWelcome

function Get-TodasLasPaginas {
    param([string]$Uri)
    while ($Uri) {
        $pagina = Invoke-MgGraphRequest -Method GET -Uri $Uri
        $pagina.value
        $Uri = $pagina.'@odata.nextLink'
    }
}

$base = 'https://graph.microsoft.com/v1.0'
$skus = @(Get-TodasLasPaginas "$base/subscribedSkus")
$nombreSku = @{}
foreach ($s in $skus) { $nombreSku[$s.skuId] = $s.skuPartNumber }

$deshabilitados = Get-TodasLasPaginas "$base/users?`$filter=accountEnabled eq false&`$select=displayName,userPrincipalName,assignedLicenses&`$top=999"

$filas = @(
    foreach ($s in $skus) {
        $compradas = [int]$s.prepaidUnits.enabled
        $asignadas = [int]$s.consumedUnits
        [pscustomobject]@{
            Hallazgo   = 'Resumen de SKU'
            Sku        = $s.skuPartNumber
            Compradas  = $compradas
            Asignadas  = $asignadas
            SinAsignar = $compradas - $asignadas
            EstadoSku  = $s.capabilityStatus
            Nombre     = $null
            Cuenta     = $null
        }
    }
    foreach ($u in $deshabilitados) {
        foreach ($l in $u.assignedLicenses) {
            [pscustomobject]@{
                Hallazgo   = 'Usuario deshabilitado con licencia'
                Sku        = if ($nombreSku[$l.skuId]) { $nombreSku[$l.skuId] } else { $l.skuId }
                Compradas  = $null
                Asignadas  = $null
                SinAsignar = $null
                EstadoSku  = $null
                Nombre     = $u.displayName
                Cuenta     = $u.userPrincipalName
            }
        }
    }
)

$resumen = @($filas | Where-Object Hallazgo -eq 'Resumen de SKU')
Write-Host "SKU revisados: $($resumen.Count). Licencias sin asignar: $(($resumen | Where-Object SinAsignar -gt 0 | Measure-Object SinAsignar -Sum).Sum + 0). Licencias en usuarios deshabilitados: $($filas.Count - $resumen.Count)"
if ($RutaCsv) {
    # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
    foreach ($fila in $filas) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $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
}