亚马逊AWS官方博客

使用 Amazon Lambda 集成 ClamAV 进行病毒扫描

概览

云上安全是个重要的话题,安全技术涉及领域非常广泛,如何保证业务能够在云上安全运行是一项艰巨的工作。亚马逊云科技提供了多种安全服务包括:网络安全、数据安全、攻击防护、主机安全和代码安全等等,来帮助客户实现在云上业务的安全运行。同时亚马逊云科技拥有众多安全产品的合作伙伴帮助客户完善自身的安全体系。有一些客户是多云部署或混合云部署,他们希望在多云或者混合云中使用开源安全服务来保证业务的安全运行,同时在多个环境中统一服务版本,从而较少运维成本。这篇博客中,我们将使用 Amazon Serverless 服务 Lambda 封装开源病毒扫描工具 ClamAV 对 S3 中的文件进行病毒扫描,将扫描结果进行可视化,并配置病毒发现通知机制。

ClamAV:是 Linux 操作系统上最流行的防病毒软件、GPL 协议免费发布、用于检测木马,病毒,恶意软件和其他恶意威胁的一个开源杀毒引擎。

架构

本方案主要会涉及以下服务:

  • Amazon EventBridge
  • Amazon Lambda
  • Amazon CloudWatch
  • Amazon SNS
  • Amazon SQS

首先 Amazon EventBridge 会定时(每天晚上 24:00 点)触发一个 Lambda 函数,该函数会统计出前一天 S3 新上传的对象文件列表,为每个对象文件生成一个扫描任务,并将任务事件推送到 Amazon SQS 服务中,让后续扫描服务异步对文件进行病毒扫描。在此之前,我们需要将 ClamAV 服务封装成 Docker Image,并将其推送到 Amazon ECR 当中。以这个 Docker Image 为基础创建一个 Lambda 病毒扫描函数,该函数会接收 Amazon SQS 中的病毒扫描任务,通过任务携带的 S3 Bucket 和 object 信息下载要进行病毒扫描的对象文件到本地临时存储目录中,然后再调用 ClamAV 扫描工具对该文件进行病毒扫描,并将结果以 Metric 的形式发送到 CloudWatch 中,如果发现文件中包含病毒的可能性,会利用 Amazon SNS 发送通知呼叫人工介入处理。

部署步骤

1. 将 ClamAV 封装成 Docker Image 并推送到 Amazon ECR

提前在 Amazon ECR 创建 Repository 用于存储 Lambda Docker image。使用 Amazon EC2 作为 Docker Image 的打包环境,准备好用于生成 Docker Image 的 Dockerfile 和 app.py。

Dockerfile 内容如下:

FROM public.ecr.aws/lambda/python:3.9

COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/

RUN python3 -m pip install boto3

#install clamav
RUN yum install -y wget

RUN yum install -y clamav

RUN freshclam

RUN clamscan -V

#copy virus file
COPY ./eicar.com ${LAMBDA_TASK_ROOT}/

CMD [ "app.handler" ]

app.py 是 Lambda 执行入口函数,包含了 Lambda 执行过程中的各个步骤。代码如下:

import re
import os
import sys
import json
import boto3
import subprocess

def put_cloudwatch_metric(Region, ScanResult):
    try:
        cw = boto3.client('cloudwatch', region_name=Region)
        cw.put_metric_data(Namespace='VirusScanResult',
            MetricData=[{
                'MetricName': ScanResult,
                'Values': [1.0],
                'Unit': 'Count'
            }])
    except Exception as e:
        print(str(e))

def download_scan_object(region, bucket, key):
    try:
        #download object from s3
        s3 = boto3.client('s3', region_name=region)
        with open('/tmp/temp_file', 'wb') as data:
            s3.download_fileobj(bucket, key, data)

        #scaning
        pipe = subprocess.Popen('clamscan /tmp/temp_file', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        scan_result = pipe.stdout.read().decode()

        #update object tag
        TagValue = 'Scaning'
        result = re.search('Infected files: 1', scan_result)
        if result is not None:
            TagValue = 'Infected'
        else:
            TagValue = 'Not Infected'
        s3.put_object_tagging(Bucket=bucket,
            Key=key,
            Tagging={
                'TagSet': [{'Key': 'Clamav scan result', 'Value': TagValue}]
            })
        return TagValue
    except Exception as e:
        print(str(e))
        return None

def send_sns_message(region, message):
    try:
        sns = boto3.client('sns', region_name=region)
        sns.publish(
            TopicArn = os.environ['TopicArn'],
            Message=message
            )
    except Exception as e:
        print(str(e))

def handler(event, context):
    try:
        #Parsing SQS Event parameters
        MessageBody = json.loads(event['Records'][0]['body'])
        bucket = MessageBody['bucket']
        Key = MessageBody['key']

        ScanResult = download_scan_object(os.environ['region'], bucket, Key)
        put_cloudwatch_metric(os.environ['region'], ScanResult)

        if ScanResult == 'Infected':
            meesage = 'Bucket:%s, Key:%s  has been infected, Please manually review.' % (bucket, Key)
            send_sns_message(os.environ['region'], meesage)
    except Exception as e:
        print(str(e))

1) 在 EC2 环境中执行 Docker 打包 Image 命令(请提前在 EC2 中安装 Docker 环境)

docker build -t lambda-clamav .

2) 在 EC2 上登录刚刚创建的 ECR Repository

aws ecr get-login-password --region cn-northwest-1 | docker login --username AWS --password-stdin <account-id>.dkr.ecr.cn-northwest-1.amazonaws.com.cn

3) 为刚刚创建的 Docker Image 打 Tag

docker tag lambda-clamav:latest <account-id>.dkr.ecr.cn-northwest-1.amazonaws.com.cn/clamav:latest

4) 将打好 Tag 的 Image 推送到刚刚创建好的 ECR Repository 中

docker push <account-id>.dkr.ecr.cn-northwest-1.amazonaws.com.cn/clamav:latest

5) 执行成功以上步骤后,我们在 ECR 中看到最新上传的 Docker Image

2. 创建病毒扫描 Lambda 函数和任务分发 Lambda 函数

1) 创建 Lambda 执行所需要的 IAM Role: LambdaScanRole, 并配置所需的 Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricStream",
                "s3:GetObject",
                "logs:CreateLogStream",
                "cloudwatch:PutMetricData",
                "sqs:ReceiveMessage",
                "sqs:SendMessage",
                "s3:PutObjectTagging",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

2) 创建病毒扫描 Lambda 函数 SQSTrigerScanLambda,使用刚推送到 ECR 的 Docker Image 创建函数,并配置刚创建的 IAM Role。 配置 Lambda 参数,由于 Lambda 需要将 S3 object 下载到本地临时存储,对象大小可能比较大耗时比较长,ClamAV 扫描文件也需要时间,所以将 Timeout 时间修改为 5min(后期可以根据实际情况自行调整)。Lambda 会将 S3 对象下载到 Ephemeral storage 中然后进行扫描,默认 Ephemeral storage 存储空间是 512MB(请根据实际情况进行调整)。


3) 创建任务分发 Lambda 函数 EventTrigerCreateScanJobLambda。该函数会为指定 S3 Bucket 目录中的对象生成一个扫描任务,并将任务发送到指定 SQS 中。代码如下:

import os
import json
import boto3
from datetime import *

def get_object_list(region, bucket, prefix):
  try:
    object_list = []
    s3 = boto3.client('s3', region_name=region)
    response = s3.list_objects(Bucket=bucket, Prefix=prefix)
    for r in response['Contents']:
      if not r['Key'].endswith('/'):
        object_list.append(r['Key'])
    return object_list
  except Exception as e:
    print(str(e))

def send_sqs_message(region, meessages):
  try:
    sqs = boto3.client('sqs', region_name=region)

    for Body in meessages:
      result = sqs.send_message(
        QueueUrl=os.environ['SQSQueueUrl'],
        MessageBody= Body
        )
  except Exception as e:
    print(str(e))

def lambda_handler(event, context):
    bodys = []
    prefix = datetime.now().strftime("%Y/%m/%d")
    object_list = get_object_list(os.environ['region'], os.environ['bucket'], prefix)
    for key in object_list:
        bodys.append('{"bucket": "%s", "key": "%s"}' % (str(os.environ['bucket']),key))
    send_sqs_message(os.environ['region'], bodys)

3. 配置 Amazon EventBridge 定时任务触发器和 SQS 任务队列

1) 创建一个标准 SQS,并配置 Lambda 触发器,触发函数为刚刚创建的 Lambda Function SQSTrigerScanLambda。

2) 在 Amazon EventBridge 中创建计划任务,每天 24 点 EventBridge 调用 Lambda 函数 EventTrigerCreateScanJobLambda 收集前一天 S3 新增对象并为每个对象产生一个病毒扫描任务。


4. 配置 CloudWatch 监

封装了 ClamAV 的 Lambda 函数 SQSTrigerScanLambda 执行完扫描工作会将扫描结果以 Metric 值的形式发送到 CloudWatch。我们可以用这个自定义 Metric 设计监控 Dashboard。发送 Metric 到 CloudWatch 的代码段如下:


5. 配置 Amazon SNS 发现病毒通知流程

在 SNS 中创建 ScanVirusNotice Topic,并绑定接收通知的订阅邮箱。

6. 更新 Lambda 函数运行环境变量

在 Lambda 函数 EventTrigerCreateScanJobLambda 中配置 region、bucket、SQSQueueUrl 三个环境变量,region 为 S3 Bucket 所在区域,bucket 为要扫描的 Bucket 名字,SQSQueueUrl 为任务队列的 URL。例如下图:

在 Lambda 函数 SQSTrigerScanLambda 中配置 Region 和 TopicArn 两个环境变量。Region 是 S3 Bucket 所在区域,TopicArn 是 SNS 发送通知 Topic 的 ARN。例如下图:

总结

可视化:在这个方案里,扫描任务会将结果以 Tag 的形式更新到对象的属性中,方便用户了解该对象扫描的情况。同时扫描任务也会将结果以 Metric 的形式发送到 CloudWatch,我们利用这个 Metric 设计了监控 Dashboard,方便用户了解扫描对象数量和其中包含病毒的对象比例。


优势:由于该方案我们采用无服务架构,实际资源消耗量根据客户扫描对象数量而定,不需要客户预制任何计算资源,客户也不用担心资源浪费或不足的情况。Amazon Serverless 服务让客户无需担心服务的可用性,从而简化了客户的运维成本。此方案中,核心病毒扫描程序 ClamAV 属于开源代码,方便客户在多云和混合云中保持方案一致性。

注意:该方案中,我们已经将 S3 Bucket 的目录按照时期进行进行划分,例如:S3://BucketName/2022/12/22/,每天定时任务触发 Lambda 会列出当天目录下的所有对象,并产生扫描任务。如果用户对要扫描任务延迟要求比较低,可以缩短定时任务的时间间隔,改为每小时触发一次扫描任务,或者改为 S3 触发 Lambda 的扫描形式。

本篇作者

刘欣然

AWS 解决方案架构师, 目前负责互联网媒体行业云端应用的架构设计与技术咨询。在加入 AWS 之前从事多年互联网开发工作,目前专注于 Devops 与边缘计算领域。

王文巍

亚马逊云科技资深解决方案架构师,10 多年互联网企业研发、团队管理经验,主要专注于电商、新零售、社交媒体等领域。