feat(azure): add new check iam_role_user_access_admin_restricted (#8040)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Rubén De la Torre Vico
2025-06-18 15:24:23 +02:00
committed by GitHub
parent a626e41162
commit b572575c8d
13 changed files with 348 additions and 86 deletions
@@ -41,9 +41,15 @@ class app_function_identity_without_admin_privileges(Check):
USER_ACCESS_ADMINISTRATOR_ROLE_ID,
]
):
for role in iam_client.roles[subscription_name]:
if role.id.split("/")[-1] == role_assignment.role_id:
admin_roles_assigned.append(role.name)
admin_roles_assigned.append(
getattr(
iam_client.roles[subscription_name].get(
f"/subscriptions/{iam_client.subscriptions[subscription_name]}/providers/Microsoft.Authorization/roleDefinitions/{role_assignment.role_id}"
),
"name",
"",
)
)
if admin_roles_assigned:
report.status = "FAIL"
@@ -17,7 +17,7 @@ class entra_user_with_vm_access_has_mfa(Check):
findings = []
for users in entra_client.users.values():
for user_domain_name, user in users.items():
for user in users.values():
for (
subscription_name,
role_assigns,
@@ -10,7 +10,7 @@ class iam_custom_role_has_permissions_to_administer_resource_locks(Check):
for subscription, roles in iam_client.custom_roles.items():
exits_role_with_permission_over_locks = False
for custom_role in roles:
for custom_role in roles.values():
if exits_role_with_permission_over_locks:
break
report = Check_Report_Azure(
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "iam_role_user_access_admin_restricted",
"CheckTitle": "Ensure 'User Access Administrator' role is restricted",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "AzureIAMRoleassignment",
"Description": "Checks for active assignments of the highly privileged 'User Access Administrator' role in Azure subscriptions.",
"Risk": "Persistent assignment of this role can lead to privilege escalation and unauthorized access, increasing the risk of security breaches.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/privileged#user-access-administrator",
"Remediation": {
"Code": {
"CLI": "az role assignment delete --role 'User Access Administrator' --scope '/subscriptions/<subscription_id>'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "Remove 'User Access Administrator' role assignments immediately after use to minimize security risks.",
"Url": "https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin?tabs=azure-portal%2Centra-audit-logs"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,29 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.iam.iam_client import iam_client
class iam_role_user_access_admin_restricted(Check):
def execute(self):
findings = []
for subscription_name, assignments in iam_client.role_assignments.items():
for assignment in assignments.values():
role_assignment_name = getattr(
iam_client.roles[subscription_name].get(
f"/subscriptions/{iam_client.subscriptions[subscription_name]}/providers/Microsoft.Authorization/roleDefinitions/{assignment.role_id}"
),
"name",
"",
)
report = Check_Report_Azure(
metadata=self.metadata(), resource=assignment
)
report.subscription = subscription_name
if role_assignment_name == "User Access Administrator":
report.status = "FAIL"
report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} grants User Access Administrator role to {getattr(assignment, 'agent_type', '')} {getattr(assignment, 'agent_id', '')}."
else:
report.status = "PASS"
report.status_extended = f"Role assignment {assignment.name} in subscription {subscription_name} does not grant User Access Administrator role."
findings.append(report)
return findings
@@ -20,45 +20,40 @@ class IAM(AzureService):
custom_roles = {}
for subscription, client in self.clients.items():
try:
builtin_roles.update({subscription: []})
custom_roles.update({subscription: []})
builtin_roles.update({subscription: {}})
custom_roles.update({subscription: {}})
all_roles = client.role_definitions.list(
scope=f"/subscriptions/{self.subscriptions[subscription]}",
)
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=[
Permission(
condition=getattr(permission, "condition", ""),
condition_version=getattr(
permission, "condition_version", ""
),
actions=getattr(permission, "actions", []),
)
for permission in getattr(role, "permissions", [])
],
)
custom_roles[subscription][role.id] = Role(
id=role.id,
name=role.role_name,
type=role.role_type,
assignable_scopes=role.assignable_scopes,
permissions=[
Permission(
condition=getattr(permission, "condition", ""),
condition_version=getattr(
permission, "condition_version", ""
),
actions=getattr(permission, "actions", []),
)
for permission in getattr(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,
)
builtin_roles[subscription][role.id] = 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}"
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return builtin_roles, custom_roles
@@ -75,6 +70,9 @@ class IAM(AzureService):
role_assignments[subscription].update(
{
role_assignment.id: RoleAssignment(
id=role_assignment.id,
name=role_assignment.name,
scope=role_assignment.scope,
agent_id=role_assignment.principal_id,
agent_type=role_assignment.principal_type,
role_id=role_assignment.role_definition_id.split("/")[
@@ -84,9 +82,8 @@ class IAM(AzureService):
}
)
except Exception as error:
logger.error(f"Subscription name: {subscription}")
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return role_assignments
@@ -109,6 +106,21 @@ class Role:
@dataclass
class RoleAssignment:
"""
Represents an Azure Role Assignment.
Attributes:
id: The unique identifier of the role assignment.
name: The name of the role assignment.
scope: The scope at which the role assignment applies.
agent_id: The principal (user, group, service principal, etc.) ID assigned the role.
agent_type: The type of the principal. Known values: "User", "Group", "ServicePrincipal", "ForeignGroup", and "Device".
role_id: The ID of the role definition assigned.
"""
id: str
name: str
scope: str
agent_id: str
agent_type: str
role_id: str
@@ -8,7 +8,7 @@ class iam_subscription_roles_owner_custom_not_created(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for subscription, roles in iam_client.custom_roles.items():
for custom_role in roles:
for custom_role in roles.values():
report = Check_Report_Azure(
metadata=self.metadata(), resource=custom_role
)
@@ -144,24 +144,29 @@ class Test_app_function_identity_without_admin_privileges:
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
"1": RoleAssignment(
role_id="1",
"role-assignment-id-1": RoleAssignment(
id="role-assignment-id-1",
name="role-assignment-name-1",
scope="/subscriptions/{}/resourceGroups/rg/providers/Microsoft.Web/sites/function1".format(
AZURE_SUBSCRIPTION_ID
),
agent_id="123",
agent_type="User",
role_id="role-id-1",
)
}
}
iam_client.roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id="1",
AZURE_SUBSCRIPTION_ID: {
"role-id-1": Role(
id="role-id-1",
name="role1",
type="User",
type="BuiltInRole",
assignable_scopes=[],
permissions=[],
)
]
}
}
check = app_function_identity_without_admin_privileges()
@@ -205,9 +210,9 @@ class Test_app_function_identity_without_admin_privileges:
)
function_id = str(uuid4())
function_scope = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Web/sites/function1"
app_client.functions = {
AZURE_SUBSCRIPTION_ID: {
"subscription-name-1": {
function_id: FunctionApp(
id=function_id,
name="function1",
@@ -223,26 +228,33 @@ class Test_app_function_identity_without_admin_privileges:
}
}
iam_client.subscriptions = {
"subscription-name-1": AZURE_SUBSCRIPTION_ID,
}
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
"1": RoleAssignment(
role_id=USER_ACCESS_ADMINISTRATOR_ROLE_ID,
"subscription-name-1": {
"role-assignment-id-2": RoleAssignment(
id="role-assignment-id-2",
name="role-assignment-name-2",
scope=function_scope,
agent_id="123",
agent_type="User",
role_id=USER_ACCESS_ADMINISTRATOR_ROLE_ID,
)
}
}
iam_client.roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=USER_ACCESS_ADMINISTRATOR_ROLE_ID,
"subscription-name-1": {
f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{USER_ACCESS_ADMINISTRATOR_ROLE_ID}": Role(
id=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{USER_ACCESS_ADMINISTRATOR_ROLE_ID}",
name="User Access Administrator",
type="User",
type="BuiltInRole",
assignable_scopes=[],
permissions=[],
)
]
),
}
}
check = app_function_identity_without_admin_privileges()
@@ -255,5 +267,5 @@ class Test_app_function_identity_without_admin_privileges:
)
assert result[0].resource_id == function_id
assert result[0].resource_name == "function1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].subscription == "subscription-name-1"
assert result[0].location == "West Europe"
@@ -75,6 +75,9 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa:
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
id=role_assigment_id,
name="test",
scope=AZURE_SUBSCRIPTION_ID,
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
@@ -149,6 +152,9 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa:
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
id=role_assigment_id,
name="test",
scope=AZURE_SUBSCRIPTION_ID,
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
@@ -216,6 +222,9 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa:
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
id=role_assigment_id,
name="test",
scope=AZURE_SUBSCRIPTION_ID,
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
@@ -269,6 +278,9 @@ class Test_iam_assignment_priviledge_access_vm_has_mfa:
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
id=role_assigment_id,
name="test",
scope=AZURE_SUBSCRIPTION_ID,
role_id=str(uuid4()),
agent_type="User",
agent_id=user_id,
@@ -1,5 +1,4 @@
from unittest import mock
from uuid import uuid4
from azure.mgmt.authorization.v2022_04_01.models import Permission
@@ -39,9 +38,9 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
defender_client = mock.MagicMock
role_name = "test-role"
defender_client.custom_roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=str(uuid4()),
AZURE_SUBSCRIPTION_ID: {
"test-role-id": Role(
id="test-role-id",
name=role_name,
type="CustomRole",
assignable_scopes=["/.*", "/test"],
@@ -54,7 +53,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
)
],
)
]
}
}
with (
@@ -82,7 +81,9 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert (
result[0].resource_id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][0].id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][
"test-role-id"
].id
)
assert result[0].resource_name == role_name
@@ -92,15 +93,15 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
defender_client = mock.MagicMock
role_name = "test-role"
defender_client.custom_roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=str(uuid4()),
AZURE_SUBSCRIPTION_ID: {
"test-role-id": Role(
id="test-role-id",
name=role_name,
type="CustomRole",
assignable_scopes=["/*"],
permissions=[Permission(actions=["*"])],
)
]
}
}
with (
@@ -128,7 +129,9 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert (
result[0].resource_id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][0].id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][
"test-role-id"
].id
)
assert result[0].resource_name == role_name
@@ -139,9 +142,9 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
role_name = "test-role"
role_name2 = "test-role2"
defender_client.custom_roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=str(uuid4()),
AZURE_SUBSCRIPTION_ID: {
"test-role-id": Role(
id="test-role-id",
name=role_name,
type="CustomRole",
assignable_scopes=["/.*", "/test"],
@@ -154,8 +157,8 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
)
],
),
Role(
id=str(uuid4()),
"test-role-id2": Role(
id="test-role-id2",
name=role_name2,
type="CustomRole",
assignable_scopes=["/.*", "/test"],
@@ -168,7 +171,7 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
)
],
),
]
}
}
with (
@@ -196,12 +199,14 @@ class Test_iam_custom_role_has_permissions_to_administer_resource_locks:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert (
result[0].resource_id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][0].id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][
"test-role-id"
].id
)
def test_iam_custom_roles_empty_list_but_with_key(self):
defender_client = mock.MagicMock
defender_client.custom_roles = {AZURE_SUBSCRIPTION_ID: []}
defender_client.custom_roles = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
@@ -0,0 +1,153 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.services.iam.iam_service import Role, RoleAssignment
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_iam_role_user_access_admin_restricted:
def test_iam_no_role_assignments(self):
iam_client = mock.MagicMock
iam_client.role_assignments = {}
iam_client.roles = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted.iam_client",
new=iam_client,
),
):
from prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted import (
iam_role_user_access_admin_restricted,
)
check = iam_role_user_access_admin_restricted()
result = check.execute()
assert len(result) == 0
def test_iam_user_access_administrator_role_assigned(self):
iam_client = mock.MagicMock
role_id = str(uuid4())
role_assignment_id = str(uuid4())
agent_id = str(uuid4())
role_name = "User Access Administrator"
iam_client.subscriptions = {
"subscription-name-1": AZURE_SUBSCRIPTION_ID,
}
iam_client.role_assignments = {
"subscription-name-1": {
role_assignment_id: RoleAssignment(
id=role_assignment_id,
name="test-assignment",
scope=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}",
agent_id=agent_id,
agent_type="User",
role_id=role_id,
)
}
}
iam_client.roles = {
"subscription-name-1": {
f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{role_id}": Role(
id=role_id,
name=role_name,
type="BuiltInRole",
assignable_scopes=[f"/subscriptions/{AZURE_SUBSCRIPTION_ID}"],
permissions=[],
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted.iam_client",
new=iam_client,
),
):
from prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted import (
iam_role_user_access_admin_restricted,
)
check = iam_role_user_access_admin_restricted()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Role assignment test-assignment in subscription subscription-name-1 grants User Access Administrator role to User {agent_id}."
)
assert result[0].subscription == "subscription-name-1"
assert result[0].resource_id == role_assignment_id
def test_iam_non_user_access_administrator_role_assigned(self):
iam_client = mock.MagicMock
role_id = str(uuid4())
role_assignment_id = str(uuid4())
agent_id = str(uuid4())
role_name = "Reader"
iam_client.subscriptions = {
"subscription-name-1": AZURE_SUBSCRIPTION_ID,
}
iam_client.role_assignments = {
"subscription-name-1": {
role_assignment_id: RoleAssignment(
id=role_assignment_id,
name="test-assignment",
scope=f"/subscriptions/{AZURE_SUBSCRIPTION_ID}",
agent_id=agent_id,
agent_type="User",
role_id=role_id,
)
}
}
iam_client.roles = {
"subscription-name-1": {
f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Authorization/roleDefinitions/{role_id}": Role(
id=role_id,
name=role_name,
type="BuiltInRole",
assignable_scopes=[f"/subscriptions/{AZURE_SUBSCRIPTION_ID}"],
permissions=[],
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted.iam_client",
new=iam_client,
),
):
from prowler.providers.azure.services.iam.iam_role_user_access_admin_restricted.iam_role_user_access_admin_restricted import (
iam_role_user_access_admin_restricted,
)
check = iam_role_user_access_admin_restricted()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Role assignment test-assignment in subscription subscription-name-1 does not grant User Access Administrator role."
)
assert result[0].subscription == "subscription-name-1"
assert result[0].resource_id == role_assignment_id
@@ -1,5 +1,4 @@
from unittest import mock
from uuid import uuid4
from azure.mgmt.authorization.v2022_04_01.models import Permission
@@ -37,15 +36,15 @@ class Test_iam_subscription_roles_owner_custom_not_created:
defender_client = mock.MagicMock
role_name = "test-role"
defender_client.custom_roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=str(uuid4()),
AZURE_SUBSCRIPTION_ID: {
"test-role-id": Role(
id="test-role-id",
name=role_name,
type="CustomRole",
assignable_scopes=["/*"],
permissions=[Permission(actions="*")],
)
]
}
}
with (
@@ -73,7 +72,9 @@ class Test_iam_subscription_roles_owner_custom_not_created:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert (
result[0].resource_id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][0].id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][
"test-role-id"
].id
)
assert result[0].resource_name == role_name
@@ -81,15 +82,15 @@ class Test_iam_subscription_roles_owner_custom_not_created:
defender_client = mock.MagicMock
role_name = "test-role"
defender_client.custom_roles = {
AZURE_SUBSCRIPTION_ID: [
Role(
id=str(uuid4()),
AZURE_SUBSCRIPTION_ID: {
"test-role-id": Role(
id="test-role-id",
name=role_name,
type="type-role",
assignable_scopes=[""],
permissions=[Permission()],
)
]
}
}
with (
@@ -117,6 +118,8 @@ class Test_iam_subscription_roles_owner_custom_not_created:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert (
result[0].resource_id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][0].id
== defender_client.custom_roles[AZURE_SUBSCRIPTION_ID][
"test-role-id"
].id
)
assert result[0].resource_name == role_name