mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(m365): add entra_app_registration_no_unused_privileged_permissions security check (#10080)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
23e51158e0
commit
20b26bc7d0
@@ -45,6 +45,7 @@ When using service principal authentication, add these **Application Permissions
|
||||
- `SecurityIdentitiesHealth.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
|
||||
- `SecurityIdentitiesSensors.Read.All`: Required for `defenderidentity_health_issues_no_open` check.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `ThreatHunting.Read.All`: Required for Entra checks that use Defender XDR Advanced Hunting (e.g., unused privileged permissions detection). Also requires App Governance to be enabled in Microsoft Defender for Cloud Apps.
|
||||
|
||||
**External API Permissions:**
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `entra_app_registration_no_unused_privileged_permissions` check for m365 provider [(#10080)](https://github.com/prowler-cloud/prowler/pull/10080)
|
||||
- `defenderidentity_health_issues_no_open` check for M365 provider [(#10087)](https://github.com/prowler-cloud/prowler/pull/10087)
|
||||
- `organization_verified_badge` check for GitHub provider [(#10033)](https://github.com/prowler-cloud/prowler/pull/10033)
|
||||
- OpenStack provider `clouds_yaml_content` parameter for API integration [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
|
||||
|
||||
@@ -983,6 +983,7 @@
|
||||
"Id": "5.1.5.1",
|
||||
"Description": "Control when end users and group owners are allowed to grant consent to applications, and when they will be required to request administrator review and approval. Allowing users to grant apps access to data helps them acquire useful applications and be productive but can represent a risk in some situations if it's not monitored and controlled carefully.",
|
||||
"Checks": [
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_restricts_user_consent_for_apps"
|
||||
],
|
||||
"Attributes": [
|
||||
|
||||
@@ -1215,6 +1215,7 @@
|
||||
"Id": "5.1.5.1",
|
||||
"Description": "User consent to apps accessing company data on their behalf allows users to grant permissions to applications without administrator involvement. The recommended state is Do not allow user consent.",
|
||||
"Checks": [
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_restricts_user_consent_for_apps"
|
||||
],
|
||||
"Attributes": [
|
||||
|
||||
@@ -256,11 +256,12 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"sharepoint_external_sharing_restricted",
|
||||
"sharepoint_external_sharing_managed",
|
||||
"sharepoint_guest_sharing_restricted",
|
||||
"entra_admin_portals_access_restriction",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_admin_portals_access_restriction"
|
||||
"sharepoint_external_sharing_managed",
|
||||
"sharepoint_external_sharing_restricted",
|
||||
"sharepoint_guest_sharing_restricted"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -636,6 +637,7 @@
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_ensure_default_user_cannot_create_tenants",
|
||||
"entra_policy_guest_invite_only_for_admin_roles"
|
||||
]
|
||||
@@ -767,8 +769,9 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_thirdparty_integrated_apps_not_allowed",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_thirdparty_integrated_apps_not_allowed",
|
||||
"teams_external_domains_restricted",
|
||||
"teams_external_users_cannot_start_conversations"
|
||||
]
|
||||
@@ -859,9 +862,10 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"admincenter_users_admins_reduced_license_footprint",
|
||||
"defender_malware_policy_comprehensive_attachments_filter_applied",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_thirdparty_integrated_apps_not_allowed",
|
||||
"sharepoint_modern_authentication_required"
|
||||
]
|
||||
|
||||
@@ -712,6 +712,7 @@
|
||||
"Id": "1.3.3",
|
||||
"Description": "Ensure third party integrated applications are not allowed",
|
||||
"Checks": [
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_thirdparty_integrated_apps_not_allowed"
|
||||
],
|
||||
"Attributes": [
|
||||
@@ -748,6 +749,7 @@
|
||||
"Id": "1.3.5",
|
||||
"Description": "Ensure user consent to apps accessing company data on their behalf is not allowed",
|
||||
"Checks": [
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_policy_restricts_user_consent_for_apps"
|
||||
],
|
||||
"Attributes": [
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_app_registration_no_unused_privileged_permissions",
|
||||
"CheckTitle": "App registration has no unused privileged API permissions",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "App Registration",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "OAuth app registrations with privileged API permissions (High privilege level) that are not being actively used. Usage status is determined by Microsoft Defender for Cloud Apps App Governance.",
|
||||
"Risk": "Unused privileged permissions expand the attack surface. If a compromised app has dormant privileged permissions, attackers can exploit them for **privilege escalation**, **unauthorized access** to sensitive data, or **lateral movement** within the environment.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/defender-cloud-apps/app-governance-visibility-insights-overview",
|
||||
"https://learn.microsoft.com/en-us/defender-xdr/advanced-hunting-oauthappinfo-table"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to Microsoft Defender XDR portal (https://security.microsoft.com)\n2. Go to Cloud apps > App governance > Overview\n3. Review the Applications inventory for apps with unused permissions\n4. For each flagged app, view details and navigate to the Permissions tab\n5. Remove unnecessary permissions via Microsoft Entra admin center",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Apply the **principle of least privilege** by regularly reviewing and revoking unused privileged permissions from app registrations. Use Microsoft Defender for Cloud Apps App Governance to monitor permission usage.",
|
||||
"Url": "https://hub.prowler.com/check/entra_app_registration_no_unused_privileged_permissions"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This check requires Microsoft Defender for Cloud Apps with App Governance enabled and ThreatHunting.Read.All permission. If App Governance data is unavailable, the check fails due to missing visibility."
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_app_registration_no_unused_privileged_permissions(Check):
|
||||
"""
|
||||
Ensure that app registrations do not have unused privileged API permissions.
|
||||
|
||||
This check evaluates OAuth applications registered in Microsoft Entra ID to identify
|
||||
those with privileged API permissions (High privilege level or Control/Management Plane
|
||||
classifications) that are assigned but not actively being used.
|
||||
|
||||
The check uses data from Microsoft Defender for Cloud Apps App Governance via
|
||||
the OAuthAppInfo table in Defender XDR Advanced Hunting.
|
||||
|
||||
- PASS: The app has no unused privileged permissions.
|
||||
- FAIL: The app has one or more unused privileged permissions that should be revoked.
|
||||
It also fails when OAuth App Governance data is not available.
|
||||
"""
|
||||
|
||||
# InUse field values from OAuthAppInfo:
|
||||
# - "true" / "1" / "True" = permission is actively used
|
||||
# - "false" / "0" / "False" = permission is NOT used (this triggers FAIL)
|
||||
# - "Not supported" = Microsoft cannot determine usage
|
||||
# - "" (empty) = No tracking data available
|
||||
# Note: Microsoft is changing from numeric (1/0) to textual (True/False) on Feb 25, 2026
|
||||
_UNUSED_STATUSES = {"false", "0", "notinuse", "not in use"}
|
||||
_PRIVILEGED_PLANE_LABELS = ("control plane", "management plane")
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""
|
||||
Execute the unused privileged permissions check for app registrations.
|
||||
|
||||
Iterates over OAuth applications retrieved from the Entra client and generates
|
||||
reports indicating whether each app has unused privileged permissions.
|
||||
|
||||
Returns:
|
||||
list[CheckReportM365]: A list of reports with the result of the check for each app.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# If OAuth app data is None, the API call failed (missing permissions or App Governance not enabled)
|
||||
if entra_client.oauth_apps is None:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="OAuth Applications",
|
||||
resource_id="oauthApps",
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"OAuth App Governance data is unavailable. "
|
||||
"Enable App Governance in Microsoft Defender for Cloud Apps and "
|
||||
"grant ThreatHunting.Read.All to evaluate unused privileged permissions."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
# If OAuth apps is empty dict, no apps are registered - this is compliant
|
||||
if not entra_client.oauth_apps:
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource={},
|
||||
resource_name="OAuth Applications",
|
||||
resource_id="oauthApps",
|
||||
)
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
"No OAuth applications are registered in the tenant."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
|
||||
# Check each OAuth app for unused privileged permissions
|
||||
for app_id, app in entra_client.oauth_apps.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=app,
|
||||
resource_name=app.name,
|
||||
resource_id=app_id,
|
||||
)
|
||||
|
||||
# Find unused privileged permissions
|
||||
# A permission is considered privileged if it has:
|
||||
# - PrivilegeLevel == "High"
|
||||
# Or if it's part of Control Plane / Management Plane (typically High privilege)
|
||||
unused_privileged_permissions = []
|
||||
|
||||
for permission in app.permissions:
|
||||
# Check if the permission is privileged
|
||||
is_privileged = self._is_privileged_permission(permission)
|
||||
|
||||
# Check if the permission is unused
|
||||
normalized_usage = self._normalize(permission.usage_status)
|
||||
is_unused = normalized_usage in self._UNUSED_STATUSES
|
||||
|
||||
if is_privileged and is_unused:
|
||||
unused_privileged_permissions.append(permission.name)
|
||||
|
||||
if unused_privileged_permissions:
|
||||
# The app has unused privileged permissions
|
||||
report.status = "FAIL"
|
||||
# Truncate list to first 5 permissions for readability
|
||||
total_count = len(unused_privileged_permissions)
|
||||
if total_count > 5:
|
||||
displayed = unused_privileged_permissions[:5]
|
||||
permissions_list = ", ".join(displayed)
|
||||
remaining = total_count - 5
|
||||
permissions_list += f" (and {remaining} more)"
|
||||
else:
|
||||
permissions_list = ", ".join(unused_privileged_permissions)
|
||||
report.status_extended = (
|
||||
f"App registration {app.name} has {total_count} "
|
||||
f"unused privileged permission(s): {permissions_list}."
|
||||
)
|
||||
else:
|
||||
# The app has no unused privileged permissions
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"App registration {app.name} has no unused privileged permissions."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
|
||||
@classmethod
|
||||
def _is_privileged_permission(cls, permission) -> bool:
|
||||
privilege_level = cls._normalize(permission.privilege_level)
|
||||
permission_type = cls._normalize(permission.permission_type)
|
||||
classification = cls._normalize(getattr(permission, "classification", ""))
|
||||
|
||||
if privilege_level == "high":
|
||||
return True
|
||||
|
||||
return any(
|
||||
label in permission_type or label in classification
|
||||
for label in cls._PRIVILEGED_PLANE_LABELS
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize(value: str) -> str:
|
||||
return (
|
||||
value.lower().replace("_", " ").replace("-", " ").strip() if value else ""
|
||||
)
|
||||
@@ -1,9 +1,13 @@
|
||||
import asyncio
|
||||
import json
|
||||
from asyncio import gather
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import (
|
||||
RunHuntingQueryPostRequestBody,
|
||||
)
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
@@ -12,7 +16,33 @@ from prowler.providers.m365.m365_provider import M365Provider
|
||||
|
||||
|
||||
class Entra(M365Service):
|
||||
"""
|
||||
Microsoft Entra ID service class.
|
||||
|
||||
This class provides methods to retrieve and manage Microsoft Entra ID
|
||||
security policies and configurations, including authorization policies,
|
||||
conditional access policies, admin consent policies, groups, organizations,
|
||||
users, and OAuth application data from Defender XDR.
|
||||
|
||||
Attributes:
|
||||
tenant_domain (str): The tenant domain.
|
||||
authorization_policy (AuthorizationPolicy): The authorization policy.
|
||||
conditional_access_policies (dict): Dictionary of conditional access policies.
|
||||
admin_consent_policy (AdminConsentPolicy): The admin consent policy.
|
||||
groups (list): List of groups.
|
||||
organizations (list): List of organizations.
|
||||
users (dict): Dictionary of users.
|
||||
user_accounts_status (dict): Dictionary of user account statuses.
|
||||
oauth_apps (dict): Dictionary of OAuth applications from Defender XDR.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: M365Provider):
|
||||
"""
|
||||
Initialize the Entra service client.
|
||||
|
||||
Args:
|
||||
provider: The M365Provider instance for authentication and configuration.
|
||||
"""
|
||||
super().__init__(provider)
|
||||
|
||||
if self.powershell:
|
||||
@@ -47,6 +77,7 @@ class Entra(M365Service):
|
||||
self._get_groups(),
|
||||
self._get_organization(),
|
||||
self._get_users(),
|
||||
self._get_oauth_apps(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -56,6 +87,7 @@ class Entra(M365Service):
|
||||
self.groups = attributes[3]
|
||||
self.organizations = attributes[4]
|
||||
self.users = attributes[5]
|
||||
self.oauth_apps: Optional[Dict[str, OAuthApp]] = attributes[6]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
@@ -461,6 +493,122 @@ class Entra(M365Service):
|
||||
|
||||
return registration_details
|
||||
|
||||
async def _get_oauth_apps(self) -> Optional[Dict[str, "OAuthApp"]]:
|
||||
"""
|
||||
Retrieve OAuth applications from Defender XDR using Advanced Hunting.
|
||||
|
||||
This method queries the OAuthAppInfo table to get information about
|
||||
OAuth applications registered in the tenant, including their permissions
|
||||
and usage status.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, OAuthApp]]: Dictionary of OAuth applications keyed by app ID,
|
||||
or None if the API call failed (missing permissions or App Governance not enabled).
|
||||
"""
|
||||
logger.info("Entra - Getting OAuth apps from Defender XDR...")
|
||||
oauth_apps: Optional[Dict[str, OAuthApp]] = {}
|
||||
try:
|
||||
# Query the OAuthAppInfo table using Advanced Hunting
|
||||
# The query gets apps with their permissions including usage status
|
||||
query = """
|
||||
OAuthAppInfo
|
||||
| project OAuthAppId, AppName, AppStatus, PrivilegeLevel, Permissions,
|
||||
ServicePrincipalId, IsAdminConsented, LastUsedTime, AppOrigin
|
||||
"""
|
||||
request_body = RunHuntingQueryPostRequestBody(query=query)
|
||||
|
||||
result = await self.client.security.microsoft_graph_security_run_hunting_query.post(
|
||||
request_body
|
||||
)
|
||||
|
||||
if result and result.results:
|
||||
for row in result.results:
|
||||
row_data = row.additional_data
|
||||
raw_app_id = row_data.get("OAuthAppId", "")
|
||||
# Convert to string in case API returns non-string type
|
||||
app_id = str(raw_app_id) if raw_app_id else ""
|
||||
if not app_id:
|
||||
continue
|
||||
|
||||
# Parse the permissions array
|
||||
# Permissions can be a list of JSON strings or a list of dicts
|
||||
permissions = []
|
||||
raw_permissions = row_data.get("Permissions", [])
|
||||
if raw_permissions:
|
||||
for perm in raw_permissions:
|
||||
# Parse JSON string if needed
|
||||
if isinstance(perm, str):
|
||||
try:
|
||||
perm = json.loads(perm)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(perm, dict):
|
||||
permissions.append(
|
||||
OAuthAppPermission(
|
||||
name=str(perm.get("PermissionValue", "")),
|
||||
target_app_id=str(perm.get("TargetAppId", "")),
|
||||
target_app_name=str(
|
||||
perm.get("TargetAppDisplayName", "")
|
||||
),
|
||||
permission_type=str(
|
||||
perm.get("PermissionType", "")
|
||||
),
|
||||
classification=str(
|
||||
perm.get(
|
||||
"Classification",
|
||||
perm.get(
|
||||
"PermissionClassification", ""
|
||||
),
|
||||
)
|
||||
),
|
||||
privilege_level=str(
|
||||
perm.get("PrivilegeLevel", "")
|
||||
),
|
||||
usage_status=str(perm.get("InUse", "")),
|
||||
)
|
||||
)
|
||||
|
||||
# Convert values to strings to handle API returning non-string types
|
||||
raw_service_principal_id = row_data.get("ServicePrincipalId", "")
|
||||
service_principal_id = (
|
||||
str(raw_service_principal_id)
|
||||
if raw_service_principal_id
|
||||
else ""
|
||||
)
|
||||
|
||||
raw_last_used_time = row_data.get("LastUsedTime")
|
||||
last_used_time = (
|
||||
str(raw_last_used_time)
|
||||
if raw_last_used_time is not None
|
||||
else None
|
||||
)
|
||||
|
||||
oauth_apps[app_id] = OAuthApp(
|
||||
id=app_id,
|
||||
name=str(row_data.get("AppName", "")),
|
||||
status=str(row_data.get("AppStatus", "")),
|
||||
privilege_level=str(row_data.get("PrivilegeLevel", "")),
|
||||
permissions=permissions,
|
||||
service_principal_id=service_principal_id,
|
||||
is_admin_consented=bool(
|
||||
row_data.get("IsAdminConsented", False)
|
||||
),
|
||||
last_used_time=last_used_time,
|
||||
app_origin=str(row_data.get("AppOrigin", "")),
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
# Log the error and return None to indicate API failure
|
||||
# This API requires ThreatHunting.Read.All permission and App Governance to be enabled
|
||||
logger.warning(
|
||||
f"Entra - Could not retrieve OAuth apps from Defender XDR. "
|
||||
f"This requires ThreatHunting.Read.All permission and App Governance enabled. "
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
return oauth_apps
|
||||
|
||||
|
||||
class ConditionalAccessPolicyState(Enum):
|
||||
ENABLED = "enabled"
|
||||
@@ -651,3 +799,53 @@ class AuthPolicyRoles(Enum):
|
||||
USER = UUID("a0b1b346-4d3e-4e8b-98f8-753987be4970")
|
||||
GUEST_USER = UUID("10dae51f-b6af-4016-8d66-8c2a99b929b3")
|
||||
GUEST_USER_ACCESS_RESTRICTED = UUID("2af84b1e-32c8-42b7-82bc-daa82404023b")
|
||||
|
||||
|
||||
class OAuthAppPermission(BaseModel):
|
||||
"""
|
||||
Model for OAuth application permission.
|
||||
|
||||
Attributes:
|
||||
name: The permission name.
|
||||
target_app_id: The target application ID that provides this permission.
|
||||
target_app_name: The target application display name.
|
||||
permission_type: The type of permission (Application or Delegated).
|
||||
classification: Optional plane classification (e.g. Control Plane, Management Plane).
|
||||
privilege_level: The privilege level (High, Medium, Low).
|
||||
usage_status: The usage status (InUse or NotInUse).
|
||||
"""
|
||||
|
||||
name: str
|
||||
target_app_id: str = ""
|
||||
target_app_name: str = ""
|
||||
permission_type: str = ""
|
||||
classification: str = ""
|
||||
privilege_level: str = ""
|
||||
usage_status: str = ""
|
||||
|
||||
|
||||
class OAuthApp(BaseModel):
|
||||
"""
|
||||
Model for OAuth application from Defender XDR.
|
||||
|
||||
Attributes:
|
||||
id: The application ID.
|
||||
name: The application display name.
|
||||
status: The application status (Enabled, Disabled, etc.).
|
||||
privilege_level: The overall privilege level of the app.
|
||||
permissions: List of permissions assigned to the app.
|
||||
service_principal_id: The service principal ID.
|
||||
is_admin_consented: Whether the app has admin consent.
|
||||
last_used_time: When the app was last used.
|
||||
app_origin: Whether the app is internal or external.
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
status: str = ""
|
||||
privilege_level: str = ""
|
||||
permissions: List[OAuthAppPermission] = []
|
||||
service_principal_id: str = ""
|
||||
is_admin_consented: bool = False
|
||||
last_used_time: Optional[str] = None
|
||||
app_origin: str = ""
|
||||
|
||||
@@ -0,0 +1,895 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
OAuthApp,
|
||||
OAuthAppPermission,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_app_registration_no_unused_privileged_permissions:
|
||||
def test_no_oauth_apps(self):
|
||||
"""No OAuth apps registered in tenant (empty dict): expected PASS."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "No OAuth applications are registered in the tenant."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "OAuth Applications"
|
||||
assert result[0].resource_id == "oauthApps"
|
||||
|
||||
def test_no_oauth_apps_none(self):
|
||||
"""OAuth apps is None (App Governance not enabled): expected FAIL."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = None
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "OAuth App Governance data is unavailable. Enable App Governance in Microsoft Defender for Cloud Apps and grant ThreatHunting.Read.All to evaluate unused privileged permissions."
|
||||
)
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "OAuth Applications"
|
||||
assert result[0].resource_id == "oauthApps"
|
||||
|
||||
def test_app_no_permissions(self):
|
||||
"""App with no permissions: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App No Permissions"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="Low",
|
||||
permissions=[],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=False,
|
||||
last_used_time=None,
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has no unused privileged permissions."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_all_permissions_in_use(self):
|
||||
"""App with all privileged permissions in use: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App All In Use"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="InUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.Read.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="InUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has no unused privileged permissions."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_low_privilege_unused(self):
|
||||
"""App with unused low privilege permissions (not high): expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Low Privilege Unused"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="Low",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="User.Read",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Low",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="openid",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Low",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=False,
|
||||
last_used_time=None,
|
||||
app_origin="External",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has no unused privileged permissions."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_medium_privilege_unused(self):
|
||||
"""App with unused medium privilege permissions (not high): expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Medium Privilege Unused"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="Medium",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Files.Read",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Medium",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=False,
|
||||
last_used_time=None,
|
||||
app_origin="External",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has no unused privileged permissions."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_one_unused_high_privilege_permission(self):
|
||||
"""App with one unused high privilege permission: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App One Unused High"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.Read",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Low",
|
||||
usage_status="InUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 1 unused privileged permission(s): Mail.ReadWrite.All."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_multiple_unused_high_privilege_permissions(self):
|
||||
"""App with multiple unused high privilege permissions: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Multiple Unused High"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Directory.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="External",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 3 unused privileged permission(s): Mail.ReadWrite.All, Directory.ReadWrite.All, User.ReadWrite.All."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_more_than_five_unused_high_privilege_permissions(self):
|
||||
"""App with more than 5 unused high privilege permissions: expected FAIL with truncated list."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Many Unused High"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Directory.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Group.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Sites.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="RoleManagement.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Application.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="External",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 7 unused privileged permission(s): Mail.ReadWrite.All, Directory.ReadWrite.All, User.ReadWrite.All, Group.ReadWrite.All, Sites.ReadWrite.All (and 2 more)."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_unused_with_not_in_use_status(self):
|
||||
"""App with unused permission using 'not_in_use' status variant: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App NotInUse Variant"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="not_in_use",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time=None,
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 1 unused privileged permission(s): Mail.ReadWrite.All."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_multiple_apps_mixed_results(self):
|
||||
"""Multiple apps with mixed results: one PASS and one FAIL."""
|
||||
app_id_pass = str(uuid4())
|
||||
app_name_pass = "Test App Pass"
|
||||
app_id_fail = str(uuid4())
|
||||
app_name_fail = "Test App Fail"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id_pass: OAuthApp(
|
||||
id=app_id_pass,
|
||||
name=app_name_pass,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="InUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="Internal",
|
||||
),
|
||||
app_id_fail: OAuthApp(
|
||||
id=app_id_fail,
|
||||
name=app_name_fail,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Directory.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="External",
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
# Find results by app ID
|
||||
result_pass = next(r for r in result if r.resource_id == app_id_pass)
|
||||
result_fail = next(r for r in result if r.resource_id == app_id_fail)
|
||||
|
||||
assert result_pass.status == "PASS"
|
||||
assert (
|
||||
result_pass.status_extended
|
||||
== f"App registration {app_name_pass} has no unused privileged permissions."
|
||||
)
|
||||
assert result_pass.resource_name == app_name_pass
|
||||
|
||||
assert result_fail.status == "FAIL"
|
||||
assert (
|
||||
result_fail.status_extended
|
||||
== f"App registration {app_name_fail} has 1 unused privileged permission(s): Directory.ReadWrite.All."
|
||||
)
|
||||
assert result_fail.resource_name == app_name_fail
|
||||
|
||||
def test_app_mixed_privilege_levels_unused(self):
|
||||
"""App with mixed privilege levels (High and Low) unused: only High triggers FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Mixed Privileges"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.Read",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Low",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Files.Read",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Delegated",
|
||||
privilege_level="Medium",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
# Only the High privilege permission should be reported
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 1 unused privileged permission(s): Mail.ReadWrite.All."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_high_privilege_in_use_and_unused(self):
|
||||
"""App with some high privilege permissions in use and some unused: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Partial Usage"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name=app_name,
|
||||
status="Enabled",
|
||||
privilege_level="High",
|
||||
permissions=[
|
||||
OAuthAppPermission(
|
||||
name="Mail.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="InUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="Directory.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="NotInUse",
|
||||
),
|
||||
OAuthAppPermission(
|
||||
name="User.ReadWrite.All",
|
||||
target_app_id="00000003-0000-0000-c000-000000000000",
|
||||
target_app_name="Microsoft Graph",
|
||||
permission_type="Application",
|
||||
privilege_level="High",
|
||||
usage_status="InUse",
|
||||
),
|
||||
],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=True,
|
||||
last_used_time="2024-01-15T10:30:00Z",
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} has 1 unused privileged permission(s): Directory.ReadWrite.All."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_without_name_uses_id(self):
|
||||
"""App without a name should use app_id as resource_name."""
|
||||
app_id = str(uuid4())
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_no_unused_privileged_permissions.entra_app_registration_no_unused_privileged_permissions import (
|
||||
entra_app_registration_no_unused_privileged_permissions,
|
||||
)
|
||||
|
||||
entra_client.oauth_apps = {
|
||||
app_id: OAuthApp(
|
||||
id=app_id,
|
||||
name="",
|
||||
status="Enabled",
|
||||
privilege_level="Low",
|
||||
permissions=[],
|
||||
service_principal_id=str(uuid4()),
|
||||
is_admin_consented=False,
|
||||
last_used_time=None,
|
||||
app_origin="Internal",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_no_unused_privileged_permissions()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_name == ""
|
||||
assert result[0].resource_id == app_id
|
||||
Reference in New Issue
Block a user