feat(azure): Add new check "iam_custom_role_permits_administering_resource_locks" (#3317)

Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Pedro Martín
2024-01-25 14:29:29 +01:00
committed by GitHub
parent dff3e72e7d
commit 8a6ae68b9a
10 changed files with 233 additions and 27 deletions

View File

@@ -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": ""
}

View File

@@ -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

View File

@@ -11,33 +11,46 @@ from prowler.providers.azure.lib.service.service import AzureService
class IAM(AzureService): class IAM(AzureService):
def __init__(self, audit_info): def __init__(self, audit_info):
super().__init__(AuthorizationManagementClient, audit_info) super().__init__(AuthorizationManagementClient, audit_info)
self.roles = self.__get_roles__() self.roles, self.custom_roles = self.__get_roles__()
def __get_roles__(self): def __get_roles__(self):
logger.info("IAM - Getting roles...") logger.info("IAM - Getting roles...")
roles = {} builtin_roles = {}
custom_roles = {}
for subscription, client in self.clients.items(): for subscription, client in self.clients.items():
try: try:
roles.update({subscription: []}) builtin_roles.update({subscription: []})
for role in client.role_definitions.list( custom_roles.update({subscription: []})
all_roles = client.role_definitions.list(
scope=f"/subscriptions/{self.subscriptions[subscription]}", scope=f"/subscriptions/{self.subscriptions[subscription]}",
filter="type eq 'CustomRole'", )
): for role in all_roles:
roles[subscription].append( if role.role_type == "CustomRole":
Role( custom_roles[subscription].append(
id=role.id, Role(
name=role.role_name, id=role.id,
type=role.role_type, name=role.role_name,
assignable_scopes=role.assignable_scopes, type=role.role_type,
permissions=role.permissions, 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: except Exception as error:
logger.error(f"Subscription name: {subscription}") logger.error(f"Subscription name: {subscription}")
logger.error( logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
) )
return roles return builtin_roles, custom_roles
@dataclass @dataclass

View File

@@ -10,17 +10,17 @@
"ResourceType": "AzureRole", "ResourceType": "AzureRole",
"Description": "Ensure that no custom subscription owner roles are created", "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.", "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": { "Remediation": {
"Code": { "Code": {
"CLI": "", "CLI": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html",
"NativeIaC": "", "NativeIaC": "",
"Other": "", "Other": "",
"Terraform": "" "Terraform": ""
}, },
"Recommendation": { "Recommendation": {
"Text": "Subscriptions will need to be handled by Administrators with permissions.", "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": "" "Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/AccessControl/remove-custom-owner-roles.html"
} }
}, },
"Categories": [], "Categories": [],

View File

@@ -7,7 +7,7 @@ from prowler.providers.azure.services.iam.iam_client import iam_client
class iam_subscription_roles_owner_custom_not_created(Check): class iam_subscription_roles_owner_custom_not_created(Check):
def execute(self) -> Check_Report_Azure: def execute(self) -> Check_Report_Azure:
findings = [] findings = []
for subscription, roles in iam_client.roles.items(): for subscription, roles in iam_client.custom_roles.items():
for role in roles: for role in roles:
report = Check_Report_Azure(self.metadata()) report = Check_Report_Azure(self.metadata())
report.subscription = subscription report.subscription = subscription

View File

@@ -96,6 +96,10 @@ def mock_recover_checks_from_azure_provider(*_):
"iam_subscription_roles_owner_custom_not_created", "iam_subscription_roles_owner_custom_not_created",
"/root_dir/fake_path/iam/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", "storage_default_network_access_rule_is_denied",
"/root_dir/fake_path/storage/storage_default_network_access_rule_is_denied", "/root_dir/fake_path/storage/storage_default_network_access_rule_is_denied",

View File

@@ -0,0 +1,3 @@
from uuid import uuid4
AZURE_SUSCRIPTION = str(uuid4())

View File

@@ -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

View File

@@ -4,14 +4,13 @@ from uuid import uuid4
from azure.mgmt.authorization.v2022_04_01.models import Permission from azure.mgmt.authorization.v2022_04_01.models import Permission
from prowler.providers.azure.services.iam.iam_service import Role from prowler.providers.azure.services.iam.iam_service import Role
from tests.providers.azure.azure_fixtures import AZURE_SUSCRIPTION
AZURE_SUSCRIPTION = str(uuid4())
class Test_defender_ensure_defender_for_storage_is_on: class Test_iam_subscription_roles_owner_custom_not_created:
def test_iam_no_roles(self): def test_iam_no_roles(self):
defender_client = mock.MagicMock defender_client = mock.MagicMock
defender_client.roles = {} defender_client.custom_roles = {}
with mock.patch( with mock.patch(
"prowler.providers.azure.services.iam.iam_subscription_roles_owner_custom_not_created.iam_subscription_roles_owner_custom_not_created.iam_client", "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): def test_iam_custom_owner_role_created_with_all(self):
defender_client = mock.MagicMock defender_client = mock.MagicMock
role_name = "test-role" role_name = "test-role"
defender_client.roles = { defender_client.custom_roles = {
AZURE_SUSCRIPTION: [ AZURE_SUSCRIPTION: [
Role( Role(
id=str(uuid4()), id=str(uuid4()),
name=role_name, name=role_name,
type="type-role", type="CustomRole",
assignable_scopes=["/*"], assignable_scopes=["/*"],
permissions=[Permission(actions="*")], permissions=[Permission(actions="*")],
) )
@@ -56,11 +55,17 @@ class Test_defender_ensure_defender_for_storage_is_on:
result[0].status_extended result[0].status_extended
== f"Role {role_name} from subscription {AZURE_SUSCRIPTION} is a custom owner role." == 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): def test_iam_custom_owner_role_created_with_no_permissions(self):
defender_client = mock.MagicMock defender_client = mock.MagicMock
role_name = "test-role" role_name = "test-role"
defender_client.roles = { defender_client.custom_roles = {
AZURE_SUSCRIPTION: [ AZURE_SUSCRIPTION: [
Role( Role(
id=str(uuid4()), id=str(uuid4()),
@@ -88,3 +93,9 @@ class Test_defender_ensure_defender_for_storage_is_on:
result[0].status_extended result[0].status_extended
== f"Role {role_name} from subscription {AZURE_SUSCRIPTION} is not a custom owner role." == 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