0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M365/GWSの監査ログ異常をPower Automate・Apps Scriptで自動通知する実装ガイド

0
Posted at

個人情報保護法の改正(2026年4月閣議決定、2028年頃施行見込み)では、個人情報を含むシステムへのアクセスログ管理が安全管理措置として一層重要視されます。また能動的サイバー防御法(2026年秋頃施行見込み)への対応でも、ログの取得・保全・異常検知は不可欠です。

本記事では、Microsoft 365(M365)Google Workspace(GWS) それぞれの監査ログから異常を検知し、Teams / Slack / メールに自動通知する仕組みを実装コード付きで解説します。


対象読者・前提条件

  • Microsoft 365 / Google Workspace の管理者権限を持つエンジニア・情シス担当
  • M365: Microsoft Entra ID(旧Azure AD)テナント管理者権限
  • GWS: スーパー管理者権限 / Google Apps Script 実行権限
  • 難易度: 中級(API認証・JSON操作の基礎知識があること)

検知したい異常イベントの定義

まず「何を異常とするか」を定義します。本記事では以下を対象とします。

カテゴリ 異常イベント例
認証 不審なサインイン(is_suspicious=true)、海外IPからのアクセス
権限変更 管理者ロールの付与・剥奪、グループメンバー変更
データ操作 大量ダウンロード、外部共有の有効化、ドライブファイルの一括削除
メール フォワードルール設定(外部メール転送)、メールボックス委任

Part 1:Microsoft 365 編(Power Automate + Office 365 Management API)

アーキテクチャ

Office 365 Management API
        ↓(定期ポーリング)
Power Automate(スケジュールフロー)
        ↓(異常イベント検出)
Teams チャネル通知 / メール

Step 1:Microsoft Entra ID でアプリ登録

Microsoft Entra管理センター(https://entra.microsoft.com)でアプリを登録し、以下のAPIアクセス許可を付与します。

API: Office 365 Management APIs
  - ActivityFeed.Read(委任されたアクセス許可)
  - ActivityFeed.ReadDlp(委任されたアクセス許可)

クライアントIDとクライアントシークレットを控えておきます。

Step 2:Power Automate フローの作成

以下のフロー構成で実装します。

トリガー:スケジュール(毎時0分)

{
  "trigger": {
    "type": "Recurrence",
    "recurrence": {
      "frequency": "Hour",
      "interval": 1
    }
  }
}

アクション1:OAuthトークン取得(HTTPアクション)

メソッド: POST
URI: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
ヘッダー: Content-Type: application/x-www-form-urlencoded
本文:
  client_id={clientId}
  &client_secret={clientSecret}
  &scope=https://manage.office.com/.default
  &grant_type=client_credentials

アクション2:監査ログ取得(HTTPアクション)

メソッド: GET
URI: https://manage.office.com/api/v1.0/{tenantId}/activity/feed/subscriptions/content
     ?contentType=Audit.AzureActiveDirectory
     &startTime={utcNow('yyyy-MM-ddTHH:mm:ss')が1時間前}
     &endTime={utcNow('yyyy-MM-ddTHH:mm:ss')}
ヘッダー: Authorization: Bearer {access_token}

アクション3:各コンテンツURLからログ取得&フィルタリング

Power Automateの「Apply to each」アクションでコンテンツURLを順に取得し、以下の式で異常イベントをフィルタリングします。

@or(
  equals(item()?['Operation'], 'UserLoginFailed'),
  equals(item()?['Operation'], 'Add member to role.'),
  equals(item()?['Operation'], 'Set-Mailbox'),
  and(
    equals(item()?['Operation'], 'FileDownloaded'),
    greater(item()?['SourceFileSize'], 104857600)
  )
)

アクション4:Teams通知(Microsoft Teams コネクター)

異常イベントが検出された場合のみ、「Post a message in a chat or channel」アクションでTeamsに通知します。

チャネル: セキュリティアラート
メッセージ:
🚨 [監査ログアラート] 異常操作を検出しました

📅 発生日時: {items('Apply_to_each')?['CreationTime']}
👤 ユーザー: {items('Apply_to_each')?['UserId']}
🔴 操作種別: {items('Apply_to_each')?['Operation']}
🌐 IPアドレス: {items('Apply_to_each')?['ClientIP']}

M365 実装のポイント

  • Office 365 Management APIはコンテンツが最大24時間遅延することがあります。リアルタイム検知にはMicrosoft Sentinel(有償)の利用を検討してください。
  • ActivityFeed.Read の権限付与後、サブスクリプションの有効化が必要です。初回のみ以下のAPIを叩きます。
POST https://manage.office.com/api/v1.0/{tenantId}/activity/feed/subscriptions/start
     ?contentType=Audit.AzureActiveDirectory
Authorization: Bearer {access_token}
Content-Type: application/json
{}

Part 2:Google Workspace 編(Apps Script + Admin SDK Reports API)

アーキテクチャ

Google Admin SDK Reports API(LoginActivity / AdminActivity)
        ↓(時間ベーストリガー)
Google Apps Script
        ↓(異常イベント検出)
Gmail / Google Chat Webhook 通知

Step 1:Apps Script の準備

Google Apps Script(https://script.google.com)で新規プロジェクトを作成し、「サービスを追加」から Admin SDK API を有効化します。

プロジェクトのマニフェスト(appsscript.json)に以下を追加します。

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/admin.reports.audit.readonly",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/script.external_request"
  ],
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "AdminReports",
        "serviceId": "admin",
        "version": "reports_v1"
      }
    ]
  }
}

Step 2:不審ログイン検知スクリプト

/**
 * 不審なログインイベントを検知してメール通知する
 * 毎時トリガーで実行
 */
function checkSuspiciousLogins() {
  const ALERT_EMAIL = 'security-alert@your-domain.com'; // 通知先メールアドレス
  const CHAT_WEBHOOK_URL = PropertiesService.getScriptProperties()
    .getProperty('CHAT_WEBHOOK_URL'); // Google ChatのWebhook URL

  const now = new Date();
  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);

  try {
    // Reports APIでログインイベントを取得
    const response = AdminReports.Activities.list('all', 'login', {
      startTime: oneHourAgo.toISOString(),
      endTime: now.toISOString(),
      eventName: 'login_failure',
      maxResults: 1000,
    });

    const activities = response.items || [];

    // ユーザーごとの失敗回数を集計
    const failureCount = {};
    activities.forEach(activity => {
      const userId = activity.actor?.email || 'unknown';
      failureCount[userId] = (failureCount[userId] || 0) + 1;
    });

    // 5回以上失敗したユーザーをアラート対象に
    const alertTargets = Object.entries(failureCount)
      .filter(([_, count]) => count >= 5);

    if (alertTargets.length === 0) return;

    const alertMessage = buildAlertMessage(alertTargets, 'ログイン失敗多発');
    sendEmailAlert(ALERT_EMAIL, alertMessage);
    if (CHAT_WEBHOOK_URL) {
      sendChatAlert(CHAT_WEBHOOK_URL, alertMessage);
    }

  } catch (error) {
    console.error('監査ログ取得エラー:', error.message);
  }
}

/**
 * 外部メール転送ルール設定を検知する
 */
function checkForwardingRules() {
  const ALERT_EMAIL = 'security-alert@your-domain.com';

  const now = new Date();
  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);

  try {
    const response = AdminReports.Activities.list('all', 'gmail', {
      startTime: oneHourAgo.toISOString(),
      endTime: now.toISOString(),
      eventName: 'CREATE_EMAIL_FORWARDING_SETTING',
      maxResults: 100,
    });

    const activities = response.items || [];
    if (activities.length === 0) return;

    const targets = activities.map(a => [a.actor?.email, 1]);
    const alertMessage = buildAlertMessage(targets, '外部転送ルール設定を検知');
    sendEmailAlert(ALERT_EMAIL, alertMessage);

  } catch (error) {
    console.error('Gmail監査ログ取得エラー:', error.message);
  }
}

/**
 * 管理者ロール変更を検知する
 */
function checkAdminRoleChanges() {
  const ALERT_EMAIL = 'security-alert@your-domain.com';

  const now = new Date();
  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);

  try {
    const response = AdminReports.Activities.list('all', 'admin', {
      startTime: oneHourAgo.toISOString(),
      endTime: now.toISOString(),
      eventName: 'GRANT_ADMIN_PRIVILEGE',
      maxResults: 100,
    });

    const activities = response.items || [];
    if (activities.length === 0) return;

    const targets = activities.map(a => [
      `${a.actor?.email}${getEventParam(a, 'USER_EMAIL')}`,
      1
    ]);
    const alertMessage = buildAlertMessage(targets, '管理者権限付与を検知');
    sendEmailAlert(ALERT_EMAIL, alertMessage);

  } catch (error) {
    console.error('Admin監査ログ取得エラー:', error.message);
  }
}

// ---- ユーティリティ関数 ----

function getEventParam(activity, paramName) {
  const events = activity.events || [];
  for (const event of events) {
    const params = event.parameters || [];
    const param = params.find(p => p.name === paramName);
    if (param) return param.value || param.multiValue?.join(', ') || '';
  }
  return '';
}

function buildAlertMessage(targets, eventType) {
  const lines = targets.map(([user, count]) =>
    count > 1 ? `・${user}${count}件)` : `・${user}`
  );
  return [
    `[GWSセキュリティアラート] ${eventType}`,
    `検出日時: ${new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' })}`,
    '',
    '対象ユーザー:',
    ...lines,
    '',
    '管理コンソールで詳細を確認してください:',
    'https://admin.google.com/ac/reporting/audit/login',
  ].join('\n');
}

function sendEmailAlert(to, message) {
  GmailApp.sendEmail(
    to,
    '[要確認] GWS セキュリティアラート',
    message
  );
}

function sendChatAlert(webhookUrl, message) {
  const payload = JSON.stringify({ text: message });
  UrlFetchApp.fetch(webhookUrl, {
    method: 'POST',
    contentType: 'application/json',
    payload: payload,
  });
}

Step 3:時間ベーストリガーの設定

Apps Scriptのエディタで「トリガーを追加」から以下を設定します。

関数 トリガー種別 頻度
checkSuspiciousLogins 時間ベース 1時間ごと
checkForwardingRules 時間ベース 1時間ごと
checkAdminRoleChanges 時間ベース 30分ごと

Chat Webhook URLは「スクリプトプロパティ」に保存します(コードに直書き禁止)。

// 初回設定時に一度だけ実行
function setProperties() {
  PropertiesService.getScriptProperties().setProperties({
    'CHAT_WEBHOOK_URL': 'https://chat.googleapis.com/v1/spaces/XXXXX/messages?key=...'
  });
}

運用上の注意点

ログ保存期間

プラットフォーム デフォルト保存期間 延長方法
M365 監査ログ 90日(E3)/ 1年(E5) Microsoft Purview Audit(Premium)で10年まで延長可
GWS 監査ログ 6ヶ月(Workspace)/ 1年(Enterprise) Cloud Logging連携でGCSに長期保存可

個人情報保護法対応としては最低1年、可能なら3年の保存を推奨します。

アラートの誤検知対策

  • 閾値(例:ログイン失敗5回)は組織の利用実態に合わせて調整してください
  • 出張・テレワーク時の海外IPによる誤検知を減らすため、既知のIPレンジを除外リストに登録する運用も有効です

コスト

  • Power Automate:Microsoft 365 Business Premiumに含まれる(追加費用なし)。HTTPアクションはPremiumコネクターのため、プラン確認が必要
  • Apps Script:Google Workspace の範囲内で無料(実行クォータに注意:6時間/日)

まとめ

本記事では、M365とGWSそれぞれの監査ログを定期取得し、異常イベントを自動通知する仕組みをコード付きで解説しました。

  • M365:Power Automate + Office 365 Management API で低コードに実装
  • GWS:Apps Script + Admin SDK Reports API でサーバーレスに実装

この仕組みは、個人情報保護法改正への安全管理措置対応と、能動的サイバー防御法時代のインシデント早期検知の両面で活用できます。

社内の情シスリソースが限られている場合、情シス365ではこうした監査ログ監視の設計・構築・運用代行まで対応しています。


著者:亀田 英佑 / 株式会社BTNコンサルティング / 情シス365
中小企業の情シスアウトソーシング・セキュリティ対応支援
▶ 無料相談(オンライン30分)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?