Qué vas a conseguir
Hacer con un script lo que el portal te obliga a hacer a clics: cruzar dispositivos de Intune con usuarios y managers de Entra, sacar un CSV cada lunes y, cuando lo tengas claro, automatizarlo sin que nadie inicie sesión. Es la base de los repos de mi GitHub que leen el tenant.
Antes de empezar
- PowerShell 7 (
pwsh), en Windows, macOS o Linux. - Una cuenta con los roles de lectura necesarios: Global Reader basta para todo lo de esta guía.
- Para la automatización final, permiso para crear un registro de aplicación (o que lo cree el administrador de identidad con los permisos que le pidas).
1. El SDK y la primera conexión
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.DeviceManagement -Scope CurrentUser -ForceConnect-MgGraph -Scopes "User.Read.All","Device.Read.All","DeviceManagementManagedDevices.Read.All" -NoWelcomeGet-MgContext | Select-Object Account, TenantId, @{n="Scopes";e={$_.Scopes -join ", "}}Account TenantId Scopes------- -------- ------inigo.admin@contoso.com 7f1c…-…-… Device.Read.All, DeviceManagementManagedDevices.Read.All, User.Read.All, ...Get-MgUser -UserId laura.prado@contoso.com -Property displayName,department,jobTitle,accountEnabled | Format-List displayName,department,jobTitle,accountEnableddisplayName : Laura Pradodepartment : FinanzasjobTitle : AnalistaaccountEnabled : TrueDos cosas que importan desde el primer día. Instala solo los módulos que necesitas (Microsoft.Graph completo son 40 módulos y tarda diez minutos). Y pide solo los ámbitos que la tarea necesita: -Scopes es lo que un administrador te concede y lo que un auditor verá; Directory.ReadWrite.All para un informe de lectura es lo que no quieres explicar.
2. Encontrar la consulta: Graph Explorer
Graph Explorer es donde pruebas la URL antes de escribir una línea de PowerShell: la pestaña Permisos modificados te dice exactamente qué ámbito necesita y Fragmentos de código te da el cmdlet equivalente. La regla de oro: $select siempre (menos datos, más rápido) y $filter en el servidor, no Where-Object en tu máquina con 5.000 dispositivos descargados.
3. Paginación: lo que rompe los scripts
$r = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,userPrincipalName&`$top=999"$r.value.Count; $r.'@odata.nextLink' -ne $null999True# con el cmdlet, -All recorre todas las páginas por ti:$users = Get-MgUser -All -Property id,userPrincipalName,accountEnabled -PageSize 999$users.Count3412Graph devuelve como mucho 999 objetos por llamada y un @odata.nextLink para la siguiente página. Un script que "funcionaba" con 800 usuarios y devuelve 999 de 3.412 cuando la empresa crece no da error: da datos incompletos. -All en los cmdlets o un bucle sobre nextLink con Invoke-MgGraphRequest es obligatorio.
4. El informe: dispositivos sin check-in, con usuario y manager
#Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Users, Microsoft.Graph.DeviceManagementparam([int]$Days = 30, [string]$Out = "stale-devices-$(Get-Date -Format yyyy-MM-dd).csv") Connect-MgGraph -Scopes "User.Read.All","DeviceManagementManagedDevices.Read.All" -NoWelcome$since = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") $devices = Get-MgDeviceManagementManagedDevice -All -Filter "lastSyncDateTime le $since" ` -Property deviceName,userPrincipalName,operatingSystem,osVersion,lastSyncDateTime,complianceState,managedDeviceOwnerType $cache = @{}$rows = foreach ($d in $devices) { $mgr = $null if ($d.UserPrincipalName) { if (-not $cache.ContainsKey($d.UserPrincipalName)) { $u = Get-MgUser -UserId $d.UserPrincipalName -Property accountEnabled,department -ExpandProperty manager -ErrorAction SilentlyContinue $cache[$d.UserPrincipalName] = $u } $u = $cache[$d.UserPrincipalName] $mgr = $u.Manager.AdditionalProperties.userPrincipalName } [pscustomobject]@{ Device = $d.DeviceName; OS = "$($d.OperatingSystem) $($d.OsVersion)"; Owner = $d.ManagedDeviceOwnerType LastSync = $d.LastSyncDateTime; DaysSilent = [int]((Get-Date) - $d.LastSyncDateTime).TotalDays Compliance = $d.ComplianceState; User = $d.UserPrincipalName; UserEnabled = $u.AccountEnabled Department = $u.Department; Manager = $mgr }}$rows | Sort-Object DaysSilent -Descending | Export-Csv $Out -NoTypeInformation -Encoding utf8"$($rows.Count) dispositivos sin check-in en $Days días -> $Out"./Get-StaleDevices.ps1 -Days 3047 dispositivos sin check-in en 30 días -> stale-devices-2026-10-22.csvImport-Csv .\stale-devices-2026-10-22.csv | Where-Object UserEnabled -eq False | Select-Object Device, User, DaysSilent, Manager | Format-TableDevice User DaysSilent Manager------ ---- ---------- -------LAPTOP-3K8MQ2 j.navarro@contoso.com 53 m.ortega@contoso.comDESKTOP-Q71A p.ruiz@contoso.com 41 m.ortega@contoso.comEsto es lo que el portal no te da en una pantalla: el equipo que lleva 53 días sin sincronizar pertenece a un usuario deshabilitado (una baja), y quien tiene que devolverlo es su manager. La caché de usuarios evita 47 llamadas repetidas al mismo manager. El CSV va al ticket de bajas y, con la guía de Jenkins, el script se prueba en cada cambio.
5. Automatizar sin usuario: registro de aplicación
$cert = New-SelfSignedCertificate -Subject "CN=it-reports-graph" -CertStoreLocation Cert:\CurrentUser\My -KeyExportPolicy NonExportable -NotAfter (Get-Date).AddYears(1)Export-Certificate -Cert $cert -FilePath .\it-reports-graph.cer # este .cer es el que se sube al registroConnect-MgGraph -ClientId "0a1b…-…" -TenantId "7f1c…-…" -CertificateThumbprint $cert.Thumbprint -NoWelcome(Get-MgContext).AuthTypeAppOnlyPara la tarea programada, el script usa certificado, no secreto de cliente: la clave privada no sale del servidor y no hay nada que copiar en un fichero. Los permisos de aplicación (no delegados) son los que importan aquí, y el consentimiento del administrador queda en el registro de auditoría. El repo 20 de mi GitHub revisa cada mes estos registros: quién tiene qué permiso y cuándo caduca el certificado.
6. Lo que revisas cada mes
- Registros de aplicaciones de IT: permisos concedidos frente a los usados (
Registros de aplicaciones → la app → Permisos de API), certificados que caducan en 90 días. - Que ningún script del equipo usa
Directory.ReadWrite.AlloMail.ReadWrite"por si acaso". - Los
-Scopesinteractivos que has consentido tú mismo:Aplicaciones empresariales → Microsoft Graph Command Line Tools → Permisos; revoca lo que no uses.
Siguientes pasos
- Escritura con cuidado:
Update-MgUser,Invoke-MgSyncDeviceManagementManagedDevice(forzar check-in a los 47 del informe) siempre con-WhatIfprimero y en un grupo pequeño. - Azure Automation o una tarea en el servidor de herramientas para que el informe llegue cada lunes al canal de Teams.
- Las mismas consultas sirven desde Python (
msgraph-sdk) ocurlcon un token: Graph es una API REST, PowerShell es solo la puerta más cómoda para IT.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.
Apúntate y llévate las 33 ideas →