From 2477fcea59e729e9a66969cb3c31584416fd0424 Mon Sep 17 00:00:00 2001 From: Cristina Borges Date: Fri, 29 May 2026 15:07:58 +0100 Subject: [PATCH] Add iam-no-mfa audit rule with tests --- app/rules/iam.py | 20 ++++++++++++++++ tests/test_iam_rule.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 app/rules/iam.py create mode 100644 tests/test_iam_rule.py diff --git a/app/rules/iam.py b/app/rules/iam.py new file mode 100644 index 0000000..c85e089 --- /dev/null +++ b/app/rules/iam.py @@ -0,0 +1,20 @@ +"""IAM audit rules for Sentinel.""" + +import boto3 + + +def find_users_without_mfa() -> list[str]: + """Return names of IAM users that have no MFA device enabled.""" + iam = boto3.client("iam") + response = iam.list_users() + + users_without_mfa: list[str] = [] + + for user in response["Users"]: + user_name = user["UserName"] + mfa_response = iam.list_mfa_devices(UserName=user_name) + + if not mfa_response["MFADevices"]: + users_without_mfa.append(user_name) + + return users_without_mfa diff --git a/tests/test_iam_rule.py b/tests/test_iam_rule.py new file mode 100644 index 0000000..1677f25 --- /dev/null +++ b/tests/test_iam_rule.py @@ -0,0 +1,53 @@ +"""Tests for IAM audit rules.""" + +import boto3 +import pytest +from moto import mock_aws + +from app.rules.iam import find_users_without_mfa + + +@pytest.fixture +def iam_client(): + """Yield a mocked IAM client. All AWS calls in the test are intercepted.""" + with mock_aws(): + yield boto3.client("iam", region_name="us-east-1") + + +def _make_user_with_mfa(iam_client, user_name: str) -> None: + """Helper: create an IAM user and attach an enabled virtual MFA device.""" + iam_client.create_user(UserName=user_name) + device = iam_client.create_virtual_mfa_device( + VirtualMFADeviceName=f"{user_name}-mfa" + ) + iam_client.enable_mfa_device( + UserName=user_name, + SerialNumber=device["VirtualMFADevice"]["SerialNumber"], + AuthenticationCode1="123456", + AuthenticationCode2="234567", + ) + + +def test_empty_account_returns_empty_list(iam_client): + assert find_users_without_mfa() == [] + + +def test_user_with_mfa_not_flagged(iam_client): + _make_user_with_mfa(iam_client, "secure-user") + assert find_users_without_mfa() == [] + + +def test_user_without_mfa_is_flagged(iam_client): + iam_client.create_user(UserName="exposed-user") + assert find_users_without_mfa() == ["exposed-user"] + + +def test_mixed_users_only_no_mfa_returned(iam_client): + _make_user_with_mfa(iam_client, "secure-1") + iam_client.create_user(UserName="exposed-1") + _make_user_with_mfa(iam_client, "secure-2") + iam_client.create_user(UserName="exposed-2") + + result = find_users_without_mfa() + + assert sorted(result) == ["exposed-1", "exposed-2"]