📧

Defeder for Cloud 推奨事項サマリをサブスクリプションの各担当者にメールする

2025/02/12に公開

はじめに

Defender for Cloud では、ワークフローの自動化や連続エクスポート + Azure Monitor アラート ルールを使用して、推奨事項が新たに検出したタイミングで通知ができます。また、ガバナンス ルールを使用すれば担当者にリマインド メールを出すことが可能です。

しかしながら、週次や月次でレポートのような形で送ることが難しいため、Logic Apps を使用して実装していこうと思います。

1. Logic Apps のマネージド ID を有効化

Logic Apps からストレージ アカウントや Azure Resource Graph クエリを実行できるようにマネージド ID を有効化します。


ロールを割り当てます。ストレージ アカウントから連絡先一覧を取得するため、所属しているサブスクリプションの閲覧者と BLOB データ閲覧者を付与します。


他のサブスクリプションの推奨事項を Azure Resource Graph で取得するのですが、Lighthouse で他テナントのサブスクリプションを追加することを想定し、ユーザー アカウントと同様にグループに所属させて、グループにセキュリティ閲覧者ロールを付与します。


2. ストレージ アカウントに連絡先一覧を格納

以下の JSON フォーマットにして、txt ファイルとして任意のコンテナに格納します。
CSV にしたかったのですが、Logic Apps で CSV の処理が難しいため、JSON フォーマットにしています。

3. Logic Apps の実装

Logic Apps を実装していきます。トリガーは [Recurrence] です。周期は通知したい頻度を指定します。

3-1. BLOB ファイルを取得

[BLOB コンテンツを取得する (V2)] コネクタを使用します。認証はマネージド ID です。


さきほど格納した連絡先一覧の BLOB ファイルを指定します。ここで表示されない場合はマネージド ID に適切なロール付与ができていないと考えられます。


[Parse JSON] コネクタでデータをパースします。Content に [BLOB コンテンツを取得する (V2)] の出力である ファイル コンテンツ を指定します。[サンプルのペイロードを使用してスキーマを作成する] をクリックして連絡先一覧の JSON を貼り付ければ自動でスキーマを作成してくれます。


取得したデータを格納する変数を [Initialize variable] で準備し、Value に [Parse JSON] の出力である Body を指定します。


一旦保存し、[Run] でテストしてみます。
[実行履歴] に移動し、データが取れていることを確認します。

3-2. Azure Resource Graph クエリを実行

取得した subscriptionId を使用して、Azure Resource Graph クエリを実行します。
Azure Resource Graph 用のコネクタがないため、HTTP リクエストか Azure Monitor のクエリ実行のコネクタを使用します。

a. HTTP リクエストを利用

[HTTP] コネクタを追加し、以下のように設定します。クエリで [Parse JSON] の出力結果である Body subscriptionId を指定すると、自動的に [For each] 内に格納されます。


保存し、[Run] でテストします。[実行履歴] に移動し、データが取れていることを確認します。

b. Azure Monitor のクエリ実行を利用

[クエリを実行して結果を一覧表示する V2 (プレビュー)] コネクタを追加し、以下のように設定します。クエリでは [Parse JSON] の出力結果である Body subscriptionId を指定すると、自動的に [For each] 内に格納されます。Azure Monitor 内で arg("") を使用して Azure Resource Graph クエリを実行します。クエリによっては arg("") 内での非サポートがあるようで、エラーになる場合は HTTP コネクタを検討します。


保存し、[Run] でテストします。[実行履歴] に移動し、データが取れていることを確認します。
処理時間は HTTP コネクタのほうが早いように見えています。

3-3. 実際に利用するクエリに変更

レポートには集計情報とリスクが緊急/高の推奨事項を記載するため、そのクエリに変更します。 (今回は HTTP コネクタを利用) 以下の記事のベース クエリを利用し、HTTP コネクタ用に "' に変換し、改行とコメントを削除します。
https://zenn.dev/microsoft/articles/9e778418469652

集計用クエリ
<ベース クエリ>
| where statusCode == 'Unhealthy'
| where subscriptionId == '<Parse JSON の Body SubscriptionId>'
| summarize Critical = countif(riskLevel == '4'),
            High = countif(riskLevel == '3'),
            Medium = countif(riskLevel == '2'),
            Low = countif(riskLevel == '1') 
| project Critical, High, Medium, Low
集計用クエリ - 改行なし
securityresources | where type =~ 'microsoft.security/assessments' | extend assessmentType = (tostring(properties.metadata.assessmentType)) | extend assessmentTypeSkimmed = case(tostring(properties.metadata.assessmentType) == 'BuiltIn', 'BuiltIn', tostring(properties.metadata.assessmentType) == 'BuiltInPolicy', 'BuiltIn', tostring(properties.metadata.assessmentType) == 'CustomPolicy', 'Custom', tostring(properties.metadata.assessmentType) == 'CustomerManaged', 'Custom', tostring(properties.metadata.assessmentType) == 'ManualCustomPolicy', 'Custom', tostring(properties.metadata.assessmentType) == 'ManualBuiltInPolicy', 'BuiltIn', dynamic(null)) | extend assessmentId = tolower(id) | extend assessmentKey = name | extend source = tostring(properties.resourceDetails.Source) | extend resourceId = tostring(properties.resourceDetails.ResourceId) | extend displayName = tostring(properties.displayName) | extend statusCode = tostring(properties.status.code) | extend severity = tostring(properties.metadata.severity) | extend severityLevel = (case(severity =~ 'High', 3, severity =~ 'Medium', 2, severity =~ 'Low', 1, 0)) | extend riskLevelText = tostring(properties.risk.level) | extend riskLevel = (case(riskLevelText =~ 'Critical', 4, riskLevelText =~ 'High', 3, riskLevelText =~ 'Medium', 2, riskLevelText =~ 'Low', 1, 0)) | extend riskFactors = iff(isnull(properties.risk.riskFactors), dynamic([]), properties.risk.riskFactors) | extend statusCause = tostring(properties.status.cause) | extend isExempt = iff(statusCause == 'Exempt', tobool(1), tobool(0)) | extend firstEvaluationDate = tostring(todatetime(properties.status.firstEvaluationDate)) | extend statusChangeDate = tostring(todatetime(properties.status.statusChangeDate)) | extend url  = strcat('https://', tostring(todatetime(properties.links.azurePortal))) | project tenantId, subscriptionId, resourceGroup, resourceId, source, displayName, statusCode, severity, severityLevel, riskLevelText, riskLevel, riskFactors, isExempt, statusCause, statusChangeDate, assessmentType, assessmentTypeSkimmed, assessmentKey, url | where statusCode == 'Unhealthy' | where subscriptionId == '<Parse JSON の Body SubscriptionId>' | summarize Critical = countif(riskLevel == '4'), High = countif(riskLevel == '3'), Medium = countif(riskLevel == '2'), Low = countif(riskLevel == '1') | project Critical, High, Medium, Low

リスク緊急/高の推奨事項
<ベース クエリ>
| where statusCode == 'Unhealthy'
| where subscriptionId == '<Parse JSON の Body SubscriptionId>'
| where riskLevel >=3
| extend resourceName = tostring(split(resourceId, '/')[-1])
| project resourceGroup, resourceName, displayName, severity, severityLevel, riskLevelText,riskLevel, url
| order by riskLevel desc, severityLevel desc
| project-away riskLevel, severityLevel
リスク緊急/高の推奨事項 - 改行なし
securityresources | where type =~ 'microsoft.security/assessments' | extend assessmentType = (tostring(properties.metadata.assessmentType)) | extend assessmentTypeSkimmed = case(tostring(properties.metadata.assessmentType) == 'BuiltIn', 'BuiltIn', tostring(properties.metadata.assessmentType) == 'BuiltInPolicy', 'BuiltIn', tostring(properties.metadata.assessmentType) == 'CustomPolicy', 'Custom', tostring(properties.metadata.assessmentType) == 'CustomerManaged', 'Custom', tostring(properties.metadata.assessmentType) == 'ManualCustomPolicy', 'Custom', tostring(properties.metadata.assessmentType) == 'ManualBuiltInPolicy', 'BuiltIn', dynamic(null)) | extend assessmentId = tolower(id) | extend assessmentKey = name | extend source = tostring(properties.resourceDetails.Source) | extend resourceId = tostring(properties.resourceDetails.ResourceId) | extend displayName = tostring(properties.displayName) | extend statusCode = tostring(properties.status.code) | extend severity = tostring(properties.metadata.severity) | extend severityLevel = (case(severity =~ 'High', 3, severity =~ 'Medium', 2, severity =~ 'Low', 1, 0)) | extend riskLevelText = tostring(properties.risk.level) | extend riskLevel = (case(riskLevelText =~ 'Critical', 4, riskLevelText =~ 'High', 3, riskLevelText =~ 'Medium', 2, riskLevelText =~ 'Low', 1, 0)) | extend riskFactors = iff(isnull(properties.risk.riskFactors), dynamic([]), properties.risk.riskFactors) | extend statusCause = tostring(properties.status.cause) | extend isExempt = iff(statusCause == 'Exempt', tobool(1), tobool(0)) | extend firstEvaluationDate = tostring(todatetime(properties.status.firstEvaluationDate)) | extend statusChangeDate = tostring(todatetime(properties.status.statusChangeDate)) | extend url  = strcat('https://', tostring(todatetime(properties.links.azurePortal))) | project tenantId, subscriptionId, resourceGroup, resourceId, source, displayName, statusCode, severity, severityLevel, riskLevelText, riskLevel, riskFactors, isExempt, statusCause, statusChangeDate, assessmentType, assessmentTypeSkimmed, assessmentKey, url | where statusCode == 'Unhealthy' | where subscriptionId == '<Parse JSON の Body SubscriptionId>' | where riskLevel >= 3 | extend resourceName = tostring(split(resourceId, '/')[-1]) | project resourceGroup, resourceName, displayName, severity, severityLevel, riskLevelText, riskLevel, url | order by riskLevel desc, severityLevel desc | project-away riskLevel, severityLevel


それぞれの実行結果はこちらです。後で [Parse JSON] で使用するため、結果をコピーしておきます。

3-4. クエリ結果を整形

[Parse JSON] で後続の処理に使用できるようにそれぞれのクエリ実行結果をパースします。さきほどの実行結果を [サンプルのペイロードを使用してスキーマを作成する] から貼り付けることで自動で Schema を作成してくれます。


メール本文で使用しやすいように [Create HTML Table] コネクタで [Parse JSON] の Body data を HTML テーブル形式に変換します。なお、処理が長くなったため並列分岐に変更しています。

3-5. メール送信

今回は簡易的に実装するため、Office365 Outlook の [メールの送信 (V2)] コネクタを使用します。よりシステム的に処理したい、ユーザーの作成やライセンスの付与ができないという場合は以下を参考に Azure Communication Service を検討ください。
https://qiita.com/hisnakad/items/814ae1a87a6964012d05

ここで分岐処理を集約するため、[設定] タブの [Run After] にもう一方も追加しておきます。


宛先は [BLOB コンテンツを取得する (V2)] から取得したメールアドレスを指定、本文内にそれぞれ先ほど作成した [Create HTML table] の Output を指定します。なお、既定では Logic Apps を構築しているユーザーの接続情報とメール ボックスが使われるため、送信元のアカウントを変更する場合は最下部の [接続の変更] をクリックして、使用するアカウントを変更してください。


なお、日時の箇所は convertFromUtc(utcNow(),'Tokyo Standard Time','yyyy/MM/dd') を関数で指定しています。


テストしてみます。以下のようなメールが飛ぶことが確認できました。


可読性が良くないので、コードビューに切り替え、css を指定します。

<style>
    body {
    font-family: Arial, sans-serif;
    font-size: 14px;
    line-height: 1.6;
    color: #333333;
    }
    h1, h2, h3 {
        color: #2c3e50;
    }
    table {
        width: 100%;
        border-collapse: collapse;
        margin: 20px 0;
    }
    table th, table td {
        border: 1px solid #dddddd;
        text-align: left;
        padding: 8px;
    }
    table th {
        background-color: #f4f4f4;
        font-weight: bold;
    }
    table tr:nth-child(even) {
        background-color: #f9f9f9;
    }
    table tr:hover {
        background-color: #f1f1f1;
    }
.important-text {
color: #e74c3c;
font-weight: bold;
}
</style>

以下は <span class="important-text"> [convertFromUtc(utcNow(),'Tokyo Standard Time','yyyy/MM/dd') 関数 を挿入]</span> 時点であなたの担当で管理している Azure サブスクリプション [Parse JSON の Body SubscriptionId を挿入] で出力している推奨事項の一覧です。
リスク軽減のため、早急に対処をお願いいたします。

<h2>リスク別推奨事項サマリ</h2>
[Create HTML table - summarize recommendations の Output を挿入]

<h2>リスク別推奨事項一覧 (リスクレベル:緊急 / 高)</h2>
[Create HTML table - critical and high recommendations の Output を挿入]

以下のようにメール送信されます。かなりまともになりました。

(オプション) メールの可読性を上げる

上記の場合、[Create HTML table] を使用しているため、テーブル自体の書式を指定することが難しいです。そのため、カスタムで HTML テーブルを作成してきます。まず、HTML テーブルの文字列を格納する変数を作成します。


[Set variable] で HTML テーブルの見出しの情報を格納します。のちほど CSS 内で出てきますが、class="half-width-table" を指定します。


[Append to string variable] で [Parse JSON] で取得した情報を以下で追記します。[Parse JSON] の値を指定すると自動的に For each 内に格納されます。


最後に [Append to string variable] で </table> でタグを閉じます。


同じようにリスク別推奨事項一覧のほうも設定してきます。
[Set variable] で HTML テーブルの見出しの情報を格納します。


[Append to string variable] で [Parse JSON] で取得した情報を以下で追記します。CSS で CriticalHigh などのクラスを作成することで、そのまま変数として指定できるようにしています。こちらも [Parse JSON] の値を指定すると自動的に For each 内に格納されます。


最後に [Append to string variable] で </table> でタグを閉じます。

メール内の CSS と本文を以下のように指定します。

<style>
    body {
        font-family: Arial, sans-serif;
        font-size: 14px;
        line-height: 1.6;
        color: #333333;
    }
    h1, h2 {
       color: #2c3e50;
    }
    table {
        width: 100%;
        border-collapse: collapse;
        margin: 20px 0;
    }
    table th, table td {
        border: 1px solid #dddddd;
        text-align: left;
        padding: 8px;
    }
    table th {
        background-color: #f4f4f4;
        font-weight: bold;
    }
    .half-width-table {
        width: 50%;
        border-collapse: collapse;
        margin: 20px 0;
    }
    .half-width-table th, .half-width-table td {
        border: 1px solid #dddddd;
        text-align: left;
        padding: 8px;
        width: 25%; 
    }

    table tr:nth-child(even) {
        background-color: #f9f9f9;
    }
    table tr:hover {
        background-color: #f1f1f1;
    }
    .Critical {
        background-color: #ffcccc;
    }
    .High {
        background-color: #ffe6cc;
    }
    .Medium {
        background-color: #ffffcc;
    }
    .Low {
        background-color: #e6ffcc;
    }
    a {
        color: #3498db;
        text-decoration: none;
    }
    a:hover {
        text-decoration: underline;
    }
</style>

以下は [convertFromUtc(utcNow(),'Tokyo Standard Time','yyyy/MM/dd') 関数を挿入] 時点であなたの担当で管理している Azure サブスクリプションで出力している推奨事項の一覧です。
リスク軽減のため、早急に対処をお願いいたします。

<h2>対象サブスクリプション ID</h2>
<h3> [Parse JSON の subscriptionId を挿入] </h3>
<br>

<h2>リスク別推奨事項サマリ</h2>
[変数 sumRecommTableString を挿入]
<br>

<h2>リスク別推奨事項一覧 (リスクレベル:緊急 / 高)</h2>
[変数 crithighRecommTableString を挿入]


最終的にこのようなメールになりました。


フローの全体は以下の通りです。


ARM テンプレート

すぐに使用できるように ARM テンプレート化しました。デプロイ後に BLOB へのアクセス設定と、Office 365 の接続情報を更新してください。また、マネージド ID への権限付与はしていませんので、個別に設定する必要があります。
https://github.com/katsato-ms/Microsoft/tree/main/Logic Apps/Send-DefenderforCloudRecommendationReport

まとめ

かなり大作になりましたが、今回は Defender for Cloud の推奨事項をレポート形式でメール通知する仕組みを Logic Apps で作成しました。Logic Apps では簡単にフローを作成できますが、今回のように多数の文字列処理がある場合は、非常に複雑になってしまいます。
そのため、Azure Functions などで 文字列処理用の PowerShell スクリプトを準備し、Logic Apps からキックさせる方法も一案かと思います。

Microsoft (有志)

Discussion