はじめに
最近アップロードされたファイルウィルススキャンする要件を満たすために、ClamAVをLambdaで動かしてウィルススキャンする様開発した機会があったので、ClamAVをAWS Lambdaで動かすまでの諸々を記載したいと思います。
下図の様にS3にファイルがアップロードされたトリガーで、Lambdaを起動し、LambdaでClamAVのスキャンを実行してウィルススキャンする流れです。
今回はClamAVは使ってウィルススキャンすることとなりましたが、
先日、AWS re:Inforce 2024でS3のファイルに対してマルウェア検出と保護の機能である「Amazon GuardDuty Malware Protection for Amazon S3」のリリースが発表されたため、今後S3のファイルに対して ClamAV を使ってウィルススキャンする方式が採用される機会は少なくなるのではと予想されますが、今回の開発を通して、ClamAVについて学ぶことがあったのでアウトプットします。
ClamAVとは
正式名称:Clam AntiVirus で 略称: ClamAV で本記事では略称で記載しています。
オープンソース (GPL) で提供されているクロスプラットフォームのウィルススキャンソフトウェアです。
ClamAV公式サイト 、ClamAVGithub
コマンド
Lambdaで実行する際に必要なClamAVのコマンドは2つだけです。
- clamscan
- freshclam
・clamscanclamscan {ファイルパス}
でファイルパスで指定したファイルに対してウィルススキャンを実行します。
下記サイトに安全なテスト用のマルウェアファイルがあるためダウンロードして使います。
https://www.eicar.org/download-anti-malware-testfile/
実行例: sh-5.2# ls /tmp EICAR.TXT eicar_com.zip eicarcom2.zip sh-5.2# clamscan /tmp WARNING: Failed to set locale LibClamAV Warning: ************************************************** LibClamAV Warning: *** The virus database is older than 7 days! *** LibClamAV Warning: *** Please update it as soon as possible. *** LibClamAV Warning: ************************************************** /tmp/eicar_com.zip: Win.Test.EICAR_HDB-1 FOUND /tmp/eicarcom2.zip: Win.Test.EICAR_HDB-1 FOUND /tmp/.gitignore: OK /tmp/EICAR.TXT: Win.Test.EICAR_HDB-1 FOUND ----------- SCAN SUMMARY ----------- Known viruses: 8685899 Engine version: 0.103.9 Scanned directories: 1 Scanned files: 4 Infected files: 3 Data scanned: 0.00 MB Data read: 0.00 MB (ratio 0.00:1) Time: 17.847 sec (0 m 17 s) Start Date: 2024:07:02 01:43:51 End Date: 2024:07:02 01:44:09
・ウィルス定義ファイルが古いとWarningが出力されます。
・ファイル毎のスキャン結果とSUMMARYが出力されます。
・SUMMARYには、スキャンしたファイル数、ウィルス検出したファイル数、処理時間等が出力されます。
・freshclam
ウィルス定義ファイルを更新します。
実行例: sh-5.2# freshclam ClamAV update process started at Fri Jul 5 18:24:34 2024 Trying to retrieve CVD header from https://database.clamav.net/daily.cvd Time: 1.3s, ETA: 0.0s [========================>] 512B/512B OK daily database available for update (local version: 27203, remote version: 27326) Current database is 123 versions behind. Downloading database patch # 27204... Time: 16.0s, ETA: 0.0s [========================>] 60.89MiB/60.89MiB Testing database: '/var/lib/clamav/tmp.01c90f3f6a/clamav-0ef08e4da800f4c5fe35e8fece256a5a.tmp-daily.cvd' ... Database test passed. daily.cvd updated (version: 27326, sigs: 2063837, f-level: 90, builder: raynman) Trying to retrieve CVD header from https://database.clamav.net/main.cvd main.cvd database is up-to-date (version: 62, sigs: 6647427, f-level: 90, builder: sigmgr) Trying to retrieve CVD header from https://database.clamav.net/bytecode.cvd bytecode.cld database is up-to-date (version: 335, sigs: 86, f-level: 90, builder: raynman) sh-5.2#
やること
- 前提
- ECR のコンテナイメージで動作する AWS Lambda を GitHub Actions からデプロイする を使い、LambdaはGitHub Actionsでデプロイします。
- Dockerfile 作成
- ClamAVを使ってウィルススキャンする Lambda 処理作成
- LambdaのCloudFormationテンプレート 作成
- GitHub Actions ワークフローファイル作成
- デプロイ前準備
- 動作テスト
Dockerfile 作成
- lambda/python:3.12 のイメージに、ClamAVをインストール
- Dockerビルド時にウィルス定義ファイルを更新 ・・・ Lambda起動の度にウィルス定義ファイルを更新すると処理時間がかかるためDockerビルド時に更新する様にしました
FROM public.ecr.aws/lambda/python:3.12 RUN set -ex && dnf -y update && dnf install -y tar gzip # Copy function code COPY lambda/ ${LAMBDA_TASK_ROOT} # Install dependencies RUN pip3 install -r requirements.txt # Install ClamAV RUN set -ex && dnf install -y clamav clamav-update # Update virus definition file RUN set -ex && freshclam ENV TZ='Asia/Tokyo' CMD [ "app.lambda_handler" ]
ClamAVを使ってウィルススキャンする Lambda 処理作成
- S3イベントトリガーで渡ってくる引数からバケット、ファイルパスを取得してLambda処理内にダウンロード
- ダウンロードしたファイルをClamAVでスキャン
import os import json import boto3 import subprocess import urllib.parse s3 = boto3.client('s3') def lambda_handler(event, context): """ Lambdaメイン処理 """ print(json.dumps(event, ensure_ascii=False)) # s3からファイルダウンロード bucket_name = event['Records'][0]['s3']['bucket']['name'] key = urllib.parse.unquote_plus( event['Records'][0]['s3']['object']['key'], encoding='utf-8') file_name = os.path.basename(key) download_path = f'/tmp/{file_name}' s3.download_file(bucket_name, key, download_path) print('スキャン対象:'+event['Records'][0]['s3']['object']['key']) # スキャン対象ファイルの存在出力 res = subprocess.run(["ls", "-la", "/tmp"], stdout=subprocess.PIPE, text=True) print(res.stdout) # ClamAV スキャン実行 res = subprocess.run(["clamscan", download_path], stdout=subprocess.PIPE, text=True) print(res.stdout) return { 'statusCode': 200, }
LambdaのCloudFormationテンプレート 作成
- Lambda、Lambdaの実行ロール、S3、S3イベントトリガーを定義
- Lambdaは指定されたECR イメージのコンテナを起動する指定
- Lambdaのメモリーは1536M(1.5G)を指定、1024MだとClamAVのスキャンが実行できない(タイムアウトする)
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Parameters: Project: Type: String AccountId: Type: String Resources: ClamAVFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub ${Project} CodeUri: lambda/ MemorySize: 1536 Timeout: 900 EphemeralStorage: Size: 512 Role: !GetAtt ClamAVFunctionRole.Arn PackageType: Image ImageUri: !Sub ${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${Project}-ecr:latest ImageConfig: Command: - "app.lambda_handler" Events: S3TriggerFunctionEvent: Type: S3 Properties: Bucket: !Ref ClamAVTriggerBucket Events: s3:ObjectCreated:* ClamAVFunctionRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub ${Project}-run-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: !Sub ${Project}-run-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - Effect: Allow Action: - s3:GetObject Resource: "*" ClamAVTriggerFunctionPermission: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt ClamAVFunction.Arn Principal: "s3.amazonaws.com" SourceArn: !GetAtt ClamAVTriggerBucket.Arn ClamAVTriggerBucket: Type: "AWS::S3::Bucket" Properties: BucketName: !Sub ${Project}-${AWS::AccountId}
GitHub Actions ワークフローファイル作成
- ECR のコンテナイメージで動作する AWS Lambda を GitHub Actions からデプロイするのGitHub Actions ワークフローファイルを一部修正して使います。
--capabilities CAPABILITY_IAM
⇒--capabilities CAPABILITY_NAMED_IAM
へ修正します。CloudFormationでIAMロールを作成するため指定を変える必要があります。
name: Lambda Deploy Development on: workflow_dispatch: env: AWS_ACCOUNT_ID: XXXXX AWS_IAM_ROLE_ARN: arn:aws:iam::XXXXX:role/lambda-ecr-deploy-role AWS_DEFAULT_REGION: ap-northeast-1 PROJECT: clamav-lambda SAM_CLI_TELEMETRY: 1 jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ env.AWS_IAM_ROLE_ARN }} role-session-name: github-actions-${{ github.run_id }} aws-region: ${{ env.AWS_DEFAULT_REGION }} - run: aws sts get-caller-identity - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - name: Build Docker working-directory: ./ run: | docker build -t ${{ env.PROJECT }}-ecr -f ./Dockerfile . docker tag ${{ env.PROJECT }}-ecr:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest - name: Push Image to Amazon ECR working-directory: ./ run: | docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest - name: ECR Lambda Stack env: STACK_NAME: ${{ env.PROJECT }}-stack TEMPLATE_NAME: lambda-stack.yaml working-directory: ./ run: | sam build --use-container --template-file ${TEMPLATE_NAME} sam deploy --no-confirm-changeset --stack-name ${STACK_NAME} --template-file ${TEMPLATE_NAME} --s3-bucket ${{ env.PROJECT }}-${AWS_ACCOUNT_ID} --image-repository ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr --parameter-overrides Project=${{ env.PROJECT }} AccountId=${AWS_ACCOUNT_ID} --capabilities CAPABILITY_NAMED_IAM --no-fail-on-empty-changeset - name: Lambda Renew Image run: | aws lambda update-function-code --function-name ${{ env.PROJECT }} --image-uri ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest
デプロイ前準備とデプロイ
- 先にECR リポジトリ作成します。LambdaのCloudFormationテンプレートで指定されている名前でプライベートなECR リポジトリを作成します。
- ECR のコンテナイメージで動作する AWS Lambda を GitHub Actions からデプロイする では、CloudFormationテンプレートでLambda関数のみ作成していますが、今回は、IAMロール、S3バケット、S3イベントトリガーを作成するために、必要な権限をGitHub ActionsのIAMロールに権限付与します。
- GitHub Actions 実行します。実行例は、こちらの記事 を参考にしてください。
動作テスト
- https://www.eicar.org/download-anti-malware-testfile/のテストファイルをS3へアップロードします。
- S3イベントトリガーでLambdaの動作結果をCloudWatchのログで確認します。