Cloud Storage にアップロードされたドキュメントのマルウェア スキャンを自動化する

このチュートリアルでは、悪意のあるコードのドキュメントの評価を自動化するためのイベント ドリブン パイプラインを構築する方法について説明します。

Cloud Storage にアップロードされた多数のドキュメントを手動で評価することは、ほとんどのアプリにとって時間がかかりすぎます。

このパイプラインは、Google Cloud プロダクトと、ClamAV というオープンソースのウィルス対策エンジンを使用して構築されています。このチュートリアルでは、ClamAV は App Engine フレキシブル環境でホストされている Docker コンテナで実行されます。パイプラインは、マルウェアに感染したドキュメントが検出されると、Cloud Logging にログエントリを書き込みます。

これらの Logging ログエントリを使用して、感染したドキュメントのログベースのアラートをトリガーできますが、このようなアラートの設定はこのチュートリアルの対象外です。

このチュートリアルでは、「マルウェア」という用語を、トロイの木馬、ウィルス、その他の悪意のあるコードを指す包括的な用語として使用します。

このチュートリアルでは、Cloud Storage、App EngineCloud FunctionsDockerNode.js の基本的な機能について理解していることを前提としています。

アーキテクチャ

次の図は、パイプラインのステップの概要を示しています。

マルウェア スキャン パイプラインのアーキテクチャ。

次のステップは、アーキテクチャ パイプラインの概要を示しています。

  • ファイルを Cloud Storage にアップロードします。
  • アップロード イベントが自動的に Cloud Functions の関数をトリガーします。
  • この Cloud Functions の関数が、App Engine で実行されているマルウェア スキャン サービスを呼び出します。
  • マルウェア スキャン サービスが、アップロードされたドキュメントのマルウェアをスキャンします。
  • ドキュメントが感染している場合は、隔離されたバケットに移動されます。それ以外の場合は、感染していないスキャン済みドキュメントを保持する別のバケットにドキュメントが移動されます。

目標

  • ClamAV を使用してドキュメントのマルウェアをスキャンする App Engine フレキシブル環境のマルウェア スキャン サービスを構築します。

  • ドキュメントが Cloud Storage にアップロードされたときにマルウェア スキャン サービスを呼び出す Node.js の Cloud Functions の関数を構築します。

  • スキャン結果に基づいて、スキャンされたドキュメントをクリーンなバケットまたは隔離されたバケットに移動するサービスを構築します。

料金

このチュートリアルでは、課金対象である次の Google Cloud コンポーネントを使用します。

料金計算ツールを使用すると、予想使用量に基づいて費用の見積もりを算出できます。新しい Google Cloud ユーザーは無料トライアルをご利用いただけます。

このチュートリアルを終了した後、作成したリソースを削除すると、それ以上の請求は発生しません。詳しくは、クリーンアップをご覧ください。

始める前に

  1. Google Cloud アカウントにログインします。Google Cloud を初めて使用する場合は、アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  3. Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Cloud Functions and App Engine API を有効にします。

    API を有効にする

  5. Cloud Console で、Cloud Shell をアクティブにします。

    Cloud Shell をアクティブにする

    Cloud Console の下部にある Cloud Shell セッションが開始し、コマンドライン プロンプトが表示されます。Cloud Shell はシェル環境です。gcloud コマンドライン ツールなどの Cloud SDK がすでにインストールされており、現在のプロジェクトの値もすでに設定されています。セッションが初期化されるまで数秒かかることがあります。

  6. このチュートリアルでは、すべてのコマンドを Cloud Shell で実行します。

環境設定

このセクションでは、チュートリアル全体で使用される値(リージョンやゾーンなど)の設定を行います。このチュートリアルでは、リージョンとして us-central1 を使用し、ゾーンとして us-central1-b を使用します。

  1. Cloud Shell で、リージョンとゾーンを設定します。

    gcloud config set compute/zone us-central1-b
    
  2. Google Cloud プロジェクト ID の環境変数を作成します。

    export PROJECT_NUMBER=$(gcloud projects describe $DEVSHELL_PROJECT_ID \
        --format='value(projectNumber)')
    
  3. 一意の名前で 3 つの Cloud Storage バケットを作成します。

    Cloud Shell

    1. 次の 3 つのバケットを作成します。

      gsutil mb gs://unscanned-$DEVSHELL_PROJECT_ID
      gsutil mb gs://quarantined-$DEVSHELL_PROJECT_ID
      gsutil mb gs://scanned-$DEVSHELL_PROJECT_ID
      

      $DEVSHELL_PROJECT_ID は、Cloud Console でアクティブな Google Cloud プロジェクトを指すように Cloud Shell を設定する環境変数です。バケット名を一意にするために使用されます。

    Cloud Console

    1. Cloud Console で、[ブラウザ] に移動します。

      [ブラウザ] に移動

    2. [バケットを作成] をクリックします。

    3. [バケット名] テキスト ボックスにバケットの名前「unscanned-PROJECT_ID」を入力し、[作成] をクリックします。

      PROJECT_ID を実際の Google Cloud プロジェクト ID に置き換えます。

    4. 上記のステップを繰り返して、quarantined-PROJECT_IDscanned-PROJECT_ID という 2 つのバケットをさらに作成します。

    作成した 3 つのバケットは、パイプラインのさまざまな段階でドキュメントを保持します。

    • unscanned-PROJECT_ID: 処理前のドキュメントを保持します。これは、ドキュメントのアップロード先となるバケットです。PROJECT_ID は Google Cloud プロジェクト ID を表します。
    • quarantined-PROJECT_ID: マルウェア スキャン サービスでスキャンされ、マルウェアが含まれていると判断されたドキュメントが保持されます。
    • scanned-PROJECT_ID: マルウェア スキャン サービスでスキャンされ、感染していないと判断されたドキュメントが保存されます。

App Engine にマルウェア スキャン サービスを作成する

このセクションでは、server.js スクリプトをデプロイして、App Engine フレキシブル環境でマルウェア スキャン サービスを実行します。このサービスは App Engine フレキシブル環境の Docker コンテナで実行され、次のものが含まれます。

  • マルウェア スキャン サービス用の server.js という Node.js スクリプト。
  • サービスと ClamAV バイナリを含むイメージを構築する Dockerfile。
  • app.yaml ファイル。AppEngine にデプロイされたサービスの定義が記述された構成ファイル。
  • コンテナの起動時に ClamAV と freshclam デーモンを実行する bootstrap.sh シェル スクリプト。

マルウェア スキャン サービスを作成する

  1. Cloud Shell で、コードファイルを含む GitHub リポジトリのクローンを作成します。

    git clone https://github.com/GoogleCloudPlatform/docker-clamav-malware-scanner.git
    
  2. appengine-malwarescanningservice-node ディレクトリに移動します。

    cd malware-scanner-tutorial/appengine-malwarescanningservice-node
    
  3. 次の sed コマンドを実行して、app.yaml ファイル内のプレースホルダを Google Cloud プロジェクト ID に置き換えます。

    sed -i -e "s/PROJECT_ID/$DEVSHELL_PROJECT_ID/g" app.yaml
    
  4. これが App Engine にデプロイする最初のサービスの場合は、このディレクトリの現在の app.yaml ファイルでサービス名を default に設定します。

    service: default
    

    App Engine に初めてデプロイするサービスでない場合は、サービス名を以下のように置き換えます。

    sed -i -e "s/malware-scanner/default/g" app.yaml
    

    App Engine サービスの構造の詳細については、デフォルト サービスをご覧ください。

  5. サービスを作成して App Engine にデプロイします。

    gcloud app create --project=$DEVSHELL_PROJECT_ID --region=us-central
    gcloud app deploy
    
  6. プロンプトが表示されたら、「Y」と入力します。

    アプリのデプロイが完了したら、出力でサービス URL をメモします。後のステップでアプリの URL が必要になります。サービス URL の形式は次のとおりです。

    https://service-name-dot-PROJECT_ID.appspot.com
    

    これが最初の App Engine サービスの場合、サービス URL は次の形式になります。

    https://PROJECT_ID.appspot.com
    

バケットの権限を割り当てる

  1. App Engine フレキシブル環境のサービス アカウント名を確認します。これは、作成したバケットにアクセスする権限を割り当てる次のステップで必要なためです。サービス アカウントの形式は次のとおりです。

    service-${PROJECT_NUMBER}@gae-api-prod.google.com.iam.gserviceaccount.com
    
  2. Cloud Shell で、App Engine サービス アカウントを roles/storage.legacyBucketWriter ロールを持つメンバーとして unscanned-PROJECT_ID バケットに追加します。

    gsutil iam ch \
        serviceAccount:service-${PROJECT_NUMBER}@gae-api-prod.google.com.iam.gserviceaccount.com:roles/storage.legacyBucketWriter \
        gs://unscanned-$DEVSHELL_PROJECT_ID
    
  3. App Engine サービス アカウントを roles/storage.objectCreator ロールを持つメンバーとして quarantined-PROJECT_ID バケットに追加します。

     gsutil iam ch \
         serviceAccount:service-${PROJECT_NUMBER}@gae-api-prod.google.com.iam.gserviceaccount.com:roles/storage.objectCreator \
         gs://quarantined-$DEVSHELL_PROJECT_ID
    
  4. App Engine サービス アカウントを roles/storage.objectCreator ロールを持つメンバーとして scanned-PROJECT_ID/var> バケットに追加します。

    gsutil iam ch \
        serviceAccount:service-${PROJECT_NUMBER}@gae-api-prod.google.com.iam.gserviceaccount.com:roles/storage.objectCreator \
        gs://scanned-$DEVSHELL_PROJECT_ID
    

マルウェア スキャン サービスをトリガーする Cloud Functions の関数を作成する

これらの手順では、ドキュメントが unscanned-PROJECT_ID Cloud Storage バケットにアップロードされたときに呼び出される Cloud Functions の関数を含む index.js スクリプトをデプロイします。この関数はバックグラウンド関数として実行され、新しいドキュメントのアップロードやドキュメント バージョンの変更などの Cloud Storage イベントに応答して呼び出されます。

Cloud Shell

  1. Cloud Shell で、ディレクトリをクローンとして作成されたリポジトリの function-scantrigger-node フォルダに変更します。

    cd ../function-scantrigger-node
    
  2. 関数をデプロイして、https://malware-scanner-dot-PROJECT_ID.appspot.com を以前にコピーしたサービス URL に置き換えます。

    gcloud functions deploy requestMalwareScan \
        --runtime nodejs8 \
        --set-env-vars SCAN_SERVICE_URL=your-service-url/scan \
        --trigger-resource gs://unscanned-$DEVSHELL_PROJECT_ID \
        --trigger-event google.storage.object.finalize
    
  3. 関数が正常にデプロイされたことを確認します。

    gcloud functions describe requestMalwareScan
    

    デプロイが成功すると、次のような準備完了ステータスが表示されます。

    status:  ACTIVE
    timeout:  60s
    

GCP Console

  1. Cloud Console で、Cloud Functions の概要ページに移動します。

    Cloud Functions の概要ページに移動

  2. Cloud Functions を有効にしたプロジェクトを選択します。

  3. [関数を作成] をクリックします。

  4. [名前] ボックスで、デフォルトの名前を requestMalwareScan で置き換えます。

  5. [トリガー] フィールドで、[Cloud Storage] を選択します。

  6. [バケット] フィールドで [参照] をクリックし、プルダウン リストで unscanned-PROJECT_ID バケットをクリックして、[選択] をクリックします。

  7. [ランタイム] で Node.js 8 を選択します。

  8. [ソースコード] で [インライン エディタ] をオンにします。

  9. [index.js] ボックスに次のコードを貼り付け、既存のテキストを置き換えます。

    /*
    * Copyright 2019 Google LLC
    
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    
    *     https://www.apache.org/licenses/LICENSE-2.0
    
    * Unless required by applicable law or agreed to in writing, software
    * distributed under the License is distributed on an "AS IS" BASIS,
    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    * See the License for the specific language governing permissions and
    * limitations under the License.
    */
    
    const request = require('request-promise');
    
    /**
     * Background Cloud Function that handles the 'google.storage.object.finalize'
     * event. It invokes the Malware Scanner service running in App Engine Flex
     * requesting a scan for the uploaded document.
     *
     * @param {object} data The event payload.
     * @param {object} context The event metadata.
     */
    exports.requestMalwareScan = async (data, context) => {
    
      const file = data;
      console.log(`  Event ${context.eventId}`);
      console.log(`  Event Type: ${context.eventType}`);
      console.log(`  Bucket: ${file.bucket}`);
      console.log(`  File: ${file.name}`);
    
      let options = {
        method: 'POST',
        uri: process.env.SCAN_SERVICE_URL,
        body: {
          location: `gs://${file.bucket}/${file.name}`,
          filename: file.name,
          bucketname: file.bucket
        },
        json: true
      }
    
      try {
        if(context.eventType === "google.storage.object.finalize") {
          await request(options);
          console.log(`Malware scan succeeded for: ${file.name}`);
        } else {
          console.log('Malware scanning is only invoked when documents are uploaded or updated');
        }
      } catch(e) {
        console.error(`Error occurred while scanning ${file.name}`, e);
      }
    }
  10. [実行する関数] テキスト ボックスで helloWorldrequestMalwareScan に置き換えます。

  11. [package.json] テキスト ボックスに次のコードを貼り付け、既存のテキストを置き換えます。

    {
      "name": "function_malware_scanner",
      "version": "1.0.0",
      "description": "Triggers the Malware Scanner service when a document is uploaded to Cloud Storage",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "Google LLC.",
      "license": "Apache-2.0",
      "dependencies": {
        "request": "^2.88.0",
        "request-promise": "^4.2.4"
      }
    }
    
  12. [その他] をクリックし、[サービス アカウント] を [App Engine デフォルト サービス アカウント] に設定します。

  13. [環境変数] をクリックします。

  14. [名前] フィールドに「SCAN_SERVICE_URL」と入力します。

  15. [] フィールドに、以前にコピーしたマルウェア スキャン サービスの URL に「/scan」を加えて入力します。

    https://malware-scanner-dot-PROJECT_ID.appspot.com/scan
    

    これが最初の App Engine サービスの場合、サービス URL は次の形式になります。

    https://PROJECT_ID.appspot.com/scan
    
  16. [保存] をクリックします。関数の横にある緑色のチェックマークは、デプロイが成功したことを示します。

ファイルをアップロードしてパイプラインをテストする

クリーンな(マルウェアのない)ファイルと感染したファイルを 1 つずつアップロードして、パイプラインをテストします。

  1. サンプル テキスト ファイルを作成するか、既存のクリーンなファイルを使用して、パイプライン プロセスをテストします。

  2. サンプル データファイルをスキャンされていないファイル バケットにコピーします。

    gsutil cp filename gs://unscanned-$DEVSHELL_PROJECT_ID
    

    filename を、クリーンなテキスト ファイルの名前に置き換えます。マルウェア スキャン サービスは、各ドキュメントを検査し、適切なバケットに移動します。このドキュメントは scanned-PROJECT_ID バケットに移動されます。

  3. scanned-PROJECT_ID バケットをチェックして、処理されたドキュメントがあるかどうかを確認します。

    gsutil ls -r gs://scanned-PROJECT_ID
    
  4. Cloud Shell で eicar-infected.txt というドキュメントを作成し、マルウェアのテキストを追加して、感染したドキュメントが unscanned-PROJECT_ID バケットにアップロードされたときのワークフローをテストします。

    echo -e 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' \
        > eicar-infected.txt
    
  5. ドキュメントを unscanned-PROJECT_ID バケットにアップロードします。

    gsutil cp eicar-infected.txt gs://unscanned-$DEVSHELL_PROJECT_ID
    
  6. パイプラインでのドキュメント処理にかかる数秒の後、quarantined-PROJECT_ID バケットをチェックし、ドキュメントがパイプラインを正常に通過したかどうかを確認します。また、マルウェアに感染したドキュメントが検出されると、Logging のログエントリが記録されます。

    gsutil ls -r gs://quarantined-PROJECT_ID
    

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

プロジェクトの削除

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

次のステップ