Domina el PowerShell Graph API: Ideas fáciles de seguir

La API de Microsoft Graph es un servicio que te permite leer, modificar y gestionar casi todos los aspectos de Azure AD y Office 365 bajo un único punto de conexión de API REST. En este artículo, aprende cómo transformar tu API a la API de PowerShell Graph.

Requisitos previos

Si deseas seguir este artículo, asegúrate de cumplir con los siguientes criterios:

  • Ejecutar Windows PowerShell 5.1 (Esta es la versión con la que se realizó la prueba. Otras versiones podrían funcionar, pero no están garantizadas)
  • Tener un inquilino de Azure
  • Autenticado en Azure con una cuenta con permisos de administrador global o permisos de registro de aplicación en la suscripción y un administrador global para aceptar las solicitudes de registro de tu aplicación.

Crear una identidad de aplicación para la API de Microsoft Graph

Para acceder a la API de Microsoft Graph, primero necesitas una identidad para obtener un token de OAuth. Esto se hace principalmente con una identidad de aplicación que puedes crear en el Portal de Azure. Puedes crear una identidad de aplicación mediante el portal de Azure. Para hacerlo:

  • Ve al Portal de Azure y ve a Azure Active Directory.
  • Haz clic en Registros de aplicaciones bajo Gestionar en el menú de la izquierda y haz clic en el botón Nuevo registro.
Authenticating before creating the PowerShell Graph API
  • Ingresa un nombre para tu aplicación y haz clic en Registrar.
  • Copia el identificador de la aplicación (Application Id) para usarlo más adelante.
Registering an application

Crear secretos para la API de Microsoft Graph.

Puedes autenticarte en la API de Graph con dos métodos principales: AppId/Secret y autenticación basada en certificados. Necesitarás autenticarte al conectarte a la API de Graph con PowerShell.

Vamos a ver cómo autenticar con ambos métodos.

AppId/Secret

Un ID de aplicación/secreto es como un nombre de usuario/contraseña regular. El ID de la aplicación consta de un GUID en lugar de un nombre de usuario y la contraseña es simplemente una cadena aleatoria.

Para crear un secreto, haz clic en Certificados y secretos en el menú izquierdo y presiona en Nuevo secreto del cliente.

Creating a client secret

Ingresa una descripción para el secreto y selecciona cuándo quieres que expire. Ahora solo es cuestión de solicitar permiso para acceder a los datos que necesitas.

Certificado

Existe la posibilidad de crear un certificado autofirmado y cargar su clave pública en Azure. Esta es la forma preferida y más segura de autenticación.

Primero deberás generar un certificado autofirmado. Afortunadamente, esto se hace fácilmente con PowerShell.

# Su nombre de inquilino (puede ser algo más descriptivo también)
$TenantName        = "contoso.onmicrosoft.com"

# Dónde exportar el certificado sin la clave privada
$CerOutputPath     = "C:\Temp\PowerShellGraphCert.cer"

# En qué almacén de certificados desea que esté
$StoreLocation     = "Cert:\CurrentUser\My"

# Fecha de vencimiento del nuevo certificado
$ExpirationDate    = (Get-Date).AddYears(2)


# Splat para legibilidad
$CreateCertificateSplat = @{
    FriendlyName      = "AzureApp"
    DnsName           = $TenantName
    CertStoreLocation = $StoreLocation
    NotAfter          = $ExpirationDate
    KeyExportPolicy   = "Exportable"
    KeySpec           = "Signature"
    Provider          = "Microsoft Enhanced RSA and AES Cryptographic Provider"
    HashAlgorithm     = "SHA256"
}

# Crear certificado
$Certificate = New-SelfSignedCertificate @CreateCertificateSplat

# Obtener ruta del certificado
$CertificatePath = Join-Path -Path $StoreLocation -ChildPath $Certificate.Thumbprint

# Exportar certificado sin clave privada
Export-Certificate -Cert $CertificatePath -FilePath $CerOutputPath | Out-Null

Ahora, cargue el certificado autofirmado que exportó a $CerOutputPath en su aplicación de Azure haciendo clic en Certificados y secretos en el menú de la izquierda y presionando en Cargar certificado.

Uploading a certificate

Agregar permisos a la aplicación

Dar a la aplicación los permisos adecuados es importante, no solo para la funcionalidad de su aplicación, sino también para la seguridad. El conocimiento de esto y (casi) todo lo demás en la API de Microsoft Graph se puede encontrar en la documentación.

Una vez que haya configurado esto, voy a recopilar todos los eventos de seguridad de mi inquilino. Para poder hacer eso, necesito SecurityEvents.Read.All como permiso mínimo. Con esto, puedo recopilar y tomar medidas sobre eventos de viajes imposibles, usuarios que se conectan a través de VPN/TOR, y demás.

Para agregar el permiso SecurityEvents.Read.All a tu aplicación, haz clic en Permisos de API y luego en Agregar permiso. Esto te presentará no solo la API de Graph, sino también un montón de otras aplicaciones en Azure. La mayoría de estas aplicaciones son fáciles de conectar una vez que sabes cómo conectarte a la API de Microsoft Graph.

Haz clic en Microsoft Graph > Permisos de aplicación > Eventos de seguridad y marca el SecurityEvents.Read.All. Después, presiona el botón Agregar permiso.

¿Viste que la columna Consentimiento de administrador requerido estaba configurada como en ese permiso? Esto significa que un administrador del inquilino debe aprobar antes de que se agregue el permiso a la aplicación.

Tenant admin permission approval

Si eres un administrador global, presiona el Dar consentimiento de administrador para o pídele a un Administrador Global que lo apruebe. Pedir permiso al usuario en lugar de que un administrador simplemente configure permisos de lectura/escritura es una gran parte de la autenticación de OAuth. Pero esto nos permite evitarlo para la mayoría de los permisos en Microsoft Graph.

Probablemente ya hayas visto esto en Facebook o Google: “¿Permites que la aplicación X acceda a tu perfil?”

Granting admin consent

Ahora estás listo; ¡vamos a iniciar sesión y obtener algunos datos!

Adquiera un Token de Acceso (ID de Aplicación y Secreto)

Para esto, necesitaremos enviar una solicitud para obtener un token de acceso desde un punto de conexión de Microsoft Graph OAuth. Y en el cuerpo de esa solicitud necesitamos proporcionar:

  • client_id – Su ID de Aplicación – codificado en URL
  • client_secret – El secreto de su aplicación – codificado en URL
  • scope – Una URL codificada que especifique lo que desea acceder
  • grant_type – Qué método de autenticación está utilizando

La URL para el punto de conexión es https://login.microsoftonline.com/<tenantname>/oauth2/v2.0/token. Puede solicitar un token de acceso con PowerShell y la API de Graph utilizando el fragmento de código a continuación.

 # Definir AppId, secreto y scope, el nombre de su inquilino y la URL del punto de conexión 
$AppId = '2d10909e-0396-49f2-ba2f-854b77c1e45b'
$AppSecret = 'abcdefghijklmnopqrstuv12345'
$Scope = "https://graph.microsoft.com/.default"
$TenantName = "contoso.onmicrosoft.com"

$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

 # Agregar System.Web para urlencode 
Add-Type -AssemblyName System.Web

 # Crear cuerpo 
$Body = @{
    client_id = $AppId
	client_secret = $AppSecret
	scope = $Scope
	grant_type = 'client_credentials'
}

 # Splat los parámetros para Invoke-Restmethod para un código más limpio 
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
     # Crear cadena uniendo bodylist con '&' 
    Body = $Body
    Uri = $Url
}

 # ¡Solicitar el token! 
$Request = Invoke-RestMethod @PostSplat

Adquirir un Token de Acceso (Usando un Certificado)

La autenticación en la API de Microsoft Graph con un certificado es un poco diferente al flujo normal de AppId/Secret. Para obtener un token de acceso usando un certificado, tienes que:

  1. Crea un encabezado de Java Web Token (JWT).
  2. Crea un payload de JWT.
  3. Firma el encabezado y el payload del JWT con el certificado auto-firmado previamente creado. Esto creará un token de acceso hecho por ti mismo utilizado para solicitar un token de acceso de Microsoft Graph.
  4. Crea un cuerpo de solicitud que contenga:
  • client_id=<id de aplicación>
  • client_assertion=<el JWT>
  • client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • scope=<Ámbito Codificado por URL>
  • grant_type=client_credentials
  1. Haz una solicitud POST con cuerpo al endpoint de oauth con Authorization=<JWT> en su encabezado.

Cómo hacer esto no era obvio en la documentación de Microsoft pero aquí está el script de PowerShell para hacer que esto suceda:

$TenantName = "<your tenant name>.onmicrosoft.com"
$AppId = "<your application id"
$Certificate = Get-Item Cert:\CurrentUser\My\<self signed and uploaded cert thumbprint>
$Scope = "https://graph.microsoft.com/.default"

# Crear hash base64 del certificado
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

# Crear sello de tiempo JWT para la expiración
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

# Crear sello de tiempo de inicio de validez JWT
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

# Crear encabezado JWT
$JWTHeader = @{
    alg = "RS256"
    typ = "JWT"
    # Utilizar el CertificadoBase64Hash y reemplazar/eliminar para que coincida con la codificación web de base64
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}

# Crear carga útil JWT
$JWTPayLoad = @{
    # ¿Qué endpoint tiene permitido usar este JWT?
    aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"

    # Sello de tiempo de expiración
    exp = $JWTExpiration

    # Emisor = tu aplicación
    iss = $AppId

    # ID JWT: GUID aleatorio
    jti = [guid]::NewGuid()

    # No utilizar antes de
    nbf = $NotBefore

    # Asunto JWT
    sub = $AppId
}

# Convertir encabezado y carga útil a base64
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

$JWTPayLoadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)

# Unir encabezado y carga útil con "." para crear un JWT válido (sin firmar)
$JWT = $EncodedHeader + "." + $EncodedPayload

# Obtener el objeto de clave privada de tu certificado
$PrivateKey = $Certificate.PrivateKey

# Definir algoritmo de firma RSA y hash
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

# Crear una firma del JWT
$Signature = [Convert]::ToBase64String(
    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
) -replace '\+','-' -replace '/','_' -replace '='

# Unir la firma al JWT con "."
$JWT = $JWT + "." + $Signature

# Crear un hash con parámetros del cuerpo
$Body = @{
    client_id = $AppId
    client_assertion = $JWT
    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    scope = $Scope
    grant_type = "client_credentials"

}

$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

# Utilizar el JWT autogenerado como autorización
$Header = @{
    Authorization = "Bearer $JWT"
}

# Separar los parámetros para Invoke-RestMethod para un código más limpio
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $Body
    Uri = $Url
    Headers = $Header
}

$Request = Invoke-RestMethod @PostSplat

Entendiendo la Salida del Token de Acceso

Una vez que hayas obtenido un token de acceso ya sea a través del ID/secreto de la aplicación o mediante un certificado, deberías ver un objeto con cuatro propiedades.

  • token_type – Qué tipo de token es
  • expires_in – Tiempo en segundos que el token de acceso es válido
  • ext_expires_in – Similar a expires_in pero para la resistencia en caso de una interrupción del servicio de tokens
  • access_token – Para lo que vinimos

A continuación, crearás un encabezado usando token_type y access_token y comenzarás a hacer solicitudes con PowerShell a la API de Microsoft Graph.

Haciendo Solicitudes a la API de Gráficos de Microsoft Powershell

Ahora comienza a hacer algunas solicitudes a la API.

Usando nuestro ejemplo, primero necesitarás la URL para listar alertas de seguridad. Recuerda utilizar la documentación de la API de Gráficos de Microsoft para ver qué se necesita.

En este caso, necesitas un encabezado con Authorization=Bearer <access_token> y una solicitud GET hacia el endpoint de Alertas de la API de Gráficos. Así es como se hace eso con PowerShell.

# Crear encabezado
$Header = @{
    Authorization = "$($Request.token_type) $($Request.access_token)"
}

$Uri = "https://graph.microsoft.com/v1.0/security/alerts"

# Obtener todas las alertas de seguridad
$SecurityAlertsRequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json"

$SecurityAlerts = $SecurityAlertsRequest.Value

Ahora, si tienes alguna alerta de seguridad en la variable $SecurityAlerts, debería lucir algo así:

$SecurityAlerts | select eventDateTime,Title

eventDateTime                title
-------------                -----
2019-08-05T17:59:47.6271981Z Atypical travel
2019-08-05T08:23:01.7325708Z Anonymous IP address
2019-08-05T08:23:55.5000456Z Anonymous IP address
2019-08-04T22:06:51.063797Z  Anonymous IP address
2019-08-04T21:56:10.981437Z  Anonymous IP address
2019-08-08T09:30:00Z         Creation of forwarding/redirect rule
2019-07-19T13:30:00Z         eDiscovery search started or exported
2019-07-19T08:00:00Z         eDiscovery search started or exported

Inspeccionar una sola alerta de seguridad como JSON se verá así:

"id":  "censored",
    "azureTenantId":  "censored",
    "azureSubscriptionId":  "censored",
    "riskScore":  null,
    "tags":  [

             ],
    "activityGroupName":  null,
    "assignedTo":  null,
    "category":  "AnonymousLogin",
    "closedDateTime":  null,
    "comments":  [

                 ],
    "confidence":  null,
    "createdDateTime":  "2019-08-08T09:46:59.65722253Z",
    "description":  "Sign-in from an anonymous IP address (e.g. Tor browser, anonymizer VPNs)",
    "detectionIds":  [

                     ],
    "eventDateTime":  "2019-08-08T09:46:59.65722253Z",
    "feedback":  null,
    "lastModifiedDateTime":  "2019-08-08T09:54:30.7256251Z",
    "recommendedActions":  [

                           ],
    "severity":  "medium",
    "sourceMaterials":  [

                        ],
    "status":  "newAlert",
    "title":  "Anonymous IP address",
    "vendorInformation":  {
                              "provider":  "IPC",
                              "providerVersion":  null,
                              "subProvider":  null,
                              "vendor":  "Microsoft"
                          },
    "cloudAppStates":  [

                       ],
    "fileStates":  [

                   ],
    "hostStates":  [

                   ],
    "historyStates":  [

                      ],
    "malwareStates":  [

                      ],
    "networkConnections":  [

                           ],
    "processes":  [

                  ],
    "registryKeyStates":  [

                          ],
    "triggers":  [

                 ],
    "userStates":  [
                       {
                           "aadUserId":  "censored",
                           "accountName":  "john.doe",
                           "domainName":  "contoso.com",
                           "emailRole":  "unknown",
                           "isVpn":  null,
                           "logonDateTime":  "2019-08-08T09:45:59.6174156Z",
                           "logonId":  null,
                           "logonIp":  "censored",
                           "logonLocation":  "Denver, Colorado, US",
                           "logonType":  null,
                           "onPremisesSecurityIdentifier":  null,
                           "riskScore":  null,
                           "userAccountType":  null,
                           "userPrincipalName":  "[email protected]"
                       }
                   ],
    "vulnerabilityStates":  [

                            ]
}

Comprensión y gestión de la paginación de salida de la API

La API de Microsoft Graph tiene un límite por función en la cantidad de elementos que devolverá. Este límite es por función, pero digamos que son 1000 elementos. Eso significa que solo puedes obtener un máximo de 1000 elementos en tu solicitud.

Cuando se alcanza este límite, se utiliza la paginación para entregar el resto de los elementos. Esto se hace agregando la propiedad @odata.nextLink a la respuesta de tu solicitud. @odata.nextLink contiene una URL que puedes llamar para obtener la siguiente página de tu solicitud.

Puedes leer todos los elementos revisando esta propiedad y usando un bucle:

$Uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns"

# Obtener todas las alertas de seguridad
$AuditLogRequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json"

$AuditLogs = @()
$AuditLogs+=$AuditLogRequest.value

while($AuditLogRequest.'@odata.nextLink' -ne $null) {
    $AuditLogRequest += Invoke-RestMethod -Uri $AuditLogRequest.'@odata.nextLink' -Headers $Header -Method Get -ContentType "application/json"
}

Conclusión

Después de aprender cómo autenticarse en la API de Graph, es bastante fácil recopilar datos de ella. Es un servicio potente que se utiliza mucho menos de lo que debería.

Afortunadamente, ya hay muchos módulos disponibles para utilizar la API de Microsoft Graph, pero para satisfacer tus propias necesidades, es posible que necesites crear tu propio módulo para ello. Usando las habilidades que has aprendido en este artículo, deberías estar bien encaminado.

Para obtener más información sobre cómo controlar el acceso de invitados en Office 365, escribí un artículo detallado en mi blog que te animo a echar un vistazo.

Source:
https://adamtheautomator.com/powershell-graph-api/