From 28ea37f3673716598b5cda55b695713b6add0329 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Tue, 18 Jul 2023 09:36:37 +0200 Subject: [PATCH] test(aws_provider): Role and User MFA (#2486) --- prowler/config/config.py | 1 - prowler/providers/aws/config.py | 1 + .../aws/lib/audit_info/audit_info.py | 4 +- prowler/providers/common/audit_info.py | 3 +- tests/providers/aws/aws_provider_test.py | 240 +++++++++++++----- tests/providers/common/audit_info_test.py | 118 ++++++--- 6 files changed, 271 insertions(+), 96 deletions(-) diff --git a/prowler/config/config.py b/prowler/config/config.py index 0a918475..0cf46b28 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -11,7 +11,6 @@ from prowler.lib.logger import logger timestamp = datetime.today() timestamp_utc = datetime.now(timezone.utc).replace(tzinfo=timezone.utc) prowler_version = "3.7.1" -boto3_user_agent_extra = "APN_1826889" html_logo_url = "https://github.com/prowler-cloud/prowler/" html_logo_img = "https://user-images.githubusercontent.com/3985464/113734260-7ba06900-96fb-11eb-82bc-d4f68a1e2710.png" square_logo_img = "https://user-images.githubusercontent.com/38561120/235905862-9ece5bd7-9aa3-4e48-807a-3a9035eb8bfb.png" diff --git a/prowler/providers/aws/config.py b/prowler/providers/aws/config.py index fed5d2dc..619b433f 100644 --- a/prowler/providers/aws/config.py +++ b/prowler/providers/aws/config.py @@ -1 +1,2 @@ AWS_STS_GLOBAL_ENDPOINT_REGION = "us-east-1" +BOTO3_USER_AGENT_EXTRA = "APN_1826889" diff --git a/prowler/providers/aws/lib/audit_info/audit_info.py b/prowler/providers/aws/lib/audit_info/audit_info.py index bcb59933..70043ecc 100644 --- a/prowler/providers/aws/lib/audit_info/audit_info.py +++ b/prowler/providers/aws/lib/audit_info/audit_info.py @@ -1,7 +1,7 @@ from boto3 import session from botocore.config import Config -from prowler.config.config import boto3_user_agent_extra +from prowler.providers.aws.config import BOTO3_USER_AGENT_EXTRA from prowler.providers.aws.lib.audit_info.models import AWS_Assume_Role, AWS_Audit_Info # Default Current Audit Info @@ -15,7 +15,7 @@ current_audit_info = AWS_Audit_Info( # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html session_config=Config( retries={"max_attempts": 3, "mode": "standard"}, - user_agent_extra=boto3_user_agent_extra, + user_agent_extra=BOTO3_USER_AGENT_EXTRA, ), audited_account=None, audited_account_arn=None, diff --git a/prowler/providers/common/audit_info.py b/prowler/providers/common/audit_info.py index 8aa24e6a..0507bd61 100644 --- a/prowler/providers/common/audit_info.py +++ b/prowler/providers/common/audit_info.py @@ -3,7 +3,6 @@ import sys from botocore.config import Config from colorama import Fore, Style -from prowler.config.config import boto3_user_agent_extra from prowler.lib.logger import logger from prowler.providers.aws.aws_provider import ( AWS_Provider, @@ -88,6 +87,7 @@ Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RE if input_session_duration and input_session_duration not in range(900, 43201): raise Exception("Value for -T option must be between 900 and 43200") + # Handle if session_duration is not the default value or external_id is set if ( input_session_duration and input_session_duration != 3600 ) or input_external_id: @@ -114,7 +114,6 @@ Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RE "max_attempts": aws_retries_max_attempts, "mode": "standard", }, - user_agent_extra=boto3_user_agent_extra, ) # Merge the new configuration new_boto3_config = current_audit_info.session_config.merge(config) diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py index 0f7c9378..53d3ee5b 100644 --- a/tests/providers/aws/aws_provider_test.py +++ b/tests/providers/aws/aws_provider_test.py @@ -14,20 +14,17 @@ from prowler.providers.aws.aws_provider import ( from prowler.providers.aws.lib.audit_info.models import AWS_Assume_Role, AWS_Audit_Info ACCOUNT_ID = 123456789012 +AWS_REGION = "us-east-1" class Test_AWS_Provider: @mock_iam @mock_sts - def test_assume_role_without_mfa(self): - # Variables - role_name = "test-role" - role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" - session_duration_seconds = 900 + def test_aws_provider_user_without_mfa(self): audited_regions = ["eu-west-1"] - sessionName = "ProwlerAsessmentSession" + # sessionName = "ProwlerAsessmentSession" # Boto 3 client to create our user - iam_client = boto3.client("iam", region_name="us-east-1") + iam_client = boto3.client("iam", region_name=AWS_REGION) # IAM user iam_user = iam_client.create_user(UserName="test-user")["User"] access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ @@ -39,7 +36,7 @@ class Test_AWS_Provider: session = boto3.session.Session( aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key, - region_name="us-east-1", + region_name=AWS_REGION, ) # Fulfil the input session object for Prowler @@ -56,8 +53,8 @@ class Test_AWS_Provider: profile_region=None, credentials=None, assumed_role_info=AWS_Assume_Role( - role_arn=role_arn, - session_duration=session_duration_seconds, + role_arn=None, + session_duration=None, external_id=None, mfa_enabled=False, ), @@ -68,47 +65,25 @@ class Test_AWS_Provider: ) # Call assume_role - aws_provider = AWS_Provider(audit_info) - assume_role_response = assume_role( - aws_provider.aws_session, aws_provider.role_info - ) - # Recover credentials for the assume role operation - credentials = assume_role_response["Credentials"] - # Test the response - # SessionToken - credentials["SessionToken"].should.have.length_of(356) - credentials["SessionToken"].startswith("FQoGZXIvYXdzE") - # AccessKeyId - credentials["AccessKeyId"].should.have.length_of(20) - credentials["AccessKeyId"].startswith("ASIA") - # SecretAccessKey - credentials["SecretAccessKey"].should.have.length_of(40) - # Assumed Role - assume_role_response["AssumedRoleUser"]["Arn"].should.equal( - f"arn:aws:sts::{ACCOUNT_ID}:assumed-role/{role_name}/{sessionName}" - ) - # AssumedRoleUser - assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].startswith( - "AROA" - ) - assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].endswith( - ":" + sessionName - ) - assume_role_response["AssumedRoleUser"]["AssumedRoleId"].should.have.length_of( - 21 + 1 + len(sessionName) - ) + with patch( + "prowler.providers.aws.aws_provider.input_role_mfa_token_and_code", + return_value=(f"arn:aws:iam::{ACCOUNT_ID}:mfa/test-role-mfa", "111111"), + ): + aws_provider = AWS_Provider(audit_info) + assert aws_provider.aws_session.region_name is None + assert aws_provider.role_info == AWS_Assume_Role( + role_arn=None, + session_duration=None, + external_id=None, + mfa_enabled=False, + ) @mock_iam @mock_sts - def test_assume_role_with_mfa(self): - # Variables - role_name = "test-role" - role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" - session_duration_seconds = 900 - audited_regions = ["eu-west-1"] - sessionName = "ProwlerAsessmentSession" + def test_aws_provider_user_with_mfa(self): + audited_regions = "eu-west-1" # Boto 3 client to create our user - iam_client = boto3.client("iam", region_name="us-east-1") + iam_client = boto3.client("iam", region_name=AWS_REGION) # IAM user iam_user = iam_client.create_user(UserName="test-user")["User"] access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ @@ -120,7 +95,71 @@ class Test_AWS_Provider: session = boto3.session.Session( aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key, - region_name="us-east-1", + region_name=AWS_REGION, + ) + + # Fulfil the input session object for Prowler + audit_info = AWS_Audit_Info( + session_config=None, + original_session=session, + audit_session=None, + audited_account=None, + audited_account_arn=None, + audited_partition=None, + audited_identity_arn=None, + audited_user_id=None, + profile=None, + profile_region=AWS_REGION, + credentials=None, + assumed_role_info=AWS_Assume_Role( + role_arn=None, + session_duration=None, + external_id=None, + mfa_enabled=False, + ), + audited_regions=audited_regions, + organizations_metadata=None, + audit_resources=None, + mfa_enabled=True, + ) + + # # Call assume_role + with patch( + "prowler.providers.aws.aws_provider.input_role_mfa_token_and_code", + return_value=(f"arn:aws:iam::{ACCOUNT_ID}:mfa/test-role-mfa", "111111"), + ): + aws_provider = AWS_Provider(audit_info) + assert aws_provider.aws_session.region_name is None + assert aws_provider.role_info == AWS_Assume_Role( + role_arn=None, + session_duration=None, + external_id=None, + mfa_enabled=False, + ) + + @mock_iam + @mock_sts + def test_aws_provider_assume_role_with_mfa(self): + # Variables + role_name = "test-role" + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" + session_duration_seconds = 900 + audited_regions = ["eu-west-1"] + sessionName = "ProwlerAsessmentSession" + # Boto 3 client to create our user + iam_client = boto3.client("iam", region_name=AWS_REGION) + # IAM user + iam_user = iam_client.create_user(UserName="test-user")["User"] + access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ + "AccessKey" + ] + access_key_id = access_key["AccessKeyId"] + secret_access_key = access_key["SecretAccessKey"] + # New Boto3 session with the previously create user + session = boto3.session.Session( + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_access_key, + region_name=AWS_REGION, ) # Fulfil the input session object for Prowler @@ -186,17 +225,15 @@ class Test_AWS_Provider: @mock_iam @mock_sts - def test_assume_role_with_sts_endpoint_region(self): + def test_aws_provider_assume_role_without_mfa(self): # Variables role_name = "test-role" role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" session_duration_seconds = 900 - aws_region = "eu-west-1" - sts_endpoint_region = aws_region - audited_regions = [aws_region] + audited_regions = "eu-west-1" sessionName = "ProwlerAsessmentSession" # Boto 3 client to create our user - iam_client = boto3.client("iam", region_name=aws_region) + iam_client = boto3.client("iam", region_name=AWS_REGION) # IAM user iam_user = iam_client.create_user(UserName="test-user")["User"] access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ @@ -208,7 +245,90 @@ class Test_AWS_Provider: session = boto3.session.Session( aws_access_key_id=access_key_id, aws_secret_access_key=secret_access_key, - region_name=aws_region, + region_name=AWS_REGION, + ) + + # Fulfil the input session object for Prowler + audit_info = AWS_Audit_Info( + session_config=None, + original_session=session, + audit_session=None, + audited_account=None, + audited_account_arn=None, + audited_partition=None, + audited_identity_arn=None, + audited_user_id=None, + profile=None, + profile_region=None, + credentials=None, + assumed_role_info=AWS_Assume_Role( + role_arn=role_arn, + session_duration=session_duration_seconds, + external_id=None, + mfa_enabled=False, + ), + audited_regions=audited_regions, + organizations_metadata=None, + audit_resources=None, + mfa_enabled=False, + ) + + # Call assume_role + aws_provider = AWS_Provider(audit_info) + assume_role_response = assume_role( + aws_provider.aws_session, aws_provider.role_info + ) + # Recover credentials for the assume role operation + credentials = assume_role_response["Credentials"] + # Test the response + # SessionToken + credentials["SessionToken"].should.have.length_of(356) + credentials["SessionToken"].startswith("FQoGZXIvYXdzE") + # AccessKeyId + credentials["AccessKeyId"].should.have.length_of(20) + credentials["AccessKeyId"].startswith("ASIA") + # SecretAccessKey + credentials["SecretAccessKey"].should.have.length_of(40) + # Assumed Role + assume_role_response["AssumedRoleUser"]["Arn"].should.equal( + f"arn:aws:sts::{ACCOUNT_ID}:assumed-role/{role_name}/{sessionName}" + ) + # AssumedRoleUser + assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].startswith( + "AROA" + ) + assert assume_role_response["AssumedRoleUser"]["AssumedRoleId"].endswith( + ":" + sessionName + ) + assume_role_response["AssumedRoleUser"]["AssumedRoleId"].should.have.length_of( + 21 + 1 + len(sessionName) + ) + + @mock_iam + @mock_sts + def test_assume_role_with_sts_endpoint_region(self): + # Variables + role_name = "test-role" + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" + session_duration_seconds = 900 + aws_region = "eu-west-1" + sts_endpoint_region = aws_region + audited_regions = [aws_region] + sessionName = "ProwlerAsessmentSession" + # Boto 3 client to create our user + iam_client = boto3.client("iam", region_name=AWS_REGION) + # IAM user + iam_user = iam_client.create_user(UserName="test-user")["User"] + access_key = iam_client.create_access_key(UserName=iam_user["UserName"])[ + "AccessKey" + ] + access_key_id = access_key["AccessKeyId"] + secret_access_key = access_key["SecretAccessKey"] + # New Boto3 session with the previously create user + session = boto3.session.Session( + aws_access_key_id=access_key_id, + aws_secret_access_key=secret_access_key, + region_name=AWS_REGION, ) # Fulfil the input session object for Prowler @@ -270,9 +390,9 @@ class Test_AWS_Provider: def test_generate_regional_clients(self): # New Boto3 session with the previously create user session = boto3.session.Session( - region_name="us-east-1", + region_name=AWS_REGION, ) - audited_regions = ["eu-west-1", "us-east-1"] + audited_regions = ["eu-west-1", AWS_REGION] # Fulfil the input session object for Prowler audit_info = AWS_Audit_Info( session_config=None, @@ -301,10 +421,10 @@ class Test_AWS_Provider: def test_generate_regional_clients_global_service(self): # New Boto3 session with the previously create user session = boto3.session.Session( - region_name="us-east-1", + region_name=AWS_REGION, ) - audited_regions = ["eu-west-1", "us-east-1"] - profile_region = "us-east-1" + audited_regions = ["eu-west-1", AWS_REGION] + profile_region = AWS_REGION # Fulfil the input session object for Prowler audit_info = AWS_Audit_Info( session_config=None, @@ -333,7 +453,7 @@ class Test_AWS_Provider: def test_generate_regional_clients_cn_partition(self): # New Boto3 session with the previously create user session = boto3.session.Session( - region_name="us-east-1", + region_name=AWS_REGION, ) audited_regions = ["cn-northwest-1", "cn-north-1"] # Fulfil the input session object for Prowler diff --git a/tests/providers/common/audit_info_test.py b/tests/providers/common/audit_info_test.py index f80be028..be6f4f7f 100644 --- a/tests/providers/common/audit_info_test.py +++ b/tests/providers/common/audit_info_test.py @@ -1,5 +1,6 @@ import boto3 import botocore +import pytest import sure # noqa from boto3 import session from mock import patch @@ -119,37 +120,6 @@ class Test_Set_Audit_Info: return audit_info - @patch( - "prowler.providers.common.audit_info.validate_aws_credentials", - new=mock_validate_credentials, - ) - @patch( - "prowler.providers.common.audit_info.print_aws_credentials", - new=mock_print_audit_credentials, - ) - def test_set_audit_info_aws(self): - with patch( - "prowler.providers.common.audit_info.current_audit_info", - new=self.set_mocked_audit_info(), - ): - provider = "aws" - arguments = { - "profile": None, - "role": None, - "session_duration": None, - "external_id": None, - "regions": None, - "organizations_role": None, - "subscriptions": None, - "az_cli_auth": None, - "sp_env_auth": None, - "browser_auth": None, - "managed_entity_auth": None, - } - - audit_info = set_provider_audit_info(provider, arguments) - assert isinstance(audit_info, AWS_Audit_Info) - @patch( "prowler.providers.common.audit_info.azure_audit_info", new=mock_azure_audit_info, @@ -245,3 +215,89 @@ class Test_Set_Audit_Info: assert instance_id in str( get_tagged_resources(["MY_TAG1=MY_VALUE1"], mock_audit_info) ) + + @patch( + "prowler.providers.common.audit_info.validate_aws_credentials", + new=mock_validate_credentials, + ) + @patch( + "prowler.providers.common.audit_info.print_aws_credentials", + new=mock_print_audit_credentials, + ) + def test_set_audit_info_aws(self): + with patch( + "prowler.providers.common.audit_info.current_audit_info", + new=self.set_mocked_audit_info(), + ): + provider = "aws" + arguments = { + "profile": None, + "role": None, + "session_duration": None, + "external_id": None, + "regions": None, + "organizations_role": None, + } + + audit_info = set_provider_audit_info(provider, arguments) + assert isinstance(audit_info, AWS_Audit_Info) + + def test_set_audit_info_aws_bad_session_duration(self): + with patch( + "prowler.providers.common.audit_info.current_audit_info", + new=self.set_mocked_audit_info(), + ): + provider = "aws" + arguments = { + "profile": None, + "role": None, + "session_duration": 100, + "external_id": None, + "regions": None, + "organizations_role": None, + } + + with pytest.raises(SystemExit) as exception: + _ = set_provider_audit_info(provider, arguments) + # assert exception == "Value for -T option must be between 900 and 43200" + assert isinstance(exception, pytest.ExceptionInfo) + + def test_set_audit_info_aws_session_duration_without_role(self): + with patch( + "prowler.providers.common.audit_info.current_audit_info", + new=self.set_mocked_audit_info(), + ): + provider = "aws" + arguments = { + "profile": None, + "role": None, + "session_duration": 1000, + "external_id": None, + "regions": None, + "organizations_role": None, + } + + with pytest.raises(SystemExit) as exception: + _ = set_provider_audit_info(provider, arguments) + # assert exception == "To use -I/-T options -R option is needed" + assert isinstance(exception, pytest.ExceptionInfo) + + def test_set_audit_info_external_id_without_role(self): + with patch( + "prowler.providers.common.audit_info.current_audit_info", + new=self.set_mocked_audit_info(), + ): + provider = "aws" + arguments = { + "profile": None, + "role": None, + "session_duration": 3600, + "external_id": "test-external-id", + "regions": None, + "organizations_role": None, + } + + with pytest.raises(SystemExit) as exception: + _ = set_provider_audit_info(provider, arguments) + # assert exception == "To use -I/-T options -R option is needed" + assert isinstance(exception, pytest.ExceptionInfo)