Овладение PowerShell Graph API: Простые и понятные идеи

Служба Microsoft Graph API позволяет читать, изменять и управлять практически всеми аспектами Azure AD и Office 365 с использованием одного единственного конечного точка REST API. В этой статье вы узнаете, как преобразовать ваш API в PowerShell Graph API.

Предварительные требования

Если вы хотите идти в ногу со мной в этой статье, убедитесь, что вы соответствуете следующим критериям:

  • Запущен Windows PowerShell 5.1 (С этой версией я проводил тестирование. Другие версии могут работать, но не гарантируются)
  • Наличие арендатора Azure
  • Аутентификация в Azure от учетной записи с глобальными правами администратора или правами регистрации приложения на подписку и наличие глобального администратора для подтверждения запросов на регистрацию вашего приложения.

Создание идентификатора приложения для Microsoft Graph API

Для доступа к Microsoft Graph API вам сначала нужен идентификатор для получения токена OAuth. Это в основном выполняется с использованием идентификатора приложения, который вы можете создать в портале Azure. Вы можете создать идентификатор приложения через портал Azure. Для этого:

  • Перейдите в Портал Azure и перейдите в Azure Active Directory.
  • Нажмите на Регистрация приложений в разделе Управление в левом меню и нажмите кнопку Новая регистрация.
Authenticating before creating the PowerShell Graph API
  • Введите имя для вашего приложения и нажмите Зарегистрировать.
  • Скопируйте идентификатор приложения (Application Id) для дальнейшего использования.
Registering an application

Создание секретов для Microsoft Graph API

Вы можете аутентифицироваться в Graph API двумя основными методами: AppId/Secret и аутентификация на основе сертификата. Вам потребуется аутентифицироваться при подключении к Graph API с помощью PowerShell.

Давайте рассмотрим, как аутентифицироваться обоими методами.

AppId/Secret

Идентификатор приложения/секрет похожи на обычное имя пользователя/пароль. Идентификатор приложения состоит из GUID вместо имени пользователя, а пароль – это просто случайная строка.

Чтобы создать секрет, щелкните Certificates & secrets в левом меню и нажмите New client secret.

Creating a client secret

Введите описание для секрета и выберите срок его действия. Теперь просто остается запросить разрешение, чтобы получить доступ к данным, которые вам нужны.

Сертификат

Существует возможность создать самоподписанный сертификат и загрузить его открытый ключ в Azure. Это предпочтительный и более безопасный способ аутентификации.

Сначала вам нужно сгенерировать самоподписанный сертификат. К счастью, это легко сделать с помощью PowerShell.

# Ваше имя арендатора (может быть что-то более описательное также)
$TenantName        = "contoso.onmicrosoft.com"

# Куда экспортировать сертификат без закрытого ключа
$CerOutputPath     = "C:\Temp\PowerShellGraphCert.cer"

# В каком хранилище сертификатов вы хотите его разместить
$StoreLocation     = "Cert:\CurrentUser\My"

# Срок действия нового сертификата
$ExpirationDate    = (Get-Date).AddYears(2)


# Splat для читаемости
$CreateCertificateSplat = @{
    FriendlyName      = "AzureApp"
    DnsName           = $TenantName
    CertStoreLocation = $StoreLocation
    NotAfter          = $ExpirationDate
    KeyExportPolicy   = "Exportable"
    KeySpec           = "Signature"
    Provider          = "Microsoft Enhanced RSA and AES Cryptographic Provider"
    HashAlgorithm     = "SHA256"
}

# Создать сертификат
$Certificate = New-SelfSignedCertificate @CreateCertificateSplat

# Получить путь сертификата
$CertificatePath = Join-Path -Path $StoreLocation -ChildPath $Certificate.Thumbprint

# Экспортировать сертификат без закрытого ключа
Export-Certificate -Cert $CertificatePath -FilePath $CerOutputPath | Out-Null

Теперь загрузите самоподписанный сертификат, который вы экспортировали в $CerOutputPath в ваше приложение Azure, нажав на Сертификаты и секреты в левом меню и нажав на Загрузить сертификат.

Uploading a certificate

Добавление разрешений для приложения

Предоставление правильных разрешений для приложения важно – не только для функциональности вашего приложения, но и для безопасности. Знание этого и (почти) всего остального в API Microsoft Graph можно найти в документации.

Как только я это настрою, я собираюсь собрать все события безопасности из моего арендатора. Для этого мне нужно иметь разрешение как минимум SecurityEvents.Read.All. С его помощью я могу собирать и принимать меры по событиям невозможного перемещения, пользователи, подключающиеся через VPN/TOR и так далее.

Чтобы добавить SecurityEvents.Read.All к вашему приложению – щелкните API Permissions, а затем Add Permission. Это позволит вам не только получить доступ к Graph API, но и к множеству других приложений в Azure. Большинство из этих приложений легко подключить, как только вы узнаете, как подключиться к Microsoft Graph API.

Нажмите Microsoft Graph > Application Permissions > Security Events и установите флажок SecurityEvents.Read.All. После этого нажмите кнопку Add Permission.

Вы видели, что столбец Admin consent required установлен в значение Yes для этого разрешения? Это означает, что администратор арендатора должен утвердить, прежде чем разрешение будет добавлено к приложению.

Tenant admin permission approval

Если вы являетесь глобальным администратором, нажмите Grant admin consent for или попросите глобального администратора утвердить его. Запрос разрешения у пользователя вместо администратора, который просто устанавливает разрешение на чтение/запись, является важной частью аутентификации OAuth. Но это позволяет нам обойти это для большинства разрешений в Microsoft Graph.

Вы наверняка уже видели это на Facebook или Google: “Разрешаете ли вы приложению X получить доступ к вашему профилю?”

Granting admin consent

Теперь вы готовы – давайте войдем и получим некоторые данные!

Получите токен доступа (идентификатор приложения и секрет)

Для этого нам нужно отправить запрос, чтобы получить токен доступа от конечной точки Microsoft Graph OAuth. И в теле этого запроса нам нужно указать:

  • client_id – Идентификатор вашего приложения – закодированный URL
  • client_secret – Секрет вашего приложения – закодированный URL
  • scope – URL-кодированный URL, указывающий, к чему вы хотите получить доступ
  • grant_type – Какой метод аутентификации вы используете

URL для конечной точки: https://login.microsoftonline.com/<tenantname>/oauth2/v2.0/token. Вы можете запросить токен доступа с помощью PowerShell и Graph API, используя следующий фрагмент кода:

# Определите AppId, секрет и область, имя вашего арендатора и URL конечной точки
$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"

# Добавьте System.Web для кодирования URL
Add-Type -AssemblyName System.Web

# Создайте тело
$Body = @{
    client_id = $AppId
	client_secret = $AppSecret
	scope = $Scope
	grant_type = 'client_credentials'
}

# Расширьте параметры для Invoke-Restmethod для более чистого кода
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    # Создайте строку, объединив список параметров с '&'
    Body = $Body
    Uri = $Url
}

# Запросите токен!
$Request = Invoke-RestMethod @PostSplat

Получение токена доступа (использование сертификата)

Аутентификация в Microsoft Graph API с помощью сертификата немного отличается от обычного потока с использованием идентификатора приложения/секрета. Чтобы получить токен доступа с использованием сертификата, вам нужно:

  1. Создайте заголовок Java Web Token (JWT).
  2. Создайте полезную нагрузку JWT.
  3. Подпишите заголовок JWT И полезную нагрузку ранее созданным самоподписанным сертификатом. Это создаст собственный доступный токен, используемый для запроса токена доступа к Microsoft Graph.
  4. Создайте тело запроса, содержащее:
  • client_id=<идентификатор приложения>
  • client_assertion=<JWT>
  • client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • scope=<URL-кодированный область>
  • grant_type=client_credentials
  1. Выполните POST-запрос с телом к конечной точке oauth с Authorization=<JWT> в заголовке.

Как это сделать не было очевидно в документации Microsoft, но вот сценарий PowerShell для его выполнения:

$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"

# Создание хэша сертификата в формате base64
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

# Создание временной метки JWT для истечения срока действия
$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)

# Создание временной метки начала действия JWT
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

# Создание заголовка JWT
$JWTHeader = @{
    alg = "RS256"
    typ = "JWT"
    # Использование хэша сертификата в формате base64 и замена/очистка для соответствия веб-кодированию base64
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}

# Создание тела JWT
$JWTPayLoad = @{
    # Какой конечной точке разрешено использовать этот JWT
    aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"

    # Временная метка истечения срока действия
    exp = $JWTExpiration

    # Издатель = ваше приложение
    iss = $AppId

    # ID JWT: случайный глобально уникальный идентификатор
    jti = [guid]::NewGuid()

    # Не использовать до
    nbf = $NotBefore

    # Подпись JWT
    sub = $AppId
}

# Преобразование заголовка и тела в формат 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)

# Объединение заголовка и тела с помощью "." для создания допустимого (неподписанного) JWT
$JWT = $EncodedHeader + "." + $EncodedPayload

# Получение объекта закрытого ключа вашего сертификата
$PrivateKey = $Certificate.PrivateKey

# Определение алгоритма подписи RSA и хэширования
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

# Создание подписи JWT
$Signature = [Convert]::ToBase64String(
    $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
) -replace '\+','-' -replace '/','_' -replace '='

# Объединение подписи с JWT с помощью "."
$JWT = $JWT + "." + $Signature

# Создание хэша с параметрами тела
$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"

# Использование самоподписанного JWT в качестве авторизации
$Header = @{
    Authorization = "Bearer $JWT"
}

# Развертывание параметров для Invoke-Restmethod для более чистого кода
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'
    Body = $Body
    Uri = $Url
    Headers = $Header
}

$Request = Invoke-RestMethod @PostSplat

Понимание вывода токена запроса

После того как вы получили токен доступа, либо через идентификатор/секрет приложения, либо через сертификат, вы должны увидеть объект с четырьмя свойствами.

  • token_type – Тип токена
  • expires_in – Время в секундах, в течение которого токен доступа действителен
  • ext_expires_in – Аналогично expires_in, но для обеспечения надежности в случае сбоя службы токенов
  • access_token – То, за что мы пришли

Затем вы создадите заголовок, используя token_type и access_token, и начнете делать запросы с помощью PowerShell к API Microsoft Graph.

Делаем запросы к API Microsoft PowerShell Graph

Теперь начнем делать некоторые запросы к API.

Идем по нашему примеру, сначала вам понадобится URL для отображения предупреждений о безопасности. Не забудьте использовать документацию API Microsoft Graph, чтобы узнать, что нужно.

В этом случае вам понадобится заголовок с Authorization=Bearer <access_token> и запрос GET к конечной точке предупреждений Graph API. Вот как это сделать с помощью PowerShell.

# Создаем заголовок
$Header = @{
    Authorization = "$($Request.token_type) $($Request.access_token)"
}

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

# Получаем все предупреждения о безопасности
$SecurityAlertsRequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json"

$SecurityAlerts = $SecurityAlertsRequest.Value

Теперь, если у вас есть какие-либо предупреждения о безопасности в переменной $SecurityAlerts, они должны выглядеть примерно так:

$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

Проверка одного оповещения о безопасности в формате JSON будет выглядеть следующим образом:

"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":  [

                            ]
}

Понимание и управление разбиением вывода API

У Microsoft Graph API есть ограничение на количество элементов, которое он вернет. Это ограничение действует на каждую функцию, но допустим, это 1000 элементов. Это означает, что вы можете получить максимум 1000 элементов в вашем запросе.

Когда это ограничение достигается, для передачи остальных элементов используется разбиение на страницы. Для этого к ответу на ваш запрос добавляется свойство @odata.nextLink. @odata.nextLink содержит URL, по которому можно вызвать следующую страницу вашего запроса.

Вы можете прочитать все элементы, проверив наличие этого свойства и используя цикл:

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

# Получение всех оповещений о безопасности
$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"
}

Вывод

После того, как вы узнали, как аутентифицироваться в Graph API, довольно легко собирать данные из него. Это мощный сервис, который используется гораздо меньше, чем мог бы.

К счастью, уже существует множество модулей для использования Microsoft Graph API, но для удовлетворения собственных потребностей вам может потребоваться создать собственный модуль для этого. Используя навыки, которые вы приобрели в этой статье, вы должны быть на правильном пути.

Для получения дополнительной информации о контроле доступа гостей в Office 365 я написал подробную статью на своем блоге, которую я рекомендую прочитать.

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