From 4762e1cc4c2aaf8e7ac39803b3371d9be551d3cd Mon Sep 17 00:00:00 2001 From: Nacho Rivera <59198746+n4ch04@users.noreply.github.com> Date: Fri, 4 Nov 2022 13:38:22 +0100 Subject: [PATCH] feat(test): Remaining IAM tests (#1451) --- .../iam_administrator_access_with_mfa.py | 30 +-- .../iam_administrator_access_with_mfa_test.py | 202 ++++++++++++++++++ .../iam_avoid_root_usage_test.py | 134 ++++++++++++ .../iam_disable_30_days_credentials.py | 2 +- .../iam_disable_30_days_credentials_test.py | 97 +++++++++ .../iam_disable_90_days_credentials.py | 2 +- .../iam_disable_90_days_credentials_test.py | 97 +++++++++ .../iam_no_root_access_key_test.py | 159 ++++++++++++++ ...s_passwords_within_90_days_or_less_test.py | 74 +++++++ .../iam_policy_allows_privilege_escalation.py | 17 +- ...policy_allows_privilege_escalation_test.py | 185 ++++++++++++++++ .../iam_root_hardware_mfa_enabled_test.py | 65 ++++++ .../iam_root_mfa_enabled_test.py | 67 ++++++ .../iam_rotate_access_key_90_days_test.py | 102 +++++++++ .../iam_user_hardware_mfa_enabled.py | 12 +- .../iam_user_hardware_mfa_enabled_test.py | 103 +++++++++ ...am_user_mfa_enabled_console_access_test.py | 98 +++++++++ ...m_user_no_setup_initial_access_key_test.py | 6 +- 18 files changed, 1417 insertions(+), 35 deletions(-) create mode 100644 providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa_test.py create mode 100644 providers/aws/services/iam/iam_avoid_root_usage/iam_avoid_root_usage_test.py create mode 100644 providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials_test.py create mode 100644 providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials_test.py create mode 100644 providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key_test.py create mode 100644 providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less_test.py create mode 100644 providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py create mode 100644 providers/aws/services/iam/iam_root_hardware_mfa_enabled/iam_root_hardware_mfa_enabled_test.py create mode 100644 providers/aws/services/iam/iam_root_mfa_enabled/iam_root_mfa_enabled_test.py create mode 100644 providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py create mode 100644 providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled_test.py create mode 100644 providers/aws/services/iam/iam_user_mfa_enabled_console_access/iam_user_mfa_enabled_console_access_test.py diff --git a/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py b/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py index c7a9a1f2..1717a39c 100644 --- a/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py +++ b/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py @@ -12,14 +12,18 @@ class iam_administrator_access_with_mfa(Check): report.resource_id = group.name report.resource_arn = group.arn report.region = iam_client.region + report.status = "PASS" + report.status_extended = f"Group {group.name} has no policies." + if group.attached_policies: - admin_policy = False + report.status_extended = ( + f"Group {group.name} provides non-administrative access." + ) for group_policy in group.attached_policies: if ( group_policy["PolicyArn"] == "arn:aws:iam::aws:policy/AdministratorAccess" ): - admin_policy = True # users in group are Administrators if group.users: for group_user in group.users: @@ -30,27 +34,9 @@ class iam_administrator_access_with_mfa(Check): ): report.status = "FAIL" report.status_extended = f"Group {group.name} provides administrator access to User {group_user.name} with MFA disabled." - findings.append(report) - elif ( - user["user"] == group_user.name - and user["mfa_active"] == "true" - ): - report.status = "PASS" - report.status_extended = f"Group {group.name} provides administrator access to User {group_user.name} with MFA enabled." - findings.append(report) else: - report.status = "PASS" report.status_extended = f"Group {group.name} provides administrative access but does not have users." - findings.append(report) - if not admin_policy: - report.status = "PASS" - report.status_extended = ( - f"Group {group.name} provides non-administrative access." - ) - findings.append(report) - else: - report.status = "PASS" - report.status_extended = f"Group {group.name} has no policies." - findings.append(report) + + findings.append(report) return findings diff --git a/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa_test.py b/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa_test.py new file mode 100644 index 00000000..a25a0e53 --- /dev/null +++ b/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa_test.py @@ -0,0 +1,202 @@ +from json import dumps +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_administrator_access_with_mfa_test: + @mock_iam + def test_group_with_no_policies(self): + iam = client("iam") + group_name = "test-group" + + arn = iam.create_group(GroupName=group_name)["Group"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa.iam_client", + new=IAM(current_audit_info), + ): + from providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa import ( + iam_administrator_access_with_mfa, + ) + + check = iam_administrator_access_with_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == group_name + assert result[0].resource_arn == arn + assert search( + f"Group {group_name} has no policies.", result[0].status_extended + ) + + @mock_iam + def test_group_non_administrative_policy(self): + iam = client("iam") + group_name = "test-group" + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "*"}, + ], + } + policy_arn = iam.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + arn = iam.create_group(GroupName=group_name)["Group"]["Arn"] + iam.attach_group_policy(GroupName=group_name, PolicyArn=policy_arn) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa.iam_client", + new=IAM(current_audit_info), + ): + from providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa import ( + iam_administrator_access_with_mfa, + ) + + check = iam_administrator_access_with_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == group_name + assert result[0].resource_arn == arn + assert search( + f"Group {group_name} provides non-administrative access.", + result[0].status_extended, + ) + + @mock_iam + def test_admin_policy_no_users(self): + iam = client("iam") + group_name = "test-group" + + arn = iam.create_group(GroupName=group_name)["Group"]["Arn"] + iam.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa.iam_client", + new=IAM(current_audit_info), + ): + from providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa import ( + iam_administrator_access_with_mfa, + ) + + check = iam_administrator_access_with_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == group_name + assert result[0].resource_arn == arn + assert search( + f"Group {group_name} provides administrative access but does not have users.", + result[0].status_extended, + ) + + @mock_iam + def test_admin_policy_with_user_without_mfa(self): + iam = client("iam") + group_name = "test-group" + user_name = "user-test" + iam.create_user(UserName=user_name) + arn = iam.create_group(GroupName=group_name)["Group"]["Arn"] + iam.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + iam.add_user_to_group(GroupName=group_name, UserName=user_name) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa.iam_client", + new=IAM(current_audit_info), + ): + from providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa import ( + iam_administrator_access_with_mfa, + ) + + check = iam_administrator_access_with_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == group_name + assert result[0].resource_arn == arn + assert search( + f"Group {group_name} provides administrator access to User {user_name} with MFA disabled.", + result[0].status_extended, + ) + + @mock_iam + def test_various_policies_with_users_with_and_without_mfa(self): + iam = client("iam") + group_name = "test-group" + user_name_no_mfa = "user-no-mfa" + user_name_mfa = "user-mfa" + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "*"}, + ], + } + mfa_device_name = "mfa-test" + mfa_serial_number = iam.create_virtual_mfa_device( + VirtualMFADeviceName=mfa_device_name + )["VirtualMFADevice"]["SerialNumber"] + iam.create_user(UserName=user_name_no_mfa) + iam.create_user(UserName=user_name_mfa) + iam.enable_mfa_device( + UserName=user_name_mfa, + SerialNumber=mfa_serial_number, + AuthenticationCode1="123456", + AuthenticationCode2="123466", + ) + policy_arn = iam.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + arn_group = iam.create_group(GroupName=group_name)["Group"]["Arn"] + iam.attach_group_policy(GroupName=group_name, PolicyArn=policy_arn) + iam.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess", + ) + iam.add_user_to_group(GroupName=group_name, UserName=user_name_no_mfa) + iam.add_user_to_group(GroupName=group_name, UserName=user_name_mfa) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa.iam_client", + new=IAM(current_audit_info), + ): + from providers.aws.services.iam.iam_administrator_access_with_mfa.iam_administrator_access_with_mfa import ( + iam_administrator_access_with_mfa, + ) + + check = iam_administrator_access_with_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == group_name + assert result[0].resource_arn == arn_group + assert search( + f"Group {group_name} provides administrator access to User {user_name_no_mfa} with MFA disabled.", + result[0].status_extended, + ) diff --git a/providers/aws/services/iam/iam_avoid_root_usage/iam_avoid_root_usage_test.py b/providers/aws/services/iam/iam_avoid_root_usage/iam_avoid_root_usage_test.py new file mode 100644 index 00000000..776c898f --- /dev/null +++ b/providers/aws/services/iam/iam_avoid_root_usage/iam_avoid_root_usage_test.py @@ -0,0 +1,134 @@ +import datetime +from csv import DictReader +from re import search +from unittest import mock + +from moto import mock_iam + + +class Test_iam_avoid_root_usage: + @mock_iam + def test_root_not_used(self): + raw_credential_report = r"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated +,arn:aws:iam::123456789012:,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,true,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" + credential_lines = raw_credential_report.split("\n") + csv_reader = DictReader(credential_lines, delimiter=",") + credential_list = list(csv_reader) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage import ( + iam_avoid_root_usage, + ) + + service_client.credential_report = credential_list + check = iam_avoid_root_usage() + result = check.execute() + assert result[0].status == "PASS" + assert search( + "Root user in the account wasn't accessed in the last", + result[0].status_extended, + ) + assert result[0].resource_id == "" + assert result[0].resource_arn == "arn:aws:iam::123456789012:" + + @mock_iam + def test_root_password_used(self): + password_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=2) + ).strftime("%Y-%m-%dT%H:%M:%S+00:00") + raw_credential_report = rf"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated +,arn:aws:iam::123456789012:,2022-04-17T14:59:38+00:00,true,{password_last_used},not_supported,not_supported,false,true,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" + credential_lines = raw_credential_report.split("\n") + csv_reader = DictReader(credential_lines, delimiter=",") + credential_list = list(csv_reader) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage import ( + iam_avoid_root_usage, + ) + + service_client.credential_report = credential_list + check = iam_avoid_root_usage() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + "Root user in the account was last accessed", result[0].status_extended + ) + assert result[0].resource_id == "" + assert result[0].resource_arn == "arn:aws:iam::123456789012:" + + @mock_iam + def test_root_access_key_1_used(self): + access_key_1_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=2) + ).strftime("%Y-%m-%dT%H:%M:%S+00:00") + raw_credential_report = rf"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated +,arn:aws:iam::123456789012:,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,true,N/A,{access_key_1_last_used},N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" + credential_lines = raw_credential_report.split("\n") + csv_reader = DictReader(credential_lines, delimiter=",") + credential_list = list(csv_reader) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage import ( + iam_avoid_root_usage, + ) + + service_client.credential_report = credential_list + check = iam_avoid_root_usage() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + "Root user in the account was last accessed", result[0].status_extended + ) + assert result[0].resource_id == "" + assert result[0].resource_arn == "arn:aws:iam::123456789012:" + + @mock_iam + def test_root_access_key_2_used(self): + access_key_2_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=2) + ).strftime("%Y-%m-%dT%H:%M:%S+00:00") + raw_credential_report = rf"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated +,arn:aws:iam::123456789012:,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,true,N/A,N/A,N/A,N/A,false,N/A,{access_key_2_last_used},N/A,N/A,false,N/A,false,N/A""" + credential_lines = raw_credential_report.split("\n") + csv_reader = DictReader(credential_lines, delimiter=",") + credential_list = list(csv_reader) + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_avoid_root_usage.iam_avoid_root_usage import ( + iam_avoid_root_usage, + ) + + service_client.credential_report = credential_list + check = iam_avoid_root_usage() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + "Root user in the account was last accessed", result[0].status_extended + ) + assert result[0].resource_id == "" + assert result[0].resource_arn == "arn:aws:iam::123456789012:" diff --git a/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py index 8277dfdf..69994752 100644 --- a/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py +++ b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials.py @@ -16,7 +16,7 @@ class iam_disable_30_days_credentials(Check): report.resource_id = user.name report.resource_arn = user.arn report.region = iam_client.region - if user.password_last_used and user.password_last_used != "": + if user.password_last_used: time_since_insertion = ( datetime.datetime.now() - datetime.datetime.strptime( diff --git a/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials_test.py b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials_test.py new file mode 100644 index 00000000..ccb724a1 --- /dev/null +++ b/providers/aws/services/iam/iam_disable_30_days_credentials/iam_disable_30_days_credentials_test.py @@ -0,0 +1,97 @@ +import datetime +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_disable_30_days_credentials_test: + @mock_iam + def test_iam_user_logged_30_days(self): + password_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=2) + ).strftime("%Y-%m-%d %H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials import ( + iam_disable_30_days_credentials, + ) + + service_client.users[0].password_last_used = password_last_used + check = iam_disable_30_days_credentials() + result = check.execute() + assert result[0].status == "PASS" + assert search( + f"User {user} has logged into the console in the past 30 days.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_iam_user_not_logged_30_days(self): + password_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=40) + ).strftime("%Y-%m-%d %H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials import ( + iam_disable_30_days_credentials, + ) + + service_client.users[0].password_last_used = password_last_used + check = iam_disable_30_days_credentials() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + f"User {user} has not logged into the console in the past 30 days.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_iam_user_not_logged(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_30_days_credentials.iam_disable_30_days_credentials import ( + iam_disable_30_days_credentials, + ) + + service_client.users[0].password_last_used = "" + print(service_client.users) + # raise Exception + check = iam_disable_30_days_credentials() + result = check.execute() + assert result[0].status == "PASS" + assert search( + f"User {user} has not a console password or is unused.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn diff --git a/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py index 964bc9dc..614d1896 100644 --- a/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py +++ b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials.py @@ -16,7 +16,7 @@ class iam_disable_90_days_credentials(Check): report.region = iam_client.region report.resource_id = user.name report.resource_arn = user.arn - if user.password_last_used and user.password_last_used != "": + if user.password_last_used: time_since_insertion = ( datetime.datetime.now() - datetime.datetime.strptime( diff --git a/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials_test.py b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials_test.py new file mode 100644 index 00000000..66927a0d --- /dev/null +++ b/providers/aws/services/iam/iam_disable_90_days_credentials/iam_disable_90_days_credentials_test.py @@ -0,0 +1,97 @@ +import datetime +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_disable_90_days_credentials_test: + @mock_iam + def test_iam_user_logged_90_days(self): + password_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=2) + ).strftime("%Y-%m-%d %H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials import ( + iam_disable_90_days_credentials, + ) + + service_client.users[0].password_last_used = password_last_used + check = iam_disable_90_days_credentials() + result = check.execute() + assert result[0].status == "PASS" + assert search( + f"User {user} has logged into the console in the past 90 days.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_iam_user_not_logged_90_days(self): + password_last_used = ( + datetime.datetime.now() - datetime.timedelta(days=100) + ).strftime("%Y-%m-%d %H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials import ( + iam_disable_90_days_credentials, + ) + + service_client.users[0].password_last_used = password_last_used + check = iam_disable_90_days_credentials() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + f"User {user} has not logged into the console in the past 90 days.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_iam_user_not_logged(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_disable_90_days_credentials.iam_disable_90_days_credentials import ( + iam_disable_90_days_credentials, + ) + + service_client.users[0].password_last_used = "" + print(service_client.users) + # raise Exception + check = iam_disable_90_days_credentials() + result = check.execute() + assert result[0].status == "PASS" + assert search( + f"User {user} has not a console password or is unused.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn diff --git a/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key_test.py b/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key_test.py new file mode 100644 index 00000000..7432f913 --- /dev/null +++ b/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key_test.py @@ -0,0 +1,159 @@ +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_no_root_access_key_test: + @mock_iam + def test_iam_root_no_access_keys(self): + iam_client = client("iam") + user = "test" + iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import ( + iam_no_root_access_key, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012:user/" + service_client.credential_report[0]["access_key_1_active"] = "false" + service_client.credential_report[0]["access_key_2_active"] = "false" + check = iam_no_root_access_key() + result = check.execute() + print(service_client.credential_report) + # raise Exception + assert result[0].status == "PASS" + assert search( + f"User has not access keys.", + result[0].status_extended, + ) + assert result[0].resource_id == "" + assert ( + result[0].resource_arn + == "arn:aws:iam::123456789012:user/" + ) + + @mock_iam + def test_iam_root_access_key_1(self): + iam_client = client("iam") + user = "test" + iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import ( + iam_no_root_access_key, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012:user/" + service_client.credential_report[0]["access_key_1_active"] = "true" + service_client.credential_report[0]["access_key_2_active"] = "false" + check = iam_no_root_access_key() + result = check.execute() + print(service_client.credential_report) + # raise Exception + assert result[0].status == "FAIL" + assert search( + f"User has one active access key.", + result[0].status_extended, + ) + assert result[0].resource_id == "" + assert ( + result[0].resource_arn + == "arn:aws:iam::123456789012:user/" + ) + + @mock_iam + def test_iam_root_access_key_2(self): + iam_client = client("iam") + user = "test" + iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import ( + iam_no_root_access_key, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012:user/" + service_client.credential_report[0]["access_key_1_active"] = "false" + service_client.credential_report[0]["access_key_2_active"] = "true" + check = iam_no_root_access_key() + result = check.execute() + print(service_client.credential_report) + # raise Exception + assert result[0].status == "FAIL" + assert search( + f"User has one active access key.", + result[0].status_extended, + ) + assert result[0].resource_id == "" + assert ( + result[0].resource_arn + == "arn:aws:iam::123456789012:user/" + ) + + @mock_iam + def test_iam_root_both_access_keys(self): + iam_client = client("iam") + user = "test" + iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_no_root_access_key.iam_no_root_access_key import ( + iam_no_root_access_key, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012:user/" + service_client.credential_report[0]["access_key_1_active"] = "true" + service_client.credential_report[0]["access_key_2_active"] = "true" + check = iam_no_root_access_key() + result = check.execute() + print(service_client.credential_report) + # raise Exception + assert result[0].status == "FAIL" + assert search( + f"User has two active access key.", + result[0].status_extended, + ) + assert result[0].resource_id == "" + assert ( + result[0].resource_arn + == "arn:aws:iam::123456789012:user/" + ) diff --git a/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less_test.py b/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less_test.py new file mode 100644 index 00000000..73ecfb86 --- /dev/null +++ b/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less_test.py @@ -0,0 +1,74 @@ +from re import search +from unittest import mock + +from moto import mock_iam + + +class Test_iam_password_policy_expires_passwords_within_90_days_or_less: + @mock_iam + def test_password_expiration_lower_90(self): + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM, PasswordPolicy + + with mock.patch( + "providers.aws.services.iam.iam_password_policy_expires_passwords_within_90_days_or_less.iam_password_policy_expires_passwords_within_90_days_or_less.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_password_policy_expires_passwords_within_90_days_or_less.iam_password_policy_expires_passwords_within_90_days_or_less import ( + iam_password_policy_expires_passwords_within_90_days_or_less, + ) + + service_client.password_policy = PasswordPolicy( + length=10, + symbols=True, + numbers=True, + uppercase=True, + lowercase=True, + allow_change=True, + expiration=True, + max_age=40, + reuse_prevention=2, + hard_expiry=True, + ) + check = iam_password_policy_expires_passwords_within_90_days_or_less() + result = check.execute() + assert result[0].status == "PASS" + assert result[0].resource_id == "password_policy" + assert search( + "Password expiration is set lower than 90 days", + result[0].status_extended, + ) + + @mock_iam + def test_password_expiration_greater_90(self): + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM, PasswordPolicy + + with mock.patch( + "providers.aws.services.iam.iam_password_policy_expires_passwords_within_90_days_or_less.iam_password_policy_expires_passwords_within_90_days_or_less.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_password_policy_expires_passwords_within_90_days_or_less.iam_password_policy_expires_passwords_within_90_days_or_less import ( + iam_password_policy_expires_passwords_within_90_days_or_less, + ) + + service_client.password_policy = PasswordPolicy( + length=10, + symbols=True, + numbers=True, + uppercase=True, + lowercase=True, + allow_change=True, + expiration=True, + max_age=100, + reuse_prevention=2, + hard_expiry=True, + ) + check = iam_password_policy_expires_passwords_within_90_days_or_less() + result = check.execute() + assert result[0].status == "FAIL" + assert result[0].resource_id == "password_policy" + assert search( + "Password expiration is set greater than 90 days", + result[0].status_extended, + ) diff --git a/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py b/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py index b76fc7bf..789485e3 100644 --- a/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py +++ b/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation.py @@ -14,6 +14,9 @@ from providers.aws.services.iam.iam_client import iam_client class iam_policy_allows_privilege_escalation(Check): def execute(self) -> Check_Report: + # Is necessary to include the "Action:*" for + # each service that has a policy that could + # allow for privilege escalation privilege_escalation_iam_actions = { "iam:AttachGroupPolicy", "iam:SetDefaultPolicyVersion2", @@ -30,23 +33,31 @@ class iam_policy_allows_privilege_escalation(Check): "iam:SetDefaultPolicyVersion", "iam:UpdateAssumeRolePolicy", "iam:UpdateLoginProfile", + "iam:*", "sts:AssumeRole", + "sts:*", "ec2:RunInstances", + "ec2:*", "lambda:CreateEventSourceMapping", "lambda:CreateFunction", "lambda:InvokeFunction", "lambda:UpdateFunctionCode", + "lambda:*", "dynamodb:CreateTable", "dynamodb:PutItem", + "dynamodb:*", "glue:CreateDevEndpoint", "glue:GetDevEndpoint", "glue:GetDevEndpoints", "glue:UpdateDevEndpoint", + "glue:*", "cloudformation:CreateStack", "cloudformation:DescribeStacks", + "cloudformation:*", "datapipeline:CreatePipeline", "datapipeline:PutPipelineDefinition", "datapipeline:ActivatePipeline", + "datapipeline:*", } findings = [] for policy in iam_client.customer_managed_policies: @@ -86,7 +97,11 @@ class iam_policy_allows_privilege_escalation(Check): # First, we need to perform a left join with ALLOWED_ACTIONS and DENIED_ACTIONS left_actions = allowed_actions.difference(denied_actions) # Then, we need to find the DENIED_NOT_ACTIONS in LEFT_ACTIONS - privileged_actions = left_actions.intersection(denied_not_actions) + if denied_not_actions: + privileged_actions = left_actions.intersection(denied_not_actions) + # If there is no Denied Not Actions + else: + privileged_actions = left_actions # Finally, check if there is a privilege escalation action within this policy policy_privilege_escalation_actions = privileged_actions.intersection( privilege_escalation_iam_actions diff --git a/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py b/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py new file mode 100644 index 00000000..a9c044d3 --- /dev/null +++ b/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py @@ -0,0 +1,185 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_iam + +AWS_REGION = "us-east-1" + + +class Test_iam_policy_allows_privilege_escalation: + @mock_iam + def test_iam_policy_allows_privilege_escalation_sts(self): + iam_client = client("iam", region_name=AWS_REGION) + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "sts:*", "Resource": "*"}, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Customer Managed IAM Policy {policy_arn} allows for privilege escalation using the following actions: {{'sts:*'}}" + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == policy_arn + + @mock_iam + def test_iam_policy_not_allows_privilege_escalation(self): + + iam_client = client("iam", region_name=AWS_REGION) + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "sts:*", "Resource": "*"}, + {"Effect": "Deny", "Action": "sts:*", "Resource": "*"}, + {"Effect": "Deny", "NotAction": "sts:*", "Resource": "*"}, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Customer Managed IAM Policy {policy_arn} not allows for privilege escalation" + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == policy_arn + + @mock_iam + def test_iam_policy_not_allows_privilege_escalation_glue_GetDevEndpoints(self): + + iam_client = client("iam", region_name=AWS_REGION) + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "lambda:*", "Resource": "*"}, + {"Effect": "Deny", "Action": "lambda:InvokeFunction", "Resource": "*"}, + { + "Effect": "Deny", + "NotAction": "glue:GetDevEndpoints", + "Resource": "*", + }, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Customer Managed IAM Policy {policy_arn} not allows for privilege escalation" + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == policy_arn + + @mock_iam + def test_iam_policy_not_allows_privilege_escalation_dynamodb_PutItem(self): + + iam_client = client("iam", region_name=AWS_REGION) + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:*", + "iam:PassRole", + "dynamodb:PutItem", + "cloudformation:CreateStack", + "cloudformation:DescribeStacks", + "ec2:RunInstances", + ], + "Resource": "*", + }, + { + "Effect": "Deny", + "Action": ["lambda:InvokeFunction", "cloudformation:CreateStack"], + "Resource": "*", + }, + {"Effect": "Deny", "NotAction": "dynamodb:PutItem", "Resource": "*"}, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Customer Managed IAM Policy {policy_arn} allows for privilege escalation using the following actions: {{'dynamodb:PutItem'}}" + ) + assert result[0].resource_id == policy_name + assert result[0].resource_arn == policy_arn diff --git a/providers/aws/services/iam/iam_root_hardware_mfa_enabled/iam_root_hardware_mfa_enabled_test.py b/providers/aws/services/iam/iam_root_hardware_mfa_enabled/iam_root_hardware_mfa_enabled_test.py new file mode 100644 index 00000000..6d81bfa0 --- /dev/null +++ b/providers/aws/services/iam/iam_root_hardware_mfa_enabled/iam_root_hardware_mfa_enabled_test.py @@ -0,0 +1,65 @@ +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_root_hardware_mfa_enabled_test: + @mock_iam + def test_root_hardware_virtual_mfa_enabled(self): + iam = client("iam") + mfa_device_name = "mfa-test" + iam.create_virtual_mfa_device(VirtualMFADeviceName=mfa_device_name) + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import ( + iam_root_hardware_mfa_enabled, + ) + + service_client.account_summary["SummaryMap"]["AccountMFAEnabled"] = 1 + service_client.virtual_mfa_devices[0]["SerialNumber"] = "sddfaf-root-sfsfds" + + check = iam_root_hardware_mfa_enabled() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + "Root account has a virtual MFA instead of a hardware MFA enabled.", + result[0].status_extended, + ) + assert result[0].resource_id == "root" + + @mock_iam + def test_root_hardware_virtual_hardware_mfa_enabled(self): + iam = client("iam") + mfa_device_name = "mfa-test" + iam.create_virtual_mfa_device(VirtualMFADeviceName=mfa_device_name) + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_root_hardware_mfa_enabled.iam_root_hardware_mfa_enabled import ( + iam_root_hardware_mfa_enabled, + ) + + service_client.account_summary["SummaryMap"]["AccountMFAEnabled"] = 1 + service_client.virtual_mfa_devices[0]["SerialNumber"] = "" + + check = iam_root_hardware_mfa_enabled() + result = check.execute() + assert result[0].status == "PASS" + assert search( + "Root account has hardware MFA enabled.", result[0].status_extended + ) + assert result[0].resource_id == "root" + assert ( + result[0].resource_arn == f"arn:aws:iam::{service_client.account}:root" + ) diff --git a/providers/aws/services/iam/iam_root_mfa_enabled/iam_root_mfa_enabled_test.py b/providers/aws/services/iam/iam_root_mfa_enabled/iam_root_mfa_enabled_test.py new file mode 100644 index 00000000..7ce1984e --- /dev/null +++ b/providers/aws/services/iam/iam_root_mfa_enabled/iam_root_mfa_enabled_test.py @@ -0,0 +1,67 @@ +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_root_mfa_enabled_test: + @mock_iam + def test_root_mfa_not_enabled(self): + iam_client = client("iam") + user = "test-user" + iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled import ( + iam_root_mfa_enabled, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0]["mfa_active"] = "false" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012::root" + + check = iam_root_mfa_enabled() + result = check.execute() + assert result[0].status == "FAIL" + assert search( + "MFA is not enabled for root account.", result[0].status_extended + ) + assert result[0].resource_id == "" + assert result[0].resource_arn == service_client.credential_report[0]["arn"] + + @mock_iam + def test_root_mfa_enabled(self): + iam_client = client("iam") + user = "test-user" + iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_root_mfa_enabled.iam_root_mfa_enabled import ( + iam_root_mfa_enabled, + ) + + service_client.credential_report[0]["user"] = "" + service_client.credential_report[0]["mfa_active"] = "true" + service_client.credential_report[0][ + "arn" + ] = "arn:aws:iam::123456789012::root" + + check = iam_root_mfa_enabled() + result = check.execute() + assert result[0].status == "PASS" + assert search("MFA is enabled for root account.", result[0].status_extended) + assert result[0].resource_id == "" + assert result[0].resource_arn == service_client.credential_report[0]["arn"] diff --git a/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py b/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py new file mode 100644 index 00000000..48c1e52b --- /dev/null +++ b/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days_test.py @@ -0,0 +1,102 @@ +import datetime +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_rotate_access_key_90_days_test: + @mock_iam + def test_user_no_access_keys(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( + iam_rotate_access_key_90_days, + ) + + service_client.credential_report[0]["access_key_1_last_rotated"] == "N/A" + service_client.credential_report[0]["access_key_2_last_rotated"] == "N/A" + + check = iam_rotate_access_key_90_days() + result = check.execute() + assert result[0].status == "PASS" + assert result[0].status_extended == f"User {user} has not access keys." + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_access_key_1_not_rotated(self): + credentials_last_rotated = ( + datetime.datetime.now() - datetime.timedelta(days=100) + ).strftime("%Y-%m-%dT%H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( + iam_rotate_access_key_90_days, + ) + + service_client.credential_report[0][ + "access_key_1_last_rotated" + ] = credentials_last_rotated + + check = iam_rotate_access_key_90_days() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"User {user} has not rotated access key 1 in over 90 days (100 days)." + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_access_key_2_not_rotated(self): + credentials_last_rotated = ( + datetime.datetime.now() - datetime.timedelta(days=100) + ).strftime("%Y-%m-%dT%H:%M:%S+00:00") + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_rotate_access_key_90_days.iam_rotate_access_key_90_days import ( + iam_rotate_access_key_90_days, + ) + + service_client.credential_report[0][ + "access_key_2_last_rotated" + ] = credentials_last_rotated + + check = iam_rotate_access_key_90_days() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"User {user} has not rotated access key 2 in over 90 days (100 days)." + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn diff --git a/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled.py b/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled.py index 18729b00..8fe5e27f 100644 --- a/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled.py +++ b/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled.py @@ -13,17 +13,15 @@ class iam_user_hardware_mfa_enabled(Check): report.resource_arn = user.arn report.region = iam_client.region if user.mfa_devices: + report.status = "PASS" + report.status_extended = f"User {user.name} has hardware MFA enabled." for mfa_device in user.mfa_devices: if mfa_device.type == "mfa" or mfa_device.type == "sms-mfa": report.status = "FAIL" report.status_extended = f"User {user.name} has a virtual MFA instead of a hardware MFA enabled." - findings.append(report) - else: - report.status = "PASS" - report.status_extended = ( - f"User {user.name} has hardware MFA enabled." - ) - findings.append(report) + break + + findings.append(report) else: report.status = "FAIL" report.status_extended = ( diff --git a/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled_test.py b/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled_test.py new file mode 100644 index 00000000..cb380e87 --- /dev/null +++ b/providers/aws/services/iam/iam_user_hardware_mfa_enabled/iam_user_hardware_mfa_enabled_test.py @@ -0,0 +1,103 @@ +from re import search +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_user_hardware_mfa_enabled_test: + @mock_iam + def test_user_no_mfa_devices(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled import ( + iam_user_hardware_mfa_enabled, + ) + + service_client.users[0].mfa_devices = [] + check = iam_user_hardware_mfa_enabled() + result = check.execute() + + assert result[0].status == "FAIL" + assert search( + f"User {user} has not any type of MFA enabled.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_virtual_mfa_devices(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM, MFADevice + + with mock.patch( + "providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled import ( + iam_user_hardware_mfa_enabled, + ) + + mfa_devices = [ + MFADevice(serial_number="123454", type="mfa"), + MFADevice(serial_number="1234547", type="sms-mfa"), + ] + + service_client.users[0].mfa_devices = mfa_devices + check = iam_user_hardware_mfa_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + f"User {user} has a virtual MFA instead of a hardware MFA enabled.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_virtual_sms_mfa_devices(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM, MFADevice + + with mock.patch( + "providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_hardware_mfa_enabled.iam_user_hardware_mfa_enabled import ( + iam_user_hardware_mfa_enabled, + ) + + mfa_devices = [ + MFADevice(serial_number="123454", type="test-mfa"), + MFADevice(serial_number="1234547", type="sms-mfa"), + ] + + service_client.users[0].mfa_devices = mfa_devices + check = iam_user_hardware_mfa_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + f"User {user} has a virtual MFA instead of a hardware MFA enabled.", + result[0].status_extended, + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn diff --git a/providers/aws/services/iam/iam_user_mfa_enabled_console_access/iam_user_mfa_enabled_console_access_test.py b/providers/aws/services/iam/iam_user_mfa_enabled_console_access/iam_user_mfa_enabled_console_access_test.py new file mode 100644 index 00000000..fe3f0be1 --- /dev/null +++ b/providers/aws/services/iam/iam_user_mfa_enabled_console_access/iam_user_mfa_enabled_console_access_test.py @@ -0,0 +1,98 @@ +from unittest import mock + +from boto3 import client +from moto import mock_iam + + +class Test_iam_user_mfa_enabled_console_access_test: + @mock_iam + def test_user_not_password_console_enabled(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access import ( + iam_user_mfa_enabled_console_access, + ) + + service_client.credential_report[0]["password_enabled"] = "not_supported" + + check = iam_user_mfa_enabled_console_access() + result = check.execute() + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"User {user} has not Console Password enabled." + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_password_console_and_mfa_enabled(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access import ( + iam_user_mfa_enabled_console_access, + ) + + service_client.credential_report[0]["password_enabled"] = "true" + service_client.credential_report[0]["mfa_active"] = "true" + + check = iam_user_mfa_enabled_console_access() + result = check.execute() + + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"User {user} has Console Password enabled and MFA enabled." + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn + + @mock_iam + def test_user_password_console_enabled_and_mfa_not_enabled(self): + iam_client = client("iam") + user = "test-user" + arn = iam_client.create_user(UserName=user)["User"]["Arn"] + + from providers.aws.lib.audit_info.audit_info import current_audit_info + from providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access.iam_client", + new=IAM(current_audit_info), + ) as service_client: + from providers.aws.services.iam.iam_user_mfa_enabled_console_access.iam_user_mfa_enabled_console_access import ( + iam_user_mfa_enabled_console_access, + ) + + service_client.credential_report[0]["password_enabled"] = "true" + service_client.credential_report[0]["mfa_active"] = "false" + + check = iam_user_mfa_enabled_console_access() + result = check.execute() + + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"User {user} has Console Password enabled but MFA disabled." + ) + assert result[0].resource_id == user + assert result[0].resource_arn == arn diff --git a/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key_test.py b/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key_test.py index cf3e4c0f..4c0a5419 100644 --- a/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key_test.py +++ b/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key_test.py @@ -9,7 +9,7 @@ class Test_iam_user_no_setup_initial_access_key_test: @mock_iam def test_setup_access_key_1_fail(self): raw_credential_report = r"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated -test_false_access_key_1,arn:aws:iam::106908755756:test_false_access_key_1,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,true,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" +test_false_access_key_1,arn:aws:iam::123456789012:test_false_access_key_1,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,true,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" credential_lines = raw_credential_report.split("\n") csv_reader = DictReader(credential_lines, delimiter=",") credential_list = list(csv_reader) @@ -35,7 +35,7 @@ test_false_access_key_1,arn:aws:iam::106908755756:test_false_access_key_1,2022-0 @mock_iam def test_setup_access_key_2_fail(self): raw_credential_report = r"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated -test_false_access_key_2,arn:aws:iam::106908755756:test_false_access_key_2,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,true,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" +test_false_access_key_2,arn:aws:iam::123456789012:test_false_access_key_2,2022-04-17T14:59:38+00:00,true,no_information,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,true,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" credential_lines = raw_credential_report.split("\n") csv_reader = DictReader(credential_lines, delimiter=",") credential_list = list(csv_reader) @@ -61,7 +61,7 @@ test_false_access_key_2,arn:aws:iam::106908755756:test_false_access_key_2,2022-0 @mock_iam def test_setup_access_key_pass(self): raw_credential_report = r"""user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated -test_pass,arn:aws:iam::106908755756:test_pass,2022-02-17T14:59:38+00:00,not_supported,no_information,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" +test_pass,arn:aws:iam::123456789012:test_pass,2022-02-17T14:59:38+00:00,not_supported,no_information,not_supported,not_supported,false,false,N/A,N/A,N/A,N/A,false,N/A,N/A,N/A,N/A,false,N/A,false,N/A""" credential_lines = raw_credential_report.split("\n") csv_reader = DictReader(credential_lines, delimiter=",") credential_list = list(csv_reader)