Microsoft Entra ID アプリケーションのシークレットや証明書の有効期限を定期的にチェックする
はじめに
前回の記事で Microsoft Graph PowerShell を使用して、Microsoft Entra のアプリ登録のシークレット・証明書とエンタープライズ アプリの SAML 証明書の有効期限を確認する方法を整理しました。今回はこちらを Azure Functions に実装し、Logic Apps から定期的に実行、結果をメールに通知する仕組みを作ります。
Azure Functions の設定
Azure Functions で Microsoft Graph PowerShell を実行するための準備はこちらです。なお、インポートするモジュールは Microsoft.Graph.Applications
です。
以下の Powershell スクリプトを HTTP Trigger の関数で作成します。なお、後続で処理しやすいようにアプトプットを json フォーマットにしています。(エラー ハンドリングはしてません。すいません。)
using namespace System.Net
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
# Graph API PowerShell
# Import-Module Microsoft.Graph.Applications
Connect-MgGraph -Identity
# Get-MgServicePrincipal でアプリ登録の一覧を取得
$Applications = Get-MgApplication -All
# 30 日以内に期限が切れる証明書やシークレットを持つアプリ登録をリストアップ
$body = ""
$Applications | ForEach-Object {
$Application = $_
$ApplicationDisplayName = $Application.DisplayName
$ApplicationKeyCredential = $Application.KeyCredentials
$ApplicationPasswordCredential = $Application.PasswordCredentials
$ApplicationKeyCredential | ForEach-Object {
$KeyCredential = $_
$KeyCredentialEndDate = $KeyCredential.EndDateTime
$KeyCredentialKeyId = $KeyCredential.KeyId
if ($KeyCredentialEndDate -ne $null) {
$KeyCredentialEndDate = [DateTime]::Parse($KeyCredentialEndDate)
$DaysToExpire = ($KeyCredentialEndDate - (Get-Date)).Days
if ($DaysToExpire -lt 30) {
# json 形式で $body に 追加
$tmp = @{
"Type" = "Application"
"ApplicationDisplayName" = $ApplicationDisplayName
"CredentialType" = "Key"
"CredentialKeyId" = $KeyCredentialKeyId
"CredentialEndDate" = $KeyCredentialEndDate
"DaysToExpire" = $DaysToExpire
} | ConvertTo-Json
$body += $tmp + ",`r`n"
}
}
}
$ApplicationPasswordCredential | ForEach-Object {
$PasswordCredential = $_
$PasswordCredentialEndDate = $PasswordCredential.EndDateTime
$PasswordCredentialKeyId = $PasswordCredential.KeyId
if ($PasswordCredentialEndDate -ne $null) {
$PasswordCredentialEndDate = [DateTime]::Parse($PasswordCredentialEndDate)
$DaysToExpire = ($PasswordCredentialEndDate - (Get-Date)).Days
if ($DaysToExpire -lt 30) {
# json 形式で $body に 追加
$tmp = @{
"Type" = "Application"
"ApplicationDisplayName" = $ApplicationDisplayName
"CredentialType" = "Password"
"CredentialKeyId" = $PasswordCredentialKeyId
"CredentialEndDate" = $PasswordCredentialEndDate
"DaysToExpire" = $DaysToExpire
} | ConvertTo-Json
$body += $tmp + ",`r`n"
}
}
}
}
# $body の最後のカンマを削除し整形
if($body -ne "") {
$body = "[`r`n" + $body.Substring(0, $body.Length - 3) + "`r`n]"
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = $body
})
using namespace System.Net
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
# Graph API PowerShell
Connect-MgGraph -Identity
# Get-MgServicePrincipal でサービス プリンシパルの一覧を取得
$ServicePrincipals = Get-MgServicePrincipal -All
# 30 日以内に期限が切れる証明書やシークレットを持つ Service Principal をリストアップ
$body = ""
$ServicePrincipals | ForEach-Object {
$ServicePrincipal = $_
$ServicePrincipalDisplayName = $ServicePrincipal.DisplayName
$ServicePrincilalType = $ServicePrincipal.ServicePrincipalType
$ServicePrincipalKeyCredential = $ServicePrincipal.KeyCredentials
$ServicePrincipalPasswordCredential = $ServicePrincipal.PasswordCredentials
# マネージド ID 以外を対象
if ($ServicePrincilalType -ne "ManagedIdentity") {
$ServicePrincipalKeyCredential | ForEach-Object {
$KeyCredential = $_
$KeyCredentialEndDate = $KeyCredential.EndDateTime
$KeyCredentialKeyId = $KeyCredential.KeyId
if ($KeyCredentialEndDate -ne $null) {
$KeyCredentialEndDate = [DateTime]::Parse($KeyCredentialEndDate)
$DaysToExpire = ($KeyCredentialEndDate - (Get-Date)).Days
if ($DaysToExpire -lt 30) {
# json 形式で $body に 追加
$tmp = @{
"Type" = "Service Principal"
"ServicePrincipalDisplayName" = $ServicePrincipalDisplayName
"CredentialType" = "Key"
"CredentialKeyId" = $KeyCredentialKeyId
"CredentialEndDate" = $KeyCredentialEndDate
"DaysToExpire" = $DaysToExpire
} | ConvertTo-Json
$body += $tmp + ",`r`n"
}
}
}
$ServicePrincipalPasswordCredential | ForEach-Object {
$PasswordCredential = $_
$PasswordCredentialEndDate = $PasswordCredential.EndDateTime
$PasswordCredentialKeyId = $PasswordCredential.KeyId
if ($PasswordCredentialEndDate -ne $null) {
$PasswordCredentialEndDate = [DateTime]::Parse($PasswordCredentialEndDate)
$DaysToExpire = ($PasswordCredentialEndDate - (Get-Date)).Days
if ($DaysToExpire -lt 30) {
# json 形式で $body に 追加
$tmp = @{
"Type" = "Service Principal"
"ServicePrincipalDisplayName" = $ServicePrincipalDisplayName
"CredentialType" = "Password"
"CredentialKeyId" = $PasswordCredentialKeyId
"CredentialEndDate" = $PasswordCredentialEndDate
"DaysToExpire" = $DaysToExpire
} | ConvertTo-Json
$body += $tmp + ",`r`n"
}
}
}
}
}
# $body の最後のカンマを削除し整形
if($body -ne "") {
$body = "[`r`n" + $body.Substring(0, $body.Length - 3) + "`r`n]"
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = $body
})
こんな感じです。[テストと実行] で json フォーマットで出力することを確認します。
Logic Apps の設定
Logic Apps を以下の流れで設定します。全体像は以下の通りです。
まずトリガーに[Recurrence] を使用し、実行間隔を設定しています。下記では週 1 回 月曜日 0:00 に実行するように設定しています。
次に並列分岐を追加して [Azure Functions] アクションを追加し、先ほど作成した関数を選択します。
Azure Functions からのアウトプットである json をオブジェクトに変換するため、[JSON の解析] アクションを使用し、コンテンツに前段の Azure Functions の [本文] を指定、スキーマに以下をコピーします。以下のスキーマは Application 用と ServicePrincipal 用です。
{
"items": {
"properties": {
"ApplicationDisplayName": {
"type": "string"
},
"CredentialEndDate": {
"type": "string"
},
"CredentialKeyId": {
"type": "string"
},
"CredentialType": {
"type": "string"
},
"DaysToExpire": {
"type": "integer"
},
"Type": {
"type": "string"
}
},
"required": [
"ApplicationDisplayName",
"CredentialEndDate",
"CredentialKeyId",
"CredentialType",
"DaysToExpire",
"Type"
],
"type": "object"
},
"type": "array"
}
{
"items": {
"properties": {
"ServicePrincipalDisplayName": {
"type": "string"
},
"CredentialEndDate": {
"type": "string"
},
"CredentialKeyId": {
"type": "string"
},
"CredentialType": {
"type": "string"
},
"DaysToExpire": {
"type": "integer"
},
"Type": {
"type": "string"
}
},
"required": [
"ServicePrincipalDisplayName",
"CredentialEndDate",
"CredentialKeyId",
"CredentialType",
"DaysToExpire",
"Type"
],
"type": "object"
},
"type": "array"
}
フォーマットを整えるため、HTML テーブルに変換します。[HTML テーブルの作成] アクションを追加し、以下のように設定します。
同様に見栄えを良くするため、CSS を作成しておきます。[変数を初期化する] アクションで以下の CSS を設定しておきます。(自身の好みにカスタマイズしてください)
<style> table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; } th { background-color: #f2f2f2; } tr:nth-child(even) { background-color: #f9f9f9; } tr:hover { background-color: #e8e8e8; }</style>
最後に通知用のメールを設定します。今回は Office365 Outlook を使用しています。この辺りのツールやフォーマットは用途に応じてかと思います。
実行結果
[トリガーの実行] で動作を確認します。意図したとおりにメールが受信できていることを確認できました。
参考
Logic Apps に関してはひとつずつアクションを説明しましたが、以下の ARM テンプレートでまとめてデプロイ可能です。Azure Functions 内の関数名を一致させる必要がある点と、デプロイ後に Office365 Outlook の接続を設定が必要です。
template.json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workflows_name": {
"defaultValue": "logicapp-expiringspapp",
"type": "string"
},
"sites_functions_externalid": {
"defaultValue": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/application-rg/providers/Microsoft.Web/sites/functionapps",
"type": "string"
},
"DeployersUserName": {
"defaultValue": "<username>@<domain>",
"type": "string"
},
"MailList": {
"defaultValue": "<user1>@<domain>;<user2>@<domain>;...",
"type": "string"
}
},
"variables": {
"o365ConnectionName": "[concat('o365-', parameters('workflows_name'))]"
},
"resources": [
{
"type": "Microsoft.Web/connections",
"apiVersion": "2016-06-01",
"name": "[variables('o365ConnectionName')]",
"location": "[resourceGroup().location]",
"properties": {
"displayName": "[parameters('DeployersUserName')]",
"customParameterValues": {},
"api": {
"id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/office365')]"
}
}
},
{
"type": "Microsoft.Logic/workflows",
"apiVersion": "2017-07-01",
"name": "[parameters('workflows_name')]",
"location": "japaneast",
"dependsOn": [
"[resourceId('Microsoft.Web/connections', variables('o365ConnectionName'))]"
],
"identity": {
"type": "SystemAssigned"
},
"properties": {
"state": "Enabled",
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"Recurrence": {
"recurrence": {
"frequency": "Day",
"interval": 1,
"schedule": {
"hours": [
"0"
],
"minutes": [
0
]
},
"timeZone": "Tokyo Standard Time"
},
"evaluatedRecurrence": {
"frequency": "Day",
"interval": 1,
"schedule": {
"hours": [
"0"
],
"minutes": [
0
]
},
"timeZone": "Tokyo Standard Time"
},
"type": "Recurrence"
}
},
"actions": {
"GetExpiringApplication": {
"runAfter": {},
"type": "Function",
"inputs": {
"function": {
"id": "[concat(parameters('sites_functions_externalid'), '/functions/GetExpiringApplication')]"
}
}
},
"GetExpiringServicePrincipal": {
"runAfter": {},
"type": "Function",
"inputs": {
"function": {
"id": "[concat(parameters('sites_functions_externalid'), '/functions/GetExpiringServicePrincipal')]"
}
}
},
"Parse_Json_-_Application": {
"runAfter": {
"GetExpiringApplication": [
"Succeeded"
]
},
"type": "ParseJson",
"inputs": {
"content": "@body('GetExpiringApplication')",
"schema": {
"items": {
"properties": {
"ApplicationDisplayName": {
"type": "string"
},
"CredentialEndDate": {
"type": "string"
},
"CredentialKeyId": {
"type": "string"
},
"CredentialType": {
"type": "string"
},
"DaysToExpire": {
"type": "integer"
},
"Type": {
"type": "string"
}
},
"required": [
"CredentialType",
"DaysToExpire",
"ApplicationDisplayName",
"CredentialEndDate",
"CredentialKeyId",
"Type"
],
"type": "object"
},
"type": "array"
}
}
},
"Parse_Json_-_Service_Principal": {
"runAfter": {
"GetExpiringServicePrincipal": [
"Succeeded"
]
},
"type": "ParseJson",
"inputs": {
"content": "@body('GetExpiringServicePrincipal')",
"schema": {
"items": {
"properties": {
"CredentialEndDate": {
"type": "string"
},
"CredentialKeyId": {
"type": "string"
},
"CredentialType": {
"type": "string"
},
"DaysToExpire": {
"type": "integer"
},
"ServicePrincipalDisplayName": {
"type": "string"
},
"Type": {
"type": "string"
}
},
"required": [
"Type",
"DaysToExpire",
"CredentialType",
"ServicePrincipalDisplayName",
"CredentialEndDate",
"CredentialKeyId"
],
"type": "object"
},
"type": "array"
}
}
},
"Create_HTML_Table_-_Application": {
"runAfter": {
"Parse_Json_-_Application": [
"Succeeded"
]
},
"type": "Table",
"inputs": {
"columns": [
{
"header": "Type",
"value": "@item()['Type']"
},
{
"header": "ApplicationDisplayName",
"value": "@item()?['ApplicationDisplayName']"
},
{
"header": "CredentialType",
"value": "@item()['CredentialType']"
},
{
"header": "CredentialEndDate",
"value": "@item()['CredentialEndDate']"
},
{
"header": "DaysToExpire",
"value": "@item()['DaysToExpire']"
}
],
"format": "HTML",
"from": "@body('Parse_Json_-_Application')"
}
},
"Create_HTML_Table_-_Service_Principal": {
"runAfter": {
"Parse_Json_-_Service_Principal": [
"Succeeded"
]
},
"type": "Table",
"inputs": {
"columns": [
{
"header": "Type",
"value": "@item()['Type']"
},
{
"header": "ServicePrincipalDisplayName",
"value": "@item()['ServicePrincipalDisplayName']"
},
{
"header": "CredentialType",
"value": "@item()['CredentialType']"
},
{
"header": "CredentialEndDate",
"value": "@item()['CredentialEndDate']"
},
{
"header": "DaysToExpire",
"value": "@item()['DaysToExpire']"
}
],
"format": "HTML",
"from": "@body('Parse_Json_-_Service_Principal')"
}
},
"Set_CSS": {
"runAfter": {
"Create_HTML_Table_-_Application": [
"Succeeded"
],
"Create_HTML_Table_-_Service_Principal": [
"Succeeded"
]
},
"type": "InitializeVariable",
"inputs": {
"variables": [
{
"name": "css",
"type": "string",
"value": "<style> table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #dddddd; text-align: left; padding: 8px; } th { background-color: #f2f2f2; } tr:nth-child(even) { background-color: #f9f9f9; } tr:hover { background-color: #e8e8e8; }</style>"
}
]
}
},
"メールの送信_(V2)": {
"runAfter": {
"Set_CSS": [
"Succeeded"
]
},
"type": "ApiConnection",
"inputs": {
"body": {
"Body": "<p>@{variables('css')}<br>\n<br>\nアプリケーション<br>\n@{body('Create_HTML_Table_-_Application')}<br>\n<br>\nサービスプリンシパル (SAML証明書)<br>\n@{body('Create_HTML_Table_-_Service_Principal')}</p>",
"Importance": "Normal",
"Subject": "アプリケーション / サービスプリンシパルの有効期限",
"To": "[parameters('MailList')]"
},
"host": {
"connection": {
"name": "@parameters('$connections')['office365']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail"
}
}
},
"outputs": {}
},
"parameters": {
"$connections": {
"value": {
"office365": {
"connectionId": "[resourceId('Microsoft.Web/connections', variables('o365ConnectionName'))]",
"connectionName": "[variables('o365ConnectionName')]",
"id": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Web/locations/', resourceGroup().location, '/managedApis/office365')]"
}
}
}
}
}
}
]
}
Discussion