2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GuardDuty Malware Protection for S3 を使って、アップロードファイルの検知・振り分けしてみた

2
Last updated at Posted at 2026-10-22

はじめに

S3にユーザーがファイルをアップロードするシステムを運用している場合、マルウェアが含まれたファイルが本番環境に混入するリスクがあります。

本記事では、Amazon GuardDuty Malware Protection for S3 を使って、S3にアップロードされたファイルのマルウェアスキャンを行い、結果に応じてファイルを振り分ける仕組みを実際に構築・検証の結果をまとめてみます。

構成概要

今回構築した構成は以下のとおりです。

ユーザー
  ↓ ファイルをPUT
一時保存用S3バケット(malware-protection-dev)
  ↓ 自動スキャン
GuardDuty Malware Protection for S3
  ↓ スキャン結果をイベント通知
Amazon EventBridge
  ↓ Lambdaを起動
AWS Lambda(振り分け処理)
  ├─ NO_THREATS_FOUND / SKIPPED / UNSUPPORTED → 本番S3バケットに転送
  └─ THREATS_FOUND → 一時S3バケットから削除

一時バケットと本番バケットを分離することで、マルウェアが含まれるファイルが本番環境に混入するリスクを低減しています。

使用するAWSサービスとその構成

サービス 役割
Amazon S3(一時バケット) ユーザーのアップロード先
GuardDuty Malware Protection for S3 マルウェアスキャン
Amazon EventBridge スキャン結果の検知・ルーティング
AWS Lambda ファイルの転送・削除処理
Amazon S3(本番バケット) クリーンなファイルの保存先
Amazon CloudWatch Logs Lambdaのログ出力先

guardduty_architecture.png

料金について

課金軸 料金
スキャンデータ量 約$0.09/GB
スキャンオブジェクト数 一定数ごとに課金

2025年2月のアップデートにより、スキャンデータ量の料金が85%値下げされています。
検証が終わったら即時無効化できるため、検証目的であれば費用はほぼかかりません。

私の検証でかかったコストも$1以下でした。

構築手順

1. S3バケットの作成

一時バケットと本番バケットをそれぞれ作成します。

  • 一時バケット:malware-protection-dev
  • 本番バケット:malware-protection-prod

バージョニングはデフォルトのままで問題ありません。

2. GuardDuty Malware Protection for S3の有効化

GuardDutyコンソール
  → 左メニュー「Malware Protection for S3」
  → 「Discover GuardDuty Protection features」を選択
  → 「Malware Protection for S3」を選択して有効化
  → 対象バケット:malware-protection-dev
  → タグ付け:有効化
  → IAMロール:自動作成
  → 「保護プランを追加」

ポイント:「Enable All GuardDuty features」ではなく「Discover GuardDuty Protection features」を選択することで、GuardDuty本体を有効化せずにMalware Protection for S3だけを使えます。これにより余計な料金の発生を防げます。

3. IAMロールの作成(Lambda用)

LambdaがS3を操作するためのIAMロールを作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::malware-protection-dev/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::malware-protection-prod/*"
    }
  ]
}

注意:AmazonS3FullAccessを付与したい気持ちも分かりますが、最小権限のためGetとDeleteのみ付与しています。

4. Lambda関数の作成

Lambdaコンソール → 「関数の作成」
  → 関数名:guardduty-malware-handler
  → ランタイム:Python 3.12
  → 実行ロール:手順3で作成したロール

以下のコードを貼り付けて「Deploy」をクリックします。

import boto3
import json

s3 = boto3.client('s3')

SOURCE_BUCKET = 'malware-protection-dev'
DEST_BUCKET = 'malware-protection-prod'

def lambda_handler(event, context):
    print(json.dumps(event))

    detail = event['detail']
    scan_status = detail['scanResultDetails']['scanResultStatus']
    key = detail['s3ObjectDetails']['objectKey']

    print(f"File: {key}, Status: {scan_status}")

    if scan_status == 'THREATS_FOUND':
        # マルウェア検知 → 削除
        s3.delete_object(Bucket=SOURCE_BUCKET, Key=key)
        print(f"Deleted malware file: {key}")
    else:
        # NO_THREATS_FOUND / SKIPPED / UNSUPPORTED → 本番に転送
        s3.copy_object(
            CopySource={'Bucket': SOURCE_BUCKET, 'Key': key},
            Bucket=DEST_BUCKET,
            Key=key
        )
        s3.delete_object(Bucket=SOURCE_BUCKET, Key=key)
        print(f"Transferred clean file: {key}")

    return {'statusCode': 200}

5. EventBridgeルールの作成

EventBridgeコンソール → 「ルールを作成」
  → 名前:guardduty-malware-scan-rule
  → イベントバス:default
  → ルールタイプ:イベントパターンを持つルール

イベントパターンは「カスタムパターン(JSONエディター)」で以下を入力します。

{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Malware Protection Object Scan Result"],
  "detail": {
    "s3ObjectDetails": {
      "bucketName": ["malware-protection-dev"]
    }
  }
}

ターゲットには手順4で作成したLambda関数を指定します。

スキャン結果のタグについて

GuardDutyはスキャン後、S3オブジェクトに以下のタグを付与します。

タグ値 意味
NO_THREATS_FOUND マルウェアなし(正常)
THREATS_FOUND マルウェアを検出
SKIPPED サイズ超過等でスキャンをスキップ
UNSUPPORTED パスワード付きZIP等、スキャン非対応
FAILED スキャン処理自体が失敗

Findingsについて

GuardDuty本体を有効化した状態でMalware Protection for S3を使用している場合、スキャン結果はGuardDuty Findingsにも記録されます。

Findingsには以下の情報が含まれます。

  • 検出されたマルウェアの種類
  • 対象のS3バケット名・オブジェクトキー
  • 検出日時

GuardDutyを有効化せずにMalware Protection for S3を単体で使用している場合、Findingsは生成されません。スキャン結果はEventBridgeのイベントとCloudWatchメトリクスで確認できます。

SKIPPEDになった場合のCloudWatchでの確認

SKIPPEDタグが付いたオブジェクトについては、なぜSKIPPEDになったかをCloudWatchメトリクスで確認できます。

CloudWatchコンソール
  → 左メニュー「メトリクス」
  → 「GuardDuty」を選択
  → スキャンステータスごとのカウントを確認

また、LambdaのCloudWatch Logsを確認することで、対象ファイルのスキャン結果をより詳細に把握できます。

検証

テストファイルの準備

EICARテストファイルを使用しました。EICARファイルは実際のマルウェアではなく、セキュリティ製品のテスト用に業界標準として使われる無害なファイルです。

# ①クリーンファイル(マルウェアなし)
echo "This is a clean file" > clean_file.jpg

# ②EICARファイル(マルウェアあり)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > eicar_file.jpg

# ③パスワード付きZIP(クリーン)
echo "This is a clean file" > test_clean.txt
zip -e clean_encrypted.zip test_clean.txt

# ④パスワード付きZIP(EICARを含む)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > test_eicar.txt
zip -e eicar_encrypted.zip test_eicar.txt

検証結果

ファイル GuardDutyタグ 本番S3への転送
①クリーンファイル NO_THREATS_FOUND 転送される
②EICARファイル THREATS_FOUND 削除される
③パスワード付きZIP(クリーン) UNSUPPORTED 転送される
④パスワード付きZIP(EICAR) UNSUPPORTED 転送されてしまう

注意点:UNSUPPORTEDの落とし穴
パスワード付きZIPはGuardDutyがスキャンできないため、UNSUPPORTEDタグが付きます。
今回の実装ではTHREATS_FOUND以外は本番に転送する設計にしているため、マルウェアが含まれるパスワード付きZIPも本番バケットに転送されてしまいます。
対策としては、アプリケーション側でパスワード付きZIPのアップロードを禁止するか、UNSUPPORTEDもLambdaで削除する運用にすることが考えられます。

スキャン上限について

2025年9月のアップデートにより、スキャン可能なファイルサイズの上限が5GBから100GBに拡張されました。

100GBを超えるファイルはSKIPPEDタグが付き、スキャンがスキップされます。

つまずいたポイント

EventBridgeのイベントパターンのキー名が想定と異なっていた

EventBridgeのイベントパターンで最初は以下を指定していました。

{
  "detail": {
    "s3Bucket": {
      "name": ["malware-protection-dev"]
    }
  }
}

しかし、実際にGuardDutyが送るイベントのJSONを確認すると、キー名が異なっていました。

{
  "detail": {
    "s3ObjectDetails": {
      "bucketName": "malware-protection-dev"
    }
  }
}

原因の特定方法: EventBridgeのイベントパターンを{"source": ["aws.guardduty"]}に一時的に緩め、Lambdaに届いたイベントのJSONをCloudWatch Logsで確認することで特定しました。

Lambdaのコードでもキー名が誤っていた

同様に、Lambdaコード内のキー参照も誤っていました。

# 誤り
key = detail['s3Object']['key']

# 正しい
key = detail['s3ObjectDetails']['objectKey']

IAMロールの権限設定ミス

最初はIAMロールの作成を手動で行い、Lambda作成時に既存ロールを指定するつもりでした。しかし実際にはLambda作成時に自動生成されたロールが使われており、そのロールにはS3への操作権限が付与されていませんでした。

[ERROR] AccessDenied: An error occurred (AccessDenied) 
when calling the DeleteObject operation: 
User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/
guardduty-malware-handler-role-xxxxxxxx/
guardduty-malware-handler is not authorized to perform: 
s3:DeleteObject

原因の特定方法: CloudWatch LogsでLambdaのエラーログを確認し、エラーメッセージ内のロール名を確認することで、意図したロールとは別のロールが使われていることが判明しました。

対処法: 自動生成されたロール(guardduty-malware-handler-role-xxxxxxxx)に対して必要な権限を付与しました。

なお、最初は検証のためAmazonS3FullAccessを付与しましたが、これは最小権限の原則に反します。本来必要なのは以下の権限のみです。

  • 一時バケット:s3:GetObject、s3:DeleteObject
  • 本番バケット:s3:PutObject

まとめ

GuardDuty Malware Protection for S3を使うことで、S3へのマルウェアファイルのアップロードをサーバーレスかつシンプルな構成で検知・振り分けできることを確認しました。

ただし、以下の点には注意が必要です。

  • GuardDuty単体ではファイルの削除・隔離はできない(タグ付けのみ)EventBridge + Lambdaとの組み合わせが必要
  • パスワード付きZIPやサイズ超過ファイルはスキャンされない(UNSUPPORTED / SKIPPED)
  • ゼロデイマルウェアや難読化されたマルウェアはすり抜ける可能性がある

完全なセキュリティはGuardDuty単体では実現できないため、アプリケーション側のバリデーションと組み合わせた多層防御が重要だと身をもって認識できました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?