From 415c3192083bcb3f144c7fc6ca9520f560b077ef Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:41:58 +0200 Subject: [PATCH] feat(iam): add new check `iam_policy_cloudshell_admin_not_attached` (#5437) Co-authored-by: Sergio --- .../__init__.py | 0 ...loudshell_admin_not_attached.metadata.json | 34 +++ ...am_policy_cloudshell_admin_not_attached.py | 34 +++ .../providers/aws/services/iam/iam_service.py | 44 +++ ...licy_cloudshell_admin_not_attached_test.py | 269 ++++++++++++++++++ .../aws/services/iam/iam_service_test.py | 39 +++ 6 files changed, 420 insertions(+) create mode 100644 prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/__init__.py create mode 100644 prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.metadata.json create mode 100644 prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.py create mode 100644 tests/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached_test.py diff --git a/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/__init__.py b/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.metadata.json b/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.metadata.json new file mode 100644 index 00000000000..9e174a0ebd0 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "iam_policy_cloudshell_admin_not_attached", + "CheckTitle": "Check if IAM identities (users,groups,roles) have the AWSCloudShellFullAccess policy attached.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/CIS AWS Foundations Benchmark" + ], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:iam::{account-id}:{resource-type}/{resource-id}", + "Severity": "medium", + "ResourceType": "AwsIamPolicy", + "Description": "This control checks whether an IAM identity (user, role, or group) has the AWS managed policy AWSCloudShellFullAccess attached. The control fails if an IAM identity has the AWSCloudShellFullAccess policy attached.", + "Risk": "Attaching the AWSCloudShellFullAccess policy to IAM identities grants broad permissions, including internet access and file transfer capabilities, which can lead to security risks such as data exfiltration. The principle of least privilege should be followed to avoid excessive permissions.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/iam-policy-blacklisted-check.html", + "Remediation": { + "Code": { + "CLI": "aws iam detach-user/role/group-policy --user/role/group-name --policy-arn arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/iam-controls.html#iam-27", + "Terraform": "" + }, + "Recommendation": { + "Text": "Detach the AWSCloudShellFullAccess policy from the IAM identity to restrict excessive permissions and adhere to the principle of least privilege.", + "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage-attach-detach.html" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.py b/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.py new file mode 100644 index 00000000000..bb40835acf3 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached.py @@ -0,0 +1,34 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client + + +class iam_policy_cloudshell_admin_not_attached(Check): + def execute(self) -> Check_Report_AWS: + findings = [] + if iam_client.entities_attached_to_cloudshell_policy: + report = Check_Report_AWS(self.metadata()) + report.region = iam_client.region + report.resource_id = iam_client.audited_account + report.resource_arn = f"arn:{iam_client.audited_partition}:iam::aws:policy/AWSCloudShellFullAccess" + entities = iam_client.entities_attached_to_cloudshell_policy + + if entities["Users"] or entities["Groups"] or entities["Roles"]: + report.status = "FAIL" + attached_entities = [ + (key, ", ".join(entities[key])) + for key in ["Users", "Groups", "Roles"] + if entities[key] + ] + entity_strings = [ + f"{entity[0]}: {entity[1]}" for entity in attached_entities + ] + report.status_extended = f"AWS CloudShellFullAccess policy attached to IAM {', '.join(entity_strings)}." + else: + report.status = "PASS" + report.status_extended = ( + "AWS CloudShellFullAccess policy is not attached to any IAM entity." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index b32113bce76..b7f1ef0c3df 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -80,6 +80,12 @@ def __init__(self, provider): self.entities_role_attached_to_securityaudit_policy = ( self._list_entities_role_for_policy(securityaudit_policy_arn) ) + cloudshell_admin_policy_arn = ( + f"arn:{self.audited_partition}:iam::aws:policy/AWSCloudShellFullAccess" + ) + self.entities_attached_to_cloudshell_policy = self._list_entities_for_policy( + cloudshell_admin_policy_arn + ) # List both Customer (attached and unattached) and AWS Managed (only attached) policies self.policies = [] self.policies.extend(self._list_policies("AWS")) @@ -685,6 +691,44 @@ def _list_entities_role_for_policy(self, policy_arn): finally: return roles + def _list_entities_for_policy(self, policy_arn): + logger.info("IAM - List Entities Role For Policy...") + try: + entities = { + "Users": [], + "Groups": [], + "Roles": [], + } + + paginator = self.client.get_paginator("list_entities_for_policy") + for response in paginator.paginate(PolicyArn=policy_arn): + entities["Users"].extend( + user["UserName"] for user in response.get("PolicyUsers", []) + ) + entities["Groups"].extend( + group["GroupName"] for group in response.get("PolicyGroups", []) + ) + entities["Roles"].extend( + role["RoleName"] for role in response.get("PolicyRoles", []) + ) + return entities + except ClientError as error: + if error.response["Error"]["Code"] == "AccessDenied": + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + entities = None + else: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + finally: + return entities + def _list_policies(self, scope): logger.info("IAM - List Policies...") try: diff --git a/tests/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached_test.py b/tests/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached_test.py new file mode 100644 index 00000000000..123683c9f5a --- /dev/null +++ b/tests/providers/aws/services/iam/iam_policy_cloudshell_admin_not_attached/iam_policy_cloudshell_admin_not_attached_test.py @@ -0,0 +1,269 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.iam.iam_service import IAM +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + + +class Test_iam_policy_cloudshell_admin_not_attached: + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_access_denied(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ) as service_client: + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + service_client.entities_attached_to_cloudshell_policy = None + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert len(result) == 0 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_nocloudshell_policy(self): + iam = client("iam") + role_name = "test_nocloudshell_policy" + role_policy = { + "Version": "2012-10-17", + } + iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(role_policy), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/SecurityAudit", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "AWS CloudShellFullAccess policy is not attached to any IAM entity." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == "arn:aws:iam::aws:policy/AWSCloudShellFullAccess" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_role_cloudshell_policy(self): + iam = client("iam") + role_name = "test_cloudshell_policy_role" + role_policy = { + "Version": "2012-10-17", + } + iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(role_policy), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"AWS CloudShellFullAccess policy attached to IAM Roles: {role_name}." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == "arn:aws:iam::aws:policy/AWSCloudShellFullAccess" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_user_cloudshell_policy(self): + iam = client("iam") + user_name = "test_cloudshell_policy_user" + iam.create_user( + UserName=user_name, + ) + iam.attach_user_policy( + UserName=user_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"AWS CloudShellFullAccess policy attached to IAM Users: {user_name}." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == "arn:aws:iam::aws:policy/AWSCloudShellFullAccess" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_group_cloudshell_policy(self): + iam = client("iam") + group_name = "test_cloudshell_policy_group" + iam.create_group( + GroupName=group_name, + ) + iam.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"AWS CloudShellFullAccess policy attached to IAM Groups: {group_name}." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == "arn:aws:iam::aws:policy/AWSCloudShellFullAccess" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_user_role_group_cloudshell_policy(self): + iam = client("iam") + user_name = "test_cloudshell_policy_user" + iam.create_user( + UserName=user_name, + ) + iam.attach_user_policy( + UserName=user_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + group_name = "test_cloudshell_policy_group" + iam.create_group( + GroupName=group_name, + ) + iam.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + role_name = "test_cloudshell_policy_role" + role_policy = { + "Version": "2012-10-17", + } + iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(role_policy), + ) + iam.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached.iam_client", + new=IAM(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_policy_cloudshell_admin_not_attached.iam_policy_cloudshell_admin_not_attached import ( + iam_policy_cloudshell_admin_not_attached, + ) + + check = iam_policy_cloudshell_admin_not_attached() + result = check.execute() + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"AWS CloudShellFullAccess policy attached to IAM Users: {user_name}, Groups: {group_name}, Roles: {role_name}." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == "arn:aws:iam::aws:policy/AWSCloudShellFullAccess" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/iam/iam_service_test.py b/tests/providers/aws/services/iam/iam_service_test.py index 35d04c3bac7..615d25dec60 100644 --- a/tests/providers/aws/services/iam/iam_service_test.py +++ b/tests/providers/aws/services/iam/iam_service_test.py @@ -984,3 +984,42 @@ def test_get_user_temporary_credentials_usage(self): ) assert iam.user_temporary_credentials_usage[(username, user_arn)] + + @mock_aws(config={"iam": {"load_aws_managed_policies": True}}) + def test_list_entities_attached_to_cloudshell_policy(self): + iam_client = client("iam") + user_name = "test_cloudshell_policy_user" + iam_client.create_user( + UserName=user_name, + ) + iam_client.attach_user_policy( + UserName=user_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + group_name = "test_cloudshell_policy_group" + iam_client.create_group( + GroupName=group_name, + ) + iam_client.attach_group_policy( + GroupName=group_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + role_name = "test_cloudshell_policy_role" + role_policy = { + "Version": "2012-10-17", + } + iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(role_policy), + ) + iam_client.attach_role_policy( + RoleName=role_name, + PolicyArn="arn:aws:iam::aws:policy/AWSCloudShellFullAccess", + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam = IAM(aws_provider) + assert len(iam.entities_attached_to_cloudshell_policy) == 3 + assert iam.entities_attached_to_cloudshell_policy["Users"] == [user_name] + assert iam.entities_attached_to_cloudshell_policy["Groups"] == [group_name] + assert iam.entities_attached_to_cloudshell_policy["Roles"] == [role_name]