【AWS】GuardDuty Malware Protection for Amazon S3のコストが85％削減！Terraformで使ってみる！

# はじめに

最近、GuardDuty Malware Protection for Amazon S3のスキャンしたデータ量に発生するコストが最大85％削減されました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/02/amazon-guardduty-malware-protection-s3-price-reduction/

今回は、そのGuardDuty Malware Protection for Amazon S3をTerraformで使ってみました！


# Amazon GuardDuty Malware Protection for S3 の概要

Amazon GuardDuty Malware Protection for Amazon S3 は、S3 にアップロードされた新規オブジェクトを自動スキャンし、マルウェアを検出する AWS のセキュリティ機能です。特定のバケットやプレフィックスを対象に設定でき、検出結果は GuardDuty Findings や CloudWatch メトリクスなどで確認することが可能です！


# クウォータ制限

今回機能等を調査して一番気をつけないといけない部分だと感じた部分はクウォータです。
S3にオブジェクトをアップロードする際は、ここの制限に気をつけてお使いください！


| クォータ名                          | AWS デフォルトのクォータ値 | 調整可能 | 説明 |
|----------------------------------|--------------------|--------|--------------------------------------------------------------|
| S3 オブジェクトの最大サイズ       | 5 GB               | いいえ | スキャン対象の S3 オブジェクトの最大サイズ。 |
| アーカイブバイトの抽出           | 5 GB               | いいえ | アーカイブから抽出して分析できるデータの最大量。超過分はスキップ。 |
| アーカイブファイルの抽出         | 1,000              | いいえ | 抽出・分析可能なアーカイブ内のファイル数。超過分はスキップ。 |
| アーカイブの最大深度             | 5                  | いいえ | 抽出可能なネストされたアーカイブの最大レベル。超過分はスキップ。 |
| 最大保護バケット                 | 25                 | いいえ | Malware Protection for S3 を有効にできる S3 バケット数（リージョンごと）。 |


https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/malware-protection-s3-quotas-guardduty.html


# 作成したリソース

大きく分けて二つのリソースで、オブジェクト格納用S3バケットとそのバケットに対するAmazon GuardDuty Malware Protection for S3を作成しました。それぞれに紐づく設定等は以下のようになります。

## Amazon S3

S3バケットポリシーでは、以下のDenyをしています。
- マルウェア検出（THREATS_FOUND）タグが付いたオブジェクトの取得を拒否
- HTTPS 以外でのアクセスを拒否


Amazon GuardDuty Malware Protection for S3 を利用すると、S3 バケット内のオブジェクトを自動的にスキャンし、マルウェアが検出された場合にタグ（GuardDutyMalwareScanStatus=THREATS_FOUND）が付与されます。

このタグが付いたオブジェクトへのアクセスを防ぐために、S3 バケットポリシーを設定し、s3:GetObject アクションを拒否することで、マルウェアと判定されたファイルのダウンロードをブロックできます。これにより、感染ファイルが意図せず利用されるリスクを軽減し、セキュリティを強化できます。

他にもスキャン結果に応じてオブジェクトにタグが付与されます！

| ステータス      | 説明 |
|---------------|---------------------|
| NO_THREATS_FOUND      | 脅威は検出されませんでした。 |
| THREATS_FOUND        | 脅威が検出されました。 |
| UNSUPPORTED          | スキャンがスキップされました（パスワード保護やクォータ制限など）。 |
| ACCESS_DENIED        | アクセス権限が不足し、スキャンできませんでした。 |
| FAILED               | 内部エラーによりスキャンが失敗しました。 |

https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/monitoring-malware-protection-s3-scans-gdu.html#s3-object-scan-result-value-malware-protection


## Amazon GuardDuty Malware Protection for S3

紐付けている権限はコンソール上で作成する際に自動付与されるIAMポリシーを参考にしました！


![CleanShot 2025-02-18 at 22.55.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/9ceacff3-3dea-42c6-9fd8-f004288eab2a.png)


# Terraformフォルダ構成はこちら

```
├── guardduty_maluware_s3
│   ├── backend.tf
│   ├── data.tf
│   ├── locals.tf
│   ├── main.tf
│   ├── provider.tf
│   └── variables.tf
├── modules
│   ├── guardduty_s3_malware_protection
│   │   ├── main.tf
│   │   └── variables.tf
│   └── s3
│       ├── main.tf
│       ├── output.tf
│       └── variables.tf
```

上記からリソース作成しているmain.tfとポリシーを定義しているdata.tfのファイルを記載いたします。


```main.tf
module "s3_script_bucket" {
  source = "../modules/s3"
  bucket_name = local.s3_script_bucket
}

resource "aws_s3_bucket_policy" "main" {
  bucket = module.s3_script_bucket.name
  policy = data.aws_iam_policy_document.s3_script_bucket_policy.json
}

module "malware_protection_s3_script_bucket" {
  depends_on = [aws_iam_role.guardduty_malware_protection_role]
  source = "../modules/guardduty_s3_malware_protection"
  iam_role_arn = aws_iam_role.guardduty_malware_protection_role.arn
  plan_name = local.s3_script_bucket_malware_plan_name
  bucket_name = module.s3_script_bucket.name
}

resource "aws_iam_role" "guardduty_malware_protection_role" {
  name = "guardduty-malware-protection-role"
  assume_role_policy = data.aws_iam_policy_document.guardduty_malware_protection_assume_role_policy.json
}

resource "aws_iam_policy" "guardduty_malware_protection_policy" {
  name = "guardduty-malware-protection-policy"
  policy = data.aws_iam_policy_document.guardduty_malware_protection_iam_policy.json
}

resource "aws_iam_role_policy_attachment" "guardduty_malware_protection_role_policy_attachment_1" {
  depends_on = [aws_iam_role.guardduty_malware_protection_role, aws_iam_policy.guardduty_malware_protection_policy]
  role = aws_iam_role.guardduty_malware_protection_role.name
  policy_arn = aws_iam_policy.guardduty_malware_protection_policy.arn
}
```

```data.tf
data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "s3_script_bucket_policy" {
  statement {
    effect = "Deny"
    actions = [
      "s3:GetObject"
    ]
    resources = [
      "${module.s3_script_bucket.arn}/*",
      "${module.s3_script_bucket.arn}"
    ]
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
    condition {
      test     = "StringEquals"
      variable = "s3:ExistingObjectTag/GuardDutyMalwareScanStatus"
      values   = ["THREATS_FOUND"]
    }
  }

  statement {
    effect = "Deny"
    actions = [
      "s3:*"
    ]
    resources = [
      "${module.s3_script_bucket.arn}/*",
      "${module.s3_script_bucket.arn}"
    ]
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
  }
}


data "aws_iam_policy_document" "guardduty_malware_protection_assume_role_policy" {
    statement {
        effect = "Allow"
        actions = ["sts:AssumeRole"]
        principals {
            type = "Service"
            identifiers = ["malware-protection-plan.guardduty.amazonaws.com"]
        }
        condition {
            test = "StringEquals"
            values = [local.aws_account]
            variable = "aws:SourceAccount"
        }
    }
}



data "aws_iam_policy_document" "guardduty_malware_protection_iam_policy" {
  statement {
    effect = "Allow"
    actions = [
      "events:PutRule"
    ]
    resources = [
      "arn:aws:events:${local.aws_region}:${local.aws_account}:rule/DO-NOT-DELETE-AmazonGuardDutyMalwareProtectionS3*"
    ]
    condition {
      test     = "StringEquals"
      variable = "events:ManagedBy"
      values   = ["malware-protection-plan.guardduty.amazonaws.com"]
    }
    condition {
      test = "ForAllValues:StringEquals"
      variable = "events:source"
      values = ["aws.s3"]
    }
    condition {
      test = "ForAllValues:StringEquals"
      variable = "events:detail-type"
      values = ["Object Created", "AWS API Call via CloudTrail"]
    }
    condition {
      test = "Null"
      variable = "events:source"
      values = ["false"]
    }
    condition {
      test = "Null"
      variable = "events:detail-type"
      values = ["false"]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "events:DeleteRule",
      "events:PutTargets",
      "events:RemoveTargets"
    ]
    resources = [
      "arn:aws:events:${local.aws_region}:${local.aws_account}:rule/DO-NOT-DELETE-AmazonGuardDutyMalwareProtectionS3*"
    ]
    condition {
      test     = "StringEquals"
      variable = "events:ManagedBy"
      values   = ["malware-protection-plan.guardduty.amazonaws.com"]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:PutBucketNotification",
      "s3:GetBucketNotification"
    ]
    resources = [
      "arn:aws:s3:::${module.s3_script_bucket.name}"
    ]
    condition {
      test     = "StringEquals"
      variable = "aws:ResourceAccount"
      values   = [local.aws_account]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:GetObjectTagging",
      "s3:GetObjectVersionTagging",
      "s3:PutObjectTagging",
      "s3:PutObjectVersionTagging"
    ]
    resources = [
      "arn:aws:s3:::${module.s3_script_bucket.name}/*"
    ]
    condition {
      test     = "StringEquals"
      variable = "aws:ResourceAccount"
      values   = [local.aws_account]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:PutObject"
    ]
    resources = [
      "arn:aws:s3:::${module.s3_script_bucket.name}/malware-protection-resource-validation-object"
    ]
    condition {
      test     = "StringEquals"
      variable = "aws:ResourceAccount"
      values   = [local.aws_account]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:ListBucket"
    ]
    resources = [
      "arn:aws:s3:::${module.s3_script_bucket.name}"
    ]
    condition {
      test     = "StringEquals"
      variable = "aws:ResourceAccount"
      values   = [local.aws_account]
    }
  }

  statement {
    effect = "Allow"
    actions = [
      "s3:GetObject",
      "s3:GetObjectVersion"
    ]
    resources = [
      "arn:aws:s3:::{module.s3_script_bucket.name}/*"
    ]
    condition {
      test     = "StringEquals"
      variable = "aws:ResourceAccount"
      values   = [local.aws_account]
    }
  }
}
```

# 試してみる

それでは、実際に安全なデータとEICARテストファイルをS3に格納して、Amazon GuardDuty Malware Protection for S3の動作を見ていきます。

※EICAR (European Institute for Computer Antivirus Research) テストファイル は、ウイルス対策ソフトの動作確認用に作られたテストファイルです。このファイルは 実際のマルウェアではなく、ウイルス対策ソフトが正常に動作しているかを確認するためのもので、多くのウイルス対策ソフトがこれを「テストウイルス」として検出するように設計されています。

## 事前準備

CloudShellにて、テストファイル(.zip)の作成とEICARテストZipファイル(.zip)のダウンロードを実行します。

```sh
# テストファイル(.zip)の作成
  
$ mkdir test
$ cd test
$ touch test.json
$ cd ..
$ zip -r test.zip test

# EICARテストZipファイル(.zip)のダウンロード

$ wget https://secure.eicar.org/eicar_com.zip
  
```
  
## S3にアップロード＆S3からダウンロード

ここからはS3にアップロードした後に、S3からダウンロードをしてみます！
S3バケットポリシーにて、Amazon GuardDuty Malware Protection for S3でマルウェア検出されたオブジェクトはGetObjectできないようにしているため、EICARテストZipファイル(.zip)はS3からダウンロードできなくなっています。


```sh

# ダウンロード先のフォルダを作成

mkdir download

# ファイル確認

$ ls -l
total 16
drwxr-xr-x. 2 cloudshell-user cloudshell-user 4096 Feb 18 21:04 download
-rw-r--r--. 1 cloudshell-user cloudshell-user  184 Jan  3 15:29 eicar_com.zip
drwxr-xr-x. 2 cloudshell-user cloudshell-user 4096 Feb 18 20:49 test
-rw-r--r--. 1 cloudshell-user cloudshell-user  316 Feb 18 20:52 test.zip

# テストファイル(.zip)のアップロード＆ダウンロード

$ aws s3 cp test.zip s3://<S3バケット名>/test.zip
upload: ./test.zip to s3://<S3バケット名>/test.zip

$ aws s3 cp s3://<S3バケット名>/test.zip ./download
download: s3://<S3バケット名>/test.zip to download/test.zip


# EICARテストZipファイル(.zip)のアップロード＆ダウンロード

$ aws s3 cp eicar_com.zip s3://<S3バケット名>/eicar_com.zip
upload: ./eicar_com.zip to s3://<S3バケット名>/eicar_com.zip

$ aws s3 cp s3://<S3バケット名>/eicar_com.zip ./download
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden

# ダウンロード先のフォルダを確認

$ cd download
$ ls -l
total 4
-rw-r--r--. 1 cloudshell-user cloudshell-user 316 Feb 18 21:19 test.zip
```

## 結果確認

先ほどEICARテストZipファイル(.zip)はS3からダウンロードできないことを確認しました。
それぞれのリソースがどのようになっているか確認してみましょう！

### オブジェクトのタグ

```

# テストファイル(.zip)のtag確認

$ aws s3api get-object-tagging --bucket <S3バケット名> --key test.zip
{
    "TagSet": [
        {
            "Key": "GuardDutyMalwareScanStatus",
            "Value": "NO_THREATS_FOUND"
        }
    ]
}

# EICARテストZipファイル(.zip)のtag確認

$ aws s3api get-object-tagging --bucket <S3バケット名> --key eicar_com.zip
{
    "TagSet": [
        {
            "Key": "GuardDutyMalwareScanStatus",
            "Value": "THREATS_FOUND"
        }
    ]
}

```

### GuardDutyの結果確認


![CleanShot 2025-02-19 at 07.15.07@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/be57bbd9-fd37-4084-b458-66020f1cde01.png)

![CleanShot 2025-02-19 at 07.17.07@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1611984/1ad4551c-594d-45cd-ad5b-d823bddfb5c0.png)


# まとめ

今回GuardDuty Malware Protection for Amazon S3を使ってみました。
検出されたオブジェクトをS3バケットポリシーでGetObjectを拒否しましたが、検出スキップされたオブジェクトはどうしようか考えています。CloudWatchメトリクスを使用してアラームを作成して気づけるようにしておくなど、使い方を模索しながら今後使っていこうと思いました！