GuíasGuides · Nivel 3Level 3

Microsoft Graph con PowerShell: las primeras consultas y un informe que no existe en el portalMicrosoft Graph with PowerShell: your first queries and a report the portal doesn't have

El SDK, los ámbitos de permiso y por qué pedir los mínimos, Graph Explorer para encontrar la consulta, paginación, y el informe de dispositivos sin check-in con su usuario y su manager en un CSV.The SDK, permission scopes and why to ask for the minimum, Graph Explorer to find the query, paging, and the report of devices with no check-in together with their user and manager in a CSV.

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

pwsh
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 : True

Dos 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 · https://developer.microsoft.com/graph/graph-explorer
GETv1.0https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$filter=lastSyncDateTime le 2026-09-22T00:00:00Z&$select=deviceName,userPrincipalName,operatingSystem,lastSyncDateTime,complianceState&$top=5
Permisos modificadosDeviceManagementManagedDevices.Read.All ✓ consentido
Respuesta 200 OK
{ "value": [
{ "deviceName": "LAPTOP-3K8MQ2", "userPrincipalName": "j.navarro@contoso.com", "operatingSystem": "Windows", "lastSyncDateTime": "2026-08-30T06:12:44Z", "complianceState": "noncompliant" },
{ "deviceName": "Laura's iPhone", "userPrincipalName": "laura.prado@contoso.com", "operatingSystem": "iOS", "lastSyncDateTime": "2026-09-12T19:40:02Z", "complianceState": "compliant" } ] }
Ejecutar consulta
Fragmentos de código

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

pwsh
$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.Count3412

Graph 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

Get-StaleDevices.ps1
#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"
pwsh
./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.com

Esto 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

Entra admin center · Registros de aplicaciones
IdentidadAplicacionesRegistros de aplicacionesNuevo registro
Nombreit-reports-graph
Tipos de cuenta admitidosSolo esta organización
it-reports-graphPermisos de APIAgregar un permisoMicrosoft GraphPermisos de aplicación
User.Read.All
DeviceManagementManagedDevices.Read.All
Conceder consentimiento de administrador para Contoso
it-reports-graphCertificados y secretosCertificadosCargar certificado
Huella digitalA1B2…F0 · Expira: 22/10/2027
pwsh · servidor de tareas
$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).AuthTypeAppOnly

Para 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.All o Mail.ReadWrite "por si acaso".
  • Los -Scopes interactivos 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 -WhatIf primero 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) o curl con 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.

What you'll get

Do with a script what the portal makes you do with clicks: join Intune devices with Entra users and managers, produce a CSV every Monday and, once it is clear, automate it without anyone signing in. It is the foundation of the repos on my GitHub that read the tenant.

Before you start

  • PowerShell 7 (pwsh), on Windows, macOS or Linux.
  • An account with the necessary read roles: Global Reader is enough for everything in this guide.
  • For the final automation, permission to create an app registration (or have the identity admin create it with the permissions you ask for).

1. The SDK and the first connection

pwsh
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     : FinancejobTitle       : AnalystaccountEnabled : True

Two things matter from day one. Install only the modules you need (the full Microsoft.Graph is 40 modules and takes ten minutes). And ask for only the scopes the task needs: -Scopes is what an administrator grants you and what an auditor will see; Directory.ReadWrite.All for a read-only report is what you don't want to explain.

2. Find the query: Graph Explorer

Graph Explorer · https://developer.microsoft.com/graph/graph-explorer
GETv1.0https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$filter=lastSyncDateTime le 2026-09-22T00:00:00Z&$select=deviceName,userPrincipalName,operatingSystem,lastSyncDateTime,complianceState&$top=5
Modify permissionsDeviceManagementManagedDevices.Read.All ✓ consented
Response 200 OK
{ "value": [
{ "deviceName": "LAPTOP-3K8MQ2", "userPrincipalName": "j.navarro@contoso.com", "operatingSystem": "Windows", "lastSyncDateTime": "2026-08-30T06:12:44Z", "complianceState": "noncompliant" },
{ "deviceName": "Laura's iPhone", "userPrincipalName": "laura.prado@contoso.com", "operatingSystem": "iOS", "lastSyncDateTime": "2026-09-12T19:40:02Z", "complianceState": "compliant" } ] }
Run query
Code snippets

Graph Explorer is where you test the URL before writing a line of PowerShell: the Modify permissions tab tells you exactly which scope it needs and Code snippets gives you the equivalent cmdlet. The golden rule: $select always (less data, faster) and $filter on the server, not Where-Object on your machine with 5,000 downloaded devices.

3. Paging: what breaks scripts

pwsh
$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# with the cmdlet, -All walks every page for you:$users = Get-MgUser -All -Property id,userPrincipalName,accountEnabled -PageSize 999$users.Count3412

Graph returns at most 999 objects per call and an @odata.nextLink for the next page. A script that "worked" with 800 users and returns 999 out of 3,412 when the company grows does not error: it gives incomplete data. -All on the cmdlets, or a loop over nextLink with Invoke-MgGraphRequest, is mandatory.

4. The report: devices with no check-in, with user and manager

Get-StaleDevices.ps1
#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) devices with no check-in in $Days days -> $Out"
pwsh
./Get-StaleDevices.ps1 -Days 3047 devices with no check-in in 30 days -> 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.com

This is what the portal does not give you on one screen: the laptop that has not synced for 53 days belongs to a disabled user (a leaver), and the person who has to return it is their manager. The user cache avoids 47 repeated calls for the same manager. The CSV goes to the leavers ticket and, with the Jenkins guide, the script is tested on every change.

5. Automate without a user: app registration

Entra admin center · App registrations
IdentityApplicationsApp registrationsNew registration
Nameit-reports-graph
Supported account typesThis organization only
it-reports-graphAPI permissionsAdd a permissionMicrosoft GraphApplication permissions
User.Read.All
DeviceManagementManagedDevices.Read.All
Grant admin consent for Contoso
it-reports-graphCertificates & secretsCertificatesUpload certificate
ThumbprintA1B2…F0 · Expires: 22/10/2027
pwsh · task server
$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     # this .cer is what you upload to the registrationConnect-MgGraph -ClientId "0a1b…-…" -TenantId "7f1c…-…" -CertificateThumbprint $cert.Thumbprint -NoWelcome(Get-MgContext).AuthTypeAppOnly

For the scheduled task, the script uses a certificate, not a client secret: the private key never leaves the server and there is nothing to copy into a file. Application permissions (not delegated) are what matter here, and the admin consent lands in the audit log. Repo 20 on my GitHub reviews these registrations every month: who has which permission and when the certificate expires.

6. What you review each month

  • IT app registrations: permissions granted versus used (App registrations → the app → API permissions), certificates expiring within 90 days.
  • That no team script uses Directory.ReadWrite.All or Mail.ReadWrite "just in case".
  • The interactive -Scopes you consented to yourself: Enterprise applications → Microsoft Graph Command Line Tools → Permissions; revoke what you don't use.

Next steps

  • Writing, carefully: Update-MgUser, Invoke-MgSyncDeviceManagementManagedDevice (force a check-in on the 47 in the report), always with -WhatIf first and on a small group.
  • Azure Automation or a task on the tools server so the report reaches the Teams channel every Monday.
  • The same queries work from Python (msgraph-sdk) or curl with a token: Graph is a REST API, PowerShell is just the most comfortable door for IT.

Is something missing, or different in your environment? Get in touch and I'll fix it.

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.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →