diff --git a/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/__init__.py b/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.metadata.json b/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.metadata.json new file mode 100644 index 00000000..7baa06cf --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "iam_role_cross_account_readonlyaccess_policy", + "CheckTitle": "Ensure IAM Roles do not have ReadOnlyAccess access for external AWS accounts", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "high", + "ResourceType": "AwsIamRole", + "Description": "Ensure IAM Roles do not have ReadOnlyAccess access for external AWS accounts", + "Risk": "The AWS-managed ReadOnlyAccess policy is highly potent and exposes the customer to a significant data leakage threat. It should be granted very conservatively. For granting access to 3rd party vendors, consider using alternative managed policies, such as ViewOnlyAccess or SecurityAudit.", + "RelatedUrl": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_job-functions.html#awsmp_readonlyaccess", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove the AWS-managed ReadOnlyAccess policy from all roles that have a trust policy, including third-party cloud accounts, or remove third-party cloud accounts from the trust policy of all roles that need the ReadOnlyAccess policy.", + "Url": "https://docs.securestate.vmware.com/rule-docs/aws-iam-role-cross-account-readonlyaccess-policy" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "CAF Security Epic: IAM" +} diff --git a/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.py b/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.py new file mode 100644 index 00000000..6c59a0e8 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy.py @@ -0,0 +1,79 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client + + +class iam_role_cross_account_readonlyaccess_policy(Check): + def execute(self) -> Check_Report_AWS: + findings = [] + for role in iam_client.roles: + if ( + not role.is_service_role + ): # Avoid service roles since they cannot be modified by the user + report = Check_Report_AWS(self.metadata()) + report.region = iam_client.region + report.resource_arn = role.arn + report.resource_id = role.name + report.resource_tags = role.tags + report.status = "PASS" + report.status_extended = ( + f"IAM Role {role.name} has not ReadOnlyAccess policy" + ) + for policy in role.attached_policies: + if policy["PolicyName"] == "ReadOnlyAccess": + report.status_extended = f"IAM Role {role.name} has read-only access but is not cross account" + cross_account_access = False + if type(role.assume_role_policy["Statement"]) == list: + for statement in role.assume_role_policy["Statement"]: + if not cross_account_access: + if ( + statement["Effect"] == "Allow" + and "AWS" in statement["Principal"] + ): + if type(statement["Principal"]["AWS"]) == list: + for aws_account in statement["Principal"][ + "AWS" + ]: + if ( + iam_client.account + not in aws_account + or "*" == aws_account + ): + cross_account_access = True + break + else: + if ( + iam_client.account + not in statement["Principal"]["AWS"] + or "*" == statement["Principal"]["AWS"] + ): + cross_account_access = True + else: + break + else: + statement = role.assume_role_policy["Statement"] + if ( + statement["Effect"] == "Allow" + and "AWS" in statement["Principal"] + ): + if type(statement["Principal"]["AWS"]) == list: + for aws_account in statement["Principal"]["AWS"]: + if ( + iam_client.account not in aws_account + or "*" == aws_account + ): + cross_account_access = True + break + else: + if ( + iam_client.account + not in statement["Principal"]["AWS"] + or "*" == statement["Principal"]["AWS"] + ): + cross_account_access = True + if cross_account_access: + report.status = "FAIL" + report.status_extended = f"IAM Role {role.name} gives cross account read-only access!" + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json index e7a84fba..54f30ccb 100644 --- a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json +++ b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", - "ResourceType": "AwsIamPolicy", + "ResourceType": "AwsIamRole", "Description": "Ensure IAM Service Roles prevents against a cross-service confused deputy attack", "Risk": "Allow attackers to gain unauthorized access to resources", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index 7da3ba22..7453c314 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -11,19 +11,38 @@ from prowler.providers.aws.aws_provider import generate_regional_clients def is_service_role(role): - if "Statement" in role["AssumeRolePolicyDocument"]: - for statement in role["AssumeRolePolicyDocument"]["Statement"]: - if ( - statement["Effect"] == "Allow" - and ( - "sts:AssumeRole" in statement["Action"] - or "sts:*" in statement["Action"] - or "*" in statement["Action"] - ) - # This is what defines a service role - and "Service" in statement["Principal"] - ): - return True + try: + if "Statement" in role["AssumeRolePolicyDocument"]: + if type(role["AssumeRolePolicyDocument"]["Statement"]) == list: + for statement in role["AssumeRolePolicyDocument"]["Statement"]: + if ( + statement["Effect"] == "Allow" + and ( + "sts:AssumeRole" in statement["Action"] + or "sts:*" in statement["Action"] + or "*" in statement["Action"] + ) + # This is what defines a service role + and "Service" in statement["Principal"] + ): + return True + else: + statement = role["AssumeRolePolicyDocument"]["Statement"] + if ( + statement["Effect"] == "Allow" + and ( + "sts:AssumeRole" in statement["Action"] + or "sts:*" in statement["Action"] + or "*" in statement["Action"] + ) + # This is what defines a service role + and "Service" in statement["Principal"] + ): + return True + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) return False @@ -50,6 +69,7 @@ class IAM: self.__get_group_users__() self.__list_attached_group_policies__() self.__list_attached_user_policies__() + self.__list_attached_role_policies__() self.__list_inline_user_policies__() self.__list_mfa_devices__() self.password_policy = self.__get_password_policy__() @@ -338,6 +358,27 @@ class IAM: f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def __list_attached_role_policies__(self): + logger.info("IAM - List Attached User Policies...") + try: + for role in self.roles: + attached_role_policies = [] + list_attached_role_policies_paginator = self.client.get_paginator( + "list_attached_role_policies" + ) + for page in list_attached_role_policies_paginator.paginate( + RoleName=role.name + ): + for policy in page["AttachedPolicies"]: + attached_role_policies.append(policy) + + role.attached_policies = attached_role_policies + + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + def __list_inline_user_policies__(self): logger.info("IAM - List Inline User Policies...") try: @@ -501,6 +542,7 @@ class Role(BaseModel): arn: str assume_role_policy: dict is_service_role: bool + attached_policies: list[dict] = [] tags: Optional[list] = [] diff --git a/tests/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy_test.py b/tests/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy_test.py new file mode 100644 index 00000000..5c24f6fa --- /dev/null +++ b/tests/providers/aws/services/iam/iam_role_cross_account_readonlyaccess_policy/iam_role_cross_account_readonlyaccess_policy_test.py @@ -0,0 +1,284 @@ +from json import dumps +from unittest import mock + +from boto3 import client, session +from moto import mock_iam + +from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info +from prowler.providers.aws.services.iam.iam_service import Role + +AWS_REGION = "us-east-1" +AWS_ACCOUNT_ID = "123456789012" + + +class Test_iam_role_cross_account_readonlyaccess_policy: + def set_mocked_audit_info(self): + audit_info = AWS_Audit_Info( + session_config=None, + original_session=None, + audit_session=session.Session( + profile_name=None, + botocore_session=None, + ), + audited_account=AWS_ACCOUNT_ID, + audited_user_id=None, + audited_partition="aws", + audited_identity_arn=None, + profile=None, + profile_region=None, + credentials=None, + assumed_role_info=None, + audited_regions=["us-east-1", "eu-west-1"], + organizations_metadata=None, + audit_resources=None, + ) + + return audit_info + + @mock_iam + def test_no_roles(self): + from prowler.providers.aws.services.iam.iam_service import IAM + + current_audit_info = self.set_mocked_audit_info() + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=current_audit_info, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 0 + + @mock_iam + def test_role_without_readonlyaccess_policy(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_ID}:root"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + + current_audit_info = self.set_mocked_audit_info() + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=current_audit_info, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Role test has not ReadOnlyAccess policy" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + + @mock_iam + def test_internal_role_with_readonlyaccess_policy(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_ID}:root"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/ReadOnlyAccess", + ) + + current_audit_info = self.set_mocked_audit_info() + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=current_audit_info, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Role test has read-only access but is not cross account" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + + @mock_iam + def test_cross_account_role_with_readonlyaccess_policy(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::012345678910:root"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/ReadOnlyAccess", + ) + + current_audit_info = self.set_mocked_audit_info() + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=current_audit_info, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "IAM Role test gives cross account read-only access!" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + + @mock_iam + def test_asterisk_cross_account_role_with_readonlyaccess_policy(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/ReadOnlyAccess", + ) + + current_audit_info = self.set_mocked_audit_info() + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=current_audit_info, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "IAM Role test gives cross account read-only access!" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + assert result[0].resource_tags == [] + + @mock_iam + def test_only_aws_service_linked_roles(self): + iam_client = mock.MagicMock + iam_client.roles = [] + iam_client.roles.append( + Role( + name="AWSServiceRoleForAmazonGuardDuty", + arn="arn:aws:iam::106908755756:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty", + assume_role_policy={ + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + }, + is_service_role=True, + ) + ) + + with mock.patch( + "prowler.providers.aws.services.iam.iam_service.IAM", + iam_client, + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_account_readonlyaccess_policy.iam_role_cross_account_readonlyaccess_policy import ( + iam_role_cross_account_readonlyaccess_policy, + ) + + check = iam_role_cross_account_readonlyaccess_policy() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/aws/services/iam/iam_service_test.py b/tests/providers/aws/services/iam/iam_service_test.py index 9ecec3d2..017bf6e1 100644 --- a/tests/providers/aws/services/iam/iam_service_test.py +++ b/tests/providers/aws/services/iam/iam_service_test.py @@ -518,6 +518,43 @@ class Test_IAM_Service: iam.groups[0].attached_policies[0]["PolicyArn"] == policy["Policy"]["Arn"] ) + # Test IAM List Attached Role Policies + @mock_iam + def test__list_attached_role_policies__(self): + iam = client("iam") + role_name = "test" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "sts:AssumeRole", + }, + } + response = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/ReadOnlyAccess", + ) + + # IAM client for this test class + audit_info = self.set_mocked_audit_info() + iam = IAM(audit_info) + + assert len(iam.roles) == 1 + assert iam.roles[0].name == role_name + assert iam.roles[0].arn == response["Role"]["Arn"] + assert len(iam.roles[0].attached_policies) == 1 + assert iam.roles[0].attached_policies[0]["PolicyName"] == "ReadOnlyAccess" + assert ( + iam.roles[0].attached_policies[0]["PolicyArn"] + == "arn:aws:iam::aws:policy/ReadOnlyAccess" + ) + @mock_iam def test__get_entities_attached_to_support_roles__no_roles(self): iam_client = client("iam")