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