diff --git a/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/__init__.py b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.metadata.json b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.metadata.json new file mode 100644 index 00000000..ba0b057d --- /dev/null +++ b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "iam_custom_role_has_permissions_to_administer_resource_locks", + "CheckTitle": "Ensure an IAM custom role has permissions to administer resource locks", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "AzureRole", + "Description": "Ensure a Custom Role is Assigned Permissions for Administering Resource Locks", + "Risk": "In Azure, resource locks are a way to prevent accidental deletion or modification of critical resources. These locks can be set at the resource group level or the individual resource level. Resource locks administration is a critical task that should be preformed from a custom role with the appropriate permissions. This ensures that only authorized users can administer resource locks.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources?tabs=json", + "Remediation": { + "Code": { + "CLI": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/resource-lock-custom-role.html", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Resouce locks are needed to prevent accidental deletion or modification of critical Azure resources. The administration of resource locks should be performed from a custom role with the appropriate permissions.", + "Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/resource-lock-custom-role.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py new file mode 100644 index 00000000..a903d7c3 --- /dev/null +++ b/prowler/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks.py @@ -0,0 +1,33 @@ +from re import search + +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.iam.iam_client import iam_client + + +class iam_custom_role_has_permissions_to_administer_resource_locks(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + for subscription, roles in iam_client.custom_roles.items(): + for role in roles: + report = Check_Report_Azure(self.metadata()) + report.subscription = subscription + report.resource_id = role.id + report.resource_name = role.name + has_lock_permission = False + for permission_item in role.permissions: + if has_lock_permission: + break + for action in permission_item.actions: + if has_lock_permission: + break + if search("^Microsoft.Authorization/locks/.*", action): + report.status = "PASS" + report.status_extended = f"Role {role.name} from subscription {subscription} has permission to administer resource locks." + has_lock_permission = True + break + else: + report.status = "FAIL" + report.status_extended = f"Role {role.name} from subscription {subscription} has no permission to administer resource locks." + break + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/iam/iam_service.py b/prowler/providers/azure/services/iam/iam_service.py index b2822221..465f3647 100644 --- a/prowler/providers/azure/services/iam/iam_service.py +++ b/prowler/providers/azure/services/iam/iam_service.py @@ -11,33 +11,46 @@ from prowler.providers.azure.lib.service.service import AzureService class IAM(AzureService): def __init__(self, audit_info): super().__init__(AuthorizationManagementClient, audit_info) - self.roles = self.__get_roles__() + self.roles, self.custom_roles = self.__get_roles__() def __get_roles__(self): logger.info("IAM - Getting roles...") - roles = {} + builtin_roles = {} + custom_roles = {} for subscription, client in self.clients.items(): try: - roles.update({subscription: []}) - for role in client.role_definitions.list( + builtin_roles.update({subscription: []}) + custom_roles.update({subscription: []}) + all_roles = client.role_definitions.list( scope=f"/subscriptions/{self.subscriptions[subscription]}", - filter="type eq 'CustomRole'", - ): - roles[subscription].append( - Role( - id=role.id, - name=role.role_name, - type=role.role_type, - assignable_scopes=role.assignable_scopes, - permissions=role.permissions, + ) + for role in all_roles: + if role.role_type == "CustomRole": + custom_roles[subscription].append( + Role( + id=role.id, + name=role.role_name, + type=role.role_type, + assignable_scopes=role.assignable_scopes, + permissions=role.permissions, + ) + ) + else: + builtin_roles[subscription].append( + Role( + id=role.id, + name=role.role_name, + type=role.role_type, + assignable_scopes=role.assignable_scopes, + permissions=role.permissions, + ) ) - ) except Exception as error: logger.error(f"Subscription name: {subscription}") logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - return roles + return builtin_roles, custom_roles @dataclass diff --git a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.metadata.json b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.metadata.json index 0f2aa1d1..d1b25286 100644 --- a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.metadata.json +++ b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.metadata.json @@ -10,17 +10,17 @@ "ResourceType": "AzureRole", "Description": "Ensure that no custom subscription owner roles are created", "Risk": "Subscription ownership should not include permission to create custom owner roles. The principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access.", - "RelatedUrl": "", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/role-based-access-control/custom-roles", "Remediation": { "Code": { - "CLI": "", + "CLI": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html", "NativeIaC": "", "Other": "", "Terraform": "" }, "Recommendation": { - "Text": "Subscriptions will need to be handled by Administrators with permissions.", - "Url": "" + "Text": "Custom subscription owner roles should not be created. This is because the principle of least privilege should be followed and only necessary privileges should be assigned instead of allowing full administrative access", + "Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html" } }, "Categories": [], diff --git a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py index 07fae327..85bea3b4 100644 --- a/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py +++ b/prowler/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created.py @@ -7,7 +7,7 @@ from prowler.providers.azure.services.iam.iam_client import iam_client class iam_subscription_roles_owner_custom_not_created(Check): def execute(self) -> Check_Report_Azure: findings = [] - for subscription, roles in iam_client.roles.items(): + for subscription, roles in iam_client.custom_roles.items(): for role in roles: report = Check_Report_Azure(self.metadata()) report.subscription = subscription diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index 593f57da..e3b3e41b 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -96,6 +96,10 @@ def mock_recover_checks_from_azure_provider(*_): "iam_subscription_roles_owner_custom_not_created", "/root_dir/fake_path/iam/iam_subscription_roles_owner_custom_not_created", ), + ( + "iam_custom_role_has_permissions_to_administer_resource_locks", + "/root_dir/fake_path/iam/iam_custom_role_has_permissions_to_administer_resource_locks", + ), ( "storage_default_network_access_rule_is_denied", "/root_dir/fake_path/storage/storage_default_network_access_rule_is_denied", diff --git a/tests/providers/azure/azure_fixtures.py b/tests/providers/azure/azure_fixtures.py new file mode 100644 index 00000000..87dc97c8 --- /dev/null +++ b/tests/providers/azure/azure_fixtures.py @@ -0,0 +1,3 @@ +from uuid import uuid4 + +AZURE_SUSCRIPTION = str(uuid4()) diff --git a/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py b/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py new file mode 100644 index 00000000..3eb0f5b0 --- /dev/null +++ b/tests/providers/azure/services/iam/iam_custom_role_has_permissions_to_administer_resource_locks/iam_custom_role_has_permissions_to_administer_resource_locks_test.py @@ -0,0 +1,112 @@ +from unittest import mock +from uuid import uuid4 + +from azure.mgmt.authorization.v2022_04_01.models import Permission + +from prowler.providers.azure.services.iam.iam_service import Role +from tests.providers.azure.azure_fixtures import AZURE_SUSCRIPTION + + +class Test_iam_custom_role_has_permissions_to_administer_resource_locks: + def test_iam_no_roles(self): + defender_client = mock.MagicMock + defender_client.custom_roles = {} + + with mock.patch( + "prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks.iam_client", + new=defender_client, + ): + from prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks import ( + iam_custom_role_has_permissions_to_administer_resource_locks, + ) + + check = iam_custom_role_has_permissions_to_administer_resource_locks() + result = check.execute() + assert len(result) == 0 + + def test_iam_custom_owner_role_created_with_lock_administration_permissions( + self, + ): + defender_client = mock.MagicMock + role_name = "test-role" + defender_client.custom_roles = { + AZURE_SUSCRIPTION: [ + Role( + id=str(uuid4()), + name=role_name, + type="CustomRole", + assignable_scopes=["/.*", "/test"], + permissions=[ + Permission( + actions=[ + "Microsoft.Authorization/locks/*", + "microsoft.aadiam/azureADMetrics/read", + ] + ) + ], + ) + ] + } + + with mock.patch( + "prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks.iam_client", + new=defender_client, + ): + from prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks import ( + iam_custom_role_has_permissions_to_administer_resource_locks, + ) + + check = iam_custom_role_has_permissions_to_administer_resource_locks() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Role {role_name} from subscription {AZURE_SUSCRIPTION} has permission to administer resource locks." + ) + assert result[0].subscription == AZURE_SUSCRIPTION + assert ( + result[0].resource_id + == defender_client.custom_roles[AZURE_SUSCRIPTION][0].id + ) + assert result[0].resource_name == role_name + + def test_iam_custom_owner_role_created_with_no_lock_administration_permissions( + self, + ): + defender_client = mock.MagicMock + role_name = "test-role" + defender_client.custom_roles = { + AZURE_SUSCRIPTION: [ + Role( + id=str(uuid4()), + name=role_name, + type="CustomRole", + assignable_scopes=["/*"], + permissions=[Permission(actions=["*"])], + ) + ] + } + + with mock.patch( + "prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks.iam_client", + new=defender_client, + ): + from prowler.providers.azure.services.iam.iam_custom_role_has_permissions_to_administer_resource_locks.iam_custom_role_has_permissions_to_administer_resource_locks import ( + iam_custom_role_has_permissions_to_administer_resource_locks, + ) + + check = iam_custom_role_has_permissions_to_administer_resource_locks() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Role {role_name} from subscription {AZURE_SUSCRIPTION} has no permission to administer resource locks." + ) + assert result[0].subscription == AZURE_SUSCRIPTION + assert ( + result[0].resource_id + == defender_client.custom_roles[AZURE_SUSCRIPTION][0].id + ) + assert result[0].resource_name == role_name diff --git a/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py b/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py index 238fcb13..629bc5f8 100644 --- a/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py +++ b/tests/providers/azure/services/iam/iam_subscription_roles_owner_custom_not_created/iam_subscription_roles_owner_custom_not_created_test.py @@ -4,14 +4,13 @@ from uuid import uuid4 from azure.mgmt.authorization.v2022_04_01.models import Permission from prowler.providers.azure.services.iam.iam_service import Role - -AZURE_SUSCRIPTION = str(uuid4()) +from tests.providers.azure.azure_fixtures import AZURE_SUSCRIPTION -class Test_defender_ensure_defender_for_storage_is_on: +class Test_iam_subscription_roles_owner_custom_not_created: def test_iam_no_roles(self): defender_client = mock.MagicMock - defender_client.roles = {} + defender_client.custom_roles = {} with mock.patch( "prowler.providers.azure.services.iam.iam_subscription_roles_owner_custom_not_created.iam_subscription_roles_owner_custom_not_created.iam_client", @@ -28,12 +27,12 @@ class Test_defender_ensure_defender_for_storage_is_on: def test_iam_custom_owner_role_created_with_all(self): defender_client = mock.MagicMock role_name = "test-role" - defender_client.roles = { + defender_client.custom_roles = { AZURE_SUSCRIPTION: [ Role( id=str(uuid4()), name=role_name, - type="type-role", + type="CustomRole", assignable_scopes=["/*"], permissions=[Permission(actions="*")], ) @@ -56,11 +55,17 @@ class Test_defender_ensure_defender_for_storage_is_on: result[0].status_extended == f"Role {role_name} from subscription {AZURE_SUSCRIPTION} is a custom owner role." ) + assert result[0].subscription == AZURE_SUSCRIPTION + assert ( + result[0].resource_id + == defender_client.custom_roles[AZURE_SUSCRIPTION][0].id + ) + assert result[0].resource_name == role_name def test_iam_custom_owner_role_created_with_no_permissions(self): defender_client = mock.MagicMock role_name = "test-role" - defender_client.roles = { + defender_client.custom_roles = { AZURE_SUSCRIPTION: [ Role( id=str(uuid4()), @@ -88,3 +93,9 @@ class Test_defender_ensure_defender_for_storage_is_on: result[0].status_extended == f"Role {role_name} from subscription {AZURE_SUSCRIPTION} is not a custom owner role." ) + assert result[0].subscription == AZURE_SUSCRIPTION + assert ( + result[0].resource_id + == defender_client.custom_roles[AZURE_SUSCRIPTION][0].id + ) + assert result[0].resource_name == role_name