Firebaseの監査ログ監視とアラート通知を Google Apps Script で構築

#はじめに
`Google Apps Script` 、GoogleFormと連携して、アンケートへの自動返信メールを構築できたりといろいろ便利ですよね。
今回は `Google Apps Script` を活用して、Firebaseの監査ログ収集とアラート通知を構築する手順について纏めています。

# 方針
Firebase Management にて監査ログが取得可能です。
また、ログは自動的に Cloud Loggingへ出力されています。

上記の前提のもと、

* BigQueryへCloud Loggingのエクスポート設定を追加し、監査ログを永続化する
* Google Apps Scriptから不正アクセスなどのイベントをBigQueryへクエリを実行して検知し、Slackなどに通知する

でログの保存、監視を構築していきます。


# 設計
## 1. ログの種類
### リファレンス：
[Firebase Management の監査ログ](https://firebase.google.com/support/guides/cloud-audit-logging/firebase-management?hl=ja)

### 監査ログの種類：
* ログエントリ自体。LogEntry 型のオブジェクト
    * logName: プロジェクト ID と監査ログのタイプを保持
    * resource: 監査対象オペレーションのターゲットを保持
    * timeStamp: 監査対象オペレーションの時刻を保持
    * protoPayload: 監査対象情報を保持
* 監査ログデータ。ログエントリの protoPayload フィールドに保持される AuditLog
* サービス固有の監査情報（オプション）。AuditLog オブジェクトの serviceData フィールドに保持されるサービス固有のオブジェクト

### 料金：
* Stackdriver Logging の無効にできない監査ログ（すべての管理アクティビティ監査ログを含む）は無料

### ログのエクスポート：
Stackdriver Loggingから、ログを次の宛先にエクスポート
* BigQuery → 永続化

### データの無料枠:
* Stackdriver : 30日間保存されるログサイズ50GB
* BigQuery : クエリ使用量1TB, ログ保存サイズ10GB


## 2. ログの永続化

下記リファレンスを参照し、Stackdriver Loggingから、ログをBigQueryにエクスポートする設定を追加します。
[Google Cloud Console でのログのエクスポート](https://cloud.google.com/logging/docs/export/configure_export_v2?hl=ja)



## 3.ログの通知
[Cloud Monitoring API ](https://cloud.google.com/monitoring/support/notification-options?hl=ja#slack)

BigQueryへエクスポートしたログを、1時間毎に確認し、Slackへアラート通知する仕組みをGoogleAppsScriptを用いて構築していきます。

ログの永続化と通知のイメージ図
![gcp_alert.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/3a715f46-be20-ba0f-f408-310f428750f0.jpeg)

# 構築
## 1. ログの永続化
### ログシンク
環境ごとにログシンクを作成し、ログをBigQueryに出力する設定します。

*  [Stackdriver Loggingのログを失う前にエクスポートしておく方法](https://www.apps-gcp.com/stackdriver-logging-export-bigquery-pubsub/) 

### BigQuery：
環境ごとにデータセットを作成し、ログを永続化していきます。


### テーブル構造：

永続化ログのテーブル概要と、アラート条件は下記の通りです。

| テーブル名  | 概要 | アラート条件 |
| ------------- | ------------- | ------------- |
| cloudbuild | CI/CDサービスの実行ログ  | 対象外  |
| bigquerydatatransfer_googleapis_com_transfer_config  | LogginなどからBigqueryへのデータ転送ログ  | 対象外 |
| cloudaudit_googleapis_com_activity  | GCPリソースへのアクティビティログ | 対象外 |
| cloudaudit_googleapis_com_data_access  | GCPリソースへのアクセスログ | 社外からのアクセスが監視間隔内に1回以上 |
| cloudfunctions_googleapis_com_cloud_functions  | CloudFunctionsの実行ログ |  監視間隔内の実行エラーが規定回数以上 |


## 2.ログの通知

### 設計

2件の監視対象に対し、アラート通知の仕組みを構築していきます。

#### 1. GCPリソースへのアクセスログ

```
GCP上のデータアクセスのaudit_logの異常検知 して、Slack通知

対象テーブル：cloudaudit_googleapis_com_data_access（GCP上のデータアクセスのaudit_log）
エラーの条件：protopayload_auditlog.authenticationInfo.principalEmailが社外
```

1時間以内に社外メンバがリソースにアクセスした回数をカウントするクエリ

```sql
SELECT
  count(*) as result
FROM
  `[プロジェクト名].[データセット名].cloudaudit_googleapis_com_data_access`
WHERE
  TIMESTAMP(timestamp) >= TIMESTAMP_ADD(timestamp, INTERVAL 1 HOUR)
  AND ENDS_WITH(protopayload_auditlog.authenticationInfo.principalEmail, '[ドメイン]') IS FALSE
```

#### ２．CloudFunctionsのエラーレートが上がっていることを検知して、Slack通知


CloudFunctions、通常はエラーが発生起きないので、エラーカウントが1時間以内に10以上発生している場合、
何らかの障害が起きていると判定する

対象テーブル：cloudfunctions_googleapis_com_cloud_functions（CloudFunctionsの実行ログ）
エラーの条件：textPayload の 「failureCount: 1,」のカウントが10以上


■1時間以内に、textPayload の 「failureCount: 1,」が発生した回数をカウントするクエリ

```sql
SELECT
  count(*) as result
FROM
  `[プロジェクト名].[データセット名].cloudfunctions_googleapis_com_cloud_functions`
WHERE
  TIMESTAMP(timestamp) >= TIMESTAMP_ADD(timestamp, INTERVAL 1 HOUR)
  AND ENDS_WITH(textPayload, 'failureCount: 1,') IS TRUE
```

### 実装

#### Googleスプレッドシートでマスタを用意

下記のようにGoogleスプレッドシートで監視用マスタを設定していきます。

| No | project | category | limit | query |
|:-:|:-:|:-:|:-:|:-:|
| 1  | DEV  | AccessCheck  | 0  | SELECT count(*) as result FROM...  |
| 2  | STG  | ErrorRateCheck  | 10  | SELECT count(*) as result FROM...  |


監視用マスタを設定したGoogleスプレッドシートからスクリプトエディタを開き、Google Apps Scriptの編集画面を開きます。
![GAS登録.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/818a14ca-18e0-4dab-81ba-5e9d2003a92a.jpeg)

事前にGoogle Apps ScriptからBigQueryを操作するためのサービスを追加しておきます。
![BigQuery.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/9b35f15e-f35b-478c-7ce6-60b51ecdc884.jpeg)

Googleスプレッドシートの値を読み取ってBigQueryへクエリを発行し、閾値を超えていたらSlackへアラート通知する処理をGoogle Apps Script で構築します。

```JavaScript
/**
 * GCP Alert main function
 */
function main(){
  const spreadsheet = SpreadsheetApp.getActive();

  const data = spreadsheet
  .getSheetByName('master')
  .getDataRange()
  .getValues();

  readMaster(data);
}

/**
 * Read Master Spreadsheet and call runQuery()
 */
function readMaster(data) {
  try {
    if (data.length > 1) {
      for (var i = 0; i < data.length; i++) {
        if(i == 0){
          continue; // 1行目はスキップ
        }
        runQuery(data[i][1], data[i][2], data[i][3], data[i][4]);
      }
    }
  }
  catch(e) {
    Logger.log(e);
  }
}

/**
 * Runs a BigQuery query and check logs
 */
function runQuery(project, category, limit, query) {

  const projectId = 'synq-production';

  const request = {
    query: query,
    useLegacySql: false
  };
  const queryResults = BigQuery.Jobs.query(request, projectId);
  const jobId = queryResults.jobReference.jobId;

  // Check on status of the Query Job.
  const sleepTimeMs = 500;
  while (!queryResults.jobComplete) {
    Utilities.sleep(sleepTimeMs);
    sleepTimeMs *= 2;
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
  }

  // Get all the rows of results.
  const rows = queryResults.rows;
  while (queryResults.pageToken) {
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
      pageToken: queryResults.pageToken
    });
    rows = rows.concat(queryResults.rows);
  }

  if (rows) {
    // Append the results.
    const data = new Array(rows.length);
    for (var i = 0; i < rows.length; i++) {
      const cols = rows[i].f;
      data[i] = new Array(cols.length);
      for (var j = 0; j < cols.length; j++) {
        data[i][j] = cols[j].v;
      }
    }

    if(parseInt(data[0][0], 10) > parseInt(limit, 10)){
      Logger.log("gcp alert check comp ->(: " + "プロジェクト名: " + project + ", カテゴリ: " + category + ":) check gcp log!!");
      postSlack("GCPログを確認してください", project, category);

    }else{
      Logger.log("gcp alert check comp ->(: " + "プロジェクト名: " + project + ", カテゴリ: " + category + ":) no alert!!");
    }

  } else {
    Logger.log('No rows returned.');
  }
}

/**
 * Send Notification to Sack WebHook
 */
function postSlack(message, project, category) {

  //payload
  const payload  = { 
    'username'  : "Notification",
    'text'      : 'gcp alert check comp ->: ' + 'プロジェクト名: '+ project + ', カテゴリ: ' + category + ', メッセージ: ' +  message,
    'channel'   : "#alerts",
    'icon_emoji': ":chart_with_upwards_trend:"
  };

  const options = {
    'method'      : 'post'                 ,
    'contentType' : 'application/json'     ,
    'payload'     : JSON.stringify(payload),
  };

  // Webhook URL
  const url = '[SlackのWebhook URLを設定]'; 
  UrlFetchApp.fetch(url, options);
}
```

作成した処理をタイマーで実行できるように設定しておきます。

![スクリーンショット_2021-07-14_12_49_07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/bb1537d4-9719-bb89-df4e-1c3e3d44ad9b.jpeg)


#最後に

以上、Google Apps Scriptを用いてFirebaseの監査ログ収集とアラート通知を構築する手順でした。
処理を分岐したり、データ加工などが必要になってくると、`Pub/Sub` ＋ `CloudFunctison` で構築したほうが良いですが、簡単な監視 → アラート通知ならGoogle Apps Scriptだけで構築できてしまいます。
うまく使い分けて行きたいところです。

私が所属している株式会社クアンドではエンジニア募集中です。
地域産業のアップデートをミッションに、製造業や建設業など「現場」の課題を解決するためのアプリケーションを開発しています！
私もフルフレックス＆フルリモートで働いています。
興味のある方はWantedlyの募集ページをご覧ください！

https://www.wantedly.com/companies/quando