[Python]boto3からGuardDutyへのアクセスをmotoでモックしてみた

2023.05.18

はじめに

最近PythonでAWSサービス関連操作をモックしてくれる「moto」というライブラリを使って開発をしているのですが、GuardDuty周りも一部対応しているようなので試してみました。

boto3を使っている方はテストを書く時とっても便利なので、以下の記事も合わせてご覧ください。

環境

以下の環境で実施してます。

環境 バージョン
Python 3.8.13
Poetry 1.2.2
moto 4.1.9
boto3 1.26.135
pytest 7.3.1

やってみる

motoのドキュメントを確認したところ、create_detectorget_detectorは対応しているようだったので、これらを使ってテストを書いてみます。(その他にいくつか対応しているものはあったので、こちらから確認してみてください。)

テスト対象コード

まずはGuardDutyを有効化する関数をテストしてみます。

以下のコードではDataSourcesとしてS3Logsを有効にしてGuardDutyを有効化しています。

main.py

import boto3


def create_guardduty_detector(client):
    response = client.create_detector(
        Enable=True,
        DataSources={
            'S3Logs': {
                'Enable': True
            }
        }
    )
    detector_id = response['DetectorId']
    return detector_id

テストコード

テストでmotoを利用するには、各AWSサービスに応じたモックを用意します。 motoからGuardDutyに対応するmock_guarddutyをインポートして、テスト対象の関数にデコレーター(@mock_guardduty)として記述してください。

このテストコードでは、clientを取得し、テスト対象コード内のcreate_guardduty_detectorをモック内で実行します。この時、guarddutyのcreate_detectorが実行されますが、テストコード側で@mock_guarddutyを記述しているためAWS環境でGuardDutyが有効化されることはありません。

get_detectorを実行することで、モック内で有効化したGuardDutyのdetectorから情報を取得して出力してみます。

test_main.py

from moto import mock_guardduty
import main
import boto3


@mock_guardduty
def test_create_guardduty_detector_success():
    
    client = boto3.client('guardduty', region_name='ap-northeast-1')
    detector_id = main.create_guardduty_detector(client)
    res = client.get_detector(DetectorId=detector_id)
    print(res)
    assert res["Status"] == "ENABLED"
    # S3Logsが有効化されていることを確認
    assert res["DataSources"]["S3Logs"]["Status"] == "ENABLED"

最後のassertでは、取得したdetectorの情報からDataSourcesのS3Logsが有効化されているか?をテストしています。

実行結果

テストを実行してみると、問題なくパスしました。 有効化したGuardDutyは、DataSourcesとしてS3Logsを有効されていることが確認できました。

.venv ❯ poetry run pytest -s
===================================================================== test session starts ======================================================================
platform darwin -- Python 3.8.13, pytest-7.3.1, pluggy-1.0.0
rootdir: /Users/suzuki.jun/Documents/moto-guardduty
collected 1 item                                                                                                                                               

test_main.py {'ResponseMetadata': {'HTTPStatusCode': 200, 'HTTPHeaders': {}, 'RetryAttempts': 0}, 'CreatedAt': '2023-05-18T15:47:46.755182Z', 'FindingPublishingFrequency': 'SIX_HOURS', 'ServiceRole': 'arn:aws:iam::123456789012:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty', 'Status': 'ENABLED', 'UpdatedAt': '2023-05-18T15:47:46.755182Z', 'DataSources': {'CloudTrail': {'Status': 'DISABLED'}, 'DNSLogs': {'Status': 'DISABLED'}, 'FlowLogs': {'Status': 'DISABLED'}, 'S3Logs': {'Status': 'ENABLED'}, 'Kubernetes': {'AuditLogs': {'Status': 'DISABLED'}}}, 'Tags': {}}
.

====================================================================== 1 passed in 0.37s =======================================================================

print(res)で出力しているdetectorの情報が見づらいので整形してみます。 有効化した際に指定したS3LogsがENABLEDになっていて、ServiceRoleなどのARNはサンプルのアカウントIDに置き換えられていますね。

{
    "ResponseMetadata": {
        "HTTPStatusCode": 200,
        "HTTPHeaders": {},
        "RetryAttempts": 0
    },
    "CreatedAt": "2023-05-18T15:45:31.213323Z",
    "FindingPublishingFrequency": "SIX_HOURS",
    "ServiceRole": "arn:aws:iam::123456789012:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty",
    "Status": "ENABLED",
    "UpdatedAt": "2023-05-18T15:45:31.213323Z",
    "DataSources": {
        "CloudTrail": {
            "Status": "DISABLED"
        },
        "DNSLogs": {
            "Status": "DISABLED"
        },
        "FlowLogs": {
            "Status": "DISABLED"
        },
        "S3Logs": {
            "Status": "ENABLED"
        },
        "Kubernetes": {
            "AuditLogs": {
                "Status": "DISABLED"
            }
        }
    },
    "Tags": {}
}

わざと失敗させてみる

テスト対象コードを以下のように、S3LogsをFalseに変更してみます。

main.py

import boto3


def create_guardduty_detector(client):
    response = client.create_detector(
        Enable=True,
        DataSources={
            'S3Logs': {
                'Enable': False
            }
        }
    )
    detector_id = response['DetectorId']
    return detector_id

この状態でテストを実行すると、想定通り失敗しました。 テストコード上のassertで、res["DataSources"]["S3Logs"]["Status"]の値で期待していたのはENABLEDのはずが、実際は有効化されていないためDISABLEDになっていると怒られます。分かりやすいですね。

=========================================================================== FAILURES ===========================================================================
____________________________________________________________ test_create_guardduty_detector_success ____________________________________________________________

    @mock_guardduty
    def test_create_guardduty_detector_success():
    
        client = boto3.client('guardduty', region_name='ap-northeast-1')
        detector_id = main.create_guardduty_detector(client)
        res = client.get_detector(DetectorId=detector_id)
        print(res)
        assert res["Status"] == "ENABLED"
>       assert res["DataSources"]["S3Logs"]["Status"] == "ENABLED"
E       AssertionError: assert 'DISABLED' == 'ENABLED'
E         - ENABLED
E         + DISABLED

test_main.py:14: AssertionError
=================================================================== short test summary info ====================================================================
FAILED test_main.py::test_create_guardduty_detector_success - AssertionError: assert 'DISABLED' == 'ENABLED'
====================================================================== 1 failed in 0.37s =======================================================================

まとめ

motoを使ったGuardDutyのテストを試してみました。テストコード側にインポートしたmotoのライブラリをデコレーターとして記述するだけなのでとても便利です。 AWS側のアップデートに追いついていない部分もあったりしますが、テストコードをサクッと書けるので是非利用してみてください。

参考