对上传到 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 中运行的恶意软件扫描工具服务。
  • 恶意软件扫描工具服务会扫描上传的文档是否包含恶意软件。
  • 如果文档受感染,则服务会将其移到隔离的存储分区;否则,该文档会被移到另一个存储分区以存放未受感染的已扫描文档。

目标

  • 构建一个 App Engine 柔性环境恶意软件扫描工具服务,以使用 ClamAV 扫描文档是否包含恶意软件。

  • 构建 Node.js Cloud Functions 函数,以在将文档上传到 Cloud Storage 时调用恶意软件扫描工具服务。

  • 构建服务,根据扫描结果将扫描的文档移到干净存储分区或隔离的存储分区。

费用

本教程使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用量来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本教程后,您可以删除所创建的资源以避免继续计费。如需了解详情,请参阅清理

准备工作

  1. 登录您的 Google Cloud 帐号。如果您是 Google Cloud 新手,请创建一个帐号来评估我们的产品在实际场景中的表现。新客户还可获享 $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 Shell 会话随即会在 Cloud Console 的底部启动,并显示命令行提示符。Cloud Shell 是一个已安装 Cloud SDK 的 Shell 环境,其中包括 gcloud 命令行工具以及已为当前项目设置的值。该会话可能需要几秒钟时间来完成初始化。

  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. 创建三个具有唯一名称的 Cloud Storage 存储分区:

    Cloud Shell

    1. 创建三个存储分区:

      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 Shell 设置为指向 Cloud Console 中的活动 Google Cloud 项目。它用于确保存储分区名称是唯一的。

    Cloud Console

    1. 在 Cloud Console 中,转到浏览器

      转到浏览器

    2. 点击创建存储分区

    3. 存储分区名称文本框中,输入存储分区 unscanned-PROJECT_ID 的名称,然后点击创建

      PROJECT_ID 替换为您的 Google Cloud 项目 ID。

    4. 重复这些步骤以另外创建两个名为 quarantined-PROJECT_IDscanned-PROJECT_ID 的存储分区。

    您创建的三个存储分区会在流水线的不同阶段保存文档:

    • unscanned-PROJECT_ID:在文档处理前保存文档。这是您将文档上传到其中的存储分区。PROJECT_ID 表示您的 Google Cloud 项目 ID。
    • quarantined-PROJECT_ID:保存恶意软件扫描工具服务所扫描并认为包含恶意软件的文档。
    • scanned-PROJECT_ID:保存恶意软件扫描工具服务所扫描并发现未受感染的文档。

在 App Engine 中创建恶意软件扫描工具服务

在本部分中,您将部署 server.js 脚本以在 App Engine 柔性环境中运行恶意软件扫描工具服务。该服务在 App Engine 柔性环境的 Docker 容器中运行,并包含以下内容:

  • 用于恶意软件扫描工具服务的 Node.js 脚本 server.js
  • 用于通过服务和 ClamAV 二进制文件构建映像的 Dockerfile。
  • app.yaml 文件,此配置文件汇总了部署到 App Engine 的服务的定义。
  • 用于在容器启动时运行 clamAV 和 freshclam 守护程序的 bootstrap.sh shell 脚本。

创建恶意软件扫描工具服务:

  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

    在应用部署完成后,请记下输出内容中的服务网址。在后续步骤中,您需要应用的网址。服务网址的格式如下:

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

    如果这是您的第一项 App Engine 服务,则服务网址的格式如下:

    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 函数以触发恶意软件扫描工具服务

在这些步骤中,您将部署 index.js 脚本,该脚本包含当文档上传到 unscanned-PROJECT_ID Cloud Storage 存储分区时调用的 Cloud Functions 函数。此函数作为后台函数运行,而且会被调用以响应 Cloud Storage 事件(如上传新文档或更改文档版本)。

Cloud Shell

  1. 在 Cloud Shell 中,将目录更改为所克隆代码库的 function-scantrigger-node 文件夹:

    cd ../function-scantrigger-node
    
  2. 部署函数,将 https://malware-scanner-dot-PROJECT_ID.appspot.com 替换为您之前复制的服务网址。

    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. 要执行的函数文本框中,将 helloWorld 替换为 requestMalwareScan

  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. 字段中,输入您之前附加了 /scan 的恶意软件扫描工具服务网址。

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

    如果这是您的第一项 App Engine 服务,则服务网址采用以下格式:

    https://PROJECT_ID.appspot.com/scan
    
  16. 点击保存。函数旁边的绿色对勾标记表示部署成功。

通过上传文件测试流水线

您可以上传一个干净(不含恶意软件)的文件和一个已受感染的文件来测试流水线。

  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,然后点击关闭以删除项目。

后续步骤